feat(B1): 建站权限拆分-校级可建/编辑本院系子站,顶层站仍仅超管
- SiteService.save 入口换 requireSiteSavePermission 私有方法(安全边界单点): 超管直通;无校级角色/顶层编辑/新建顶层/子站提升/跨校主站/伪造schId 全拒 - SiteServiceIT 新增10例(方案7例+伪造schId/跨校接管/同校改挂3个边界) - mvn -o test 236/236 全绿(基线226+10)
This commit is contained in:
@@ -106,10 +106,10 @@ public class SiteService {
|
||||
return ExtJsonUtils.getString(config, key);
|
||||
}
|
||||
|
||||
/** 站点保存(仅超管;MVP 1-2 校以初始化脚本为主,本接口为后台兜底) */
|
||||
/** 站点保存(B1 权限拆分:校级可建/编辑本院系子站,顶层站仅超管;MVP 1-2 校以初始化脚本为主) */
|
||||
@Transactional
|
||||
public SiteVo save(BoaUser user, SiteSaveRequest req) {
|
||||
requireSuperAdmin(user);
|
||||
requireSiteSavePermission(user, req);
|
||||
CmsSite site;
|
||||
if (req.getId() == null) {
|
||||
if (siteRepository.findByCode(req.getCode()).isPresent()) {
|
||||
@@ -144,6 +144,50 @@ public class SiteService {
|
||||
return SiteVo.of(site);
|
||||
}
|
||||
|
||||
/**
|
||||
* 站点保存权限(B1 建站权限拆分,安全边界收敛于本单一方法,前端按钮仅入口显性化):
|
||||
* 超管直通;非超管仅可新建/编辑「本校主站下的院系子站」,具体拒绝项——
|
||||
* ① 新建顶层站(parentSiteId≤0)→ 拒(仅平台超管可创建学校主站);
|
||||
* ② 编辑对象为顶层站(含本校主站)→ 拒;
|
||||
* ③ 编辑子站时 parentSiteId 传 0/null(提升为顶层)→ 拒;
|
||||
* ④ 目标主站 schId 与 currentRole.schId 不一致(跨校/改挂接管)→ 拒;
|
||||
* ⑤ 编辑对象本身跨校 → 拒(复用 canManage 同校语义);
|
||||
* ⑥ 无校级角色(schId 为空)→ 拒;
|
||||
* ⑦ 请求体显式 schId 与本人角色不一致(伪造通道)→ 拒(留空则由 validateParent 继承主站)。
|
||||
*/
|
||||
private void requireSiteSavePermission(BoaUser user, SiteSaveRequest req) {
|
||||
if (authProperties.isSuperAdmin(user)) {
|
||||
return;
|
||||
}
|
||||
Integer schId = user.getCurrentRole() == null ? null : user.getCurrentRole().getSchId();
|
||||
if (schId == null) {
|
||||
throw new BizException(ErrorCode.FORBIDDEN, "无权管理该站点(需校级角色)");
|
||||
}
|
||||
if (req.getId() != null) {
|
||||
CmsSite target = siteRepository.findById(req.getId())
|
||||
.orElseThrow(() -> new BizException(ErrorCode.PARAM_ERROR, "站点不存在"));
|
||||
boolean targetIsTop = target.getParentSiteId() == null || target.getParentSiteId() == 0;
|
||||
if (targetIsTop) {
|
||||
throw new BizException(ErrorCode.FORBIDDEN, "仅平台超管可管理学校主站(顶层站点)");
|
||||
}
|
||||
if (!canManage(user, target)) {
|
||||
throw new BizException(ErrorCode.FORBIDDEN, "无权管理该站点");
|
||||
}
|
||||
}
|
||||
Integer parentSiteId = req.getParentSiteId() == null ? 0 : req.getParentSiteId();
|
||||
if (parentSiteId <= 0) {
|
||||
throw new BizException(ErrorCode.FORBIDDEN, "仅平台超管可创建学校主站(顶层站点)");
|
||||
}
|
||||
CmsSite parent = siteRepository.findById(parentSiteId)
|
||||
.orElseThrow(() -> new BizException(ErrorCode.PARAM_ERROR, "上级站点不存在"));
|
||||
if (parent.getSchId() == null || !schId.equals(parent.getSchId())) {
|
||||
throw new BizException(ErrorCode.FORBIDDEN, "无权管理该站点");
|
||||
}
|
||||
if (req.getSchId() != null && !schId.equals(req.getSchId())) {
|
||||
throw new BizException(ErrorCode.FORBIDDEN, "无权管理该站点(学校ID与本人角色不符)");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* themeParams 服务端合并(方案 §5.3,对应评审 R6):仅替换 config_json.theme_params 键,
|
||||
* 保留 logo/contact/domain 等其余键;未声明 key 与非法值按所选主题 schema 丢弃(不回传错误)。
|
||||
|
||||
Reference in New Issue
Block a user