Files
llm-wiki/raw/branch-notes/feature-sample-portfolio-public-access.md

216 lines
14 KiB
Markdown

---
title: branch / feature-sample-portfolio-public-access
source_type: branch-note
status: raw
branch: feature-sample-portfolio-public-access
parent_branch:
related_projects: [ca-tmpl]
tags: [branch, ca-tmpl, security, testing, spring-boot, spring-security, component-scan]
created: 2026-07-03
target_merge:
status_label: in-progress
id: BR-CA-SKELETON-OPERATIONAL-CONTRACT-057
kind: project-work-item
project: ca-skeleton-operational-contract
work_item: WI-CA-SKELETON-OPERATIONAL-CONTRACT-057
inherits: [DEC-CA-SKELETON-OPERATIONAL-CONTRACT-MODULE-LAYOUT-001@1, DEC-CA-SKELETON-OPERATIONAL-CONTRACT-STACK-FRAMEWORK-001@1, DEC-CA-SKELETON-OPERATIONAL-CONTRACT-STACK-TEST-001@1]
refines: []
overrides: []
depends_on: [WI-CA-SKELETON-OPERATIONAL-CONTRACT-048, WI-CA-SKELETON-OPERATIONAL-CONTRACT-021]
contract_packet: 1
contract_packet_sha256: 48cb041d3ff0bcd03ab8fb89745313d975a25f44f39b64d8eb849eb1d466e501
---
# branch: feature-sample-portfolio-public-access
> Layer: `raw/branch-notes/` — sample-portfolio standalone demo URL 공개 정책과 관련 테스트 보정 기록.
<!-- section-id: branch-parent -->
## 부모 (필수)
- [[raw/project-notes/ca-skeleton-operational-contract]]
<!-- GENERATED: branch-contract:start -->
<!-- section-id: branch-contract-packet -->
## 브랜치 계약 패킷
- **생성 시 프로젝트 개정**: `1`
- **패킷 스키마**: `contract_packet: 1`
- **완료 조건**: sample public endpoint allowlist와 authenticated endpoint negative test가 명시된다
<!-- section-id: inherited-project-decisions -->
### 상속한 프로젝트 결정
| 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-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-TEST-001@1` | test framework는 JUnit 5다 | Work Item 완료 조건에 적용 | [[raw/project-notes/ca-skeleton-operational-contract]] |
<!-- section-id: branch-local-decisions -->
### 브랜치 지역 결정
> 기존 branch-local 결정은 아래 `## Decision Evidence Map / 결정-근거 매핑`의 D-row가 소유하며 이 packet에서 복제하지 않는다.
| Decision ID | Decision | Relation | Supporting Claims | Status |
|---|---|---|---|---|
<!-- section-id: declared-overrides -->
### 선언한 예외
| Override ID | Overrides | Reason | Approval | Status |
|---|---|---|---|---|
<!-- GENERATED: branch-contract:end -->
<!-- section-id: branch-goal -->
## 목표
- 이슈: sample-portfolio는 별도 로그인/IdP 플로우가 없는 참고 구현인데, URL 확인 시 JWT와 method-security가 같이 걸려 데모 접근성이 떨어졌다.
- PR: 없음.
<!-- section-id: branch-scope -->
## 범위
### 포함 범위
- sample-portfolio standalone composition root에서 production JWT `SecurityConfig``MethodSecurityConfig`를 스캔 제외한다.
- sample-portfolio 전용 `SecurityFilterChain`을 추가해 모든 demo URL을 `permitAll`로 공개한다.
- sample actuator chain도 sample-local 정책으로 전체 `permitAll` 처리한다.
- 기존 `:sample-portfolio:test` 포트 충돌을 막기 위해 web integration test의 `management.server.port`를 랜덤 포트로 둔다.
### 제외 범위
- production `adapter-web` JWT/authz 정책 변경.
- `app-bootstrap` production actuator 보안 정책 변경.
- sample에 실제 로그인/IdP 플로우 추가.
## 근거 (필수, 최소 1개+)
| Source | 정당화하는 결정 |
|---|---|
| [[raw/official-docs/spring-security-authorization-architecture]] | method security가 AOP 기반으로 service/use case 호출을 가로채므로 sample runtime에서 URL 공개만으로는 write endpoint가 완전히 열리지 않는다는 판단 |
| [[raw/official-docs/actuator-endpoint-exposure-spring-official]] | custom `SecurityFilterChain`이 있으면 actuator auto-security에 의존할 수 없으므로 sample-local actuator chain을 명시해야 한다는 판단 |
| [[raw/official-docs/actuator-management-port-spring-official]] | `management.server.port`가 별도 HTTP port로 설정 가능하므로 테스트에서는 `0`으로 격리할 수 있다는 판단 |
## TODO
- [x] sample runtime에서 production JWT `SecurityConfig` 스캔 제외 — 등급: `actually-implemented`
- [x] sample runtime에서 production `MethodSecurityConfig` 스캔 제외 — 등급: `actually-implemented`
- [x] sample 전용 public `SecurityFilterChain` 추가 — 등급: `actually-implemented`
- [x] sample actuator chain 전체 공개 — 등급: `actually-implemented`
- [x] sample web integration tests의 management port collision 제거 — 등급: `locally-verified`
## 진행 중 메모
- `SecurityFilterChain`만 공개하면 HTTP 필터는 통과하지만, `@RequiresPermission`이 붙은 sample use case는 `MethodSecurityConfig` AOP advisor에 의해 여전히 unauthenticated/unauthorized로 막힌다.
- 따라서 sample standalone runtime에서는 production authn/authz configuration을 composition root에서 제외해야 한다.
- 기존 권한 계약 테스트(`WorkLogAuthorizationContractTest`, `WorkLogAuthorizationE2ETest`)는 `MethodSecurityConfig`를 직접 import하는 보안 프레임워크 fixture로 유지했다.
## 결정 사항
- 2026-07-03: sample-portfolio standalone app은 로그인/IdP 없는 공개 데모로 취급하고 모든 sample URL을 permit-all로 연다. / 이유: 사용자가 브라우저/URL 접근으로 sample API를 확인할 수 있어야 한다. / 검토한 대안: public-paths에 sample 경로 열거, mock/demo login 추가, production security 그대로 유지. / 근거: `UNSUPPORTED_DECISION` — 제품 정책 판단이며 외부 공식 문서가 직접 정당화하지 않는다.
- 2026-07-03: `SecurityConfig`뿐 아니라 `MethodSecurityConfig`도 sample component scan에서 제외한다. / 이유: method security AOP가 write use case를 필터 이후에도 막기 때문이다. / 근거: [[raw/official-docs/spring-security-authorization-architecture]]
- 2026-07-03: test-only web contexts는 `management.server.port=0`을 명시한다. / 이유: local process가 9001을 사용 중이어도 `:sample-portfolio:test`가 deterministic하게 통과해야 한다. / 근거: [[raw/official-docs/actuator-management-port-spring-official]]
## 결정-근거 매핑
| Decision ID | Decision | 선택 조건 (언제 이 결정 / 언제 대안) | Supporting Claims | Evidence Strength | Open Risk |
|---|---|---|---|---|---|
| D1 | sample-portfolio standalone URL은 전부 공개한다. | 로그인/IdP 없는 sample demo일 때 이 결정. 운영 서비스나 민감 actuator가 있는 앱이면 production security 정책 유지. | `UNSUPPORTED_DECISION` | `project-policy` | sample을 운영 배포하면 actuator/loggers까지 공개되므로 별도 production profile 또는 sample 제거 필요 |
| D2 | sample composition root에서 `SecurityConfig``MethodSecurityConfig`를 제외하고 sample-local permit-all chain을 둔다. | sample runtime에서 write endpoint까지 열어야 할 때 이 결정. authz contract fixture는 별도 test import로 유지. | `raw/official-docs/spring-security-authorization-architecture.md#SS-AUTHZ-ARCH-C2`, `raw/official-docs/spring-security-authorization-architecture.md#SS-AUTHZ-ARCH-C3` | `official-vendor-doc + UNSUPPORTED_DECISION` | Spring Security auto-config 조건 변화 시 sample chain 조건 재검증 필요 |
| D3 | sample actuator chain은 sample-local로 전체 `permitAll`한다. | sample demo 확인성이 우선인 local/reference app일 때 이 결정. production actuator는 app-bootstrap 정책 유지. | `raw/official-docs/actuator-endpoint-exposure-spring-official.md#SB-ACT-EXP-C2`, `raw/official-docs/actuator-endpoint-exposure-spring-official.md#SB-ACT-EXP-C3` | `official-vendor-doc + project-policy` | sample config를 운영에 재사용하면 노출 위험 |
| D4 | web integration tests는 `management.server.port=0`으로 격리한다. | test context가 management server를 띄우고 local fixed port 충돌 가능성이 있을 때 이 결정. runtime default port는 유지. | `raw/official-docs/actuator-management-port-spring-official.md#SB-ACT-PORT-C3` | `official-vendor-doc` | parallel test에서 다른 fixed port가 남아 있으면 별도 격리 필요 |
## 구현 가이드
### 1. sample runtime security override
> **Trace**: D1, D2.
>
> - **UNSUPPORTED_IMPL_DECISION**: sample config class/package naming은 repo local convention (`bootstrap.security`)에 맞춘 결정.
| File | Implementation |
|---|---|
| `src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/SamplePortfolioApplication.java` | `@ComponentScan` exclude filter에 `SecurityConfig`, `MethodSecurityConfig` 추가 |
| `src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/bootstrap/security/SamplePublicAccessSecurityConfig.java` | servlet web app일 때만 `/**` matcher, CSRF disable, stateless, `anyRequest().permitAll()` chain 등록 |
| `src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/bootstrap/management/SampleManagementSecurityConfig.java` | servlet web app일 때 actuator endpoint chain 전체 `permitAll()` |
### 2. test port isolation
> **Trace**: D4.
>
> - **UNSUPPORTED_IMPL_DECISION**: affected tests에 property를 직접 붙이는 방식은 가장 좁은 변경을 위한 repo-local 판단.
| File | Implementation |
|---|---|
| `OpenApiSnapshotTest` | `management.server.port=0` |
| `OpenApiDriftContractTest` | `management.server.port=0` |
| `DateHeaderContractTest` | `management.server.port=0` |
| `VirtualThreadMdcE2ETest` | `management.server.port=0` |
## 엣지·실패·의존
- **실패·엣지 경로**: `webEnvironment=NONE` context에는 `HttpSecurity`가 없으므로 sample security configs는 `@ConditionalOnWebApplication(SERVLET)`로 제한해야 한다.
- **실패·엣지 경로**: sample URL 공개는 method-security 제외 없이는 write endpoint까지 보장하지 못한다.
- **다른 계약 의존**: [[raw/branch-notes/feature-authentication-authorization-contract]] — production authn/authz contract는 변경하지 않고 sample fixture에서만 우회한다.
- **다른 계약 의존**: [[raw/branch-notes/feature-management-actuator-security-contract]] — production actuator posture는 app-bootstrap 소유로 유지한다.
## 검증해야 할 주장
| Claim | Why uncertain | How to verify | Status |
|---|---|---|---|
| sample write endpoint가 Authorization 헤더 없이 통과한다. | filter-chain 공개와 method-security 제외가 함께 필요하다. | `./gradlew :sample-portfolio:test --tests dev.caskeleton.sample.portfolio.bootstrap.security.SamplePublicAccessSecurityConfigTest` | `locally-verified` |
| sample full context는 webEnvironment NONE에서도 뜬다. | `HttpSecurity`가 없는 context에서 security config bean 생성이 실패할 수 있다. | `./gradlew :sample-portfolio:test --tests dev.caskeleton.sample.portfolio.SampleApplicationContextTest --tests dev.caskeleton.sample.portfolio.bootstrap.security.SamplePublicAccessSecurityConfigTest` | `locally-verified` |
| sample-portfolio 전체 테스트가 fixed management port 충돌 없이 통과한다. | local 9001 process가 떠 있으면 기존 test가 실패했다. | `./gradlew :sample-portfolio:test` | `locally-verified` |
| 전체 repository check가 통과한다. | sample change가 ArchUnit/Spotless/Checkstyle/SpotBugs와 충돌할 수 있다. | `./gradlew check` | `locally-verified` |
## 마주친 문제
- `SamplePublicAccessSecurityConfigTest` 작성 직후 `SamplePublicAccessSecurityConfig`가 없어서 컴파일 실패했다. TDD red 단계로 의도된 실패.
- `@WebMvcTest`에서 `HttpSecurity`가 제공되지 않아 테스트 부트스트랩을 최소 `@SpringBootTest`로 전환했다.
- `SampleApplicationContextTest``webEnvironment=NONE`이라 sample security configs에 servlet web app 조건을 추가했다.
- `./gradlew check` 1차 재실행은 Spotless import/indent 위반으로 실패했고 `:sample-portfolio:spotlessApply` 후 통과했다.
- 기존 포트 충돌은 [[raw/errors/sample-portfolio-tomcat-port-in-use-check-2026-07-03]] 로 분리 기록했다.
## 묶음 (이 branch에서 파생된 자료)
<!-- GENERATED: errors:start -->
- [[raw/errors/sample-portfolio-oauth2-resource-server-dependency-2026-05-27]]
- [[raw/errors/sample-portfolio-tomcat-port-in-use-check-2026-07-03]]
<!-- GENERATED: errors:end -->
### Sub-branches (세부 작업)
- 없음.
### 오류 기록 (이 branch 작업 중 발생)
- [[raw/errors/sample-portfolio-tomcat-port-in-use-check-2026-07-03]] — sample web integration tests의 management fixed port 충돌을 test-only random port로 해결.
### 면접 준비 (이 작업에서 나올 수 있는 면접 질문)
- 없음 — 별도 면접 질문으로 추출할 만큼 독립적인 새 개념 없음.
### 강의 (이 작업을 위해 학습한 강의)
- 없음.
### job-posting tie-ins (이 작업에서 파생된 글감)
- 추출할 별도 글감 없음.
## 관련 일일 노트
- 없음 — 2026-07-03 daily note가 아직 없어 broken wikilink를 만들지 않음.
## 완료 후 정리
- PR 링크:
- 리뷰 메모:
- 머지 결과 / 배포 환경: local verification only.
- **wiki 추출 대상**:
- `actually-implemented` 항목: sample runtime public access override.
- `locally-verified` 항목: sample-portfolio tests and full `check`.
- **추출하지 않을 항목**:
- production security posture 변경 없음.