From aae2ef21523a17818fe12f7aaff955869c5a89b8 Mon Sep 17 00:00:00 2001 From: Tyke Chen <190473011+chentyke@users.noreply.github.com> Date: Wed, 8 Jul 2026 15:52:47 +0800 Subject: [PATCH] fix: prevent agent IDOR access --- .../agent/controller/AgentController.java | 76 +++++----- .../service/AgentChatHistoryService.java | 16 +++ .../modules/agent/service/AgentService.java | 36 +++++ .../impl/AgentChatHistoryServiceImpl.java | 26 ++++ .../agent/service/impl/AgentServiceImpl.java | 136 +++++++++++++++--- 5 files changed, 234 insertions(+), 56 deletions(-) diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/controller/AgentController.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/controller/AgentController.java index 5e2435e9..429db8ee 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/agent/controller/AgentController.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/controller/AgentController.java @@ -28,6 +28,8 @@ import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.AllArgsConstructor; import xiaozhi.common.constant.Constant; +import xiaozhi.common.exception.ErrorCode; +import xiaozhi.common.exception.RenException; import xiaozhi.common.page.PageData; import xiaozhi.common.redis.RedisKeys; import xiaozhi.common.redis.RedisUtils; @@ -48,15 +50,10 @@ import xiaozhi.modules.agent.service.AgentTagService; import xiaozhi.modules.agent.service.AgentChatAudioService; import xiaozhi.modules.agent.service.AgentChatHistoryService; import xiaozhi.modules.agent.service.AgentChatSummaryService; -import xiaozhi.modules.agent.service.AgentContextProviderService; -import xiaozhi.modules.agent.service.AgentPluginMappingService; import xiaozhi.modules.agent.service.AgentService; import xiaozhi.modules.agent.service.AgentTemplateService; -import xiaozhi.modules.correctword.service.CorrectWordFileService; import xiaozhi.modules.agent.vo.AgentChatHistoryUserVO; import xiaozhi.modules.agent.vo.AgentInfoVO; -import xiaozhi.modules.device.entity.DeviceEntity; -import xiaozhi.modules.device.service.DeviceService; import xiaozhi.modules.security.user.SecurityUser; @Tag(name = "智能体管理") @@ -64,17 +61,39 @@ import xiaozhi.modules.security.user.SecurityUser; @RestController @RequestMapping("/agent") public class AgentController { + private static final long AUDIO_PLAY_TOKEN_EXPIRE_SECONDS = 300L; + private final AgentService agentService; private final AgentTemplateService agentTemplateService; - private final DeviceService deviceService; private final AgentChatHistoryService agentChatHistoryService; private final AgentChatAudioService agentChatAudioService; - private final AgentPluginMappingService agentPluginMappingService; - private final AgentContextProviderService agentContextProviderService; private final AgentChatSummaryService agentChatSummaryService; private final RedisUtils redisUtils; private final AgentTagService agentTagService; - private final CorrectWordFileService correctWordFileService; + + private void requireAgentPermission(String agentId) { + if (!agentService.checkAgentPermission(agentId, SecurityUser.getUserId())) { + throw new RenException(ErrorCode.NO_PERMISSION); + } + } + + private String requireSessionAgent(String sessionId) { + String agentId = agentChatHistoryService.getAgentIdBySessionId(sessionId); + if (StringUtils.isBlank(agentId)) { + throw new RenException(ErrorCode.AGENT_NOT_FOUND); + } + agentService.getAgentById(agentId); + return agentId; + } + + private String requireAudioPermission(String audioId) { + String agentId = agentChatHistoryService.getAgentIdByAudioId(audioId); + if (StringUtils.isBlank(agentId)) { + throw new RenException(ErrorCode.NO_PERMISSION); + } + requireAgentPermission(agentId); + return agentId; + } @GetMapping("/list") @Operation(summary = "获取用户智能体列表") @@ -106,7 +125,7 @@ public class AgentController { @Operation(summary = "获取智能体详情") @RequiresPermissions("sys:role:normal") public Result getAgentById(@PathVariable("id") String id) { - AgentInfoVO agent = agentService.getAgentById(id); + AgentInfoVO agent = agentService.getAgentById(id, SecurityUser.getUserId()); return ResultUtils.success(agent); } @@ -120,20 +139,16 @@ public class AgentController { @PutMapping("/saveMemory/{macAddress}") @Operation(summary = "根据设备id更新智能体") + @RequiresPermissions("sys:role:normal") public Result updateByDeviceId(@PathVariable String macAddress, @RequestBody @Valid AgentMemoryDTO dto) { - DeviceEntity device = deviceService.getDeviceByMacAddress(macAddress); - if (device == null) { - return new Result<>(); - } - AgentUpdateDTO agentUpdateDTO = new AgentUpdateDTO(); - agentUpdateDTO.setSummaryMemory(dto.getSummaryMemory()); - agentService.updateAgentById(device.getAgentId(), agentUpdateDTO); - return new Result<>(); + agentService.updateAgentMemoryByDeviceMacAddress(macAddress, dto, SecurityUser.getUserId()); + return new Result().ok(null); } @PostMapping("/chat-summary/{sessionId}/save") @Operation(summary = "根据会话ID生成聊天记录总结并保存(异步执行)") public Result generateAndSaveChatSummary(@PathVariable String sessionId) { + requireSessionAgent(sessionId); try { // 异步执行总结生成任务,立即返回成功响应 new Thread(() -> { @@ -155,6 +170,7 @@ public class AgentController { @PostMapping("/chat-title/{sessionId}/generate") @Operation(summary = "根据会话ID生成聊天标题") public Result generateAndSaveChatTitle(@PathVariable String sessionId) { + requireSessionAgent(sessionId); agentChatSummaryService.generateAndSaveChatTitle(sessionId); return new Result().ok(null); } @@ -163,7 +179,7 @@ public class AgentController { @Operation(summary = "更新智能体") @RequiresPermissions("sys:role:normal") public Result update(@PathVariable String id, @RequestBody @Valid AgentUpdateDTO dto) { - agentService.updateAgentById(id, dto); + agentService.updateAgentById(id, dto, SecurityUser.getUserId()); return new Result<>(); } @@ -171,18 +187,7 @@ public class AgentController { @Operation(summary = "删除智能体") @RequiresPermissions("sys:role:normal") public Result delete(@PathVariable String id) { - // 先删除关联的设备 - deviceService.deleteByAgentId(id); - // 删除关联的聊天记录 - agentChatHistoryService.deleteByAgentId(id, true, true); - // 删除关联的插件 - agentPluginMappingService.deleteByAgentId(id); - // 删除关联的上下文源配置 - agentContextProviderService.deleteByAgentId(id); - // 删除关联的替换词文件关联记录 - correctWordFileService.deleteMappingsByAgentId(id); - // 再删除智能体 - agentService.deleteById(id); + agentService.deleteAgentById(id, SecurityUser.getUserId()); return new Result<>(); } @@ -205,6 +210,7 @@ public class AgentController { public Result> getAgentSessions( @PathVariable("id") String id, @Parameter(hidden = true) @RequestParam Map params) { + requireAgentPermission(id); params.put("agentId", id); PageData page = agentChatHistoryService.getSessionListByAgentId(params); return new Result>().ok(page); @@ -252,6 +258,7 @@ public class AgentController { @RequiresPermissions("sys:role:normal") public Result getContentByAudioId( @PathVariable("id") String id) { + requireAudioPermission(id); // 查询聊天记录 String data = agentChatHistoryService.getContentByAudioId(id); return new Result().ok(data); @@ -261,12 +268,13 @@ public class AgentController { @Operation(summary = "获取音频下载ID") @RequiresPermissions("sys:role:normal") public Result getAudioId(@PathVariable("audioId") String audioId) { + requireAudioPermission(audioId); byte[] audioData = agentChatAudioService.getAudio(audioId); if (audioData == null) { return new Result().error("音频不存在"); } String uuid = UUID.randomUUID().toString(); - redisUtils.set(RedisKeys.getAgentAudioIdKey(uuid), audioId); + redisUtils.set(RedisKeys.getAgentAudioIdKey(uuid), audioId, AUDIO_PLAY_TOKEN_EXPIRE_SECONDS); return new Result().ok(uuid); } @@ -322,6 +330,7 @@ public class AgentController { @Operation(summary = "获取智能体的标签") @RequiresPermissions("sys:role:normal") public Result> getAgentTags(@PathVariable String id) { + requireAgentPermission(id); List tags = agentTagService.getTagsByAgentId(id); return new Result>().ok(tags); } @@ -330,10 +339,11 @@ public class AgentController { @Operation(summary = "保存智能体的标签") @RequiresPermissions("sys:role:normal") public Result saveAgentTags(@PathVariable String id, @RequestBody Map params) { + requireAgentPermission(id); List tagIds = (List) params.get("tagIds"); List tagNames = (List) params.get("tagNames"); agentTagService.saveAgentTags(id, tagIds, tagNames); return new Result().ok(null); } -} \ No newline at end of file +} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/AgentChatHistoryService.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/AgentChatHistoryService.java index 459b5a5a..48fc0539 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/AgentChatHistoryService.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/AgentChatHistoryService.java @@ -37,6 +37,14 @@ public interface AgentChatHistoryService extends IService getChatHistoryBySessionId(String agentId, String sessionId); + /** + * 根据会话ID获取智能体ID + * + * @param sessionId 会话ID + * @return 智能体ID + */ + String getAgentIdBySessionId(String sessionId); + /** * 根据智能体ID删除聊天记录 * @@ -62,6 +70,14 @@ public interface AgentChatHistoryService extends IService { */ AgentInfoVO getAgentById(String id); + /** + * 根据ID获取当前用户有权访问的智能体 + * + * @param id 智能体ID + * @param userId 当前用户ID + * @return 智能体实体 + */ + AgentInfoVO getAgentById(String id, Long userId); + /** * 插入智能体 * @@ -93,6 +103,32 @@ public interface AgentService extends BaseService { */ void updateAgentById(String agentId, AgentUpdateDTO dto); + /** + * 更新当前用户有权访问的智能体 + * + * @param agentId 智能体ID + * @param dto 更新智能体所需的信息 + * @param userId 当前用户ID + */ + void updateAgentById(String agentId, AgentUpdateDTO dto, Long userId); + + /** + * 根据设备MAC地址更新当前用户有权访问的智能体记忆 + * + * @param macAddress 设备MAC地址 + * @param dto 智能体记忆 + * @param userId 当前用户ID + */ + void updateAgentMemoryByDeviceMacAddress(String macAddress, AgentMemoryDTO dto, Long userId); + + /** + * 删除当前用户有权访问的智能体 + * + * @param agentId 智能体ID + * @param userId 当前用户ID + */ + void deleteAgentById(String agentId, Long userId); + /** * 创建智能体 * diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentChatHistoryServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentChatHistoryServiceImpl.java index 9cfece89..d2e8e905 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentChatHistoryServiceImpl.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentChatHistoryServiceImpl.java @@ -88,6 +88,19 @@ public class AgentChatHistoryServiceImpl extends ServiceImpl() + .select(AgentChatHistoryEntity::getAgentId) + .eq(AgentChatHistoryEntity::getSessionId, sessionId) + .last("LIMIT 1")); + return entity == null ? null : entity.getAgentId(); + } + @Override @Transactional(rollbackFor = Exception.class) public void deleteByAgentId(String agentId, Boolean deleteAudio, Boolean deleteText) { @@ -176,6 +189,19 @@ public class AgentChatHistoryServiceImpl extends ServiceImpl() + .select(AgentChatHistoryEntity::getAgentId) + .eq(AgentChatHistoryEntity::getAudioId, audioId) + .last("LIMIT 1")); + return entity == null ? null : entity.getAgentId(); + } + @Override public boolean isAudioOwnedByAgent(String audioId, String agentId) { // 查询是否有指定音频id和智能体id的数据,如果有且只有一条说明此数据属性此智能体 diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentServiceImpl.java index 981a2bcf..cdf7cd0a 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentServiceImpl.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentServiceImpl.java @@ -34,6 +34,7 @@ import xiaozhi.modules.agent.dao.AgentDao; import xiaozhi.modules.agent.dao.AgentTagDao; import xiaozhi.modules.agent.dto.AgentCreateDTO; import xiaozhi.modules.agent.dto.AgentDTO; +import xiaozhi.modules.agent.dto.AgentMemoryDTO; import xiaozhi.modules.agent.dto.AgentTagDTO; import xiaozhi.modules.agent.dto.AgentUpdateDTO; import xiaozhi.modules.agent.entity.AgentContextProviderEntity; @@ -92,6 +93,7 @@ public class AgentServiceImpl extends BaseServiceImpl imp if (agent == null) { throw new RenException(ErrorCode.AGENT_NOT_FOUND); } + requireCurrentUserPermissionIfPresent(agent); if (agent.getMemModelId() != null && agent.getMemModelId().equals(Constant.MEMORY_NO_MEM)) { agent.setChatHistoryConf(Constant.ChatHistoryConfEnum.IGNORE.getCode()); @@ -114,6 +116,65 @@ public class AgentServiceImpl extends BaseServiceImpl imp return agent; } + @Override + public AgentInfoVO getAgentById(String id, Long userId) { + AgentInfoVO agent = getAgentById(id); + requireAgentPermission(agent, userId); + return agent; + } + + private AgentEntity getAgentEntityOrThrow(String agentId) { + AgentEntity agent = agentDao.selectById(agentId); + if (agent == null) { + throw new RenException(ErrorCode.AGENT_NOT_FOUND); + } + return agent; + } + + private boolean isCurrentUserSuperAdmin() { + UserDetail user = SecurityUser.getUser(); + return user != null && Integer.valueOf(SuperAdminEnum.YES.value()).equals(user.getSuperAdmin()); + } + + private void requireCurrentUserPermissionIfPresent(AgentEntity agent) { + Long userId = SecurityUser.getUserId(); + if (userId != null) { + requireAgentPermission(agent, userId); + } + } + + private boolean hasAgentPermission(AgentEntity agent, Long userId) { + if (agent == null) { + return false; + } + if (isCurrentUserSuperAdmin()) { + return true; + } + return userId != null && userId.equals(agent.getUserId()); + } + + private void requireAgentPermission(AgentEntity agent, Long userId) { + if (!hasAgentPermission(agent, userId)) { + throw new RenException(ErrorCode.NO_PERMISSION); + } + } + + private boolean hasDevicePermission(DeviceEntity device, Long userId) { + if (device == null) { + return false; + } + if (isCurrentUserSuperAdmin()) { + return true; + } + return userId != null && userId.equals(device.getUserId()); + } + + private void requireDevicePermission(DeviceEntity device, Long userId) { + if (!hasDevicePermission(device, userId)) { + throw new RenException(ErrorCode.NO_PERMISSION); + } + } + @Override public boolean insert(AgentEntity entity) { // 如果ID为空,自动生成一个UUID作为ID @@ -254,34 +315,28 @@ public class AgentServiceImpl extends BaseServiceImpl imp @Override public boolean checkAgentPermission(String agentId, Long userId) { - if (SecurityUser.getUser() == null || SecurityUser.getUser().getId() == null) { - return false; - } - // 获取智能体信息 - AgentEntity agent = getAgentById(agentId); - if (agent == null) { - return false; - } - - // 如果是超级管理员,直接返回true - if (SecurityUser.getUser().getSuperAdmin() == SuperAdminEnum.YES.value()) { - return true; - } - - // 检查是否是智能体的所有者 - return userId.equals(agent.getUserId()); + AgentEntity agent = agentDao.selectById(agentId); + return hasAgentPermission(agent, userId); } // 根据id更新智能体信息 @Override @Transactional(rollbackFor = Exception.class) public void updateAgentById(String agentId, AgentUpdateDTO dto) { - // 先查询现有实体 - AgentEntity existingEntity = this.getAgentById(agentId); - if (existingEntity == null) { - throw new RenException(ErrorCode.AGENT_NOT_FOUND); - } - + AgentEntity existingEntity = getAgentEntityOrThrow(agentId); + requireCurrentUserPermissionIfPresent(existingEntity); + updateAgentEntity(agentId, dto, existingEntity); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateAgentById(String agentId, AgentUpdateDTO dto, Long userId) { + AgentEntity existingEntity = getAgentEntityOrThrow(agentId); + requireAgentPermission(existingEntity, userId); + updateAgentEntity(agentId, dto, existingEntity); + } + + private void updateAgentEntity(String agentId, AgentUpdateDTO dto, AgentEntity existingEntity) { // 只更新提供的非空字段 if (dto.getAgentName() != null) { existingEntity.setAgentName(dto.getAgentName()); @@ -437,6 +492,41 @@ public class AgentServiceImpl extends BaseServiceImpl imp this.updateById(existingEntity); } + @Override + @Transactional(rollbackFor = Exception.class) + public void updateAgentMemoryByDeviceMacAddress(String macAddress, AgentMemoryDTO dto, Long userId) { + DeviceEntity device = deviceService.getDeviceByMacAddress(macAddress); + if (device == null || StringUtils.isBlank(device.getAgentId()) || dto == null) { + return; + } + + requireDevicePermission(device, userId); + + AgentUpdateDTO agentUpdateDTO = new AgentUpdateDTO(); + agentUpdateDTO.setSummaryMemory(dto.getSummaryMemory()); + updateAgentById(device.getAgentId(), agentUpdateDTO, userId); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteAgentById(String agentId, Long userId) { + AgentEntity agent = getAgentEntityOrThrow(agentId); + requireAgentPermission(agent, userId); + + // 先删除关联的设备 + deviceService.deleteByAgentId(agentId); + // 删除关联的聊天记录 + agentChatHistoryService.deleteByAgentId(agentId, true, true); + // 删除关联的插件 + agentPluginMappingService.deleteByAgentId(agentId); + // 删除关联的上下文源配置 + agentContextProviderService.deleteByAgentId(agentId); + // 删除关联的替换词文件关联记录 + correctWordFileService.deleteMappingsByAgentId(agentId); + // 再删除智能体 + baseDao.deleteById(agentId); + } + /** * 验证大语言模型和意图识别的参数是否符合匹配 * @@ -576,4 +666,4 @@ public class AgentServiceImpl extends BaseServiceImpl imp } } -} \ No newline at end of file +}