feat(workspace,system): add @RequireGlobalAdmin and gate admin-only controllers

This commit is contained in:
matevip 2026-05-15 10:18:10 +08:00
parent bba9975749
commit 3ba22b99f3
16 changed files with 132 additions and 8 deletions

View File

@ -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<AcpEndpointEntity>> list() {
return R.ok(service.list());
}
@Operation(summary = "Get ACP endpoint by id")
@GetMapping("/{id}")
@RequireWorkspaceRole("admin")
public R<AcpEndpointEntity> get(@PathVariable Long id) {
return R.ok(service.get(id));
}
@Operation(summary = "Create a custom ACP endpoint")
@PostMapping
@RequireWorkspaceRole("admin")
public R<AcpEndpointEntity> create(@RequestBody AcpEndpointEntity body) {
return R.ok(service.create(body));
}
@Operation(summary = "Update an ACP endpoint")
@PutMapping("/{id}")
@RequireWorkspaceRole("admin")
public R<AcpEndpointEntity> 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<Void> 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<AcpEndpointEntity> 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<Map<String, Object>> test(@PathVariable Long id) {
AcpEndpointEntity endpoint = service.get(id);
return R.ok(tester.testEndpoint(endpoint));

View File

@ -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<Map<String, Object>> feed(
@RequestParam(required = false) Long workspaceId,
@RequestParam(required = false) String source,

View File

@ -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<Map<String, Object>> 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<Map<String, Object>> setPaused(@RequestBody Map<String, Object> 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<Map<String, Object>> listActive(@RequestParam(required = false) String parentConversationId,
Authentication auth) {
if (parentConversationId == null || parentConversationId.isBlank()) {

View File

@ -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<AgentRuntimeAggregator.RuntimeSnapshot> 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<Map<String, Object>> 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<Map<String, Object>> 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<Map<String, Object>> 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<Map<String, Object>> sweep(Authentication auth) {
requireAdmin(auth);
AgentRuntimeAggregator.RuntimeSnapshot snap = aggregator.snapshot();

View File

@ -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<List<UserEntity>> listUsers() {
return R.ok(authService.listUsers());
}
@Operation(summary = "创建用户")
@PostMapping("/users")
@RequireGlobalAdmin
public R<UserEntity> createUser(@RequestBody UserEntity user) {
return R.ok(authService.createUser(user));
}

View File

@ -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",

View File

@ -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);

View File

@ -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<SystemSettingsDTO> getSettings() {
return R.ok(systemSettingService.getSettings());
}
@Operation(summary = "保存系统设置")
@PutMapping
@RequireWorkspaceRole("admin")
public R<SystemSettingsDTO> saveSettings(@RequestBody SystemSettingsDTO dto) {
return R.ok(systemSettingService.saveSettings(dto));
}
@ -32,12 +36,15 @@ public class SystemSettingController {
@Operation(summary = "获取当前语言")
@GetMapping("/language")
public R<String> getLanguage() {
// Stays anonymous via SecurityConfig (first-paint i18n).
return R.ok(systemSettingService.getLanguage());
}
@Operation(summary = "更新当前语言")
@PutMapping("/language")
@RequireGlobalAdmin
public R<String> 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<SystemSettingsDTO> saveSidecar(@RequestBody SidecarRequest request) {
return R.ok(systemSettingService.updateSidecarSettings(
request.getDefaultVisionModelId(),

View File

@ -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<FeatureFlagEntity>> 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<Void> update(@PathVariable @NotBlank String flagKey,
@RequestBody UpdateRequest req) {
FeatureFlagEntity flag = mapper.selectOne(

View File

@ -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<ToolGuardConfigEntity> getGuardConfig() {
return R.ok(configService.getConfig());
}
@Operation(summary = "更新 Guard 配置")
@PutMapping("/guard/config")
@RequireWorkspaceRole("admin")
public R<ToolGuardConfigEntity> updateGuardConfig(@RequestBody ToolGuardConfigEntity config) {
return R.ok(configService.updateConfig(config));
}
@Operation(summary = "获取 File Guard 配置")
@GetMapping("/guard/config/file-guard")
@RequireWorkspaceRole("admin")
public R<Map<String, Object>> getFileGuardConfig() {
ToolGuardConfigEntity config = configService.getConfig();
Map<String, Object> result = new HashMap<>();
@ -64,6 +68,7 @@ public class SecurityController {
@Operation(summary = "更新 File Guard 配置")
@PutMapping("/guard/config/file-guard")
@RequireWorkspaceRole("admin")
public R<ToolGuardConfigEntity> 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<IPage<ToolGuardRuleEntity>> 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<IPage<ToolGuardRuleEntity>> 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<ToolGuardRuleEntity> 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<ToolGuardRuleEntity> 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<String> toggleRule(
@PathVariable String ruleId,
@RequestParam boolean enabled) {
@ -134,6 +144,7 @@ public class SecurityController {
@Operation(summary = "删除自定义规则")
@DeleteMapping("/guard/rules/{ruleId}")
@RequireWorkspaceRole("admin")
public R<String> 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<String> deleteRuleByPk(@PathVariable Long id) {
try {
ruleService.deleteRuleByPk(id);
@ -158,6 +170,7 @@ public class SecurityController {
@Operation(summary = "审计日志")
@GetMapping("/audit/logs")
@RequireWorkspaceRole("admin")
public R<IPage<ToolGuardAuditLogEntity>> 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<Map<String, Object>> getAuditStats() {
return R.ok(auditService.getStats());
}
@ -177,6 +191,7 @@ public class SecurityController {
@Operation(summary = "审批记录(管理视角)")
@GetMapping("/approvals")
@RequireWorkspaceRole("admin")
public R<Object> listApprovals(
@RequestParam(required = false) String conversationId,
@RequestParam(required = false, defaultValue = "0") int limit) {

View File

@ -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<McpServerEntity>> list() {
return R.ok(mcpServerService.sanitizeList(mcpServerService.listAll()));
}
@Operation(summary = "获取 MCP Server 详情")
@GetMapping("/{id}")
@RequireWorkspaceRole("admin")
public R<McpServerEntity> get(@PathVariable Long id) {
return R.ok(mcpServerService.sanitize(mcpServerService.getById(id)));
}
@Operation(summary = "创建 MCP Server")
@PostMapping
@RequireWorkspaceRole("admin")
public R<McpServerEntity> 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<McpServerEntity> 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<Void> 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<McpServerEntity> 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<ConnectionResult> 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<List<McpToolDescriptor>> listTools(@PathVariable Long id) {
return R.ok(mcpServerService.listToolsByServer(id));
}
@Operation(summary = "刷新所有 MCP Server 连接")
@PostMapping("/refresh")
@RequireWorkspaceRole("admin")
public R<Void> refresh() {
mcpServerService.refreshAll();
return R.ok();

View File

@ -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<TriggerEntity>> 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<TriggerEntity> 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<TriggerEntity> 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<TriggerEntity> 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<Void> delete(@PathVariable long id,
@RequestHeader("X-Workspace-Id") long workspaceId) {
triggerService.delete(id, workspaceId);

View File

@ -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<Map<String, Object>> rebuildOverview(@PathVariable Long kbId) {
Map<String, Object> 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<Map<String, Object>> backfillTokens() {
Map<String, Object> body = new HashMap<>();
if (backfillJob == null) {

View File

@ -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<WorkflowEntity>> 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<WorkflowEntity> 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<WorkflowEntity> 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<WorkflowEntity> 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<WorkflowEntity> 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<Void> 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<List<WorkflowRunEntity>> 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<List<PausedRunSummary>> 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<RunDetail> 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<List<vip.mate.workflow.draftgen.WorkflowDraftTemplate>> 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()) {

View File

@ -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) {

View File

@ -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 &mdash; the user must be a global admin regardless of workspace membership.
* <p>
* Used by:
* <ul>
* <li>{@code AuthController.listUsers / createUser} &mdash; user management</li>
* <li>{@code AgentRuntimeController.*} &mdash; runtime ops under {@code /api/v1/admin}</li>
* <li>{@code SubagentController.*} &mdash; runtime subagent operations</li>
* <li>workspace creation (only global admins may create new workspaces)</li>
* </ul>
* 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 {
}