feat(agent): enforce tool policy
This commit is contained in:
+49
@@ -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);
|
||||
}
|
||||
}
|
||||
+93
@@ -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<String> roles) {
|
||||
return new AihrKnowledgePrincipal("000000", 1L, "app_user", "person-1", roles, Set.of("P1"), "app");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user