348 lines
14 KiB
Java
348 lines
14 KiB
Java
package com.baiyue.iam.service;
|
|
|
|
import com.baiyue.common.core.api.ResultCode;
|
|
import com.baiyue.common.core.exception.BizException;
|
|
import com.baiyue.iam.dto.RbacDtos.AssignRolesRequest;
|
|
import com.baiyue.iam.dto.RbacDtos.GrantPermissionsRequest;
|
|
import com.baiyue.iam.dto.RbacDtos.CreateRoleRequest;
|
|
import com.baiyue.iam.dto.RbacDtos.UpdateRoleRequest;
|
|
import com.baiyue.iam.dto.RbacDtos.ChangeRoleStatusRequest;
|
|
import com.baiyue.iam.dto.RbacDtos.CreatePermissionRequest;
|
|
import com.baiyue.iam.dto.RbacDtos.UpdatePermissionRequest;
|
|
import com.baiyue.iam.dto.RbacDtos.ChangePermissionStatusRequest;
|
|
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 org.junit.jupiter.api.BeforeEach;
|
|
import org.junit.jupiter.api.DisplayName;
|
|
import org.junit.jupiter.api.Test;
|
|
import org.junit.jupiter.api.extension.ExtendWith;
|
|
import org.mockito.ArgumentCaptor;
|
|
import org.mockito.Mock;
|
|
import org.mockito.junit.jupiter.MockitoExtension;
|
|
import org.springframework.test.util.ReflectionTestUtils;
|
|
|
|
import java.util.List;
|
|
import java.util.Set;
|
|
|
|
import static org.junit.jupiter.api.Assertions.*;
|
|
import static org.mockito.ArgumentMatchers.any;
|
|
import static org.mockito.Mockito.*;
|
|
|
|
@ExtendWith(MockitoExtension.class)
|
|
@DisplayName("角色与权限服务")
|
|
class RbacServiceTest {
|
|
@Mock private RbacRepository repository;
|
|
@Mock private MemberRepository memberRepository;
|
|
private RbacService service;
|
|
|
|
@BeforeEach
|
|
void setUp() {
|
|
service = new RbacService();
|
|
ReflectionTestUtils.setField(service, "repository", repository);
|
|
ReflectionTestUtils.setField(service, "memberRepository", memberRepository);
|
|
}
|
|
|
|
@Test
|
|
void roleListReturnsCurrentPermissionsForSafeView() {
|
|
IamRole role = role(11L, 100L, "MERCHANT", "CUSTOM");
|
|
when(repository.findRoles(100L, "MERCHANT")).thenReturn(List.of(role));
|
|
when(repository.findRolePermissionIds(100L, 11L)).thenReturn(Set.of(21L, 22L));
|
|
|
|
var result = service.roles(100L, "MERCHANT");
|
|
|
|
assertEquals(Set.of(21L, 22L), result.get(0).permissionIds());
|
|
assertEquals(100L, result.get(0).tenantId());
|
|
}
|
|
|
|
@Test
|
|
void permissionListPrefersCurrentTenantAndRemovesDuplicateGlobalTemplatesWithSameCode() {
|
|
IamPermission global = permission(21L, 0L, "merchant:member:list");
|
|
IamPermission tenant = permission(22L, 100L, "merchant:member:list");
|
|
IamPermission fallback = permission(23L, 0L, "merchant:member:status");
|
|
when(repository.findPermissions(100L, "MERCHANT"))
|
|
.thenReturn(List.of(global, fallback, tenant));
|
|
|
|
var result = service.permissions(100L, "MERCHANT");
|
|
|
|
assertEquals(2, result.size());
|
|
assertEquals(22L, result.get(0).getId());
|
|
assertEquals(23L, result.get(1).getId());
|
|
}
|
|
|
|
@Test
|
|
void crossTenantRoleAccessIsRejected() {
|
|
when(repository.findRole(11L, 200L, "MERCHANT")).thenReturn(null);
|
|
|
|
BizException exception = assertThrows(BizException.class,
|
|
() -> service.role(200L, "MERCHANT", 11L));
|
|
|
|
assertEquals(ResultCode.FORBIDDEN, exception.getResultCode());
|
|
}
|
|
|
|
@Test
|
|
void systemRoleCannotBeDeleted() {
|
|
when(repository.findRole(11L, 0L, "PLATFORM"))
|
|
.thenReturn(role(11L, 0L, "PLATFORM", "SYSTEM"));
|
|
|
|
BizException exception = assertThrows(BizException.class,
|
|
() -> service.deleteRole(0L, "PLATFORM", 11L));
|
|
|
|
assertEquals(ResultCode.CONFLICT, exception.getResultCode());
|
|
verify(repository, never()).deleteRole(anyLong(), any(), anyLong());
|
|
}
|
|
|
|
@Test
|
|
void customRoleAssignedToMembersCannotBeDeleted() {
|
|
when(repository.findRole(11L, 100L, "MERCHANT"))
|
|
.thenReturn(role(11L, 100L, "MERCHANT", "CUSTOM"));
|
|
when(repository.isRoleAssigned(100L, 11L)).thenReturn(true);
|
|
|
|
BizException exception = assertThrows(BizException.class,
|
|
() -> service.deleteRole(100L, "MERCHANT", 11L));
|
|
|
|
assertEquals(ResultCode.CONFLICT, exception.getResultCode());
|
|
verify(repository, never()).deleteRole(anyLong(), any(), anyLong());
|
|
}
|
|
|
|
@Test
|
|
void emptyPermissionSetExplicitlyClearsAuthorization() {
|
|
when(repository.findRole(11L, 100L, "MERCHANT"))
|
|
.thenReturn(role(11L, 100L, "MERCHANT", "CUSTOM"));
|
|
|
|
service.grantPermissions(100L, "MERCHANT", new GrantPermissionsRequest(11L, Set.of()));
|
|
|
|
verify(repository).replaceRolePermissions(100L, 11L, Set.of());
|
|
}
|
|
|
|
@Test
|
|
void emptyRoleSetExplicitlyClearsMemberAuthorization() {
|
|
IamMembership membership = new IamMembership();
|
|
membership.setId(31L);
|
|
membership.setTenantId(100L);
|
|
when(memberRepository.findById(31L)).thenReturn(membership);
|
|
|
|
service.assignRoles(100L, "MERCHANT", new AssignRolesRequest(31L, Set.of()));
|
|
|
|
verify(repository).replaceMemberRoles(100L, "MERCHANT", 31L, Set.of());
|
|
}
|
|
|
|
@Test
|
|
void memberRoleViewReturnsRolesFromCurrentApplicationDomainOnly() {
|
|
IamMembership membership = new IamMembership();
|
|
membership.setId(31L);
|
|
membership.setTenantId(100L);
|
|
when(memberRepository.findById(31L)).thenReturn(membership);
|
|
when(repository.findMemberRoleIds(100L, 31L)).thenReturn(Set.of(11L, 12L));
|
|
when(repository.findRole(11L, 100L, "MERCHANT"))
|
|
.thenReturn(role(11L, 100L, "MERCHANT", "CUSTOM"));
|
|
when(repository.findRole(12L, 100L, "MERCHANT")).thenReturn(null);
|
|
|
|
var result = service.memberRoles(100L, "MERCHANT", 31L);
|
|
|
|
assertEquals(Set.of(11L), result.roleIds());
|
|
}
|
|
|
|
@Test
|
|
void duplicateRoleCodeWithinTenantAndApplicationReturnsConflict() {
|
|
when(repository.existsRoleCode(100L, "MERCHANT", "ORDER_ADMIN", null)).thenReturn(true);
|
|
|
|
BizException exception = assertThrows(BizException.class, () -> service.createRole(
|
|
100L, "MERCHANT", new CreateRoleRequest("ORDER_ADMIN", "订单管理员", null)));
|
|
|
|
assertEquals(ResultCode.CONFLICT, exception.getResultCode());
|
|
verify(repository, never()).insertRole(any());
|
|
}
|
|
|
|
@Test
|
|
void disabledRoleCannotBeAssignedToMember() {
|
|
IamMembership membership = new IamMembership();
|
|
membership.setId(31L);
|
|
membership.setTenantId(100L);
|
|
when(memberRepository.findById(31L)).thenReturn(membership);
|
|
IamRole disabled = role(11L, 100L, "MERCHANT", "CUSTOM");
|
|
disabled.setStatus("DISABLED");
|
|
when(repository.findRole(11L, 100L, "MERCHANT")).thenReturn(disabled);
|
|
|
|
BizException exception = assertThrows(BizException.class, () -> service.assignRoles(
|
|
100L, "MERCHANT", new AssignRolesRequest(31L, Set.of(11L))));
|
|
|
|
assertEquals(ResultCode.CONFLICT, exception.getResultCode());
|
|
verify(repository, never()).replaceMemberRoles(anyLong(), any(), anyLong(), any());
|
|
}
|
|
|
|
@Test
|
|
void customRoleCanBeUpdatedAndDisabledWhileRetainingPermissionView() {
|
|
IamRole role = role(11L, 100L, "MERCHANT", "CUSTOM");
|
|
when(repository.findRole(11L, 100L, "MERCHANT")).thenReturn(role);
|
|
when(repository.findRolePermissionIds(100L, 11L)).thenReturn(Set.of(21L));
|
|
|
|
var updated = service.updateRole(100L, "MERCHANT", 11L,
|
|
new UpdateRoleRequest("新名称", "新说明"));
|
|
var disabled = service.changeStatus(100L, "MERCHANT", 11L,
|
|
new ChangeRoleStatusRequest("DISABLED"));
|
|
|
|
assertEquals("新名称", updated.roleName());
|
|
assertEquals(Set.of(21L), updated.permissionIds());
|
|
assertEquals("DISABLED", disabled.status());
|
|
verify(repository, times(2)).updateRole(role);
|
|
}
|
|
|
|
@Test
|
|
void unreferencedCustomRoleCanBeDeletedSafely() {
|
|
when(repository.findRole(11L, 100L, "MERCHANT"))
|
|
.thenReturn(role(11L, 100L, "MERCHANT", "CUSTOM"));
|
|
when(repository.isRoleAssigned(100L, 11L)).thenReturn(false);
|
|
|
|
service.deleteRole(100L, "MERCHANT", 11L);
|
|
|
|
verify(repository).deleteRole(100L, "MERCHANT", 11L);
|
|
}
|
|
|
|
@Test
|
|
void tenantOwnerRolesCannotBeChangedThroughRegularMemberAuthorizationEndpoint() {
|
|
IamMembership owner = new IamMembership();
|
|
owner.setId(31L);
|
|
owner.setTenantId(100L);
|
|
owner.setMemberType("OWNER");
|
|
when(memberRepository.findById(31L)).thenReturn(owner);
|
|
|
|
BizException exception = assertThrows(BizException.class, () -> service.assignRoles(
|
|
100L, "MERCHANT", new AssignRolesRequest(31L, Set.of())));
|
|
|
|
assertEquals(ResultCode.FORBIDDEN, exception.getResultCode());
|
|
verify(repository, never()).replaceMemberRoles(anyLong(), any(), anyLong(), any());
|
|
}
|
|
|
|
@Test
|
|
void systemRolePermissionsCannotBeChangedThroughRegularAuthorizationEndpoint() {
|
|
when(repository.findRole(11L, 0L, "PLATFORM"))
|
|
.thenReturn(role(11L, 0L, "PLATFORM", "SYSTEM"));
|
|
|
|
BizException exception = assertThrows(BizException.class, () -> service.grantPermissions(
|
|
0L, "PLATFORM", new GrantPermissionsRequest(11L, Set.of())));
|
|
|
|
assertEquals(ResultCode.CONFLICT, exception.getResultCode());
|
|
verify(repository, never()).replaceRolePermissions(anyLong(), anyLong(), any());
|
|
}
|
|
|
|
@Test
|
|
void tenantOwnerSystemRoleCannotBeAssignedToRegularMember() {
|
|
IamMembership member = new IamMembership();
|
|
member.setId(31L);
|
|
member.setTenantId(100L);
|
|
member.setMemberType("EMPLOYEE");
|
|
when(memberRepository.findById(31L)).thenReturn(member);
|
|
IamRole ownerRole = role(11L, 100L, "MERCHANT", "SYSTEM");
|
|
ownerRole.setRoleCode("MERCHANT_OWNER");
|
|
when(repository.findRole(11L, 100L, "MERCHANT")).thenReturn(ownerRole);
|
|
|
|
BizException exception = assertThrows(BizException.class, () -> service.assignRoles(
|
|
100L, "MERCHANT", new AssignRolesRequest(31L, Set.of(11L))));
|
|
|
|
assertEquals(ResultCode.FORBIDDEN, exception.getResultCode());
|
|
verify(repository, never()).replaceMemberRoles(anyLong(), any(), anyLong(), any());
|
|
}
|
|
|
|
@Test
|
|
void duplicatePermissionCodeReturnsStableConflictWithoutDatabaseWrite() {
|
|
when(repository.existsPermissionCode(100L, "MERCHANT", "merchant:role:test"))
|
|
.thenReturn(true);
|
|
CreatePermissionRequest request = new CreatePermissionRequest(
|
|
"BUTTON", "merchant:role:test", "测试按钮", 0L, null, null, 1);
|
|
|
|
BizException exception = assertThrows(BizException.class,
|
|
() -> service.createPermission(100L, "MERCHANT", request));
|
|
|
|
assertEquals(ResultCode.CONFLICT, exception.getResultCode());
|
|
verify(repository, never()).insertPermission(any());
|
|
}
|
|
|
|
@Test
|
|
void builtInSystemPermissionCannotBeUpdatedDisabledOrDeleted() {
|
|
IamPermission builtIn = permission(21L, 0L, "platform:permission:list");
|
|
builtIn.setAppCode("PLATFORM");
|
|
builtIn.setBuiltIn(true);
|
|
when(repository.findOwnedPermission(21L, 0L, "PLATFORM")).thenReturn(builtIn);
|
|
|
|
BizException exception = assertThrows(BizException.class, () -> service.changePermissionStatus(
|
|
0L, "PLATFORM", 21L, new ChangePermissionStatusRequest("DISABLED")));
|
|
|
|
assertEquals(ResultCode.CONFLICT, exception.getResultCode());
|
|
verify(repository, never()).updatePermission(any());
|
|
verify(repository, never()).deletePermission(any());
|
|
}
|
|
|
|
@Test
|
|
void customPermissionCanBeUpdatedAndToggledWithoutChangingCode() {
|
|
IamPermission custom = permission(21L, 100L, "merchant:custom:edit");
|
|
when(repository.findOwnedPermission(21L, 100L, "MERCHANT")).thenReturn(custom);
|
|
|
|
var updated = service.updatePermission(100L, "MERCHANT", 21L,
|
|
new UpdatePermissionRequest("新名称", 0L, "/custom", "PUT", 10));
|
|
var disabled = service.changePermissionStatus(100L, "MERCHANT", 21L,
|
|
new ChangePermissionStatusRequest("DISABLED"));
|
|
|
|
assertEquals("merchant:custom:edit", updated.getPermissionCode());
|
|
assertEquals("新名称", updated.getPermissionName());
|
|
assertEquals("DISABLED", disabled.getStatus());
|
|
verify(repository, times(2)).updatePermission(custom);
|
|
}
|
|
|
|
@Test
|
|
void permissionResourceWithChildrenOrRoleBindingsRejectsDeletion() {
|
|
IamPermission custom = permission(21L, 100L, "merchant:custom:edit");
|
|
when(repository.findOwnedPermission(21L, 100L, "MERCHANT")).thenReturn(custom);
|
|
when(repository.hasPermissionChildren(100L, "MERCHANT", 21L)).thenReturn(true);
|
|
|
|
BizException childFailure = assertThrows(BizException.class,
|
|
() -> service.deletePermission(100L, "MERCHANT", 21L));
|
|
assertEquals(ResultCode.CONFLICT, childFailure.getResultCode());
|
|
|
|
when(repository.hasPermissionChildren(100L, "MERCHANT", 21L)).thenReturn(false);
|
|
when(repository.isPermissionGranted(100L, 21L)).thenReturn(true);
|
|
BizException bindingFailure = assertThrows(BizException.class,
|
|
() -> service.deletePermission(100L, "MERCHANT", 21L));
|
|
assertEquals(ResultCode.CONFLICT, bindingFailure.getResultCode());
|
|
verify(repository, never()).deletePermission(any());
|
|
}
|
|
|
|
@Test
|
|
void unreferencedCustomPermissionCanBeDeleted() {
|
|
IamPermission custom = permission(21L, 100L, "merchant:custom:edit");
|
|
when(repository.findOwnedPermission(21L, 100L, "MERCHANT")).thenReturn(custom);
|
|
when(repository.hasPermissionChildren(100L, "MERCHANT", 21L)).thenReturn(false);
|
|
when(repository.isPermissionGranted(100L, 21L)).thenReturn(false);
|
|
|
|
service.deletePermission(100L, "MERCHANT", 21L);
|
|
|
|
verify(repository).deletePermission(custom);
|
|
}
|
|
|
|
private IamRole role(Long id, Long tenantId, String appCode, String roleType) {
|
|
IamRole role = new IamRole();
|
|
role.setId(id);
|
|
role.setTenantId(tenantId);
|
|
role.setAppCode(appCode);
|
|
role.setRoleCode("ROLE_" + id);
|
|
role.setRoleName("测试角色");
|
|
role.setRoleType(roleType);
|
|
role.setStatus("ACTIVE");
|
|
return role;
|
|
}
|
|
|
|
private IamPermission permission(Long id, Long tenantId, String code) {
|
|
IamPermission permission = new IamPermission();
|
|
permission.setId(id);
|
|
permission.setTenantId(tenantId);
|
|
permission.setAppCode("MERCHANT");
|
|
permission.setPermissionCode(code);
|
|
permission.setPermissionName(code);
|
|
permission.setPermissionType("BUTTON");
|
|
permission.setStatus("ACTIVE");
|
|
return permission;
|
|
}
|
|
}
|