@@ -604,18 +604,26 @@ public class GroupAuthzService implements IGroupAuthzService {
|
||||
// 角色授权不依赖组归属与组开通应用,故下方不再因无组归属而提前返回
|
||||
Set<Integer> roleModuleIds = collectEnabledRoleModuleIds(userId);
|
||||
|
||||
// 1. 用户所属的全部组 + 【强制包含接口传入的 groupId 上下文】
|
||||
// 场景修复:管理员在「组8的授权面板」给模块M打勾(写 sys_group_module_perm 组8的记录),
|
||||
// 然后同面板点「用户权限一览」UserPermissionTree?userId=X&groupId=8,必须读到组8的设置,
|
||||
// 即便用户X还没加入组8(预览模式、或界面只做"组视角下的权限查询");否则"本组设置"不生效。
|
||||
// 1. 归属组集合:区分「组上下文模式」和「用户全局模式」
|
||||
// A) 接口显式传了 groupId(= 管理员在"某个组的授权面板"点用户权限一览,是典型的组视角查询):
|
||||
// 以传入的 groupId 作为唯一 selfGroupId,仅沿「该组 -> 祖先 -> 第一个学校组(含)」链路判定;
|
||||
// 【关键】不再把用户真实加入的其他组(例如用户还属于学校组)并入判断。
|
||||
// 原因:如果允许用户归属的"学校组"作为独立 selfGroupId 参与本组显式判定,
|
||||
// 则子组未设置的模块会直接命中学校组自己的显式记录(permMap.get(学校组)!=null),
|
||||
// 导致"子组设置的覆盖限制"完全失效=父级所有模块都拿到,即你观察到的"本组继承没生效/还是父级全量"。
|
||||
// B) 未传 groupId(全局查用户权限/开放接口 modules 未传schId以外的其他上下文):
|
||||
// 正常取用户所有真实归属组并集。
|
||||
List<SysUserGroupRel> userRels = sysUserGroupRelRepository.findByUserId(userId);
|
||||
Set<Integer> selfGroupIds = new LinkedHashSet<>();
|
||||
if (groupId != null) {
|
||||
// 组上下文模式:只认传入的 groupId,不认其他真实归属组(避免越级命中显式记录)
|
||||
selfGroupIds.add(groupId);
|
||||
}
|
||||
for (SysUserGroupRel rel : userRels) {
|
||||
if (rel.getGroupId() != null) {
|
||||
selfGroupIds.add(rel.getGroupId());
|
||||
} else {
|
||||
// 用户全局模式:按真实归属组并集
|
||||
for (SysUserGroupRel rel : userRels) {
|
||||
if (rel.getGroupId() != null) {
|
||||
selfGroupIds.add(rel.getGroupId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -667,9 +675,11 @@ public class GroupAuthzService implements IGroupAuthzService {
|
||||
}
|
||||
Set<Integer> allModuleIds = new LinkedHashSet<>();
|
||||
Map<Integer, List<AppModule>> appModulesMap = new HashMap<>();
|
||||
Map<Integer, Integer> moduleToAppMap = new HashMap<>();
|
||||
for (AppModule m : allModules) {
|
||||
allModuleIds.add(m.getId());
|
||||
appModulesMap.computeIfAbsent(m.getAppId(), k -> new ArrayList<>()).add(m);
|
||||
moduleToAppMap.put(m.getId(), m.getAppId());
|
||||
}
|
||||
|
||||
// 6. 批量查询所有相关模块在所有相关组/祖先上的权限记录
|
||||
@@ -686,13 +696,26 @@ public class GroupAuthzService implements IGroupAuthzService {
|
||||
}
|
||||
}
|
||||
|
||||
// AI-FIX(应用级覆盖): 某组只要对该应用有任意显式模块配置,子孙/本组就只按该组配置判定,
|
||||
// 不再继承更上层祖先的同应用模块权限(避免「子组只勾了1个模块,却仍继承学校全部模块」)。
|
||||
Map<Integer, Set<Integer>> groupConfiguredApps = new HashMap<>();
|
||||
for (Map.Entry<Integer, Map<Integer, SysGroupModulePerm>> entry : modulePermMap.entrySet()) {
|
||||
Integer appId = moduleToAppMap.get(entry.getKey());
|
||||
if (appId == null) {
|
||||
continue;
|
||||
}
|
||||
for (Integer gid : entry.getValue().keySet()) {
|
||||
groupConfiguredApps.computeIfAbsent(gid, k -> new HashSet<>()).add(appId);
|
||||
}
|
||||
}
|
||||
|
||||
// 7. 判定每个模块是否有权限
|
||||
// AI-FIX(角色授权): 最终模块权限 = 组授权模块 ∪ 角色授权模块
|
||||
Set<Integer> permittedModuleIds = new LinkedHashSet<>();
|
||||
for (AppModule m : allModules) {
|
||||
if (roleModuleIds.contains(m.getId())
|
||||
|| hasUserModulePermission(m.getId(), m.getAppId(), selfGroupIds, chainMap, groupToApps,
|
||||
modulePermMap, allGroupMap)) {
|
||||
modulePermMap, allGroupMap, groupConfiguredApps)) {
|
||||
permittedModuleIds.add(m.getId());
|
||||
}
|
||||
}
|
||||
@@ -859,29 +882,57 @@ public class GroupAuthzService implements IGroupAuthzService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户维度权限判定:用户对指定模块是否有权限(两段式 + 学校截止 + 超管兜底(超管在入口短路,这里只处理普通用户))。
|
||||
* 就近原则:在 groupId 的祖先链上,找到离它最近的、对目标应用有显式模块配置的组(含自身)。
|
||||
*
|
||||
* <p>
|
||||
* 两段式(用户拍板:边界1选B):
|
||||
* ① 先查「本组」(归属组 selfGroupId)有没有 sys_group_module_perm
|
||||
* 记录(仅记录存在即=显式设置,无论inherit_flag值)→ 有 = 本组说了算,直接true,不再查祖先;
|
||||
* ② 本组无记录 = 没有显式设置 = 进入继承查找:沿祖先链向上找,直到第一个学校组(包含学校组本身,不跨学校到教育局/根),第一个「记录存在且
|
||||
* inherit_flag=true」的组即继承成功;
|
||||
* ③ 学校组之上的节点(区域/教育局/根)都不查找,避免跨学校继承到未开通的应用(用户新增边界3)。
|
||||
* <p>链首即自身({@link #ancestorChain} 由自身向根排列),天然「就近优先」;遍历时第一个
|
||||
* 「groupConfiguredApps 中包含该应用」的组即作为该应用在本组范围内的<b>唯一权限来源</b>返回。
|
||||
*
|
||||
* <p>
|
||||
* 其余不变:归属组或其祖先需已开通该模块所属应用(双保险,防越权)。
|
||||
* <p>学校截止:遇到学校组(无论是否已配置)即作为继承上限,不再向教育局/根越级,避免跨学校继承到未开通的应用。
|
||||
*
|
||||
* <p>找不到返回 null(该应用在此范围内没有任何组的显式配置,对应模块不许可)。
|
||||
*/
|
||||
private Integer findNearestAuthorityGroup(Integer groupId, List<Integer> chain,
|
||||
Integer appId, Map<Integer, Set<Integer>> groupConfiguredApps,
|
||||
Map<Integer, SysGroup> allGroupMap) {
|
||||
if (chain == null || chain.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
for (Integer gid : chain) {
|
||||
Set<Integer> apps = groupConfiguredApps.get(gid);
|
||||
if (apps != null && apps.contains(appId)) {
|
||||
return gid;
|
||||
}
|
||||
SysGroup g = allGroupMap.get(gid);
|
||||
if (g != null && g.getType() == GroupTypeEnum.SCHOOL_GROUP) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户维度权限判定:用户对指定模块是否有权限(就近原则 + 学校截止 + 角色授权并集 + 超管兜底)。
|
||||
*
|
||||
* <p><b>就近原则</b>(统一了「本组覆盖」与「最近祖先继承」两种情形,修复两类合并 bug):
|
||||
* 沿 selfGroupId 的祖先链(含自身)向上,找到「离它最近、且对 appId 有任意显式模块配置」的组作为
|
||||
* 该应用的<b>唯一权限来源</b>;该模块被许可,当且仅当这个来源组对它自身有显式 {@code sys_group_module_perm} 记录。
|
||||
* 这样:
|
||||
* ① 本组有配置(如英语组只勾模块8)→ 本组即来源,只返回本组勾选的模块,不再合并学校全部模块;
|
||||
* ② 本组无配置(如英语组为空)→ 取<b>最近</b>的有配置祖先(如教师组)为来源,不再越级到更远的学校组;
|
||||
* ③ 来源组的模块集合即最终集合,更上层同应用模块(如学校组多勾的模块8)一律不合并。
|
||||
*
|
||||
* <p><b>学校截止</b>:祖先链只查到第一个学校组(含)为止,不跨学校到教育局/根。
|
||||
*
|
||||
* <p><b>其余</b>:归属组需已开通该模块所属应用(双保险,防越权)。
|
||||
*/
|
||||
private boolean hasUserModulePermission(Integer moduleId, Integer appId,
|
||||
Set<Integer> selfGroupIds,
|
||||
Map<Integer, List<Integer>> chainMap,
|
||||
Map<Integer, Set<Integer>> groupToApps,
|
||||
Map<Integer, Map<Integer, SysGroupModulePerm>> modulePermMap,
|
||||
Map<Integer, SysGroup> allGroupMap) {
|
||||
Map<Integer, SysGroup> allGroupMap,
|
||||
Map<Integer, Set<Integer>> groupConfiguredApps) {
|
||||
Map<Integer, SysGroupModulePerm> permMap = modulePermMap.get(moduleId);
|
||||
if (permMap == null || permMap.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
for (Integer selfGroupId : selfGroupIds) {
|
||||
Set<Integer> openedApps = groupToApps.get(selfGroupId);
|
||||
if (openedApps == null || !openedApps.contains(appId)) {
|
||||
@@ -891,33 +942,14 @@ public class GroupAuthzService implements IGroupAuthzService {
|
||||
if (chain == null || chain.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
// 第一段:本组显式设置优先(记录存在=true,完全按本组来,不继承不向上)
|
||||
SysGroupModulePerm selfPerm = permMap.get(selfGroupId);
|
||||
if (selfPerm != null) {
|
||||
return true;
|
||||
}
|
||||
// 本组无记录 → 第二段:继承查找。
|
||||
// 如果「本组本身就是学校组」:学校组已经是继承上限,学校组之上(区域/教育局/根)不再允许查找,直接跳过本段。
|
||||
// 否则(本组是班级/年级/教师/学生等学校内子组):沿祖先向上,直到包含第一个学校组为止,出了学校组立刻break,不跨学校。
|
||||
SysGroup selfG = allGroupMap.get(selfGroupId);
|
||||
boolean selfIsSchool = selfG != null && selfG.getType() == GroupTypeEnum.SCHOOL_GROUP;
|
||||
if (selfIsSchool) {
|
||||
// 就近原则:取离本组最近、对该应用有显式配置的组作为唯一权限来源
|
||||
Integer authority = findNearestAuthorityGroup(selfGroupId, chain, appId, groupConfiguredApps, allGroupMap);
|
||||
if (authority == null) {
|
||||
continue;
|
||||
}
|
||||
for (Integer ancestorId : chain) {
|
||||
if (Objects.equals(ancestorId, selfGroupId)) {
|
||||
continue;
|
||||
}
|
||||
SysGroup g = allGroupMap.get(ancestorId);
|
||||
SysGroupModulePerm perm = permMap.get(ancestorId);
|
||||
boolean isSchool = g != null && g.getType() == GroupTypeEnum.SCHOOL_GROUP;
|
||||
if (perm != null && Boolean.TRUE.equals(perm.getInheritFlag())) {
|
||||
return true;
|
||||
}
|
||||
// 用户要求:向上查找只查到学校组这一层,包含学校组本身(上面已判断学校组的perm),之后不再跨学校
|
||||
if (isSchool) {
|
||||
break;
|
||||
}
|
||||
// 权限来源组对该模块有显式记录 → 许可;否则(来源组未勾此模块)不许可
|
||||
if (permMap != null && permMap.get(authority) != null) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
@@ -1135,6 +1167,7 @@ public class GroupAuthzService implements IGroupAuthzService {
|
||||
if (module == null) {
|
||||
return false;
|
||||
}
|
||||
Integer appId = module.getAppId();
|
||||
// AI-FIX(角色授权): 用户经「启用」角色获得该模块授权即通过,不要求组开通模块所属应用;禁用角色已在收集时过滤
|
||||
if (collectEnabledRoleModuleIds(userId).contains(moduleId)) {
|
||||
return true;
|
||||
@@ -1149,18 +1182,20 @@ public class GroupAuthzService implements IGroupAuthzService {
|
||||
for (SysGroup g : sysGroupRepository.findAll()) {
|
||||
allGroupMap.put(g.getId(), g);
|
||||
}
|
||||
// 3. 前置校验:所属组必须已开通模块所属应用(最终权限 = 继承的模块权限 ∩ 已开通应用);
|
||||
// 3. 前置校验:所属组必须已开通模块所属应用(最终权限 = 就近来源模块权限 ∩ 已开通应用);
|
||||
// 通过校验的组构建祖先链(含自身),并收集祖先链并集用于一次批量 IN 查询
|
||||
Map<Integer, List<Integer>> chainMap = new HashMap<>();
|
||||
Set<Integer> allChainIds = new LinkedHashSet<>();
|
||||
Set<Integer> selfGroupIds = new LinkedHashSet<>();
|
||||
for (SysUserGroupRel rel : userRels) {
|
||||
Integer groupId = rel.getGroupId();
|
||||
if (groupId == null || chainMap.containsKey(groupId)) {
|
||||
continue;
|
||||
}
|
||||
if (!sysGroupAppRelRepository.existsByGroupIdAndAppId(groupId, module.getAppId())) {
|
||||
if (!sysGroupAppRelRepository.existsByGroupIdAndAppId(groupId, appId)) {
|
||||
continue;
|
||||
}
|
||||
selfGroupIds.add(groupId);
|
||||
List<Integer> chain = ancestorChain(groupId, allGroupMap);
|
||||
chainMap.put(groupId, chain);
|
||||
allChainIds.addAll(chain);
|
||||
@@ -1168,30 +1203,38 @@ public class GroupAuthzService implements IGroupAuthzService {
|
||||
if (chainMap.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
// 4. 一次批量查询目标模块在所有祖先链节点上的权限记录
|
||||
Map<Integer, SysGroupModulePerm> permMap = new HashMap<>();
|
||||
List<SysGroupModulePerm> perms = sysGroupModulePermRepository.findByModuleIdAndGroupIdIn(moduleId,
|
||||
new ArrayList<>(allChainIds));
|
||||
for (SysGroupModulePerm perm : perms) {
|
||||
permMap.put(perm.getGroupId(), perm);
|
||||
// 4. 一次批量查询「该应用下所有模块」在全部祖先链节点上的权限记录
|
||||
// (要判定就近来源,需要知道链上哪些组对该应用有显式配置,不能只看单模块)
|
||||
List<AppModule> appModules = appModuleRepository.findByAppIdIn(Collections.singletonList(appId));
|
||||
Set<Integer> appModuleIds = new LinkedHashSet<>();
|
||||
for (AppModule m : appModules) {
|
||||
appModuleIds.add(m.getId());
|
||||
}
|
||||
if (permMap.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
// 5. 判定:自身显式勾选直接通过;祖先勾选且开启继承则子孙生效
|
||||
for (Map.Entry<Integer, List<Integer>> entry : chainMap.entrySet()) {
|
||||
Integer selfGroupId = entry.getKey();
|
||||
for (Integer ancestorId : entry.getValue()) {
|
||||
SysGroupModulePerm perm = permMap.get(ancestorId);
|
||||
if (perm == null) {
|
||||
Map<Integer, Map<Integer, SysGroupModulePerm>> modulePermMap = new HashMap<>();
|
||||
Map<Integer, Set<Integer>> groupConfiguredApps = new HashMap<>();
|
||||
if (!appModuleIds.isEmpty() && !allChainIds.isEmpty()) {
|
||||
List<SysGroupModulePerm> perms = sysGroupModulePermRepository.findByModuleIdInAndGroupIdIn(
|
||||
new ArrayList<>(appModuleIds), new ArrayList<>(allChainIds));
|
||||
for (SysGroupModulePerm perm : perms) {
|
||||
if (perm.getModuleId() == null || perm.getGroupId() == null) {
|
||||
continue;
|
||||
}
|
||||
if (Objects.equals(ancestorId, selfGroupId)) {
|
||||
return true;
|
||||
}
|
||||
if (Boolean.TRUE.equals(perm.getInheritFlag())) {
|
||||
return true;
|
||||
}
|
||||
modulePermMap.computeIfAbsent(perm.getModuleId(), k -> new HashMap<>())
|
||||
.put(perm.getGroupId(), perm);
|
||||
groupConfiguredApps.computeIfAbsent(perm.getGroupId(), k -> new HashSet<>()).add(appId);
|
||||
}
|
||||
}
|
||||
// 5. 就近原则判定:对每个归属组,取离它最近的、对该应用有显式配置的组为唯一权限来源;
|
||||
// 该来源组对目标模块有记录即通过(多个归属组取并集)。
|
||||
for (Integer selfGroupId : selfGroupIds) {
|
||||
List<Integer> chain = chainMap.get(selfGroupId);
|
||||
Integer authority = findNearestAuthorityGroup(selfGroupId, chain, appId, groupConfiguredApps, allGroupMap);
|
||||
if (authority == null) {
|
||||
continue;
|
||||
}
|
||||
Map<Integer, SysGroupModulePerm> pm = modulePermMap.get(moduleId);
|
||||
if (pm != null && pm.get(authority) != null) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
|
||||
Reference in New Issue
Block a user