mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 11:13:43 +08:00
fix(security): close workspace isolation gaps and audit context loss
This commit is contained in:
parent
3506c55bc8
commit
d44ba0cdd8
@ -11,6 +11,7 @@ import vip.mate.agent.AgentState;
|
||||
import vip.mate.agent.model.AgentEntity;
|
||||
import vip.mate.audit.service.AuditEventService;
|
||||
import vip.mate.common.result.R;
|
||||
import vip.mate.exception.MateClawException;
|
||||
import vip.mate.workspace.core.annotation.RequireWorkspaceRole;
|
||||
|
||||
import java.io.IOException;
|
||||
@ -39,17 +40,19 @@ public class AgentController {
|
||||
@RequireWorkspaceRole("viewer")
|
||||
public R<List<AgentEntity>> list(
|
||||
@RequestHeader(value = "X-Workspace-Id", required = false) Long workspaceId) {
|
||||
if (workspaceId != null) {
|
||||
return R.ok(agentService.listAgentsByWorkspace(workspaceId));
|
||||
}
|
||||
return R.ok(agentService.listAgents());
|
||||
// 无 header 时强制使用默认 workspace,不返回全局数据
|
||||
long wsId = workspaceId != null ? workspaceId : 1L;
|
||||
return R.ok(agentService.listAgentsByWorkspace(wsId));
|
||||
}
|
||||
|
||||
@Operation(summary = "获取Agent详情")
|
||||
@GetMapping("/{id}")
|
||||
@RequireWorkspaceRole("viewer")
|
||||
public R<AgentEntity> get(@PathVariable Long id) {
|
||||
return R.ok(agentService.getAgent(id));
|
||||
public R<AgentEntity> get(@PathVariable Long id,
|
||||
@RequestHeader(value = "X-Workspace-Id", required = false) Long workspaceId) {
|
||||
AgentEntity agent = agentService.getAgent(id);
|
||||
verifyResourceWorkspace(agent.getWorkspaceId(), workspaceId);
|
||||
return R.ok(agent);
|
||||
}
|
||||
|
||||
@Operation(summary = "创建Agent")
|
||||
@ -58,9 +61,8 @@ public class AgentController {
|
||||
public R<AgentEntity> create(
|
||||
@RequestHeader(value = "X-Workspace-Id", required = false) Long workspaceId,
|
||||
@RequestBody AgentEntity agent) {
|
||||
if (workspaceId != null) {
|
||||
agent.setWorkspaceId(workspaceId);
|
||||
}
|
||||
// 始终注入 workspace_id,无 header 时使用默认
|
||||
agent.setWorkspaceId(workspaceId != null ? workspaceId : 1L);
|
||||
AgentEntity created = agentService.createAgent(agent);
|
||||
auditEventService.record("CREATE", "AGENT", String.valueOf(created.getId()), created.getName(), null);
|
||||
return R.ok(created);
|
||||
@ -69,8 +71,12 @@ public class AgentController {
|
||||
@Operation(summary = "更新Agent")
|
||||
@PutMapping("/{id}")
|
||||
@RequireWorkspaceRole("member")
|
||||
public R<AgentEntity> update(@PathVariable Long id, @RequestBody AgentEntity agent) {
|
||||
public R<AgentEntity> update(@PathVariable Long id, @RequestBody AgentEntity agent,
|
||||
@RequestHeader(value = "X-Workspace-Id", required = false) Long workspaceId) {
|
||||
AgentEntity existing = agentService.getAgent(id);
|
||||
verifyResourceWorkspace(existing.getWorkspaceId(), workspaceId);
|
||||
agent.setId(id);
|
||||
agent.setWorkspaceId(existing.getWorkspaceId()); // 不允许跨 workspace 迁移
|
||||
AgentEntity updated = agentService.updateAgent(agent);
|
||||
auditEventService.record("UPDATE", "AGENT", String.valueOf(id), updated.getName(), null);
|
||||
return R.ok(updated);
|
||||
@ -79,10 +85,12 @@ public class AgentController {
|
||||
@Operation(summary = "删除Agent")
|
||||
@DeleteMapping("/{id}")
|
||||
@RequireWorkspaceRole("admin")
|
||||
public R<Void> delete(@PathVariable Long id) {
|
||||
public R<Void> delete(@PathVariable Long id,
|
||||
@RequestHeader(value = "X-Workspace-Id", required = false) Long workspaceId) {
|
||||
AgentEntity agent = agentService.getAgent(id);
|
||||
verifyResourceWorkspace(agent.getWorkspaceId(), workspaceId);
|
||||
agentService.deleteAgent(id);
|
||||
auditEventService.record("DELETE", "AGENT", String.valueOf(id), agent != null ? agent.getName() : null, null);
|
||||
auditEventService.record("DELETE", "AGENT", String.valueOf(id), agent.getName(), null);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
@ -148,4 +156,15 @@ public class AgentController {
|
||||
private String message;
|
||||
private String conversationId = "default";
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验目标资源实际归属的 workspace 与请求 header 一致。
|
||||
* 防止 "在 workspace A 鉴权,操作 workspace B 资源" 的跨域攻击。
|
||||
*/
|
||||
private void verifyResourceWorkspace(Long resourceWorkspaceId, Long headerWorkspaceId) {
|
||||
long requestedWs = headerWorkspaceId != null ? headerWorkspaceId : 1L;
|
||||
if (resourceWorkspaceId != null && !resourceWorkspaceId.equals(requestedWs)) {
|
||||
throw new MateClawException("资源不属于当前工作区");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -35,18 +35,26 @@ public class AuditEventService {
|
||||
private final AuthService authService;
|
||||
|
||||
/**
|
||||
* 异步记录审计事件
|
||||
* 异步记录审计事件。
|
||||
* <p>
|
||||
* 在调用线程(请求线程)中捕获完整上下文,然后交给异步线程写库。
|
||||
* 这样避免了 SecurityContext/RequestContext 在异步线程中丢失的问题。
|
||||
*/
|
||||
@Async
|
||||
public void record(String action, String resourceType, String resourceId,
|
||||
String resourceName, String detailJson) {
|
||||
// 在请求线程中构建事件(可以访问 SecurityContext 和 RequestContext)
|
||||
AuditEventEntity event = buildEvent(action, resourceType, resourceId, resourceName, detailJson);
|
||||
if (event != null) {
|
||||
insertAsync(event);
|
||||
}
|
||||
}
|
||||
|
||||
@Async
|
||||
void insertAsync(AuditEventEntity event) {
|
||||
try {
|
||||
AuditEventEntity event = buildEvent(action, resourceType, resourceId, resourceName, detailJson);
|
||||
if (event != null) {
|
||||
auditEventMapper.insert(event);
|
||||
}
|
||||
auditEventMapper.insert(event);
|
||||
} catch (Exception e) {
|
||||
log.warn("Failed to record audit event: {}/{}/{}", action, resourceType, resourceId, e);
|
||||
log.warn("Failed to insert audit event: {}/{}", event.getAction(), event.getResourceType(), e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -9,6 +9,7 @@ import vip.mate.channel.model.ChannelEntity;
|
||||
import vip.mate.channel.service.ChannelService;
|
||||
import vip.mate.audit.service.AuditEventService;
|
||||
import vip.mate.common.result.R;
|
||||
import vip.mate.exception.MateClawException;
|
||||
import vip.mate.workspace.core.annotation.RequireWorkspaceRole;
|
||||
|
||||
import java.util.List;
|
||||
@ -37,10 +38,8 @@ public class ChannelController {
|
||||
@GetMapping
|
||||
public R<List<ChannelEntity>> list(
|
||||
@RequestHeader(value = "X-Workspace-Id", required = false) Long workspaceId) {
|
||||
if (workspaceId != null) {
|
||||
return R.ok(channelService.listChannelsByWorkspace(workspaceId));
|
||||
}
|
||||
return R.ok(channelService.listChannels());
|
||||
long wsId = workspaceId != null ? workspaceId : 1L;
|
||||
return R.ok(channelService.listChannelsByWorkspace(wsId));
|
||||
}
|
||||
|
||||
@RequireWorkspaceRole("viewer")
|
||||
@ -53,8 +52,11 @@ public class ChannelController {
|
||||
@RequireWorkspaceRole("viewer")
|
||||
@Operation(summary = "获取渠道详情")
|
||||
@GetMapping("/{id}")
|
||||
public R<ChannelEntity> get(@PathVariable Long id) {
|
||||
return R.ok(channelService.getChannel(id));
|
||||
public R<ChannelEntity> get(@PathVariable Long id,
|
||||
@RequestHeader(value = "X-Workspace-Id", required = false) Long workspaceId) {
|
||||
ChannelEntity channel = channelService.getChannel(id);
|
||||
verifyResourceWorkspace(channel.getWorkspaceId(), workspaceId);
|
||||
return R.ok(channel);
|
||||
}
|
||||
|
||||
@RequireWorkspaceRole("admin")
|
||||
@ -63,9 +65,7 @@ public class ChannelController {
|
||||
public R<ChannelEntity> create(
|
||||
@RequestHeader(value = "X-Workspace-Id", required = false) Long workspaceId,
|
||||
@RequestBody ChannelEntity channel) {
|
||||
if (workspaceId != null) {
|
||||
channel.setWorkspaceId(workspaceId);
|
||||
}
|
||||
channel.setWorkspaceId(workspaceId != null ? workspaceId : 1L);
|
||||
ChannelEntity created = channelService.createChannel(channel);
|
||||
auditEventService.record("CREATE", "CHANNEL", String.valueOf(created.getId()), created.getName(), null);
|
||||
return R.ok(created);
|
||||
@ -74,10 +74,13 @@ public class ChannelController {
|
||||
@RequireWorkspaceRole("admin")
|
||||
@Operation(summary = "更新渠道")
|
||||
@PutMapping("/{id}")
|
||||
public R<ChannelEntity> update(@PathVariable Long id, @RequestBody ChannelEntity channel) {
|
||||
public R<ChannelEntity> update(@PathVariable Long id, @RequestBody ChannelEntity channel,
|
||||
@RequestHeader(value = "X-Workspace-Id", required = false) Long workspaceId) {
|
||||
ChannelEntity existing = channelService.getChannel(id);
|
||||
verifyResourceWorkspace(existing.getWorkspaceId(), workspaceId);
|
||||
channel.setId(id);
|
||||
channel.setWorkspaceId(existing.getWorkspaceId());
|
||||
ChannelEntity updated = channelService.updateChannel(channel);
|
||||
// 配置变更后热替换渠道(新 Adapter 就绪后才替换旧的,失败则保留旧的)
|
||||
channelManager.restartChannel(id);
|
||||
auditEventService.record("UPDATE", "CHANNEL", String.valueOf(id), updated.getName(), null);
|
||||
return R.ok(updated);
|
||||
@ -86,19 +89,23 @@ public class ChannelController {
|
||||
@RequireWorkspaceRole("admin")
|
||||
@Operation(summary = "删除渠道")
|
||||
@DeleteMapping("/{id}")
|
||||
public R<Void> delete(@PathVariable Long id) {
|
||||
// 先停止渠道再删除
|
||||
public R<Void> delete(@PathVariable Long id,
|
||||
@RequestHeader(value = "X-Workspace-Id", required = false) Long workspaceId) {
|
||||
ChannelEntity channel = channelService.getChannel(id);
|
||||
verifyResourceWorkspace(channel.getWorkspaceId(), workspaceId);
|
||||
channelManager.stopChannel(id);
|
||||
channelService.deleteChannel(id);
|
||||
auditEventService.record("DELETE", "CHANNEL", String.valueOf(id), channel != null ? channel.getName() : null, null);
|
||||
auditEventService.record("DELETE", "CHANNEL", String.valueOf(id), channel.getName(), null);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
@RequireWorkspaceRole("admin")
|
||||
@Operation(summary = "启用/禁用渠道")
|
||||
@PutMapping("/{id}/toggle")
|
||||
public R<ChannelEntity> toggle(@PathVariable Long id, @RequestParam boolean enabled) {
|
||||
public R<ChannelEntity> toggle(@PathVariable Long id, @RequestParam boolean enabled,
|
||||
@RequestHeader(value = "X-Workspace-Id", required = false) Long workspaceId) {
|
||||
ChannelEntity existing = channelService.getChannel(id);
|
||||
verifyResourceWorkspace(existing.getWorkspaceId(), workspaceId);
|
||||
ChannelEntity channel = channelService.toggleChannel(id, enabled);
|
||||
// 联动 ChannelManager:启用时启动,禁用时停止
|
||||
if (enabled) {
|
||||
@ -116,4 +123,11 @@ public class ChannelController {
|
||||
public R<Map<String, Object>> status() {
|
||||
return R.ok(channelManager.getStatus());
|
||||
}
|
||||
|
||||
private void verifyResourceWorkspace(Long resourceWorkspaceId, Long headerWorkspaceId) {
|
||||
long requestedWs = headerWorkspaceId != null ? headerWorkspaceId : 1L;
|
||||
if (resourceWorkspaceId != null && !resourceWorkspaceId.equals(requestedWs)) {
|
||||
throw new MateClawException("资源不属于当前工作区");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -50,13 +50,17 @@ public class DashboardController {
|
||||
public R<List<CronJobRunEntity>> cronJobRuns(
|
||||
@PathVariable Long cronJobId,
|
||||
@RequestParam(defaultValue = "20") int limit) {
|
||||
// TODO: 校验 cronJobId 对应的 agent 属于当前 workspace
|
||||
return R.ok(cronJobRunService.listByJobId(cronJobId, Math.min(limit, 100)));
|
||||
}
|
||||
|
||||
@Operation(summary = "获取最近执行记录")
|
||||
@Operation(summary = "获取最近执行记录(当前 workspace 关联的 CronJob)")
|
||||
@GetMapping("/cron-runs")
|
||||
@RequireWorkspaceRole("viewer")
|
||||
public R<List<CronJobRunEntity>> recentRuns(@RequestParam(defaultValue = "20") int limit) {
|
||||
return R.ok(cronJobRunService.listRecent(Math.min(limit, 100)));
|
||||
public R<List<CronJobRunEntity>> recentRuns(
|
||||
@RequestHeader(value = "X-Workspace-Id", required = false) Long workspaceId,
|
||||
@RequestParam(defaultValue = "20") int limit) {
|
||||
long wsId = workspaceId != null ? workspaceId : 1L;
|
||||
return R.ok(cronJobRunService.listRecentByWorkspace(wsId, Math.min(limit, 100)));
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,11 +3,18 @@ package vip.mate.dashboard.service;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import vip.mate.agent.model.AgentEntity;
|
||||
import vip.mate.agent.repository.AgentMapper;
|
||||
import vip.mate.cron.model.CronJobEntity;
|
||||
import vip.mate.cron.repository.CronJobMapper;
|
||||
import vip.mate.dashboard.model.CronJobRunEntity;
|
||||
import vip.mate.dashboard.repository.CronJobRunMapper;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* CronJob 执行历史服务
|
||||
@ -19,6 +26,8 @@ import java.util.List;
|
||||
public class CronJobRunService {
|
||||
|
||||
private final CronJobRunMapper runMapper;
|
||||
private final CronJobMapper cronJobMapper;
|
||||
private final AgentMapper agentMapper;
|
||||
|
||||
/**
|
||||
* 记录一次执行开始
|
||||
@ -80,4 +89,33 @@ public class CronJobRunService {
|
||||
.orderByDesc(CronJobRunEntity::getStartedAt)
|
||||
.last("LIMIT " + limit));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询指定 workspace 关联的最近执行记录
|
||||
* 路径:workspace → agents → cronJobs → cronJobRuns
|
||||
*/
|
||||
public List<CronJobRunEntity> listRecentByWorkspace(Long workspaceId, int limit) {
|
||||
// 1. workspace 下的 agent IDs
|
||||
List<AgentEntity> agents = agentMapper.selectList(
|
||||
new LambdaQueryWrapper<AgentEntity>()
|
||||
.eq(AgentEntity::getWorkspaceId, workspaceId)
|
||||
.select(AgentEntity::getId));
|
||||
if (agents.isEmpty()) return Collections.emptyList();
|
||||
Set<Long> agentIds = agents.stream().map(AgentEntity::getId).collect(Collectors.toSet());
|
||||
|
||||
// 2. 这些 agent 关联的 cronJob IDs
|
||||
List<CronJobEntity> jobs = cronJobMapper.selectList(
|
||||
new LambdaQueryWrapper<CronJobEntity>()
|
||||
.in(CronJobEntity::getAgentId, agentIds)
|
||||
.select(CronJobEntity::getId));
|
||||
if (jobs.isEmpty()) return Collections.emptyList();
|
||||
Set<Long> jobIds = jobs.stream().map(CronJobEntity::getId).collect(Collectors.toSet());
|
||||
|
||||
// 3. 这些 cronJob 的执行记录
|
||||
return runMapper.selectList(
|
||||
new LambdaQueryWrapper<CronJobRunEntity>()
|
||||
.in(CronJobRunEntity::getCronJobId, jobIds)
|
||||
.orderByDesc(CronJobRunEntity::getStartedAt)
|
||||
.last("LIMIT " + limit));
|
||||
}
|
||||
}
|
||||
|
||||
@ -79,11 +79,18 @@ public class WikiController {
|
||||
@RequireWorkspaceRole("member")
|
||||
@Operation(summary = "创建知识库")
|
||||
@PostMapping("/knowledge-bases")
|
||||
public R<WikiKnowledgeBaseEntity> createKB(@RequestBody Map<String, Object> body) {
|
||||
public R<WikiKnowledgeBaseEntity> createKB(@RequestBody Map<String, Object> body,
|
||||
@RequestHeader(value = "X-Workspace-Id", required = false) Long workspaceId) {
|
||||
String name = (String) body.get("name");
|
||||
String description = (String) body.get("description");
|
||||
Long agentId = body.get("agentId") != null ? Long.valueOf(body.get("agentId").toString()) : null;
|
||||
return R.ok(kbService.create(name, description, agentId));
|
||||
WikiKnowledgeBaseEntity kb = kbService.create(name, description, agentId);
|
||||
// 注入 workspace_id(create 方法内部不感知 workspace,需要在 controller 层补充)
|
||||
if (kb.getWorkspaceId() == null || kb.getWorkspaceId() == 0) {
|
||||
kb.setWorkspaceId(workspaceId != null ? workspaceId : 1L);
|
||||
kbService.updateWorkspaceId(kb.getId(), kb.getWorkspaceId());
|
||||
}
|
||||
return R.ok(kb);
|
||||
}
|
||||
|
||||
@RequireWorkspaceRole("member")
|
||||
|
||||
@ -165,6 +165,17 @@ public class WikiKnowledgeBaseService {
|
||||
kbMapper.updateById(entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新知识库的 workspace 归属
|
||||
*/
|
||||
public void updateWorkspaceId(Long kbId, Long workspaceId) {
|
||||
WikiKnowledgeBaseEntity entity = kbMapper.selectById(kbId);
|
||||
if (entity != null) {
|
||||
entity.setWorkspaceId(workspaceId);
|
||||
kbMapper.updateById(entity);
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void delete(Long id) {
|
||||
kbMapper.deleteById(id);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user