Files
document-haness/docs/clean-architecture-backend-template/final/evidence/raw/191-inbound-web-security-probes.txt
T
DongHyeonkaandClaude Opus 5 b2963105a8 docs(keycloak-session-store): import the session-storage lab as a new project
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>
2026-09-04 22:51:59 +09:00

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