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>
165 lines
13 KiB
Plaintext
165 lines
13 KiB
Plaintext
Sub-scope 03 (inbound-web) auth + authz + security
|
|
revision=a24ece9cf797f7ea647e33bf846b115208ed1ba5
|
|
generatedAt=2026-08-30T07:19:58+00:00
|
|
|
|
=== OWNED FILES ===
|
|
5e06c2caeecc3b985673b29ca845364716bffef9 main/java/dev/caskeleton/adapter/inbound/web/auth/AuthErrorResponseWriter.java 76
|
|
d85d9caec98b10eb1e442d3b4f1298c8129b7143 main/java/dev/caskeleton/adapter/inbound/web/auth/AuthenticatedPrincipal.java 19
|
|
7968966a4048df417e6283602b1f6931df5736cd main/java/dev/caskeleton/adapter/inbound/web/auth/EnvelopeAccessDeniedHandler.java 35
|
|
bb127448ecca12668e76c8016f967d451fb09dce main/java/dev/caskeleton/adapter/inbound/web/auth/EnvelopeAuthenticationEntryPoint.java 35
|
|
50fc5f5e1bdc718855d574f4d15149b9841e246d main/java/dev/caskeleton/adapter/inbound/web/auth/JwtDecoderConfig.java 110
|
|
2f07433aa5020e83e180f82ac2ed062195d76b66 main/java/dev/caskeleton/adapter/inbound/web/auth/JwtToAuthenticatedPrincipalConverter.java 95
|
|
f51dd6464eb972389b507afcc74cfc877533f9b2 main/java/dev/caskeleton/adapter/inbound/web/auth/PrimitiveSessionSecurityContextRepository.java 327
|
|
890e5c6a11e71288ac6271c36bf0e20c5d45483a main/java/dev/caskeleton/adapter/inbound/web/auth/RedisSessionWebConfig.java 34
|
|
24cf8265230ac6511bb72bcd73b8bd454d380c0e main/java/dev/caskeleton/adapter/inbound/web/auth/RestrictedPathRule.java 41
|
|
c5ec53e63559854e6b76349841365e1de4914888 main/java/dev/caskeleton/adapter/inbound/web/auth/SecurityConfig.java 172
|
|
dfbe16dc2aaa5e703ea8a77efd92313e7d3d5d60 main/java/dev/caskeleton/adapter/inbound/web/auth/SecurityErrorClassifier.java 132
|
|
dcc5991ca3a35b1ed2f260786688aad063942cb1 main/java/dev/caskeleton/adapter/inbound/web/authz/AuthorizationAdapter.java 33
|
|
f01b7f01e244081f896e95e4a15efa9e780480ee main/java/dev/caskeleton/adapter/inbound/web/authz/MethodSecurityConfig.java 61
|
|
acea282c134c2dd09fedfc1cf2df5d228835a42c main/java/dev/caskeleton/adapter/inbound/web/authz/RequiresPermissionAuthorizationManager.java 87
|
|
09be195e7af550b78e4eb81d6b884c7ef89d5c05 main/java/dev/caskeleton/adapter/inbound/web/authz/RolePermissionPolicy.java 36
|
|
118fd90282f87299ac77df784417beb6fd60708d main/java/dev/caskeleton/adapter/inbound/web/authz/RolePermissionRegistry.java 52
|
|
8f074d09eb566d270475bb60a8ef654cf03b03a8 main/java/dev/caskeleton/adapter/inbound/web/security/AuthenticationView.java 60
|
|
5679e864708a5d81a94b689eff2ce13947c7d123 main/java/dev/caskeleton/adapter/inbound/web/security/CorsProfile.java 88
|
|
428ba0ac41ccd70cdc97e2f39b80e5239b5867cf main/java/dev/caskeleton/adapter/inbound/web/security/CsrfProfile.java 44
|
|
ebacf4fc9590e3ca1913009d8b95891dbc5b9699 main/java/dev/caskeleton/adapter/inbound/web/security/SecurityIdentity.java 22
|
|
ceb95d338a365ad220f85d398d46af329389f05a main/java/dev/caskeleton/adapter/inbound/web/security/UntrustedTenantInputException.java 23
|
|
8bf20591cd78b894191903952048df0686bd8796 main/java/dev/caskeleton/adapter/inbound/web/security/WebActorContextResolver.java 29
|
|
c634486dff5d257f063c91ce58823cef3dc0d755 main/java/dev/caskeleton/adapter/inbound/web/security/WebCorsPolicyValidator.java 96
|
|
2efd1eca6c7992b634364ae6bff5b0d0a61d84bf main/java/dev/caskeleton/adapter/inbound/web/security/WebCredentialMode.java 42
|
|
348a0596feda88c814690c70b8ed17174209812c main/java/dev/caskeleton/adapter/inbound/web/security/WebCsrfPolicyResolver.java 43
|
|
6aaa56cb3cfabcbfaef1976e0a265f26cf89ff6c main/java/dev/caskeleton/adapter/inbound/web/security/WebSecurityContextBridge.java 63
|
|
8cf47c6f9457b7032629f739a4a5ab3fb6d81465 main/java/dev/caskeleton/adapter/inbound/web/security/WebTenantContextResolver.java 51
|
|
6b721fc0fbea94ac0ce10660fb92ea1070d90523 test/java/dev/caskeleton/adapter/inbound/web/auth/AuthenticatedPrincipalTest.java 33
|
|
86bfeab4c99f67b585be3a2e7a340e766b500bd3 test/java/dev/caskeleton/adapter/inbound/web/auth/CorsSecurityFilterIntegrationTest.java 233
|
|
9fe3e743b6efc179483bfadb15348e07f91d8148 test/java/dev/caskeleton/adapter/inbound/web/auth/EnvelopeAccessDeniedHandlerTest.java 38
|
|
9b8b9427f7b7de3357cbb73713f600bec79fbb6b test/java/dev/caskeleton/adapter/inbound/web/auth/EnvelopeAuthenticationEntryPointTest.java 129
|
|
42fc9d36f7673e607f3310c0d6ed08dee976dce1 test/java/dev/caskeleton/adapter/inbound/web/auth/JwtDecoderConfigTest.java 93
|
|
fc807a30af5fbefb14810df62e78e1931aefed76 test/java/dev/caskeleton/adapter/inbound/web/auth/JwtJwksSecurityFilterIntegrationTest.java 455
|
|
75de183b8b3fbbfbdaf757521a0dba368c67a4ff test/java/dev/caskeleton/adapter/inbound/web/auth/JwtToAuthenticatedPrincipalConverterTest.java 34
|
|
3abf02fb8d3481703cf7aa7cb7e18c4b1a3bbe96 test/java/dev/caskeleton/adapter/inbound/web/auth/PrimitiveSessionSecurityContextRepositoryTest.java 226
|
|
b35c4e6caa0689fa088c575db34c6917cf05f8b2 test/java/dev/caskeleton/adapter/inbound/web/auth/RedisSessionWebConfigTest.java 72
|
|
9be1c45d31d63ad45a2f559ccd00a4690f45e655 test/java/dev/caskeleton/adapter/inbound/web/auth/SecurityErrorClassifierTest.java 166
|
|
b736fd3246ab5a0be66feb6fc0e862a2080ed8fe test/java/dev/caskeleton/adapter/inbound/web/auth/SecurityModeWebContractTest.java 220
|
|
513127906df4e0421575a95e36f10c0b6a1bcc5a test/java/dev/caskeleton/adapter/inbound/web/authz/AuthorizationAdapterTest.java 47
|
|
3e8f3d2672bfc5778c7811a98e4896a8d83a78a7 test/java/dev/caskeleton/adapter/inbound/web/authz/RequiresPermissionAuthorizationManagerTest.java 141
|
|
94e24a09f847acbe8018ca304d91ba31ba891d50 test/java/dev/caskeleton/adapter/inbound/web/authz/RolePermissionPolicyTest.java 69
|
|
e20d02e10f8d2395d0a3ac57a9c47e1005b23570 test/java/dev/caskeleton/adapter/inbound/web/authz/RolePermissionRegistryTest.java 70
|
|
e8d4107c1e66140e2d16e773278f033a26c2a4c6 test/java/dev/caskeleton/adapter/inbound/web/security/WebCorsCsrfPolicyTest.java 176
|
|
80279cedb7a4d2b0cc4cdee0aaac7607a4d3c684 test/java/dev/caskeleton/adapter/inbound/web/security/WebSecurityContextBridgeTest.java 106
|
|
file_count=44
|
|
|
|
=== 8.1 도달성: 신원 모델이 프로덕션에서 참조되는가 ===
|
|
WebSecurityContextBridge : main_refs=0 test_refs=1
|
|
WebActorContextResolver : main_refs=1 test_refs=0
|
|
WebTenantContextResolver : main_refs=1 test_refs=1
|
|
AuthenticationView : main_refs=3 test_refs=1
|
|
SecurityIdentity : main_refs=1 test_refs=1
|
|
rejectTenantInput : main_refs=2 test_refs=1
|
|
WebCorsPolicyValidator : main_refs=0 test_refs=1
|
|
WebCsrfPolicyResolver : main_refs=0 test_refs=1
|
|
|
|
$ grep -rn 'new AuthenticationView(\|AuthenticationView\.\(authenticated\|anonymous\)' --include=*.java adapter/inbound/web/src | grep -v 'security/AuthenticationView.java' | sed 's|adapter/inbound/web/src/||'
|
|
test/java/dev/caskeleton/adapter/inbound/web/security/WebSecurityContextBridgeTest.java:27: AuthenticationView.authenticated("actor-1", Set.of("ROLE_USER"), "tenant-authenticated");
|
|
test/java/dev/caskeleton/adapter/inbound/web/security/WebSecurityContextBridgeTest.java:42: bridge.resolve(AuthenticationView.anonymous(), Map.of("tenant", "tenant-attacker")))
|
|
test/java/dev/caskeleton/adapter/inbound/web/security/WebSecurityContextBridgeTest.java:64: SecurityIdentity identity = bridge.resolve(AuthenticationView.anonymous());
|
|
test/java/dev/caskeleton/adapter/inbound/web/security/WebSecurityContextBridgeTest.java:75: bridge.resolve(AuthenticationView.authenticated("actor-1", Set.of("ROLE_ADMIN")));
|
|
test/java/dev/caskeleton/adapter/inbound/web/security/WebSecurityContextBridgeTest.java:90: new AuthenticationView(
|
|
exit=0
|
|
|
|
=== 8.2 두 전송의 WebRequestContext 생산자 ===
|
|
$ grep -rn 'new WebRequestContext(' --include=*.java adapter/inbound/web/src app-bootstrap/src | sed 's|adapter/inbound/web/src/||'
|
|
test/java/dev/caskeleton/adapter/inbound/web/core/WebRequestContextTest.java:100: return new WebRequestContext(
|
|
test/java/dev/caskeleton/adapter/inbound/web/mvc/idempotency/WebMvcIdempotentInvokerTest.java:237: return new WebRequestContext(
|
|
test/java/dev/caskeleton/adapter/inbound/web/webflux/idempotency/WebFluxIdempotentInvokerTest.java:212: return new WebRequestContext(
|
|
test/java/dev/caskeleton/adapter/inbound/web/webflux/context/WebFluxRequestContextAccessorTest.java:81: return new WebRequestContext(
|
|
test/java/dev/caskeleton/adapter/inbound/web/operationasync/OperationAccessPolicyTest.java:54: new WebRequestContext(
|
|
test/java/dev/caskeleton/adapter/inbound/web/operationasync/OperationAccessPolicyTest.java:110: return new WebRequestContext(
|
|
main/java/dev/caskeleton/adapter/inbound/web/webflux/context/WebFluxRequestContextFilter.java:81: new WebRequestContext(
|
|
testkit/java/dev/caskeleton/adapter/inbound/web/testkit/fault/FaultFixtureController.java:143: return new WebRequestContext(
|
|
testkit/java/dev/caskeleton/adapter/inbound/web/testkit/fault/ReactiveFaultFixtureController.java:141: return new WebRequestContext(
|
|
testkit/java/dev/caskeleton/adapter/inbound/web/testkit/throttle/ThrottleFixtureSupport.java:63: return new WebRequestContext(
|
|
testkit/java/dev/caskeleton/adapter/inbound/web/testkit/operation/OperationFixtureSupport.java:123: return new WebRequestContext(
|
|
exit=0
|
|
|
|
$ sed -n '77,90p' adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/webflux/context/WebFluxRequestContextFilter.java
|
|
ServerHttpRequest request = exchange.getRequest();
|
|
Instant receivedAt = clock.instant();
|
|
WebRequestId requestId = resolveRequestId(request);
|
|
WebRequestContext context =
|
|
new WebRequestContext(
|
|
requestId,
|
|
resolveTraceId(request),
|
|
operationName(request),
|
|
new ApiMajorVersion(1),
|
|
ActorContext.anonymous(),
|
|
TenantContext.none(),
|
|
Locale.ENGLISH,
|
|
receivedAt,
|
|
receivedAt.plus(requestBudget),
|
|
exit=0
|
|
|
|
$ grep -rn 'WebMvcRequestContextHolder.store' --include=*.java . | wc -l
|
|
0
|
|
exit=0
|
|
|
|
$ grep -rn 'WebMvcRequestContextHolder' --include=*.java adapter/inbound/web/src app-bootstrap/src | grep -v 'WebMvcRequestContextHolder.java' | sed 's|adapter/inbound/web/src/||'
|
|
main/java/dev/caskeleton/adapter/inbound/web/mvc/context/WebMvcRequestContextArgumentResolver.java:40: return WebMvcRequestContextHolder.require(request);
|
|
exit=0
|
|
|
|
$ grep -n 'require(request)\|orElseThrow' adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/mvc/context/WebMvcRequestContextHolder.java adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/mvc/context/WebMvcRequestContextArgumentResolver.java
|
|
adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/mvc/context/WebMvcRequestContextHolder.java:60: .orElseThrow(
|
|
adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/mvc/context/WebMvcRequestContextArgumentResolver.java:40: return WebMvcRequestContextHolder.require(request);
|
|
exit=0
|
|
|
|
$ grep -n 'addArgumentResolvers' -A 4 adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/mvc/autoconfigure/WebMvcPlatformAutoConfiguration.java
|
|
163: public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
|
|
164- resolvers.add(new WebMvcRequestContextArgumentResolver());
|
|
165- }
|
|
166- };
|
|
167- }
|
|
exit=0
|
|
|
|
=== 8.3 필터 체인 순서: publicPaths 대 RestrictedPathRule ===
|
|
$ sed -n '83,94p' adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/auth/SecurityConfig.java
|
|
.authorizeHttpRequests(
|
|
auth -> {
|
|
if (publicPaths.length > 0) {
|
|
auth.requestMatchers(publicPaths).permitAll();
|
|
}
|
|
// Ordered before the authenticated catch-all: a management path must be refused at
|
|
// the transport, not by an application policy the request has already passed.
|
|
for (RestrictedPathRule rule : restricted) {
|
|
auth.requestMatchers(rule.pathPattern()).hasAnyAuthority(rule.authorities());
|
|
}
|
|
auth.anyRequest().authenticated();
|
|
})
|
|
exit=0
|
|
|
|
$ grep -rn 'new RestrictedPathRule(' --include=*.java app-bootstrap/src/main adapter
|
|
app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/fileserver/FileserverAdminPlaneConfiguration.java:36: return new RestrictedPathRule(ADMIN_PATH_PATTERN, settings.security().adminRoles());
|
|
exit=0
|
|
|
|
$ grep -rn 'public-paths' app-bootstrap/src/main/resources/application.yml
|
|
694: public-paths: ${SECURITY_PUBLIC_PATHS:${PRESENTATION_API_BASE_PATH:/v1}/healthcheck}
|
|
exit=0
|
|
|
|
=== 8.4 mayAccess 가 익명 액터에 대해 무엇을 하는가 ===
|
|
$ sed -n '28,41p' adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/operationasync/OperationAccessPolicy.java
|
|
public static boolean mayAccess(DurableOperation operation, WebRequestContext context) {
|
|
Objects.requireNonNull(operation, "operation");
|
|
Objects.requireNonNull(context, "context");
|
|
if (!context.actor().authenticated()) {
|
|
return false;
|
|
}
|
|
if (!operation.principal().equals(context.actor().subject())) {
|
|
return false;
|
|
}
|
|
String tenant = operation.tenantId();
|
|
return tenant == null
|
|
? context.tenant().value().isEmpty()
|
|
: context.tenant().value().filter(tenant::equals).isPresent();
|
|
}
|
|
exit=0
|
|
|