mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
feat(workspace,system): add @RequireGlobalAdmin and gate admin-only controllers
This commit is contained in:
parent
bba9975749
commit
3ba22b99f3
@ -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));
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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()) {
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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));
|
||||
}
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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()) {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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.
|
||||
* <p>
|
||||
* Used by:
|
||||
* <ul>
|
||||
* <li>{@code AuthController.listUsers / createUser} — user management</li>
|
||||
* <li>{@code AgentRuntimeController.*} — runtime ops under {@code /api/v1/admin}</li>
|
||||
* <li>{@code SubagentController.*} — 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 {
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user