package jnpf.dmsService;
|
|
import jnpf.dmsEntity.permission.entity.DmsFilePermissionEntity;
|
import jnpf.dmsEntity.permission.entity.DmsPermissionResourceEntity;
|
import jnpf.dmsEntity.permission.enums.DmsActorType;
|
import jnpf.dmsEntity.permission.enums.DmsPermissionAction;
|
import jnpf.dmsEntity.permission.enums.DmsPrincipalType;
|
import jnpf.dmsEntity.permission.enums.DmsResourceScope;
|
import jnpf.dmsEntity.permission.enums.DmsResourceType;
|
import jnpf.dmsEntity.permission.form.PermissionGrantBatchForm;
|
import jnpf.dmsEntity.permission.form.PermissionGrantItemForm;
|
import jnpf.dmsEntity.permission.form.PermissionRevokeForm;
|
import jnpf.dmsEntity.permission.vo.DmsPermissionGrantVO;
|
import jnpf.dmsIntegration.audit.DmsAuditAdapter;
|
import jnpf.dmsMapper.DmsFilePermissionMapper;
|
import jnpf.dmsMapper.DmsPermissionResourceMapper;
|
import jnpf.dmsPermission.DmsCurrentIdentity;
|
import jnpf.dmsPermission.DmsPermissionError;
|
import jnpf.dmsPermission.DmsPermissionException;
|
import jnpf.dmsPermission.DmsPermissionProperties;
|
import jnpf.dmsPermission.DmsPrincipalValidator;
|
import lombok.RequiredArgsConstructor;
|
import org.springframework.dao.DuplicateKeyException;
|
import org.springframework.stereotype.Service;
|
import org.springframework.transaction.annotation.Transactional;
|
|
import java.util.ArrayList;
|
import java.util.Collections;
|
import java.util.Comparator;
|
import java.util.Date;
|
import java.util.HashMap;
|
import java.util.HashSet;
|
import java.util.LinkedHashMap;
|
import java.util.LinkedHashSet;
|
import java.util.List;
|
import java.util.Map;
|
import java.util.Objects;
|
import java.util.Set;
|
import java.util.UUID;
|
import java.util.function.Function;
|
import java.util.stream.Collectors;
|
|
@Service
|
@RequiredArgsConstructor
|
public class DmsPermissionGrantService {
|
private static final String FOLDER_CREATOR_CREATE_SOURCE_PREFIX = "folder-creator-create:";
|
private static final String FOLDER_CREATOR_CREATE_REASON = "目录创建者默认 CREATE 权限";
|
|
private final DmsCurrentIdentity currentIdentity;
|
private final DmsPermissionProperties properties;
|
private final DmsPrincipalValidator principalValidator;
|
private final DmsPermissionResourceMapper resourceMapper;
|
private final DmsFilePermissionMapper permissionMapper;
|
private final DmsAuditAdapter auditAdapter;
|
|
@Transactional
|
public List<DmsPermissionGrantVO> grant(PermissionGrantBatchForm form) {
|
currentIdentity.requirePermissionAdministrator();
|
if (form.getItems().size() > properties.getMaxGrantBatchSize()) {
|
throw DmsPermissionException.of(DmsPermissionError.INVALID_REQUEST,
|
"授权批量大小不能超过 " + properties.getMaxGrantBatchSize());
|
}
|
String tenantId = currentIdentity.tenantId();
|
String sourceType = form.getSourceType().name();
|
String sourceId = form.getSourceId().trim();
|
List<PermissionGrantItemForm> items = normalizeAndValidate(form.getItems());
|
permissionMapper.lockSource(tenantId, sourceType, sourceId);
|
List<DmsFilePermissionEntity> existing = permissionMapper.selectBySource(tenantId, sourceType, sourceId);
|
if (!existing.isEmpty()) {
|
verifyIdempotentRetry(existing, items);
|
return existing.stream().map(this::toVO).collect(Collectors.toList());
|
}
|
|
validateResources(tenantId, items);
|
principalValidator.validate(items);
|
List<DmsFilePermissionEntity> created = new ArrayList<>();
|
try {
|
for (PermissionGrantItemForm item : items) {
|
DmsFilePermissionEntity permission = toEntity(tenantId, sourceType, sourceId, item);
|
created.add(permissionMapper.insertReturning(permission));
|
}
|
} catch (DuplicateKeyException ex) {
|
throw DmsPermissionException.of(DmsPermissionError.SOURCE_CONFLICT,
|
"授权来源或授权项与已有数据冲突");
|
}
|
for (DmsFilePermissionEntity permission : created) {
|
auditAdapter.dataChange("CREATE", "创建 DMS 文件权限", "dms_file_permissions",
|
permission.getId().toString(), permission.getGrantReason(), null);
|
}
|
return created.stream().map(this::toVO).collect(Collectors.toList());
|
}
|
|
@Transactional
|
public DmsFilePermissionEntity ensureFolderCreatorCreatePermission(String tenantId, UUID folderId,
|
String creatorUserId) {
|
String sourceType = "SYSTEM";
|
String sourceId = FOLDER_CREATOR_CREATE_SOURCE_PREFIX + folderId;
|
permissionMapper.lockSource(tenantId, sourceType, sourceId);
|
List<DmsFilePermissionEntity> existing = permissionMapper.selectBySource(tenantId, sourceType, sourceId);
|
if (!existing.isEmpty()) {
|
if (existing.size() != 1 || !sameFolderCreatorCreatePermission(
|
existing.get(0), tenantId, folderId, creatorUserId, sourceId)) {
|
throw DmsPermissionException.of(DmsPermissionError.SOURCE_CONFLICT,
|
"目录创建者默认权限来源与已有授权不一致");
|
}
|
return existing.get(0);
|
}
|
|
DmsFilePermissionEntity permission = new DmsFilePermissionEntity();
|
permission.setTenantId(tenantId);
|
permission.setResourceType(DmsResourceType.FOLDER.name());
|
permission.setResourceId(folderId);
|
permission.setResourceScope(DmsResourceScope.SELF.name());
|
permission.setPrincipalType(DmsPrincipalType.USER.name());
|
permission.setPrincipalId(creatorUserId);
|
permission.setIncludeChildOrgs(false);
|
permission.setActionCode(DmsPermissionAction.CREATE.name());
|
permission.setSourceType(sourceType);
|
permission.setSourceId(sourceId);
|
permission.setValidFrom(new Date());
|
permission.setGrantReason(FOLDER_CREATOR_CREATE_REASON);
|
permission.setGrantedByType(DmsActorType.USER.name());
|
permission.setGrantedById(creatorUserId);
|
try {
|
permission = permissionMapper.insertReturning(permission);
|
} catch (DuplicateKeyException ex) {
|
throw DmsPermissionException.of(DmsPermissionError.SOURCE_CONFLICT,
|
"目录创建者默认权限与已有授权冲突");
|
}
|
auditAdapter.dataChange("CREATE", "创建 DMS 目录创建者默认权限", "dms_file_permissions",
|
permission.getId().toString(), FOLDER_CREATOR_CREATE_REASON, null);
|
return permission;
|
}
|
|
@Transactional
|
public List<DmsPermissionGrantVO> revoke(PermissionRevokeForm form) {
|
currentIdentity.requirePermissionAdministrator();
|
String tenantId = currentIdentity.tenantId();
|
String actorId = currentIdentity.userId();
|
String reason = form.getReason().trim();
|
List<UUID> ids = new ArrayList<>(new LinkedHashSet<>(form.getPermissionIds()));
|
ids.sort(Comparator.comparing(UUID::toString));
|
List<DmsFilePermissionEntity> locked = new ArrayList<>();
|
for (UUID id : ids) {
|
DmsFilePermissionEntity permission = permissionMapper.selectByTenantAndIdForUpdate(tenantId, id);
|
if (permission == null) {
|
throw DmsPermissionException.of(DmsPermissionError.RESOURCE_NOT_FOUND,
|
"权限不存在: " + id);
|
}
|
if (permission.getRevokedAt() != null
|
&& (!DmsActorType.USER.name().equals(permission.getRevokedByType())
|
|| !actorId.equals(permission.getRevokedById())
|
|| !reason.equals(permission.getRevokeReason()))) {
|
throw DmsPermissionException.of(DmsPermissionError.REVISION_CONFLICT,
|
"权限已被其他请求撤销: " + id);
|
}
|
locked.add(permission);
|
}
|
for (DmsFilePermissionEntity permission : locked) {
|
if (permission.getRevokedAt() == null) {
|
int changed = permissionMapper.revoke(tenantId, permission.getId(), DmsActorType.USER.name(),
|
actorId, reason);
|
if (changed != 1) {
|
throw DmsPermissionException.of(DmsPermissionError.REVISION_CONFLICT,
|
"权限撤销发生并发冲突: " + permission.getId());
|
}
|
auditAdapter.dataChange("UPDATE", "撤销 DMS 文件权限", "dms_file_permissions",
|
permission.getId().toString(), reason, null);
|
}
|
}
|
return ids.stream().map(id -> toVO(permissionMapper.selectByTenantAndId(tenantId, id)))
|
.collect(Collectors.toList());
|
}
|
|
private List<PermissionGrantItemForm> normalizeAndValidate(List<PermissionGrantItemForm> source) {
|
List<PermissionGrantItemForm> result = new ArrayList<>();
|
Set<String> identities = new HashSet<>();
|
for (PermissionGrantItemForm original : source) {
|
PermissionGrantItemForm item = copy(original);
|
validateItem(item);
|
String identity = identity(item);
|
if (!identities.add(identity)) {
|
throw DmsPermissionException.of(DmsPermissionError.INVALID_REQUEST,
|
"同一授权批次包含重复授权项");
|
}
|
result.add(item);
|
}
|
result.sort(Comparator.comparing(this::identity));
|
return result;
|
}
|
|
private PermissionGrantItemForm copy(PermissionGrantItemForm value) {
|
PermissionGrantItemForm result = new PermissionGrantItemForm();
|
result.setResourceType(value.getResourceType());
|
result.setResourceId(value.getResourceId());
|
result.setResourceScope(value.getResourceScope());
|
result.setPrincipalType(value.getPrincipalType());
|
result.setPrincipalId(value.getPrincipalId().trim());
|
result.setIncludeChildOrgs(value.isIncludeChildOrgs());
|
result.setActionCode(value.getActionCode());
|
result.setValidFrom(value.getValidFrom());
|
result.setValidTo(value.getValidTo());
|
result.setMaxUseCount(value.getMaxUseCount());
|
result.setGrantReason(trimToNull(value.getGrantReason()));
|
return result;
|
}
|
|
private void validateItem(PermissionGrantItemForm item) {
|
if (item.getResourceType() != DmsResourceType.FOLDER
|
&& item.getResourceScope() != DmsResourceScope.SELF) {
|
invalid("Document 和 FileVersion 只允许 SELF 范围");
|
}
|
if (item.getActionCode() == DmsPermissionAction.CREATE
|
&& item.getResourceType() != DmsResourceType.FOLDER) {
|
invalid("CREATE 只允许授予 Folder");
|
}
|
if (item.isIncludeChildOrgs() && item.getPrincipalType() != DmsPrincipalType.ORG) {
|
invalid("includeChildOrgs 只适用于 ORG 主体");
|
}
|
if (item.getMaxUseCount() != null && item.getPrincipalType() != DmsPrincipalType.USER) {
|
invalid("按次权限只允许授予 USER 主体");
|
}
|
Date effectiveFrom = item.getValidFrom() == null ? new Date() : item.getValidFrom();
|
if (item.getValidTo() != null && !item.getValidTo().after(effectiveFrom)) {
|
invalid("validTo 必须晚于 validFrom");
|
}
|
}
|
|
private void validateResources(String tenantId, List<PermissionGrantItemForm> items) {
|
List<UUID> ids = items.stream().map(PermissionGrantItemForm::getResourceId).distinct()
|
.collect(Collectors.toList());
|
Map<UUID, DmsPermissionResourceEntity> resources = resourceMapper.selectByTenantAndIds(tenantId, ids)
|
.stream().collect(Collectors.toMap(DmsPermissionResourceEntity::getId, Function.identity()));
|
for (PermissionGrantItemForm item : items) {
|
DmsPermissionResourceEntity resource = resources.get(item.getResourceId());
|
if (resource == null || !item.getResourceType().name().equals(resource.getResourceType())) {
|
throw DmsPermissionException.of(DmsPermissionError.RESOURCE_NOT_FOUND,
|
"授权资源不存在或类型不一致: " + item.getResourceId());
|
}
|
}
|
}
|
|
private void verifyIdempotentRetry(List<DmsFilePermissionEntity> existing,
|
List<PermissionGrantItemForm> requested) {
|
if (existing.size() != requested.size() || existing.stream().anyMatch(row -> row.getRevokedAt() != null)) {
|
throw DmsPermissionException.of(DmsPermissionError.SOURCE_CONFLICT,
|
"source_id 已存在,但授权集合数量不同或包含已撤销权限");
|
}
|
Map<String, DmsFilePermissionEntity> byIdentity = existing.stream().collect(Collectors.toMap(
|
this::identity, Function.identity(), (left, right) -> left, LinkedHashMap::new));
|
for (PermissionGrantItemForm item : requested) {
|
DmsFilePermissionEntity row = byIdentity.get(identity(item));
|
if (row == null || !sameDefinition(row, item)) {
|
throw DmsPermissionException.of(DmsPermissionError.SOURCE_CONFLICT,
|
"source_id 已存在,但完整授权集合与本次请求不一致");
|
}
|
}
|
}
|
|
private boolean sameDefinition(DmsFilePermissionEntity row, PermissionGrantItemForm item) {
|
return (item.getValidFrom() == null || sameInstant(row.getValidFrom(), item.getValidFrom()))
|
&& sameInstant(row.getValidTo(), item.getValidTo())
|
&& Objects.equals(row.getMaxUseCount(), item.getMaxUseCount())
|
&& Objects.equals(row.getGrantReason(), item.getGrantReason());
|
}
|
|
private boolean sameFolderCreatorCreatePermission(DmsFilePermissionEntity row, String tenantId,
|
UUID folderId, String creatorUserId, String sourceId) {
|
return tenantId.equals(row.getTenantId())
|
&& DmsResourceType.FOLDER.name().equals(row.getResourceType())
|
&& folderId.equals(row.getResourceId())
|
&& DmsResourceScope.SELF.name().equals(row.getResourceScope())
|
&& DmsPrincipalType.USER.name().equals(row.getPrincipalType())
|
&& creatorUserId.equals(row.getPrincipalId())
|
&& Boolean.FALSE.equals(row.getIncludeChildOrgs())
|
&& DmsPermissionAction.CREATE.name().equals(row.getActionCode())
|
&& "SYSTEM".equals(row.getSourceType())
|
&& sourceId.equals(row.getSourceId())
|
&& row.getValidTo() == null
|
&& row.getMaxUseCount() == null
|
&& FOLDER_CREATOR_CREATE_REASON.equals(row.getGrantReason())
|
&& DmsActorType.USER.name().equals(row.getGrantedByType())
|
&& creatorUserId.equals(row.getGrantedById());
|
}
|
|
private DmsFilePermissionEntity toEntity(String tenantId, String sourceType, String sourceId,
|
PermissionGrantItemForm item) {
|
DmsFilePermissionEntity entity = new DmsFilePermissionEntity();
|
entity.setTenantId(tenantId);
|
entity.setResourceType(item.getResourceType().name());
|
entity.setResourceId(item.getResourceId());
|
entity.setResourceScope(item.getResourceScope().name());
|
entity.setPrincipalType(item.getPrincipalType().name());
|
entity.setPrincipalId(item.getPrincipalId());
|
entity.setIncludeChildOrgs(item.isIncludeChildOrgs());
|
entity.setActionCode(item.getActionCode().name());
|
entity.setSourceType(sourceType);
|
entity.setSourceId(sourceId);
|
entity.setValidFrom(item.getValidFrom() == null ? new Date() : item.getValidFrom());
|
entity.setValidTo(item.getValidTo());
|
entity.setMaxUseCount(item.getMaxUseCount());
|
entity.setGrantReason(item.getGrantReason());
|
entity.setGrantedByType(DmsActorType.USER.name());
|
entity.setGrantedById(currentIdentity.userId());
|
return entity;
|
}
|
|
private String identity(PermissionGrantItemForm item) {
|
return item.getResourceType() + "|" + item.getResourceId() + "|" + item.getResourceScope()
|
+ "|" + item.getPrincipalType() + "|" + item.getPrincipalId() + "|"
|
+ item.isIncludeChildOrgs() + "|" + item.getActionCode();
|
}
|
|
private String identity(DmsFilePermissionEntity item) {
|
return item.getResourceType() + "|" + item.getResourceId() + "|" + item.getResourceScope()
|
+ "|" + item.getPrincipalType() + "|" + item.getPrincipalId() + "|"
|
+ item.getIncludeChildOrgs() + "|" + item.getActionCode();
|
}
|
|
private boolean sameInstant(Date left, Date right) {
|
return left == null ? right == null : right != null && left.toInstant().equals(right.toInstant());
|
}
|
|
private String trimToNull(String value) {
|
return value == null || value.trim().isEmpty() ? null : value.trim();
|
}
|
|
private void invalid(String message) {
|
throw DmsPermissionException.of(DmsPermissionError.INVALID_REQUEST, message);
|
}
|
|
public DmsPermissionGrantVO toVO(DmsFilePermissionEntity value) {
|
DmsPermissionGrantVO result = new DmsPermissionGrantVO();
|
result.setId(value.getId());
|
result.setResourceType(value.getResourceType());
|
result.setResourceId(value.getResourceId());
|
result.setResourceScope(value.getResourceScope());
|
result.setPrincipalType(value.getPrincipalType());
|
result.setPrincipalId(value.getPrincipalId());
|
result.setIncludeChildOrgs(value.getIncludeChildOrgs());
|
result.setActionCode(value.getActionCode());
|
result.setSourceType(value.getSourceType());
|
result.setSourceId(value.getSourceId());
|
result.setValidFrom(value.getValidFrom());
|
result.setValidTo(value.getValidTo());
|
result.setMaxUseCount(value.getMaxUseCount());
|
result.setUsedCount(value.getUsedCount());
|
result.setGrantReason(value.getGrantReason());
|
result.setRevokedAt(value.getRevokedAt());
|
result.setRevokeReason(value.getRevokeReason());
|
result.setRevision(value.getRevision());
|
result.setCreatedAt(value.getCreatedAt());
|
result.setUpdatedAt(value.getUpdatedAt());
|
return result;
|
}
|
}
|