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>
146 lines
10 KiB
Plaintext
146 lines
10 KiB
Plaintext
# 감사 실패를 플랫폼 예외로 바꾸는 헬퍼
|
|
MongoAdminGateway.java:120 // The terminal record is written before the failure propagates, and its detail is the
|
|
MongoAdminGateway.java:121 // exception type rather than its message.
|
|
MongoAdminGateway.java:122 audit(MongoAdminAuditRecord.failed(command, failure, clock.instant()));
|
|
MongoAdminGateway.java:123 throw failure;
|
|
MongoAdminGateway.java:124 }
|
|
MongoAdminGateway.java:125 }
|
|
MongoAdminGateway.java:126
|
|
MongoAdminGateway.java:127 private void audit(MongoAdminAuditRecord record) {
|
|
MongoAdminGateway.java:128 try {
|
|
MongoAdminGateway.java:129 auditSink.accept(record);
|
|
MongoAdminGateway.java:130 } catch (RuntimeException sinkFailed) {
|
|
MongoAdminGateway.java:131 throw MongoOperationRejectedException.of(
|
|
MongoAdminGateway.java:132 "admin.audit",
|
|
MongoAdminGateway.java:133 "the admin audit sink refused the "
|
|
MongoAdminGateway.java:134 + record.phase()
|
|
MongoAdminGateway.java:135 + " record; an administrative operation that cannot be audited does not run");
|
|
MongoAdminGateway.java:136 }
|
|
MongoAdminGateway.java:137 }
|
|
|
|
# 실행 경로가 그 헬퍼를 쓰는 자리
|
|
MongoAdminGateway.java:83 public <T> T execute(MongoAdminCommand command, MongoAdminApproval approval, Supplier<T> body) {
|
|
MongoAdminGateway.java:84 Objects.requireNonNull(command, "command");
|
|
MongoAdminGateway.java:85 Objects.requireNonNull(body, "body");
|
|
MongoAdminGateway.java:86 Instant now = clock.instant();
|
|
MongoAdminGateway.java:87 authorization.require(command.operation());
|
|
MongoAdminGateway.java:88 if (command.expiredAt(now)) {
|
|
MongoAdminGateway.java:89 throw MongoOperationRejectedException.of(
|
|
MongoAdminGateway.java:90 "admin.command",
|
|
MongoAdminGateway.java:91 "this admin command expired before it ran; a stale command may describe a cluster that no"
|
|
MongoAdminGateway.java:92 + " longer looks like the one it was written for");
|
|
MongoAdminGateway.java:93 }
|
|
MongoAdminGateway.java:94 if (command.operation().highRisk()) {
|
|
MongoAdminGateway.java:95 if (approval == null) {
|
|
MongoAdminGateway.java:96 throw MongoOperationRejectedException.of(
|
|
MongoAdminGateway.java:97 "admin.approval",
|
|
MongoAdminGateway.java:98 "admin operation "
|
|
MongoAdminGateway.java:99 + command.operation()
|
|
MongoAdminGateway.java:100 + " destroys data or rewrites a collection; it runs under an approval bound to this"
|
|
MongoAdminGateway.java:101 + " exact command or not at all");
|
|
MongoAdminGateway.java:102 }
|
|
MongoAdminGateway.java:103 approval.require(command, now);
|
|
MongoAdminGateway.java:104 // Single use. Without this, one approval for one reshard authorizes every later reshard of
|
|
MongoAdminGateway.java:105 // the same shape, including the one nobody discussed.
|
|
MongoAdminGateway.java:106 if (!consumed.add(command.commandDigest())) {
|
|
MongoAdminGateway.java:107 throw MongoOperationRejectedException.of(
|
|
MongoAdminGateway.java:108 "admin.approval",
|
|
MongoAdminGateway.java:109 "this approval has already been used; a second execution needs a second decision");
|
|
MongoAdminGateway.java:110 }
|
|
MongoAdminGateway.java:111 }
|
|
MongoAdminGateway.java:112
|
|
MongoAdminGateway.java:113 // Fail closed: if the intent cannot be recorded, the command does not run.
|
|
MongoAdminGateway.java:114 audit(MongoAdminAuditRecord.intent(command, now));
|
|
MongoAdminGateway.java:115 try {
|
|
MongoAdminGateway.java:116 T value = body.get();
|
|
MongoAdminGateway.java:117 audit(MongoAdminAuditRecord.succeeded(command, clock.instant()));
|
|
MongoAdminGateway.java:118 return value;
|
|
MongoAdminGateway.java:119 } catch (RuntimeException failure) {
|
|
MongoAdminGateway.java:120 // The terminal record is written before the failure propagates, and its detail is the
|
|
MongoAdminGateway.java:121 // exception type rather than its message.
|
|
MongoAdminGateway.java:122 audit(MongoAdminAuditRecord.failed(command, failure, clock.instant()));
|
|
MongoAdminGateway.java:123 throw failure;
|
|
MongoAdminGateway.java:124 }
|
|
MongoAdminGateway.java:125 }
|
|
MongoAdminGateway.java:126
|
|
auditSink 를 직접 부르는 줄 : 2 개
|
|
audit 헬퍼를 부르는 줄 : 3 개
|
|
auditSink 라는 이름이 나오는 자리 전부 :
|
|
MongoAdminGateway.java:26 private final Consumer<MongoAdminAuditRecord> auditSink;
|
|
MongoAdminGateway.java:38 Consumer<MongoAdminAuditRecord> auditSink,
|
|
MongoAdminGateway.java:42 this.auditSink = Objects.requireNonNull(auditSink, "auditSink");
|
|
MongoAdminGateway.java:129 auditSink.accept(record);
|
|
MongoAdminGateway.java:147 auditSink.accept(
|
|
audit 헬퍼를 부르는 자리 전부 :
|
|
MongoAdminGateway.java:114 audit(MongoAdminAuditRecord.intent(command, now));
|
|
MongoAdminGateway.java:117 audit(MongoAdminAuditRecord.succeeded(command, clock.instant()));
|
|
MongoAdminGateway.java:122 audit(MongoAdminAuditRecord.failed(command, failure, clock.instant()));
|
|
|
|
# 드라이런은 헬퍼를 거치지 않는다
|
|
MongoAdminGateway.java:139 /**
|
|
MongoAdminGateway.java:140 * Validates an operation and records the intent without executing it.
|
|
MongoAdminGateway.java:141 *
|
|
MongoAdminGateway.java:142 * <p>The dry run is a prerequisite for every high-risk operation, so it has to be a first-class
|
|
MongoAdminGateway.java:143 * call rather than a flag somebody remembers to pass.
|
|
MongoAdminGateway.java:144 */
|
|
MongoAdminGateway.java:145 public void dryRun(MongoAdminOperation operation, String target, String operator, String reason) {
|
|
MongoAdminGateway.java:146 authorization.require(operation);
|
|
MongoAdminGateway.java:147 auditSink.accept(
|
|
MongoAdminGateway.java:148 MongoAdminAuditRecord.dryRun(operation, target, operator, reason, clock.instant()));
|
|
MongoAdminGateway.java:149 }
|
|
|
|
# 그 원칙을 고정하는 시험
|
|
MongoAdminAuditStateMachineTest.java:84 @Test
|
|
MongoAdminAuditStateMachineTest.java:85 @DisplayName("an audit sink that fails stops the command from running")
|
|
MongoAdminAuditStateMachineTest.java:86 void anUnauditableCommandDoesNotRun() {
|
|
MongoAdminAuditStateMachineTest.java:87 MongoAdminGateway gateway =
|
|
MongoAdminAuditStateMachineTest.java:88 new MongoAdminGateway(
|
|
MongoAdminAuditStateMachineTest.java:89 new MongoAdminRuntimeGuard(true, "app-credential", "migration-credential"),
|
|
MongoAdminAuditStateMachineTest.java:90 routineAuthorization(),
|
|
MongoAdminAuditStateMachineTest.java:91 record -> {
|
|
MongoAdminAuditStateMachineTest.java:92 throw new IllegalStateException("the audit collection is unreachable");
|
|
MongoAdminAuditStateMachineTest.java:93 },
|
|
MongoAdminAuditStateMachineTest.java:94 FIXED);
|
|
MongoAdminAuditStateMachineTest.java:95 boolean[] ran = {false};
|
|
MongoAdminAuditStateMachineTest.java:96
|
|
MongoAdminAuditStateMachineTest.java:97 assertThatThrownBy(
|
|
MongoAdminAuditStateMachineTest.java:98 () ->
|
|
MongoAdminAuditStateMachineTest.java:99 gateway.execute(
|
|
MongoAdminAuditStateMachineTest.java:100 MongoAdminCommand.routine(
|
|
MongoAdminAuditStateMachineTest.java:101 MongoAdminOperation.COLL_MOD,
|
|
MongoAdminAuditStateMachineTest.java:102 "shop.orders",
|
|
MongoAdminAuditStateMachineTest.java:103 "operator",
|
|
MongoAdminAuditStateMachineTest.java:104 "widen",
|
|
MongoAdminAuditStateMachineTest.java:105 NOW.plusSeconds(60)),
|
|
MongoAdminAuditStateMachineTest.java:106 null,
|
|
MongoAdminAuditStateMachineTest.java:107 () -> ran[0] = true))
|
|
MongoAdminAuditStateMachineTest.java:108 .isInstanceOf(MongoOperationRejectedException.class)
|
|
MongoAdminAuditStateMachineTest.java:109 .hasMessageContaining("cannot be audited");
|
|
MongoAdminAuditStateMachineTest.java:110 assertThat(ran[0])
|
|
MongoAdminAuditStateMachineTest.java:111 .as("an administrative operation nobody can reconstruct afterwards must not happen")
|
|
MongoAdminAuditStateMachineTest.java:112 .isFalse();
|
|
MongoAdminAuditStateMachineTest.java:113 }
|
|
드라이런의 감사 실패를 보는 시험 줄 : 0 개
|
|
이 게이트웨이의 dryRun 을 부르는 줄 (저장소 전체) : 0 개
|
|
[대조] 같은 게이트웨이의 execute 를 부르는 줄 : 18 개
|
|
그 execute 호출 전부 :
|
|
main · MongoQueryableEncryptionCollectionManager.java:41 adminGateway.execute(
|
|
main · MongoQueryableEncryptionCollectionManager.java:63 adminGateway.execute(
|
|
main · MongoQueryableEncryptionCollectionManager.java:70 adminGateway.execute(MongoAdminOperation.COLL_MOD, collection, operator, reason, apply);
|
|
main · MongoShardingAdminGateway.java:64 adminGateway.execute(MongoAdminOperation.SHARD_COLLECTION, collection, operator, reason, apply);
|
|
main · MongoShardingAdminGateway.java:75 adminGateway.execute(MongoAdminOperation.REFINE_SHARD_KEY, collection, operator, reason, apply);
|
|
main · MongoShardingAdminGateway.java:86 adminGateway.execute(
|
|
main · MongoShardingAdminGateway.java:92 adminGateway.execute(MongoAdminOperation.BALANCER_CONTROL, scope, operator, reason, apply);
|
|
test · MongoAdminAuditStateMachineTest.java:45 gateway.execute(
|
|
test · MongoAdminAuditStateMachineTest.java:65 gateway.execute(
|
|
test · MongoAdminAuditStateMachineTest.java:99 gateway.execute(
|
|
test · MongoAdminAuditStateMachineTest.java:140 assertThatThrownBy(() -> gateway.execute(real, approval, () -> "dropped"))
|
|
test · MongoAdminAuditStateMachineTest.java:155 gateway.execute(
|
|
test · MongoAdminAuditStateMachineTest.java:169 gateway.execute(command, approval, () -> "dropped");
|
|
test · MongoAdminAuditStateMachineTest.java:171 assertThatThrownBy(() -> gateway.execute(command, approval, () -> "dropped"))
|
|
test · MongoAdminAuditStateMachineTest.java:185 gateway.execute(
|
|
test · MongoAdminAuditStateMachineTest.java:198 assertThatThrownBy(() -> gateway.execute(dropOf("shop.orders"), null, () -> "dropped"))
|
|
test · MongoAdminAuditStateMachineTest.java:207 gateway.execute(
|
|
test · MongoAdminRuntimeGuardTest.java:80 gateway.execute(
|
|
[참고] 같은 정규식이 다른 게이트웨이에서 잡는 줄 : 8 개
|