From b4cc83168d3b0dd97482f8948b38dcf8f9893a55 Mon Sep 17 00:00:00 2001 From: let5sne Date: Fri, 24 Jul 2026 20:12:43 +0800 Subject: [PATCH] feat(agent): enforce tool policy --- .../dromara/aihr/agent/AihrAgentPolicy.java | 49 ++++++++++ .../aihr/agent/AihrAgentPolicyTest.java | 93 +++++++++++++++++++ 2 files changed, 142 insertions(+) create mode 100644 backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/agent/AihrAgentPolicy.java create mode 100644 backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/agent/AihrAgentPolicyTest.java diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/agent/AihrAgentPolicy.java b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/agent/AihrAgentPolicy.java new file mode 100644 index 00000000..87afce4b --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/agent/AihrAgentPolicy.java @@ -0,0 +1,49 @@ +package org.dromara.aihr.agent; + +import org.dromara.aihr.agent.AihrAgentDto.AgentPlan; +import org.dromara.aihr.agent.AihrAgentDto.Intent; +import org.dromara.aihr.agent.AihrAgentDto.Tool; +import org.dromara.aihr.knowledge.domain.AihrKnowledgePrincipal; +import org.dromara.common.core.exception.ServiceException; +import org.springframework.stereotype.Service; + +import java.util.Locale; + +@Service +public class AihrAgentPolicy { + + public void authorize(AihrKnowledgePrincipal principal, AgentPlan plan, boolean externalConsent) { + if (principal == null || principal.userId() == null) { + throw new ServiceException("登录身份缺失,请重新登录", 401); + } + if (plan == null || plan.intent() == null || plan.tool() == null) { + throw new ServiceException("Agent 工具计划无效", 403); + } + if ((plan.intent() == Intent.CAPTURE_FACT || plan.intent() == Intent.DRAFT_ACTION) + != plan.requiresConfirmation()) { + throw new ServiceException("写入类操作必须先确认", 403); + } + if ((plan.tool() == Tool.WEB_RESEARCH) != plan.requiresExternalConsent() + || plan.requiresExternalConsent() && !externalConsent) { + throw new ServiceException("全网查询需要用户明确同意", 403); + } + + boolean employee = hasRole(principal, "employee"); + boolean supervisor = hasRole(principal, "supervisor"); + boolean allowed = switch (plan.tool()) { + case NONE -> plan.intent() != Intent.DRAFT_ACTION || employee || supervisor; + case KNOWLEDGE_SEARCH, KNOWLEDGE_RESOURCE, MY_CURRENT_TASKS, MY_PRACTICE_SUMMARY, + CAPTURE_MEMORY_DRAFT, MEDIA_ANALYZE, WEB_RESEARCH -> employee || supervisor; + case TEAM_PRACTICE_SUMMARY -> supervisor; + }; + if (!allowed) { + throw new ServiceException("当前身份无权使用该 Agent 工具", 403); + } + } + + private static boolean hasRole(AihrKnowledgePrincipal principal, String role) { + return principal.roles().stream() + .map(value -> value == null ? "" : value.trim().toLowerCase(Locale.ROOT)) + .anyMatch(role::equals); + } +} diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/agent/AihrAgentPolicyTest.java b/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/agent/AihrAgentPolicyTest.java new file mode 100644 index 00000000..55acd205 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/agent/AihrAgentPolicyTest.java @@ -0,0 +1,93 @@ +package org.dromara.aihr.agent; + +import org.dromara.aihr.agent.AihrAgentDto.AgentPlan; +import org.dromara.aihr.knowledge.domain.AihrKnowledgePrincipal; +import org.dromara.common.core.exception.ServiceException; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import java.util.Set; + +import static org.dromara.aihr.agent.AihrAgentDto.Intent.CAPTURE_FACT; +import static org.dromara.aihr.agent.AihrAgentDto.Intent.LIVE_MY_WORK; +import static org.dromara.aihr.agent.AihrAgentDto.Intent.LIVE_TEAM_WORK; +import static org.dromara.aihr.agent.AihrAgentDto.Intent.WEB_RESEARCH; +import static org.dromara.aihr.agent.AihrAgentDto.ResponseStyle.DRAFT; +import static org.dromara.aihr.agent.AihrAgentDto.ResponseStyle.FACT; +import static org.dromara.aihr.agent.AihrAgentDto.ResponseStyle.WEB; +import static org.dromara.aihr.agent.AihrAgentDto.Tool.CAPTURE_MEMORY_DRAFT; +import static org.dromara.aihr.agent.AihrAgentDto.Tool.MY_CURRENT_TASKS; +import static org.dromara.aihr.agent.AihrAgentDto.Tool.TEAM_PRACTICE_SUMMARY; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@Tag("dev") +class AihrAgentPolicyTest { + + private final AihrAgentPolicy policy = new AihrAgentPolicy(); + + @Test + void employeeCanReadOwnTasksButCannotReadTeamSummary() { + assertDoesNotThrow(() -> policy.authorize(employee(), plan(LIVE_MY_WORK, MY_CURRENT_TASKS, false, false), false)); + assertThrows(ServiceException.class, + () -> policy.authorize(employee(), plan(LIVE_TEAM_WORK, TEAM_PRACTICE_SUMMARY, false, false), false)); + } + + @Test + void supervisorCanReadTeamSummary() { + assertDoesNotThrow(() -> + policy.authorize(supervisor(), plan(LIVE_TEAM_WORK, TEAM_PRACTICE_SUMMARY, false, false), false)); + } + + @Test + void candidateCannotReadEmployeeTasks() { + assertThrows(ServiceException.class, + () -> policy.authorize(candidate(), plan(LIVE_MY_WORK, MY_CURRENT_TASKS, false, false), false)); + } + + @Test + void writeLikePlanMustRemainConfirmationGated() { + assertDoesNotThrow(() -> + policy.authorize(employee(), plan(CAPTURE_FACT, CAPTURE_MEMORY_DRAFT, true, false), false)); + assertThrows(ServiceException.class, + () -> policy.authorize(employee(), plan(CAPTURE_FACT, CAPTURE_MEMORY_DRAFT, false, false), false)); + } + + @Test + void externalToolRequiresExplicitRequestConsent() { + AgentPlan web = new AgentPlan(WEB_RESEARCH, "查询公开信息", AihrAgentDto.Tool.WEB_RESEARCH, + false, true, WEB); + + assertThrows(ServiceException.class, () -> policy.authorize(employee(), web, false)); + assertDoesNotThrow(() -> policy.authorize(employee(), web, true)); + } + + @Test + void missingOrUnsupportedToolIsRejected() { + AgentPlan forged = new AgentPlan(LIVE_MY_WORK, "绕过工具注册", null, false, false, FACT); + + assertThrows(ServiceException.class, () -> policy.authorize(employee(), forged, false)); + } + + private static AgentPlan plan(AihrAgentDto.Intent intent, AihrAgentDto.Tool tool, + boolean confirmation, boolean externalConsent) { + return new AgentPlan(intent, "test", tool, confirmation, externalConsent, + confirmation ? DRAFT : FACT); + } + + private static AihrKnowledgePrincipal employee() { + return principal(Set.of("employee")); + } + + private static AihrKnowledgePrincipal supervisor() { + return principal(Set.of("employee", "supervisor")); + } + + private static AihrKnowledgePrincipal candidate() { + return principal(Set.of("candidate")); + } + + private static AihrKnowledgePrincipal principal(Set roles) { + return new AihrKnowledgePrincipal("000000", 1L, "app_user", "person-1", roles, Set.of("P1"), "app"); + } +}