fix(workspace): verify a path-bound agent belongs to the request workspace

This commit is contained in:
matevip 2026-05-19 20:07:02 +08:00
parent 92d35a3d3d
commit 32d633ae4b
2 changed files with 53 additions and 0 deletions

View File

@ -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 访问拦截器
* <p>
@ -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<String, String>) 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()) {

View File

@ -39,6 +39,7 @@ public class WorkspaceFileController {
* 列出 Agent 的所有工作区文件不含内容
*/
@Operation(summary = "列出工作区文件")
@RequireWorkspaceRole("viewer")
@GetMapping("/files")
public R<List<WorkspaceFileEntity>> 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<WorkspaceFileEntity> 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<WorkspaceFileEntity> saveFile(@PathVariable Long agentId,
HttpServletRequest httpRequest,
@ -74,6 +77,7 @@ public class WorkspaceFileController {
* 删除文件支持子目录
*/
@Operation(summary = "删除工作区文件")
@RequireWorkspaceRole("member")
@DeleteMapping("/files/**")
public R<Void> 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<List<String>> 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<Void> setPromptFiles(@PathVariable Long agentId,
@RequestBody PromptFilesRequest request) {