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 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