优化
This commit is contained in:
@@ -1,18 +0,0 @@
|
||||
package com.baiyue.iam.client;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.event.EventEnvelope;
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
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",
|
||||
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,
|
||||
@RequestBody EventEnvelope envelope);
|
||||
|
||||
record Acknowledgement(String eventId, String result, String receivedAt) { }
|
||||
}
|
||||
@@ -42,6 +42,14 @@ public final class AuthorizationDtos {
|
||||
private Long permissionVersion;
|
||||
private Long overseasWarehouseId;
|
||||
private Long domesticWarehouseId;
|
||||
private Long tenantVersion;
|
||||
|
||||
public SnapshotResponse(Long userId, Long tenantId, Long membershipId, String appCode,
|
||||
String clientType, Set<String> roles, Set<String> permissions,
|
||||
Long permissionVersion, Long overseasWarehouseId, Long domesticWarehouseId) {
|
||||
this(userId, tenantId, membershipId, appCode, clientType, roles, permissions,
|
||||
permissionVersion, overseasWarehouseId, domesticWarehouseId, 0L);
|
||||
}
|
||||
|
||||
public SnapshotResponse(Long userId, Long tenantId, Long membershipId, String appCode,
|
||||
String clientType, Set<String> roles, Set<String> permissions,
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.baiyue.iam.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/** Rows from one MVCC statement; all identity/authorization facts share one read view. */
|
||||
@Data
|
||||
public class AuthorizationSnapshotRow {
|
||||
private String roleCode;
|
||||
private String permissionCode;
|
||||
private Long permissionVersion;
|
||||
private Long tenantVersion;
|
||||
private Long overseasWarehouseId;
|
||||
private Long domesticWarehouseId;
|
||||
}
|
||||
@@ -12,105 +12,27 @@ import java.util.List;
|
||||
@Mapper
|
||||
public interface AuthorizationQueryMapper {
|
||||
@Select("""
|
||||
SELECT COUNT(1)
|
||||
FROM iam_user u
|
||||
JOIN iam_membership m ON m.user_id = u.id
|
||||
WHERE u.auth_user_id = #{userId}
|
||||
AND u.status = 'ACTIVE' AND u.deleted = 0
|
||||
AND m.id = #{membershipId} AND m.tenant_id = #{tenantId}
|
||||
AND m.status = 'ACTIVE' AND m.deleted = 0
|
||||
AND (m.tenant_id = 0 OR EXISTS (
|
||||
SELECT 1 FROM iam_tenant t WHERE t.id = m.tenant_id AND t.deleted = 0
|
||||
AND t.status IN ('ACTIVE', 'PROFILE_REQUIRED', 'PENDING', 'REJECTED')
|
||||
))
|
||||
SELECT r.role_code AS roleCode, p.permission_code AS permissionCode,
|
||||
COALESCE(sv.permission_version,0) AS permissionVersion,
|
||||
COALESCE(tv.status_version,0) AS tenantVersion,
|
||||
m.overseas_warehouse_id AS overseasWarehouseId,
|
||||
m.domestic_warehouse_id AS domesticWarehouseId
|
||||
FROM iam_user u
|
||||
JOIN iam_membership m ON m.user_id=u.id
|
||||
LEFT JOIN iam_tenant t ON t.id=m.tenant_id AND t.deleted=0
|
||||
LEFT JOIN iam_membership_role mr ON mr.membership_id=m.id AND mr.tenant_id=m.tenant_id
|
||||
LEFT JOIN iam_role r ON r.id=mr.role_id AND (r.tenant_id=0 OR r.tenant_id=m.tenant_id)
|
||||
AND r.app_code=#{appCode} AND r.status='ACTIVE' AND r.deleted=0
|
||||
LEFT JOIN iam_role_permission rp ON rp.role_id=r.id
|
||||
LEFT JOIN iam_permission p ON p.id=rp.permission_id AND (p.tenant_id=0 OR p.tenant_id=m.tenant_id)
|
||||
AND p.app_code=#{appCode} AND p.status='ACTIVE' AND p.deleted=0
|
||||
LEFT JOIN iam_security_subject_version sv ON sv.auth_user_id=u.auth_user_id
|
||||
LEFT JOIN iam_security_tenant_version tv ON tv.tenant_id=m.tenant_id
|
||||
WHERE u.auth_user_id=#{userId} AND u.status='ACTIVE' AND u.deleted=0
|
||||
AND m.id=#{membershipId} AND m.tenant_id=#{tenantId} AND m.status='ACTIVE' AND m.deleted=0
|
||||
AND (m.tenant_id=0 OR t.status IN ('ACTIVE','PROFILE_REQUIRED','PENDING','REJECTED'))
|
||||
""")
|
||||
int countActiveIdentity(@Param("userId") long userId,
|
||||
@Param("tenantId") long tenantId,
|
||||
@Param("membershipId") long membershipId);
|
||||
|
||||
@Select("""
|
||||
SELECT DISTINCT r.role_code
|
||||
FROM iam_user u
|
||||
JOIN iam_membership m ON m.user_id = u.id
|
||||
JOIN iam_membership_role mr ON mr.membership_id = m.id AND mr.tenant_id = m.tenant_id
|
||||
JOIN iam_role r ON r.id = mr.role_id
|
||||
WHERE u.auth_user_id = #{userId}
|
||||
AND m.id = #{membershipId} AND m.tenant_id = #{tenantId}
|
||||
AND (r.tenant_id = 0 OR r.tenant_id = #{tenantId})
|
||||
AND r.app_code = #{appCode}
|
||||
AND r.status = 'ACTIVE' AND r.deleted = 0
|
||||
ORDER BY r.role_code
|
||||
""")
|
||||
List<String> findRoles(@Param("userId") long userId,
|
||||
@Param("tenantId") long tenantId,
|
||||
@Param("membershipId") long membershipId,
|
||||
@Param("appCode") String appCode);
|
||||
|
||||
@Select("""
|
||||
SELECT DISTINCT p.permission_code
|
||||
FROM iam_user u
|
||||
JOIN iam_membership m ON m.user_id = u.id
|
||||
JOIN iam_membership_role mr ON mr.membership_id = m.id AND mr.tenant_id = m.tenant_id
|
||||
JOIN iam_role r ON r.id = mr.role_id
|
||||
JOIN iam_role_permission rp ON rp.role_id = r.id
|
||||
JOIN iam_permission p ON p.id = rp.permission_id
|
||||
WHERE u.auth_user_id = #{userId}
|
||||
AND m.id = #{membershipId} AND m.tenant_id = #{tenantId}
|
||||
AND (r.tenant_id = 0 OR r.tenant_id = #{tenantId})
|
||||
AND r.app_code = #{appCode} AND r.status = 'ACTIVE' AND r.deleted = 0
|
||||
AND (p.tenant_id = 0 OR p.tenant_id = #{tenantId})
|
||||
AND p.app_code = #{appCode} AND p.status = 'ACTIVE' AND p.deleted = 0
|
||||
ORDER BY p.permission_code
|
||||
""")
|
||||
List<String> findPermissions(@Param("userId") long userId,
|
||||
@Param("tenantId") long tenantId,
|
||||
@Param("membershipId") long membershipId,
|
||||
@Param("appCode") String appCode);
|
||||
|
||||
@Select("""
|
||||
SELECT DISTINCT r.role_code
|
||||
FROM iam_user u
|
||||
JOIN iam_membership m ON m.user_id = u.id
|
||||
JOIN iam_membership_role mr ON mr.membership_id = m.id AND mr.tenant_id = m.tenant_id
|
||||
JOIN iam_role r ON r.id = mr.role_id
|
||||
WHERE u.auth_user_id = #{userId} AND u.status = 'ACTIVE' AND u.deleted = 0
|
||||
AND m.tenant_id = #{tenantId} AND m.status = 'ACTIVE' AND m.deleted = 0
|
||||
AND (r.tenant_id = 0 OR r.tenant_id = #{tenantId})
|
||||
AND r.app_code = #{appCode} AND r.status = 'ACTIVE' AND r.deleted = 0
|
||||
ORDER BY r.role_code
|
||||
""")
|
||||
List<String> findRolesByIdentity(@Param("userId") long userId,
|
||||
@Param("tenantId") long tenantId,
|
||||
@Param("appCode") String appCode);
|
||||
|
||||
@Select("""
|
||||
SELECT DISTINCT p.permission_code
|
||||
FROM iam_user u
|
||||
JOIN iam_membership m ON m.user_id = u.id
|
||||
JOIN iam_membership_role mr ON mr.membership_id = m.id AND mr.tenant_id = m.tenant_id
|
||||
JOIN iam_role r ON r.id = mr.role_id
|
||||
JOIN iam_role_permission rp ON rp.role_id = r.id
|
||||
JOIN iam_permission p ON p.id = rp.permission_id
|
||||
WHERE u.auth_user_id = #{userId} AND u.status = 'ACTIVE' AND u.deleted = 0
|
||||
AND m.tenant_id = #{tenantId} AND m.status = 'ACTIVE' AND m.deleted = 0
|
||||
AND (r.tenant_id = 0 OR r.tenant_id = #{tenantId})
|
||||
AND r.app_code = #{appCode} AND r.status = 'ACTIVE' AND r.deleted = 0
|
||||
AND (p.tenant_id = 0 OR p.tenant_id = #{tenantId})
|
||||
AND p.app_code = #{appCode} AND p.status = 'ACTIVE' AND p.deleted = 0
|
||||
ORDER BY p.permission_code
|
||||
""")
|
||||
List<String> findPermissionsByIdentity(@Param("userId") long userId,
|
||||
@Param("tenantId") long tenantId,
|
||||
@Param("appCode") String appCode);
|
||||
|
||||
@Select("SELECT COALESCE(MAX(permission_version),0) FROM iam_security_subject_version WHERE auth_user_id=#{userId}")
|
||||
long findPermissionVersion(@Param("userId") long userId);
|
||||
|
||||
@Select("SELECT overseas_warehouse_id FROM iam_membership WHERE id=#{membershipId} AND tenant_id=#{tenantId} AND status='ACTIVE' AND deleted=0")
|
||||
Long findOverseasWarehouseId(@Param("tenantId") long tenantId,
|
||||
@Param("membershipId") long membershipId);
|
||||
|
||||
@Select("SELECT domestic_warehouse_id FROM iam_membership WHERE id=#{membershipId} AND tenant_id=#{tenantId} AND status='ACTIVE' AND deleted=0")
|
||||
Long findDomesticWarehouseId(@Param("tenantId") long tenantId,
|
||||
@Param("membershipId") long membershipId);
|
||||
List<com.baiyue.iam.dto.AuthorizationSnapshotRow> findSnapshot(
|
||||
@Param("userId") long userId, @Param("tenantId") long tenantId,
|
||||
@Param("membershipId") long membershipId, @Param("appCode") String appCode);
|
||||
}
|
||||
|
||||
@@ -19,7 +19,8 @@ public interface IamMembershipRoleMapper extends BaseMapper<IamMembershipRole> {
|
||||
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}
|
||||
JOIN iam_role r ON r.id = mr.role_id
|
||||
WHERE (#{tenantId} = 0 OR r.tenant_id = #{tenantId})
|
||||
AND mr.role_id = #{roleId}
|
||||
AND m.deleted = 0
|
||||
AND u.deleted = 0
|
||||
|
||||
@@ -69,7 +69,7 @@ public class RbacRepository {
|
||||
public Set<Long> findRoleIdsByPermission(long tenantId, Long permissionId) {
|
||||
List<IamRolePermission> rows = rolePermissionMapper.selectList(
|
||||
new LambdaQueryWrapper<IamRolePermission>()
|
||||
.eq(IamRolePermission::getTenantId, tenantId)
|
||||
.eq(tenantId != 0, IamRolePermission::getTenantId, tenantId)
|
||||
.eq(IamRolePermission::getPermissionId, permissionId));
|
||||
Set<Long> result = new LinkedHashSet<>();
|
||||
rows.forEach(row -> result.add(row.getRoleId()));
|
||||
|
||||
@@ -24,7 +24,7 @@ public class AuthorizationSnapshotService {
|
||||
private AuthorizationQueryMapper mapper;
|
||||
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
@Transactional(readOnly = true, isolation = org.springframework.transaction.annotation.Isolation.READ_COMMITTED)
|
||||
public SnapshotResponse load(SnapshotRequest request) {
|
||||
requireRequest(request);
|
||||
String appCode = request.getAppCode().trim().toUpperCase(java.util.Locale.ROOT);
|
||||
@@ -37,25 +37,24 @@ public class AuthorizationSnapshotService {
|
||||
if ("PLATFORM".equals(appCode) && request.getTenantId() != 0) {
|
||||
throw new BizException(ResultCode.FORBIDDEN, "平台账号必须属于平台租户");
|
||||
}
|
||||
if (mapper.countActiveIdentity(request.getUserId(), request.getTenantId(), request.getMembershipId()) == 0) {
|
||||
var rows = mapper.findSnapshot(request.getUserId(), request.getTenantId(), request.getMembershipId(), appCode);
|
||||
if (rows.isEmpty()) {
|
||||
throw new BizException(ResultCode.FORBIDDEN, "IAM 用户或成员身份无效");
|
||||
}
|
||||
|
||||
Set<String> roles = normalizedSet(mapper.findRoles(
|
||||
request.getUserId(), request.getTenantId(), request.getMembershipId(), appCode));
|
||||
Set<String> permissions = normalizedSet(mapper.findPermissions(
|
||||
request.getUserId(), request.getTenantId(), request.getMembershipId(), appCode));
|
||||
Set<String> roles = normalizedSet(rows.stream().map(com.baiyue.iam.dto.AuthorizationSnapshotRow::getRoleCode).toList());
|
||||
Set<String> permissions = normalizedSet(rows.stream().map(com.baiyue.iam.dto.AuthorizationSnapshotRow::getPermissionCode).toList());
|
||||
var identity = rows.get(0);
|
||||
return new SnapshotResponse(request.getUserId(), request.getTenantId(), request.getMembershipId(),
|
||||
appCode, appCode, Set.copyOf(roles), Set.copyOf(permissions),
|
||||
mapper.findPermissionVersion(request.getUserId()),
|
||||
mapper.findOverseasWarehouseId(request.getTenantId(), request.getMembershipId()),
|
||||
mapper.findDomesticWarehouseId(request.getTenantId(), request.getMembershipId()));
|
||||
identity.getPermissionVersion(), identity.getOverseasWarehouseId(),
|
||||
identity.getDomesticWarehouseId(), identity.getTenantVersion());
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据已认证请求主体重新计算有效权限,而不是复用 JWT 中可能已经过期的权限声明。
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
@Transactional(readOnly = true, isolation = org.springframework.transaction.annotation.Isolation.READ_COMMITTED)
|
||||
public Set<String> currentPermissions(AuthenticatedUser user, String appCode) {
|
||||
if (user == null || user.userId() == null || user.tenantId() == null || user.membershipId() == null) {
|
||||
throw new BizException(ResultCode.UNAUTHORIZED, "Authentication identity is incomplete / 登录身份不完整");
|
||||
|
||||
@@ -41,7 +41,7 @@ public class MemberService {
|
||||
@Resource
|
||||
private AuthAccountClient authClient;
|
||||
@Resource
|
||||
private SecurityEventOutboxService securityEvents;
|
||||
private SecurityVersionService securityVersions;
|
||||
@Value("${baiyue.security.internal-service-token}")
|
||||
private String internalToken;
|
||||
|
||||
@@ -106,8 +106,7 @@ 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");
|
||||
securityVersions.subjectChanged(authId);
|
||||
return response(tenantId, membership, user);
|
||||
} catch (RuntimeException exception) {
|
||||
try {
|
||||
@@ -146,6 +145,7 @@ public class MemberService {
|
||||
if (!new HashSet<>(currentRoleIds).equals(new HashSet<>(request.roleIds()))) {
|
||||
call(() -> authClient.revokeSessions(internalToken,
|
||||
new AuthAccountClient.RevokeAccountSessionsRequest(Set.of(user.getAuthUserId()))));
|
||||
securityVersions.subjectChanged(user.getAuthUserId());
|
||||
}
|
||||
return response(tenantId, membership);
|
||||
}
|
||||
@@ -169,9 +169,7 @@ public class MemberService {
|
||||
membership.setStatus(request.status());
|
||||
membership.setLeftAt("DISABLED".equals(request.status()) ? LocalDateTime.now() : null);
|
||||
user.setStatus(request.status()); repository.saveUser(user); repository.save(membership);
|
||||
securityEvents.userStatus(user.getAuthUserId(), tenantId, "MERCHANT", request.status());
|
||||
securityEvents.permissionChanged(user.getAuthUserId(), tenantId, "MERCHANT",
|
||||
"MEMBER_STATUS_CHANGED");
|
||||
securityVersions.subjectChanged(user.getAuthUserId());
|
||||
} catch (RuntimeException localFailure) {
|
||||
try {
|
||||
authClient.status(internalToken, user.getAuthUserId(), new AuthAccountClient.StatusRequest(oldStatus));
|
||||
|
||||
@@ -43,7 +43,7 @@ public class MerchantOnboardingService {
|
||||
@Resource private IamTenantReviewMapper reviewMapper;
|
||||
@Resource private CustomerDimensionRuleInitializationService dimensionRuleInitialization;
|
||||
@Resource private OverseasStorageFeeInitializationService overseasStorageFeeInitialization;
|
||||
@Resource private SecurityEventOutboxService securityEvents;
|
||||
@Resource private SecurityVersionService securityVersions;
|
||||
@Resource private CustomerCodeRepository customerCodes;
|
||||
|
||||
@Transactional
|
||||
@@ -82,7 +82,7 @@ public class MerchantOnboardingService {
|
||||
tenant.setDeleted(false);
|
||||
tenant.setVersion(0);
|
||||
tenantMapper.insert(tenant);
|
||||
securityEvents.tenantStatus(tenantId, tenant.getStatus());
|
||||
securityVersions.tenantChanged(tenantId);
|
||||
|
||||
IamUser user = new IamUser();
|
||||
user.setId(userId);
|
||||
@@ -114,8 +114,7 @@ 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");
|
||||
securityVersions.subjectChanged(request.authUserId());
|
||||
|
||||
if (StringUtils.hasText(request.address())) {
|
||||
IamTenantAddress address = new IamTenantAddress();
|
||||
|
||||
@@ -39,7 +39,7 @@ public class PlatformInternalUserService {
|
||||
@Resource
|
||||
private AuthAccountClient authClient;
|
||||
@Resource
|
||||
private SecurityEventOutboxService securityEvents;
|
||||
private SecurityVersionService securityVersions;
|
||||
@Value("${baiyue.security.internal-service-token}")
|
||||
private String internalToken;
|
||||
|
||||
@@ -77,7 +77,7 @@ public class PlatformInternalUserService {
|
||||
repository.insertUser(user);
|
||||
repository.insert(membership);
|
||||
repository.replaceRoles(PLATFORM_TENANT_ID, membershipId, roleIds);
|
||||
publishInitialSecurityProjection(user);
|
||||
initializeSecurityVersion(user);
|
||||
return response(membership, user, request.username(), roleIds);
|
||||
} catch (RuntimeException localFailure) {
|
||||
compensateCreate(authUserId, localFailure);
|
||||
@@ -116,6 +116,7 @@ public class PlatformInternalUserService {
|
||||
repository.save(membership);
|
||||
repository.replaceRoles(PLATFORM_TENANT_ID, membershipId, roleIds);
|
||||
if (rolesChanged && !displayNameChanged) revokeSessions(user.getAuthUserId());
|
||||
if (rolesChanged) securityVersions.subjectChanged(user.getAuthUserId());
|
||||
} catch (RuntimeException localFailure) {
|
||||
if (displayNameChanged) compensateProfile(user.getAuthUserId(), oldAccount.displayName(), localFailure);
|
||||
throw localFailure;
|
||||
@@ -146,9 +147,7 @@ public class PlatformInternalUserService {
|
||||
user.setStatus(request.status());
|
||||
repository.saveUser(user);
|
||||
repository.save(membership);
|
||||
securityEvents.userStatus(user.getAuthUserId(), PLATFORM_TENANT_ID, "PLATFORM", request.status());
|
||||
securityEvents.permissionChanged(user.getAuthUserId(), PLATFORM_TENANT_ID, "PLATFORM",
|
||||
"PLATFORM_INTERNAL_USER_STATUS_CHANGED");
|
||||
securityVersions.subjectChanged(user.getAuthUserId());
|
||||
return response(membership, user, authAccount.username(), roleIds);
|
||||
} catch (RuntimeException localFailure) {
|
||||
compensateStatus(user.getAuthUserId(), authAccount.status(), localFailure);
|
||||
@@ -178,8 +177,7 @@ public class PlatformInternalUserService {
|
||||
repository.replaceRoles(PLATFORM_TENANT_ID, membershipId, roleIds);
|
||||
if (!new java.util.HashSet<>(currentRoleIds).equals(new java.util.HashSet<>(roleIds))) {
|
||||
revokeSessions(user.getAuthUserId());
|
||||
securityEvents.permissionChanged(user.getAuthUserId(), PLATFORM_TENANT_ID, "PLATFORM",
|
||||
"MEMBER_ROLES_CHANGED");
|
||||
securityVersions.subjectChanged(user.getAuthUserId());
|
||||
}
|
||||
AuthAccountClient.AccountSummary authAccount = account(user.getAuthUserId());
|
||||
return response(membership, user, authAccount.username(), roleIds);
|
||||
@@ -204,14 +202,12 @@ public class PlatformInternalUserService {
|
||||
IamMembership membership = newMembership(membershipId, user.getId(), memberNo);
|
||||
repository.insert(membership);
|
||||
repository.replaceRoles(PLATFORM_TENANT_ID, membershipId, roleIds);
|
||||
publishInitialSecurityProjection(user);
|
||||
initializeSecurityVersion(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 void initializeSecurityVersion(IamUser user) {
|
||||
securityVersions.subjectChanged(user.getAuthUserId());
|
||||
}
|
||||
|
||||
private InternalUserResponse response(IamMembership membership) {
|
||||
|
||||
@@ -44,7 +44,7 @@ public class PlatformUserManagementService {
|
||||
@Resource
|
||||
private AuthAccountClient authClient;
|
||||
@Resource
|
||||
private SecurityEventOutboxService securityEvents;
|
||||
private SecurityVersionService securityVersions;
|
||||
@Value("${baiyue.security.internal-service-token}")
|
||||
private String internalToken;
|
||||
|
||||
@@ -107,8 +107,7 @@ 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");
|
||||
securityVersions.subjectChanged(authUserId);
|
||||
return response(membership);
|
||||
} catch (RuntimeException localFailure) {
|
||||
compensateCreate(authUserId, localFailure);
|
||||
@@ -170,8 +169,7 @@ public class PlatformUserManagementService {
|
||||
new AuthAccountClient.RevokeAccountSessionsRequest(Set.of(user.getAuthUserId()))));
|
||||
}
|
||||
if (rolesChanged || warehouseChanged || scopeChanged) {
|
||||
securityEvents.permissionChanged(user.getAuthUserId(), newTenantId,
|
||||
appCode(newTenantId), "PLATFORM_USER_AUTHORIZATION_CHANGED");
|
||||
securityVersions.subjectChanged(user.getAuthUserId());
|
||||
}
|
||||
} catch (RuntimeException localFailure) {
|
||||
compensateUpdate(user.getAuthUserId(), membershipId, oldTenantId, oldAccount,
|
||||
@@ -204,10 +202,7 @@ public class PlatformUserManagementService {
|
||||
user.setStatus(request.status());
|
||||
members.saveUser(user);
|
||||
members.save(membership);
|
||||
securityEvents.userStatus(user.getAuthUserId(), membership.getTenantId(),
|
||||
appCode(membership.getTenantId()), request.status());
|
||||
securityEvents.permissionChanged(user.getAuthUserId(), membership.getTenantId(),
|
||||
appCode(membership.getTenantId()), "PLATFORM_USER_STATUS_CHANGED");
|
||||
securityVersions.subjectChanged(user.getAuthUserId());
|
||||
} catch (RuntimeException localFailure) {
|
||||
call(() -> authClient.status(internalToken, user.getAuthUserId(),
|
||||
new AuthAccountClient.StatusRequest(oldAccount.status())));
|
||||
|
||||
@@ -33,7 +33,7 @@ public class RbacService {
|
||||
@Resource private RbacRepository repository;
|
||||
@Resource private MemberRepository memberRepository;
|
||||
@Resource private AuthAccountClient authClient;
|
||||
@Resource private SecurityEventOutboxService securityEvents;
|
||||
@Resource private SecurityVersionService securityVersions;
|
||||
@Value("${baiyue.security.internal-service-token}")
|
||||
private String internalToken;
|
||||
|
||||
@@ -186,6 +186,7 @@ public class RbacService {
|
||||
affectedAccounts.addAll(repository.findAssignedAuthUserIds(tenantId, roleId));
|
||||
}
|
||||
revokeSessions(List.copyOf(affectedAccounts));
|
||||
affectedAccounts.forEach(id -> securityVersions.subjectChanged(id));
|
||||
return permission;
|
||||
}
|
||||
|
||||
@@ -240,7 +241,7 @@ public class RbacService {
|
||||
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");
|
||||
securityVersions.subjectChanged(user.getAuthUserId());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -260,12 +261,13 @@ public class RbacService {
|
||||
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"));
|
||||
affected.forEach(userId -> securityVersions.subjectChanged(userId));
|
||||
}
|
||||
|
||||
private void revokeAssignedSessions(long tenantId, Long roleId) {
|
||||
revokeSessions(repository.findAssignedAuthUserIds(tenantId, roleId));
|
||||
List<Long> affected = repository.findAssignedAuthUserIds(tenantId, roleId);
|
||||
revokeSessions(affected);
|
||||
affected.forEach(id -> securityVersions.subjectChanged(id));
|
||||
}
|
||||
|
||||
private void revokeSessions(List<Long> accountIds) {
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
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;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/** IAM 安全事件至少一次投递器,支持过期租约回收、退避重试和死信。 */
|
||||
@Component
|
||||
@Slf4j
|
||||
public class SecurityEventOutboxRelay {
|
||||
@Resource private JdbcTemplate jdbc;
|
||||
@Resource private ObjectMapper objectMapper;
|
||||
@Resource private SecurityEventBridgeClient client;
|
||||
@Value("${baiyue.security.internal-service-token}") private String internalToken;
|
||||
@Value("${baiyue.security-event.max-attempts:12}") private int maxAttempts;
|
||||
private final String workerId = UUID.randomUUID().toString();
|
||||
|
||||
@Scheduled(fixedDelayString = "${baiyue.security-event.fixed-delay-ms:5000}")
|
||||
public void relay() {
|
||||
recoverExpiredLeases();
|
||||
for (String eventId : candidates()) deliver(eventId);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void deliver(String eventId) {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
int claimed = jdbc.update("UPDATE iam_security_event_outbox SET status='SENDING',lease_owner=?,"
|
||||
+ "lease_until=?,updated_at=? WHERE event_id=? AND status='PENDING' AND next_retry_at<=?",
|
||||
workerId, now.plusMinutes(2), now, eventId, now);
|
||||
if (claimed != 1) return;
|
||||
String json = jdbc.queryForObject("SELECT payload_json FROM iam_security_event_outbox "
|
||||
+ "WHERE event_id=? AND status='SENDING' AND lease_owner=?", String.class, eventId, workerId);
|
||||
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())) {
|
||||
throw new IllegalStateException("事件桥未确认 IAM 安全事件");
|
||||
}
|
||||
jdbc.update("UPDATE iam_security_event_outbox SET status='SENT',sent_at=?,lease_owner=NULL,"
|
||||
+ "lease_until=NULL,last_error=NULL,updated_at=? WHERE event_id=? AND lease_owner=?",
|
||||
LocalDateTime.now(), LocalDateTime.now(), eventId, workerId);
|
||||
} catch (Exception exception) {
|
||||
fail(eventId, exception.getMessage());
|
||||
log.warn("IAM 安全事件投递失败,eventId={}, reason={}", eventId, exception.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private List<String> candidates() {
|
||||
return jdbc.queryForList("SELECT event_id FROM iam_security_event_outbox WHERE status='PENDING' "
|
||||
+ "AND next_retry_at<=NOW(3) ORDER BY created_at,event_id LIMIT 50", String.class);
|
||||
}
|
||||
|
||||
private void recoverExpiredLeases() {
|
||||
jdbc.update("UPDATE iam_security_event_outbox SET status='PENDING',lease_owner=NULL,lease_until=NULL,"
|
||||
+ "next_retry_at=NOW(3),updated_at=NOW(3) WHERE status='SENDING' AND lease_until<NOW(3)");
|
||||
}
|
||||
|
||||
private void fail(String eventId, String message) {
|
||||
Integer attempts = jdbc.queryForObject("SELECT attempt_count FROM iam_security_event_outbox WHERE event_id=?",
|
||||
Integer.class, eventId);
|
||||
int next = (attempts == null ? 0 : attempts) + 1;
|
||||
String status = next >= maxAttempts ? "DEAD" : "PENDING";
|
||||
long delay = Math.min(1800, 5L << Math.min(next, 8));
|
||||
jdbc.update("UPDATE iam_security_event_outbox SET status=?,attempt_count=?,next_retry_at=?,"
|
||||
+ "lease_owner=NULL,lease_until=NULL,last_error=?,updated_at=? WHERE event_id=? AND lease_owner=?",
|
||||
status, next, LocalDateTime.now().plusSeconds(delay), truncate(message), LocalDateTime.now(), eventId, workerId);
|
||||
}
|
||||
|
||||
private String truncate(String value) {
|
||||
String text = value == null ? "unknown" : value.replaceAll("[\\r\\n\\t]+", " ");
|
||||
return text.length() > 1000 ? text.substring(0, 1000) : text;
|
||||
}
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
package com.baiyue.iam.service;
|
||||
|
||||
import com.baiyue.common.event.EventEnvelope;
|
||||
import com.baiyue.common.event.EventIdentifiers;
|
||||
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.mapper.AuthorizationQueryMapper;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/** 在 IAM 状态事务内递增安全版本并写入可靠事件发送箱。 */
|
||||
@Service
|
||||
public class SecurityEventOutboxService {
|
||||
private static final List<String> OVERSEAS_AUTHORITY_PREFIXES = List.of(
|
||||
"platform:overseas-warehouse:", "platform:overseas-business:",
|
||||
"platform:overseas-master-data:", "merchant:warehouse:", "merchant:overseas-warehouse:",
|
||||
"merchant:cross-border:", "platform:finance:", "lastleg:");
|
||||
@Resource private JdbcTemplate jdbc;
|
||||
@Resource private ObjectMapper objectMapper;
|
||||
@Resource private AuthorizationQueryMapper authorizationQueries;
|
||||
|
||||
public void userStatus(long authUserId, long tenantId, String appCode, String status) {
|
||||
long version = bump(authUserId);
|
||||
write(EventTypes.IAM_USER_STATUS_CHANGED, "USER", String.valueOf(authUserId), tenantId, version,
|
||||
new IamSecurityEventsV1.UserStatusChanged(authUserId, tenantId, appCode, status, version, Instant.now()));
|
||||
}
|
||||
|
||||
public void tenantStatus(long tenantId, String status) {
|
||||
jdbc.update("INSERT INTO iam_security_tenant_version(tenant_id,status_version) VALUES (?,1) "
|
||||
+ "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, securityStatus, Instant.now()));
|
||||
}
|
||||
|
||||
public void permissionChanged(long authUserId, long tenantId, String appCode, String reason) {
|
||||
long version = bump(authUserId);
|
||||
Set<String> roles = Set.copyOf(authorizationQueries.findRolesByIdentity(authUserId, tenantId, appCode));
|
||||
Set<String> authorities = authorizationQueries.findPermissionsByIdentity(authUserId, tenantId, appCode)
|
||||
.stream().filter(permission -> permission != null && OVERSEAS_AUTHORITY_PREFIXES.stream()
|
||||
.anyMatch(permission::startsWith)).collect(Collectors.toUnmodifiableSet());
|
||||
write(EventTypes.IAM_PERMISSION_VERSION_CHANGED, "AUTHORIZATION", String.valueOf(authUserId), tenantId, version,
|
||||
new IamSecurityEventsV1.PermissionVersionChanged(authUserId, tenantId, appCode, version, reason,
|
||||
Instant.now(), roles, authorities));
|
||||
}
|
||||
|
||||
private long bump(long authUserId) {
|
||||
jdbc.update("INSERT INTO iam_security_subject_version(auth_user_id,permission_version) VALUES (?,1) "
|
||||
+ "ON DUPLICATE KEY UPDATE permission_version=permission_version+1", authUserId);
|
||||
return jdbc.queryForObject("SELECT permission_version FROM iam_security_subject_version WHERE auth_user_id=?",
|
||||
Long.class, authUserId);
|
||||
}
|
||||
|
||||
private void write(String type, String aggregateType, String aggregateId, long tenantId,
|
||||
long aggregateVersion, Object value) {
|
||||
JsonNode payload = objectMapper.valueToTree(value);
|
||||
String hash = EventPayloadHash.sha256(EventPayloadCanonicalizer.canonicalize(payload));
|
||||
String flowId = EventIdentifiers.newUuid();
|
||||
EventEnvelope envelope = new EventEnvelope(EventIdentifiers.newUuid(), type, IamSecurityEventsV1.VERSION,
|
||||
Instant.now(), RegionCode.CN, RegionCode.OVERSEAS, String.valueOf(tenantId), aggregateType,
|
||||
aggregateId, aggregateVersion, null, flowId, flowId, null, payload, hash).requireValid();
|
||||
try {
|
||||
jdbc.update("INSERT INTO iam_security_event_outbox(event_id,event_type,aggregate_type,aggregate_id,payload_json) "
|
||||
+ "VALUES (?,?,?,?,CAST(? AS JSON))", envelope.eventId(), type, aggregateType, aggregateId,
|
||||
objectMapper.writeValueAsString(envelope));
|
||||
} catch (Exception exception) {
|
||||
throw new IllegalStateException("IAM 安全事件写入失败", exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.baiyue.iam.service;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Propagation;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
/** Durable security epochs commit or roll back with their IAM mutation, with no projection relay. */
|
||||
@Service
|
||||
@Transactional(propagation = Propagation.MANDATORY)
|
||||
public class SecurityVersionService {
|
||||
@Resource private JdbcTemplate jdbc;
|
||||
|
||||
public void subjectChanged(long authUserId) {
|
||||
jdbc.update("INSERT INTO iam_security_subject_version(auth_user_id,permission_version) VALUES (?,1) "
|
||||
+ "ON DUPLICATE KEY UPDATE permission_version=permission_version+1", authUserId);
|
||||
}
|
||||
|
||||
public void tenantChanged(long tenantId) {
|
||||
jdbc.update("INSERT INTO iam_security_tenant_version(tenant_id,status_version) VALUES (?,1) "
|
||||
+ "ON DUPLICATE KEY UPDATE status_version=status_version+1", tenantId);
|
||||
}
|
||||
}
|
||||
+6
-8
@@ -44,7 +44,7 @@ public class TenantAdministratorProvisioningService implements TenantApprovalIni
|
||||
@Resource
|
||||
private AuthAccountClient authClient;
|
||||
@Resource
|
||||
private SecurityEventOutboxService securityEvents;
|
||||
private SecurityVersionService securityVersions;
|
||||
@Value("${baiyue.security.internal-service-token}")
|
||||
private String internalToken;
|
||||
private SecureRandom random = new SecureRandom();
|
||||
@@ -115,7 +115,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());
|
||||
initializeSecurityVersion(authUserId, tenant.getId());
|
||||
return result;
|
||||
} catch (RuntimeException localFailure) {
|
||||
compensate(context, result);
|
||||
@@ -156,7 +156,7 @@ public class TenantAdministratorProvisioningService implements TenantApprovalIni
|
||||
String password = temporaryPassword();
|
||||
String username = createAuthAccount(user.getAuthUserId(), password, tenant, owner.getId(),
|
||||
user.getPhone(), user.getDisplayName());
|
||||
publishInitialSecurityProjection(user.getAuthUserId(), tenant.getId());
|
||||
initializeSecurityVersion(user.getAuthUserId(), tenant.getId());
|
||||
return ProvisioningResult.created(user.getAuthUserId(), username, password);
|
||||
}
|
||||
if (!"ACTIVE".equals(account.status())
|
||||
@@ -171,8 +171,7 @@ 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");
|
||||
securityVersions.subjectChanged(user.getAuthUserId());
|
||||
}
|
||||
return ProvisioningResult.existing();
|
||||
}
|
||||
@@ -232,9 +231,8 @@ 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 initializeSecurityVersion(long authUserId, long tenantId) {
|
||||
securityVersions.subjectChanged(authUserId);
|
||||
}
|
||||
|
||||
private void compensate(long authUserId) {
|
||||
|
||||
@@ -76,7 +76,7 @@ public class TenantService {
|
||||
@Resource
|
||||
private TenantAdministratorProvisioningService administratorProvisioning;
|
||||
@Resource
|
||||
private SecurityEventOutboxService securityEvents;
|
||||
private SecurityVersionService securityVersions;
|
||||
|
||||
@Transactional
|
||||
@BusinessAudit(event = "PLATFORM_CUSTOMER_OWNER_PASSWORD_RESET", resourceType = "TENANT",
|
||||
@@ -194,7 +194,7 @@ public class TenantService {
|
||||
tenant.setCreatedBy(operatorId);
|
||||
tenant.setUpdatedBy(operatorId);
|
||||
repository.insertTenant(tenant);
|
||||
securityEvents.tenantStatus(tenant.getId(), tenant.getStatus());
|
||||
securityVersions.tenantChanged(tenant.getId());
|
||||
dimensionRuleInitialization.initialize(tenant.getId(), operatorId);
|
||||
overseasStorageFeeInitialization.initialize(tenant.getId(), operatorId, tenant.getTenantName());
|
||||
var administrator = administratorProvisioning.provisionPlatformCreatedTenant(tenant, operatorId);
|
||||
@@ -302,7 +302,7 @@ public class TenantService {
|
||||
tenant.setApprovedAt(approve ? LocalDateTime.now() : null);
|
||||
tenant.setApprovedBy(approve ? reviewerId : null);
|
||||
repository.saveTenant(tenant);
|
||||
securityEvents.tenantStatus(tenant.getId(), tenant.getStatus());
|
||||
securityVersions.tenantChanged(tenant.getId());
|
||||
for (var qualification : profileRepository.findQualifications(tenant.getId())) {
|
||||
if ("PENDING".equals(qualification.getStatus())) {
|
||||
qualification.setStatus(approve ? "APPROVED" : "REJECTED");
|
||||
@@ -424,7 +424,7 @@ public class TenantService {
|
||||
tenant.setStatus(request.status());
|
||||
repository.saveTenant(tenant);
|
||||
revokeMerchantSessions(tenantId);
|
||||
securityEvents.tenantStatus(tenantId, request.status());
|
||||
securityVersions.tenantChanged(tenantId);
|
||||
return response(tenant);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
spring:
|
||||
datasource:
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
url: ${IAM_DB_URL:jdbc:mysql://127.0.0.1:3306/baiyue_iam?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true}
|
||||
username: ${IAM_DB_USERNAME:root}
|
||||
password: ${IAM_DB_PASSWORD:123456}
|
||||
@@ -1,15 +0,0 @@
|
||||
spring:
|
||||
cloud:
|
||||
nacos:
|
||||
config:
|
||||
namespace: ${NACOS_NAMESPACE:test}
|
||||
group: ${NACOS_GROUP:BAIYUE}
|
||||
encode: UTF-8
|
||||
discovery:
|
||||
namespace: ${NACOS_NAMESPACE:test}
|
||||
group: ${NACOS_GROUP:BAIYUE}
|
||||
datasource:
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
url: ${IAM_TEST_DB_URL:jdbc:mysql://${TEST_DB_HOST:8.163.30.128}:${TEST_DB_PORT:3306}/baiyue_iam?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true&rewriteBatchedStatements=true}
|
||||
username: ${IAM_TEST_DB_USERNAME:${TEST_DB_USERNAME:root}}
|
||||
password: ${IAM_TEST_DB_PASSWORD:${TEST_DB_PASSWORD}}
|
||||
@@ -2,29 +2,28 @@ server:
|
||||
port: ${IAM_PORT:9201}
|
||||
spring:
|
||||
profiles:
|
||||
active: ${SPRING_PROFILES_ACTIVE:dev}
|
||||
active: ${SPRING_PROFILES_ACTIVE:test}
|
||||
application:
|
||||
name: iam-service
|
||||
config:
|
||||
import:
|
||||
- optional:nacos:baiyue-common.yml?group=BAIYUE&refreshEnabled=true
|
||||
- optional:nacos:${spring.application.name}.yml?group=BAIYUE&refreshEnabled=true
|
||||
- nacos:baiyue-common.yml?group=${NACOS_GROUP:BAIYUE}&refreshEnabled=true
|
||||
- nacos:${spring.application.name}.yml?group=${NACOS_GROUP:BAIYUE}&refreshEnabled=true
|
||||
cloud:
|
||||
nacos:
|
||||
server-addr: ${NACOS_SERVER_ADDR:127.0.0.1:8848}
|
||||
username: ${NACOS_USERNAME:nacos}
|
||||
password: ${NACOS_PASSWORD:nacos}
|
||||
server-addr: ${NACOS_SERVER_ADDR}
|
||||
username: ${NACOS_USERNAME}
|
||||
password: ${NACOS_PASSWORD}
|
||||
discovery:
|
||||
enabled: ${NACOS_DISCOVERY_ENABLED:true}
|
||||
register-enabled: ${NACOS_REGISTER_ENABLED:true}
|
||||
group: ${NACOS_GROUP:BAIYUE}
|
||||
namespace: ${NACOS_NAMESPACE:public}
|
||||
namespace: ${NACOS_NAMESPACE}
|
||||
config:
|
||||
namespace: ${NACOS_NAMESPACE:public}
|
||||
namespace: ${NACOS_NAMESPACE}
|
||||
encode: UTF-8
|
||||
baiyue:
|
||||
security-event:
|
||||
event-bridge-url: ${SECURITY_EVENT_BRIDGE_URL:http://event-bridge-overseas:9312}
|
||||
customer-dimension-initialization:
|
||||
fixed-delay-ms: ${CUSTOMER_DIMENSION_INITIALIZATION_FIXED_DELAY_MS:5000}
|
||||
max-attempts: ${CUSTOMER_DIMENSION_INITIALIZATION_MAX_ATTEMPTS:12}
|
||||
@@ -36,6 +35,5 @@ baiyue:
|
||||
auth:
|
||||
url: ${AUTH_SERVICE_URL:}
|
||||
security:
|
||||
jwt-secret: ${JWT_SECRET:change-me-to-at-least-32-bytes-long-before-start}
|
||||
internal-service-token: ${INTERNAL_SERVICE_TOKEN:change-me-to-an-internal-token-at-least-32-bytes-long}
|
||||
internal-service-token: ${INTERNAL_SERVICE_TOKEN}
|
||||
management.endpoints.web.exposure.include: health,info,prometheus
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
USE baiyue_iam;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS iam_security_subject_version (
|
||||
auth_user_id BIGINT NOT NULL COMMENT '认证账号ID',
|
||||
permission_version BIGINT NOT NULL DEFAULT 0 COMMENT '权限版本,身份或授权变化时单调递增',
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||
PRIMARY KEY (auth_user_id)
|
||||
) ENGINE=InnoDB COMMENT='安全主体权限版本';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS iam_security_tenant_version (
|
||||
tenant_id BIGINT NOT NULL COMMENT '客户ID',
|
||||
status_version BIGINT NOT NULL DEFAULT 0 COMMENT '客户状态单调版本',
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||
PRIMARY KEY (tenant_id)
|
||||
) ENGINE=InnoDB COMMENT='客户状态事件版本';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS iam_security_event_outbox (
|
||||
event_id VARCHAR(64) NOT NULL COMMENT '全局事件ID,消费者幂等键',
|
||||
event_type VARCHAR(64) NOT NULL COMMENT '安全事件类型',
|
||||
aggregate_type VARCHAR(32) NOT NULL COMMENT 'USER、TENANT或AUTHORIZATION',
|
||||
aggregate_id VARCHAR(64) NOT NULL COMMENT '聚合标识',
|
||||
payload_json JSON NOT NULL COMMENT '标准跨区事件信封,不含密码、令牌和权限全集',
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'PENDING' COMMENT '发送状态:PENDING、SENDING、SENT、DEAD',
|
||||
attempt_count INT NOT NULL DEFAULT 0 COMMENT '发送尝试次数',
|
||||
next_retry_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '下次重试时间',
|
||||
lease_owner VARCHAR(128) NULL COMMENT '租约持有者',
|
||||
lease_until DATETIME(3) NULL COMMENT '租约到期时间',
|
||||
last_error VARCHAR(1000) NULL COMMENT '最近一次错误',
|
||||
sent_at DATETIME(3) NULL COMMENT '发送成功时间',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||
PRIMARY KEY (event_id),
|
||||
KEY idx_security_outbox_publish (status,next_retry_at,lease_until,created_at),
|
||||
KEY idx_security_outbox_aggregate (aggregate_type,aggregate_id,created_at)
|
||||
) ENGINE=InnoDB COMMENT='IAM可靠安全事件本地发送箱';
|
||||
@@ -0,0 +1,16 @@
|
||||
USE baiyue_iam;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS iam_security_subject_version (
|
||||
auth_user_id BIGINT NOT NULL COMMENT '认证账号ID',
|
||||
permission_version BIGINT NOT NULL DEFAULT 0 COMMENT '权限版本,身份或授权变化时单调递增',
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||
PRIMARY KEY (auth_user_id)
|
||||
) ENGINE=InnoDB COMMENT='安全主体权限版本';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS iam_security_tenant_version (
|
||||
tenant_id BIGINT NOT NULL COMMENT '客户ID',
|
||||
status_version BIGINT NOT NULL DEFAULT 0 COMMENT '客户状态单调版本',
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||
PRIMARY KEY (tenant_id)
|
||||
) ENGINE=InnoDB COMMENT='客户状态持久化版本';
|
||||
|
||||
@@ -869,6 +869,37 @@ paths:
|
||||
'400': {$ref: '#/components/responses/BadRequest'}
|
||||
'403': {$ref: '#/components/responses/Forbidden'}
|
||||
'409': {$ref: '#/components/responses/Conflict'}
|
||||
/internal/iam/authorization-snapshot:
|
||||
post:
|
||||
tags: [IAM Resources / IAM 资源]
|
||||
summary: Read a consistent authoritative authorization snapshot
|
||||
description: Internal service credential required; no public Gateway route. Identity, permissions, warehouse bindings and monotonic subject/customer versions are read by one MVCC SELECT statement, without cross-statement snapshot mixing. Auth fingerprints this state for login, refresh and every Access validation.
|
||||
security: []
|
||||
parameters:
|
||||
- {$ref: '#/components/parameters/InternalServiceToken'}
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [userId, tenantId, membershipId, appCode]
|
||||
properties:
|
||||
userId: {type: integer, format: int64}
|
||||
tenantId: {type: integer, format: int64}
|
||||
membershipId: {type: integer, format: int64}
|
||||
appCode: {type: string, enum: [PLATFORM, MERCHANT]}
|
||||
responses:
|
||||
'200':
|
||||
description: Consistent snapshot or business error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
allOf:
|
||||
- {$ref: '#/components/schemas/ApiResponse'}
|
||||
- type: object
|
||||
properties:
|
||||
data: {$ref: '#/components/schemas/AuthorizationSnapshot'}
|
||||
/internal/iam/merchant-registrations/{authUserId}:
|
||||
get:
|
||||
tags: [IAM Resources / IAM 资源]
|
||||
@@ -1747,3 +1778,18 @@ components:
|
||||
properties:
|
||||
roleId: {type: integer, format: int64}
|
||||
permissionIds: {type: array, items: {type: integer, format: int64}}
|
||||
AuthorizationSnapshot:
|
||||
type: object
|
||||
required: [userId, tenantId, membershipId, appCode, clientType, roles, permissions, permissionVersion, tenantVersion]
|
||||
properties:
|
||||
userId: {type: integer, format: int64}
|
||||
tenantId: {type: integer, format: int64}
|
||||
membershipId: {type: integer, format: int64}
|
||||
appCode: {type: string, enum: [PLATFORM, MERCHANT]}
|
||||
clientType: {type: string}
|
||||
roles: {type: array, items: {type: string}}
|
||||
permissions: {type: array, items: {type: string}}
|
||||
permissionVersion: {type: integer, format: int64, description: Persistent subject security epoch}
|
||||
tenantVersion: {type: integer, format: int64, description: Persistent customer status epoch}
|
||||
overseasWarehouseId: {type: integer, format: int64, nullable: true}
|
||||
domesticWarehouseId: {type: integer, format: int64, nullable: true}
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
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}");
|
||||
}
|
||||
}
|
||||
@@ -12,26 +12,32 @@ class AuthorizationIdentitySqlTest {
|
||||
void disabledArchivedDeletedOrMissingCustomerCannotGetNewAuthorizationSnapshot() throws Exception {
|
||||
var jdbc = new JdbcTemplate(new DriverManagerDataSource("jdbc:h2:mem:" + UUID.randomUUID() + ";DB_CLOSE_DELAY=-1", "sa", ""));
|
||||
jdbc.execute("CREATE TABLE iam_user(id BIGINT, auth_user_id BIGINT, status VARCHAR(30), deleted INT)");
|
||||
jdbc.execute("CREATE TABLE iam_membership(id BIGINT, user_id BIGINT, tenant_id BIGINT, status VARCHAR(30), deleted INT)");
|
||||
jdbc.execute("CREATE TABLE iam_membership(id BIGINT, user_id BIGINT, tenant_id BIGINT, status VARCHAR(30), deleted INT, overseas_warehouse_id BIGINT, domestic_warehouse_id BIGINT)");
|
||||
jdbc.execute("CREATE TABLE iam_tenant(id BIGINT, status VARCHAR(30), deleted INT)");
|
||||
jdbc.execute("CREATE TABLE iam_membership_role(membership_id BIGINT,tenant_id BIGINT,role_id BIGINT)");
|
||||
jdbc.execute("CREATE TABLE iam_role(id BIGINT,tenant_id BIGINT,app_code VARCHAR(20),role_code VARCHAR(20),status VARCHAR(20),deleted INT)");
|
||||
jdbc.execute("CREATE TABLE iam_role_permission(role_id BIGINT,permission_id BIGINT)");
|
||||
jdbc.execute("CREATE TABLE iam_permission(id BIGINT,tenant_id BIGINT,app_code VARCHAR(20),permission_code VARCHAR(20),status VARCHAR(20),deleted INT)");
|
||||
jdbc.execute("CREATE TABLE iam_security_subject_version(auth_user_id BIGINT,permission_version BIGINT)");
|
||||
jdbc.execute("CREATE TABLE iam_security_tenant_version(tenant_id BIGINT,status_version BIGINT)");
|
||||
jdbc.update("INSERT INTO iam_user VALUES(1,42,'ACTIVE',0)");
|
||||
jdbc.update("INSERT INTO iam_membership VALUES(9,1,7,'ACTIVE',0)");
|
||||
jdbc.update("INSERT INTO iam_membership VALUES(9,1,7,'ACTIVE',0,NULL,NULL)");
|
||||
jdbc.update("INSERT INTO iam_tenant VALUES(7,'ACTIVE',0)");
|
||||
String sql = String.join("\n", AuthorizationQueryMapper.class.getMethod("countActiveIdentity", long.class, long.class, long.class)
|
||||
.getAnnotation(Select.class).value()).replace("#{userId}", "42").replace("#{membershipId}", "9").replace("#{tenantId}", "7");
|
||||
String sql = String.join("\n", AuthorizationQueryMapper.class.getMethod("findSnapshot", long.class, long.class, long.class, String.class)
|
||||
.getAnnotation(Select.class).value()).replace("#{userId}", "42").replace("#{membershipId}", "9").replace("#{tenantId}", "7").replace("#{appCode}", "'MERCHANT'");
|
||||
for (String status : new String[]{"ACTIVE", "PROFILE_REQUIRED", "PENDING", "REJECTED"}) {
|
||||
jdbc.update("UPDATE iam_tenant SET status=?", status);
|
||||
assertThat(jdbc.queryForObject(sql, Integer.class)).as(status).isEqualTo(1);
|
||||
assertThat(jdbc.queryForList(sql).size()).as(status).isEqualTo(1);
|
||||
}
|
||||
for (String status : new String[]{"DISABLED", "ARCHIVED", "UNKNOWN"}) {
|
||||
jdbc.update("UPDATE iam_tenant SET status=?", status);
|
||||
assertThat(jdbc.queryForObject(sql, Integer.class)).as(status).isZero();
|
||||
assertThat(jdbc.queryForList(sql).size()).as(status).isZero();
|
||||
}
|
||||
jdbc.update("UPDATE iam_tenant SET status='ACTIVE', deleted=1");
|
||||
assertThat(jdbc.queryForObject(sql, Integer.class)).isZero();
|
||||
assertThat(jdbc.queryForList(sql).size()).isZero();
|
||||
jdbc.update("DELETE FROM iam_tenant");
|
||||
assertThat(jdbc.queryForObject(sql, Integer.class)).isZero();
|
||||
assertThat(jdbc.queryForList(sql).size()).isZero();
|
||||
jdbc.update("UPDATE iam_membership SET tenant_id=0");
|
||||
assertThat(jdbc.queryForObject(sql.replace("m.tenant_id = 7", "m.tenant_id = 0"), Integer.class)).isEqualTo(1);
|
||||
assertThat(jdbc.queryForList(sql.replace("m.tenant_id=7", "m.tenant_id=0")).size()).isEqualTo(1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
package com.baiyue.iam.repository;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class SecurityEventSqlContractTest {
|
||||
@Test
|
||||
void securityOutboxHasVersionsAndReliableDeliveryColumns() throws Exception {
|
||||
String sql = new ClassPathResource("db/security-event-outbox.sql")
|
||||
.getContentAsString(StandardCharsets.UTF_8);
|
||||
assertThat(sql)
|
||||
.contains("iam_security_subject_version")
|
||||
.contains("permission_version BIGINT NOT NULL DEFAULT 0")
|
||||
.contains("iam_security_tenant_version")
|
||||
.contains("status_version BIGINT NOT NULL DEFAULT 0")
|
||||
.contains("iam_security_event_outbox")
|
||||
.contains("payload_json JSON NOT NULL")
|
||||
.contains("idx_security_outbox_publish")
|
||||
.contains("PENDING", "SENDING", "SENT", "DEAD");
|
||||
}
|
||||
|
||||
@Test
|
||||
void cleanInitializationIncludesSecurityEventSchema() throws Exception {
|
||||
Path direct = Path.of("deploy", "sql", "头程数据库统一增量-MySQL8.sql");
|
||||
Path migration = Files.exists(direct)
|
||||
? direct : Path.of("..", "deploy", "sql", "头程数据库统一增量-MySQL8.sql").normalize();
|
||||
String sql = Files.readString(migration, StandardCharsets.UTF_8);
|
||||
assertThat(sql)
|
||||
.contains("CREATE TABLE iam_security_subject_version")
|
||||
.contains("CREATE TABLE iam_security_tenant_version")
|
||||
.contains("CREATE TABLE iam_security_event_outbox")
|
||||
.contains("idx_security_outbox_publish", "idx_security_outbox_aggregate")
|
||||
.contains("SELECT 'PASS' AS initialization_status")
|
||||
.doesNotContain("assert_iam_security_event_schema", "CREATE TABLE IF NOT EXISTS iam_security_event");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.baiyue.iam.repository;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class SecurityVersionSqlContractTest {
|
||||
@Test void versionsRemainDurableWithoutProjectionOutbox() throws Exception {
|
||||
String sql = new ClassPathResource("db/security-version.sql").getContentAsString(StandardCharsets.UTF_8);
|
||||
assertThat(sql).contains("iam_security_subject_version", "permission_version BIGINT NOT NULL DEFAULT 0",
|
||||
"iam_security_tenant_version", "status_version BIGINT NOT NULL DEFAULT 0")
|
||||
.doesNotContain("iam_security_event_outbox");
|
||||
}
|
||||
|
||||
@Test void cleanInitializationIncludesVersionsWithoutRetiredProjectionTables() throws Exception {
|
||||
Path direct = Path.of("deploy", "sql", "头程数据库统一增量-MySQL8.sql");
|
||||
Path baseline = Files.exists(direct) ? direct : Path.of("..", "deploy", "sql", "头程数据库统一增量-MySQL8.sql");
|
||||
assertThat(Files.readString(baseline, StandardCharsets.UTF_8))
|
||||
.contains("CREATE TABLE iam_security_subject_version", "CREATE TABLE iam_security_tenant_version",
|
||||
"SELECT 'PASS' AS initialization_status")
|
||||
.doesNotContain("CREATE TABLE iam_security_event_outbox", "CREATE TABLE ll_security_event_inbox", "CREATE TABLE ll_security_projection");
|
||||
}
|
||||
}
|
||||
@@ -24,12 +24,7 @@ class AuthorizationSnapshotServiceTest {
|
||||
@Resource private AuthorizationSnapshotService service;
|
||||
@Test
|
||||
void loadsRolesAndPermissionsForActiveMerchantIdentity() {
|
||||
when(mapper.countActiveIdentity(42L, 7L, 9L)).thenReturn(1);
|
||||
when(mapper.findRoles(42L, 7L, 9L, "MERCHANT"))
|
||||
.thenReturn(List.of("ADMIN", "OPERATOR"));
|
||||
when(mapper.findPermissions(42L, 7L, 9L, "MERCHANT"))
|
||||
.thenReturn(List.of("order:read", "order:create"));
|
||||
when(mapper.findOverseasWarehouseId(7L, 9L)).thenReturn(900L);
|
||||
when(mapper.findSnapshot(42L, 7L, 9L, "MERCHANT")).thenReturn(rows(List.of("ADMIN", "OPERATOR"), List.of("order:read", "order:create")));
|
||||
|
||||
var result = service
|
||||
.load(new SnapshotRequest(42L, 7L, 9L, "merchant"));
|
||||
@@ -51,36 +46,29 @@ class AuthorizationSnapshotServiceTest {
|
||||
|
||||
@Test
|
||||
void rejectsInactiveMembership() {
|
||||
when(mapper.countActiveIdentity(42L, 7L, 9L)).thenReturn(0);
|
||||
|
||||
assertThatThrownBy(() -> service
|
||||
.load(new SnapshotRequest(42L, 7L, 9L, "MERCHANT")))
|
||||
.isInstanceOfSatisfying(BizException.class,
|
||||
exception -> assertThat(exception.getResultCode()).isEqualTo(ResultCode.FORBIDDEN));
|
||||
verify(mapper).countActiveIdentity(42L, 7L, 9L);
|
||||
when(mapper.findSnapshot(42L, 7L, 9L, "MERCHANT")).thenReturn(List.of());
|
||||
assertThatThrownBy(() -> service.load(new SnapshotRequest(42L, 7L, 9L, "MERCHANT")))
|
||||
.isInstanceOfSatisfying(BizException.class,
|
||||
exception -> assertThat(exception.getResultCode()).isEqualTo(ResultCode.FORBIDDEN));
|
||||
verify(mapper).findSnapshot(42L, 7L, 9L, "MERCHANT");
|
||||
}
|
||||
|
||||
@Test
|
||||
void loadsCurrentPlatformPermissionsWithoutRoleManagementPermission() {
|
||||
when(mapper.countActiveIdentity(42L, 0L, 9L)).thenReturn(1);
|
||||
when(mapper.findRoles(42L, 0L, 9L, "PLATFORM")).thenReturn(List.of("ORDER_OPERATOR"));
|
||||
when(mapper.findPermissions(42L, 0L, 9L, "PLATFORM"))
|
||||
.thenReturn(List.of("platform:order:list"));
|
||||
when(mapper.findSnapshot(42L, 0L, 9L, "PLATFORM")).thenReturn(rows(List.of("ORDER_OPERATOR"), List.of("platform:order:list")));
|
||||
|
||||
var result = service.currentPermissions(new AuthenticatedUser(42L, 0L, 0L, 9L,
|
||||
"operator", "PLATFORM", "PLATFORM", java.util.Set.of()), "PLATFORM");
|
||||
|
||||
assertThat(result).containsExactly("platform:order:list");
|
||||
verify(mapper).findPermissions(42L, 0L, 9L, "PLATFORM");
|
||||
verify(mapper).findSnapshot(42L, 0L, 9L, "PLATFORM");
|
||||
}
|
||||
|
||||
@Test
|
||||
void exposesPhase5DeliveryPermissionsFromCurrentRoleAssignments() {
|
||||
when(mapper.countActiveIdentity(42L, 7L, 9L)).thenReturn(1);
|
||||
when(mapper.findRoles(42L, 7L, 9L, "MERCHANT")).thenReturn(List.of("MERCHANT_OWNER"));
|
||||
when(mapper.findPermissions(42L, 7L, 9L, "MERCHANT")).thenReturn(List.of(
|
||||
when(mapper.findSnapshot(42L, 7L, 9L, "MERCHANT")).thenReturn(rows(List.of("MERCHANT_OWNER"), List.of(
|
||||
"merchant:warehouse:delivery:list",
|
||||
"merchant:warehouse:delivery:create"));
|
||||
"merchant:warehouse:delivery:create")));
|
||||
|
||||
var result = service.currentPermissions(new AuthenticatedUser(42L, 7L, 7L, 9L,
|
||||
"owner", "MERCHANT", "MERCHANT", java.util.Set.of()), "MERCHANT");
|
||||
@@ -92,14 +80,12 @@ class AuthorizationSnapshotServiceTest {
|
||||
|
||||
@Test
|
||||
void exposesCustomerOverseasBusinessPermissionsFromCurrentRoleAssignments() {
|
||||
when(mapper.countActiveIdentity(42L, 7L, 9L)).thenReturn(1);
|
||||
when(mapper.findRoles(42L, 7L, 9L, "MERCHANT")).thenReturn(List.of("MERCHANT_OWNER"));
|
||||
when(mapper.findPermissions(42L, 7L, 9L, "MERCHANT")).thenReturn(List.of(
|
||||
when(mapper.findSnapshot(42L, 7L, 9L, "MERCHANT")).thenReturn(rows(List.of("MERCHANT_OWNER"), List.of(
|
||||
"merchant:cross-border:procurement:list",
|
||||
"merchant:cross-border:procurement:create",
|
||||
"merchant:cross-border:domestic-trucking:list",
|
||||
"merchant:cross-border:pallet-shipping:list",
|
||||
"merchant:warehouse:inbound:list"));
|
||||
"merchant:warehouse:inbound:list")));
|
||||
|
||||
var result = service.currentPermissions(new AuthenticatedUser(42L, 7L, 7L, 9L,
|
||||
"owner", "MERCHANT", "MERCHANT", java.util.Set.of()), "MERCHANT");
|
||||
@@ -114,11 +100,9 @@ class AuthorizationSnapshotServiceTest {
|
||||
|
||||
@Test
|
||||
void exposesWalletPermissionsFromCurrentRoleAssignments() {
|
||||
when(mapper.countActiveIdentity(42L, 7L, 9L)).thenReturn(1);
|
||||
when(mapper.findRoles(42L, 7L, 9L, "MERCHANT")).thenReturn(List.of("MERCHANT_OWNER"));
|
||||
when(mapper.findPermissions(42L, 7L, 9L, "MERCHANT")).thenReturn(List.of(
|
||||
when(mapper.findSnapshot(42L, 7L, 9L, "MERCHANT")).thenReturn(rows(List.of("MERCHANT_OWNER"), List.of(
|
||||
"merchant:finance:account:view",
|
||||
"merchant:finance:recharge:create"));
|
||||
"merchant:finance:recharge:create")));
|
||||
|
||||
var merchantPermissions = service.currentPermissions(new AuthenticatedUser(42L, 7L, 7L, 9L,
|
||||
"owner", "MERCHANT", "MERCHANT", java.util.Set.of()), "MERCHANT");
|
||||
@@ -127,11 +111,9 @@ class AuthorizationSnapshotServiceTest {
|
||||
"merchant:finance:account:view",
|
||||
"merchant:finance:recharge:create");
|
||||
|
||||
when(mapper.countActiveIdentity(51L, 0L, 19L)).thenReturn(1);
|
||||
when(mapper.findRoles(51L, 0L, 19L, "PLATFORM")).thenReturn(List.of("PLATFORM_ADMIN"));
|
||||
when(mapper.findPermissions(51L, 0L, 19L, "PLATFORM")).thenReturn(List.of(
|
||||
when(mapper.findSnapshot(51L, 0L, 19L, "PLATFORM")).thenReturn(rows(List.of("PLATFORM_ADMIN"), List.of(
|
||||
"platform:finance:recharge:review",
|
||||
"platform:finance:customer-account:adjust"));
|
||||
"platform:finance:customer-account:adjust")));
|
||||
|
||||
var platformPermissions = service.currentPermissions(new AuthenticatedUser(51L, 0L, 0L, 19L,
|
||||
"platform-admin", "PLATFORM", "PLATFORM", java.util.Set.of()), "PLATFORM");
|
||||
@@ -143,11 +125,9 @@ class AuthorizationSnapshotServiceTest {
|
||||
|
||||
@Test
|
||||
void exposesInboundHoldIndependentlyFromReadPermissions() {
|
||||
when(mapper.countActiveIdentity(51L, 0L, 19L)).thenReturn(1);
|
||||
when(mapper.findRoles(51L, 0L, 19L, "PLATFORM")).thenReturn(List.of("PLATFORM_ADMIN"));
|
||||
when(mapper.findPermissions(51L, 0L, 19L, "PLATFORM")).thenReturn(List.of(
|
||||
when(mapper.findSnapshot(51L, 0L, 19L, "PLATFORM")).thenReturn(rows(List.of("PLATFORM_ADMIN"), List.of(
|
||||
"platform:firstleg:inbound:list",
|
||||
"platform:firstleg:inbound:hold"));
|
||||
"platform:firstleg:inbound:hold")));
|
||||
|
||||
var permissions = service.currentPermissions(new AuthenticatedUser(51L, 0L, 0L, 19L,
|
||||
"platform-admin", "PLATFORM", "PLATFORM", java.util.Set.of()), "PLATFORM");
|
||||
@@ -168,9 +148,7 @@ class AuthorizationSnapshotServiceTest {
|
||||
|
||||
@Test
|
||||
void shouldHandleCase01() {
|
||||
when(mapper.countActiveIdentity(42L, 0L, 9L)).thenReturn(1);
|
||||
when(mapper.findRoles(42L, 0L, 9L, "PLATFORM")).thenReturn(null);
|
||||
when(mapper.findPermissions(42L, 0L, 9L, "PLATFORM")).thenReturn(null);
|
||||
when(mapper.findSnapshot(42L, 0L, 9L, "PLATFORM")).thenReturn(rows(null, null));
|
||||
|
||||
var result = service.currentPermissions(new AuthenticatedUser(42L, 0L, 0L, 9L,
|
||||
"operator", "PLATFORM", "PLATFORM", java.util.Set.of()), "PLATFORM");
|
||||
@@ -180,12 +158,10 @@ class AuthorizationSnapshotServiceTest {
|
||||
|
||||
@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"));
|
||||
when(mapper.findSnapshot(42L, 7L, 9L, "MERCHANT"))
|
||||
.thenReturn(rows(List.of("ORDER_VIEWER"), List.of("merchant:order:list", "merchant:order:view")),
|
||||
rows(List.of(), List.of()),
|
||||
rows(List.of("ORDER_VIEWER"), List.of("merchant:order:list", "merchant:order:view")));
|
||||
SnapshotRequest request = new SnapshotRequest(42L, 7L, 9L, "MERCHANT");
|
||||
|
||||
assertThat(service.load(request).getPermissions())
|
||||
@@ -195,4 +171,16 @@ class AuthorizationSnapshotServiceTest {
|
||||
.containsExactlyInAnyOrder("merchant:order:list", "merchant:order:view");
|
||||
}
|
||||
|
||||
private List<com.baiyue.iam.dto.AuthorizationSnapshotRow> rows(List<String> roles, List<String> permissions) {
|
||||
var result = new java.util.ArrayList<com.baiyue.iam.dto.AuthorizationSnapshotRow>();
|
||||
List<String> roleValues = roles == null || roles.isEmpty() ? java.util.Collections.singletonList(null) : roles;
|
||||
List<String> permissionValues = permissions == null || permissions.isEmpty() ? java.util.Collections.singletonList(null) : permissions;
|
||||
for (String role : roleValues) for (String permission : permissionValues) {
|
||||
var row = new com.baiyue.iam.dto.AuthorizationSnapshotRow();
|
||||
row.setRoleCode(role); row.setPermissionCode(permission);
|
||||
row.setPermissionVersion(1L); row.setTenantVersion(1L); row.setOverseasWarehouseId(900L);
|
||||
result.add(row);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
package com.baiyue.iam.service;
|
||||
|
||||
import com.baiyue.iam.dto.AuthorizationDtos.SnapshotRequest;
|
||||
import com.baiyue.iam.mapper.AuthorizationQueryMapper;
|
||||
import org.h2.jdbcx.JdbcDataSource;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mybatis.spring.SqlSessionFactoryBean;
|
||||
import org.mybatis.spring.SqlSessionTemplate;
|
||||
import org.springframework.aop.framework.ProxyFactory;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.transaction.annotation.AnnotationTransactionAttributeSource;
|
||||
import org.springframework.transaction.interceptor.TransactionInterceptor;
|
||||
import org.springframework.transaction.support.TransactionTemplate;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
/** Production SQL and transaction annotation, with a commit between the atomic SQL read and response assembly. */
|
||||
class AuthorizationSnapshotTransactionTest {
|
||||
@Test void singleStatementSnapshotCannotMixIdentityPermissionsAndVersion() throws Exception {
|
||||
var ds = new JdbcDataSource(); ds.setURL("jdbc:h2:mem:" + UUID.randomUUID() + ";MODE=MySQL;DB_CLOSE_DELAY=-1");
|
||||
var jdbc = new JdbcTemplate(ds);
|
||||
jdbc.execute("CREATE TABLE iam_user(id BIGINT,auth_user_id BIGINT,status VARCHAR(20),deleted INT)");
|
||||
jdbc.execute("CREATE TABLE iam_membership(id BIGINT,user_id BIGINT,tenant_id BIGINT,status VARCHAR(20),deleted INT,overseas_warehouse_id BIGINT,domestic_warehouse_id BIGINT)");
|
||||
jdbc.execute("CREATE TABLE iam_tenant(id BIGINT,status VARCHAR(20),deleted INT)");
|
||||
jdbc.execute("CREATE TABLE iam_role(id BIGINT,tenant_id BIGINT,app_code VARCHAR(20),role_code VARCHAR(20),status VARCHAR(20),deleted INT)");
|
||||
jdbc.execute("CREATE TABLE iam_membership_role(membership_id BIGINT,tenant_id BIGINT,role_id BIGINT)");
|
||||
jdbc.execute("CREATE TABLE iam_permission(id BIGINT,tenant_id BIGINT,app_code VARCHAR(20),permission_code VARCHAR(20),status VARCHAR(20),deleted INT)");
|
||||
jdbc.execute("CREATE TABLE iam_role_permission(role_id BIGINT,permission_id BIGINT)");
|
||||
jdbc.execute("CREATE TABLE iam_security_subject_version(auth_user_id BIGINT PRIMARY KEY,permission_version BIGINT)");
|
||||
jdbc.execute("CREATE TABLE iam_security_tenant_version(tenant_id BIGINT PRIMARY KEY,status_version BIGINT)");
|
||||
jdbc.update("INSERT INTO iam_user VALUES(1,42,'ACTIVE',0)");
|
||||
jdbc.update("INSERT INTO iam_membership VALUES(9,1,7,'ACTIVE',0,100,NULL)");
|
||||
jdbc.update("INSERT INTO iam_tenant VALUES(7,'ACTIVE',0)");
|
||||
jdbc.update("INSERT INTO iam_role VALUES(2,7,'MERCHANT','OPERATOR','ACTIVE',0)");
|
||||
jdbc.update("INSERT INTO iam_membership_role VALUES(9,7,2)");
|
||||
jdbc.update("INSERT INTO iam_permission VALUES(3,7,'MERCHANT','write','ACTIVE',0)");
|
||||
jdbc.update("INSERT INTO iam_role_permission VALUES(2,3)");
|
||||
jdbc.update("INSERT INTO iam_security_subject_version VALUES(42,1)");
|
||||
jdbc.update("INSERT INTO iam_security_tenant_version VALUES(7,1)");
|
||||
var factory = new SqlSessionFactoryBean(); factory.setDataSource(ds);
|
||||
var configuration = new org.apache.ibatis.session.Configuration(); configuration.addMapper(AuthorizationQueryMapper.class);
|
||||
configuration.addMapper(com.baiyue.iam.mapper.IamMembershipRoleMapper.class);
|
||||
factory.setConfiguration(configuration);
|
||||
var mapper = new SqlSessionTemplate(factory.getObject()).getMapper(AuthorizationQueryMapper.class);
|
||||
var read = new CountDownLatch(1); var committed = new CountDownLatch(1); var pause = new AtomicBoolean(true);
|
||||
AuthorizationQueryMapper controlled = (AuthorizationQueryMapper) Proxy.newProxyInstance(getClass().getClassLoader(),
|
||||
new Class<?>[]{AuthorizationQueryMapper.class}, (proxy, method, args) -> {
|
||||
Object result = method.invoke(mapper, args);
|
||||
if (method.getName().equals("findSnapshot") && pause.compareAndSet(true, false)) {
|
||||
read.countDown(); if (!committed.await(5, TimeUnit.SECONDS)) throw new AssertionError("Commit barrier timed out");
|
||||
}
|
||||
return result;
|
||||
});
|
||||
var target = new AuthorizationSnapshotService(); ReflectionTestUtils.setField(target, "mapper", controlled);
|
||||
var tx = new DataSourceTransactionManager(ds); var proxy = new ProxyFactory(target); proxy.setProxyTargetClass(true);
|
||||
proxy.addAdvice(new TransactionInterceptor(tx, new AnnotationTransactionAttributeSource()));
|
||||
var service = (AuthorizationSnapshotService) proxy.getProxy();
|
||||
var executor = Executors.newSingleThreadExecutor();
|
||||
try {
|
||||
var previous = executor.submit(() -> service.load(new SnapshotRequest(42L,7L,9L,"MERCHANT")));
|
||||
assertThat(read.await(5, TimeUnit.SECONDS)).isTrue();
|
||||
new TransactionTemplate(tx).executeWithoutResult(status -> {
|
||||
jdbc.update("UPDATE iam_permission SET permission_code='read' WHERE id=3");
|
||||
jdbc.update("UPDATE iam_security_subject_version SET permission_version=2 WHERE auth_user_id=42");
|
||||
jdbc.update("UPDATE iam_membership SET overseas_warehouse_id=200 WHERE id=9");
|
||||
});
|
||||
committed.countDown();
|
||||
var old = previous.get(5, TimeUnit.SECONDS);
|
||||
assertThat(old.getPermissions()).containsExactly("write");
|
||||
assertThat(old.getPermissionVersion()).isEqualTo(1); assertThat(old.getOverseasWarehouseId()).isEqualTo(100);
|
||||
var fresh = service.load(new SnapshotRequest(42L,7L,9L,"MERCHANT"));
|
||||
assertThat(fresh.getPermissions()).containsExactly("read");
|
||||
assertThat(fresh.getPermissionVersion()).isEqualTo(2); assertThat(fresh.getOverseasWarehouseId()).isEqualTo(200);
|
||||
// Shared platform role: enumerate both customers, not just membership tenant 0.
|
||||
jdbc.update("INSERT INTO iam_user VALUES(2,43,'ACTIVE',0)");
|
||||
jdbc.update("INSERT INTO iam_membership VALUES(10,2,8,'ACTIVE',0,NULL,NULL)");
|
||||
jdbc.update("INSERT INTO iam_role VALUES(5,0,'MERCHANT','SHARED','ACTIVE',0)");
|
||||
jdbc.update("INSERT INTO iam_membership_role VALUES(9,7,5),(10,8,5)");
|
||||
var assignments = new SqlSessionTemplate(factory.getObject())
|
||||
.getMapper(com.baiyue.iam.mapper.IamMembershipRoleMapper.class);
|
||||
assertThat(assignments.findAssignedAuthUserIds(0, 5L)).containsExactly(42L, 43L);
|
||||
assertThat(assignments.findAssignedAuthUserIds(7, 5L)).isEmpty();
|
||||
} finally { committed.countDown(); executor.shutdownNow(); }
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,7 @@ class MemberServiceTest {
|
||||
@MockBean private TenantRepository tenantRepository;
|
||||
@MockBean private IamRoleMapper roleMapper;
|
||||
@MockBean private AuthAccountClient auth;
|
||||
@MockBean private SecurityEventOutboxService securityEvents;
|
||||
@MockBean private SecurityVersionService securityVersions;
|
||||
@Resource private MemberService service;
|
||||
private IamMembership member;
|
||||
private IamUser user;
|
||||
@@ -106,8 +106,7 @@ class MemberServiceTest {
|
||||
assertEquals("DISABLED", result.status());
|
||||
verify(repository).save(member);
|
||||
verify(repository).saveUser(user);
|
||||
verify(securityEvents).userStatus(40L, 100L, "MERCHANT", "DISABLED");
|
||||
verify(securityEvents).permissionChanged(40L, 100L, "MERCHANT", "MEMBER_STATUS_CHANGED");
|
||||
verify(securityVersions).subjectChanged(40L);
|
||||
}
|
||||
|
||||
@Test void shouldHandleCase05() {
|
||||
@@ -156,8 +155,7 @@ class MemberServiceTest {
|
||||
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");
|
||||
verify(securityVersions).subjectChanged(authUserId);
|
||||
}
|
||||
|
||||
@Test void shouldHandleCase08() {
|
||||
|
||||
@@ -44,7 +44,7 @@ class MerchantOnboardingServiceTest {
|
||||
@MockBean private IamTenantReviewMapper reviews;
|
||||
@MockBean private CustomerDimensionRuleInitializationService dimensionRuleInitialization;
|
||||
@MockBean private OverseasStorageFeeInitializationService overseasStorageFeeInitialization;
|
||||
@MockBean private SecurityEventOutboxService securityEvents;
|
||||
@MockBean private SecurityVersionService securityVersions;
|
||||
@MockBean private CustomerCodeRepository customerCodes;
|
||||
@Resource private MerchantOnboardingService service;
|
||||
|
||||
@@ -78,10 +78,8 @@ 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");
|
||||
verify(securityVersions).tenantChanged(result.tenantId());
|
||||
verify(securityVersions).subjectChanged(9001L);
|
||||
|
||||
assertEquals("PROFILE_REQUIRED", result.tenantStatus());
|
||||
assertEquals("PENDING", result.dimensionInitializationStatus());
|
||||
|
||||
@@ -30,7 +30,7 @@ class PlatformInternalUserServiceTest {
|
||||
@MockBean private MemberRepository repository;
|
||||
@MockBean private IamRoleMapper roleMapper;
|
||||
@MockBean private AuthAccountClient auth;
|
||||
@MockBean private SecurityEventOutboxService securityEvents;
|
||||
@MockBean private SecurityVersionService securityVersions;
|
||||
@Resource private PlatformInternalUserService service;
|
||||
private IamMembership membership;
|
||||
private IamUser user;
|
||||
@@ -86,8 +86,7 @@ class PlatformInternalUserServiceTest {
|
||||
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");
|
||||
verify(securityVersions).subjectChanged(authUserId);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -103,8 +102,7 @@ class PlatformInternalUserServiceTest {
|
||||
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(securityVersions).subjectChanged(10L);
|
||||
verify(auth, never()).create(anyString(), any());
|
||||
}
|
||||
|
||||
@@ -116,7 +114,7 @@ class PlatformInternalUserServiceTest {
|
||||
|
||||
service.create(request);
|
||||
|
||||
verifyNoInteractions(securityEvents);
|
||||
verifyNoInteractions(securityVersions);
|
||||
verify(repository, never()).replaceRoles(anyLong(), anyLong(), anyList());
|
||||
}
|
||||
|
||||
@@ -150,9 +148,7 @@ class PlatformInternalUserServiceTest {
|
||||
new ChangeInternalUserStatusRequest("DISABLED"), 99L);
|
||||
|
||||
assertEquals("DISABLED", result.status());
|
||||
verify(securityEvents).userStatus(10L, 0L, "PLATFORM", "DISABLED");
|
||||
verify(securityEvents).permissionChanged(10L, 0L, "PLATFORM",
|
||||
"PLATFORM_INTERNAL_USER_STATUS_CHANGED");
|
||||
verify(securityVersions).subjectChanged(10L);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -33,7 +33,7 @@ class PlatformUserManagementServiceTest {
|
||||
@MockBean private MemberRepository members;
|
||||
@MockBean private IamRoleMapper roles;
|
||||
@MockBean private AuthAccountClient auth;
|
||||
@MockBean private SecurityEventOutboxService securityEvents;
|
||||
@MockBean private SecurityVersionService securityVersions;
|
||||
@Resource private PlatformUserManagementService service;
|
||||
private IamRole platformRole;
|
||||
private IamMembership membership;
|
||||
@@ -107,8 +107,7 @@ class PlatformUserManagementServiceTest {
|
||||
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");
|
||||
verify(securityVersions).subjectChanged(authUserId);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -159,8 +158,7 @@ class PlatformUserManagementServiceTest {
|
||||
assertEquals(900L, membership.getOverseasWarehouseId());
|
||||
assertEquals(900L, response.overseasWarehouseId());
|
||||
verify(auth).revokeSessions(eq("internal-token"), any());
|
||||
verify(securityEvents).permissionChanged(10L, 0L, "PLATFORM",
|
||||
"PLATFORM_USER_AUTHORIZATION_CHANGED");
|
||||
verify(securityVersions).subjectChanged(10L);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -197,8 +195,7 @@ class PlatformUserManagementServiceTest {
|
||||
assertEquals(901L, response.overseasWarehouseId());
|
||||
verify(auth).revokeSessions(eq("internal-token"),
|
||||
eq(new AuthAccountClient.RevokeAccountSessionsRequest(Set.of(10L))));
|
||||
verify(securityEvents).permissionChanged(10L, 0L, "PLATFORM",
|
||||
"PLATFORM_USER_AUTHORIZATION_CHANGED");
|
||||
verify(securityVersions).subjectChanged(10L);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -225,8 +222,7 @@ class PlatformUserManagementServiceTest {
|
||||
assertNull(response.overseasWarehouseId());
|
||||
verify(auth).revokeSessions(eq("internal-token"),
|
||||
eq(new AuthAccountClient.RevokeAccountSessionsRequest(Set.of(10L))));
|
||||
verify(securityEvents).permissionChanged(10L, 0L, "PLATFORM",
|
||||
"PLATFORM_USER_AUTHORIZATION_CHANGED");
|
||||
verify(securityVersions).subjectChanged(10L);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -246,8 +242,7 @@ class PlatformUserManagementServiceTest {
|
||||
UserResponse result = service.changeStatus(30L, new ChangeStatusRequest("ACTIVE"), 99L);
|
||||
|
||||
assertEquals("ACTIVE", result.status());
|
||||
verify(securityEvents).userStatus(10L, 0L, "PLATFORM", "ACTIVE");
|
||||
verify(securityEvents).permissionChanged(10L, 0L, "PLATFORM", "PLATFORM_USER_STATUS_CHANGED");
|
||||
verify(securityVersions).subjectChanged(10L);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -40,7 +40,7 @@ class RbacServiceTest {
|
||||
@MockBean private RbacRepository repository;
|
||||
@MockBean private MemberRepository memberRepository;
|
||||
@MockBean private AuthAccountClient authClient;
|
||||
@MockBean private SecurityEventOutboxService securityEvents;
|
||||
@MockBean private SecurityVersionService securityVersions;
|
||||
@Resource private RbacService service;
|
||||
|
||||
@Test
|
||||
@@ -338,6 +338,8 @@ class RbacServiceTest {
|
||||
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());
|
||||
verify(securityVersions, times(2)).subjectChanged(501L);
|
||||
verify(securityVersions, times(2)).subjectChanged(502L);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -368,6 +370,7 @@ class RbacServiceTest {
|
||||
|
||||
verify(authClient).revokeSessions(eq("test-internal-token"),
|
||||
eq(new AuthAccountClient.RevokeAccountSessionsRequest(Set.of(501L, 502L, 503L))));
|
||||
for (long id : new long[]{501, 502, 503}) verify(securityVersions).subjectChanged(id);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
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"),
|
||||
java.util.Set.of("PLATFORM_ADMIN"),
|
||||
java.util.Set.of("platform:overseas-warehouse:last-mile-order:list"));
|
||||
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,103 +0,0 @@
|
||||
package com.baiyue.iam.service;
|
||||
|
||||
import com.baiyue.common.event.EventEnvelope;
|
||||
import com.baiyue.common.event.IamSecurityEventsV1;
|
||||
import com.baiyue.iam.mapper.AuthorizationQueryMapper;
|
||||
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;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
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.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class SecurityEventOutboxServiceTest {
|
||||
@Mock JdbcTemplate jdbc;
|
||||
@Mock AuthorizationQueryMapper authorizationQueries;
|
||||
private final ObjectMapper objectMapper = new ObjectMapper().findAndRegisterModules();
|
||||
private SecurityEventOutboxService service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
service = new SecurityEventOutboxService();
|
||||
ReflectionTestUtils.setField(service, "jdbc", jdbc);
|
||||
ReflectionTestUtils.setField(service, "objectMapper", objectMapper);
|
||||
ReflectionTestUtils.setField(service, "authorizationQueries", authorizationQueries);
|
||||
}
|
||||
|
||||
@Test
|
||||
void userStatusWritesAValidEnvelopeWithCorrelationId() throws Exception {
|
||||
when(jdbc.queryForObject(anyString(), eq(Long.class), any())).thenReturn(1L);
|
||||
|
||||
service.userStatus(101L, 0L, "PLATFORM", "DISABLED");
|
||||
|
||||
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);
|
||||
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);
|
||||
}
|
||||
|
||||
@Test
|
||||
void permissionEventCarriesOnlyTheOverseasAuthorizationSnapshot() throws Exception {
|
||||
when(jdbc.queryForObject(anyString(), eq(Long.class), any())).thenReturn(7L);
|
||||
when(authorizationQueries.findRolesByIdentity(101L, 0L, "ADMIN"))
|
||||
.thenReturn(List.of("PLATFORM_ADMIN"));
|
||||
when(authorizationQueries.findPermissionsByIdentity(101L, 0L, "ADMIN"))
|
||||
.thenReturn(List.of("platform:overseas-warehouse:orders:view", "platform:iam:users:view",
|
||||
"merchant:overseas-warehouse:return-inventory:list", "lastleg:orders:view"));
|
||||
|
||||
service.permissionChanged(101L, 0L, "ADMIN", "ROLE_CHANGED");
|
||||
|
||||
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.PermissionVersionChanged payload = objectMapper.treeToValue(
|
||||
envelope.payload(), IamSecurityEventsV1.PermissionVersionChanged.class);
|
||||
assertThat(payload.roles()).containsExactly("PLATFORM_ADMIN");
|
||||
assertThat(payload.authorities()).containsExactlyInAnyOrder(
|
||||
"platform:overseas-warehouse:orders:view",
|
||||
"merchant:overseas-warehouse:return-inventory:list", "lastleg:orders:view");
|
||||
assertThat(payload.authorities()).doesNotContain("platform:iam:users:view");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.baiyue.iam.service;
|
||||
|
||||
import org.h2.jdbcx.JdbcDataSource;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.aop.framework.ProxyFactory;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.transaction.annotation.AnnotationTransactionAttributeSource;
|
||||
import org.springframework.transaction.interceptor.TransactionInterceptor;
|
||||
import org.springframework.transaction.support.TransactionTemplate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.*;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
class SecurityVersionTransactionTest {
|
||||
@Test void concurrentVersionsAreDurableAndRollbackWithMutation() throws Exception {
|
||||
var ds = new JdbcDataSource(); ds.setURL("jdbc:h2:mem:" + UUID.randomUUID() + ";MODE=MySQL;DB_CLOSE_DELAY=-1;LOCK_TIMEOUT=5000");
|
||||
var jdbc = new JdbcTemplate(ds);
|
||||
jdbc.execute("CREATE TABLE iam_security_subject_version(auth_user_id BIGINT PRIMARY KEY,permission_version BIGINT)");
|
||||
jdbc.execute("CREATE TABLE iam_security_tenant_version(tenant_id BIGINT PRIMARY KEY,status_version BIGINT)");
|
||||
var manager = new DataSourceTransactionManager(ds);
|
||||
var target = new SecurityVersionService(); ReflectionTestUtils.setField(target, "jdbc", jdbc);
|
||||
var proxy = new ProxyFactory(target); proxy.setProxyTargetClass(true);
|
||||
proxy.addAdvice(new TransactionInterceptor(manager, new AnnotationTransactionAttributeSource()));
|
||||
var service = (SecurityVersionService) proxy.getProxy();
|
||||
assertThatThrownBy(() -> service.subjectChanged(42)).isInstanceOf(org.springframework.transaction.IllegalTransactionStateException.class);
|
||||
new TransactionTemplate(manager).executeWithoutResult(tx -> { service.subjectChanged(42); service.tenantChanged(7); });
|
||||
var start = new CountDownLatch(1); var executor = Executors.newFixedThreadPool(4);
|
||||
try {
|
||||
var futures = new ArrayList<Future<?>>();
|
||||
for (int i=0; i<8; i++) futures.add(executor.submit(() -> {
|
||||
try { if (!start.await(5, TimeUnit.SECONDS)) throw new AssertionError("Start barrier timed out"); }
|
||||
catch (InterruptedException failure) { Thread.currentThread().interrupt(); throw new AssertionError(failure); }
|
||||
new TransactionTemplate(manager).executeWithoutResult(tx -> service.subjectChanged(42));
|
||||
}));
|
||||
start.countDown(); for (var future : futures) future.get(10, TimeUnit.SECONDS);
|
||||
} finally { executor.shutdownNow(); }
|
||||
new TransactionTemplate(manager).executeWithoutResult(tx -> {
|
||||
service.subjectChanged(42); service.tenantChanged(7); tx.setRollbackOnly();
|
||||
});
|
||||
assertThat(jdbc.queryForObject("SELECT permission_version FROM iam_security_subject_version WHERE auth_user_id=42", Long.class)).isEqualTo(9);
|
||||
assertThat(jdbc.queryForObject("SELECT status_version FROM iam_security_tenant_version WHERE tenant_id=7", Long.class)).isEqualTo(1);
|
||||
}
|
||||
}
|
||||
+5
-10
@@ -44,7 +44,7 @@ class TenantAdministratorProvisioningServiceTest {
|
||||
@MockBean private MemberRepository members;
|
||||
@MockBean private IamRoleMapper roles;
|
||||
@MockBean private AuthAccountClient auth;
|
||||
@MockBean private SecurityEventOutboxService securityEvents;
|
||||
@MockBean private SecurityVersionService securityVersions;
|
||||
@Resource private TenantAdministratorProvisioningService service;
|
||||
private IamTenant tenant;
|
||||
|
||||
@@ -104,9 +104,7 @@ 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");
|
||||
verify(securityVersions).subjectChanged(account.getValue().accountId());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -141,7 +139,7 @@ class TenantAdministratorProvisioningServiceTest {
|
||||
assertEquals(null, result.temporaryPassword());
|
||||
verify(auth, never()).create(anyString(), any());
|
||||
verify(members, never()).replaceRoles(anyLong(), anyLong(), any());
|
||||
verifyNoInteractions(securityEvents);
|
||||
verifyNoInteractions(securityVersions);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -160,8 +158,7 @@ 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");
|
||||
verify(securityVersions).subjectChanged(user.getAuthUserId());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -199,9 +196,7 @@ 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");
|
||||
verify(securityVersions).subjectChanged(user.getAuthUserId());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -30,7 +30,7 @@ class TenantCustomerExportTest {
|
||||
@MockBean private CustomerDimensionRuleInitializationService dimensionRuleInitialization;
|
||||
@MockBean private OverseasStorageFeeInitializationService overseasStorageFeeInitialization;
|
||||
@MockBean private TenantAdministratorProvisioningService administratorProvisioning;
|
||||
@MockBean private SecurityEventOutboxService securityEvents;
|
||||
@MockBean private SecurityVersionService securityVersions;
|
||||
@Resource private TenantService service;
|
||||
@Test
|
||||
void returnsUnmaskedCustomerRowsAndAdvancingCursor() {
|
||||
|
||||
@@ -57,7 +57,7 @@ class TenantServiceTest {
|
||||
@MockBean private MemberRepository members;
|
||||
@MockBean private IamRoleMapper roles;
|
||||
@MockBean private AuthAccountClient authClient;
|
||||
@MockBean private SecurityEventOutboxService securityEvents;
|
||||
@MockBean private SecurityVersionService securityVersions;
|
||||
@MockBean private CustomerDimensionRuleInitializationService dimensionRuleInitialization;
|
||||
@MockBean private OverseasStorageFeeInitializationService overseasStorageFeeInitialization;
|
||||
@SpyBean private TenantAdministratorProvisioningService provisioning;
|
||||
@@ -196,13 +196,13 @@ class TenantServiceTest {
|
||||
when(authClient.revokeSessions("internal-token", request)).thenReturn(null);
|
||||
assertThrows(BizException.class,
|
||||
() -> service.changeMerchantStatus(10001L, new ChangeTenantStatusRequest("DISABLED")));
|
||||
verify(securityEvents, never()).tenantStatus(org.mockito.ArgumentMatchers.anyLong(), any());
|
||||
verify(securityVersions, never()).tenantChanged(org.mockito.ArgumentMatchers.anyLong());
|
||||
|
||||
when(authClient.revokeSessions("internal-token", request))
|
||||
.thenReturn(com.baiyue.common.core.api.ApiResponse.success(null));
|
||||
assertEquals("DISABLED", service.changeMerchantStatus(10001L,
|
||||
new ChangeTenantStatusRequest("DISABLED")).status());
|
||||
verify(securityEvents).tenantStatus(10001L, "DISABLED");
|
||||
verify(securityVersions).tenantChanged(10001L);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -357,7 +357,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(securityVersions).tenantChanged(created.tenant().id());
|
||||
verify(provisioning).provisionPlatformCreatedTenant(any(), eq(9001L));
|
||||
verify(repository, never()).insertReview(any());
|
||||
}
|
||||
@@ -474,7 +474,7 @@ class TenantServiceTest {
|
||||
assertEquals("ACTIVE", tenant.getStatus());
|
||||
verify(repository).saveReview(review);
|
||||
verify(repository).saveTenant(tenant);
|
||||
verify(securityEvents).tenantStatus(10001L, "ACTIVE");
|
||||
verify(securityVersions).tenantChanged(10001L);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user