The keycloak project ended with four open questions that design could not
settle. A two-VM lab was built to answer them by measurement, and this is
that material: 26 experiments, 125 raw command outputs, 22 browser captures.
Follows the import procedure in README.md.
source/ the originating repository verbatim — 78 documents, 28 SVGs,
8 manifests, plus .source-revision recording the commit
final/ the SSOT
document.md 729 lines written from the 29 experiment documents, not
concatenated: what was predicted, what was measured, and
where the measurement itself was wrong
evidence/raw 125 outputs, flattened to <experiment>__<file> because
the originals collided (01-baseline.txt appeared three
times) and the audit only globs the top level
evidence/meta one per raw file; command and exitCode are null and the
README says why rather than inventing them
evidence/browser 22 captures
assets/ three diagrams through techviz
.techviz/ their VizSpecs
A separate project rather than an addition to keycloak: the B-layer answers
that project's four questions, but the A, C and D layers are about cluster
failure, SSO and operations, and one document.md should hold one subject.
The four question records there can point here through 관계.
Recorded rather than papered over: only three of the 28 diagrams were
remade. The repository forbids hand-drawn SVG and forbids titles inside the
canvas; all 28 originals carry both, so converting them is redrawing, not
reformatting. They stay in source/ and the gap is written into the document.
verify-pipeline.py passes. audit-records.py reports no issues.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
124 lines
9.5 KiB
Plaintext
124 lines
9.5 KiB
Plaintext
# 계약 leaf 와 그 소비자 leaf 의 소스 규모
|
|
messaging-admin-api 의 소스 세트 : main test
|
|
main 25 파일 · 1613 줄
|
|
test 1 파일 · 147 줄
|
|
messaging-admin-runtime 의 소스 세트 : main test
|
|
main 12 파일 · 1253 줄
|
|
test 6 파일 · 1051 줄
|
|
|
|
# 계약 leaf 의 유일한 시험 파일이 담은 것
|
|
파일 : DestructiveOperationGuardTest.java · @Test 9 개
|
|
:51 void anApplicationRuntimeCannotRedrive() {
|
|
:63 void anAdminRuntimeStillNeedsAnApproval() {
|
|
:73 void anExpiredApprovalDoesNotAuthorise() {
|
|
:91 void aDryRunIsAlwaysPermitted() {
|
|
:102 void anApprovedAdminOperationIsAuthorised() {
|
|
:113 void aRedriveCannotTargetItsOwnSource() {
|
|
:119 void aRedriveBatchIsBoundedSoOneOperationCannotFloodTheSource() {
|
|
:128 void aTopologyManifestReportsEveryDifference() {
|
|
:140 void aMatchingTopologyReportsNoDifferences() {
|
|
그 시험들이 승인 객체를 어떻게 얻는가 :
|
|
DestructiveOperationGuardTest.java:27 /**
|
|
DestructiveOperationGuardTest.java:28 * A grant the issuer signed, verified the way the runtime verifies it.
|
|
DestructiveOperationGuardTest.java:29 *
|
|
DestructiveOperationGuardTest.java:30 * <p>The guard used to take a plain {@code AdminApproval} record, which any caller could
|
|
DestructiveOperationGuardTest.java:31 * construct. Every case below now has to go through a real signature to get an approval object at
|
|
DestructiveOperationGuardTest.java:32 * all, which is the property the type change was made for.
|
|
DestructiveOperationGuardTest.java:33 */
|
|
DestructiveOperationGuardTest.java:45 Long.MAX_VALUE,
|
|
DestructiveOperationGuardTest.java:46 digest);
|
|
DestructiveOperationGuardTest.java:47 return ISSUER.verify(grant, ISSUER.sign(grant), digest, from);
|
|
DestructiveOperationGuardTest.java:48 }
|
|
그 셋의 단언이 무엇에 떨어지는가 :
|
|
DestructiveOperationGuardTest.java:43 Optional.empty(),
|
|
DestructiveOperationGuardTest.java:57 DestructiveOperation.REDRIVE, ORDERS, Optional.of(VALID), false, NOW))
|
|
DestructiveOperationGuardTest.java:59 .hasMessageContaining("<가림>");
|
|
DestructiveOperationGuardTest.java:67 () -> guard.authorize(DestructiveOperation.PURGE, ORDERS, Optional.empty(), false, NOW))
|
|
DestructiveOperationGuardTest.java:69 .hasMessageContaining("approval");
|
|
DestructiveOperationGuardTest.java:85 DestructiveOperation.OFFSET_RESET, ORDERS, Optional.of(expired), false, NOW))
|
|
DestructiveOperationGuardTest.java:87 .hasMessageContaining("validity window");
|
|
DestructiveOperationGuardTest.java:97 DestructiveOperation.DELETE_DESTINATION, ORDERS, Optional.empty(), true, NOW))
|
|
DestructiveOperationGuardTest.java:98 .doesNotThrowAnyException();
|
|
DestructiveOperationGuardTest.java:108 DestructiveOperation.REPLAY, ORDERS, Optional.of(VALID), false, NOW))
|
|
DestructiveOperationGuardTest.java:109 .doesNotThrowAnyException();
|
|
가드가 그 메시지를 내는 자리 :
|
|
DestructiveOperationGuard.java:66 "APPROVAL_REQUIRED",
|
|
DestructiveOperationGuard.java:68 if (!granted.isValidAt(now)) {
|
|
DestructiveOperationGuard.java:70 "APPROVAL_EXPIRED", "approval " + granted.ticket() + " is outside its validity window");
|
|
|
|
# 소비자 leaf 의 시험 여섯
|
|
AdminOperationJournalTest.java @Test 8 개
|
|
ApprovalForgeryTest.java @Test 10 개
|
|
ApprovedPlanExecutionTest.java @Test 11 개
|
|
RedriveResumptionTest.java @Test 5 개
|
|
TopologyValidationRuntimeTest.java @Test 4 개
|
|
TopologyValidatorTest.java @Test 13 개
|
|
그중 승인 위조 시험이 단언하는 것 :
|
|
ApprovalForgeryTest.java:51 void aVerifiedApprovalCannotBeConstructedOutsideTheVerifier() {
|
|
ApprovalForgeryTest.java:69 void anAlteredGrantDoesNotVerify() {
|
|
ApprovalForgeryTest.java:84 void aSignatureFromAnotherIssuerDoesNotVerify() {
|
|
ApprovalForgeryTest.java:96 void anApprovalForOnePlanCannotExecuteAnother() {
|
|
ApprovalForgeryTest.java:110 void aVerifiedApprovalForTheWrongSourceCannotBeAttached() {
|
|
ApprovalForgeryTest.java:133 void aPlanBeyondTheApprovedImpactCeilingCannotExecute() {
|
|
ApprovalForgeryTest.java:146 void anApprovalForAReplayCannotAuthoriseARedrive() {
|
|
ApprovalForgeryTest.java:158 void anExpiredGrantDoesNotVerify() {
|
|
ApprovalForgeryTest.java:172 void theApproverAndTheOperatorMustBeDifferentPeople() {
|
|
ApprovalForgeryTest.java:192 void aCorrectlyIssuedApprovalExecutes() {
|
|
|
|
# 계약 leaf 가 담은 main 타입 스물다섯
|
|
AdminApproval AdminOperationJournal AdminOperationLease AdminOperationRecord
|
|
AdminOperationState ApprovalGrant ApprovalVerifier ApprovedRedrivePlan ApprovedReplayPlan
|
|
DestinationTopology DestructiveOperation DestructiveOperationGuard HmacApprovalVerifier
|
|
PlanDigest RedrivePlan RedriveRequest RedriveResult ReplayPlan ReplayRequest ReplayResult
|
|
TopologyIssue TopologyManagementMode TopologyManifest TopologyValidationReport
|
|
VerifiedApproval
|
|
# 승인 검증자는 어디서 만들어지는가
|
|
main 구현 :
|
|
main · messaging-admin-api · HmacApprovalVerifier.java:26 public final class HmacApprovalVerifier implements ApprovalVerifier {
|
|
생성 지점 :
|
|
test · messaging-admin-api · DestructiveOperationGuardTest.java:21 new HmacApprovalVerifier(
|
|
test · messaging-admin-runtime · ApprovalForgeryTest.java:42 new HmacApprovalVerifier(
|
|
test · messaging-admin-runtime · ApprovalForgeryTest.java:46 new HmacApprovalVerifier(
|
|
test · messaging-admin-runtime · ApprovedPlanExecutionTest.java:37 private static final HmacApprovalVerifier VERIFIER = new HmacApprovalVerifier(ISSUING_KEY);
|
|
test · messaging-admin-runtime · RedriveResumptionTest.java:45 new HmacApprovalVerifier(
|
|
생성 지점 5 곳 · 파일 4 개
|
|
|
|
# main 에 검증자를 두지 않는 것이 공백인가 경계인가
|
|
자기 자바독이 적은 이유 :
|
|
HmacApprovalVerifier.java:50 * <p>Present because the issuer and the verifier must agree on the canonical form exactly, and a
|
|
HmacApprovalVerifier.java:51 * second implementation of that agreement is a second thing to drift. Holding this key is what
|
|
HmacApprovalVerifier.java:52 * makes a caller an issuer — it is not, and must not become, available to the runtime that
|
|
HmacApprovalVerifier.java:53 * executes operations.
|
|
같은 사슬의 가드는 main 빈으로 있다 :
|
|
MessagingAdminAutoConfiguration.java:35 @Bean
|
|
MessagingAdminAutoConfiguration.java:36 @ConditionalOnMissingBean
|
|
MessagingAdminAutoConfiguration.java:37 public DestructiveOperationGuard destructiveOperationGuard() {
|
|
MessagingAdminAutoConfiguration.java:38 // false: an application runtime never holds an admin credential, so the guard refuses the
|
|
MessagingAdminAutoConfiguration.java:39 // operations that would need one. An operator tool overrides this bean with true.
|
|
MessagingAdminAutoConfiguration.java:40 return new DestructiveOperationGuard(false);
|
|
MessagingAdminAutoConfiguration.java:41 }
|
|
|
|
# 계약 타입의 경계 조건이 따로 고정돼 있는가
|
|
PlanDigest main 10 파일 · test 6 파일 · 그 이름을 건 시험 파일 0 개
|
|
ApprovalGrant main 3 파일 · test 4 파일 · 그 이름을 건 시험 파일 0 개
|
|
VerifiedApproval main 7 파일 · test 4 파일 · 그 이름을 건 시험 파일 0 개
|
|
ApprovedRedrivePlan main 2 파일 · test 2 파일 · 그 이름을 건 시험 파일 0 개
|
|
ApprovedReplayPlan main 2 파일 · test 1 파일 · 그 이름을 건 시험 파일 0 개
|
|
TopologyManifest main 5 파일 · test 4 파일 · 그 이름을 건 시험 파일 0 개
|
|
만료를 직접 고정하는 자리 :
|
|
test · messaging-admin-api · DestructiveOperationGuardTest.java:73 void anExpiredApprovalDoesNotAuthorise() {
|
|
test · messaging-admin-runtime · ApprovalForgeryTest.java:158 void anExpiredGrantDoesNotVerify() {
|
|
PlanDigest 가 test 에서 어떻게 쓰이는가 :
|
|
test · messaging-admin-api · DestructiveOperationGuardTest.java:36 PlanDigest digest = PlanDigest.ofCanonical(operation + "|" + source.value());
|
|
test · messaging-admin-runtime · AdminOperationJournalTest.java:9 import dev.caskeleton.messaging.admin.PlanDigest;
|
|
test · messaging-admin-runtime · AdminOperationJournalTest.java:29 private static final PlanDigest DIGEST = PlanDigest.ofCanonical("<가림>");
|
|
test · messaging-admin-runtime · AdminOperationJournalTest.java:30 private static final PlanDigest OTHER_DIGEST = PlanDigest.ofCanonical("<가림>");
|
|
test · messaging-admin-runtime · ApprovedPlanExecutionTest.java:13 import dev.caskeleton.messaging.admin.PlanDigest;
|
|
test · messaging-admin-runtime · ApprovedPlanExecutionTest.java:45 PlanDigest digest,
|
|
test · messaging-admin-runtime · RedriveResumptionTest.java:10 import dev.caskeleton.messaging.admin.PlanDigest;
|
|
test · messaging-admin-runtime · RedriveResumptionTest.java:146 PlanDigest digest = PlanDigest.ofCanonical("<가림>");
|
|
test · messaging-outbox-jdbc-postgresql · AdminOperationJournalPostgresIT.java:9 import dev.caskeleton.messaging.admin.PlanDigest;
|
|
test · messaging-outbox-jdbc-postgresql · AdminOperationJournalPostgresIT.java:46 private static final PlanDigest DIGEST = PlanDigest.ofCanonical("<가림>");
|
|
test · messaging-spring-boot-starter · MessagingAutoConfigurationTest.java:432 dev.caskeleton.messaging.admin.PlanDigest planDigest,
|
|
test · messaging-spring-boot-starter · MessagingAutoConfigurationTest.java:468 String approvalTicket, dev.caskeleton.messaging.admin.PlanDigest planDigest) {
|