Files
llm-wiki/raw/branch-notes/feature-authentication-authorization-contract.md
T

57 KiB

title, source_type, status, branch, parent_branch, related_projects, governing_docs, tags, created, target_merge, status_label, last_implementation, id, kind, project, work_item, inherits, refines, overrides, depends_on, contract_packet, contract_packet_sha256
title source_type status branch parent_branch related_projects governing_docs tags created target_merge status_label last_implementation id kind project work_item inherits refines overrides depends_on contract_packet contract_packet_sha256
branch / feature-authentication-authorization-contract branch-note raw feature-authentication-authorization-contract
ca-skeleton
wiki/projects/ca-tmpl/security-baseline-jwt-actuator-secrets.md
branch
ca-skeleton
security
authorization
authz
rbac
2026-06-08 review 2026-06-08 BR-CA-SKELETON-OPERATIONAL-CONTRACT-048 project-work-item ca-skeleton-operational-contract WI-CA-SKELETON-OPERATIONAL-CONTRACT-048
DEC-CA-SKELETON-OPERATIONAL-CONTRACT-MODULE-LAYOUT-001@1
DEC-CA-SKELETON-OPERATIONAL-CONTRACT-ERROR-ENVELOPE-001@1
DEC-CA-SKELETON-OPERATIONAL-CONTRACT-STACK-FRAMEWORK-001@1
DEC-CA-SKELETON-OPERATIONAL-CONTRACT-STACK-ARCHTEST-001@1
WI-CA-SKELETON-OPERATIONAL-CONTRACT-008
WI-CA-SKELETON-OPERATIONAL-CONTRACT-005
WI-CA-SKELETON-OPERATIONAL-CONTRACT-018
WI-CA-SKELETON-OPERATIONAL-CONTRACT-014
WI-CA-SKELETON-OPERATIONAL-CONTRACT-022
WI-CA-SKELETON-OPERATIONAL-CONTRACT-001
1 01bf98ba6718be772bdd1b8ffac611c2fc2f9ce395fa052a2ed2b40376847b14

2026-06-08 구현 완료 (working tree, 미커밋) — 본 노트 설계대로 ca-tmpl src/ 에 authz 계약 구현됨(코드 javadoc 이 D1/D2/D3/D4/§3 인용). 등급·발견은 §Audit & Findings 참조. 노트 정정: §2 의 ArchUnit rule 을 "REFERENCE ONLY / 미구현(host=architecture-enforcement-rules)" 로 적었으나, 실제로는 architecture-enforcement suite(app-bootstrap/.../CleanArchitectureTest)에 D4 rule 로 구현됨 — 위임 설계대로 producer=본 branch / host=suite 가 실현됨.

branch: feature-authentication-authorization-contract

Layer: raw/branch-notes/product API 인가(authorization) 계약: 인증된 principal 이 무엇을 할 수 있는가 를 결정하는 enforcement point(PEP) + permission/role 모델 + use-case 단위 권한 선언. 인증(authN)·JWT 검증·401/403 분류는 sibling feature-security-operational-baseline 가 owns(중복 금지). 머지 후 verified 결과는 /ingestwiki/projects/에 추출. status_label: in-progress | review | merged | abandoned

부모 (필수)

이 branch 는 project 의 직접 자식(parent_branch: 비어있음). ca-skeleton 은 별도 root branch 없이 project-note 가 SSOT.

  • Parent project (canonical SSOT): raw/project-notes/ca-skeleton-operational-contract — §35 D/E #5 ("AuthN/AuthZ product API baseline — JWT/OAuth2 RS + RBAC/ABAC + endpoint auth annotation") 가 본 branch 신설 근거. project §5(presentation/application/domain exception ownership)·§6(AUTHZ category)·§10(repository capability — 별개 축)·§11 Security 가 관련 영역.

선택 (형제 — 직접 의존):

브랜치 계약 패킷

  • 생성 시 프로젝트 개정: 1
  • 패킷 스키마: contract_packet: 1
  • 완료 조건: authentication·authorization ownership, error mapping, forbidden dependency test가 명시된다

상속한 프로젝트 결정

Decision Ref Project Summary Branch Application Source
DEC-CA-SKELETON-OPERATIONAL-CONTRACT-MODULE-LAYOUT-001@1 Gradle multi-module에서 domain-core·application-core·adapter-*·shared-contract·app-bootstrap·sample-portfolio 책임을 분리한다 Work Item 완료 조건에 적용 raw/project-notes/ca-skeleton-operational-contract
DEC-CA-SKELETON-OPERATIONAL-CONTRACT-ERROR-ENVELOPE-001@1 foundation이 envelope schema와 error.category enum의 단일 owner다 Work Item 완료 조건에 적용 raw/project-notes/ca-skeleton-operational-contract
DEC-CA-SKELETON-OPERATIONAL-CONTRACT-STACK-FRAMEWORK-001@1 framework는 Spring Boot 3.5.14다 Work Item 완료 조건에 적용 raw/project-notes/ca-skeleton-operational-contract
DEC-CA-SKELETON-OPERATIONAL-CONTRACT-STACK-ARCHTEST-001@1 architecture test는 archunit-junit5 1.3.0을 사용한다 Work Item 완료 조건에 적용 raw/project-notes/ca-skeleton-operational-contract

브랜치 지역 결정

기존 branch-local 결정은 아래 ## Decision Evidence Map / 결정-근거 매핑의 D-row가 소유하며 이 packet에서 복제하지 않는다.

Decision ID Decision Relation Supporting Claims Status

선언한 예외

Override ID Overrides Reason Approval Status

목표

인증(authN)은 너는 누구인가, 인가(authZ)는 너가 이 작업을 할 권한이 있는가 다 (OWASP-AUTHZ-C3). feature-security-operational-baseline 은 JWT 를 검증하고 claim 을 AuthenticatedUser.roles의 raw role로 보존하며 Spring 경계에서 ROLE_* authority를 파생하고, 권한 부족을 AUTHZ_INSUFFICIENT_PERMISSION 403 으로 분류 까지 하지만, 무엇이 그 403 을 발생시킬지(실제 authz 결정) 를 정의하지 않는다. ca-tmpl src/ 에는 method/endpoint 단위 authorization 이 전무하다 — @PreAuthorize/@EnableMethodSecurity/AuthorizationManager 0건, SecurityConfig.authenticated() (인증만 하면 누구나 통과) 뿐. 즉 AUTHZ_INSUFFICIENT_PERMISSION code 는 registry 에 등록돼 있으나 아무도 emit 하지 않는다.

본 branch 는 그 빈 자리를 채운다: 인증된 principal 의 권한을 use-case 단위로 검증하는 enforcement point + permission 중심 RBAC 모델 + 확장점. ca-tmpl 은 도메인 없는 skeleton 이므로 concrete 비즈니스 role 은 정의하지 않고, 계약 + infrastructure + sample-portfolio 시연 만 둔다.

  • 이슈:
  • PR:

범위

포함 범위

  • Enforcement point: use-case AuthorizationPort / @RequiresPermission 추상화 (application-core). Spring Security 를 application/domain layer 밖에 유지.
  • Permission 중심 RBAC 모델: permission = 집행 단위(resource:action), role = permission 묶음. role→permission 해소.
  • @RequiresPermission 선언 의무 + ArchUnit 집행 (rule host = architecture-enforcement-rules suite — REFERENCE ONLY).
  • 실패 → AUTHZ_INSUFFICIENT_PERMISSION 403 emission (code SSOT = security-baseline; 본 branch 는 emission point producer).
  • 3-tier access model: public ⊂ authenticated ⊂ authorized(permission).
  • sample-portfolio authz 시연 (worklog read/write/close permission).

제외 범위

의도적 제외 — sibling owner 영역. 면접 시 "이건 다른 계약 소관" 근거.

  • JWT 검증 / JWKS / clock skew / claim→principal·Spring authority 매핑 / CORS / 401·403 분류 matrixfeature-security-operational-baseline owns. 본 branch 는 그 출력 중 prefix 없는 raw role set만 consume 한다.
  • @UseCaseCapability (repository infra-capability)feature-repository-access-permission-contract owns. 사용자 권한과 혼동 금지(그 branch out-of-scope 에 "runtime authorization 혼동" 명시).
  • cross-tenant authz (AUTHZ_TENANT_MISMATCH)feature-tenant-context-policy owns. 본 branch 는 ABAC 확장점만 언급.
  • OAuth2 authorization server / token 발급 flow / IdP(Keycloak) realm 설정 → IdP-side. 본 branch 는 resource-server 측 authz 결정만.
  • concrete 비즈니스 role/permission 값 (도메인 영역). sample-portfolio 시연 외 실제 role 정의 안 함.
  • ArchUnit rule suite 자체feature-architecture-enforcement-rules host. 본 branch 는 rule producer.

근거 (필수, 최소 1개+)

본 branch 결정 근거. company-tech-blog 증거는 company-case-study 로 표기(공식 best practice 승격 금지).

Source 정당화하는 결정
raw/official-docs/security-authorization-cheatsheet-owasp D1(server-side enforcement·always-decide), D2(least-privilege H+V), D5(authn/authz distinct→403), D9(deny-by-default). OWASP-AUTHZ-C1~C6
raw/project-notes/ca-skeleton-operational-contract D1(§5 domain/application Spring 모름 + §14/§25 TransactionPort 추상화 선례), D4(§10 + capabilities.yaml ArchUnit 집행 패턴), D5(§6 AUTHZ category), D8(§17·§22 sample-portfolio)
raw/branch-notes/feature-security-operational-baseline D3 입력 seam: JwtToAuthenticatedUserConverter가 raw role principal과 Spring ROLE_* authority를 분리해 제공; D5: AUTHZ_INSUFFICIENT_PERMISSION code + EnvelopeAccessDeniedHandler
raw/official-docs/keycloak-identity-provider-mappers D3 role 출처(IdP realm/client role → claim) — official-vendor-doc(단 realm-role→permission 직접 발급 아님; app-side 매핑 보강 근거)
raw/official-docs/spring-security-authorization-architecture D1 — custom AuthorizationManager<MethodInvocation> 가 Java-level 집행 가능(SS-AUTHZ-ARCH-C3), @PreAuthorize 는 Spring-managed bean coupling 요구(SS-AUTHZ-ARCH-C2) → application-core 부적합. official-vendor-doc
raw/official-docs/owasp-authz-permission-model-abac-rbac D2 — permission-as-string abstraction(OWASP-PM-C3) + role→permission indirect(OWASP-PM-C4) + least-privilege H+V(OWASP-PM-C5). 반례: OWASP 는 ABAC generally prefer(OWASP-PM-C1) → trade-off 명시. official-reference
raw/official-docs/aws-iam-google-iam-permission-naming-convention D6 — resource:action colon separator 가 AWS IAM service:Action(IAM-NAMING-C1) 관행과 일관, Google 3-segment dotted(IAM-NAMING-C2)는 단일 서비스 과도. official-vendor-doc
raw/company-tech-blogs/curity-oauth2-scope-vs-permission-naming D6 — scope=entry-point vs internal permission 분리 + resource:action colon naming(CURITY-SCOPE-C2). company-case-study(AWS IAM 으로 corroborate, 단독 승격 금지)
raw/official-docs/keycloak-authorization-services-realm-client-roles D3 — KC Authorization Services(UMA) 대안은 본 branch scope 밖(KC-AUTHZ-C1/C4, official-vendor-doc). realm/client role 의 JWT claim 구조(KC-AUTHZ-C2)는 engineering-blog 수준 → needs-confirmation(1차 근거는 ca-tmpl 코드)

TODO

각 항목 옆 증거 등급: actually-implemented | locally-verified | prod-verified | documented-only | planned | needs-confirmation

  • AuthorizationPort + @RequiresPermission (application-core) 정의 — 등급: locally-verified (2026-06-08 구현. AuthorizationPort.requirePermission(AuthorizationPrincipal, Permission) + AuthorizationPrincipal/AuthorizationDeniedException/@RequiresPermission 전부 Spring-Security-free. Permission record 는 shared-contract. AuthorizationContractTest/PermissionTest 통과)
  • role→permission 해소 adapter (raw role → effective permissions) — 등급: locally-verified (2026-06-08 RolePermissionRegistry(case-insensitive, fail-closed, wildcard 미지원=§3 기본 B) + RolePermissionProperties(ca-skeleton.authz.role-permissions, key=raw role) + AuthorizationAdapter implements AuthorizationPort. RolePermissionRegistryTest/AuthorizationAdapterTest/RolePermissionPropertiesTest(binding) 통과)
  • @RequiresPermission 미선언 mutating use case ArchUnit rule — 등급: locally-verified (2026-06-08 사용자 요청으로 F4/REFERENCE ONLY 위임을 해제하고 host suite(app-bootstrap/.../CleanArchitectureTest)에 직접 구현. 2 rule: mutating_use_cases_declare_required_permission(=@UseCaseCapability(WRITE_REPOSITORY) 인데 @RequiresPermission 미선언이면 build fail — non-vacuous 검증: DeleteWorkLogUseCase 어노테이션 제거 시 정확히 이 rule 만 FAILED 확인 후 복원) + application_and_domain_do_not_depend_on_spring_security(D1 import 금지). producer=본 branch / host=suite 위임이 실현됨)
  • AuthorizationPort 거부 → AUTHZ_INSUFFICIENT_PERMISSION 403 wiring — 등급: locally-verified (2026-06-08 RequiresPermissionAuthorizationManager(AuthorizationManager<MethodInvocation>) + MethodSecurityConfig(@EnableMethodSecurity(prePostEnabled=false) + ROLE_INFRASTRUCTURE Advisor). 거부 → AuthorizationDecision(false) → Spring AccessDeniedExceptionGlobalExceptionHandler#handleForbiddenSecurityErrorClassifier.classifyAccessDeniedAUTHZ_INSUFFICIENT_PERMISSION. GlobalExceptionHandlerTest/RequiresPermissionAuthorizationManagerTest 통과. filter 경로(EnvelopeAccessDeniedHandler)와 method 경로(controller-advice) 가 이제 동일 classifier 사용)
  • permission naming registry + sample-portfolio authz 시연 — 등급: locally-verified (2026-06-08 worklog:read/write/close + role bundle(user={read,write}, admin={read,write,close}) application.yml. mutating use case 4종에 @RequiresPermission 부착(Create/Update/Batch=worklog:write, Delete=worklog:close=admin-tier). WorkLogAuthorizationContractTest(@SpringBootTest, 실제 AOP proxy 경유) 4 cases 통과: user→write 허용 / user→close 거부 / admin→close 허용 / unauth→거부)
  • negative E2E (HTTP→method security→403 envelope 전 경로) — 등급: locally-verified (2026-06-08 사용자 요청. WorkLogAuthorizationE2ETest(@WebMvcTest, 실 WorkLogController DELETE 엔드포인트): user → 403 + envelope error.code=AUTHZ_INSUFFICIENT_PERMISSION(repository.deleteById 미호출 검증), admin → 204(deleteById 호출). MVC dispatch→proxied use case method-security→AccessDeniedException→GlobalExceptionHandler→envelope 전 경로 검증)
  • 자동조사(D1/D2/D3/D6) Supporting Claim 연결 — 등급: actually-implemented (2026-06-08 wiki-decision-researcher 5 raw 산출 + 연결 완료)

진행 중 메모

  • 잔존 저위험 (2026-06-08, 의도적 미해소):
    1. authN→authz seam 미통합 검증: WorkLogAuthorizationE2ETest/WorkLogAuthorizationContractTest 는 둘 다 addFilters=false(+SecurityAutoConfiguration 제외)로 JWT 필터체인을 끄고 AuthenticatedUser 를 SecurityContext 에 직접 주입한다. 따라서 인가 leg(method-security→403 envelope)는 닫혔으나, JWT → SecurityFilterChain → JwtToAuthenticatedUserConverter → AuthenticatedUser.roles → registry lookup seam 은 authz 와 묶여 한 번에 검증되지 않음(security-baseline 단위검증에 의존). 닫으려면 full @SpringBootTest(mock JWT 발급 + 실 필터체인 + method security) 필요 — ~50 env 의존으로 별도 작업.
    2. proxy-target-class flip 미가드: E2E/contract test 둘 다 자기 컨텍스트에 @EnableAspectJAutoProxy(proxyTargetClass=true) 를 강제하므로, prod 에서 spring.aop.proxy-target-class=false 로 바꾸면 테스트는 통과하면서 prod 만 깨진다(concrete *UseCase 주입이 JDK proxy 로 fallback → BeanNotOfRequiredTypeException). 즉 테스트가 이 flip 을 잡지 못함 = 가드 없음(저위험). prod 는 Boot 기본(CGLIB)이라 현재 안전. → raw/errors/method-security-cglib-vs-jdk-proxy-usecase-injection-2026-06-08
  • security-baseline 은 2026-06-08 Phase C2 로 authN 전 영역 locally-verified 까지 구현됨. 본 branch 는 그 위에 authz 결정 만 얹으므로, JwtToAuthenticatedUserConverter(realm_access+resource_access → AuthenticatedUser raw roles + adapter ROLE_* authorities)·AuthenticatedUser·EnvelopeAccessDeniedHandler 가 본 branch 구현의 전제 anchor.
  • code SSOT 위임 (coverage audit Should-fix 해소): AUTHZ_INSUFFICIENT_PERMISSION·AUTHZ_TENANT_MISMATCHerror-codes.yamlowner_branch = feature-security-operational-baseline 로 등록(2026-06-08 코드 확인). 본 branch 는 code 를 새로 만들지 않고 emission point(실제 발생원)만 추가 — code registry SSOT = raw/branch-notes/feature-security-operational-baseline, emission producer = 본 branch.
  • TODO (project note 갱신 — §25 SSOT Owner Map row 부재, coverage audit Should-fix): project ca-skeleton-operational-contract §25 SSOT Owner Map 에 신규 row 추가 필요 — | product authorization enforcement point (PEP) | feature-authentication-authorization-contract | security-operational-baseline(ROLE_* authority consumer + AUTHZ code SSOT), architecture-enforcement-rules(rule host), sample-domain-contract-fixture(authz fixture) | AuthorizationPort + @RequiresPermission SSOT |. §35 D/E #5 의 (없음) → scaffolded 로 상태 갱신도 동반. (project note 편집은 본 branch-spec 범위 밖 — 별도 작업으로 처리.)

결정 사항

대안과 함께 기록. 각 결정 근거는 위 Sources. 상세 매핑은 아래 Decision Evidence Map.

  • 2026-06-08: D1 enforcement layer = use-case AuthorizationPort(application-core), Spring @PreAuthorize 아님 / 이유: application·domain 이 Spring Security type 을 import 하면 project §5·§19 원칙 위반(TransactionPort 선례 §14·§25); custom AuthorizationManager<MethodInvocation> 가 Java-level 집행 제공(SS-AUTHZ-ARCH-C3) / 대안: Spring method security @PreAuthorize(SS-AUTHZ-ARCH-C2 = bean coupling → 위반), web-layer authorizeHttpRequests coarse 규칙 / 근거: SS-AUTHZ-ARCH-C2/C3/C4 + project-ssot + OWASP-AUTHZ-C6 / 위험: AOP proxy bypass → ArchUnit 보강.
  • 2026-06-08: D2 authz 모델 = permission 중심 RBAC(permission=집행 단위, role=permission 묶음) / 이유: 도메인이 role 추가해도 enforcement 코드 불변 + least-privilege(H+V, OWASP-PM-C5) + action→permission string abstraction(OWASP-PM-C3) / 대안: role-only RBAC, ABAC(OWASP-PM-C1 = ABAC generally prefer — 정적 permission 규모 YAGNI 로 trade-off, AuthorizationPort interface 가 ABAC migration path 보장) / 근거: OWASP-PM-C3/C4/C5.
  • 2026-06-08: D3 AuthorizationPort 는 현재 principal 의 prefix 없는 raw role을 role→permission registry key로 사용한다. Spring ROLE_* authority는 adapter가 파생하는 표현이며 registry 입력이 아니다. 매핑 source = app-side static config 기본, IdP 가 permission claim 직접 발급 시 그것 우선 / 대안: IdP-authoritative only(Keycloak Authorization Services/UMA — KC-AUTHZ-C1/C4, 본 branch scope 밖), JWT scope claim only / 근거: security-baseline JwtToAuthenticatedUserConverter + KC-AUTHZ-C2/C3(needs-confirmation).
  • 2026-06-08: D4 mutating/sensitive use case 는 @RequiresPermission 선언 의무, ArchUnit 으로 미선언 차단(repository-access @UseCaseCapability 패턴 mirror) / 대안: compile-time annotation processor, runtime AOP(capabilities.yaml 정책상 forbidden) / 근거: project §10 + capabilities.yaml(enforcement=archunit, runtime AOP forbidden). rule host = architecture-enforcement-rules suite(REFERENCE ONLY).
  • 2026-06-08: D5 AuthorizationPort 거부 → AUTHZ_INSUFFICIENT_PERMISSION(AUTHZ 403). code SSOT = security-baseline(registry owner) / 본 branch 는 emission point producer. IDOR-sensitive 도메인은 403→404 masking 확장점(OWASP-AUTHZ-C7) / 근거: OWASP-AUTHZ-C3 + security-baseline matrix.
  • 2026-06-08: D6 permission naming = resource:action lowercase colon-delimited (예: worklog:close) / 대안: service.resource.verb(Google IAM), service:Action(AWS IAM), OAuth2 scope / 근거: 자동조사(진행 중) + OWASP-AUTHZ-C4. exact delimiter 는 근거 미확정 시 UNSUPPORTED_IMPL_DECISION.
  • 2026-06-08: D8 sample-portfolio 가 authz 시연 fixture(worklog:read/write/close, ROLE_USER/ROLE_ADMIN). sample model owner = sample-fixture branch(본 branch 는 authz 부착 producer) / 근거: project §17·§22.
  • 2026-06-08: D9 3-tier: public(permitAll) ⊂ authenticated ⊂ authorized(permission). tier1-2 = security-baseline(deny-by-default), tier3 = 본 branch(authenticated≠authorized) / 근거: OWASP-AUTHZ-C1/C2 + security-baseline D5/D6.

결정-근거 매핑

Decision ID 는 본 note 안에서 안정 유지. Supporting Claimsraw/<category>/<slug>.md#C1 형식. 선택 조건(R2): 이 조건일 때 이 결정, 다른 조건이면 어떤 대안.

Decision ID Decision 선택 조건 (언제 이 결정 / 언제 대안) Supporting Claims Evidence Strength Open Risk
D1 enforcement = use-case AuthorizationPort(application-core), Spring method security 아님 기본 = application port. application/domain 이 Spring Security type import 하면 안 됨(project 원칙) → port. coarse endpoint gating 만 필요하면 web-layer authorizeHttpRequests(security-baseline). 표준 단순성이 원칙보다 우선이면 @PreAuthorize raw/official-docs/spring-security-authorization-architecture.md#SS-AUTHZ-ARCH-C3(custom AuthorizationManager<MethodInvocation> = Java-level 집행), #SS-AUTHZ-ARCH-C2(@PreAuthorize = Spring bean coupling → 부적합), #SS-AUTHZ-ARCH-C4(rule 위치 trade-off), raw/official-docs/security-authorization-cheatsheet-owasp.md#OWASP-AUTHZ-C6, #OWASP-AUTHZ-C1, raw/project-notes/ca-skeleton-operational-contract.md(§5 layer 격리 + §14/§25 TransactionPort 선례) official-vendor-doc + project-ssot + official-reference AOP proxy bypass(self-invocation / non-Spring-bean 호출)에 취약(SS-AUTHZ-ARCH-C2) → ArchUnit 이 미보호 진입점 정적 차단 필요; @PreAuthorize 대비 boilerplate ↑
D2 authz 모델 = permission 중심 RBAC (permission=집행 단위, role=묶음) 기본 = permission-centric RBAC. owner/relationship 기반(예: worklog.owner==principal) 필요 도메인 → AuthorizationPort 구현체가 ABAC predicate 추가(거부 아님; interface 가 migration path 보장). role-only 는 도메인 role 추가 시 enforcement 수정 → 기각 raw/official-docs/owasp-authz-permission-model-abac-rbac.md#OWASP-PM-C3(action→permission string abstraction), #OWASP-PM-C4(role=permission bundle indirect), #OWASP-PM-C5(least-privilege H+V), raw/official-docs/security-authorization-cheatsheet-owasp.md#OWASP-AUTHZ-C4 official-reference (OWASP) OWASP 는 ABAC/ReBAC generally prefer(OWASP-PM-C1) — permission-centric RBAC 는 정적 permission+소수 role 규모에서 YAGNI 근거의 단순성 trade-off; dynamic attribute(시간/지리/owner) 요구 시 ABAC 전환
D3 AuthorizationPort 가 prefix 없는 raw role → role→permission registry 확장 → 요구 permission 포함 판정. ROLE_* authority는 Spring adapter의 파생 표현 기본 = app-side static config(IdP coupling 최소). IdP(Keycloak)가 permission claim 직접 발급하면 IdP-authoritative 우선. JWT scope claim 만으로 부족하면 registry 확장 raw/branch-notes/feature-security-operational-baseline — principal mapping seam을 consume; raw/official-docs/keycloak-authorization-services-realm-client-roles.md#KC-AUTHZ-C2(realm/client role JWT claim 구조 — engineering-blog 수준), #KC-AUTHZ-C3, raw/official-docs/keycloak-identity-provider-mappers.md#KC-IDP-MAPPER-C2 cross-branch (security-baseline JwtToAuthenticatedUserConverter code = locally-verified, 1차 근거) + engineering-blog (KC-AUTHZ-C2, needs-confirmation) role→permission config drift; IdP 권한 변경 시 app config 동기화. KC-AUTHZ-C2 는 engineering 수준 → 메커니즘 1차 근거는 ca-tmpl 코드(realm_access+resource_access 파싱 locally-verified)이고 KC-AUTHZ-C2 는 보조; official Keycloak doc 재확인 needs-confirmation
D4 mutating/sensitive use case @RequiresPermission 선언 의무, ArchUnit 차단 집행: 기본 = ArchUnit(capabilities.yaml 정책 상속), compile-time processor = alt, runtime AOP = forbidden(capabilities.yaml 명시). 적용 범위(depth audit #3 해소): skeleton 1차 = mutating-only(=@UseCaseCapability(WRITE_REPOSITORY) 보유 use case). authenticated read 결과 필터링이 필요해지면 read 강제로 확장(sample-portfolio 구현 후 결정); public read 는 항상 제외 raw/project-notes/ca-skeleton-operational-contract.md(§10 capability 선언 + capabilities.yaml enforcement: archunit / runtime AOP forbidden), §25 F1(ArchUnit suite SSOT=architecture-enforcement), raw/official-docs/security-authorization-cheatsheet-owasp.md#OWASP-AUTHZ-C4(least-privilege — read 무조건 강제는 과대) project-ssot + official-reference rule host = feature-architecture-enforcement-rules(REFERENCE ONLY); read 강제 확장 시점은 sample 구현 후
D5 거부 → AUTHZ_INSUFFICIENT_PERMISSION 403. code SSOT=security-baseline, 본 branch=emission point N/A (code 매핑 고정). 단 IDOR-sensitive 도메인은 403→404 masking 확장점 raw/official-docs/security-authorization-cheatsheet-owasp.md#OWASP-AUTHZ-C3, #OWASP-AUTHZ-C7(IDOR), raw/branch-notes/feature-security-operational-baseline.md(AuthN/AuthZ matrix AUTHZ_INSUFFICIENT_PERMISSION 403 행 + EnvelopeAccessDeniedHandler) official-reference + cross-branch §25 SSOT Owner Map 에 "authorization enforcement point" row 추가 필요(producer/consumer 명시)
D6 permission naming = resource:action(lowercase, colon) 기본 = resource:action(2-segment, 단일 서비스). multi-service gateway 수준 permission 필요 시 service:resource:action 으로 확장. Google service.resource.verb(dotted)는 Java package 혼동 + service prefix 중복으로 기각 raw/official-docs/aws-iam-google-iam-permission-naming-convention.md#IAM-NAMING-C1(AWS service:Action colon), #IAM-NAMING-C2(Google dotted 3-segment 대안), raw/company-tech-blogs/curity-oauth2-scope-vs-permission-naming.md#CURITY-SCOPE-C2(resource:action industry practice), raw/official-docs/security-authorization-cheatsheet-owasp.md#OWASP-AUTHZ-C4(granularity) official-vendor-doc(AWS IAM) + company-case-study(Curity corroborate) RFC 강제 표준 없음(convention) — team 문서화로 유지; wildcard(worklog:*) 전개 규칙 + permission explosion vs coarse 미정
D8 sample-portfolio authz 시연(worklog:read/write/close, ROLE_USER/ADMIN) N/A (fixture). sample model 변경은 sample-fixture branch raw/project-notes/ca-skeleton-operational-contract.md(§17 sample-portfolio + §22 "unauthorized worklog update auth/authz separation") project-ssot
D9 3-tier: public ⊂ authenticated ⊂ authorized. tier3(authenticated≠authorized) 추가 N/A (계층 고정) raw/official-docs/security-authorization-cheatsheet-owasp.md#OWASP-AUTHZ-C1, #OWASP-AUTHZ-C2, #OWASP-AUTHZ-C5, raw/branch-notes/feature-security-operational-baseline.md(D5 deny-by-default / D6 every-request) official-reference + cross-branch every-request 권한 검증(C5) 비용 — role→permission 해소 caching(stateless 유지 vs staleness)

구현 가이드

결정 이 "무엇" 이라면 본 §는 "어디에 어떻게". 2026-06-08 구현 완료 — 아래 planned 다수가 실제 코드로 실현됨(as-built 등급·파일 anchor 는 §Audit & Findings 의 구현 인벤토리 참조; 본 § 표의 planned설계 시점 표기로 보존). 설계 시점 "ca-tmpl 0건" 기술은 구현 전 상태.

3-rule meta principle(CLAUDE.md §15.5): R1 모든 cell = Decision ID + Supporting Claim / R2 근거 없는 detail = UNSUPPORTED_IMPL_DECISION + trade-off / R3 본 branch 범위 밖 = 별도 §/sibling 이관.

1. AuthorizationPort 계약 (application-core)

Trace: D1(use-case port, OWASP-AUTHZ-C6 + project §5/§14/§25) · D3(role→permission 해소) · D9(tier3).

  • UNSUPPORTED_IMPL_DECISION: port API 모양(requirePermission(Permission) throw vs check(...)→boolean). trade-off: throw 방식 = 호출부 단순 + fail-closed 자연스러움 vs boolean = 분기 유연. 기본 throw(fail-closed).
항목 구현 anchor 등급
port 인터페이스 dev.caskeleton.application.<core>.security.AuthorizationPortvoid requirePermission(Permission required) (application-core, Spring Security import 금지) planned
Permission 표현 domain-core 또는 shared-contractrecord Permission(String resource, String action) (resource:action, D6) planned
현재 principal 접근 adapter 가 SecurityContextAuthenticatedUser(security-baseline dev.caskeleton.adapter.web.auth.AuthenticatedUser) 에서 authorities 추출 → port 입력. application 은 principal 을 주입 받음(Spring 비의존) planned (security-baseline AuthenticatedUser = actually-implemented)
거부 신호 AuthorizationDeniedException(application/domain-neutral) throw → adapter-web 이 403 매핑(§4) planned

2. @RequiresPermission 선언 + ArchUnit 집행 (REFERENCE ONLY — host=architecture-enforcement-rules)

Trace: D4(@UseCaseCapability 패턴 mirror, capabilities.yaml enforcement: archunit / runtime AOP forbidden + project §25 F1).

  • OUT_OF_BRANCH_SCOPE(F4): ArchUnit rule 의 실제 코드 위치 = feature-architecture-enforcement-rules suite. 본 branch 는 rule producer(어떤 규칙이 필요한지 정의), host 아님. 아래 코드 skeleton = REFERENCE ONLY.
  • UNSUPPORTED_IMPL_DECISION: 적용 범위 = mutating/sensitive use case (read-only query 강제 여부 미정 — §Open Risk D4). trade-off: all-use-case 강제 = 누락 0 vs read 마다 permission 선언 boilerplate.
항목 구현 anchor 등급
annotation @RequiresPermission(String value) (value="worklog:close", TYPE 또는 METHOD target — @UseCaseCapability 와 동일 위치 convention). retention=RUNTIME (adapter 의 AuthorizationManager 가 reflect) planned
집행 메커니즘 (adapter, SS-AUTHZ-ARCH-C3) adapter-web 의 RequiresPermissionAuthorizationManager implements AuthorizationManager<MethodInvocation>MethodInvocation 에서 @RequiresPermission 읽어 AuthorizationPort.requirePermission(...) 위임. @EnableMethodSecurity(prePostEnabled=false) + custom Advisor 로 wiring — application-core 는 여전히 Spring-free(annotation 만 보유, 집행은 adapter) planned
ArchUnit rule (구현됨 — host=suite) CleanArchitectureTest.declareRequiredPermissionWhenMutating()(app-bootstrap, L335-358) — mutating use case(@UseCaseCapability(repositoryAccess=WRITE_REPOSITORY))인데 @RequiresPermission 미선언 → build fail. D4 tag. 위임 설계대로 producer=본 branch / host=architecture-enforcement suite actually-implemented
no-Spring-Security-in-application CleanArchitectureTest.application_and_domain_do_not_depend_on_spring_security(L291, "D1") — application/domain 의 org.springframework.security.. import → build fail actually-implemented
AOP proxy bypass (SS-AUTHZ-ARCH-C2 위험, 잔존) 위 D4 rule 은 annotation 존재 만 보장; self-invocation / non-Spring-bean 호출의 invocation-path 우회는 정적으로 미검출. controller→usecase 는 proxy 경유라 현재 안전하나 구조적 잔존 위험 → §Claims To Verify documented-only (gap)

3. role→permission 해소 adapter (D3, D2)

Trace: D3(role→effective permissions) · D2(permission-centric). 입력 = security-baseline AuthenticatedUser.roles.

  • registry key 형식 결정 (depth audit #2 해소): ca-tmpl AuthenticatedUser.roles(adapter-web, Set<String>)는 raw role 문자열(admin/user — Keycloak 원본, prefix 없음)을 담는다. Spring GrantedAuthority"ROLE_"+toUpperCase() prefix 를 받는다(JwtToAuthenticatedUserConverter.java L33-34). 따라서 registry key = raw role 명(prefix 없음)ROLE_ADMIN 아님. application-core 가 Spring-free 이므로 port 는 GrantedAuthority 가 아니라 raw role set 을 consume.
  • UNSUPPORTED_IMPL_DECISION: (1) 매핑 저장소 = app-side @ConfigurationProperties static map(ca-skeleton.authz.role-permissions) 기본 — IdP coupling 최소, env-driven(§9). (2) role 명 case 정규화 — Keycloak raw role 의 대소문자 보장 없음 → registry lookup 을 case-insensitive(lowercase 정규화) 로. trade-off: 정규화(Keycloak 설정 무관 안정) vs exact-match(설정 강제).
  • principal 추상화 (Spring-free): AuthenticatedUseradapter-web 타입 → application-core 가 import 불가. port 는 application-core/shared-contract 의 principal 추상(Set<String> roles + subject)을 받고, adapter 가 AuthenticatedUser→그 추상으로 매핑.
항목 구현 anchor 등급
role→permission registry RolePermissionRegistry(adapter 또는 shared-contract) ← ca-skeleton.authz.role-permissions. key = raw role 명(lowercase): admin: [worklog:read, worklog:write, worklog:close], user: [worklog:read, worklog:write] (← AuthenticatedUser.roles, ROLE_ prefix 없음). 정적 @ConfigurationProperties map = startup-bound → staleness 없음(IdP claim 직접 발급 채택 시에만 별도 TTL 필요) planned
effective permission 확장 AuthenticatedUser.roles(raw) → registry lookup → permission set union. wildcard 전개(worklog:*): UNSUPPORTED_IMPL_DECISION — (A) 정적 prefix-union(registry 등록 worklog: 전체 union, 미래 permission 자동 포함) vs (B) 명시 열거만(wildcard 미지원, admin = 명시 목록). 기본 = (B) 명시 열거(least-privilege OWASP-AUTHZ-C4 우선, worklog:delete 자동 포함 차단) planned
AuthorizationPort 구현체 AuthorizationAdapter implements AuthorizationPort(adapter-web) — effective permissions 에 required 포함 여부, fail-closed(미발견 role → 권한 0) planned

4. 거부 → AUTHZ_INSUFFICIENT_PERMISSION 403 (emission point; code SSOT=security-baseline)

Trace: D5(OWASP-AUTHZ-C3 distinct→403 + security-baseline matrix row). code 자체는 security-baseline error-codes.yaml owner.

  • OUT_OF_BRANCH_SCOPE: error code 정의/registry = security-baseline. 본 branch = 거부 → 403 envelope wiring.
  • 예외 경로 결정 (depth audit #1 해소): application-core 는 Spring-free 이므로 AuthorizationPort 는 Spring AccessDeniedException 을 throw할 수 없다. 따라서 2-hop 경로를 명시: (1) application-core port 가 domain-neutral AuthorizationDeniedException(자체 타입) throw → (2) adapter-web RequiresPermissionAuthorizationManager(Spring-aware)가 이를 Spring org.springframework.security.access.AccessDeniedException 으로 변환(또는 Spring 6.x AuthorizationDeniedException extends AccessDeniedException 사용). method-invocation 시점 throw 이므로 filter-layer EnvelopeAccessDeniedHandler 가 아니라 controller-advice GlobalExceptionHandler#handleForbidden(AccessDeniedException)(L91, actually-implemented) 에 도달.
  • UNSUPPORTED_IMPL_DECISION: handleForbidden 현재는 coarse FORBIDDEN 매핑. fine-grained AUTHZ_INSUFFICIENT_PERMISSION 을 emit 하려면 handleForbiddenSecurityErrorClassifier.classifyAccessDenied(ex)(L60, AccessDeniedException→AUTHZ_INSUFFICIENT_PERMISSION) 에 위임하도록 변경 필요. trade-off: handler 위임 변경(filter/method 양 경로 code 일치) vs coarse FORBIDDEN 수용(변경 0, 분류 손실). 기본 = 위임 변경(분류 일관).
항목 구현 anchor 등급
port 거부 신호 (application-core) AuthorizationDeniedException(application/domain-neutral 자체 타입, Spring 비의존) planned
adapter 변환 (adapter-web) RequiresPermissionAuthorizationManager 가 거부 → Spring AccessDeniedException(or AuthorizationDeniedException extends AccessDeniedException) planned
403 envelope emit GlobalExceptionHandler#handleForbidden(AccessDeniedException)(L91, actually-implemented) → 위임 변경 방법: handler 내 OperationalError.FORBIDDEN 라인을 SecurityErrorClassifier.classifyAccessDenied(ex)(L60) 반환값으로 교체(코드 1줄) → AUTHZ_INSUFFICIENT_PERMISSION. 미교체 시 coarse FORBIDDEN planned (handler 존재, classifier 위임 신규)
IDOR masking 확장점 403↔404 선택은 도메인 결정(OWASP-AUTHZ-C7). skeleton 기본 = 403(정직), masking 은 확장점만 documented-only

5. permission naming + sample-portfolio authz 시연 (D6, D8)

Trace: D6(naming resource:action) · D8(sample fixture, project §17/§22). sample model owner = sample-fixture branch(부착만).

  • naming 확정 (D6): resource:action(2-segment colon) — AWS IAM(IAM-NAMING-C1)+Curity(CURITY-SCOPE-C2) 정합. wildcard 미지원(§3 기본 B).
항목 구현 anchor 등급
permission 값 worklog:read · worklog:write · worklog:close (sample-portfolio) planned
role 묶음 (key=raw role, §3) user → {worklog:read, worklog:write}, admin → {worklog:read, worklog:write, worklog:close} (명시 열거 — wildcard 미사용, §3 기본 B) planned
use case 부착 sample-portfolio CloseWorkLogUseCase 등에 @RequiresPermission("worklog:close") (sample package 격리 — 도메인 role 오인 방지) planned
contract test authenticated+permission 없음 → 403 / 있음 → 200, sample 시연 planned

엣지·실패·의존

R4 캡처. 정상 경로 외 구현 중 부딪힐 실패/엣지/다른 계약 의존.

  • 실패·엣지 경로:
    • authenticated 인데 permission 없음: 401 아님 → AUTHZ_INSUFFICIENT_PERMISSION 403(D5/D9). 인증은 됐으나 인가 실패의 핵심 경로.
    • role→permission config 누락/오타: 미발견 role → fail-closed(권한 0, 403). config drift 시 정당 사용자도 거부 → startup 검증(알려진 role 집합 대조) 권고.
    • @RequiresPermission 미선언 mutating use case: ArchUnit build fail(D4). 누락 = silent 무인가 통과 방지.
    • wildcard 전개(worklog:*): 기본 = 미지원(§3 B 명시 열거) — admin 도 명시 permission 목록. 만약 (A) 정적 prefix-union 채택 시 worklog:* 가 미래 worklog:delete 자동 포함 → least-privilege(OWASP-AUTHZ-C4) 위반 위험. 기본값이 least-privilege 보존.
    • IDOR/BOLA(OWASP-AUTHZ-C7): resource 존재를 403 으로 노출 vs 404 masking. skeleton 기본 403, 도메인 확장점.
    • every-request 해소 비용(OWASP-AUTHZ-C5): role→permission 해소를 매 요청 수행 vs principal 단위 cache — stateless 유지(security-baseline) 와 cache staleness trade-off.
  • 다른 계약 의존:

검증해야 할 주장

공식 문서·사례는 근거지만 내 프로젝트 동작을 자동 보장하지 않는다.

Claim Why uncertain How to verify Status
application-core Spring-free authz RESOLVED: AuthorizationPort/AuthorizationPrincipal/@RequiresPermission 가 application-core 에 Spring-free, CleanArchitectureTest D1 rule(L291)이 import 차단 actually-implemented
registry key = raw role(prefix 없음) RESOLVED: AuthorizationPrincipal(raw roles, "never ROLE_*"), RolePermissionRegistry(lowercase normalize), AuthorizationContractTest actually-implemented
mutating use case @RequiresPermission 강제 RESOLVED: CleanArchitectureTest.declareRequiredPermissionWhenMutating()(L335-358, D4) — 미선언 WRITE_REPOSITORY use case build fail actually-implemented
거부 → AUTHZ_INSUFFICIENT_PERMISSION 403 emit RESOLVED(method-security 경로): GlobalExceptionHandler#handleForbiddenSecurityErrorClassifier.classifyAccessDenied 위임 → fine-grained code. GlobalExceptionHandlerTest actually-implemented
(잔존 #4, narrowed) authN→authz seam(JWT 필터체인) 미통합 검증 HTTP→method-security→403 envelope leg 는 WorkLogAuthorizationE2ETest(@WebMvcTest, 실 controller, positive+negative)로 RESOLVED. 잔존은 그 앞단 seam 뿐: WorkLogAuthorizationE2ETest/WorkLogAuthorizationContractTest 둘 다 addFilters=false(+SecurityAutoConfiguration 제외)로 JWT 필터를 끄고 AuthenticatedUser 직접 주입 → JWT 디코딩→SecurityFilterChain→JwtToAuthenticatedUserConverter→AuthenticatedUser.roles→registry seam 은 security-baseline 단위검증에 의존 full @SpringBootTest(mock JWT 발급 + 실 필터체인 + method security)로 JWT 인증된 요청이 권한 없으면 403 envelope, 있으면 200 — authN→authz 통합 1회 needs-confirmation (저위험; 머지 전 권고)
(잔존 #2) AOP self-invocation/non-bean 우회 D4 rule 은 annotation 존재 만 보장, invocation-path 미검출(SS-AUTHZ-ARCH-C2). controller→usecase 는 proxy 경유라 현재 안전 모든 mutating use case 진입점이 Spring-managed bean 경유인지 정적/통합 검증 추가 planned
(잔존 #8) config drift → 정당 사용자 fail-closed(가용성) RolePermissionProperties startup-bound static. role 명 오타/IdP role 변경 시 정당 사용자도 deny(보안 아닌 가용성). startup 검증(알려진 role 집합 대조) 미구현 startup 시 registry role 집합과 기대 role 대조 검증 추가 planned
permission-centric RBAC 채택이 OWASP "prefer ABAC" 권고(OWASP-PM-C1)에 대한 정당한 trade-off 자동조사 필요근거 확보: OWASP-PM-C3/C4/C5(permission abstraction + least-privilege). ABAC 는 정적 permission 규모에 YAGNI. 단 owner/relationship 기반 도메인 요구 시 재평가 dynamic attribute(시간/owner) 실요구 등장 시 AuthorizationPort 구현체를 ABAC 로 교체(interface 불변) — migration 통합 테스트 needs-confirmation
role→permission 매핑 source(app-config vs IdP claim) 기본값 적정 Keycloak realm/client role 의 JWT claim 위치(KC-AUTHZ-C2)가 engineering 수준 — official 재확인 필요. permission claim 직접 발급(UMA)은 scope 밖 Keycloak official doc 으로 realm_access/resource_access claim 구조 재확인 + IdP realm 설정 확인 needs-confirmation (KC-AUTHZ-C2)

관심사 커버리지 (coverage-auditor 자동 생성 — 있을 때)

/coverage 가 재생성하는 초안 — 손유지 금지. 기준: rules/coverage-gate.md. governing = security 클러스터 doc(인접) + project §35 D/E #5 가 열거하는 product-authz 관심사. 전용 canonical 은 미존재 — 향후 /ingestwiki/projects/ca-tmpl/authorization-rbac-method-security.md 가 추출 대상(현재 governing_docs 는 nearest security doc → coverage-auditor 가 MIS-SCOPED 가능성 Advisory 로 평가).

관심사 상태 owner 심각도 근거
product authorization enforcement point(PEP) covered-here D1, D3 / §구현가이드 1
permission/role 모델(permission-centric RBAC) covered-here D2, D6 / §구현가이드 3·5
use-case 권한 선언 강제(@RequiresPermission) covered-here D4 / §구현가이드 2
AUTHZ_INSUFFICIENT_PERMISSION 403 emission covered-here D5 / §구현가이드 4
3-tier access(authenticated≠authorized) covered-here D9
sample authz 시연 covered-here D8 / §구현가이드 5
JWT authN / ROLE_* 매핑 / 401·403 matrix / CORS delegated raw/branch-notes/feature-security-operational-baseline OK raw/branch-notes/feature-security-operational-baseline
repository infra-capability(@UseCaseCapability) delegated raw/branch-notes/feature-repository-access-permission-contract OK raw/branch-notes/feature-repository-access-permission-contract (out-of-scope: "runtime authorization 혼동")
cross-tenant authz(AUTHZ_TENANT_MISMATCH) delegated raw/branch-notes/feature-tenant-context-policy OK raw/branch-notes/feature-tenant-context-policy
ArchUnit rule suite host covered-here(in host suite) raw/branch-notes/feature-architecture-enforcement-rules OK D4(mutating_use_cases_declare_required_permission)+D1(application_and_domain_do_not_depend_on_spring_security) 가 host suite CleanArchitectureTest 에 실제 구현됨(REFERENCE ONLY 위임 해제). producer=본 branch / host=suite
AUTHZ_INSUFFICIENT_PERMISSION code 정의/registry delegated raw/branch-notes/feature-security-operational-baseline OK code SSOT=security-baseline(error-codes.yaml owner_branch 확인); 본 branch=emission producer. 명시 위임 = §진행 중 메모 "code SSOT 위임"
§25 SSOT Owner Map — product authz PEP row 등록 delegated (project note 갱신 작업) 🟡 Should-fix §25 에 본 branch row 부재 → §진행 중 메모 TODO 로 등록(project note 편집은 별도 작업)
Category enum(AUTHZ) SSOT delegated raw/branch-notes/feature-operational-error-observability-foundation OK raw/branch-notes/feature-operational-error-observability-foundation

마주친 문제

  • 2026-06-08 (resolved): method security 가 use case bean 을 JDK dynamic proxy 로 감싸 concrete-type 주입 실패. @EnableMethodSecurity + custom Advisor 가 @RequiresPermission use case 를 proxy 할 때, isolated test context(@SpringBootTest classes=…, auto-config 없음)에서는 JDK interface proxy 가 생성돼 WorkLogController/test 가 주입하는 concrete *UseCase 타입에 assign 불가 → BeanNotOfRequiredTypeException. 원인: Spring Boot 의 AopAutoConfiguration 이 prod 에서 spring.aop.proxy-target-class=true(CGLIB) 를 기본 설정하지만, auto-config 없는 슬라이스엔 그 기본이 안 적용됨. 해소: contract test 의 nested config 에 @EnableAspectJAutoProxy(proxyTargetClass = true) 추가(prod 동작 mirror). prod 는 CaSkeletonApplication 의 @SpringBootApplication 이 CGLIB 보장하므로 영향 없음. → 자세히 raw/errors/method-security-cglib-vs-jdk-proxy-usecase-injection-2026-06-08
  • 2026-06-08 (clarified): unauthenticated 호출은 method-security 단에서 AccessDeniedException 이 아니라 AuthenticationException(AuthenticationCredentialsNotFoundException). method-security 의 deferred Supplier<Authentication>.get() 이 null authentication 을 만나면 401-family 예외를 던진다(403 아님). prod 에서는 filter chain(.anyRequest().authenticated())이 그 전에 401 로 차단하므로 method-security 의 unauth 경로는 defense-in-depth backstop. contract test 는 이를 isInstanceOf(AuthenticationException.class) 로 단언(처음엔 AccessDeniedException 기대해 실패 → 정정). → raw/errors/method-security-cglib-vs-jdk-proxy-usecase-injection-2026-06-08 동일 노트에 기록

Audit & Findings (2026-06-08 — 구현 대조 + findings 검증)

src/ 코드와 노트 self-report 를 대조한 결과. 사용자 작성 결정 영역은 자동 rewrite 하지 않고 정합/등급만 갱신.

구현 인벤토리 (as-built, actually-implemented)

구현 항목 파일 Trace
AuthorizationPort(PEP) + AuthorizationPrincipal(raw roles) + AuthorizationDeniedException + @RequiresPermission(RUNTIME, Spring-free) application-core/.../security/ D1, D4, §1·§3
Permission(resource:action VO, 2-segment, 3-segment 거부) shared-contract/.../security/Permission.java D6
RequiresPermissionAuthorizationManager(custom AuthorizationManager<MethodInvocation>, fail-closed) + MethodSecurityConfig(@EnableMethodSecurity(prePostEnabled=false) + AuthorizationManagerBeforeMethodInterceptor advisor) adapter-web/.../authz/ D1, §2
RolePermissionRegistry(lowercase normalize, 명시 열거/wildcard 없음) + RolePermissionProperties(ca-skeleton.authz.role-permissions, raw role key) + AuthorizationAdapter(fail-closed) adapter-web/.../authz/ D2, D3, §3
handleForbiddenSecurityErrorClassifier.classifyAccessDenied 위임 → AUTHZ_INSUFFICIENT_PERMISSION adapter-web/.../error/GlobalExceptionHandler.java D5, §4
sample-portfolio @RequiresPermission: create/update/batch=worklog:write, delete=worklog:close (read 면제) + role bundle user:{read,write} / admin:{read,write,close}(application.yml) sample-portfolio/.../worklog/ + app-bootstrap/application.yml L180-182 D8, §5
D4 ArchUnit: declareRequiredPermissionWhenMutating()(미선언 WRITE use case build fail) + D1 ArchUnit: application_and_domain_do_not_depend_on_spring_security app-bootstrap/.../architecture/CleanArchitectureTest.java L291·L335-358 D4, D1
테스트: PermissionTest · AuthorizationContractTest(application-core) · RolePermissionRegistryTest · AuthorizationAdapterTest · RolePermissionPropertiesTest(binding) · RequiresPermissionAuthorizationManagerTest · GlobalExceptionHandlerTest · WorkLogAuthorizationContractTest(method-security 3-tier 시연, CGLIB pin) · WorkLogAuthorizationE2ETest(@WebMvcTest, 실 WorkLogController DELETE 엔드포인트: HTTP→MVC→method-security→AccessDeniedExceptionGlobalExceptionHandler→403 envelope; positive(admin→204)+negative(user→403 AUTHZ_INSUFFICIENT_PERMISSION)) 각 모듈 src/test/

등급: 2026-06-08 ./gradlew check GREEN(전 모듈 test + ArchUnit 49 rules + verifyCleanArchitectureDependencies + verifyPublicPathSnapshot) 실행 → 위 항목 locally-verified. D4 rule 은 비공허(non-vacuous) 검증까지 완료(DeleteWorkLogUseCase 어노테이션 제거 시 정확히 해당 rule 만 FAILED 후 복원). 미커밋 working tree. 잔존 미검증 = JWT 필터 seam(아래 Claims To Verify 잔존 #4) 뿐.

Findings 검증 (사용자 제기 10항 대조)

# 사용자 주장 코드 대조 결과
1 ArchUnit 강제 부재 → 인가 누락 silent + spring-security import 가드 없음 반증(FALSE): 둘 다 구현됨 — declareRequiredPermissionWhenMutating()(D4) 가 미선언 mutating use case build fail, application_and_domain_do_not_depend_on_spring_security(D1)가 import 차단. 위임 설계대로 host=architecture-enforcement suite 실현. (노트 §2 의 "REFERENCE ONLY/planned" 표기가 stale 이었음 → 정정함)
2 AOP proxy bypass 부분 valid: D4 rule 은 annotation 존재 만 보장, self-invocation/non-bean invocation-path 우회는 미검출. 현 호출 경로(controller→usecase proxy)는 안전. → Claims To Verify 잔존 #2
3 CGLIB/proxy-target-class 의존 valid, 기록됨: raw/errors/method-security-cglib-vs-jdk-proxy-usecase-injection-2026-06-08. 설계 대안(미채택): controller 가 concrete *UseCase 아닌 input-port 인터페이스 주입 시 JDK proxy 로 충분 → CGLIB 하드 의존 제거 + §19 정합 ↑. 구현은 test 에 CGLIB 강제(prod mirror)로 핀 — 정당한 선택이나 근본 결합은 잔존
4 E2E 전 경로 미검증 대부분 RESOLVED: method-security→AuthorizationPort→registry + 3-tier 는 WorkLogAuthorizationContractTest, HTTP→MVC→method-security→403 AUTHZ_INSUFFICIENT_PERMISSION envelope(positive admin→204 + negative user→403)는 WorkLogAuthorizationE2ETest(실 WorkLogController DELETE) 가 검증. 잔존 seam = JWT 필터체인→JwtToAuthenticatedUserConverterAuthenticatedUser.roles(두 테스트 모두 addFilters=false로 principal 직접 주입) → Claims To Verify 잔존 #4(저위험, 머지 전 권고)
5·6·7 read 미적용 / IDOR·owner ABAC / cross-tenant valid(의도적 범위): D4 mutating-only, D2/D5 ABAC·IDOR 확장점, tenant 위임 — 노트 정합
8 config drift fail-closed valid(가용성): 보안 아닌 가용성. startup known-role 검증 미구현 → Claims To Verify 잔존 #8
9 every-request 비용 valid(무시 가능): static config startup-bound, staleness 없음
10 §25 SSOT Owner Map row 부재 valid: project note 편집(본 branch 밖) — §진행 중 메모 TODO

머지 전 실질 권고(코드 작업): 1번(ArchUnit D4/D1)·4번의 HTTP→authz E2E 는 이미 해소됨(WorkLogAuthorizationE2ETest). 잔존 = (4-narrowed) authN→authz seam(full @SpringBootTest + mock JWT)·(2) invocation-path 가드 또는 (3) input-port 주입 전환 — 모두 저위험.

묶음 (이 branch에서 파생된 자료)

본 branch 는 hub. 파생 raw 누적 시 카테고리별 그룹화. 현재 leaf — 자동조사 산출 raw 가 §Sources 에 연결되면 아래 갱신.

근거 자료

오류 기록 (이 branch 작업 중 발생)

면접 준비 (이 작업에서 나올 수 있는 면접 질문)

블로그·채용공고 연계 글감

관련 일일 노트

  • (없음 — 구현 단계에서 누적)

완료 후 정리

머지/종료 시 채움. /ingest가 이 섹션 기준으로 wiki/projects/에 추출.

  • PR 링크:
  • 리뷰 메모:
  • 머지 결과 / 배포 환경:
  • wiki 추출 대상 (verified만, wiki/projects/로만 추출 — 전용 canonical 후보 wiki/projects/ca-tmpl/authorization-rbac-method-security.md):
    • actually-implemented 항목:
    • locally-verified 항목:
    • prod-verified 항목:
  • 추출하지 않을 항목 (planned / documented-only / abandoned):