学校应用开通硬闸门:单点前置闸门+应用列表/authorized-codes 收窄(D1 保超管 bypass、D2 收敛)
build / build-push (push) Successful in 54s
build / build-push (push) Successful in 54s
- GroupAuthzService: findUserModulesForApp 超管短路后插生效学校单点开通前置闸门;新增 findUserAuthorizedAppIds(userId,schoolId) 收窄重载 - AppCategoryService.listBoaAppType 改用收窄口径;authorized-codes 端点迁至 (userId,null) 走当前身份校兜底 - 测试: 新增 GroupAuthzServiceSchoolGateTest,适配 ModuleTree/Alias/BaseFilter/AuthorizedCodes mock 签名
This commit is contained in:
@@ -79,8 +79,9 @@ public class AppController {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询用户有权应用的 app_code 集合(消息跳转判权,C1)。<br>
|
* 查询用户有权应用的 app_code 集合(消息跳转判权,C1)。<br>
|
||||||
* 判权复用 {@code IGroupAuthzService.findUserAuthorizedAppIds}(组开通 ∪ 启用角色模块所属应用,
|
* 判权复用 {@code IGroupAuthzService.findUserAuthorizedAppIds(userId, null)}(2026-10-12 硬闸门 D2 收敛:
|
||||||
* 超级管理员返回全部),再映射为 app_code(空值行跳过)。只读,不透出 appSecret。<br>
|
* 走生效学校锚点单点开通集合,不沿祖先、角色不补充未开通应用,超级管理员返回全部),
|
||||||
|
* 再映射为 app_code(空值行跳过)。只读,不透出 appSecret。<br>
|
||||||
* 消费方(UserHome 消息代理)以「集合是否包含消息 jump_app」决定是否显示跳转入口;
|
* 消费方(UserHome 消息代理)以「集合是否包含消息 jump_app」决定是否显示跳转入口;
|
||||||
* 本端点不可达时消费方放行(不判权),绝不因判权通道故障批量隐藏入口。
|
* 本端点不可达时消费方放行(不判权),绝不因判权通道故障批量隐藏入口。
|
||||||
*
|
*
|
||||||
@@ -89,7 +90,7 @@ public class AppController {
|
|||||||
*/
|
*/
|
||||||
@GetMapping("authorized-codes")
|
@GetMapping("authorized-codes")
|
||||||
public List<String> authorizedCodes(@RequestParam("userId") Integer userId) {
|
public List<String> authorizedCodes(@RequestParam("userId") Integer userId) {
|
||||||
return applicationInfoService.listAppCodesByIds(groupAuthzService.findUserAuthorizedAppIds(userId));
|
return applicationInfoService.listAppCodesByIds(groupAuthzService.findUserAuthorizedAppIds(userId, null));
|
||||||
}
|
}
|
||||||
|
|
||||||
private ApplicationInfoVo toVo(ApplicationInfo info) {
|
private ApplicationInfoVo toVo(ApplicationInfo info) {
|
||||||
@@ -105,8 +106,9 @@ public class AppController {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询用户在指定客户端类型下有权访问的应用列表(按应用类型分组,已合并学校别名)。<br>
|
* 查询用户在指定客户端类型下有权访问的应用列表(按应用类型分组,已合并学校别名)。<br>
|
||||||
* 权限过滤复用 {@code IGroupAuthzService.findUserAuthorizedAppIds}(组开通 ∪ 启用角色模块所属应用),
|
* 权限过滤(2026-10-12 学校单点开通硬闸门)复用 {@code IGroupAuthzService.findUserAuthorizedAppIds(userId, schoolId)}
|
||||||
* 超级管理员返回全部;不透出 appSecret。<br>
|
* ——生效学校锚点单点开通的应用集合(不沿祖先、角色授权不再补充学校未开通的应用),超级管理员返回全部;
|
||||||
|
* 不透出 appSecret。<br>
|
||||||
* clientType 传枚举 key(2 的幂值,如 1=PC 2=移动App 4=系统模块…);非法/缺失按不限制端处理。<br>
|
* clientType 传枚举 key(2 的幂值,如 1=PC 2=移动App 4=系统模块…);非法/缺失按不限制端处理。<br>
|
||||||
* schoolId 可选:显式传入则按该校合并应用/类型别名(须为合法学校锚点且用户属该校,否则 code=5);
|
* schoolId 可选:显式传入则按该校合并应用/类型别名(须为合法学校锚点且用户属该校,否则 code=5);
|
||||||
* 不传则按「当前激活身份学校 → 主归属学校」兜底推导生效学校,均无则不合并(返回主表真名)。
|
* 不传则按「当前激活身份学校 → 主归属学校」兜底推导生效学校,均无则不合并(返回主表真名)。
|
||||||
|
|||||||
@@ -39,7 +39,9 @@ public interface IAppCategoryService {
|
|||||||
* 开放接口专用:按用户 + 客户端类型查询其有权访问的应用列表(按应用类型分组),
|
* 开放接口专用:按用户 + 客户端类型查询其有权访问的应用列表(按应用类型分组),
|
||||||
* 并按生效学校合并学校私有别名/图标/排序(2026-09-10)。<br>
|
* 并按生效学校合并学校私有别名/图标/排序(2026-09-10)。<br>
|
||||||
* 规则:<br>
|
* 规则:<br>
|
||||||
* ① 权限过滤:仅返回该用户有权访问的应用(复用 {@code IGroupAuthzService.findUserAuthorizedAppIds});<br>
|
* ① 权限过滤(2026-10-12 学校单点开通硬闸门):仅返回<b>生效学校锚点单点开通</b>的应用
|
||||||
|
* (复用 {@code IGroupAuthzService.findUserAuthorizedAppIds(userId, schoolId)} 收窄口径,
|
||||||
|
* 不沿祖先链、角色授权不补充未开通应用;超管保留全量 bypass);<br>
|
||||||
* ② 客户端过滤:仅返回支持该客户端类型的应用({@code app.clients} 位掩码与 clientType 有交集;clients 为空视为不限端);<br>
|
* ② 客户端过滤:仅返回支持该客户端类型的应用({@code app.clients} 位掩码与 clientType 有交集;clients 为空视为不限端);<br>
|
||||||
* ③ 不透出 appSecret;<br>
|
* ③ 不透出 appSecret;<br>
|
||||||
* ④ 学校别名:名称/图标/排序按 sys_school_app_override / sys_school_category_override 覆盖,
|
* ④ 学校别名:名称/图标/排序按 sys_school_app_override / sys_school_category_override 覆盖,
|
||||||
|
|||||||
@@ -151,6 +151,10 @@ public interface IGroupAuthzService {
|
|||||||
* (子树内无归属组则仅靠角色授权,不用 schId 冒充本组);schId 为空=取用户全部真实归属组
|
* (子树内无归属组则仅靠角色授权,不用 schId 冒充本组);schId 为空=取用户全部真实归属组
|
||||||
* (与权限一览「用户全局模式」一致)。<br>
|
* (与权限一览「用户全局模式」一致)。<br>
|
||||||
* 超级管理员:不做组/继承判定,直接返回该应用下全部模块(但仍只返回该应用,不会跨应用返回)。<br>
|
* 超级管理员:不做组/继承判定,直接返回该应用下全部模块(但仍只返回该应用,不会跨应用返回)。<br>
|
||||||
|
* 学校单点开通硬闸门(2026-10-12):超管短路之后、归属组解析之前,先判定目标应用是否在
|
||||||
|
* <b>生效学校锚点自身</b>的开通集合({@link #findAuthorizedAppIds(Integer)} 口径,不沿祖先、不并角色);
|
||||||
|
* 未开通直接返回空列表——角色授权与祖先组开通均不得绕过。生效锚点=显式 schId(过 assertSchoolId)
|
||||||
|
* → 当前激活身份学校 → 主归属组向上最近锚点;均解析不到=空。<br>
|
||||||
* 角色授权并集:最终权限 = 组授权模块 ∪ 启用角色下属于该应用的模块(逻辑与权限一览保持一致)。<br>
|
* 角色授权并集:最终权限 = 组授权模块 ∪ 启用角色下属于该应用的模块(逻辑与权限一览保持一致)。<br>
|
||||||
* <br>
|
* <br>
|
||||||
* 树形语义(2026-09-15 定稿):顶层只放真根(parentId 为空),兄弟按 customId 升序,叶子 children 为空列表;
|
* 树形语义(2026-09-15 定稿):顶层只放真根(parentId 为空),兄弟按 customId 升序,叶子 children 为空列表;
|
||||||
@@ -209,13 +213,31 @@ public interface IGroupAuthzService {
|
|||||||
void setGroupModulePerms(List<GroupModuleAuthzItem> items);
|
void setGroupModulePerms(List<GroupModuleAuthzItem> items);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询用户有权访问的应用ID集合(oapi 开放接口 getUserApp 权限过滤用)。<br>
|
* 查询用户有权访问的应用ID集合(历史全局口径:组开通沿祖先 ∪ 启用角色模块所属应用,超管全部)。<br>
|
||||||
* 规则与 {@link #hasModulePermission(Integer, Integer)} 对齐:<br>
|
* 规则与 {@link #hasModulePermission(Integer, Integer)} 对齐:<br>
|
||||||
* - 超级管理员:全部应用;<br>
|
* - 超级管理员:全部应用;<br>
|
||||||
* - 普通用户:用户所属组(沿祖先链)开通的应用 ∪ 启用角色模块所属应用。<br>
|
* - 普通用户:用户所属组(沿祖先链)开通的应用 ∪ 启用角色模块所属应用。<br>
|
||||||
|
* <br>
|
||||||
|
* ⚠️ 2026-10-12 学校单点开通硬闸门后,用户端应用可见性(门户列表 / authorized-codes 判权)一律走学校收窄重载
|
||||||
|
* {@link #findUserAuthorizedAppIds(Integer, Integer)};本方法现仅由该重载的超管分支内部委托,
|
||||||
|
* 不再作为对外应用可见性口径。
|
||||||
*
|
*
|
||||||
* @param userId 用户ID
|
* @param userId 用户ID
|
||||||
* @return 有权应用ID列表(无则空列表)
|
* @return 有权应用ID列表(无则空列表)
|
||||||
*/
|
*/
|
||||||
List<Integer> findUserAuthorizedAppIds(Integer userId);
|
List<Integer> findUserAuthorizedAppIds(Integer userId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按生效学校收窄的用户有权应用集合(2026-10-12 学校单点开通硬闸门,门户应用列表链口径)。<br>
|
||||||
|
* 语义 = <b>生效学校锚点单点开通的应用集合</b>(sys_group_app_rel 本组,不沿祖先、不并角色模块):<br>
|
||||||
|
* - 超级管理员:保留 bypass,返回全部应用(同无学校版口径);<br>
|
||||||
|
* - schoolId 非空:过 assertSchoolId 锚点校验后按该校自身单点判定;<br>
|
||||||
|
* - schoolId 为空:按「当前激活身份学校 → 主归属组向上最近锚点(学校/区域/根)」兜底,
|
||||||
|
* 区域/根锚点按该锚点自身单点判定;均解析不到(无学校语境)→ 空列表。
|
||||||
|
*
|
||||||
|
* @param userId 用户ID(必填)
|
||||||
|
* @param schoolId 学校上下文(可选;null 走身份/主归属兜底)
|
||||||
|
* @return 生效学校单点开通的应用ID列表(无则空列表)
|
||||||
|
*/
|
||||||
|
List<Integer> findUserAuthorizedAppIds(Integer userId, Integer schoolId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -198,7 +198,10 @@ public class AppCategoryService implements IAppCategoryService {
|
|||||||
public List<BoaAppType> listBoaAppType(Integer userId, ClientEnum clientType, Integer schoolId) {
|
public List<BoaAppType> listBoaAppType(Integer userId, ClientEnum clientType, Integer schoolId) {
|
||||||
// 生效学校先行解析:非法锚点/非所属学校直接抛 DException(异常不入缓存),null=无学校语境不合并
|
// 生效学校先行解析:非法锚点/非所属学校直接抛 DException(异常不入缓存),null=无学校语境不合并
|
||||||
Integer effectiveSchool = resolveEffectiveSchool(userId, schoolId);
|
Integer effectiveSchool = resolveEffectiveSchool(userId, schoolId);
|
||||||
Set<Integer> authorizedAppIds = new HashSet<>(groupAuthzService.findUserAuthorizedAppIds(userId));
|
// 权限过滤走学校单点开通收窄口径(2026-10-12 硬闸门):生效锚点自身开通的应用集合,
|
||||||
|
// 角色授权不再补充「学校未开通」的应用;锚点解析不到(无学校语境)= 空集合。
|
||||||
|
Set<Integer> authorizedAppIds =
|
||||||
|
new HashSet<>(groupAuthzService.findUserAuthorizedAppIds(userId, effectiveSchool));
|
||||||
List<AppCategory> categories = appCategoryRepository.findAllByOrderBySortNoAsc();
|
List<AppCategory> categories = appCategoryRepository.findAllByOrderBySortNoAsc();
|
||||||
List<ApplicationInfo> apps = applicationInfoRepository.findAll();
|
List<ApplicationInfo> apps = applicationInfoRepository.findAll();
|
||||||
apps.sort(Comparator.comparing(ApplicationInfo::getSortNo, Comparator.nullsLast(Integer::compareTo))
|
apps.sort(Comparator.comparing(ApplicationInfo::getSortNo, Comparator.nullsLast(Integer::compareTo))
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.yunhai.userbase.service.impl;
|
|||||||
|
|
||||||
import com.yunhai.platform.utils.exception.DException;
|
import com.yunhai.platform.utils.exception.DException;
|
||||||
import com.yunhai.platform.utils.model.BoaModule;
|
import com.yunhai.platform.utils.model.BoaModule;
|
||||||
|
import com.yunhai.platform.utils.model.BoaRoleInfo;
|
||||||
import com.yunhai.userbase.entity.*;
|
import com.yunhai.userbase.entity.*;
|
||||||
import com.yunhai.userbase.enums.GroupTypeEnum;
|
import com.yunhai.userbase.enums.GroupTypeEnum;
|
||||||
import com.yunhai.platform.utils.enums.IEnum;
|
import com.yunhai.platform.utils.enums.IEnum;
|
||||||
@@ -127,6 +128,10 @@ public class GroupAuthzService implements IGroupAuthzService {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private ISysGroupService sysGroupService;
|
private ISysGroupService sysGroupService;
|
||||||
|
|
||||||
|
/** 学校单点开通闸门的生效锚点解析(当前激活身份校兜底,与 AppCategoryService.resolveEffectiveSchool 同口径)。 */
|
||||||
|
@Autowired
|
||||||
|
private IdentityService identityService;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private ApplicationEventPublisher eventPublisher;
|
private ApplicationEventPublisher eventPublisher;
|
||||||
|
|
||||||
@@ -558,6 +563,12 @@ public class GroupAuthzService implements IGroupAuthzService {
|
|||||||
List<AppModule> allModules = appModuleRepository.findByAppIdIn(Collections.singletonList(appId));
|
List<AppModule> allModules = appModuleRepository.findByAppIdIn(Collections.singletonList(appId));
|
||||||
return assembleModuleTree(allModules);
|
return assembleModuleTree(allModules);
|
||||||
}
|
}
|
||||||
|
// 2.5 学校单点开通硬闸门(2026-10-12):生效锚点(显式 schId,或按「当前身份校→主归属校」兜底)
|
||||||
|
// 未在自身 sys_group_app_rel 开通该应用 → 直接空,角色授权与祖先组开通均不得绕过。
|
||||||
|
// 区域/根锚点按该锚点自身单点判定(不沿祖先);解析不到锚点=无学校语境=无应用。
|
||||||
|
if (!isAppOpenedAtEffectiveSchool(appId, schId, userId)) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
// 3. 归属组集合解析(与权限一览 findUserPermissionTree 语义对齐,修复"只返回学校节点"的 bug):
|
// 3. 归属组集合解析(与权限一览 findUserPermissionTree 语义对齐,修复"只返回学校节点"的 bug):
|
||||||
// - schId 非空:只取用户真实归属组中「位于该学校子树内」的组(含直接挂学校组本身),
|
// - schId 非空:只取用户真实归属组中「位于该学校子树内」的组(含直接挂学校组本身),
|
||||||
// 每个组独立建链向上判定;子树内无任何归属组 → 保留空 selfGroupIds(方案A:仅靠角色授权拿模块,
|
// 每个组独立建链向上判定;子树内无任何归属组 → 保留空 selfGroupIds(方案A:仅靠角色授权拿模块,
|
||||||
@@ -653,6 +664,41 @@ public class GroupAuthzService implements IGroupAuthzService {
|
|||||||
return assembleModuleTreeById(permittedModuleIds, buildAppModuleMap(appModules));
|
return assembleModuleTreeById(permittedModuleIds, buildAppModuleMap(appModules));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 学校单点开通硬闸门判定:应用是否落在「生效锚点」自身的开通集合内。<br>
|
||||||
|
* 闸门查询走 {@link #findAuthorizedAppIds(Integer)}(本组单点、不沿祖先、不并角色),
|
||||||
|
* 区域/根锚点同样按该锚点自身判定;显式 schId 先过 {@code assertSchoolId} 防误传非法 id。
|
||||||
|
*/
|
||||||
|
private boolean isAppOpenedAtEffectiveSchool(Integer appId, Integer schId, Integer userId) {
|
||||||
|
Integer anchor = resolveEffectiveSchoolAnchor(schId, userId);
|
||||||
|
if (anchor == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return findAuthorizedAppIds(anchor).contains(appId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生效学校锚点解析(与 {@code AppCategoryService.resolveEffectiveSchool} 同口径的只读版):<br>
|
||||||
|
* ① 显式 schId → assertSchoolId 锚点校验后直接使用(不校验用户归属,越权收窄由闸门自身完成);<br>
|
||||||
|
* ② null → 当前激活身份学校(sys_user_current_role);<br>
|
||||||
|
* ③ 仍无 → 主归属组向上最近学校/区域/根锚点;均无(平台账号无组关系)→ null(无学校语境)。
|
||||||
|
*/
|
||||||
|
private Integer resolveEffectiveSchoolAnchor(Integer schId, Integer userId) {
|
||||||
|
if (schId != null) {
|
||||||
|
sysGroupService.assertSchoolId(schId);
|
||||||
|
return schId;
|
||||||
|
}
|
||||||
|
BoaRoleInfo current = identityService.getCurrent(userId);
|
||||||
|
if (current != null && current.getSchId() != null) {
|
||||||
|
return current.getSchId();
|
||||||
|
}
|
||||||
|
SysUserGroupRel main = mainGroupResolver.resolve(userId).orElse(null);
|
||||||
|
if (main == null || main.getGroupId() == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return sysGroupService.resolveSchoolId(main.getGroupId());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 收集用户启用角色下=且属于目标应用 appId 的模块ID集合(角色授权并集专用的"应用过滤版")。<br>
|
* 收集用户启用角色下=且属于目标应用 appId 的模块ID集合(角色授权并集专用的"应用过滤版")。<br>
|
||||||
* 与 {@link #collectEnabledRoleModuleIds(Integer)} 的区别:后者返回全部角色模块;
|
* 与 {@link #collectEnabledRoleModuleIds(Integer)} 的区别:后者返回全部角色模块;
|
||||||
@@ -1986,4 +2032,24 @@ public class GroupAuthzService implements IGroupAuthzService {
|
|||||||
}
|
}
|
||||||
return new ArrayList<>(appIds);
|
return new ArrayList<>(appIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Integer> findUserAuthorizedAppIds(Integer userId, Integer schoolId) {
|
||||||
|
Objects.requireNonNull(userId, "用户ID不能为空");
|
||||||
|
SysUser user = sysUserRepository.findById(userId).orElse(null);
|
||||||
|
if (user == null) {
|
||||||
|
throw new DException("用户不存在");
|
||||||
|
}
|
||||||
|
// 超级管理员:保留 bypass(D1 默认,基石账号平台级不绑定单一学校),口径与无学校版一致
|
||||||
|
if (isSuperAdmin(user.getType(), userId)) {
|
||||||
|
return findUserAuthorizedAppIds(userId);
|
||||||
|
}
|
||||||
|
// 学校单点开通口径(2026-10-12 硬闸门):生效锚点自身开通的应用集合,
|
||||||
|
// 不沿祖先链、不并入角色模块所属应用(角色授权不再补充「学校未开通」的应用)。
|
||||||
|
Integer anchor = resolveEffectiveSchoolAnchor(schoolId, userId);
|
||||||
|
if (anchor == null) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
return findAuthorizedAppIds(anchor);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-7
@@ -19,9 +19,9 @@ import static org.mockito.Mockito.when;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* /oapi/app/authorized-codes 判权端点单测(消息跳转收口 C1)。<br>
|
* /oapi/app/authorized-codes 判权端点单测(消息跳转收口 C1)。<br>
|
||||||
* 控制器薄委托:判权来源 {@code IGroupAuthzService.findUserAuthorizedAppIds}(超管返回全部 /
|
* 控制器薄委托:判权来源收窄重载 {@code IGroupAuthzService.findUserAuthorizedAppIds(userId, null)}
|
||||||
* 普通用户返回子集 / 空权限返回空),数据面 {@code listAppCodesByIds} 映射 app_code
|
* (2026-10-12 硬闸门 D2 收敛,走当前身份校兜底;超管返回全部 / 普通用户返回子集 / 空权限返回空),
|
||||||
* (空值跳过/去重语义见 ApplicationInfoLookupTest)。
|
* 数据面 {@code listAppCodesByIds} 映射 app_code(空值跳过/去重语义见 ApplicationInfoLookupTest)。
|
||||||
*/
|
*/
|
||||||
@DisplayName("AppController.authorizedCodes - 有权 app_code 集合")
|
@DisplayName("AppController.authorizedCodes - 有权 app_code 集合")
|
||||||
class AppControllerAuthorizedCodesTest {
|
class AppControllerAuthorizedCodesTest {
|
||||||
@@ -42,7 +42,7 @@ class AppControllerAuthorizedCodesTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void 超管_返回全部应用code() {
|
void 超管_返回全部应用code() {
|
||||||
when(groupAuthzService.findUserAuthorizedAppIds(10006)).thenReturn(Arrays.asList(100011, 100013, 100015));
|
when(groupAuthzService.findUserAuthorizedAppIds(10006, null)).thenReturn(Arrays.asList(100011, 100013, 100015));
|
||||||
when(applicationInfoService.listAppCodesByIds(Arrays.asList(100011, 100013, 100015)))
|
when(applicationInfoService.listAppCodesByIds(Arrays.asList(100011, 100013, 100015)))
|
||||||
.thenReturn(Arrays.asList("flow-center", "notice", "userhome"));
|
.thenReturn(Arrays.asList("flow-center", "notice", "userhome"));
|
||||||
|
|
||||||
@@ -54,7 +54,7 @@ class AppControllerAuthorizedCodesTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void 普通用户_仅返回有权子集() {
|
void 普通用户_仅返回有权子集() {
|
||||||
when(groupAuthzService.findUserAuthorizedAppIds(201)).thenReturn(Collections.singletonList(100013));
|
when(groupAuthzService.findUserAuthorizedAppIds(201, null)).thenReturn(Collections.singletonList(100013));
|
||||||
when(applicationInfoService.listAppCodesByIds(Collections.singletonList(100013)))
|
when(applicationInfoService.listAppCodesByIds(Collections.singletonList(100013)))
|
||||||
.thenReturn(Collections.singletonList("notice"));
|
.thenReturn(Collections.singletonList("notice"));
|
||||||
|
|
||||||
@@ -63,12 +63,12 @@ class AppControllerAuthorizedCodesTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void 空权限_返回空集合() {
|
void 空权限_返回空集合() {
|
||||||
when(groupAuthzService.findUserAuthorizedAppIds(202)).thenReturn(Collections.emptyList());
|
when(groupAuthzService.findUserAuthorizedAppIds(202, null)).thenReturn(Collections.emptyList());
|
||||||
when(applicationInfoService.listAppCodesByIds(Collections.emptyList()))
|
when(applicationInfoService.listAppCodesByIds(Collections.emptyList()))
|
||||||
.thenReturn(Collections.emptyList());
|
.thenReturn(Collections.emptyList());
|
||||||
|
|
||||||
assertEquals(Collections.emptyList(), controller.authorizedCodes(202));
|
assertEquals(Collections.emptyList(), controller.authorizedCodes(202));
|
||||||
verify(groupAuthzService).findUserAuthorizedAppIds(202);
|
verify(groupAuthzService).findUserAuthorizedAppIds(202, null);
|
||||||
verify(applicationInfoService).listAppCodesByIds(Collections.emptyList());
|
verify(applicationInfoService).listAppCodesByIds(Collections.emptyList());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-4
@@ -91,8 +91,8 @@ class AppCategoryServiceBaseAppFilterTest {
|
|||||||
app(2, 100001, "BASE", "用户之家"),
|
app(2, 100001, "BASE", "用户之家"),
|
||||||
app(3, 100002, "BUSINESS", "通知公告"));
|
app(3, 100002, "BUSINESS", "通知公告"));
|
||||||
when(applicationInfoRepository.findAll()).thenReturn(apps);
|
when(applicationInfoRepository.findAll()).thenReturn(apps);
|
||||||
// 超管分支:授权集合为全量(基石也在内)——正是 bug1 的触发条件
|
// 超管分支:授权集合为全量(基石也在内)——正是 bug1 的触发条件(收窄重载 null 锚点=超管 bypass)
|
||||||
when(groupAuthzService.findUserAuthorizedAppIds(1)).thenReturn(Arrays.asList(1, 2, 3));
|
when(groupAuthzService.findUserAuthorizedAppIds(1, null)).thenReturn(Arrays.asList(1, 2, 3));
|
||||||
|
|
||||||
List<BoaAppType> result = appCategoryService.listBoaAppType(1, null, null);
|
List<BoaAppType> result = appCategoryService.listBoaAppType(1, null, null);
|
||||||
|
|
||||||
@@ -109,7 +109,7 @@ class AppCategoryServiceBaseAppFilterTest {
|
|||||||
when(applicationInfoRepository.findAll()).thenReturn(Arrays.asList(
|
when(applicationInfoRepository.findAll()).thenReturn(Arrays.asList(
|
||||||
app(3, 100002, "BUSINESS", "通知公告"),
|
app(3, 100002, "BUSINESS", "通知公告"),
|
||||||
app(4, null, null, "老应用")));
|
app(4, null, null, "老应用")));
|
||||||
when(groupAuthzService.findUserAuthorizedAppIds(1)).thenReturn(Arrays.asList(3, 4));
|
when(groupAuthzService.findUserAuthorizedAppIds(1, null)).thenReturn(Arrays.asList(3, 4));
|
||||||
|
|
||||||
List<BoaApp> kept = appCategoryService.listBoaAppType(1, null, null).get(0).getApps();
|
List<BoaApp> kept = appCategoryService.listBoaAppType(1, null, null).get(0).getApps();
|
||||||
|
|
||||||
@@ -123,7 +123,7 @@ class AppCategoryServiceBaseAppFilterTest {
|
|||||||
void unauthorizedBaseApp_stillNoneKept() {
|
void unauthorizedBaseApp_stillNoneKept() {
|
||||||
when(applicationInfoRepository.findAll())
|
when(applicationInfoRepository.findAll())
|
||||||
.thenReturn(new ArrayList<>(Collections.singletonList(app(2, 100001, "BASE", "用户之家"))));
|
.thenReturn(new ArrayList<>(Collections.singletonList(app(2, 100001, "BASE", "用户之家"))));
|
||||||
when(groupAuthzService.findUserAuthorizedAppIds(1)).thenReturn(Collections.singletonList(2));
|
when(groupAuthzService.findUserAuthorizedAppIds(1, null)).thenReturn(Collections.singletonList(2));
|
||||||
|
|
||||||
assertEquals(0, appCategoryService.listBoaAppType(1, null, null).get(0).getApps().size());
|
assertEquals(0, appCategoryService.listBoaAppType(1, null, null).get(0).getApps().size());
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-1
@@ -31,6 +31,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
|
|||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.ArgumentMatchers.anyList;
|
import static org.mockito.ArgumentMatchers.anyList;
|
||||||
import static org.mockito.ArgumentMatchers.anyMap;
|
import static org.mockito.ArgumentMatchers.anyMap;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
import static org.mockito.Mockito.doThrow;
|
import static org.mockito.Mockito.doThrow;
|
||||||
import static org.mockito.Mockito.never;
|
import static org.mockito.Mockito.never;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
@@ -93,7 +94,8 @@ class AppCategoryServiceSchoolAliasTest {
|
|||||||
app.setName("通知公告");
|
app.setName("通知公告");
|
||||||
app.setSortNo(1);
|
app.setSortNo(1);
|
||||||
when(applicationInfoRepository.findAll()).thenReturn(Collections.singletonList(app));
|
when(applicationInfoRepository.findAll()).thenReturn(Collections.singletonList(app));
|
||||||
when(groupAuthzService.findUserAuthorizedAppIds(1)).thenReturn(Collections.singletonList(100));
|
// 权限集合改用学校收窄重载(2026-10-12 硬闸门);本测试只断言生效学校解析,schoolId 传 null 覆盖
|
||||||
|
when(groupAuthzService.findUserAuthorizedAppIds(eq(1), any())).thenReturn(Collections.singletonList(100));
|
||||||
// 合并口透传第一参数(本测试只断言入参,合并行为见 SchoolAppOverrideServiceApplyAliasTest)
|
// 合并口透传第一参数(本测试只断言入参,合并行为见 SchoolAppOverrideServiceApplyAliasTest)
|
||||||
when(schoolAppOverrideService.applyAlias(anyList(), anyMap(), any()))
|
when(schoolAppOverrideService.applyAlias(anyList(), anyMap(), any()))
|
||||||
.thenAnswer(inv -> inv.getArgument(0));
|
.thenAnswer(inv -> inv.getArgument(0));
|
||||||
|
|||||||
+22
-1
@@ -98,6 +98,8 @@ class GroupAuthzServiceModuleTreeTest {
|
|||||||
private com.yunhai.userbase.service.resolve.MainGroupResolver mainGroupResolver;
|
private com.yunhai.userbase.service.resolve.MainGroupResolver mainGroupResolver;
|
||||||
@Mock
|
@Mock
|
||||||
private com.yunhai.userbase.service.ISysGroupService sysGroupService;
|
private com.yunhai.userbase.service.ISysGroupService sysGroupService;
|
||||||
|
@Mock
|
||||||
|
private IdentityService identityService;
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setUp() {
|
void setUp() {
|
||||||
@@ -240,6 +242,10 @@ class GroupAuthzServiceModuleTreeTest {
|
|||||||
when(sysUserGroupRelRepository.findByUserId(2))
|
when(sysUserGroupRelRepository.findByUserId(2))
|
||||||
.thenReturn(Collections.singletonList(rel(2, 16)));
|
.thenReturn(Collections.singletonList(rel(2, 16)));
|
||||||
initGroups();
|
initGroups();
|
||||||
|
// 学校单点开通闸门:findAuthorizedAppIds(16) 走 findByGroupId 单点查询
|
||||||
|
when(sysGroupRepository.existsById(16)).thenReturn(true);
|
||||||
|
when(sysGroupAppRelRepository.findByGroupId(16))
|
||||||
|
.thenReturn(Collections.singletonList(appRel(16, APP_ID)));
|
||||||
when(sysGroupAppRelRepository.findByGroupIdIn(anyList())).thenAnswer(inv ->
|
when(sysGroupAppRelRepository.findByGroupIdIn(anyList())).thenAnswer(inv ->
|
||||||
((List<Integer>) inv.getArgument(0)).contains(16)
|
((List<Integer>) inv.getArgument(0)).contains(16)
|
||||||
? Collections.singletonList(appRel(16, APP_ID)) : Collections.emptyList());
|
? Collections.singletonList(appRel(16, APP_ID)) : Collections.emptyList());
|
||||||
@@ -270,6 +276,10 @@ class GroupAuthzServiceModuleTreeTest {
|
|||||||
when(sysUserGroupRelRepository.findByUserId(3))
|
when(sysUserGroupRelRepository.findByUserId(3))
|
||||||
.thenReturn(Collections.singletonList(rel(3, 16)));
|
.thenReturn(Collections.singletonList(rel(3, 16)));
|
||||||
initGroups();
|
initGroups();
|
||||||
|
// 学校单点开通闸门:findAuthorizedAppIds(16) 走 findByGroupId 单点查询
|
||||||
|
when(sysGroupRepository.existsById(16)).thenReturn(true);
|
||||||
|
when(sysGroupAppRelRepository.findByGroupId(16))
|
||||||
|
.thenReturn(Collections.singletonList(appRel(16, APP_ID)));
|
||||||
when(sysGroupAppRelRepository.findByGroupIdIn(anyList())).thenAnswer(inv ->
|
when(sysGroupAppRelRepository.findByGroupIdIn(anyList())).thenAnswer(inv ->
|
||||||
((List<Integer>) inv.getArgument(0)).contains(16)
|
((List<Integer>) inv.getArgument(0)).contains(16)
|
||||||
? Collections.singletonList(appRel(16, APP_ID)) : Collections.emptyList());
|
? Collections.singletonList(appRel(16, APP_ID)) : Collections.emptyList());
|
||||||
@@ -313,6 +323,16 @@ class GroupAuthzServiceModuleTreeTest {
|
|||||||
when(sysUserGroupRelRepository.findByUserId(4))
|
when(sysUserGroupRelRepository.findByUserId(4))
|
||||||
.thenReturn(Arrays.asList(rel(4, 29), rel(4, 40)));
|
.thenReturn(Arrays.asList(rel(4, 29), rel(4, 40)));
|
||||||
initTwoSchoolGroups();
|
initTwoSchoolGroups();
|
||||||
|
// 学校单点开通闸门:显式上下文 16/28 与 null 上下文(主归属组 29→学校 16 兜底)均须放行
|
||||||
|
when(sysGroupRepository.existsById(16)).thenReturn(true);
|
||||||
|
when(sysGroupRepository.existsById(28)).thenReturn(true);
|
||||||
|
when(sysGroupAppRelRepository.findByGroupId(16))
|
||||||
|
.thenReturn(Collections.singletonList(appRel(16, APP_ID)));
|
||||||
|
when(sysGroupAppRelRepository.findByGroupId(28))
|
||||||
|
.thenReturn(Collections.singletonList(appRel(28, APP_ID)));
|
||||||
|
when(mainGroupResolver.resolve(4)).thenReturn(Optional.of(rel(4, 29)));
|
||||||
|
// null 上下文闸门兜底:主归属组 29 向上解析到学校 16(已开通),故放行进入角色取数
|
||||||
|
when(sysGroupService.resolveSchoolId(29)).thenReturn(16);
|
||||||
// 两校都开通该应用(组授权闸门打开;组侧不配模块,只测角色侧取数范围)
|
// 两校都开通该应用(组授权闸门打开;组侧不配模块,只测角色侧取数范围)
|
||||||
when(sysGroupAppRelRepository.findByGroupIdIn(anyList())).thenAnswer(inv -> {
|
when(sysGroupAppRelRepository.findByGroupIdIn(anyList())).thenAnswer(inv -> {
|
||||||
List<Integer> ids = inv.getArgument(0);
|
List<Integer> ids = inv.getArgument(0);
|
||||||
@@ -386,7 +406,8 @@ class GroupAuthzServiceModuleTreeTest {
|
|||||||
// B 校上下文:只带 B 校岗位角色(901 → 模块 101)的模块
|
// B 校上下文:只带 B 校岗位角色(901 → 模块 101)的模块
|
||||||
assertEquals(Collections.singletonList(101),
|
assertEquals(Collections.singletonList(101),
|
||||||
customIds(groupAuthzService.findUserModulesForApp(APP_ID, 28, 4)));
|
customIds(groupAuthzService.findUserModulesForApp(APP_ID, 28, 4)));
|
||||||
// 无学校上下文(现状):两校绑定都生效
|
// 无学校上下文:闸门按主归属组(29)兜底解析到学校 16 放行(应用已开通),
|
||||||
|
// 角色取数仍全局模式 → 两校绑定都生效
|
||||||
assertEquals(Arrays.asList(100, 101),
|
assertEquals(Arrays.asList(100, 101),
|
||||||
customIds(groupAuthzService.findUserModulesForApp(APP_ID, null, 4)));
|
customIds(groupAuthzService.findUserModulesForApp(APP_ID, null, 4)));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,284 @@
|
|||||||
|
package com.yunhai.userbase.service.impl;
|
||||||
|
|
||||||
|
import com.yunhai.platform.utils.model.BoaModule;
|
||||||
|
import com.yunhai.userbase.entity.AppModule;
|
||||||
|
import com.yunhai.platform.utils.model.BoaRoleInfo;
|
||||||
|
import com.yunhai.userbase.entity.ApplicationInfo;
|
||||||
|
import com.yunhai.userbase.entity.SysGroup;
|
||||||
|
import com.yunhai.userbase.entity.SysGroupAppRel;
|
||||||
|
import com.yunhai.userbase.entity.SysGroupModulePerm;
|
||||||
|
import com.yunhai.userbase.entity.SysUser;
|
||||||
|
import com.yunhai.userbase.entity.SysUserGroupRel;
|
||||||
|
import com.yunhai.userbase.enums.GroupTypeEnum;
|
||||||
|
import com.yunhai.userbase.enums.UserTypeEnum;
|
||||||
|
import com.yunhai.userbase.model.GroupSnapshot;
|
||||||
|
import com.yunhai.userbase.repository.AppModuleRepository;
|
||||||
|
import com.yunhai.userbase.repository.ApplicationInfoRepository;
|
||||||
|
import com.yunhai.userbase.repository.SysAppRoleModuleRelRepository;
|
||||||
|
import com.yunhai.userbase.repository.SysAppRoleRepository;
|
||||||
|
import com.yunhai.userbase.repository.SysGroupAppRelRepository;
|
||||||
|
import com.yunhai.userbase.repository.SysGroupModulePermRepository;
|
||||||
|
import com.yunhai.userbase.repository.SysGroupRepository;
|
||||||
|
import com.yunhai.userbase.repository.SysUserAppRoleRelRepository;
|
||||||
|
import com.yunhai.userbase.repository.SysUserGroupRelRepository;
|
||||||
|
import com.yunhai.userbase.repository.SysUserRepository;
|
||||||
|
import com.yunhai.userbase.service.IApplicationInfoService;
|
||||||
|
import com.yunhai.userbase.service.ISysGroupService;
|
||||||
|
import com.yunhai.userbase.service.resolve.MainGroupResolver;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.MockitoAnnotations;
|
||||||
|
import org.springframework.context.ApplicationEventPublisher;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyList;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 学校单点开通硬闸门(2026-10-12)行为单元测试——直接编码验收方案 V1/V2/V3/V4/V6 的服务层语义:
|
||||||
|
* <ul>
|
||||||
|
* <li>V1 角色授权不得绕过学校未开通(模块链返回空、应用集合不含);</li>
|
||||||
|
* <li>V2 祖先组开通不得顶替学校单点(上级开了、本校没开 → 仍空);</li>
|
||||||
|
* <li>V3 学校单点开通即恢复(本校开通 → 模块/应用集合放行);</li>
|
||||||
|
* <li>V4 应用列表收窄口径 = 生效学校单点集合(不并角色/祖先);</li>
|
||||||
|
* <li>V6 超级管理员保留 bypass(不受学校闸门约束)。</li>
|
||||||
|
* </ul>
|
||||||
|
* 基准用例:云海小学(16) 未开通「通知公告」(APP_ID),用户 U=2 归属该校。纯内存桩,无 DB。
|
||||||
|
*/
|
||||||
|
@DisplayName("学校单点开通硬闸门 - GroupAuthzService")
|
||||||
|
class GroupAuthzServiceSchoolGateTest {
|
||||||
|
|
||||||
|
private static final int APP_ID = 100003;
|
||||||
|
private static final int SCHOOL_ID = 16;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private GroupAuthzService groupAuthzService;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private GroupSnapshotService groupSnapshotService;
|
||||||
|
@Mock
|
||||||
|
private SysGroupAppRelRepository sysGroupAppRelRepository;
|
||||||
|
@Mock
|
||||||
|
private SysGroupModulePermRepository sysGroupModulePermRepository;
|
||||||
|
@Mock
|
||||||
|
private SysGroupRepository sysGroupRepository;
|
||||||
|
@Mock
|
||||||
|
private ApplicationInfoRepository applicationInfoRepository;
|
||||||
|
@Mock
|
||||||
|
private IApplicationInfoService applicationInfoService;
|
||||||
|
@Mock
|
||||||
|
private AppModuleRepository appModuleRepository;
|
||||||
|
@Mock
|
||||||
|
private SysUserGroupRelRepository sysUserGroupRelRepository;
|
||||||
|
@Mock
|
||||||
|
private SysUserRepository sysUserRepository;
|
||||||
|
@Mock
|
||||||
|
private SysAppRoleRepository sysAppRoleRepository;
|
||||||
|
@Mock
|
||||||
|
private SysAppRoleModuleRelRepository sysAppRoleModuleRelRepository;
|
||||||
|
@Mock
|
||||||
|
private SysUserAppRoleRelRepository sysUserAppRoleRelRepository;
|
||||||
|
@Mock
|
||||||
|
private ApplicationEventPublisher eventPublisher;
|
||||||
|
@Mock
|
||||||
|
private MainGroupResolver mainGroupResolver;
|
||||||
|
@Mock
|
||||||
|
private ISysGroupService sysGroupService;
|
||||||
|
@Mock
|
||||||
|
private IdentityService identityService;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
MockitoAnnotations.openMocks(this);
|
||||||
|
ApplicationInfo app = new ApplicationInfo();
|
||||||
|
app.setId(APP_ID);
|
||||||
|
app.setName("通知公告");
|
||||||
|
when(applicationInfoService.getById(APP_ID)).thenReturn(app);
|
||||||
|
// 组层级:5 教育云中心 → 27 教育局 → 16 云海小学
|
||||||
|
when(groupSnapshotService.snapshot()).thenReturn(new GroupSnapshot(Arrays.asList(
|
||||||
|
group(5, GroupTypeEnum.NORMAL_GROUP, 0),
|
||||||
|
group(27, GroupTypeEnum.AREA_GROUP, 5),
|
||||||
|
group(SCHOOL_ID, GroupTypeEnum.SCHOOL_GROUP, 27))));
|
||||||
|
when(sysGroupRepository.existsById(SCHOOL_ID)).thenReturn(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================== V1:学校未开通 → 角色/组模块授权均不 bypass =====================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void schoolNotOpened_roleGrantedButModuleEmpty() {
|
||||||
|
normalUserInSchool(2);
|
||||||
|
// 学校 16 自身未开通(单点集合为空),但祖先链(27/5)开通该应用——闸门应只看本校单点
|
||||||
|
when(sysGroupAppRelRepository.findByGroupId(SCHOOL_ID)).thenReturn(Collections.emptyList());
|
||||||
|
when(sysGroupAppRelRepository.findByGroupIdIn(anyList())).thenAnswer(inv ->
|
||||||
|
((List<Integer>) inv.getArgument(0)).contains(27)
|
||||||
|
? Collections.singletonList(appRel(27, APP_ID)) : Collections.emptyList());
|
||||||
|
// 应用下有模块、且有组授权命中(若无闸门本应返回),用于证明被前置闸门清空
|
||||||
|
when(appModuleRepository.findByAppIdIn(anyList()))
|
||||||
|
.thenReturn(new ArrayList<>(Collections.singletonList(module(100, "100"))));
|
||||||
|
when(sysGroupModulePermRepository.findByModuleIdInAndGroupIdIn(anyList(), anyList()))
|
||||||
|
.thenReturn(Collections.singletonList(perm(SCHOOL_ID, 100)));
|
||||||
|
|
||||||
|
List<BoaModule> modules = groupAuthzService.findUserModulesForApp(APP_ID, SCHOOL_ID, 2);
|
||||||
|
assertTrue(modules.isEmpty(), "学校未开通时即便模块已授权也应被单点闸门清空");
|
||||||
|
assertTrue(groupAuthzService.findUserAuthorizedAppIds(2, SCHOOL_ID).isEmpty(),
|
||||||
|
"应用列表收窄口径同样不含未开通应用");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================== V2:祖先开通不顶替学校单点(对照:本校未开→空)=====================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void ancestorOpenedButSchoolNot_stillEmpty() {
|
||||||
|
normalUserInSchool(2);
|
||||||
|
when(sysGroupAppRelRepository.findByGroupId(SCHOOL_ID)).thenReturn(Collections.emptyList());
|
||||||
|
// 仅上级 27 开通;collectAppIdsForAuthzScope(祖先并集)会命中,证明「非祖先链」口径生效
|
||||||
|
when(sysGroupAppRelRepository.findByGroupIdIn(anyList())).thenAnswer(inv ->
|
||||||
|
((List<Integer>) inv.getArgument(0)).contains(27)
|
||||||
|
? Collections.singletonList(appRel(27, APP_ID)) : Collections.emptyList());
|
||||||
|
when(appModuleRepository.findByAppIdIn(anyList())).thenReturn(new ArrayList<>());
|
||||||
|
|
||||||
|
assertTrue(groupAuthzService.findUserModulesForApp(APP_ID, SCHOOL_ID, 2).isEmpty(),
|
||||||
|
"祖先开通不得顶替学校未开通的事实");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================== V3:学校单点开通即恢复 =====================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void schoolOpened_moduleAndAppIdsRestored() {
|
||||||
|
normalUserInSchool(2);
|
||||||
|
when(sysGroupAppRelRepository.findByGroupId(SCHOOL_ID))
|
||||||
|
.thenReturn(Collections.singletonList(appRel(SCHOOL_ID, APP_ID)));
|
||||||
|
// 下游组模块判定仍按既有祖先并集口径(collectAppIdsForAuthzScope)取开通池
|
||||||
|
when(sysGroupAppRelRepository.findByGroupIdIn(anyList())).thenAnswer(inv ->
|
||||||
|
((List<Integer>) inv.getArgument(0)).contains(SCHOOL_ID)
|
||||||
|
? Collections.singletonList(appRel(SCHOOL_ID, APP_ID)) : Collections.emptyList());
|
||||||
|
AppModule parent = module(100, "100");
|
||||||
|
when(appModuleRepository.findByAppIdIn(anyList()))
|
||||||
|
.thenReturn(new ArrayList<>(Collections.singletonList(parent)));
|
||||||
|
when(sysGroupModulePermRepository.findByModuleIdInAndGroupIdIn(anyList(), anyList()))
|
||||||
|
.thenReturn(Collections.singletonList(perm(SCHOOL_ID, 100)));
|
||||||
|
|
||||||
|
List<BoaModule> modules = groupAuthzService.findUserModulesForApp(APP_ID, SCHOOL_ID, 2);
|
||||||
|
assertEquals(1, modules.size(), "本校开通后,已授权模块恢复下发");
|
||||||
|
assertEquals(Collections.singletonList(APP_ID),
|
||||||
|
groupAuthzService.findUserAuthorizedAppIds(2, SCHOOL_ID),
|
||||||
|
"应用列表恢复含本校开通的应用");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================== V4:收窄口径 = 本校单点,不并角色/祖先 =====================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void narrowedAppIds_onlySchoolSinglePoint() {
|
||||||
|
normalUserInSchool(2);
|
||||||
|
// 本校只开通 APP_ID;(祖先/角色补充的其他应用不应出现——因不并 collectAppIdsForAuthzScope 与角色模块)
|
||||||
|
when(sysGroupAppRelRepository.findByGroupId(SCHOOL_ID))
|
||||||
|
.thenReturn(Collections.singletonList(appRel(SCHOOL_ID, APP_ID)));
|
||||||
|
List<Integer> result = groupAuthzService.findUserAuthorizedAppIds(2, SCHOOL_ID);
|
||||||
|
assertEquals(Collections.singletonList(APP_ID), result,
|
||||||
|
"收窄集合严格=本校单点开通,不含祖先/角色来源");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================== V6:超级管理员保留 bypass =====================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void superAdmin_bypassesSchoolGate() {
|
||||||
|
int adminId = com.yunhai.userbase.utils.DicUtil.SUPER_ADMIN_ID;
|
||||||
|
SysUser admin = new SysUser();
|
||||||
|
admin.setId(adminId);
|
||||||
|
admin.setType(UserTypeEnum.SUPER_ADMIN);
|
||||||
|
when(sysUserRepository.findById(adminId)).thenReturn(Optional.of(admin));
|
||||||
|
// 学校 16 未开通该应用
|
||||||
|
when(sysGroupAppRelRepository.findByGroupId(SCHOOL_ID)).thenReturn(Collections.emptyList());
|
||||||
|
when(appModuleRepository.findByAppIdIn(anyList()))
|
||||||
|
.thenReturn(new ArrayList<>(Collections.singletonList(module(100, "100"))));
|
||||||
|
// 超管应用全集
|
||||||
|
ApplicationInfo app = new ApplicationInfo();
|
||||||
|
app.setId(APP_ID);
|
||||||
|
when(applicationInfoRepository.findAll()).thenReturn(Collections.singletonList(app));
|
||||||
|
|
||||||
|
assertEquals(1, groupAuthzService.findUserModulesForApp(APP_ID, SCHOOL_ID, adminId).size(),
|
||||||
|
"超管短路先于闸门,未开通学校仍返回全部模块");
|
||||||
|
assertEquals(Collections.singletonList(APP_ID), groupAuthzService.findUserAuthorizedAppIds(adminId, SCHOOL_ID),
|
||||||
|
"超管收窄重载委托全量口径");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================== 生效学校兜底:null schId 走当前身份校 =====================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void nullSchool_fallsBackToCurrentIdentitySchool() {
|
||||||
|
normalUserInSchool(2);
|
||||||
|
BoaRoleInfo current = new BoaRoleInfo();
|
||||||
|
current.setSchId(SCHOOL_ID);
|
||||||
|
when(identityService.getCurrent(2)).thenReturn(current);
|
||||||
|
// 身份校(16)未开通 → 空(无学校语境/未开通即无应用)
|
||||||
|
when(sysGroupAppRelRepository.findByGroupId(SCHOOL_ID)).thenReturn(Collections.emptyList());
|
||||||
|
when(appModuleRepository.findByAppIdIn(anyList())).thenReturn(new ArrayList<>());
|
||||||
|
|
||||||
|
assertEquals(Collections.emptyList(), groupAuthzService.findUserAuthorizedAppIds(2, null),
|
||||||
|
"schId 为空按当前身份校兜底判定,未开通返回空");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================== 夹具 =====================
|
||||||
|
|
||||||
|
private void normalUserInSchool(int userId) {
|
||||||
|
SysUser u = new SysUser();
|
||||||
|
u.setId(userId);
|
||||||
|
u.setType(UserTypeEnum.NORMAL);
|
||||||
|
when(sysUserRepository.findById(userId)).thenReturn(Optional.of(u));
|
||||||
|
when(sysUserGroupRelRepository.findByUserId(userId))
|
||||||
|
.thenReturn(Collections.singletonList(rel(userId, SCHOOL_ID)));
|
||||||
|
when(sysUserAppRoleRelRepository.findByUserId(userId)).thenReturn(Collections.emptyList());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SysGroup group(Integer id, GroupTypeEnum type, Integer parentId) {
|
||||||
|
SysGroup g = new SysGroup();
|
||||||
|
g.setId(id);
|
||||||
|
g.setType(type);
|
||||||
|
g.setParentId(parentId);
|
||||||
|
return g;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SysUserGroupRel rel(Integer userId, Integer groupId) {
|
||||||
|
SysUserGroupRel r = new SysUserGroupRel();
|
||||||
|
r.setUserId(userId);
|
||||||
|
r.setGroupId(groupId);
|
||||||
|
r.setIsMain(1);
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SysGroupAppRel appRel(Integer groupId, Integer appId) {
|
||||||
|
SysGroupAppRel r = new SysGroupAppRel();
|
||||||
|
r.setGroupId(groupId);
|
||||||
|
r.setAppId(appId);
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SysGroupModulePerm perm(Integer groupId, Integer moduleId) {
|
||||||
|
SysGroupModulePerm p = new SysGroupModulePerm();
|
||||||
|
p.setGroupId(groupId);
|
||||||
|
p.setModuleId(moduleId);
|
||||||
|
p.setInheritFlag(Boolean.FALSE);
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AppModule module(Integer id, String customId) {
|
||||||
|
AppModule m = new AppModule();
|
||||||
|
m.setId(id);
|
||||||
|
m.setAppId(APP_ID);
|
||||||
|
m.setName("模块" + customId);
|
||||||
|
m.setCustomId(customId);
|
||||||
|
m.setParentId(null);
|
||||||
|
m.setLevel(1);
|
||||||
|
m.setSortNo(1);
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user