From 3ba22b99f3997e81cfd228aad0ded8b5444cb7b8 Mon Sep 17 00:00:00 2001 From: matevip Date: Fri, 15 May 2026 10:18:10 +0800 Subject: [PATCH] feat(workspace,system): add @RequireGlobalAdmin and gate admin-only controllers --- .../acp/controller/AcpEndpointController.java | 8 ++++++ .../mate/activity/ActivityFeedController.java | 2 ++ .../agent/delegation/SubagentController.java | 4 +++ .../agent/runtime/AgentRuntimeController.java | 6 +++++ .../mate/auth/controller/AuthController.java | 3 +++ .../java/vip/mate/config/SecurityConfig.java | 5 +++- .../config/WorkspaceAccessInterceptor.java | 25 +++++++++++++------ .../controller/SystemSettingController.java | 8 ++++++ .../featureflag/FeatureFlagController.java | 3 +++ .../guard/controller/SecurityController.java | 15 +++++++++++ .../mcp/controller/McpServerController.java | 10 ++++++++ .../mate/trigger/api/TriggerController.java | 6 +++++ .../wiki/controller/WikiAdminController.java | 3 +++ .../mate/workflow/api/WorkflowController.java | 15 +++++++++++ .../api/WorkflowResumeController.java | 2 ++ .../core/annotation/RequireGlobalAdmin.java | 25 +++++++++++++++++++ 16 files changed, 132 insertions(+), 8 deletions(-) create mode 100644 mateclaw-server/src/main/java/vip/mate/workspace/core/annotation/RequireGlobalAdmin.java diff --git a/mateclaw-server/src/main/java/vip/mate/acp/controller/AcpEndpointController.java b/mateclaw-server/src/main/java/vip/mate/acp/controller/AcpEndpointController.java index 5127c5b4..37d5bb86 100644 --- a/mateclaw-server/src/main/java/vip/mate/acp/controller/AcpEndpointController.java +++ b/mateclaw-server/src/main/java/vip/mate/acp/controller/AcpEndpointController.java @@ -11,6 +11,7 @@ import vip.mate.common.result.R; import java.util.List; import java.util.Map; +import vip.mate.workspace.core.annotation.RequireWorkspaceRole; /** * RFC-090 Phase 7 — REST surface for managing ACP endpoints. @@ -29,24 +30,28 @@ public class AcpEndpointController { @Operation(summary = "List ACP endpoints") @GetMapping + @RequireWorkspaceRole("admin") public R> list() { return R.ok(service.list()); } @Operation(summary = "Get ACP endpoint by id") @GetMapping("/{id}") + @RequireWorkspaceRole("admin") public R get(@PathVariable Long id) { return R.ok(service.get(id)); } @Operation(summary = "Create a custom ACP endpoint") @PostMapping + @RequireWorkspaceRole("admin") public R create(@RequestBody AcpEndpointEntity body) { return R.ok(service.create(body)); } @Operation(summary = "Update an ACP endpoint") @PutMapping("/{id}") + @RequireWorkspaceRole("admin") public R update(@PathVariable Long id, @RequestBody AcpEndpointEntity body) { return R.ok(service.update(id, body)); @@ -54,6 +59,7 @@ public class AcpEndpointController { @Operation(summary = "Delete an ACP endpoint (builtins are protected)") @DeleteMapping("/{id}") + @RequireWorkspaceRole("admin") public R delete(@PathVariable Long id) { service.delete(id); return R.ok(); @@ -61,6 +67,7 @@ public class AcpEndpointController { @Operation(summary = "Enable / disable an ACP endpoint") @PutMapping("/{id}/toggle") + @RequireWorkspaceRole("admin") public R toggle(@PathVariable Long id, @RequestParam boolean enabled) { return R.ok(service.toggle(id, enabled)); @@ -72,6 +79,7 @@ public class AcpEndpointController { */ @Operation(summary = "Test ACP endpoint connection (initialize handshake)") @PostMapping("/{id}/test") + @RequireWorkspaceRole("admin") public R> test(@PathVariable Long id) { AcpEndpointEntity endpoint = service.get(id); return R.ok(tester.testEndpoint(endpoint)); diff --git a/mateclaw-server/src/main/java/vip/mate/activity/ActivityFeedController.java b/mateclaw-server/src/main/java/vip/mate/activity/ActivityFeedController.java index 59c0f5ff..ea361b97 100644 --- a/mateclaw-server/src/main/java/vip/mate/activity/ActivityFeedController.java +++ b/mateclaw-server/src/main/java/vip/mate/activity/ActivityFeedController.java @@ -20,6 +20,7 @@ import java.util.Comparator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import vip.mate.workspace.core.annotation.RequireWorkspaceRole; /** * RFC-090 §4.5 / §7 — unified Activity feed. @@ -74,6 +75,7 @@ public class ActivityFeedController { */ @Operation(summary = "Unified activity feed (audit + approval + tool calls)") @GetMapping("/feed") + @RequireWorkspaceRole("admin") public R> feed( @RequestParam(required = false) Long workspaceId, @RequestParam(required = false) String source, diff --git a/mateclaw-server/src/main/java/vip/mate/agent/delegation/SubagentController.java b/mateclaw-server/src/main/java/vip/mate/agent/delegation/SubagentController.java index 2dce4adb..5b2e65e5 100644 --- a/mateclaw-server/src/main/java/vip/mate/agent/delegation/SubagentController.java +++ b/mateclaw-server/src/main/java/vip/mate/agent/delegation/SubagentController.java @@ -17,6 +17,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Optional; +import vip.mate.workspace.core.annotation.RequireGlobalAdmin; /** * REST surface for managing live sub-agents: @@ -86,6 +87,7 @@ public class SubagentController { */ @Operation(summary = "Interrupt a running sub-agent") @PostMapping("/{subagentId}/interrupt") + @RequireGlobalAdmin public R> interrupt(@PathVariable String subagentId, Authentication auth) { SubagentRegistry.SubagentRecord rec = requireOwnership(subagentId, auth); boolean ok = registry.interrupt(subagentId); @@ -106,6 +108,7 @@ public class SubagentController { */ @Operation(summary = "Set sub-agent spawn-pause for a conversation") @PostMapping("/spawn-pause") + @RequireGlobalAdmin public R> setPaused(@RequestBody Map body, Authentication auth) { Object parentObj = body == null ? null : body.get("parentConversationId"); String parent = parentObj == null ? null : parentObj.toString(); @@ -134,6 +137,7 @@ public class SubagentController { */ @Operation(summary = "List active sub-agents under a parent conversation") @GetMapping("/active") + @RequireGlobalAdmin public R> listActive(@RequestParam(required = false) String parentConversationId, Authentication auth) { if (parentConversationId == null || parentConversationId.isBlank()) { diff --git a/mateclaw-server/src/main/java/vip/mate/agent/runtime/AgentRuntimeController.java b/mateclaw-server/src/main/java/vip/mate/agent/runtime/AgentRuntimeController.java index 291d60cb..f6248fc6 100644 --- a/mateclaw-server/src/main/java/vip/mate/agent/runtime/AgentRuntimeController.java +++ b/mateclaw-server/src/main/java/vip/mate/agent/runtime/AgentRuntimeController.java @@ -18,6 +18,7 @@ import vip.mate.workspace.conversation.ConversationService; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import vip.mate.workspace.core.annotation.RequireGlobalAdmin; /** * Admin-only Backstage surface: the global view of every in-flight agent @@ -42,6 +43,7 @@ public class AgentRuntimeController { @Operation(summary = "Snapshot of every in-flight agent turn") @GetMapping("/snapshot") + @RequireGlobalAdmin public R snapshot(Authentication auth) { requireAdmin(auth); return R.ok(aggregator.snapshot()); @@ -49,6 +51,7 @@ public class AgentRuntimeController { @Operation(summary = "Friendly stop — request the run to wind down at its next checkpoint") @PostMapping("/runs/{conversationId}/stop") + @RequireGlobalAdmin public R> stopFriendly(@PathVariable String conversationId, Authentication auth) { requireAdmin(auth); @@ -59,6 +62,7 @@ public class AgentRuntimeController { @Operation(summary = "Force recycle — dispose flux + drop RunState; use after friendly stop ignored") @PostMapping("/runs/{conversationId}/recycle") + @RequireGlobalAdmin public R> recycle(@PathVariable String conversationId, Authentication auth) { requireAdmin(auth); @@ -72,6 +76,7 @@ public class AgentRuntimeController { @Operation(summary = "Interrupt one sub-agent (admin override of ownership check)") @PostMapping("/subagents/{subagentId}/interrupt") + @RequireGlobalAdmin public R> interruptSubagent(@PathVariable String subagentId, Authentication auth) { requireAdmin(auth); @@ -87,6 +92,7 @@ public class AgentRuntimeController { */ @Operation(summary = "Recycle every run currently flagged as stuck") @PostMapping("/sweep") + @RequireGlobalAdmin public R> sweep(Authentication auth) { requireAdmin(auth); AgentRuntimeAggregator.RuntimeSnapshot snap = aggregator.snapshot(); diff --git a/mateclaw-server/src/main/java/vip/mate/auth/controller/AuthController.java b/mateclaw-server/src/main/java/vip/mate/auth/controller/AuthController.java index b82521fa..8e9052b4 100644 --- a/mateclaw-server/src/main/java/vip/mate/auth/controller/AuthController.java +++ b/mateclaw-server/src/main/java/vip/mate/auth/controller/AuthController.java @@ -11,6 +11,7 @@ import vip.mate.auth.model.UserEntity; import vip.mate.auth.service.AuthService; import vip.mate.common.result.R; import vip.mate.exception.MateClawException; +import vip.mate.workspace.core.annotation.RequireGlobalAdmin; import java.util.List; @@ -35,12 +36,14 @@ public class AuthController { @Operation(summary = "获取用户列表") @GetMapping("/users") + @RequireGlobalAdmin public R> listUsers() { return R.ok(authService.listUsers()); } @Operation(summary = "创建用户") @PostMapping("/users") + @RequireGlobalAdmin public R createUser(@RequestBody UserEntity user) { return R.ok(authService.createUser(user)); } diff --git a/mateclaw-server/src/main/java/vip/mate/config/SecurityConfig.java b/mateclaw-server/src/main/java/vip/mate/config/SecurityConfig.java index ceba7991..f81c4995 100644 --- a/mateclaw-server/src/main/java/vip/mate/config/SecurityConfig.java +++ b/mateclaw-server/src/main/java/vip/mate/config/SecurityConfig.java @@ -8,6 +8,7 @@ import org.springframework.security.config.annotation.web.configuration.EnableWe import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.http.HttpMethod; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @@ -48,10 +49,12 @@ public class SecurityConfig { ) .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth -> auth + // GET /settings/language stays anonymous (first-paint i18n). PUT + // requires login + admin (see @RequireGlobalAdmin on the controller). + .requestMatchers(HttpMethod.GET, "/api/v1/settings/language").permitAll() // 公开 API 接口 .requestMatchers( "/api/v1/auth/login", - "/api/v1/settings/language", "/api/v1/agents/*/chat/stream", "/api/v1/chat/stream", "/api/v1/chat/*/stop", diff --git a/mateclaw-server/src/main/java/vip/mate/config/WorkspaceAccessInterceptor.java b/mateclaw-server/src/main/java/vip/mate/config/WorkspaceAccessInterceptor.java index 21881f5b..9f3b7fea 100644 --- a/mateclaw-server/src/main/java/vip/mate/config/WorkspaceAccessInterceptor.java +++ b/mateclaw-server/src/main/java/vip/mate/config/WorkspaceAccessInterceptor.java @@ -11,6 +11,7 @@ import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.HandlerInterceptor; import vip.mate.auth.model.UserEntity; import vip.mate.auth.service.AuthService; +import vip.mate.workspace.core.annotation.RequireGlobalAdmin; import vip.mate.workspace.core.annotation.RequireWorkspaceRole; import vip.mate.workspace.core.service.WorkspaceService; @@ -44,9 +45,10 @@ public class WorkspaceAccessInterceptor implements HandlerInterceptor { return true; } - // 检查方法是否标注了 @RequireWorkspaceRole + // 检查注解:@RequireGlobalAdmin 与 @RequireWorkspaceRole 二选一 + RequireGlobalAdmin globalAdmin = handlerMethod.getMethodAnnotation(RequireGlobalAdmin.class); RequireWorkspaceRole annotation = handlerMethod.getMethodAnnotation(RequireWorkspaceRole.class); - if (annotation == null) { + if (globalAdmin == null && annotation == null) { return true; } @@ -64,15 +66,24 @@ public class WorkspaceAccessInterceptor implements HandlerInterceptor { return false; } - // 系统管理员跳过 workspace 权限检查(全局 admin 角色) - if ("admin".equalsIgnoreCase(user.getRole())) { + boolean isGlobalAdmin = "admin".equalsIgnoreCase(user.getRole()); + + // 全局 admin 注解:必须是 mate_user.role=admin,与工作区无关 + if (globalAdmin != null && !isGlobalAdmin) { + log.warn("Global admin access denied: user={}, path={}", username, request.getRequestURI()); + sendForbidden(response, "Global administrator role required"); + return false; + } + if (globalAdmin != null) { return true; } - // 解析 workspace ID - long workspaceId = resolveWorkspaceId(request); + // @RequireWorkspaceRole 分支:全局 admin 跳过 + if (isGlobalAdmin) { + return true; + } - // 检查成员资格 + 角色 + long workspaceId = resolveWorkspaceId(request); String minRole = annotation.value(); if (!workspaceService.hasPermissionCached(workspaceId, user.getId(), minRole)) { log.warn("Workspace access denied: user={}, workspaceId={}, requiredRole={}", username, workspaceId, minRole); diff --git a/mateclaw-server/src/main/java/vip/mate/system/controller/SystemSettingController.java b/mateclaw-server/src/main/java/vip/mate/system/controller/SystemSettingController.java index 741f5b8d..35a5a452 100644 --- a/mateclaw-server/src/main/java/vip/mate/system/controller/SystemSettingController.java +++ b/mateclaw-server/src/main/java/vip/mate/system/controller/SystemSettingController.java @@ -8,6 +8,8 @@ import org.springframework.web.bind.annotation.*; import vip.mate.common.result.R; import vip.mate.system.model.SystemSettingsDTO; import vip.mate.system.service.SystemSettingService; +import vip.mate.workspace.core.annotation.RequireGlobalAdmin; +import vip.mate.workspace.core.annotation.RequireWorkspaceRole; @Tag(name = "系统设置") @RestController @@ -19,12 +21,14 @@ public class SystemSettingController { @Operation(summary = "获取系统设置") @GetMapping + @RequireWorkspaceRole("admin") public R getSettings() { return R.ok(systemSettingService.getSettings()); } @Operation(summary = "保存系统设置") @PutMapping + @RequireWorkspaceRole("admin") public R saveSettings(@RequestBody SystemSettingsDTO dto) { return R.ok(systemSettingService.saveSettings(dto)); } @@ -32,12 +36,15 @@ public class SystemSettingController { @Operation(summary = "获取当前语言") @GetMapping("/language") public R getLanguage() { + // Stays anonymous via SecurityConfig (first-paint i18n). return R.ok(systemSettingService.getLanguage()); } @Operation(summary = "更新当前语言") @PutMapping("/language") + @RequireGlobalAdmin public R saveLanguage(@RequestBody LanguageRequest request) { + // System-wide setting; only the global admin may change it. return R.ok(systemSettingService.saveLanguage(request.getLanguage())); } @@ -53,6 +60,7 @@ public class SystemSettingController { */ @Operation(summary = "更新多模态 sidecar 配置") @PutMapping("/sidecar") + @RequireWorkspaceRole("admin") public R saveSidecar(@RequestBody SidecarRequest request) { return R.ok(systemSettingService.updateSidecarSettings( request.getDefaultVisionModelId(), diff --git a/mateclaw-server/src/main/java/vip/mate/system/featureflag/FeatureFlagController.java b/mateclaw-server/src/main/java/vip/mate/system/featureflag/FeatureFlagController.java index 5f634c47..56882f8e 100644 --- a/mateclaw-server/src/main/java/vip/mate/system/featureflag/FeatureFlagController.java +++ b/mateclaw-server/src/main/java/vip/mate/system/featureflag/FeatureFlagController.java @@ -16,6 +16,7 @@ import vip.mate.common.result.R; import vip.mate.system.featureflag.repository.FeatureFlagMapper; import java.util.List; +import vip.mate.workspace.core.annotation.RequireWorkspaceRole; /** * Admin endpoints for runtime feature-flag toggling. @@ -37,6 +38,7 @@ public class FeatureFlagController { /** Lists every flag currently registered, including disabled and whitelisted ones. */ @GetMapping + @RequireWorkspaceRole("admin") public R> list() { return R.ok(mapper.selectList(null)); } @@ -46,6 +48,7 @@ public class FeatureFlagController { * body are touched; unspecified fields preserve their current values. */ @PutMapping("/{flagKey}") + @RequireWorkspaceRole("admin") public R update(@PathVariable @NotBlank String flagKey, @RequestBody UpdateRequest req) { FeatureFlagEntity flag = mapper.selectOne( diff --git a/mateclaw-server/src/main/java/vip/mate/tool/guard/controller/SecurityController.java b/mateclaw-server/src/main/java/vip/mate/tool/guard/controller/SecurityController.java index 26df385f..32b0102a 100644 --- a/mateclaw-server/src/main/java/vip/mate/tool/guard/controller/SecurityController.java +++ b/mateclaw-server/src/main/java/vip/mate/tool/guard/controller/SecurityController.java @@ -18,6 +18,7 @@ import vip.mate.tool.guard.service.ToolGuardRuleService; import java.util.HashMap; import java.util.Map; +import vip.mate.workspace.core.annotation.RequireWorkspaceRole; /** * 安全管理接口 @@ -42,18 +43,21 @@ public class SecurityController { @Operation(summary = "获取 Guard 配置") @GetMapping("/guard/config") + @RequireWorkspaceRole("admin") public R getGuardConfig() { return R.ok(configService.getConfig()); } @Operation(summary = "更新 Guard 配置") @PutMapping("/guard/config") + @RequireWorkspaceRole("admin") public R updateGuardConfig(@RequestBody ToolGuardConfigEntity config) { return R.ok(configService.updateConfig(config)); } @Operation(summary = "获取 File Guard 配置") @GetMapping("/guard/config/file-guard") + @RequireWorkspaceRole("admin") public R> getFileGuardConfig() { ToolGuardConfigEntity config = configService.getConfig(); Map result = new HashMap<>(); @@ -64,6 +68,7 @@ public class SecurityController { @Operation(summary = "更新 File Guard 配置") @PutMapping("/guard/config/file-guard") + @RequireWorkspaceRole("admin") public R updateFileGuardConfig(@RequestBody ToolGuardConfigEntity config) { ToolGuardConfigEntity update = new ToolGuardConfigEntity(); update.setFileGuardEnabled(config.getFileGuardEnabled()); @@ -75,6 +80,7 @@ public class SecurityController { @Operation(summary = "规则列表") @GetMapping("/guard/rules") + @RequireWorkspaceRole("admin") public R> listRules( @RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "50") int size, @@ -87,6 +93,7 @@ public class SecurityController { @Operation(summary = "内置规则列表") @GetMapping("/guard/rules/builtin") + @RequireWorkspaceRole("admin") public R> listBuiltinRules( @RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "50") int size) { @@ -95,6 +102,7 @@ public class SecurityController { @Operation(summary = "新增自定义规则") @PostMapping("/guard/rules") + @RequireWorkspaceRole("admin") public R createRule(@RequestBody ToolGuardRuleEntity rule) { try { return R.ok(ruleService.createRule(rule)); @@ -109,6 +117,7 @@ public class SecurityController { @Operation(summary = "更新规则") @PutMapping("/guard/rules/{ruleId}") + @RequireWorkspaceRole("admin") public R updateRule( @PathVariable String ruleId, @RequestBody ToolGuardRuleEntity rule) { @@ -121,6 +130,7 @@ public class SecurityController { @Operation(summary = "启用/禁用规则") @PutMapping("/guard/rules/{ruleId}/toggle") + @RequireWorkspaceRole("admin") public R toggleRule( @PathVariable String ruleId, @RequestParam boolean enabled) { @@ -134,6 +144,7 @@ public class SecurityController { @Operation(summary = "删除自定义规则") @DeleteMapping("/guard/rules/{ruleId}") + @RequireWorkspaceRole("admin") public R deleteRule(@PathVariable String ruleId) { try { ruleService.deleteRule(ruleId); @@ -145,6 +156,7 @@ public class SecurityController { @Operation(summary = "按主键 ID 删除自定义规则(兜底,rule_id 异常时使用)") @DeleteMapping("/guard/rules/by-id/{id}") + @RequireWorkspaceRole("admin") public R deleteRuleByPk(@PathVariable Long id) { try { ruleService.deleteRuleByPk(id); @@ -158,6 +170,7 @@ public class SecurityController { @Operation(summary = "审计日志") @GetMapping("/audit/logs") + @RequireWorkspaceRole("admin") public R> listAuditLogs( @RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "20") int size, @@ -169,6 +182,7 @@ public class SecurityController { @Operation(summary = "审计统计") @GetMapping("/audit/stats") + @RequireWorkspaceRole("admin") public R> getAuditStats() { return R.ok(auditService.getStats()); } @@ -177,6 +191,7 @@ public class SecurityController { @Operation(summary = "审批记录(管理视角)") @GetMapping("/approvals") + @RequireWorkspaceRole("admin") public R listApprovals( @RequestParam(required = false) String conversationId, @RequestParam(required = false, defaultValue = "0") int limit) { diff --git a/mateclaw-server/src/main/java/vip/mate/tool/mcp/controller/McpServerController.java b/mateclaw-server/src/main/java/vip/mate/tool/mcp/controller/McpServerController.java index b99693f8..45a890f6 100644 --- a/mateclaw-server/src/main/java/vip/mate/tool/mcp/controller/McpServerController.java +++ b/mateclaw-server/src/main/java/vip/mate/tool/mcp/controller/McpServerController.java @@ -11,6 +11,7 @@ import vip.mate.tool.mcp.runtime.McpClientManager.ConnectionResult; import vip.mate.tool.mcp.service.McpServerService; import java.util.List; +import vip.mate.workspace.core.annotation.RequireWorkspaceRole; /** * MCP Server 管理接口 @@ -33,18 +34,21 @@ public class McpServerController { @Operation(summary = "获取 MCP Server 列表") @GetMapping + @RequireWorkspaceRole("admin") public R> list() { return R.ok(mcpServerService.sanitizeList(mcpServerService.listAll())); } @Operation(summary = "获取 MCP Server 详情") @GetMapping("/{id}") + @RequireWorkspaceRole("admin") public R get(@PathVariable Long id) { return R.ok(mcpServerService.sanitize(mcpServerService.getById(id))); } @Operation(summary = "创建 MCP Server") @PostMapping + @RequireWorkspaceRole("admin") public R create(@RequestBody McpServerEntity entity) { McpServerEntity created = mcpServerService.create(entity); return R.ok(mcpServerService.sanitize(created)); @@ -52,6 +56,7 @@ public class McpServerController { @Operation(summary = "更新 MCP Server") @PutMapping("/{id}") + @RequireWorkspaceRole("admin") public R update(@PathVariable Long id, @RequestBody McpServerEntity entity) { McpServerEntity updated = mcpServerService.update(id, entity); return R.ok(mcpServerService.sanitize(updated)); @@ -59,6 +64,7 @@ public class McpServerController { @Operation(summary = "删除 MCP Server") @DeleteMapping("/{id}") + @RequireWorkspaceRole("admin") public R delete(@PathVariable Long id) { mcpServerService.delete(id); return R.ok(); @@ -66,6 +72,7 @@ public class McpServerController { @Operation(summary = "启用/禁用 MCP Server") @PutMapping("/{id}/toggle") + @RequireWorkspaceRole("admin") public R toggle(@PathVariable Long id, @RequestParam boolean enabled) { McpServerEntity toggled = mcpServerService.toggle(id, enabled); return R.ok(mcpServerService.sanitize(toggled)); @@ -73,6 +80,7 @@ public class McpServerController { @Operation(summary = "测试 MCP Server 连接") @PostMapping("/{id}/test") + @RequireWorkspaceRole("admin") public R test(@PathVariable Long id) { ConnectionResult result = mcpServerService.testConnectionById(id); return R.ok(result); @@ -100,12 +108,14 @@ public class McpServerController { */ @Operation(summary = "列出 MCP Server 已发现的工具") @GetMapping("/{id}/tools") + @RequireWorkspaceRole("admin") public R> listTools(@PathVariable Long id) { return R.ok(mcpServerService.listToolsByServer(id)); } @Operation(summary = "刷新所有 MCP Server 连接") @PostMapping("/refresh") + @RequireWorkspaceRole("admin") public R refresh() { mcpServerService.refreshAll(); return R.ok(); diff --git a/mateclaw-server/src/main/java/vip/mate/trigger/api/TriggerController.java b/mateclaw-server/src/main/java/vip/mate/trigger/api/TriggerController.java index 89f78112..4c3c61ba 100644 --- a/mateclaw-server/src/main/java/vip/mate/trigger/api/TriggerController.java +++ b/mateclaw-server/src/main/java/vip/mate/trigger/api/TriggerController.java @@ -9,6 +9,7 @@ import vip.mate.trigger.ingest.TriggerEventEnvelope; import vip.mate.trigger.ingest.TriggerEventIngestService; import vip.mate.trigger.model.TriggerEntity; import vip.mate.trigger.service.TriggerService; +import vip.mate.workspace.core.annotation.RequireWorkspaceRole; import java.util.List; import java.util.Map; @@ -30,12 +31,14 @@ public class TriggerController { @Operation(summary = "List triggers in the caller's workspace.") @GetMapping + @RequireWorkspaceRole("admin") public R> list(@RequestHeader("X-Workspace-Id") long workspaceId) { return R.ok(triggerService.listByWorkspace(workspaceId)); } @Operation(summary = "Get a trigger by id, scoped to the caller's workspace.") @GetMapping("/{id}") + @RequireWorkspaceRole("admin") public R get(@PathVariable long id, @RequestHeader("X-Workspace-Id") long workspaceId) { TriggerEntity row = triggerService.get(id, workspaceId); @@ -45,6 +48,7 @@ public class TriggerController { @Operation(summary = "Create a trigger; if enabled, registers it with the scheduler.") @PostMapping + @RequireWorkspaceRole("admin") public R create(@RequestBody TriggerEntity trigger, @RequestHeader("X-Workspace-Id") long workspaceId) { // The controller forces workspace from the trusted header — the @@ -59,6 +63,7 @@ public class TriggerController { @Operation(summary = "Update a trigger; pattern_version bumps when the cron expression changes.") @PutMapping("/{id}") + @RequireWorkspaceRole("admin") public R update(@PathVariable long id, @RequestBody TriggerEntity trigger, @RequestHeader("X-Workspace-Id") long workspaceId) { @@ -71,6 +76,7 @@ public class TriggerController { @Operation(summary = "Delete a trigger and unregister its schedule.") @DeleteMapping("/{id}") + @RequireWorkspaceRole("admin") public R delete(@PathVariable long id, @RequestHeader("X-Workspace-Id") long workspaceId) { triggerService.delete(id, workspaceId); diff --git a/mateclaw-server/src/main/java/vip/mate/wiki/controller/WikiAdminController.java b/mateclaw-server/src/main/java/vip/mate/wiki/controller/WikiAdminController.java index 3ff9cde4..d9c43d50 100644 --- a/mateclaw-server/src/main/java/vip/mate/wiki/controller/WikiAdminController.java +++ b/mateclaw-server/src/main/java/vip/mate/wiki/controller/WikiAdminController.java @@ -16,6 +16,7 @@ import vip.mate.wiki.service.WikiScaffoldService; import java.util.HashMap; import java.util.Map; +import vip.mate.workspace.core.annotation.RequireWorkspaceRole; /** * RFC-051 follow-up: small set of operator-facing endpoints for things the @@ -44,6 +45,7 @@ public class WikiAdminController { @Operation(summary = "Ensure overview/log scaffold + rebuild overview stats now", description = "Idempotent. Use after manual data imports or when stats look stale.") @PostMapping("/kb/{kbId}/rebuild-overview") + @RequireWorkspaceRole("admin") public ResponseEntity> rebuildOverview(@PathVariable Long kbId) { Map body = new HashMap<>(); scaffoldService.ensureScaffold(kbId); @@ -62,6 +64,7 @@ public class WikiAdminController { description = "Picks up to BATCH_SIZE chunks with token_count IS NULL and fills them. " + "Returns the pending count after the batch so callers can poll.") @PostMapping("/backfill-tokens") + @RequireWorkspaceRole("admin") public ResponseEntity> backfillTokens() { Map body = new HashMap<>(); if (backfillJob == null) { diff --git a/mateclaw-server/src/main/java/vip/mate/workflow/api/WorkflowController.java b/mateclaw-server/src/main/java/vip/mate/workflow/api/WorkflowController.java index 177b42b8..a445d171 100644 --- a/mateclaw-server/src/main/java/vip/mate/workflow/api/WorkflowController.java +++ b/mateclaw-server/src/main/java/vip/mate/workflow/api/WorkflowController.java @@ -22,6 +22,7 @@ import vip.mate.workflow.repository.WorkflowRunStepMapper; import vip.mate.workflow.service.WorkflowService; import java.util.List; +import vip.mate.workspace.core.annotation.RequireWorkspaceRole; /** * REST surface for workflow CRUD + draft / publish / run inspection. @@ -51,12 +52,14 @@ public class WorkflowController { @Operation(summary = "List workflows in the workspace") @GetMapping + @RequireWorkspaceRole("admin") public R> list(@RequestHeader("X-Workspace-Id") long workspaceId) { return R.ok(workflowService.listByWorkspace(workspaceId)); } @Operation(summary = "Get a workflow by id (includes inline draft).") @GetMapping("/{id}") + @RequireWorkspaceRole("admin") public R get(@PathVariable long id, @RequestHeader("X-Workspace-Id") long workspaceId) { WorkflowEntity row = workflowService.get(id, workspaceId); @@ -66,6 +69,7 @@ public class WorkflowController { @Operation(summary = "Create a workflow row (draft starts empty).") @PostMapping + @RequireWorkspaceRole("admin") public R create(@RequestBody WorkflowEntity workflow, @RequestHeader("X-Workspace-Id") long workspaceId) { // Force the workspace from the trusted header — the request body @@ -77,6 +81,7 @@ public class WorkflowController { @Operation(summary = "Update workflow metadata (name / description / enabled).") @PutMapping("/{id}") + @RequireWorkspaceRole("admin") public R update(@PathVariable long id, @RequestBody WorkflowMetadataRequest body, @RequestHeader("X-Workspace-Id") long workspaceId) { @@ -86,6 +91,7 @@ public class WorkflowController { @Operation(summary = "Save the inline draft graph_json without compiling.") @PutMapping("/{id}/draft") + @RequireWorkspaceRole("admin") public R saveDraft(@PathVariable long id, @RequestBody WorkflowDraftRequest body, @RequestParam(value = "userId", required = false) Long userId, @@ -95,6 +101,7 @@ public class WorkflowController { @Operation(summary = "Compile the draft and surface diagnostics without persisting a revision.") @PostMapping("/{id}/compile") + @RequireWorkspaceRole("admin") public ResponseEntity compileDraft(@PathVariable long id, @RequestHeader("X-Workspace-Id") long workspaceId) { WorkflowEntity row = workflowService.get(id, workspaceId); @@ -131,6 +138,7 @@ public class WorkflowController { @Operation(summary = "Compile the draft and persist a new revision pointed at by latest_revision_id.") @PostMapping("/{id}/publish") + @RequireWorkspaceRole("admin") public ResponseEntity publish(@PathVariable long id, @RequestBody(required = false) WorkflowPublishRequest body, @RequestParam(value = "userId", required = false) Long userId, @@ -154,6 +162,7 @@ public class WorkflowController { @Operation(summary = "Soft-delete a workflow row.") @DeleteMapping("/{id}") + @RequireWorkspaceRole("admin") public R delete(@PathVariable long id, @RequestHeader("X-Workspace-Id") long workspaceId) { workflowService.delete(id, workspaceId); @@ -162,6 +171,7 @@ public class WorkflowController { @Operation(summary = "List the most recent runs for a workflow.") @GetMapping("/{id}/runs") + @RequireWorkspaceRole("admin") public R> listRuns(@PathVariable long id, @RequestParam(value = "limit", defaultValue = "50") int limit, @RequestHeader("X-Workspace-Id") long workspaceId) { @@ -182,6 +192,7 @@ public class WorkflowController { @Operation(summary = "List paused runs across the workspace so operators can resume them.") @GetMapping("/runs/paused") + @RequireWorkspaceRole("admin") public R> listPausedRuns(@RequestParam(value = "limit", defaultValue = "50") int limit, @RequestHeader("X-Workspace-Id") long workspaceId) { // Without this listing surface, an await_approval pause is only @@ -212,6 +223,7 @@ public class WorkflowController { @Operation(summary = "Inspect a single run with its step rows for replay / debugging.") @GetMapping("/runs/{runId}") + @RequireWorkspaceRole("admin") public R getRun(@PathVariable long runId, @RequestHeader("X-Workspace-Id") long workspaceId) { WorkflowRunEntity run = runMapper.selectById(runId); @@ -238,6 +250,7 @@ public class WorkflowController { @Operation(summary = "Generate a workflow draft from a natural-language description.") @PostMapping("/draft/generate") + @RequireWorkspaceRole("admin") public ResponseEntity generateDraft(@RequestBody DraftGenerateRequest body, @RequestHeader("X-Workspace-Id") long workspaceId) { if (draftGenerator == null) { @@ -258,6 +271,7 @@ public class WorkflowController { @Operation(summary = "List the canonical workflow templates the generator can apply directly.") @GetMapping("/draft/templates") + @RequireWorkspaceRole("admin") public R> listDraftTemplates() { if (draftTemplates == null) return R.ok(List.of()); return R.ok(draftTemplates.all()); @@ -265,6 +279,7 @@ public class WorkflowController { @Operation(summary = "Compile arbitrary draft JSON without persisting — used by the template picker / generator preview to surface real ACL + schema diagnostics before a workflow row exists.") @PostMapping("/draft/preview-compile") + @RequireWorkspaceRole("admin") public ResponseEntity previewCompile(@RequestBody WorkflowDraftRequest body, @RequestHeader("X-Workspace-Id") long workspaceId) { if (body == null || body.draftJson() == null || body.draftJson().isBlank()) { diff --git a/mateclaw-server/src/main/java/vip/mate/workflow/api/WorkflowResumeController.java b/mateclaw-server/src/main/java/vip/mate/workflow/api/WorkflowResumeController.java index 55df9356..2222e158 100644 --- a/mateclaw-server/src/main/java/vip/mate/workflow/api/WorkflowResumeController.java +++ b/mateclaw-server/src/main/java/vip/mate/workflow/api/WorkflowResumeController.java @@ -21,6 +21,7 @@ import vip.mate.workflow.runtime.WorkflowResumer; import vip.mate.workflow.service.WorkflowService; import java.nio.charset.StandardCharsets; +import vip.mate.workspace.core.annotation.RequireWorkspaceRole; /** * HTTP surface for resuming an {@code await_approval} pause. @@ -54,6 +55,7 @@ public class WorkflowResumeController { @Operation(summary = "Resume a paused workflow run with the given outcome.") @PostMapping("/{runId}/resume") + @RequireWorkspaceRole("admin") public ResponseEntity resume(@PathVariable long runId, @RequestBody ResumeRequest body, @RequestHeader("X-Workspace-Id") long workspaceId) { diff --git a/mateclaw-server/src/main/java/vip/mate/workspace/core/annotation/RequireGlobalAdmin.java b/mateclaw-server/src/main/java/vip/mate/workspace/core/annotation/RequireGlobalAdmin.java new file mode 100644 index 00000000..d0510083 --- /dev/null +++ b/mateclaw-server/src/main/java/vip/mate/workspace/core/annotation/RequireGlobalAdmin.java @@ -0,0 +1,25 @@ +package vip.mate.workspace.core.annotation; + +import java.lang.annotation.*; + +/** + * Methods annotated with this require {@code mate_user.role='admin'} (system-wide + * administrator). Unlike {@link RequireWorkspaceRole}, this is not a per-workspace + * check — the user must be a global admin regardless of workspace membership. + *

+ * Used by: + *

    + *
  • {@code AuthController.listUsers / createUser} — user management
  • + *
  • {@code AgentRuntimeController.*} — runtime ops under {@code /api/v1/admin}
  • + *
  • {@code SubagentController.*} — runtime subagent operations
  • + *
  • workspace creation (only global admins may create new workspaces)
  • + *
+ * Workspace-scoped operations should keep using {@link RequireWorkspaceRole}; a + * global admin automatically passes those by bypassing the workspace check in + * {@code WorkspaceAccessInterceptor}. + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface RequireGlobalAdmin { +}