优化
This commit is contained in:
@@ -7,7 +7,8 @@ import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
|
||||
@FeignClient(name = "event-bridge-service", contextId = "iamSecurityEventBridgeClient")
|
||||
@FeignClient(name = "event-bridge-service", contextId = "iamSecurityEventBridgeClient",
|
||||
url = "${baiyue.security-event.event-bridge-url:}")
|
||||
public interface SecurityEventBridgeClient {
|
||||
@PostMapping("/internal/event-bridge/events")
|
||||
ApiResponse<Acknowledgement> receive(@RequestHeader("X-Internal-Service-Token") String token,
|
||||
|
||||
@@ -104,6 +104,8 @@ public class MemberService {
|
||||
membership.setMemberType(request.memberType()); membership.setMemberNo(request.memberNo());
|
||||
membership.setStatus("ACTIVE"); membership.setJoinedAt(LocalDateTime.now()); repository.insert(membership);
|
||||
repository.replaceRoles(tenantId, membershipId, request.roleIds());
|
||||
securityEvents.userStatus(authId, tenantId, "MERCHANT", "ACTIVE");
|
||||
securityEvents.permissionChanged(authId, tenantId, "MERCHANT", "INITIAL_AUTHORIZATION");
|
||||
return response(tenantId, membership, user);
|
||||
} catch (RuntimeException exception) {
|
||||
try {
|
||||
|
||||
@@ -42,6 +42,7 @@ public class MerchantOnboardingService {
|
||||
@Resource private IamTenantReviewMapper reviewMapper;
|
||||
@Resource private CustomerDimensionRuleInitializationService dimensionRuleInitialization;
|
||||
@Resource private OverseasStorageFeeInitializationService overseasStorageFeeInitialization;
|
||||
@Resource private SecurityEventOutboxService securityEvents;
|
||||
|
||||
@Transactional
|
||||
public MerchantRegistrationResponse register(MerchantRegistrationRequest request) {
|
||||
@@ -78,6 +79,7 @@ public class MerchantOnboardingService {
|
||||
tenant.setDeleted(false);
|
||||
tenant.setVersion(0);
|
||||
tenantMapper.insert(tenant);
|
||||
securityEvents.tenantStatus(tenantId, tenant.getStatus());
|
||||
|
||||
IamUser user = new IamUser();
|
||||
user.setId(userId);
|
||||
@@ -109,6 +111,8 @@ public class MerchantOnboardingService {
|
||||
membershipRole.setTenantId(tenantId);
|
||||
membershipRole.setCreatedAt(LocalDateTime.now());
|
||||
membershipRoleMapper.insert(membershipRole);
|
||||
securityEvents.userStatus(request.authUserId(), tenantId, "MERCHANT", "ACTIVE");
|
||||
securityEvents.permissionChanged(request.authUserId(), tenantId, "MERCHANT", "INITIAL_AUTHORIZATION");
|
||||
|
||||
if (StringUtils.hasText(request.address())) {
|
||||
IamTenantAddress address = new IamTenantAddress();
|
||||
|
||||
@@ -77,6 +77,7 @@ public class PlatformInternalUserService {
|
||||
repository.insertUser(user);
|
||||
repository.insert(membership);
|
||||
repository.replaceRoles(PLATFORM_TENANT_ID, membershipId, roleIds);
|
||||
publishInitialSecurityProjection(user);
|
||||
return response(membership, user, request.username(), roleIds);
|
||||
} catch (RuntimeException localFailure) {
|
||||
compensateCreate(authUserId, localFailure);
|
||||
@@ -196,9 +197,16 @@ public class PlatformInternalUserService {
|
||||
IamMembership membership = newMembership(membershipId, user.getId(), memberNo);
|
||||
repository.insert(membership);
|
||||
repository.replaceRoles(PLATFORM_TENANT_ID, membershipId, roleIds);
|
||||
publishInitialSecurityProjection(user);
|
||||
return response(membership, user, authAccount.username(), roleIds);
|
||||
}
|
||||
|
||||
private void publishInitialSecurityProjection(IamUser user) {
|
||||
securityEvents.userStatus(user.getAuthUserId(), PLATFORM_TENANT_ID, "PLATFORM", user.getStatus());
|
||||
securityEvents.permissionChanged(user.getAuthUserId(), PLATFORM_TENANT_ID, "PLATFORM",
|
||||
"INITIAL_AUTHORIZATION");
|
||||
}
|
||||
|
||||
private InternalUserResponse response(IamMembership membership) {
|
||||
IamUser user = requiredUser(membership.getUserId());
|
||||
AuthAccountClient.AccountSummary authAccount = account(user.getAuthUserId());
|
||||
|
||||
@@ -106,6 +106,8 @@ public class PlatformUserManagementService {
|
||||
membership.setLeftAt("DISABLED".equals(request.status()) ? LocalDateTime.now() : null);
|
||||
members.insert(membership);
|
||||
members.replaceRoles(tenantId, membershipId, roleIds);
|
||||
securityEvents.userStatus(authUserId, tenantId, appCode, request.status());
|
||||
securityEvents.permissionChanged(authUserId, tenantId, appCode, "INITIAL_AUTHORIZATION");
|
||||
return response(membership);
|
||||
} catch (RuntimeException localFailure) {
|
||||
compensateCreate(authUserId, localFailure);
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.baiyue.iam.service;
|
||||
|
||||
import com.baiyue.common.event.EventEnvelope;
|
||||
import com.baiyue.common.event.EventEnvelopeValidator;
|
||||
import com.baiyue.common.event.RegionCode;
|
||||
import com.baiyue.iam.client.SecurityEventBridgeClient;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
@@ -45,6 +46,9 @@ public class SecurityEventOutboxRelay {
|
||||
try {
|
||||
EventEnvelope envelope = objectMapper.readValue(json, EventEnvelope.class);
|
||||
EventEnvelopeValidator.requireValid(envelope);
|
||||
if (envelope.sourceRegion() != RegionCode.CN || envelope.targetRegion() != RegionCode.OVERSEAS) {
|
||||
throw new IllegalStateException("IAM 安全事件必须从 CN 投影到 OVERSEAS");
|
||||
}
|
||||
var response = client.receive(internalToken, envelope);
|
||||
if (response == null || !response.successful() || response.data() == null
|
||||
|| !eventId.equals(response.data().eventId())) {
|
||||
|
||||
@@ -32,8 +32,9 @@ public class SecurityEventOutboxService {
|
||||
+ "ON DUPLICATE KEY UPDATE status_version=status_version+1", tenantId);
|
||||
long version = jdbc.queryForObject(
|
||||
"SELECT status_version FROM iam_security_tenant_version WHERE tenant_id=?", Long.class, tenantId);
|
||||
String securityStatus = "ACTIVE".equals(status) ? "ACTIVE" : "DISABLED";
|
||||
write(EventTypes.IAM_TENANT_STATUS_CHANGED, "TENANT", String.valueOf(tenantId), tenantId, version,
|
||||
new IamSecurityEventsV1.TenantStatusChanged(tenantId, status, Instant.now()));
|
||||
new IamSecurityEventsV1.TenantStatusChanged(tenantId, securityStatus, Instant.now()));
|
||||
}
|
||||
|
||||
public void permissionChanged(long authUserId, long tenantId, String appCode, String reason) {
|
||||
|
||||
@@ -43,6 +43,8 @@ public class TenantAdministratorProvisioningService implements TenantApprovalIni
|
||||
private IamRoleMapper roles;
|
||||
@Resource
|
||||
private AuthAccountClient authClient;
|
||||
@Resource
|
||||
private SecurityEventOutboxService securityEvents;
|
||||
@Value("${baiyue.security.internal-service-token}")
|
||||
private String internalToken;
|
||||
private SecureRandom random = new SecureRandom();
|
||||
@@ -112,6 +114,7 @@ public class TenantAdministratorProvisioningService implements TenantApprovalIni
|
||||
membership.setUpdatedBy(operatorId);
|
||||
members.insert(membership);
|
||||
members.replaceRoles(tenant.getId(), membershipId, List.of(ownerRole.getId()));
|
||||
publishInitialSecurityProjection(authUserId, tenant.getId());
|
||||
return result;
|
||||
} catch (RuntimeException localFailure) {
|
||||
compensate(context, result);
|
||||
@@ -152,6 +155,7 @@ public class TenantAdministratorProvisioningService implements TenantApprovalIni
|
||||
String password = temporaryPassword();
|
||||
String username = createAuthAccountWithUniqueUsername(user.getAuthUserId(), password,
|
||||
tenant, owner.getId());
|
||||
publishInitialSecurityProjection(user.getAuthUserId(), tenant.getId());
|
||||
return ProvisioningResult.created(user.getAuthUserId(), username, password);
|
||||
}
|
||||
if (!"ACTIVE".equals(account.status())
|
||||
@@ -166,6 +170,8 @@ public class TenantAdministratorProvisioningService implements TenantApprovalIni
|
||||
List<Long> repairedRoleIds = new java.util.ArrayList<>(roleIds);
|
||||
repairedRoleIds.add(ownerRole.getId());
|
||||
members.replaceRoles(tenant.getId(), owner.getId(), repairedRoleIds.stream().distinct().toList());
|
||||
securityEvents.permissionChanged(user.getAuthUserId(), tenant.getId(), "MERCHANT",
|
||||
"MEMBER_ROLES_CHANGED");
|
||||
}
|
||||
return ProvisioningResult.existing();
|
||||
}
|
||||
@@ -230,6 +236,11 @@ public class TenantAdministratorProvisioningService implements TenantApprovalIni
|
||||
}
|
||||
}
|
||||
|
||||
private void publishInitialSecurityProjection(long authUserId, long tenantId) {
|
||||
securityEvents.userStatus(authUserId, tenantId, "MERCHANT", "ACTIVE");
|
||||
securityEvents.permissionChanged(authUserId, tenantId, "MERCHANT", "INITIAL_AUTHORIZATION");
|
||||
}
|
||||
|
||||
private void compensate(long authUserId) {
|
||||
try {
|
||||
ApiResponse<Void> response = authClient.compensate(internalToken, authUserId);
|
||||
|
||||
@@ -190,6 +190,7 @@ public class TenantService {
|
||||
tenant.setCreatedBy(operatorId);
|
||||
tenant.setUpdatedBy(operatorId);
|
||||
repository.insertTenant(tenant);
|
||||
securityEvents.tenantStatus(tenant.getId(), tenant.getStatus());
|
||||
dimensionRuleInitialization.initialize(tenant.getId(), operatorId);
|
||||
overseasStorageFeeInitialization.initialize(tenant.getId(), operatorId, tenant.getTenantName());
|
||||
var administrator = administratorProvisioning.provisionPlatformCreatedTenant(tenant, operatorId);
|
||||
@@ -297,6 +298,7 @@ public class TenantService {
|
||||
tenant.setApprovedAt(approve ? LocalDateTime.now() : null);
|
||||
tenant.setApprovedBy(approve ? reviewerId : null);
|
||||
repository.saveTenant(tenant);
|
||||
securityEvents.tenantStatus(tenant.getId(), tenant.getStatus());
|
||||
for (var qualification : profileRepository.findQualifications(tenant.getId())) {
|
||||
if ("PENDING".equals(qualification.getStatus())) {
|
||||
qualification.setStatus(approve ? "APPROVED" : "REJECTED");
|
||||
|
||||
@@ -23,6 +23,8 @@ spring:
|
||||
namespace: ${NACOS_NAMESPACE:public}
|
||||
encode: UTF-8
|
||||
baiyue:
|
||||
security-event:
|
||||
event-bridge-url: ${SECURITY_EVENT_BRIDGE_URL:http://event-bridge-overseas:9312}
|
||||
notification:
|
||||
scheduler-enabled: ${NOTIFICATION_SCHEDULER_ENABLED:false}
|
||||
dispatch-timeout-seconds: ${NOTIFICATION_DISPATCH_TIMEOUT_SECONDS:300}
|
||||
@@ -45,3 +47,4 @@ logging:
|
||||
max-history: "${LOGGING_ROLLING_MAX_HISTORY:7}"
|
||||
total-size-cap: "${LOGGING_ROLLING_TOTAL_SIZE_CAP:5GB}"
|
||||
clean-history-on-start: "${LOGGING_ROLLING_CLEAN_HISTORY_ON_START:false}"
|
||||
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.baiyue.iam.client;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class SecurityEventBridgeClientContractTest {
|
||||
private static final Path REPOSITORY_ROOT = Path.of("..").toAbsolutePath().normalize();
|
||||
|
||||
@Test
|
||||
void feignUsesTheExplicitOverseasBridgePropertyInsteadOfSameNameDiscovery() {
|
||||
FeignClient annotation = SecurityEventBridgeClient.class.getAnnotation(FeignClient.class);
|
||||
|
||||
assertThat(annotation.name()).isEqualTo("event-bridge-service");
|
||||
assertThat(annotation.url()).isEqualTo("${baiyue.security-event.event-bridge-url:}");
|
||||
}
|
||||
|
||||
@Test
|
||||
void cnIamConfigurationTargetsTheOverseasBridgeNetworkAlias() throws Exception {
|
||||
String application = Files.readString(REPOSITORY_ROOT.resolve("iam/src/main/resources/application.yml"));
|
||||
String nacos = Files.readString(REPOSITORY_ROOT.resolve("config/nacos/cn/iam-service.yml"));
|
||||
String compose = Files.readString(REPOSITORY_ROOT.resolve("deploy/docker-compose.yml"));
|
||||
|
||||
assertThat(application).contains(
|
||||
"event-bridge-url: ${SECURITY_EVENT_BRIDGE_URL:http://event-bridge-overseas:9312}");
|
||||
assertThat(nacos).contains(
|
||||
"event-bridge-url: ${SECURITY_EVENT_BRIDGE_URL:http://event-bridge-overseas:9312}");
|
||||
assertThat(compose).contains(
|
||||
"SECURITY_EVENT_BRIDGE_URL: ${SECURITY_EVENT_BRIDGE_URL:-http://event-bridge-overseas:9312}");
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import com.baiyue.iam.repository.MemberRepository;
|
||||
import com.baiyue.iam.repository.TenantRepository;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
@@ -135,6 +136,27 @@ class MemberServiceTest {
|
||||
verify(repository, never()).insert(any());
|
||||
}
|
||||
|
||||
@Test void createPublishesActiveUserAndInitialAuthorizationAfterRoleAssignment() {
|
||||
IamRole role = activeMerchantRole(50L, 100L);
|
||||
when(roleMapper.selectById(50L)).thenReturn(role);
|
||||
when(auth.create(eq("internal-token"), any())).thenReturn(ApiResponse.success(null));
|
||||
when(auth.summary(eq("internal-token"), anyLong())).thenAnswer(invocation -> ApiResponse.success(
|
||||
new AuthAccountClient.AccountSummary(invocation.getArgument(1), "member02", "成员", "ACTIVE")));
|
||||
when(repository.findRoleIds(eq(100L), anyLong())).thenReturn(List.of(50L));
|
||||
CreateMemberRequest request = new CreateMemberRequest("member02", "Password1", "成员",
|
||||
null, null, "EMPLOYEE", "M-002", List.of(50L));
|
||||
|
||||
service.create(100L, request, Set.of("merchant:member:create", "merchant:member:grant-role"));
|
||||
|
||||
ArgumentCaptor<AuthAccountClient.CreateAccountRequest> account =
|
||||
ArgumentCaptor.forClass(AuthAccountClient.CreateAccountRequest.class);
|
||||
verify(auth).create(eq("internal-token"), account.capture());
|
||||
long authUserId = account.getValue().accountId();
|
||||
verify(repository).replaceRoles(eq(100L), anyLong(), eq(List.of(50L)));
|
||||
verify(securityEvents).userStatus(authUserId, 100L, "MERCHANT", "ACTIVE");
|
||||
verify(securityEvents).permissionChanged(authUserId, 100L, "MERCHANT", "INITIAL_AUTHORIZATION");
|
||||
}
|
||||
|
||||
@Test void shouldHandleCase08() {
|
||||
IamRole role = activeMerchantRole(50L, 100L);
|
||||
when(roleMapper.selectById(50L)).thenReturn(role);
|
||||
|
||||
@@ -41,6 +41,7 @@ class MerchantOnboardingServiceTest {
|
||||
@MockBean private IamTenantReviewMapper reviews;
|
||||
@MockBean private CustomerDimensionRuleInitializationService dimensionRuleInitialization;
|
||||
@MockBean private OverseasStorageFeeInitializationService overseasStorageFeeInitialization;
|
||||
@MockBean private SecurityEventOutboxService securityEvents;
|
||||
@Resource private MerchantOnboardingService service;
|
||||
@Test
|
||||
void clientRegistrationCreatesOwnerAndAssignsGlobalMerchantOwnerRole() {
|
||||
@@ -67,6 +68,10 @@ class MerchantOnboardingServiceTest {
|
||||
verify(membershipRoles).insert(roleGrant.capture());
|
||||
verify(dimensionRuleInitialization).initialize(result.tenantId(), 9001L);
|
||||
verify(overseasStorageFeeInitialization).initialize(result.tenantId(), 9001L, "测试企业");
|
||||
verify(securityEvents).tenantStatus(result.tenantId(), "PROFILE_REQUIRED");
|
||||
verify(securityEvents).userStatus(9001L, result.tenantId(), "MERCHANT", "ACTIVE");
|
||||
verify(securityEvents).permissionChanged(9001L, result.tenantId(), "MERCHANT",
|
||||
"INITIAL_AUTHORIZATION");
|
||||
|
||||
assertEquals("PROFILE_REQUIRED", result.tenantStatus());
|
||||
assertEquals(result.tenantId(), tenant.getValue().getId());
|
||||
@@ -194,3 +199,4 @@ class MerchantOnboardingServiceTest {
|
||||
verify(tenants, never()).deleteById((java.io.Serializable) any());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -85,6 +85,39 @@ class PlatformInternalUserServiceTest {
|
||||
verify(repository).insertUser(any(IamUser.class));
|
||||
verify(repository).insert(any(IamMembership.class));
|
||||
verify(repository).replaceRoles(eq(0L), anyLong(), eq(List.of(40L)));
|
||||
long authUserId = captor.getValue().accountId();
|
||||
verify(securityEvents).userStatus(authUserId, 0L, "PLATFORM", "ACTIVE");
|
||||
verify(securityEvents).permissionChanged(authUserId, 0L, "PLATFORM", "INITIAL_AUTHORIZATION");
|
||||
}
|
||||
|
||||
@Test
|
||||
void linkingExistingAccountPublishesInitialProjectionOnlyForNewMembership() {
|
||||
IamRole role = new IamRole();
|
||||
role.setId(40L); role.setTenantId(0L); role.setAppCode("PLATFORM"); role.setStatus("ACTIVE");
|
||||
when(roleMapper.selectById(40L)).thenReturn(role);
|
||||
when(repository.findUserByAuthId(10L)).thenReturn(user);
|
||||
when(repository.findByTenantAndAuthUser(0L, 10L)).thenReturn(null);
|
||||
CreateInternalUserRequest request = new CreateInternalUserRequest(10L, null, "平台运营",
|
||||
"13800138000", null, null, null, "PLATFORM-LINK", List.of(40L));
|
||||
|
||||
service.create(request);
|
||||
|
||||
verify(repository).replaceRoles(eq(0L), anyLong(), eq(List.of(40L)));
|
||||
verify(securityEvents).userStatus(10L, 0L, "PLATFORM", "ACTIVE");
|
||||
verify(securityEvents).permissionChanged(10L, 0L, "PLATFORM", "INITIAL_AUTHORIZATION");
|
||||
verify(auth, never()).create(anyString(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void relinkingExistingMembershipDoesNotDuplicateInitialProjection() {
|
||||
when(repository.findByTenantAndAuthUser(0L, 10L)).thenReturn(membership);
|
||||
CreateInternalUserRequest request = new CreateInternalUserRequest(10L, null, "平台运营",
|
||||
"13800138000", null, null, null, "PLATFORM-30", List.of());
|
||||
|
||||
service.create(request);
|
||||
|
||||
verifyNoInteractions(securityEvents);
|
||||
verify(repository, never()).replaceRoles(anyLong(), anyLong(), anyList());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -12,6 +12,7 @@ import com.baiyue.iam.mapper.IamRoleMapper;
|
||||
import com.baiyue.iam.repository.MemberRepository;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
@@ -20,6 +21,7 @@ import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
@@ -85,6 +87,30 @@ class PlatformUserManagementServiceTest {
|
||||
verify(auth, never()).create(anyString(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void createPublishesActualStatusAndInitialAuthorizationAfterRolesAreStored() {
|
||||
AtomicReference<IamUser> createdUser = new AtomicReference<>();
|
||||
doAnswer(invocation -> { createdUser.set(invocation.getArgument(0)); return null; })
|
||||
.when(members).insertUser(any(IamUser.class));
|
||||
when(members.findUser(anyLong())).thenAnswer(invocation -> createdUser.get());
|
||||
when(members.findRoleIds(eq(0L), anyLong())).thenReturn(List.of(40001L));
|
||||
when(auth.create(eq("internal-token"), any())).thenReturn(ApiResponse.success(null));
|
||||
when(auth.summary(eq("internal-token"), anyLong())).thenAnswer(invocation -> ApiResponse.success(
|
||||
new AuthAccountClient.AccountSummary(invocation.getArgument(1), "newuser", "新用户", "DISABLED")));
|
||||
CreateUserRequest request = new CreateUserRequest("newuser", "新用户", "13900139000",
|
||||
"new@example.com", 0L, List.of(40001L), "Password1", "DISABLED");
|
||||
|
||||
service.create(request);
|
||||
|
||||
ArgumentCaptor<AuthAccountClient.CreateAccountRequest> account =
|
||||
ArgumentCaptor.forClass(AuthAccountClient.CreateAccountRequest.class);
|
||||
verify(auth).create(eq("internal-token"), account.capture());
|
||||
long authUserId = account.getValue().accountId();
|
||||
verify(members).replaceRoles(eq(0L), anyLong(), eq(List.of(40001L)));
|
||||
verify(securityEvents).userStatus(authUserId, 0L, "PLATFORM", "DISABLED");
|
||||
verify(securityEvents).permissionChanged(authUserId, 0L, "PLATFORM", "INITIAL_AUTHORIZATION");
|
||||
}
|
||||
|
||||
@Test
|
||||
void optionsContainOnlyPlatformTenantAndPlatformRoles() {
|
||||
UserOptions result = service.options();
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
package com.baiyue.iam.service;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.event.EventEnvelope;
|
||||
import com.baiyue.common.event.EventPayloadCanonicalizer;
|
||||
import com.baiyue.common.event.EventPayloadHash;
|
||||
import com.baiyue.common.event.EventTypes;
|
||||
import com.baiyue.common.event.IamSecurityEventsV1;
|
||||
import com.baiyue.common.event.RegionCode;
|
||||
import com.baiyue.iam.client.SecurityEventBridgeClient;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
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.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class SecurityEventOutboxRelayTest {
|
||||
@Mock private JdbcTemplate jdbc;
|
||||
@Mock private SecurityEventBridgeClient client;
|
||||
private final ObjectMapper objectMapper = new ObjectMapper().findAndRegisterModules();
|
||||
private SecurityEventOutboxRelay relay;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
relay = new SecurityEventOutboxRelay();
|
||||
ReflectionTestUtils.setField(relay, "jdbc", jdbc);
|
||||
ReflectionTestUtils.setField(relay, "client", client);
|
||||
ReflectionTestUtils.setField(relay, "objectMapper", objectMapper);
|
||||
ReflectionTestUtils.setField(relay, "internalToken", "internal-token");
|
||||
ReflectionTestUtils.setField(relay, "maxAttempts", 12);
|
||||
}
|
||||
|
||||
@Test
|
||||
void cnToOverseasEventIsSentOnlyAfterMatchingBridgeAcknowledgement() throws Exception {
|
||||
EventEnvelope envelope = envelope(RegionCode.OVERSEAS);
|
||||
when(jdbc.update(anyString(), any(Object[].class))).thenReturn(1);
|
||||
when(jdbc.queryForObject(anyString(), eq(String.class), any(), any()))
|
||||
.thenReturn(objectMapper.writeValueAsString(envelope));
|
||||
when(client.receive(eq("internal-token"), any(EventEnvelope.class))).thenReturn(ApiResponse.success(
|
||||
new SecurityEventBridgeClient.Acknowledgement(envelope.eventId(), "ACCEPTED", "2026-08-25T07:00:00Z")));
|
||||
|
||||
relay.deliver(envelope.eventId());
|
||||
|
||||
ArgumentCaptor<EventEnvelope> sent = ArgumentCaptor.forClass(EventEnvelope.class);
|
||||
verify(client).receive(eq("internal-token"), sent.capture());
|
||||
assertThat(sent.getValue().sourceRegion()).isEqualTo(RegionCode.CN);
|
||||
assertThat(sent.getValue().targetRegion()).isEqualTo(RegionCode.OVERSEAS);
|
||||
verify(jdbc).update(anyString(), any(), any(), eq(envelope.eventId()), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void nonOverseasTargetNeverReachesTheBridge() throws Exception {
|
||||
EventEnvelope envelope = envelope(RegionCode.CN);
|
||||
when(jdbc.update(anyString(), any(Object[].class))).thenReturn(1);
|
||||
when(jdbc.queryForObject(anyString(), eq(String.class), any(), any()))
|
||||
.thenReturn(objectMapper.writeValueAsString(envelope));
|
||||
when(jdbc.queryForObject(anyString(), eq(Integer.class), eq(envelope.eventId()))).thenReturn(0);
|
||||
|
||||
relay.deliver(envelope.eventId());
|
||||
|
||||
verify(client, never()).receive(anyString(), any(EventEnvelope.class));
|
||||
verify(jdbc).update(anyString(), eq("PENDING"), eq(1), any(), anyString(), any(),
|
||||
eq(envelope.eventId()), anyString());
|
||||
}
|
||||
|
||||
private EventEnvelope envelope(RegionCode target) {
|
||||
var value = new IamSecurityEventsV1.PermissionVersionChanged(
|
||||
101L, 0L, "PLATFORM", 3L, "contract", Instant.parse("2026-08-25T07:00:00Z"));
|
||||
var payload = objectMapper.valueToTree(value);
|
||||
return new EventEnvelope("00000000-0000-4000-8000-000000000081",
|
||||
EventTypes.IAM_PERMISSION_VERSION_CHANGED, IamSecurityEventsV1.VERSION,
|
||||
Instant.parse("2026-08-25T07:00:00Z"), RegionCode.CN, target, "0",
|
||||
"AUTHORIZATION", "101", 3L, null, "trace-12345678", "correlation-12345678",
|
||||
null, payload, EventPayloadHash.sha256(EventPayloadCanonicalizer.canonicalize(payload)));
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,13 @@
|
||||
package com.baiyue.iam.service;
|
||||
|
||||
import com.baiyue.common.event.EventEnvelope;
|
||||
import com.baiyue.common.event.IamSecurityEventsV1;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.CsvSource;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
@@ -43,4 +46,29 @@ class SecurityEventOutboxServiceTest {
|
||||
EventEnvelope envelope = objectMapper.readValue(String.valueOf(parameters.getValue()[4]), EventEnvelope.class);
|
||||
assertThat(envelope.requireValid().correlationId()).isNotBlank().isEqualTo(envelope.traceId());
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource({
|
||||
"ACTIVE,ACTIVE",
|
||||
"PROFILE_REQUIRED,DISABLED",
|
||||
"PENDING,DISABLED",
|
||||
"REJECTED,DISABLED",
|
||||
"DISABLED,DISABLED",
|
||||
"ARCHIVED,DISABLED"
|
||||
})
|
||||
void tenantWorkflowStatusIsNormalizedToTheSecurityContract(String businessStatus,
|
||||
String expectedSecurityStatus) throws Exception {
|
||||
when(jdbc.queryForObject(anyString(), eq(Long.class), any())).thenReturn(3L);
|
||||
|
||||
service.tenantStatus(10001L, businessStatus);
|
||||
|
||||
ArgumentCaptor<Object[]> parameters = ArgumentCaptor.forClass(Object[].class);
|
||||
verify(jdbc).update(eq("INSERT INTO iam_security_event_outbox(event_id,event_type,aggregate_type,aggregate_id,payload_json) "
|
||||
+ "VALUES (?,?,?,?,CAST(? AS JSON))"), parameters.capture());
|
||||
EventEnvelope envelope = objectMapper.readValue(String.valueOf(parameters.getValue()[4]), EventEnvelope.class);
|
||||
IamSecurityEventsV1.TenantStatusChanged payload = objectMapper.treeToValue(
|
||||
envelope.payload(), IamSecurityEventsV1.TenantStatusChanged.class);
|
||||
assertThat(payload.status()).isEqualTo(expectedSecurityStatus);
|
||||
assertThat(envelope.aggregateVersion()).isEqualTo(3L);
|
||||
}
|
||||
}
|
||||
|
||||
+11
@@ -34,6 +34,7 @@ import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@@ -43,6 +44,7 @@ class TenantAdministratorProvisioningServiceTest {
|
||||
@MockBean private MemberRepository members;
|
||||
@MockBean private IamRoleMapper roles;
|
||||
@MockBean private AuthAccountClient auth;
|
||||
@MockBean private SecurityEventOutboxService securityEvents;
|
||||
@Resource private TenantAdministratorProvisioningService service;
|
||||
private IamTenant tenant;
|
||||
|
||||
@@ -101,6 +103,9 @@ class TenantAdministratorProvisioningServiceTest {
|
||||
verify(members).insertUser(any(IamUser.class));
|
||||
verify(members).insert(any(IamMembership.class));
|
||||
verify(members).replaceRoles(eq(tenant.getId()), anyLong(), eq(List.of(4000100L)));
|
||||
verify(securityEvents).userStatus(account.getValue().accountId(), tenant.getId(), "MERCHANT", "ACTIVE");
|
||||
verify(securityEvents).permissionChanged(account.getValue().accountId(), tenant.getId(), "MERCHANT",
|
||||
"INITIAL_AUTHORIZATION");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -135,6 +140,7 @@ class TenantAdministratorProvisioningServiceTest {
|
||||
assertEquals(null, result.temporaryPassword());
|
||||
verify(auth, never()).create(anyString(), any());
|
||||
verify(members, never()).replaceRoles(anyLong(), anyLong(), any());
|
||||
verifyNoInteractions(securityEvents);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -153,6 +159,8 @@ class TenantAdministratorProvisioningServiceTest {
|
||||
assertFalse(result.created());
|
||||
verify(auth, never()).create(anyString(), any());
|
||||
verify(members).replaceRoles(tenant.getId(), owner.getId(), List.of(4000200L, 4000100L));
|
||||
verify(securityEvents).permissionChanged(user.getAuthUserId(), tenant.getId(), "MERCHANT",
|
||||
"MEMBER_ROLES_CHANGED");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -190,6 +198,9 @@ class TenantAdministratorProvisioningServiceTest {
|
||||
verify(members, never()).insertUser(any());
|
||||
verify(members, never()).insert(any());
|
||||
verify(members, never()).replaceRoles(anyLong(), anyLong(), any());
|
||||
verify(securityEvents).userStatus(user.getAuthUserId(), tenant.getId(), "MERCHANT", "ACTIVE");
|
||||
verify(securityEvents).permissionChanged(user.getAuthUserId(), tenant.getId(), "MERCHANT",
|
||||
"INITIAL_AUTHORIZATION");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -320,6 +320,7 @@ class TenantServiceTest {
|
||||
verify(repository).insertTenant(argThat(tenant -> Long.valueOf(9001L).equals(tenant.getCreatedBy())
|
||||
&& Long.valueOf(9001L).equals(tenant.getUpdatedBy())));
|
||||
verify(dimensionRuleInitialization).initialize(created.tenant().id(), 9001L);
|
||||
verify(securityEvents).tenantStatus(created.tenant().id(), "PROFILE_REQUIRED");
|
||||
verify(provisioning).provisionPlatformCreatedTenant(any(), eq(9001L));
|
||||
verify(repository, never()).insertReview(any());
|
||||
}
|
||||
@@ -428,6 +429,7 @@ class TenantServiceTest {
|
||||
assertEquals("ACTIVE", tenant.getStatus());
|
||||
verify(repository).saveReview(review);
|
||||
verify(repository).saveTenant(tenant);
|
||||
verify(securityEvents).tenantStatus(10001L, "ACTIVE");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -527,3 +529,4 @@ class TenantServiceTest {
|
||||
return review;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user