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 |
|
|
|
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 |
|
|
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 분류는 siblingfeature-security-operational-baseline가 owns(중복 금지). 머지 후 verified 결과는/ingest로wiki/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(
AUTHZcategory)·§10(repository capability — 별개 축)·§11 Security 가 관련 영역.
선택 (형제 — 직접 의존):
- raw/branch-notes/feature-security-operational-baseline — authN + JWT 검증 + principal mapping + 401/403 matrix owner. 본 branch 는
AuthenticatedUser.roles의 prefix 없는 raw role을 consume하고, SpringROLE_*authority는 adapter 경계의 파생 표현으로만 취급한다. - raw/branch-notes/feature-repository-access-permission-contract —
@UseCaseCapability(use case → infrastructure capability). 사용자 권한이 아님(registry 명시) — 본 branch 와 직교하는 축.
브랜치 계약 패킷
- 생성 시 프로젝트 개정:
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_PERMISSION403 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 분류 matrix →
feature-security-operational-baselineowns. 본 branch 는 그 출력 중 prefix 없는 raw role set만 consume 한다. @UseCaseCapability(repository infra-capability) →feature-repository-access-permission-contractowns. 사용자 권한과 혼동 금지(그 branch out-of-scope 에 "runtime authorization 혼동" 명시).- cross-tenant authz (
AUTHZ_TENANT_MISMATCH) →feature-tenant-context-policyowns. 본 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-ruleshost. 본 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.Permissionrecord 는shared-contract.AuthorizationContractTest/PermissionTest통과)- role→permission 해소 adapter (raw role → effective permissions) — 등급:
locally-verified(2026-06-08RolePermissionRegistry(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_PERMISSION403 wiring — 등급:locally-verified(2026-06-08RequiresPermissionAuthorizationManager(AuthorizationManager<MethodInvocation>) +MethodSecurityConfig(@EnableMethodSecurity(prePostEnabled=false)+ ROLE_INFRASTRUCTURE Advisor). 거부 →AuthorizationDecision(false)→ SpringAccessDeniedException→GlobalExceptionHandler#handleForbidden→SecurityErrorClassifier.classifyAccessDenied→AUTHZ_INSUFFICIENT_PERMISSION.GlobalExceptionHandlerTest/RequiresPermissionAuthorizationManagerTest통과. filter 경로(EnvelopeAccessDeniedHandler)와 method 경로(controller-advice) 가 이제 동일 classifier 사용) - permission naming registry + sample-portfolio authz 시연 — 등급:
locally-verified(2026-06-08worklog: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, 실WorkLogControllerDELETE 엔드포인트): user → 403 + envelopeerror.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-08wiki-decision-researcher5 raw 산출 + 연결 완료)
진행 중 메모
- 잔존 저위험 (2026-06-08, 의도적 미해소):
- authN→authz seam 미통합 검증:
WorkLogAuthorizationE2ETest/WorkLogAuthorizationContractTest는 둘 다addFilters=false(+SecurityAutoConfiguration제외)로 JWT 필터체인을 끄고AuthenticatedUser를 SecurityContext 에 직접 주입한다. 따라서 인가 leg(method-security→403 envelope)는 닫혔으나,JWT → SecurityFilterChain → JwtToAuthenticatedUserConverter → AuthenticatedUser.roles → registry lookupseam 은 authz 와 묶여 한 번에 검증되지 않음(security-baseline 단위검증에 의존). 닫으려면 full@SpringBootTest(mock JWT 발급 + 실 필터체인 + method security) 필요 — ~50 env 의존으로 별도 작업. proxy-target-classflip 미가드: 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
- authN→authz seam 미통합 검증:
- security-baseline 은 2026-06-08 Phase C2 로 authN 전 영역
locally-verified까지 구현됨. 본 branch 는 그 위에 authz 결정 만 얹으므로,JwtToAuthenticatedUserConverter(realm_access+resource_access →AuthenticatedUserraw roles + adapterROLE_*authorities)·AuthenticatedUser·EnvelopeAccessDeniedHandler가 본 branch 구현의 전제 anchor. - code SSOT 위임 (coverage audit Should-fix 해소):
AUTHZ_INSUFFICIENT_PERMISSION·AUTHZ_TENANT_MISMATCH는error-codes.yaml에owner_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); customAuthorizationManager<MethodInvocation>가 Java-level 집행 제공(SS-AUTHZ-ARCH-C3) / 대안: Spring method security@PreAuthorize(SS-AUTHZ-ARCH-C2 = bean coupling → 위반), web-layerauthorizeHttpRequestscoarse 규칙 / 근거: 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-baselineJwtToAuthenticatedUserConverter+ 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:actionlowercase 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 Claims는raw/<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 vscheck(...)→boolean). trade-off: throw 방식 = 호출부 단순 + fail-closed 자연스러움 vs boolean = 분기 유연. 기본 throw(fail-closed).
| 항목 | 구현 anchor | 등급 |
|---|---|---|
| port 인터페이스 | dev.caskeleton.application.<core>.security.AuthorizationPort — void requirePermission(Permission required) (application-core, Spring Security import 금지) |
planned |
| Permission 표현 | domain-core 또는 shared-contract 의 record Permission(String resource, String action) (resource:action, D6) |
planned |
| 현재 principal 접근 | adapter 가 SecurityContext→AuthenticatedUser(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.yamlenforcement: archunit/runtime AOP forbidden+ project §25 F1).
- OUT_OF_BRANCH_SCOPE(F4): ArchUnit rule 의 실제 코드 위치 =
feature-architecture-enforcement-rulessuite. 본 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 없음)을 담는다. SpringGrantedAuthority만"ROLE_"+toUpperCase()prefix 를 받는다(JwtToAuthenticatedUserConverter.javaL33-34). 따라서 registry key = raw role 명(prefix 없음) —ROLE_ADMIN아님. application-core 가 Spring-free 이므로 port 는GrantedAuthority가 아니라 raw role set 을 consume.- UNSUPPORTED_IMPL_DECISION: (1) 매핑 저장소 = app-side
@ConfigurationPropertiesstatic 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):
AuthenticatedUser는adapter-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-C3distinct→403 + security-baseline matrix row). code 자체는 security-baselineerror-codes.yamlowner.
- OUT_OF_BRANCH_SCOPE: error code 정의/registry = security-baseline. 본 branch = 거부 → 403 envelope wiring.
- 예외 경로 결정 (depth audit #1 해소): application-core 는 Spring-free 이므로
AuthorizationPort는 SpringAccessDeniedException을 throw할 수 없다. 따라서 2-hop 경로를 명시: (1) application-core port 가 domain-neutralAuthorizationDeniedException(자체 타입) throw → (2) adapter-webRequiresPermissionAuthorizationManager(Spring-aware)가 이를 Springorg.springframework.security.access.AccessDeniedException으로 변환(또는 Spring 6.xAuthorizationDeniedException extends AccessDeniedException사용). method-invocation 시점 throw 이므로 filter-layerEnvelopeAccessDeniedHandler가 아니라 controller-adviceGlobalExceptionHandler#handleForbidden(AccessDeniedException)(L91,actually-implemented) 에 도달.- UNSUPPORTED_IMPL_DECISION:
handleForbidden현재는 coarseFORBIDDEN매핑. fine-grainedAUTHZ_INSUFFICIENT_PERMISSION을 emit 하려면handleForbidden이SecurityErrorClassifier.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_PERMISSION403(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.
- authenticated 인데 permission 없음: 401 아님 →
- 다른 계약 의존:
- raw/branch-notes/feature-security-operational-baseline — raw role principal을 입력으로 제공하고 Spring
ROLE_*authority는 adapter에서 파생하며,AUTHZ_INSUFFICIENT_PERMISSIONcode/emission 경로를 소유한다. 이 seam이 바뀌면 본 branch의 registry 입력 형식이 영향받는다. - raw/branch-notes/feature-repository-access-permission-contract
@UseCaseCapability(infra-capability) — 직교 축(사용자 권한 아님).@RequiresPermission와 동시 선언되며 ArchUnit 패턴 공유(mirror). 혼동 시 user-authz 를 capability 로 착각. - raw/branch-notes/feature-architecture-enforcement-rules ArchUnit suite host — D4 rule 의 실제 코드 위치(REFERENCE ONLY).
- raw/branch-notes/feature-sample-domain-contract-fixture sample-portfolio model owner — D8 authz 시연 부착 대상.
- raw/branch-notes/feature-tenant-context-policy
AUTHZ_TENANT_MISMATCH(cross-tenant authz) — ABAC tenant 축은 그 branch. 본 branch 는 확장점만. - raw/branch-notes/feature-operational-error-observability-foundation
Categoryenum(AUTHZ) SSOT — D5 category consume.
- raw/branch-notes/feature-security-operational-baseline — raw role principal을 입력으로 제공하고 Spring
검증해야 할 주장
공식 문서·사례는 근거지만 내 프로젝트 동작을 자동 보장하지 않는다.
| Claim | Why uncertain | How to verify | Status |
|---|---|---|---|
| — | RESOLVED: AuthorizationPort/AuthorizationPrincipal/@RequiresPermission 가 application-core 에 Spring-free, CleanArchitectureTest D1 rule(L291)이 import 차단 |
actually-implemented |
|
| — | RESOLVED: AuthorizationPrincipal(raw roles, "never ROLE_*"), RolePermissionRegistry(lowercase normalize), AuthorizationContractTest |
actually-implemented |
|
@RequiresPermission 강제 |
— | RESOLVED: CleanArchitectureTest.declareRequiredPermissionWhenMutating()(L335-358, D4) — 미선언 WRITE_REPOSITORY use case build fail |
actually-implemented |
AUTHZ_INSUFFICIENT_PERMISSION 403 emit |
— | RESOLVED(method-security 경로): GlobalExceptionHandler#handleForbidden 가 SecurityErrorClassifier.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 | 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 은 미존재 — 향후/ingest시wiki/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 가@RequiresPermissionuse 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 의 deferredSupplier<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 |
handleForbidden → SecurityErrorClassifier.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→AccessDeniedException→GlobalExceptionHandler→403 envelope; positive(admin→204)+negative(user→403 AUTHZ_INSUFFICIENT_PERMISSION)) |
각 모듈 src/test/ |
— |
등급: 2026-06-08
./gradlew checkGREEN(전 모듈 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 필터체인→JwtToAuthenticatedUserConverter→AuthenticatedUser.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에서 파생된 자료)
- raw/company-tech-blogs/curity-oauth2-scope-vs-permission-naming
- raw/official-docs/aws-iam-google-iam-permission-naming-convention
- raw/official-docs/keycloak-authorization-services-realm-client-roles
- raw/official-docs/owasp-authz-permission-model-abac-rbac
- raw/official-docs/spring-security-authorization-architecture
본 branch 는 hub. 파생 raw 누적 시 카테고리별 그룹화. 현재 leaf — 자동조사 산출 raw 가 §Sources 에 연결되면 아래 갱신.
근거 자료
- raw/official-docs/security-authorization-cheatsheet-owasp — OWASP-AUTHZ-C1~C7 (deny-by-default / authn-authz distinct / least-privilege / every-request / server-side / IDOR)
- raw/official-docs/spring-security-authorization-architecture — SS-AUTHZ-ARCH-C1~C6 (D1: custom
AuthorizationManager/@PreAuthorizeAOP coupling). 2026-06-08wiki-decision-researcher산출 - raw/official-docs/owasp-authz-permission-model-abac-rbac — OWASP-PM-C1~C6 (D2: permission-centric RBAC + ABAC counterclaim + least-privilege H+V). 2026-06-08 자동조사 산출
- raw/official-docs/aws-iam-google-iam-permission-naming-convention — IAM-NAMING-C1~C5 (D6:
resource:action— AWS/Google IAM 비교). 2026-06-08 자동조사 산출 - raw/official-docs/keycloak-authorization-services-realm-client-roles — KC-AUTHZ-C1~C4 (D3: realm/client role JWT claim + UMA 대안). 2026-06-08 자동조사 산출
- raw/company-tech-blogs/curity-oauth2-scope-vs-permission-naming — CURITY-SCOPE-C1~C3 (D6: scope vs permission 분리 + colon naming,
company-case-study). 2026-06-08 자동조사 산출
오류 기록 (이 branch 작업 중 발생)
- raw/errors/method-security-cglib-vs-jdk-proxy-usecase-injection-2026-06-08 — method-security AOP proxy 가 use case 를 JDK interface proxy 로 감싸 concrete-type 주입 실패(CGLIB 강제로 해소) + unauthenticated→AuthenticationException(403 아님) 명확화. 2026-06-08 구현 중 발생, 둘 다 resolved.
면접 준비 (이 작업에서 나올 수 있는 면접 질문)
- raw/interviews/clean-architecture-method-authorization-without-spring-coupling-2026-06-08 — "왜 @PreAuthorize 안 쓰고 use-case AuthorizationPort 인가", "permission vs role 모델", "거부를 어떻게 403 으로 emit 하나(2-hop)", "AOP proxy bypass 위험" 등.
블로그·채용공고 연계 글감
- raw/blog-topics/framework-free-method-authorization-clean-architecture-2026-06-08 — application layer 를 Spring-Security-free 로 유지하면서 method-level authorization 을 거는 패턴(annotation in core + AuthorizationManager in adapter).
관련 일일 노트
- (없음 — 구현 단계에서 누적)
완료 후 정리
머지/종료 시 채움.
/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):