package jnpf.dmsPermission; import jnpf.base.UserInfo; import jnpf.dmsEntity.permission.entity.DmsFilePermissionEntity; import jnpf.dmsEntity.permission.entity.DmsPermissionUsageEntity; import jnpf.dmsEntity.permission.enums.DmsPermissionAction; import jnpf.dmsEntity.permission.form.PermissionDecisionForm; import jnpf.dmsEntity.permission.model.DmsResourcePath; import jnpf.dmsEntity.permission.model.DmsSubjectSet; import jnpf.dmsEntity.permission.vo.DmsPermissionDecisionVO; import jnpf.dmsMapper.DmsFilePermissionMapper; import jnpf.dmsMapper.DmsPermissionUsageMapper; import jnpf.dmsService.DmsPermissionDecisionService; import lombok.RequiredArgsConstructor; import org.springframework.dao.DuplicateKeyException; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; import java.util.Collections; import java.util.Date; import java.util.UUID; @Component @RequiredArgsConstructor public class DmsPermissionGuard { private final DmsCurrentIdentity currentIdentity; private final DmsPermissionDecisionService decisionService; private final DmsFilePermissionMapper permissionMapper; private final DmsPermissionUsageMapper usageMapper; private final DmsResourceResolver resourceResolver; private final DmsSubjectResolver subjectResolver; private final DmsPermissionGroupMembershipResolver groupMembershipResolver; private final DmsPermissionMatcher matcher; private final DmsAccessExclusionResolver exclusionResolver; @Transactional public T execute(DmsPermissionExecutionContext context, DmsPermissionOperation operation) { validateContext(context, operation); UserInfo user = currentIdentity.requireUser(); String tenantId = currentIdentity.tenantId(user); if (context.getOperationId() != null) { DmsPermissionUsageEntity prior = usageMapper.selectByOperation(tenantId, user.getUserId(), context.getOperationId()); if (prior != null) { DmsResourcePath path = resourceResolver.resolve(tenantId, context.getResourceType(), context.getResourceId()); ensureNotReadExcluded(context, tenantId, user.getUserId().trim(), path); validateReplay(prior, context, user, tenantId, path); return operation.replay(); } } PermissionDecisionForm request = new PermissionDecisionForm(); request.setResourceType(context.getResourceType()); request.setResourceId(context.getResourceId()); request.setActionCode(context.getActionCode()); DmsPermissionDecisionVO decision = decisionService.evaluate(request); if (!decision.isAllowed()) { throw DmsPermissionException.of(DmsPermissionError.PERMISSION_DENIED, "当前用户没有所需的 " + context.getActionCode() + " 权限"); } DmsFilePermissionEntity permission = permissionMapper.selectByTenantAndIdForUpdate(tenantId, decision.getMatchedPermissionId()); if (permission == null) { throw DmsPermissionException.of(DmsPermissionError.PERMISSION_DENIED, "匹配权限已不存在"); } if (context.getOperationId() != null) { DmsPermissionUsageEntity concurrentPrior = usageMapper.selectByOperation(tenantId, user.getUserId(), context.getOperationId()); if (concurrentPrior != null) { DmsResourcePath path = resourceResolver.resolve(tenantId, context.getResourceType(), context.getResourceId()); ensureNotReadExcluded(context, tenantId, user.getUserId().trim(), path); validateReplay(concurrentPrior, context, user, tenantId, path); return operation.replay(); } } DmsResourcePath path = resourceResolver.resolve(tenantId, context.getResourceType(), context.getResourceId()); ensureNotReadExcluded(context, tenantId, user.getUserId().trim(), path); validateLockedPermission(permission, context, user, tenantId, path); if (permission.getMaxUseCount() == null) { return operation.execute(); } if (context.getOperationId() == null) { throw DmsPermissionException.of(DmsPermissionError.INVALID_REQUEST, "执行按次权限必须提供 operationId"); } Date sessionExpiresAt = normalizedSessionExpiry(context); T result = operation.execute(); DmsPermissionUsageEntity usage = new DmsPermissionUsageEntity(); usage.setTenantId(tenantId); usage.setPermissionId(permission.getId()); usage.setUserId(user.getUserId()); usage.setActionCode(context.getActionCode().name()); usage.setActualResourceType(context.getResourceType().name()); usage.setActualResourceId(context.getResourceId()); usage.setOperationId(context.getOperationId()); usage.setSessionExpiresAt(sessionExpiresAt); try { usageMapper.insert(usage); } catch (DuplicateKeyException ex) { throw DmsPermissionException.of(DmsPermissionError.IDEMPOTENCY_CONFLICT, "operationId 已被并发请求使用,请按相同 operationId 重试"); } return result; } private void validateLockedPermission(DmsFilePermissionEntity permission, DmsPermissionExecutionContext context, UserInfo user, String tenantId, DmsResourcePath path) { Date now = new Date(); if (!tenantId.equals(permission.getTenantId()) || !context.getActionCode().name().equals(permission.getActionCode()) || permission.getRevokedAt() != null || permission.getValidFrom().after(now) || (permission.getValidTo() != null && !now.before(permission.getValidTo()))) { throw DmsPermissionException.of(DmsPermissionError.PERMISSION_DENIED, "匹配权限在执行前已失效"); } if (permission.getMaxUseCount() != null && permission.getUsedCount() >= permission.getMaxUseCount()) { throw DmsPermissionException.of(DmsPermissionError.PERMISSION_EXHAUSTED, "匹配权限的使用次数已用尽"); } DmsSubjectSet subjects = subjectResolver.resolve(user); if ("GROUP".equals(permission.getPrincipalType())) { groupMembershipResolver.resolve(Collections.singleton(permission.getPrincipalId()), subjects); } if (!matcher.matches(permission, path, subjects)) { throw DmsPermissionException.of(DmsPermissionError.PERMISSION_DENIED, "当前主体或资源在执行前已不再匹配权限"); } } private void validateReplay(DmsPermissionUsageEntity prior, DmsPermissionExecutionContext context, UserInfo user, String tenantId, DmsResourcePath path) { if (!context.getResourceType().name().equals(prior.getActualResourceType()) || !context.getResourceId().equals(prior.getActualResourceId()) || !context.getActionCode().name().equals(prior.getActionCode())) { throw DmsPermissionException.of(DmsPermissionError.IDEMPOTENCY_CONFLICT, "operationId 已用于不同资源或动作"); } if ((context.getActionCode() == DmsPermissionAction.READ || context.getActionCode() == DmsPermissionAction.UPDATE) && prior.getSessionExpiresAt() != null && !new Date().before(prior.getSessionExpiresAt())) { throw DmsPermissionException.of(DmsPermissionError.IDEMPOTENCY_CONFLICT, "operationId 对应的权限会话已过期,请使用新的 operationId"); } DmsFilePermissionEntity permission = permissionMapper.selectByTenantAndId(tenantId, prior.getPermissionId()); Date now = new Date(); if (permission == null || permission.getRevokedAt() != null || permission.getValidFrom().after(now) || (permission.getValidTo() != null && !now.before(permission.getValidTo()))) { throw DmsPermissionException.of(DmsPermissionError.PERMISSION_DENIED, "原按次权限已撤销或不在有效期内"); } DmsSubjectSet subjects = subjectResolver.resolve(user); if ("GROUP".equals(permission.getPrincipalType())) { groupMembershipResolver.resolve(Collections.singleton(permission.getPrincipalId()), subjects); } if (!permission.getActionCode().equals(context.getActionCode().name()) || !matcher.matches(permission, path, subjects)) { throw DmsPermissionException.of(DmsPermissionError.PERMISSION_DENIED, "当前主体或资源不再匹配原按次权限"); } } private void ensureNotReadExcluded(DmsPermissionExecutionContext context, String tenantId, String userId, DmsResourcePath path) { if (context.getActionCode() == DmsPermissionAction.READ && exclusionResolver.isExcluded(tenantId, userId, path)) { throw DmsPermissionException.of(DmsPermissionError.PERMISSION_DENIED, "当前用户的 READ 权限已被排除"); } } private Date normalizedSessionExpiry(DmsPermissionExecutionContext context) { if (context.getActionCode() != DmsPermissionAction.READ && context.getActionCode() != DmsPermissionAction.UPDATE) { return null; } Date expiry = context.getSessionExpiresAt(); if (expiry != null && !expiry.after(new Date())) { throw DmsPermissionException.of(DmsPermissionError.INVALID_REQUEST, "sessionExpiresAt 必须晚于当前时间"); } return expiry; } private void validateContext(DmsPermissionExecutionContext context, DmsPermissionOperation operation) { if (context == null || operation == null || context.getResourceType() == null || context.getResourceId() == null || context.getActionCode() == null) { throw DmsPermissionException.of(DmsPermissionError.INVALID_REQUEST, "PermissionGuard 执行上下文不完整"); } } }