优化
This commit is contained in:
@@ -5,6 +5,7 @@ import org.springframework.cloud.openfeign.FeignClient;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Set;
|
||||
|
||||
/** auth-service 内部账号管理客户端。 */
|
||||
@FeignClient(name = "auth-service", contextId = "authAccountClient",
|
||||
@@ -30,6 +31,9 @@ public interface AuthAccountClient {
|
||||
@RequestBody ImpersonationTicketRequest request);
|
||||
@PutMapping("/{id}/scope") ApiResponse<Void> scope(@RequestHeader("X-Internal-Service-Token") String token,
|
||||
@PathVariable Long id, @RequestBody ScopeRequest request);
|
||||
@PostMapping("/sessions/revoke") ApiResponse<Void> revokeSessions(
|
||||
@RequestHeader("X-Internal-Service-Token") String token,
|
||||
@RequestBody RevokeAccountSessionsRequest request);
|
||||
@DeleteMapping("/{id}/compensation") ApiResponse<Void> compensate(@RequestHeader("X-Internal-Service-Token") String token,
|
||||
@PathVariable Long id);
|
||||
|
||||
@@ -56,6 +60,11 @@ public interface AuthAccountClient {
|
||||
public PasswordRequest(String newPassword) { this(newPassword, false); }
|
||||
}
|
||||
record ScopeRequest(Long tenantId, Long membershipId, String appCode) { }
|
||||
record RevokeAccountSessionsRequest(Set<Long> accountIds) {
|
||||
public RevokeAccountSessionsRequest {
|
||||
accountIds = Set.copyOf(accountIds);
|
||||
}
|
||||
}
|
||||
record ImpersonationTicketRequest(Long targetAuthUserId, Long tenantId, Long membershipId,
|
||||
Long actorUserId, Long actorMembershipId, String reason) { }
|
||||
record ImpersonationTicketResponse(String code, String ticketId, long expiresIn) { }
|
||||
|
||||
@@ -1,4 +1,29 @@
|
||||
/**
|
||||
* 成员角色关联 MyBatis-Plus Mapper,负责 {@code iam_membership_role} 表的基础持久化操作。
|
||||
*/
|
||||
package com.baiyue.iam.mapper; import com.baiyue.iam.entity.IamMembershipRole; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.apache.ibatis.annotations.Mapper; @Mapper public interface IamMembershipRoleMapper extends BaseMapper<IamMembershipRole>{}
|
||||
package com.baiyue.iam.mapper;
|
||||
|
||||
import com.baiyue.iam.entity.IamMembershipRole;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface IamMembershipRoleMapper extends BaseMapper<IamMembershipRole> {
|
||||
@Select("""
|
||||
SELECT DISTINCT u.auth_user_id
|
||||
FROM iam_membership_role mr
|
||||
JOIN iam_membership m ON m.id = mr.membership_id AND m.tenant_id = mr.tenant_id
|
||||
JOIN iam_user u ON u.id = m.user_id
|
||||
WHERE mr.tenant_id = #{tenantId}
|
||||
AND mr.role_id = #{roleId}
|
||||
AND m.deleted = 0
|
||||
AND u.deleted = 0
|
||||
ORDER BY u.auth_user_id
|
||||
""")
|
||||
List<Long> findAssignedAuthUserIds(@Param("tenantId") long tenantId,
|
||||
@Param("roleId") Long roleId);
|
||||
}
|
||||
|
||||
@@ -122,6 +122,10 @@ public class RbacRepository {
|
||||
.eq(IamMembershipRole::getRoleId, roleId)) > 0;
|
||||
}
|
||||
|
||||
public List<Long> findAssignedAuthUserIds(long tenantId, Long roleId) {
|
||||
return membershipRoleMapper.findAssignedAuthUserIds(tenantId, roleId);
|
||||
}
|
||||
|
||||
public void deleteRole(long tenantId, String appCode, Long roleId) {
|
||||
rolePermissionMapper.delete(new LambdaQueryWrapper<IamRolePermission>()
|
||||
.eq(IamRolePermission::getTenantId, tenantId)
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
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;
|
||||
@@ -11,6 +13,8 @@ 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;
|
||||
@@ -25,8 +29,12 @@ 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;
|
||||
@Value("${baiyue.security.internal-service-token}")
|
||||
private String internalToken;
|
||||
|
||||
public List<RoleResponse> roles(long tenantId, String appCode) {
|
||||
return repository.findRoles(tenantId, appCode).stream()
|
||||
@@ -75,11 +83,13 @@ public class RbacService {
|
||||
|
||||
@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));
|
||||
}
|
||||
|
||||
@@ -224,6 +234,22 @@ public class RbacService {
|
||||
}
|
||||
}
|
||||
repository.replaceRolePermissions(tenantId, request.roleId(), request.permissionIds());
|
||||
revokeAssignedSessions(tenantId, request.roleId());
|
||||
}
|
||||
|
||||
private void revokeAssignedSessions(long tenantId, Long roleId) {
|
||||
List<Long> accountIds = repository.findAssignedAuthUserIds(tenantId, roleId);
|
||||
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) {
|
||||
@@ -284,4 +310,9 @@ public class RbacService {
|
||||
private BizException forbidden() {
|
||||
return new BizException(ResultCode.FORBIDDEN, "禁止跨租户或跨应用访问");
|
||||
}
|
||||
|
||||
private BizException sessionRevocationUnavailable() {
|
||||
return new BizException(ResultCode.SERVICE_UNAVAILABLE,
|
||||
"认证会话撤销失败,角色变更未生效");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,4 +93,21 @@ class AuthorizationSnapshotServiceTest {
|
||||
assertThat(result).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void newLoginPermissionsDisappearWhileRoleDisabledAndReturnAfterReenable() {
|
||||
when(mapper.countActiveIdentity(42L, 7L, 9L)).thenReturn(1);
|
||||
when(mapper.findRoles(42L, 7L, 9L, "MERCHANT"))
|
||||
.thenReturn(List.of("ORDER_VIEWER"), List.of(), List.of("ORDER_VIEWER"));
|
||||
when(mapper.findPermissions(42L, 7L, 9L, "MERCHANT"))
|
||||
.thenReturn(List.of("merchant:order:list", "merchant:order:view"), List.of(),
|
||||
List.of("merchant:order:list", "merchant:order:view"));
|
||||
SnapshotRequest request = new SnapshotRequest(42L, 7L, 9L, "MERCHANT");
|
||||
|
||||
assertThat(service.load(request).getPermissions())
|
||||
.containsExactlyInAnyOrder("merchant:order:list", "merchant:order:view");
|
||||
assertThat(service.load(request).getPermissions()).isEmpty();
|
||||
assertThat(service.load(request).getPermissions())
|
||||
.containsExactlyInAnyOrder("merchant:order:list", "merchant:order:view");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package com.baiyue.iam.service;
|
||||
|
||||
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.AssignRolesRequest;
|
||||
import com.baiyue.iam.dto.RbacDtos.GrantPermissionsRequest;
|
||||
import com.baiyue.iam.dto.RbacDtos.CreateRoleRequest;
|
||||
@@ -19,6 +21,7 @@ import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
|
||||
@@ -30,9 +33,11 @@ import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@SpringJUnitConfig(RbacService.class)
|
||||
@TestPropertySource(properties = "baiyue.security.internal-service-token=test-internal-token")
|
||||
class RbacServiceTest {
|
||||
@MockBean private RbacRepository repository;
|
||||
@MockBean private MemberRepository memberRepository;
|
||||
@MockBean private AuthAccountClient authClient;
|
||||
@Resource private RbacService service;
|
||||
|
||||
@Test
|
||||
@@ -311,6 +316,53 @@ class RbacServiceTest {
|
||||
verify(repository).deletePermission(custom);
|
||||
}
|
||||
|
||||
@Test
|
||||
void roleDisableAndEnableRevokeAllAssignedAccountSessions() {
|
||||
IamRole role = role(11L, 100L, "MERCHANT", "CUSTOM");
|
||||
when(repository.findRole(11L, 100L, "MERCHANT")).thenReturn(role);
|
||||
when(repository.findRolePermissionIds(100L, 11L)).thenReturn(Set.of(21L));
|
||||
when(repository.findAssignedAuthUserIds(100L, 11L)).thenReturn(List.of(501L, 502L));
|
||||
when(authClient.revokeSessions(any(), any())).thenReturn(ApiResponse.success(null));
|
||||
|
||||
service.changeStatus(100L, "MERCHANT", 11L, new ChangeRoleStatusRequest("DISABLED"));
|
||||
service.changeStatus(100L, "MERCHANT", 11L, new ChangeRoleStatusRequest("ACTIVE"));
|
||||
|
||||
ArgumentCaptor<AuthAccountClient.RevokeAccountSessionsRequest> captor =
|
||||
ArgumentCaptor.forClass(AuthAccountClient.RevokeAccountSessionsRequest.class);
|
||||
verify(authClient, times(2)).revokeSessions(eq("test-internal-token"), captor.capture());
|
||||
assertEquals(Set.of(501L, 502L), captor.getAllValues().get(0).accountIds());
|
||||
assertEquals(Set.of(501L, 502L), captor.getAllValues().get(1).accountIds());
|
||||
}
|
||||
|
||||
@Test
|
||||
void rolePermissionReplacementRevokesAllAssignedAccountSessions() {
|
||||
when(repository.findRole(11L, 100L, "MERCHANT"))
|
||||
.thenReturn(role(11L, 100L, "MERCHANT", "CUSTOM"));
|
||||
when(repository.findAssignedAuthUserIds(100L, 11L)).thenReturn(List.of(501L));
|
||||
when(authClient.revokeSessions(any(), any())).thenReturn(ApiResponse.success(null));
|
||||
|
||||
service.grantPermissions(100L, "MERCHANT", new GrantPermissionsRequest(11L, Set.of()));
|
||||
|
||||
verify(repository).replaceRolePermissions(100L, 11L, Set.of());
|
||||
verify(authClient).revokeSessions(eq("test-internal-token"),
|
||||
eq(new AuthAccountClient.RevokeAccountSessionsRequest(Set.of(501L))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void authFailureRejectsRoleStatusChangeSoIamTransactionCanRollBack() {
|
||||
IamRole role = role(11L, 100L, "MERCHANT", "CUSTOM");
|
||||
when(repository.findRole(11L, 100L, "MERCHANT")).thenReturn(role);
|
||||
when(repository.findAssignedAuthUserIds(100L, 11L)).thenReturn(List.of(501L));
|
||||
when(authClient.revokeSessions(any(), any())).thenReturn(ApiResponse.failure(
|
||||
ResultCode.SERVICE_UNAVAILABLE, "auth unavailable", null, null));
|
||||
|
||||
BizException exception = assertThrows(BizException.class, () -> service.changeStatus(
|
||||
100L, "MERCHANT", 11L, new ChangeRoleStatusRequest("DISABLED")));
|
||||
|
||||
assertEquals(ResultCode.SERVICE_UNAVAILABLE, exception.getResultCode());
|
||||
assertEquals("认证会话撤销失败,角色变更未生效", exception.getMessage());
|
||||
}
|
||||
|
||||
private IamRole role(Long id, Long tenantId, String appCode, String roleType) {
|
||||
IamRole role = new IamRole();
|
||||
role.setId(id);
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.baiyue.iam.service;
|
||||
import com.baiyue.iam.client.AuthAccountClient;
|
||||
import com.baiyue.iam.entity.IamTenant;
|
||||
import com.baiyue.iam.repository.ProfileResourceRepository;
|
||||
import com.baiyue.iam.repository.MemberRepository;
|
||||
import com.baiyue.iam.repository.TenantRepository;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
@@ -21,6 +22,7 @@ import static org.mockito.Mockito.when;
|
||||
class TenantCustomerExportTest {
|
||||
@MockBean private TenantRepository repository;
|
||||
@MockBean private ProfileResourceRepository profileRepository;
|
||||
@MockBean private MemberRepository memberRepository;
|
||||
@MockBean private AuthAccountClient authAccountClient;
|
||||
@MockBean private TenantApprovalInitializationCoordinator approvalInitialization;
|
||||
@MockBean private CustomerDimensionRuleInitializationService dimensionRuleInitialization;
|
||||
|
||||
Reference in New Issue
Block a user