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 9f3b7fea..5421d1ad 100644 --- a/mateclaw-server/src/main/java/vip/mate/config/WorkspaceAccessInterceptor.java +++ b/mateclaw-server/src/main/java/vip/mate/config/WorkspaceAccessInterceptor.java @@ -9,12 +9,17 @@ import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.HandlerInterceptor; +import org.springframework.web.servlet.HandlerMapping; +import vip.mate.agent.model.AgentEntity; +import vip.mate.agent.repository.AgentMapper; 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; +import java.util.Map; + /** * Workspace 访问拦截器 *

@@ -34,6 +39,7 @@ public class WorkspaceAccessInterceptor implements HandlerInterceptor { private final WorkspaceService workspaceService; private final AuthService authService; + private final AgentMapper agentMapper; /** 默认 workspace ID(未传 header 时使用) */ private static final long DEFAULT_WORKSPACE_ID = 1L; @@ -91,9 +97,50 @@ public class WorkspaceAccessInterceptor implements HandlerInterceptor { return false; } + // The role check above only proves the user belongs to the *header* + // workspace — not that a path-bound {agentId} actually lives there. + // Without this a member of workspace A could read workspace B's agent + // memory / context files by supplying B's agent id with their own header. + if (!agentBelongsToWorkspace(request, workspaceId)) { + log.warn("Cross-workspace agent access denied: user={}, workspaceId={}, path={}", + username, workspaceId, request.getRequestURI()); + sendForbidden(response, "Agent does not belong to the current workspace"); + return false; + } + return true; } + /** + * When the matched route carries an {@code {agentId}} path variable, verify + * that agent belongs to the resolved workspace. Allows the request through + * when there is no agent id, the id is unparseable, the agent does not + * exist (so the handler can return its own 404), or the agent has not been + * assigned a workspace. + */ + @SuppressWarnings("unchecked") + private boolean agentBelongsToWorkspace(HttpServletRequest request, long workspaceId) { + Object attr = request.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE); + if (!(attr instanceof Map)) { + return true; + } + Object rawAgentId = ((Map) attr).get("agentId"); + if (rawAgentId == null) { + return true; + } + long agentId; + try { + agentId = Long.parseLong(rawAgentId.toString()); + } catch (NumberFormatException e) { + return true; + } + AgentEntity agent = agentMapper.selectById(agentId); + if (agent == null || agent.getWorkspaceId() == null) { + return true; + } + return agent.getWorkspaceId() == workspaceId; + } + private long resolveWorkspaceId(HttpServletRequest request) { String header = request.getHeader("X-Workspace-Id"); if (header != null && !header.isBlank()) { diff --git a/mateclaw-server/src/main/java/vip/mate/workspace/document/controller/WorkspaceFileController.java b/mateclaw-server/src/main/java/vip/mate/workspace/document/controller/WorkspaceFileController.java index 004cc177..0f049fb7 100644 --- a/mateclaw-server/src/main/java/vip/mate/workspace/document/controller/WorkspaceFileController.java +++ b/mateclaw-server/src/main/java/vip/mate/workspace/document/controller/WorkspaceFileController.java @@ -39,6 +39,7 @@ public class WorkspaceFileController { * 列出 Agent 的所有工作区文件(不含内容) */ @Operation(summary = "列出工作区文件") + @RequireWorkspaceRole("viewer") @GetMapping("/files") public R> listFiles(@PathVariable Long agentId) { return R.ok(workspaceFileService.listFiles(agentId)); @@ -48,6 +49,7 @@ public class WorkspaceFileController { * 读取单个文件内容(支持子目录,如 memory/2026-04-03.md) */ @Operation(summary = "读取工作区文件") + @RequireWorkspaceRole("viewer") @GetMapping("/files/**") public R getFile(@PathVariable Long agentId, HttpServletRequest request) { String filename = extractFilename(request); @@ -62,6 +64,7 @@ public class WorkspaceFileController { * 创建或更新文件(支持子目录) */ @Operation(summary = "保存工作区文件") + @RequireWorkspaceRole("member") @PutMapping("/files/**") public R saveFile(@PathVariable Long agentId, HttpServletRequest httpRequest, @@ -74,6 +77,7 @@ public class WorkspaceFileController { * 删除文件(支持子目录) */ @Operation(summary = "删除工作区文件") + @RequireWorkspaceRole("member") @DeleteMapping("/files/**") public R deleteFile(@PathVariable Long agentId, HttpServletRequest request) { String filename = extractFilename(request); @@ -94,6 +98,7 @@ public class WorkspaceFileController { * 获取启用的系统提示文件列表(有序) */ @Operation(summary = "获取系统提示文件列表") + @RequireWorkspaceRole("viewer") @GetMapping("/prompt-files") public R> getPromptFiles(@PathVariable Long agentId) { return R.ok(workspaceFileService.getPromptFiles(agentId)); @@ -103,6 +108,7 @@ public class WorkspaceFileController { * 设置启用的系统提示文件列表(有序) */ @Operation(summary = "设置系统提示文件列表") + @RequireWorkspaceRole("member") @PutMapping("/prompt-files") public R setPromptFiles(@PathVariable Long agentId, @RequestBody PromptFilesRequest request) {