|
|
@@ -5,9 +5,12 @@ import com.dms.common.Errors;
|
|
|
import com.dms.common.Page;
|
|
|
import com.dms.common.Payloads;
|
|
|
import com.dms.common.Reorders;
|
|
|
+import com.dms.mapper.CategoryMapper;
|
|
|
import com.dms.mapper.RoleMapper;
|
|
|
import com.dms.mapper.RolePermissionMapper;
|
|
|
import com.dms.mapper.UserMapper;
|
|
|
+import com.dms.mapper.UserRoleMapper;
|
|
|
+import com.dms.model.CategoryEntity;
|
|
|
import com.dms.model.RoleEntity;
|
|
|
import com.dms.model.RolePermissionEntity;
|
|
|
import com.dms.model.UserEntity;
|
|
|
@@ -22,7 +25,6 @@ import java.util.HashSet;
|
|
|
import java.util.LinkedHashMap;
|
|
|
import java.util.List;
|
|
|
import java.util.Map;
|
|
|
-import java.util.Objects;
|
|
|
import java.util.Set;
|
|
|
import java.util.TreeSet;
|
|
|
import org.springframework.dao.DuplicateKeyException;
|
|
|
@@ -49,32 +51,50 @@ public class RoleService {
|
|
|
private static final Set<String> ROLE_USER_FIELDS = Set.of("userIds", "rowVersion");
|
|
|
private static final Set<String> ROLE_REORDER_FIELDS = Set.of("orderedIds");
|
|
|
|
|
|
- /** 矩阵 8 个操作布尔位(camel → 实体字段一一对应;契约1.31 新增 canUpload,
|
|
|
- * canBind/canUnbind 界面合并为单一「挂载」开关,两列恒同值)。 */
|
|
|
+ /** 矩阵 12 个操作布尔位(camel → 实体字段一一对应;契约1.31 新增 canUpload,
|
|
|
+ * canBind/canUnbind 界面合并为单一「挂载」开关,两列恒同值;
|
|
|
+ * 契约1.41 附件四权限(上传/编辑/查看/下载),仅 OWN 行可设置)。 */
|
|
|
private static final List<String> PERMISSION_FLAG_NAMES = List.of(
|
|
|
"canView", "canUpload", "canDownload", "canEdit", "canDelete",
|
|
|
- "canConfigPermission", "canBindAttachment", "canUnbindAttachment");
|
|
|
+ "canConfigPermission", "canBindAttachment", "canUnbindAttachment",
|
|
|
+ "canAttachmentUpload", "canAttachmentEdit",
|
|
|
+ "canAttachmentView", "canAttachmentDownload", "canAttachmentDelete");
|
|
|
+
|
|
|
+ /** 附件五权限位(契约1.41/1.44):附件无部门归属,仅 OWN 行有意义。 */
|
|
|
+ private static final List<String> ATTACHMENT_FLAG_NAMES = List.of(
|
|
|
+ "canAttachmentUpload", "canAttachmentEdit",
|
|
|
+ "canAttachmentView", "canAttachmentDownload", "canAttachmentDelete");
|
|
|
|
|
|
private static final List<String> SECURITY_LEVELS = List.of(
|
|
|
"PUBLIC", "INTERNAL", "SECRET", "CONFIDENTIAL", "TOP_SECRET");
|
|
|
private static final List<String> DEPT_SCOPES = List.of("OWN", "OTHER");
|
|
|
|
|
|
- private static final Set<MatrixKey> MATRIX_KEYS = buildMatrixKeys();
|
|
|
+ /** 矩阵种类维度(契约1.48):MAIN=主体方案;附件四类型顺序对齐前端资料页签。 */
|
|
|
+ static final String DOC_KIND_MAIN = "MAIN";
|
|
|
+ static final List<String> ATTACHMENT_KINDS = List.of(
|
|
|
+ "ACCOMPANYING_PLAN", "SUPPORT_ACHIEVEMENT",
|
|
|
+ "TRAINING_MATERIAL", "TYPICAL_CASE");
|
|
|
|
|
|
private final RoleMapper roleMapper;
|
|
|
private final RolePermissionMapper rolePermissionMapper;
|
|
|
+ private final CategoryMapper categoryMapper;
|
|
|
private final UserMapper userMapper;
|
|
|
+ private final UserRoleMapper userRoleMapper;
|
|
|
private final AuditService auditService;
|
|
|
private final AdminPermission adminPermission;
|
|
|
private final Reauth reauth;
|
|
|
|
|
|
public RoleService(RoleMapper roleMapper,
|
|
|
- RolePermissionMapper rolePermissionMapper, UserMapper userMapper,
|
|
|
+ RolePermissionMapper rolePermissionMapper,
|
|
|
+ CategoryMapper categoryMapper, UserMapper userMapper,
|
|
|
+ UserRoleMapper userRoleMapper,
|
|
|
AuditService auditService, AdminPermission adminPermission,
|
|
|
Reauth reauth) {
|
|
|
this.roleMapper = roleMapper;
|
|
|
this.rolePermissionMapper = rolePermissionMapper;
|
|
|
+ this.categoryMapper = categoryMapper;
|
|
|
this.userMapper = userMapper;
|
|
|
+ this.userRoleMapper = userRoleMapper;
|
|
|
this.auditService = auditService;
|
|
|
this.adminPermission = adminPermission;
|
|
|
this.reauth = reauth;
|
|
|
@@ -123,6 +143,8 @@ public class RoleService {
|
|
|
Long managedOrgId = optionalOrgId(body.get("managedOrgId"));
|
|
|
Long actorId = AuthContext.get().userId();
|
|
|
LocalDateTime now = LocalDateTime.now(ZoneOffset.UTC);
|
|
|
+ // 契约1.61:新增与删除角色仅超管可操作(1.60 勾管理位的细分拒绝被全覆盖)
|
|
|
+ guardRoleCreateDelete(AuthContext.get());
|
|
|
|
|
|
try {
|
|
|
if (roleMapper.findIdByRoleNameExclude(roleName, 0L) != null) {
|
|
|
@@ -187,6 +209,14 @@ public class RoleService {
|
|
|
throw notFound("角色不存在");
|
|
|
}
|
|
|
checkVersion(role, expectedVersion);
|
|
|
+ // 契约1.60:管理员角色非超管全禁;普通角色管理位不可变更(原值回传放行)
|
|
|
+ guardRoleAdminEdit(AuthContext.get(), hasAnyManageFlag(role),
|
|
|
+ manageCategory != Boolean.TRUE.equals(role.getManageCategoryEdit())
|
|
|
+ || manageOrg != Boolean.TRUE.equals(role.getManageOrgEdit())
|
|
|
+ || manageUser != Boolean.TRUE.equals(role.getManageUserEdit())
|
|
|
+ || manageSystem != Boolean.TRUE.equals(role.getManageSystem())
|
|
|
+ || manageAuditLog != Boolean.TRUE.equals(role.getManageAuditLog())
|
|
|
+ || manageRoleEdit != Boolean.TRUE.equals(role.getManageRoleEdit()));
|
|
|
if (roleMapper.findIdByRoleNameExclude(roleName, roleId) != null) {
|
|
|
throw conflict("角色名已存在", null);
|
|
|
}
|
|
|
@@ -235,7 +265,9 @@ public class RoleService {
|
|
|
throw notFound("角色不存在");
|
|
|
}
|
|
|
checkVersion(role, rowVersion);
|
|
|
- long boundUsers = userMapper.countActiveByRoleId(roleId);
|
|
|
+ // 契约1.61:删除角色仅超管可操作(不限目标角色)
|
|
|
+ guardRoleCreateDelete(AuthContext.get());
|
|
|
+ long boundUsers = userRoleMapper.countActiveUsersByRoleId(roleId);
|
|
|
if (boundUsers > 0) {
|
|
|
throw new DmsException("ROLE_IN_USE", 409,
|
|
|
"角色仍有关联用户,无法删除", Map.of("memberCount", boundUsers));
|
|
|
@@ -262,14 +294,44 @@ public class RoleService {
|
|
|
@Transactional(readOnly = true)
|
|
|
public List<Map<String, Object>> listPermissions(long roleId) {
|
|
|
requireRole(roleId);
|
|
|
- List<Map<String, Object>> result = new ArrayList<>();
|
|
|
+ // 读时归一化(契约1.48):库行按键索引后按期望键集输出——分类新增的缺失行
|
|
|
+ // 以全 0 补齐、已删分类的残留行不输出;保存(全量替换)时自然收敛
|
|
|
+ Map<MatrixKey, RolePermissionEntity> byKey = new HashMap<>();
|
|
|
for (RolePermissionEntity row : rolePermissionMapper.listActiveByRole(roleId)) {
|
|
|
- result.add(permissionRowDto(row));
|
|
|
+ byKey.put(MatrixKey.of(row), row);
|
|
|
+ }
|
|
|
+ List<Map<String, Object>> result = new ArrayList<>();
|
|
|
+ for (MatrixKey key : expectedMatrixKeys()) {
|
|
|
+ RolePermissionEntity row = byKey.get(key);
|
|
|
+ result.add(permissionRowDto(row != null ? row : emptyRow(key)));
|
|
|
}
|
|
|
return result;
|
|
|
}
|
|
|
|
|
|
- /** 对齐 update_role_permissions:15 行矩阵校验 + 差量更新。 */
|
|
|
+ /** 全 0 空行(缺省种子,不入库——保存时按提交值 UPSERT)。 */
|
|
|
+ private static RolePermissionEntity emptyRow(MatrixKey key) {
|
|
|
+ RolePermissionEntity row = new RolePermissionEntity();
|
|
|
+ row.setTopCategoryId(key.topCategoryId());
|
|
|
+ row.setDocKind(key.docKind());
|
|
|
+ row.setSecurityLevel(key.securityLevel());
|
|
|
+ row.setDeptScope(key.deptScope());
|
|
|
+ row.setCanView(false);
|
|
|
+ row.setCanUpload(false);
|
|
|
+ row.setCanDownload(false);
|
|
|
+ row.setCanEdit(false);
|
|
|
+ row.setCanDelete(false);
|
|
|
+ row.setCanConfigPermission(false);
|
|
|
+ row.setCanBindAttachment(false);
|
|
|
+ row.setCanUnbindAttachment(false);
|
|
|
+ row.setCanAttachmentUpload(false);
|
|
|
+ row.setCanAttachmentEdit(false);
|
|
|
+ row.setCanAttachmentView(false);
|
|
|
+ row.setCanAttachmentDownload(false);
|
|
|
+ row.setCanAttachmentDelete(false);
|
|
|
+ return row;
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 对齐 update_role_permissions:三维矩阵校验 + 差量更新 + 缺失行补插(契约1.48)。 */
|
|
|
@Transactional
|
|
|
public Map<String, Object> updatePermissions(long roleId, Object payload,
|
|
|
HttpServletRequest request) {
|
|
|
@@ -285,29 +347,52 @@ public class RoleService {
|
|
|
throw notFound("角色不存在");
|
|
|
}
|
|
|
checkVersion(role, expectedVersion);
|
|
|
+ // 契约1.60:管理员角色的权限矩阵非超管不可保存
|
|
|
+ guardRoleAdminEdit(AuthContext.get(), hasAnyManageFlag(role), false);
|
|
|
List<RolePermissionEntity> existingRows = rolePermissionMapper.listActiveByRole(roleId);
|
|
|
Map<MatrixKey, RolePermissionEntity> byKey = new HashMap<>();
|
|
|
for (RolePermissionEntity row : existingRows) {
|
|
|
- byKey.put(new MatrixKey(row.getSecurityLevel(), row.getDeptScope()), row);
|
|
|
+ byKey.put(MatrixKey.of(row), row);
|
|
|
}
|
|
|
- if (!byKey.keySet().equals(MATRIX_KEYS)) {
|
|
|
+ // 仅拒绝重复脏行(同一矩阵键两行):缺失行(分类新增)在下方 diff 补插,
|
|
|
+ // 已删分类残留行不参与匹配、被提交全集自然冷落
|
|
|
+ if (byKey.size() != existingRows.size()) {
|
|
|
throw conflict("角色权限矩阵数据不完整,请联系管理员核查", null);
|
|
|
}
|
|
|
+ List<MatrixKey> expectedKeys = expectedMatrixKeys();
|
|
|
|
|
|
List<Map<String, Object>> changedRows = new ArrayList<>();
|
|
|
for (Map<String, Object> item : matrix) {
|
|
|
- RolePermissionEntity row = byKey.get(new MatrixKey(
|
|
|
- (String) item.get("securityLevel"), (String) item.get("deptScope")));
|
|
|
+ MatrixKey key = new MatrixKey(longId(item.get("topCategoryId")),
|
|
|
+ (String) item.get("docKind"),
|
|
|
+ (String) item.get("securityLevel"), (String) item.get("deptScope"));
|
|
|
+ RolePermissionEntity row = byKey.get(key);
|
|
|
+ if (row == null) {
|
|
|
+ // 分类新增后的缺失行:以全 0 空行参与 diff,变化时按提交值补插
|
|
|
+ row = emptyRow(key);
|
|
|
+ }
|
|
|
Map<String, Object> beforeFlags = entityFlags(row);
|
|
|
Map<String, Object> afterFlags = requestFlags(item);
|
|
|
if (!beforeFlags.equals(afterFlags)) {
|
|
|
- applyFlags(row, item);
|
|
|
- row.setUpdatedBy(actorId);
|
|
|
- row.setUpdatedAt(now);
|
|
|
- rolePermissionMapper.updateFlags(row);
|
|
|
+ if (row.getId() == null) {
|
|
|
+ // 分类新增后首存的缺失行:按提交值落库(含审计者信息)
|
|
|
+ applyFlags(row, item);
|
|
|
+ row.setRoleId(roleId);
|
|
|
+ row.setCreatedBy(actorId);
|
|
|
+ row.setUpdatedBy(actorId);
|
|
|
+ row.setUpdatedAt(now);
|
|
|
+ rolePermissionMapper.insert(row);
|
|
|
+ } else {
|
|
|
+ applyFlags(row, item);
|
|
|
+ row.setUpdatedBy(actorId);
|
|
|
+ row.setUpdatedAt(now);
|
|
|
+ rolePermissionMapper.updateFlags(row);
|
|
|
+ }
|
|
|
Map<String, Object> changed = Payloads.orderedMap();
|
|
|
- changed.put("securityLevel", row.getSecurityLevel());
|
|
|
- changed.put("deptScope", row.getDeptScope());
|
|
|
+ changed.put("topCategoryId", item.get("topCategoryId"));
|
|
|
+ changed.put("docKind", key.docKind());
|
|
|
+ changed.put("securityLevel", key.securityLevel());
|
|
|
+ changed.put("deptScope", key.deptScope());
|
|
|
changed.put("before", beforeFlags);
|
|
|
changed.put("after", afterFlags);
|
|
|
changedRows.add(changed);
|
|
|
@@ -337,12 +422,14 @@ public class RoleService {
|
|
|
requireRole(roleId);
|
|
|
Map<String, Object> result = Payloads.orderedMap();
|
|
|
result.put("id", serializeId(roleId));
|
|
|
- result.put("userIds", userMapper.listActiveIdsByRoleId(roleId).stream()
|
|
|
+ result.put("userIds", userRoleMapper.listActiveUserIdsByRoleId(roleId).stream()
|
|
|
.map(String::valueOf).toList());
|
|
|
return result;
|
|
|
}
|
|
|
|
|
|
- /** 对齐 update_role_users:USER/ADMIN 均可关联,双向 diff 联动 auth_version。 */
|
|
|
+ /** 对齐 update_role_users:USER/ADMIN 均可关联,双向 diff 联动 auth_version。
|
|
|
+ * 多角色化(契约1.46):只增删本角色关联、不动用户其他角色挂载;
|
|
|
+ * 关联变更逐用户 bumpForRoleChange,使旧 token(含会话角色 claim)失效。 */
|
|
|
@Transactional
|
|
|
public Map<String, Object> updateUsers(long roleId, Object payload,
|
|
|
HttpServletRequest request) {
|
|
|
@@ -357,6 +444,8 @@ public class RoleService {
|
|
|
throw notFound("角色不存在");
|
|
|
}
|
|
|
checkVersion(role, expectedVersion);
|
|
|
+ // 契约1.60:管理员角色的用户关联非超管不可变更(挂给他人/自己即自我提权)
|
|
|
+ guardRoleAdminEdit(AuthContext.get(), hasAnyManageFlag(role), false);
|
|
|
Set<Long> targetIds = new HashSet<>(userIds);
|
|
|
Map<Long, UserEntity> found = new HashMap<>();
|
|
|
if (!targetIds.isEmpty()) {
|
|
|
@@ -376,18 +465,24 @@ public class RoleService {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ List<Long> currentUserIds = userRoleMapper.listActiveUserIdsByRoleId(roleId);
|
|
|
List<Long> addedIds = new ArrayList<>();
|
|
|
List<Long> removedIds = new ArrayList<>();
|
|
|
- for (UserEntity user : userMapper.listActiveByRoleIdForUpdate(roleId)) {
|
|
|
- if (!targetIds.contains(user.getId())) {
|
|
|
- userMapper.clearRole(user.getId(), now, actorId);
|
|
|
- removedIds.add(user.getId());
|
|
|
+ for (Long currentUserId : currentUserIds) {
|
|
|
+ if (!targetIds.contains(currentUserId)) {
|
|
|
+ userRoleMapper.deleteByUserAndRole(currentUserId, roleId);
|
|
|
+ userMapper.bumpForRoleChange(currentUserId, now, actorId);
|
|
|
+ removedIds.add(currentUserId);
|
|
|
}
|
|
|
}
|
|
|
for (Long userId : new TreeSet<>(targetIds)) {
|
|
|
- UserEntity user = found.get(userId);
|
|
|
- if (user != null && !Objects.equals(user.getRoleId(), roleId)) {
|
|
|
- userMapper.assignRole(userId, roleId, now, actorId);
|
|
|
+ if (!currentUserIds.contains(userId)) {
|
|
|
+ try {
|
|
|
+ userRoleMapper.insert(userId, roleId, actorId);
|
|
|
+ } catch (DuplicateKeyException ex) {
|
|
|
+ // 幂等:关联已存在(如停用期间保留的挂载)
|
|
|
+ }
|
|
|
+ userMapper.bumpForRoleChange(userId, now, actorId);
|
|
|
addedIds.add(userId);
|
|
|
}
|
|
|
}
|
|
|
@@ -469,16 +564,16 @@ public class RoleService {
|
|
|
}
|
|
|
|
|
|
private void seedPermissionRows(long roleId, Long actorId) {
|
|
|
- for (String level : SECURITY_LEVELS) {
|
|
|
- for (String scope : DEPT_SCOPES) {
|
|
|
- RolePermissionEntity row = new RolePermissionEntity();
|
|
|
- row.setRoleId(roleId);
|
|
|
- row.setSecurityLevel(level);
|
|
|
- row.setDeptScope(scope);
|
|
|
- row.setCreatedBy(actorId);
|
|
|
- row.setUpdatedBy(actorId);
|
|
|
- rolePermissionMapper.insert(row);
|
|
|
- }
|
|
|
+ for (MatrixKey key : expectedMatrixKeys()) {
|
|
|
+ RolePermissionEntity row = new RolePermissionEntity();
|
|
|
+ row.setRoleId(roleId);
|
|
|
+ row.setTopCategoryId(key.topCategoryId());
|
|
|
+ row.setDocKind(key.docKind());
|
|
|
+ row.setSecurityLevel(key.securityLevel());
|
|
|
+ row.setDeptScope(key.deptScope());
|
|
|
+ row.setCreatedBy(actorId);
|
|
|
+ row.setUpdatedBy(actorId);
|
|
|
+ rolePermissionMapper.insert(row);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -487,7 +582,7 @@ public class RoleService {
|
|
|
return Map.of();
|
|
|
}
|
|
|
Map<Long, Long> result = new HashMap<>();
|
|
|
- for (Map<String, Object> row : userMapper.countMembersByRoleIds(roleIds)) {
|
|
|
+ for (Map<String, Object> row : userRoleMapper.countActiveUsersByRoleIds(roleIds)) {
|
|
|
result.put(((Number) row.get("roleId")).longValue(),
|
|
|
((Number) row.get("cnt")).longValue());
|
|
|
}
|
|
|
@@ -570,6 +665,8 @@ public class RoleService {
|
|
|
|
|
|
private static Map<String, Object> permissionRowDto(RolePermissionEntity row) {
|
|
|
Map<String, Object> dto = Payloads.orderedMap();
|
|
|
+ dto.put("topCategoryId", serializeId(row.getTopCategoryId()));
|
|
|
+ dto.put("docKind", row.getDocKind());
|
|
|
dto.put("securityLevel", row.getSecurityLevel());
|
|
|
dto.put("deptScope", row.getDeptScope());
|
|
|
dto.put("canView", row.getCanView());
|
|
|
@@ -580,18 +677,25 @@ public class RoleService {
|
|
|
dto.put("canConfigPermission", row.getCanConfigPermission());
|
|
|
dto.put("canBindAttachment", row.getCanBindAttachment());
|
|
|
dto.put("canUnbindAttachment", row.getCanUnbindAttachment());
|
|
|
+ dto.put("canAttachmentUpload", row.getCanAttachmentUpload());
|
|
|
+ dto.put("canAttachmentEdit", row.getCanAttachmentEdit());
|
|
|
+ dto.put("canAttachmentView", row.getCanAttachmentView());
|
|
|
+ dto.put("canAttachmentDownload", row.getCanAttachmentDownload());
|
|
|
+ dto.put("canAttachmentDelete", row.getCanAttachmentDelete());
|
|
|
return dto;
|
|
|
}
|
|
|
|
|
|
- /** 对齐 _validate_matrix。 */
|
|
|
- @SuppressWarnings("unchecked")
|
|
|
- private static List<Map<String, Object>> validateMatrix(Object value) {
|
|
|
+ /** 对齐 _validate_matrix(契约1.48):三维行结构 + 当前期望键集全集校验。 */
|
|
|
+ private List<Map<String, Object>> validateMatrix(Object value) {
|
|
|
Set<String> expectedRowFields = new TreeSet<>();
|
|
|
+ expectedRowFields.add("topCategoryId");
|
|
|
+ expectedRowFields.add("docKind");
|
|
|
expectedRowFields.add("securityLevel");
|
|
|
expectedRowFields.add("deptScope");
|
|
|
expectedRowFields.addAll(PERMISSION_FLAG_NAMES);
|
|
|
- if (!(value instanceof List<?> list) || list.size() != MATRIX_KEYS.size()) {
|
|
|
- throw Errors.invalidArgument("权限矩阵必须为" + MATRIX_KEYS.size() + "行");
|
|
|
+ List<MatrixKey> expectedKeys = expectedMatrixKeys();
|
|
|
+ if (!(value instanceof List<?> list) || list.size() != expectedKeys.size()) {
|
|
|
+ throw Errors.invalidArgument("权限矩阵必须为" + expectedKeys.size() + "行");
|
|
|
}
|
|
|
Set<MatrixKey> rows = new HashSet<>();
|
|
|
for (Object item : list) {
|
|
|
@@ -599,31 +703,91 @@ public class RoleService {
|
|
|
throw Errors.invalidArgument(
|
|
|
"矩阵行字段必须且只能包含:" + String.join("、", expectedRowFields));
|
|
|
}
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
Map<String, Object> row = (Map<String, Object>) item;
|
|
|
- Object level = row.get("securityLevel");
|
|
|
- Object scope = row.get("deptScope");
|
|
|
- if (!(level instanceof String) || !(scope instanceof String)) {
|
|
|
- throw Errors.invalidArgument("矩阵行的密级或部门范围不是有效枚举值");
|
|
|
- }
|
|
|
- MatrixKey key = new MatrixKey((String) level, (String) scope);
|
|
|
- if (!MATRIX_KEYS.contains(key)) {
|
|
|
- throw Errors.invalidArgument("矩阵行的密级或部门范围不是有效枚举值");
|
|
|
+ MatrixKey key = matrixKey(row);
|
|
|
+ if (!expectedKeys.contains(key)) {
|
|
|
+ throw Errors.invalidArgument("矩阵行的分类、种类、密级或部门范围组合无效");
|
|
|
}
|
|
|
- if (rows.contains(key)) {
|
|
|
+ if (!rows.add(key)) {
|
|
|
throw Errors.invalidArgument("矩阵行不能重复");
|
|
|
}
|
|
|
for (String name : PERMISSION_FLAG_NAMES) {
|
|
|
Payloads.booleanValue(row.get(name), name);
|
|
|
}
|
|
|
+ boolean mainRow = DOC_KIND_MAIN.equals(key.docKind());
|
|
|
+ for (String name : PERMISSION_FLAG_NAMES) {
|
|
|
+ boolean attachmentFlag = ATTACHMENT_FLAG_NAMES.contains(name);
|
|
|
+ // 主体方案行仅主案位有效;附件种类行仅附件位有效(交叉位服务端拒绝置真)
|
|
|
+ if (mainRow == attachmentFlag && Boolean.TRUE.equals(row.get(name))) {
|
|
|
+ throw Errors.invalidArgument(mainRow
|
|
|
+ ? "主体方案行不可设置附件权限位"
|
|
|
+ : "共享资料行不可设置主体方案权限位");
|
|
|
+ }
|
|
|
+ }
|
|
|
// 上传权限仅限本部门(OWN)行:其他部门行恒不可选(契约1.31)
|
|
|
- if ("OTHER".equals(scope) && Boolean.TRUE.equals(row.get("canUpload"))) {
|
|
|
+ if ("OTHER".equals(key.deptScope()) && Boolean.TRUE.equals(row.get("canUpload"))) {
|
|
|
throw Errors.invalidArgument("上传权限仅可设置本部门方案");
|
|
|
}
|
|
|
- rows.add(key);
|
|
|
}
|
|
|
return (List<Map<String, Object>>) value;
|
|
|
}
|
|
|
|
|
|
+ /** 请求行的矩阵键(topCategoryId 字符串 id 校验 + null=共享资料库)。 */
|
|
|
+ private static MatrixKey matrixKey(Map<String, Object> row) {
|
|
|
+ Object topId = row.get("topCategoryId");
|
|
|
+ Object kind = row.get("docKind");
|
|
|
+ Object level = row.get("securityLevel");
|
|
|
+ Object scope = row.get("deptScope");
|
|
|
+ if (topId != null && (!(topId instanceof String top) || !top.matches("[0-9]+"))) {
|
|
|
+ throw Errors.invalidArgument("矩阵行的一级分类标识无效");
|
|
|
+ }
|
|
|
+ if (!(kind instanceof String kindValue)
|
|
|
+ || !(DOC_KIND_MAIN.equals(kindValue) || ATTACHMENT_KINDS.contains(kindValue))) {
|
|
|
+ throw Errors.invalidArgument("矩阵行的种类不是有效枚举值");
|
|
|
+ }
|
|
|
+ if (!(level instanceof String) || !SECURITY_LEVELS.contains(level)) {
|
|
|
+ throw Errors.invalidArgument("矩阵行的密级不是有效枚举值");
|
|
|
+ }
|
|
|
+ if (!(scope instanceof String) || !DEPT_SCOPES.contains(scope)) {
|
|
|
+ throw Errors.invalidArgument("矩阵行的部门范围不是有效枚举值");
|
|
|
+ }
|
|
|
+ return new MatrixKey(topId == null ? null : Long.valueOf((String) topId),
|
|
|
+ (String) kind, (String) level, (String) scope);
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 期望矩阵键集(契约1.48):每未删一级分类 = 主体方案(OWN/OTHER)+ 非仅主案
|
|
|
+ * 分类的附件四类型(OWN);外加共享资料库(topCategoryId=null)附件四类型(OWN)。 */
|
|
|
+ private List<MatrixKey> expectedMatrixKeys() {
|
|
|
+ List<MatrixKey> keys = new ArrayList<>();
|
|
|
+ List<Long> topIds = new ArrayList<>();
|
|
|
+ for (CategoryEntity category : categoryMapper.listActiveOrdered()) {
|
|
|
+ if (category.getParentId() == null) {
|
|
|
+ topIds.add(category.getId());
|
|
|
+ boolean mainOnly = Boolean.TRUE.equals(category.getFlatMode());
|
|
|
+ for (String level : SECURITY_LEVELS) {
|
|
|
+ keys.add(new MatrixKey(category.getId(), DOC_KIND_MAIN, level, "OWN"));
|
|
|
+ keys.add(new MatrixKey(category.getId(), DOC_KIND_MAIN, level, "OTHER"));
|
|
|
+ if (!mainOnly) {
|
|
|
+ for (String kind : ATTACHMENT_KINDS) {
|
|
|
+ keys.add(new MatrixKey(category.getId(), kind, level, "OWN"));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ for (String level : SECURITY_LEVELS) {
|
|
|
+ for (String kind : ATTACHMENT_KINDS) {
|
|
|
+ keys.add(new MatrixKey(null, kind, level, "OWN"));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return keys;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static Long longId(Object value) {
|
|
|
+ return value == null ? null : Long.valueOf((String) value);
|
|
|
+ }
|
|
|
+
|
|
|
private static Map<String, Object> entityFlags(RolePermissionEntity row) {
|
|
|
Map<String, Object> flags = new LinkedHashMap<>();
|
|
|
flags.put("canView", row.getCanView());
|
|
|
@@ -634,6 +798,11 @@ public class RoleService {
|
|
|
flags.put("canConfigPermission", row.getCanConfigPermission());
|
|
|
flags.put("canBindAttachment", row.getCanBindAttachment());
|
|
|
flags.put("canUnbindAttachment", row.getCanUnbindAttachment());
|
|
|
+ flags.put("canAttachmentUpload", row.getCanAttachmentUpload());
|
|
|
+ flags.put("canAttachmentEdit", row.getCanAttachmentEdit());
|
|
|
+ flags.put("canAttachmentView", row.getCanAttachmentView());
|
|
|
+ flags.put("canAttachmentDownload", row.getCanAttachmentDownload());
|
|
|
+ flags.put("canAttachmentDelete", row.getCanAttachmentDelete());
|
|
|
return flags;
|
|
|
}
|
|
|
|
|
|
@@ -654,16 +823,11 @@ public class RoleService {
|
|
|
row.setCanConfigPermission((Boolean) item.get("canConfigPermission"));
|
|
|
row.setCanBindAttachment((Boolean) item.get("canBindAttachment"));
|
|
|
row.setCanUnbindAttachment((Boolean) item.get("canUnbindAttachment"));
|
|
|
- }
|
|
|
-
|
|
|
- private static Set<MatrixKey> buildMatrixKeys() {
|
|
|
- Set<MatrixKey> keys = new HashSet<>();
|
|
|
- for (String level : SECURITY_LEVELS) {
|
|
|
- for (String scope : DEPT_SCOPES) {
|
|
|
- keys.add(new MatrixKey(level, scope));
|
|
|
- }
|
|
|
- }
|
|
|
- return keys;
|
|
|
+ row.setCanAttachmentUpload((Boolean) item.get("canAttachmentUpload"));
|
|
|
+ row.setCanAttachmentEdit((Boolean) item.get("canAttachmentEdit"));
|
|
|
+ row.setCanAttachmentView((Boolean) item.get("canAttachmentView"));
|
|
|
+ row.setCanAttachmentDownload((Boolean) item.get("canAttachmentDownload"));
|
|
|
+ row.setCanAttachmentDelete((Boolean) item.get("canAttachmentDelete"));
|
|
|
}
|
|
|
|
|
|
private static void checkVersion(RoleEntity entity, int expectedVersion) {
|
|
|
@@ -693,6 +857,45 @@ public class RoleService {
|
|
|
return id == null ? null : String.valueOf(id);
|
|
|
}
|
|
|
|
|
|
+ private static DmsException forbidden(String message) {
|
|
|
+ return new DmsException("FORBIDDEN", 403, message, null);
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 契约1.60 防提权:管理位任一勾选的角色即管理员角色。 */
|
|
|
+ private static boolean hasAnyManageFlag(RoleEntity role) {
|
|
|
+ return Boolean.TRUE.equals(role.getManageCategoryEdit())
|
|
|
+ || Boolean.TRUE.equals(role.getManageOrgEdit())
|
|
|
+ || Boolean.TRUE.equals(role.getManageUserEdit())
|
|
|
+ || Boolean.TRUE.equals(role.getManageSystem())
|
|
|
+ || Boolean.TRUE.equals(role.getManageAuditLog())
|
|
|
+ || Boolean.TRUE.equals(role.getManageRoleEdit());
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 契约1.60 防提权守卫:非内置超管不能修改管理员角色的任何设置
|
|
|
+ * (基本信息与权限矩阵),也不能设置/变更任何角色的管理位
|
|
|
+ * (新建勾选与编辑变更同样拒绝,原值回传放行)。
|
|
|
+ */
|
|
|
+ private static void guardRoleAdminEdit(AuthContext actor, boolean targetIsAdminRole,
|
|
|
+ boolean manageFlagsChanged) {
|
|
|
+ if (AuthorizationService.isSuperAdmin(actor)) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (targetIsAdminRole) {
|
|
|
+ throw forbidden("管理员角色仅超级管理员可修改");
|
|
|
+ }
|
|
|
+ if (manageFlagsChanged) {
|
|
|
+ throw forbidden("管理权限仅超级管理员可设置");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 契约1.61:新增与删除角色仅内置超管可操作(防越权改组角色体系)。 */
|
|
|
+ private static void guardRoleCreateDelete(AuthContext actor) {
|
|
|
+ if (!AuthorizationService.isSuperAdmin(actor)) {
|
|
|
+ throw forbidden("新增与删除角色仅超级管理员可操作");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
private static DmsException conflict(String message, Object details) {
|
|
|
return new DmsException("DATA_VERSION_CONFLICT", 409, message, details);
|
|
|
}
|
|
|
@@ -701,7 +904,12 @@ public class RoleService {
|
|
|
return new DmsException("RESOURCE_NOT_FOUND", 404, message);
|
|
|
}
|
|
|
|
|
|
- /** 矩阵定位键:密级 × 部门范围。 */
|
|
|
- private record MatrixKey(String securityLevel, String deptScope) {
|
|
|
+ /** 矩阵定位键(契约1.48):一级分类 × 种类 × 密级 × 部门范围(topCategoryId null=共享资料库)。 */
|
|
|
+ private record MatrixKey(Long topCategoryId, String docKind,
|
|
|
+ String securityLevel, String deptScope) {
|
|
|
+ static MatrixKey of(RolePermissionEntity row) {
|
|
|
+ return new MatrixKey(row.getTopCategoryId(), row.getDocKind(),
|
|
|
+ row.getSecurityLevel(), row.getDeptScope());
|
|
|
+ }
|
|
|
}
|
|
|
}
|