build / build-push (push) Successful in 40s
- com.yunhai.agent,端口 8093,context-path /agent,制品 target/agent.jar;与 NoticeService 同构 - CAS 单点登录 + 三层角色权限(学生/教师/管理员/访客)+ 会话与工具调用审计落库 - DeepSeek 大模型编排 + 7 个业务模块工具(请假/公告/宿舍/活动/作业/资源库/教师发展) - 浏览器代执行协议(服务端挂起 → 前端用登录态代调 → toolResult 续跑)+ 调用失败深链兜底 - 登出修复:服务端真正 session.invalidate() + 前端两步登出 + logout-guard - 保留 projman 注入的 .gitea/workflows/build.yml 与 docker/Dockerfile;docker/application-prod.yml、run-app.sh 随工程入库
219 lines
10 KiB
Java
219 lines
10 KiB
Java
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 / 数据库)。
|
||
*
|
||
* <p>验证两件事:① 模型侧过滤(哪些工具可见);② 服务端强校验(越权直接拒绝执行)。
|
||
*/
|
||
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<ToolDef> 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<AgentRole> roles = resolver.resolve(u);
|
||
|
||
assertTrue(roles.contains(AgentRole.STUDENT), "当前身份是学生,应识别为学生");
|
||
assertFalse(roles.contains(AgentRole.TEACHER), "不应因账号另挂教师身份而获得教师能力");
|
||
assertFalse(roles.contains(AgentRole.ADMIN), "不应因账号另挂管理员身份而获得管理员能力");
|
||
|
||
List<ToolDef> 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<AgentRole> asTeacher = resolver.resolve(u);
|
||
assertTrue(asTeacher.contains(AgentRole.TEACHER));
|
||
assertFalse(asTeacher.contains(AgentRole.STUDENT));
|
||
List<ToolDef> 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<AgentRole> 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<AgentRole> roles = resolver.resolve(u);
|
||
assertTrue(roles.contains(AgentRole.STUDENT), "回退后应保留账号的学生身份");
|
||
assertTrue(roles.contains(AgentRole.TEACHER), "回退后应保留账号的教师身份");
|
||
}
|
||
|
||
@Test
|
||
void accountWithoutCurrentRoleStillResolvesByRoleIds() {
|
||
// 老口径:平台没下发 currentRole 时,仍按 roleIds 判定(不能变成访客)
|
||
Set<AgentRole> roles = resolver.resolve(user(300, 2));
|
||
assertTrue(roles.contains(AgentRole.TEACHER));
|
||
}
|
||
|
||
@Test
|
||
void studentSeesOnlyStudentSideTools() {
|
||
Set<AgentRole> roles = resolver.resolve(user(101, 1));
|
||
assertTrue(roles.contains(AgentRole.STUDENT), "roleId=1 应识别为学生");
|
||
|
||
List<ToolDef> 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<AgentRole> roles = resolver.resolve(user(102, 2));
|
||
assertTrue(roles.contains(AgentRole.TEACHER), "roleId=2 应识别为教师");
|
||
|
||
List<ToolDef> 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<AgentRole> roles = resolver.resolve(u);
|
||
assertTrue(roles.contains(AgentRole.ADMIN), "type=1 应识别为管理员");
|
||
assertTrue(roles.contains(AgentRole.TEACHER), "管理员默认兼有教师能力");
|
||
|
||
List<ToolDef> visible = registry.visibleTools(null, roles);
|
||
assertTrue(visible(visible, "leave.audit"));
|
||
assertTrue(visible(visible, "notice.publish"));
|
||
}
|
||
|
||
@Test
|
||
void guestSeesOnlyUnrestrictedTools() {
|
||
Set<AgentRole> roles = resolver.resolve(null);
|
||
assertTrue(roles.contains(AgentRole.GUEST));
|
||
|
||
List<ToolDef> 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());
|
||
}
|
||
}
|