This commit is contained in:
2026-08-25 16:40:48 +08:00
parent 1f6f1a0677
commit 87c5e18ca6
93 changed files with 1154 additions and 527 deletions
@@ -0,0 +1,54 @@
package com.baiyue.lastleg.security.service;
import com.baiyue.common.core.exception.BizException;
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.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import java.time.Instant;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
class LastlegSecurityProjectionTransportTest {
private final ObjectMapper objectMapper = new ObjectMapper().findAndRegisterModules();
@Test
void validatesEnvelopeHashAndStoresTheSixtyFourCharacterDigest() {
EventEnvelope envelope = envelope();
String stored = LastlegSecurityProjectionService.validatedStorageHash(envelope);
assertThat(stored).hasSize(64).doesNotStartWith("sha256:");
assertThat(envelope.payloadHash()).isEqualTo("sha256:" + stored);
}
@Test
void rejectsTamperedPayloadBeforeInboxMutation() {
EventEnvelope source = envelope();
EventEnvelope tampered = new EventEnvelope(source.eventId(), source.eventType(), source.eventVersion(),
source.occurredAt(), source.sourceRegion(), source.targetRegion(), source.tenantId(),
source.aggregateType(), source.aggregateId(), source.aggregateVersion(), source.handoverVersion(),
source.traceId(), source.correlationId(), source.causationId(), source.payload(),
"sha256:" + "0".repeat(64));
assertThatThrownBy(() -> LastlegSecurityProjectionService.validatedStorageHash(tampered))
.isInstanceOf(BizException.class).hasMessageContaining("payloadHash");
}
private EventEnvelope envelope() {
var value = new IamSecurityEventsV1.PermissionVersionChanged(
101L, 0L, "PLATFORM", 3L, "contract", Instant.parse("2026-08-25T07:00:00Z"));
var payload = objectMapper.valueToTree(value);
return new EventEnvelope("00000000-0000-4000-8000-000000000091",
EventTypes.IAM_PERMISSION_VERSION_CHANGED, IamSecurityEventsV1.VERSION,
Instant.parse("2026-08-25T07:00:00Z"), RegionCode.CN, RegionCode.OVERSEAS, "0",
"AUTHORIZATION", "101", 3L, null, "trace-12345678", "correlation-12345678",
null, payload, EventPayloadHash.sha256(EventPayloadCanonicalizer.canonicalize(payload)));
}
}
@@ -0,0 +1,67 @@
package com.baiyue.lastleg.security.service;
import com.baiyue.lastleg.security.dto.LastlegSecurityDtos.FailureReason;
import com.baiyue.lastleg.security.dto.LastlegSecurityDtos.ValidationRequest;
import org.junit.jupiter.api.Test;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
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.Mockito.doReturn;
import static org.mockito.Mockito.mock;
class LastlegSecurityValidationServiceTest {
private final JdbcTemplate jdbc = mock(JdbcTemplate.class);
private final LastlegSecurityValidationService service = new LastlegSecurityValidationService(jdbc);
@Test void disabledUserFailsClosed() {
doReturn(List.of("DISABLED")).when(jdbc)
.query(anyString(), any(RowMapper.class), any(Object[].class));
assertThat(service.validate(new ValidationRequest(42L, 0L, 8L)).failureReason())
.isEqualTo(FailureReason.USER_DISABLED);
}
@Test void disabledCustomerFailsClosed() {
doReturn(List.of("ACTIVE"), List.of("DISABLED")).when(jdbc)
.query(anyString(), any(RowMapper.class), any(Object[].class));
assertThat(service.validate(new ValidationRequest(42L, 100L, 8L)).failureReason())
.isEqualTo(FailureReason.TENANT_DISABLED);
}
@Test void missingUserProjectionFailsClosed() {
doReturn(List.of()).when(jdbc)
.query(anyString(), any(RowMapper.class), any(Object[].class));
assertThat(service.validate(new ValidationRequest(42L, 0L, 8L)).failureReason())
.isEqualTo(FailureReason.SECURITY_PROJECTION_MISSING);
}
@Test void customerProjectionIsMandatoryButPlatformTenantDoesNotNeedOne() {
doReturn(List.of("ACTIVE"), List.of(), List.of("ACTIVE"), List.of(8L)).when(jdbc)
.query(anyString(), any(RowMapper.class), any(Object[].class));
assertThat(service.validate(new ValidationRequest(42L, 100L, 8L)).failureReason())
.isEqualTo(FailureReason.SECURITY_PROJECTION_MISSING);
assertThat(service.validate(new ValidationRequest(42L, 0L, 8L)).valid()).isTrue();
}
@Test void permissionVersionMustExactlyMatchReliableProjection() {
doReturn(List.of("ACTIVE"), List.of(9L), List.of("ACTIVE"), List.of(9L),
List.of("ACTIVE"), List.of(9L)).when(jdbc)
.query(anyString(), any(RowMapper.class), any(Object[].class));
assertThat(service.validate(new ValidationRequest(42L, 0L, 8L)).failureReason())
.isEqualTo(FailureReason.PERMISSION_VERSION_STALE);
assertThat(service.validate(new ValidationRequest(42L, 0L, 10L)).failureReason())
.isEqualTo(FailureReason.SECURITY_PROJECTION_NOT_READY);
assertThat(service.validate(new ValidationRequest(42L, 0L, 9L)).valid()).isTrue();
}
@Test void missingPermissionProjectionFailsClosed() {
doReturn(List.of("ACTIVE"), List.of()).when(jdbc)
.query(anyString(), any(RowMapper.class), any(Object[].class));
assertThat(service.validate(new ValidationRequest(42L, 0L, 9L)).failureReason())
.isEqualTo(FailureReason.SECURITY_PROJECTION_MISSING);
}
}
@@ -0,0 +1,28 @@
package com.baiyue.lastleg.security.service;
import com.baiyue.common.core.exception.BizException;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
class SecurityProjectionMonotonicityTest {
@Test void appliesFirstAndNewerVersions() {
assertThat(SecurityProjectionMonotonicity.decide(null, null, 1, "hash-1"))
.isEqualTo(SecurityProjectionMonotonicity.Decision.APPLY);
assertThat(SecurityProjectionMonotonicity.decide(1L, "hash-1", 2, "hash-2"))
.isEqualTo(SecurityProjectionMonotonicity.Decision.APPLY);
}
@Test void ignoresOlderAndSameHashVersions() {
assertThat(SecurityProjectionMonotonicity.decide(3L, "hash-3", 2, "hash-2"))
.isEqualTo(SecurityProjectionMonotonicity.Decision.STALE);
assertThat(SecurityProjectionMonotonicity.decide(3L, "hash-3", 3, "hash-3"))
.isEqualTo(SecurityProjectionMonotonicity.Decision.DUPLICATE_VERSION);
}
@Test void rejectsSameVersionWithDifferentHash() {
assertThatThrownBy(() -> SecurityProjectionMonotonicity.decide(3L, "hash-a", 3, "hash-b"))
.isInstanceOf(BizException.class).hasMessageContaining("payloadHash 冲突");
}
}