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> T execute(DmsPermissionExecutionContext context, DmsPermissionOperation<T> 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 <T> void validateContext(DmsPermissionExecutionContext context, DmsPermissionOperation<T> operation) {
|
if (context == null || operation == null || context.getResourceType() == null
|
|| context.getResourceId() == null || context.getActionCode() == null) {
|
throw DmsPermissionException.of(DmsPermissionError.INVALID_REQUEST,
|
"PermissionGuard 执行上下文不完整");
|
}
|
}
|
}
|