This commit is contained in:
2026-08-25 16:40:48 +08:00
parent 1f6f1a0677
commit 87c5e18ca6
93 changed files with 1154 additions and 527 deletions
@@ -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");
+3
View File
@@ -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}"