package com.yunhai.agent; import com.yunhai.agent.auth.AgentRole; import com.yunhai.agent.auth.RoleResolver; import com.yunhai.agent.config.AgentSettings; import com.yunhai.agent.tool.CallerContext; import com.yunhai.agent.tool.ToolDef; import com.yunhai.agent.tool.ToolExecutor; import com.yunhai.agent.tool.ToolRegistry; import com.yunhai.agent.tool.ToolResult; import com.yunhai.platform.utils.model.BoaRoleInfo; import com.yunhai.platform.utils.model.BoaUser; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.web.client.RestTemplate; import java.util.Arrays; import java.util.Collections; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; /** * 「分学生、教师权限」的核心断言(纯单元,不依赖 Spring / SSO / 数据库)。 * *

验证两件事:① 模型侧过滤(哪些工具可见);② 服务端强校验(越权直接拒绝执行)。 */ class RolePermissionTest { private AgentSettings settings; private ToolRegistry registry; private RoleResolver resolver; @BeforeEach void setUp() { settings = new AgentSettings(); registry = new ToolRegistry(settings); registry.init(); resolver = new RoleResolver(settings); } /** 伪造一个指定角色号的平台用户(type=null 表示非超管) */ private BoaUser user(Integer id, Integer... roleIds) { BoaUser u = new BoaUser(); u.setId(id); u.setName("测试用户" + id); u.setAccount("test" + id); u.setRoleIds(Arrays.asList(roleIds)); return u; } /** 伪造一个带「当前身份」的平台用户 */ private BoaUser userWithCurrent(Integer id, Integer currentRoleId, String currentRoleName, Integer... allRoleIds) { BoaUser u = user(id, allRoleIds); if (currentRoleId != null) { BoaRoleInfo cur = new BoaRoleInfo(); cur.setRoleId(currentRoleId); cur.setRoleName(currentRoleName); cur.setSchId(3); cur.setSchName("测试学校"); u.setCurrentRole(cur); } return u; } private boolean visible(List tools, String name) { return tools.stream().anyMatch(t -> t.getName().equals(name)); } @Test void multiIdentityAccountUsesCurrentRoleOnly() { // 实测账号形态:10008 同时挂着 学生(1) + 教师(2) + 管理员(4),当前身份 = 学生 BoaUser u = userWithCurrent(10008, 1, "学生", 1, 2, 4); Set roles = resolver.resolve(u); assertTrue(roles.contains(AgentRole.STUDENT), "当前身份是学生,应识别为学生"); assertFalse(roles.contains(AgentRole.TEACHER), "不应因账号另挂教师身份而获得教师能力"); assertFalse(roles.contains(AgentRole.ADMIN), "不应因账号另挂管理员身份而获得管理员能力"); List visible = registry.visibleTools(null, roles); assertTrue(visible(visible, "homework.submit"), "学生当前身份应可用 homework.submit"); assertFalse(visible(visible, "notice.publish"), "学生当前身份不应看到 notice.publish"); assertFalse(visible(visible, "profession.myPoints"), "学生当前身份不应看到 profession.*"); } @Test void switchingCurrentRoleSwitchesCapabilities() { BoaUser u = userWithCurrent(10008, 1, "学生", 1, 2, 4); assertTrue(resolver.resolve(u).contains(AgentRole.STUDENT)); // 门户把当前身份切成教师 → 同一账号能力随之变化 u.getCurrentRole().setRoleId(2); u.getCurrentRole().setRoleName("教师"); Set asTeacher = resolver.resolve(u); assertTrue(asTeacher.contains(AgentRole.TEACHER)); assertFalse(asTeacher.contains(AgentRole.STUDENT)); List teacherTools = registry.visibleTools(null, asTeacher); assertTrue(visible(teacherTools, "notice.publish"), "教师当前身份应可用 notice.publish"); assertFalse(visible(teacherTools, "homework.submit"), "教师当前身份不应看到 homework.submit"); // 切成管理员 → 管理员 + 教师(admin-also-teacher 默认 true) u.getCurrentRole().setRoleId(4); u.getCurrentRole().setRoleName("管理员"); Set asAdmin = resolver.resolve(u); assertTrue(asAdmin.contains(AgentRole.ADMIN)); assertTrue(asAdmin.contains(AgentRole.TEACHER)); assertFalse(asAdmin.contains(AgentRole.STUDENT), "管理员当前身份不应含学生专属能力"); } @Test void unknownCurrentRoleFallsBackToAccountRoles() { // 当前身份号未在 agent.roles.* 里声明 → 回退按账号全部身份判定(避免误判成访客) BoaUser u = userWithCurrent(200, 999, "自定义角色", 1, 2); Set roles = resolver.resolve(u); assertTrue(roles.contains(AgentRole.STUDENT), "回退后应保留账号的学生身份"); assertTrue(roles.contains(AgentRole.TEACHER), "回退后应保留账号的教师身份"); } @Test void accountWithoutCurrentRoleStillResolvesByRoleIds() { // 老口径:平台没下发 currentRole 时,仍按 roleIds 判定(不能变成访客) Set roles = resolver.resolve(user(300, 2)); assertTrue(roles.contains(AgentRole.TEACHER)); } @Test void studentSeesOnlyStudentSideTools() { Set roles = resolver.resolve(user(101, 1)); assertTrue(roles.contains(AgentRole.STUDENT), "roleId=1 应识别为学生"); List visible = registry.visibleTools(null, roles); // 学生能做:交作业、报名活动、请假申请 assertTrue(visible(visible, "homework.submit"), "学生应可用 homework.submit"); assertTrue(visible(visible, "activity.signup"), "学生应可用 activity.signup"); assertTrue(visible(visible, "leave.submit"), "学生应可用 leave.submit"); // 学生不能做:审批、发布、专业发展、宿管管理 assertFalse(visible(visible, "leave.audit"), "学生不应看到 leave.audit"); assertFalse(visible(visible, "leave.batchPass"), "学生不应看到 leave.batchPass"); assertFalse(visible(visible, "notice.publish"), "学生不应看到 notice.publish"); assertFalse(visible(visible, "profession.myPoints"), "学生不应看到 profession.*"); assertFalse(visible(visible, "dorm.buildings"), "学生不应看到宿舍管理侧"); } @Test void teacherSeesStaffToolsButNotStudentHomework() { Set roles = resolver.resolve(user(102, 2)); assertTrue(roles.contains(AgentRole.TEACHER), "roleId=2 应识别为教师"); List visible = registry.visibleTools(null, roles); // 教师能做:审批、发布、专业发展 assertTrue(visible(visible, "leave.audit"), "教师应可用 leave.audit"); assertTrue(visible(visible, "notice.publish"), "教师应可用 notice.publish"); assertTrue(visible(visible, "profession.myPoints"), "教师应可用 profession.*"); assertTrue(visible(visible, "leave.stats"), "教师应可用 leave.stats"); // 教师不做:学生交作业、报名活动 assertFalse(visible(visible, "homework.submit"), "教师不应看到 homework.submit"); assertFalse(visible(visible, "activity.signup"), "教师不应看到 activity.signup"); } @Test void adminByTypeGetsStaffCapabilities() { BoaUser u = user(1); u.setType(1); // 平台超管口径 Set roles = resolver.resolve(u); assertTrue(roles.contains(AgentRole.ADMIN), "type=1 应识别为管理员"); assertTrue(roles.contains(AgentRole.TEACHER), "管理员默认兼有教师能力"); List visible = registry.visibleTools(null, roles); assertTrue(visible(visible, "leave.audit")); assertTrue(visible(visible, "notice.publish")); } @Test void guestSeesOnlyUnrestrictedTools() { Set roles = resolver.resolve(null); assertTrue(roles.contains(AgentRole.GUEST)); List visible = registry.visibleTools(null, roles); assertTrue(visible(visible, "notice.received"), "公共只读能力应对访客可见"); assertFalse(visible(visible, "leave.submit"), "访客不应看到请假申请"); assertFalse(visible(visible, "leave.audit"), "访客不应看到审批"); } @Test void executorRejectsUnauthorizedToolAtServerSide() { // 即便模型绕过过滤硬调用,服务端也必须拒绝 ToolExecutor executor = new ToolExecutor(settings, new RestTemplate()); ToolDef audit = registry.get("leave.audit"); assertTrue(audit != null && !audit.allowsAny(Collections.singleton(AgentRole.STUDENT)), "leave.audit 应限制学生使用"); CallerContext student = new CallerContext(); student.setRoles(new LinkedHashSet<>(Collections.singletonList(AgentRole.STUDENT))); ToolResult tr = executor.execute(audit, new LinkedHashMap<>(), student); assertFalse(tr.isSuccess(), "学生调用审批工具必须失败"); assertTrue(tr.getError() != null && tr.getError().contains("无权"), "拒绝理由应说明无权,实际:" + tr.getError()); } @Test void executorAllowsAuthorizedTool() { // 教师调用审批工具不会被角色闸门拦截(因未配置模块地址,会在网关层报"未配置地址",不是"无权") ToolExecutor executor = new ToolExecutor(settings, new RestTemplate()); ToolDef audit = registry.get("leave.audit"); CallerContext teacher = new CallerContext(); teacher.setRoles(new LinkedHashSet<>(Collections.singletonList(AgentRole.TEACHER))); ToolResult tr = executor.execute(audit, new LinkedHashMap<>(), teacher); assertTrue(tr.getError() == null || !tr.getError().contains("无权"), "教师调用不应被角色闸门拒绝,实际:" + tr.getError()); } }