Files
baiyue-firstleg/iam/src/main/java/com/baiyue/iam/service/RbacService.java
T

349 lines
18 KiB
Java

package com.baiyue.iam.service;
import com.baiyue.common.audit.BusinessAudit;
import com.baiyue.common.core.api.ApiResponse;
import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.iam.client.AuthAccountClient;
import com.baiyue.iam.dto.RbacDtos.*;
import com.baiyue.iam.entity.IamMembership;
import com.baiyue.iam.entity.IamPermission;
import com.baiyue.iam.entity.IamRole;
import com.baiyue.iam.repository.MemberRepository;
import com.baiyue.iam.repository.RbacRepository;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import jakarta.annotation.Resource;
import feign.FeignException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.dao.DataIntegrityViolationException;
import java.util.LinkedHashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Comparator;
/** 双端 RBAC 应用服务,严格按租户和应用域隔离。 */
@Service
public class RbacService {
private static final int SESSION_REVOCATION_BATCH_SIZE = 500;
@Resource private RbacRepository repository;
@Resource private MemberRepository memberRepository;
@Resource private AuthAccountClient authClient;
@Resource private SecurityEventOutboxService securityEvents;
@Value("${baiyue.security.internal-service-token}")
private String internalToken;
public List<RoleResponse> roles(long tenantId, String appCode) {
List<IamRole> roles = repository.findRoles(tenantId, appCode);
Map<Long, Long> memberCounts = repository.findRoleMemberCounts(
tenantId, roles.stream().map(IamRole::getId).toList());
return roles.stream()
.map(role -> response(role, repository.findRolePermissionIds(tenantId, role.getId()),
memberCounts.getOrDefault(role.getId(), 0L)))
.toList();
}
public RoleResponse role(long tenantId, String appCode, Long roleId) {
IamRole role = requireRole(tenantId, appCode, roleId);
return response(role, repository.findRolePermissionIds(tenantId, roleId),
repository.countRoleMembers(tenantId, roleId));
}
@BusinessAudit(event = "ROLE_CREATE", resourceType = "ROLE", resourceId = "#request.roleCode",
description = "创建角色", captureBefore = false, captureAfter = true)
public RoleResponse createRole(long tenantId, String appCode, CreateRoleRequest request) {
if (repository.existsRoleCode(tenantId, appCode, request.roleCode(), null)) {
throw new BizException(ResultCode.CONFLICT, "角色编码已存在");
}
IamRole role = new IamRole();
role.setId(IdWorker.getId());
role.setTenantId(tenantId);
role.setAppCode(appCode);
role.setRoleCode(request.roleCode());
role.setRoleName(request.roleName());
role.setDescription(request.description());
role.setRoleType("CUSTOM");
role.setStatus("ACTIVE");
try {
repository.insertRole(role);
} catch (DataIntegrityViolationException exception) {
// 数据库唯一键负责兜底并发创建,向调用方保持稳定的业务错误。
throw new BizException(ResultCode.CONFLICT, "角色编码已存在");
}
return response(role, Set.of(), 0L);
}
@BusinessAudit(event = "ROLE_UPDATE", resourceType = "ROLE", resourceId = "#roleId",
description = "编辑角色", captureBefore = true, captureAfter = true)
public RoleResponse updateRole(long tenantId, String appCode, Long roleId, UpdateRoleRequest request) {
IamRole role = requireCustomRole(tenantId, appCode, roleId);
role.setRoleName(request.roleName());
role.setDescription(request.description());
repository.updateRole(role);
return response(role, repository.findRolePermissionIds(tenantId, roleId),
repository.countRoleMembers(tenantId, roleId));
}
@BusinessAudit(event = "ROLE_STATUS_CHANGE", resourceType = "ROLE", resourceId = "#roleId",
description = "启停角色", captureBefore = true, captureAfter = true)
@Transactional
public RoleResponse changeStatus(long tenantId, String appCode, Long roleId,
ChangeRoleStatusRequest request) {
IamRole role = requireCustomRole(tenantId, appCode, roleId);
role.setStatus(request.status());
repository.updateRole(role);
revokeAssignedSessions(tenantId, roleId);
return response(role, repository.findRolePermissionIds(tenantId, roleId),
repository.countRoleMembers(tenantId, roleId));
}
/** 删除策略:系统角色禁止删除;已分配给成员的角色拒绝删除;其余角色清理授权关系后逻辑删除。 */
@BusinessAudit(event = "ROLE_DELETE", resourceType = "ROLE", resourceId = "#roleId",
description = "删除角色", captureBefore = true, captureAfter = false)
@Transactional
public void deleteRole(long tenantId, String appCode, Long roleId) {
IamRole role = requireCustomRole(tenantId, appCode, roleId);
if (repository.isRoleAssigned(tenantId, role.getId())) {
throw new BizException(ResultCode.CONFLICT, "角色已分配给成员,不能删除");
}
repository.deleteRole(tenantId, appCode, roleId);
}
public List<IamPermission> permissions(long tenantId, String appCode) {
Map<String, IamPermission> effective = new LinkedHashMap<>();
repository.findPermissions(tenantId, appCode).stream()
.sorted(Comparator
.comparingInt((IamPermission permission) ->
permission.getTenantId() != null && permission.getTenantId() == tenantId ? 0 : 1)
.thenComparing(IamPermission::getPermissionCode))
.forEach(permission -> effective.putIfAbsent(permission.getPermissionCode(), permission));
return List.copyOf(effective.values());
}
@BusinessAudit(event = "PERMISSION_CREATE", resourceType = "PERMISSION", resourceId = "#request.code",
description = "创建权限资源", captureBefore = false, captureAfter = true)
public IamPermission createPermission(long tenantId, String appCode, CreatePermissionRequest request) {
if (!Set.of("MENU", "PAGE", "BUTTON", "API").contains(request.type())) {
throw new BizException(ResultCode.BAD_REQUEST, "权限类型只能是 MENU、PAGE、BUTTON 或 API");
}
if (repository.existsPermissionCode(tenantId, appCode, request.code())) {
throw new BizException(ResultCode.CONFLICT, "权限编码已存在");
}
if (request.parentId() != null && request.parentId() > 0
&& repository.findPermission(request.parentId(), tenantId, appCode) == null) {
throw forbidden();
}
IamPermission permission = new IamPermission();
permission.setId(IdWorker.getId());
permission.setTenantId(tenantId);
permission.setAppCode(appCode);
permission.setPermissionType(request.type());
permission.setPermissionCode(request.code());
permission.setPermissionName(request.name());
permission.setParentId(request.parentId() == null ? 0L : request.parentId());
permission.setRoutePath(request.routePath());
permission.setHttpMethod(request.httpMethod());
permission.setSortNo(request.sortNo() == null ? 0 : request.sortNo());
permission.setStatus("ACTIVE");
permission.setBuiltIn(false);
try {
repository.insertPermission(permission);
} catch (DataIntegrityViolationException exception) {
throw new BizException(ResultCode.CONFLICT, "权限编码已存在");
}
return permission;
}
@BusinessAudit(event = "PERMISSION_UPDATE", resourceType = "PERMISSION", resourceId = "#permissionId",
description = "编辑权限资源", captureBefore = true, captureAfter = true)
public IamPermission updatePermission(long tenantId, String appCode, Long permissionId,
UpdatePermissionRequest request) {
IamPermission permission = requireMutablePermission(tenantId, appCode, permissionId);
validatePermissionParent(tenantId, appCode, permissionId, request.parentId());
permission.setPermissionName(request.name());
permission.setParentId(request.parentId() == null ? 0L : request.parentId());
permission.setRoutePath(request.routePath());
permission.setHttpMethod(request.httpMethod());
permission.setSortNo(request.sortNo() == null ? 0 : request.sortNo());
repository.updatePermission(permission);
return permission;
}
@BusinessAudit(event = "PERMISSION_STATUS_CHANGE", resourceType = "PERMISSION", resourceId = "#permissionId",
description = "启停权限资源", captureBefore = true, captureAfter = true)
@Transactional
public IamPermission changePermissionStatus(long tenantId, String appCode, Long permissionId,
ChangePermissionStatusRequest request) {
IamPermission permission = requireMutablePermission(tenantId, appCode, permissionId);
permission.setStatus(request.status());
repository.updatePermission(permission);
Set<Long> affectedAccounts = new LinkedHashSet<>();
for (Long roleId : repository.findRoleIdsByPermission(tenantId, permissionId)) {
affectedAccounts.addAll(repository.findAssignedAuthUserIds(tenantId, roleId));
}
revokeSessions(List.copyOf(affectedAccounts));
return permission;
}
/** 带有子资源或已授权给角色的资源不得直接删除。 */
@BusinessAudit(event = "PERMISSION_DELETE", resourceType = "PERMISSION", resourceId = "#permissionId",
description = "删除权限资源", captureBefore = true, captureAfter = false)
@Transactional
public void deletePermission(long tenantId, String appCode, Long permissionId) {
IamPermission permission = requireMutablePermission(tenantId, appCode, permissionId);
if (repository.hasPermissionChildren(tenantId, appCode, permissionId)) {
throw new BizException(ResultCode.CONFLICT, "Permission has child resources / 权限资源存在子节点,不能删除");
}
if (repository.isPermissionGranted(tenantId, permissionId)) {
throw new BizException(ResultCode.CONFLICT, "Permission is granted to roles / 权限资源已被角色授予,不能删除");
}
repository.deletePermission(permission);
}
public MemberRolesResponse memberRoles(long tenantId, String appCode, Long membershipId) {
requireMembership(tenantId, membershipId);
Set<Long> scopedIds = new LinkedHashSet<>();
for (Long roleId : repository.findMemberRoleIds(tenantId, membershipId)) {
if (repository.findRole(roleId, tenantId, appCode) != null) scopedIds.add(roleId);
}
return new MemberRolesResponse(membershipId, Set.copyOf(scopedIds));
}
@BusinessAudit(event = "MEMBER_ROLE_ASSIGN", resourceType = "MEMBERSHIP",
resourceId = "#request.membershipId", description = "分配成员角色",
captureBefore = true, captureAfter = true)
@Transactional
public void assignRoles(long tenantId, String appCode, AssignRolesRequest request) {
IamMembership membership = requireMembership(tenantId, request.membershipId());
if ("OWNER".equals(membership.getMemberType())) {
throw new BizException(ResultCode.FORBIDDEN, "企业所有者角色不允许通过普通授权接口修改");
}
for (Long roleId : request.roleIds()) {
IamRole role = requireRole(tenantId, appCode, roleId);
if ("MERCHANT_OWNER".equals(role.getRoleCode())) {
throw new BizException(ResultCode.FORBIDDEN, "企业所有者角色不能通过普通授权接口分配");
}
if (!"ACTIVE".equals(role.getStatus())) {
throw new BizException(ResultCode.CONFLICT, "停用角色不能分配给成员");
}
}
Set<Long> currentRoleIds = new LinkedHashSet<>();
for (Long roleId : repository.findMemberRoleIds(tenantId, request.membershipId())) {
if (repository.findRole(roleId, tenantId, appCode) != null) currentRoleIds.add(roleId);
}
repository.replaceMemberRoles(tenantId, appCode, request.membershipId(), request.roleIds());
if (!currentRoleIds.equals(request.roleIds())) {
var user = memberRepository.findUser(membership.getUserId());
if (user == null || user.getAuthUserId() == null) throw sessionRevocationUnavailable();
revokeSessions(List.of(user.getAuthUserId()));
securityEvents.permissionChanged(user.getAuthUserId(), tenantId, appCode, "MEMBER_ROLES_CHANGED");
}
}
@BusinessAudit(event = "ROLE_PERMISSION_GRANT", resourceType = "ROLE",
resourceId = "#request.roleId", description = "设置角色权限",
captureBefore = true, captureAfter = true)
@Transactional
public void grantPermissions(long tenantId, String appCode, GrantPermissionsRequest request) {
requireCustomRole(tenantId, appCode, request.roleId());
for (Long permissionId : request.permissionIds()) {
IamPermission permission = repository.findPermission(permissionId, tenantId, appCode);
if (permission == null) throw forbidden();
if (!"ACTIVE".equals(permission.getStatus())) {
throw new BizException(ResultCode.CONFLICT, "停用权限不能授予角色");
}
}
repository.replaceRolePermissions(tenantId, request.roleId(), request.permissionIds());
List<Long> affected = repository.findAssignedAuthUserIds(tenantId, request.roleId());
revokeSessions(affected);
affected.forEach(userId -> securityEvents.permissionChanged(userId, tenantId, appCode,
"ROLE_PERMISSIONS_CHANGED"));
}
private void revokeAssignedSessions(long tenantId, Long roleId) {
revokeSessions(repository.findAssignedAuthUserIds(tenantId, roleId));
}
private void revokeSessions(List<Long> accountIds) {
for (int start = 0; start < accountIds.size(); start += SESSION_REVOCATION_BATCH_SIZE) {
Set<Long> batch = Set.copyOf(accountIds.subList(start,
Math.min(start + SESSION_REVOCATION_BATCH_SIZE, accountIds.size())));
try {
ApiResponse<Void> response = authClient.revokeSessions(internalToken,
new AuthAccountClient.RevokeAccountSessionsRequest(batch));
if (response == null || !response.successful()) throw sessionRevocationUnavailable();
} catch (FeignException exception) {
throw sessionRevocationUnavailable();
}
}
}
private IamRole requireRole(long tenantId, String appCode, Long roleId) {
IamRole role = repository.findRole(roleId, tenantId, appCode);
if (role == null) throw forbidden();
return role;
}
private IamPermission requireMutablePermission(long tenantId, String appCode, Long permissionId) {
IamPermission permission = repository.findOwnedPermission(permissionId, tenantId, appCode);
if (permission == null) throw forbidden();
if (Boolean.TRUE.equals(permission.getBuiltIn())) {
throw new BizException(ResultCode.CONFLICT,
"Built-in permission cannot be changed / 系统预置权限不允许编辑、启停或删除");
}
return permission;
}
private void validatePermissionParent(long tenantId, String appCode, Long permissionId, Long parentId) {
if (parentId == null || parentId == 0) return;
if (parentId.equals(permissionId)) {
throw new BizException(ResultCode.BAD_REQUEST, "Permission cannot be its own parent / 权限不能设置自身为父节点");
}
IamPermission parent = repository.findPermission(parentId, tenantId, appCode);
if (parent == null) throw forbidden();
Long cursor = parent.getParentId();
while (cursor != null && cursor > 0) {
if (cursor.equals(permissionId)) {
throw new BizException(ResultCode.BAD_REQUEST, "Permission parent cycle is not allowed / 权限节点不能形成环");
}
IamPermission ancestor = repository.findPermission(cursor, tenantId, appCode);
if (ancestor == null || cursor.equals(ancestor.getParentId())) break;
cursor = ancestor.getParentId();
}
}
private IamRole requireCustomRole(long tenantId, String appCode, Long roleId) {
IamRole role = requireRole(tenantId, appCode, roleId);
if ("SYSTEM".equals(role.getRoleType())) {
throw new BizException(ResultCode.CONFLICT, "系统角色不允许修改、停用或删除");
}
return role;
}
private IamMembership requireMembership(long tenantId, Long membershipId) {
IamMembership membership = memberRepository.findById(membershipId);
if (membership == null || membership.getTenantId() == null
|| membership.getTenantId().longValue() != tenantId) throw forbidden();
return membership;
}
private RoleResponse response(IamRole role, Set<Long> permissionIds, long memberCount) {
return new RoleResponse(role.getId(), role.getTenantId(), role.getAppCode(), role.getRoleCode(),
role.getRoleName(), role.getRoleType(), role.getStatus(), role.getDescription(),
memberCount, Set.copyOf(permissionIds));
}
private BizException forbidden() {
return new BizException(ResultCode.FORBIDDEN, "禁止跨租户或跨应用访问");
}
private BizException sessionRevocationUnavailable() {
return new BizException(ResultCode.SERVICE_UNAVAILABLE,
"认证会话撤销失败,角色变更未生效");
}
}