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> 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
> 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