# Portfolio Ownership and Auth Design - Related REQ: `REQ-004` ## 목표 - 포트폴리오 리소스에 소유주(`owner_user_id`)를 도입해 사용자 단위 데이터 격리를 강제한다. - 기존 RBAC(`read`, `trade-write`, `admin`)와 소유권 검사 규칙을 결합해 접근 제어를 표준화한다. - frontend와 portfolio-hub가 같은 서버에서 개별 컨테이너로 운영되는 구조에서도 단일 인증 체계로 일관된 권한 모델을 유지한다. ## 범위 - 포함: - DB 스키마 변경 및 레거시 데이터 백필 - API/서비스/리포지토리 ownership 정책 - 통합 테스트 및 회귀 테스트 시나리오 - 제외: - 외부 IdP/OIDC 연동 - 인증 서비스를 별도 프로세스로 분리하는 배포 작업 ## DB 마이그레이션 설계 ## Revision Plan 1. `owner_user_id` nullable 추가 2. 레거시 row 백필 3. 인덱스 추가 4. not null 제약 적용 5. 필요 시 FK 제약(`users.id`) 적용 ## 권장 DDL 단계 1. 컬럼 추가 ```sql ALTER TABLE portfolios ADD COLUMN owner_user_id TEXT; ``` 2. 백필 기준 사용자 결정 - 환경변수 기반 bootstrap admin이 있으면 해당 사용자 id를 우선 사용한다. - 없으면 `users.created_at ASC` 첫 사용자 id를 사용한다. - 사용자 자체가 없으면 migration을 실패시키고 운영자가 사용자 생성 후 재시도한다. 3. 레거시 데이터 백필 ```sql UPDATE portfolios SET owner_user_id = :fallback_user_id WHERE owner_user_id IS NULL; ``` 4. 제약 및 인덱스 적용 ```sql ALTER TABLE portfolios ALTER COLUMN owner_user_id SET NOT NULL; CREATE INDEX idx_portfolios_owner_updated ON portfolios (owner_user_id, updated_at DESC); ``` 5. 선택적 FK 제약 ```sql ALTER TABLE portfolios ADD CONSTRAINT fk_portfolios_owner_user FOREIGN KEY (owner_user_id) REFERENCES users(id); ``` ## 롤백 전략 - downgrade에서는 FK, 인덱스, not null 제약 해제 후 컬럼 제거 순서로 진행한다. - 운영 데이터 보존이 필요하면 컬럼 삭제 대신 nullable 유지 downgrade를 별도 운영 방침으로 둔다. ## API 권한 규칙 설계 ## 공통 정책 - 1단계: RBAC 검사 통과 필요 - 2단계: admin이 아니면 ownership 검사 수행 - ownership 검사 기준: `portfolio.owner_user_id == auth_context.user_id` - `auth_context.user_id`가 없는 요청은 ownership 필요한 엔드포인트에서 `403` ## Endpoint Policy Matrix | Endpoint | RBAC 최소 권한 | Ownership 적용 | 비고 | | --- | --- | --- | --- | | `GET /portfolios` | read | 적용 | admin은 전체, 그 외 owner만 필터 | | `GET /portfolios/{portfolio_id}` | read | 적용 | owner 불일치 시 403 | | `POST /portfolios` | trade-write | 생성 시 owner 고정 | owner_user_id는 인증 주체로 강제 | | `PATCH /portfolios/{portfolio_id}` | trade-write | 적용 | owner 불일치 시 403 | | `POST /portfolios/{portfolio_id}/buy` | trade-write | 적용 | owner 불일치 시 403 | | `POST /portfolios/{portfolio_id}/sell` | trade-write | 적용 | owner 불일치 시 403 | | `POST /portfolios/{portfolio_id}/invest` | trade-write | 적용 | owner 불일치 시 403 | | `POST /portfolios/{portfolio_id}/transfer` | trade-write | 적용 | owner 불일치 시 403 | | `POST /portfolios/{portfolio_id}/reset` | trade-write | 적용 | owner 불일치 시 403 | | `DELETE /portfolios/{portfolio_id}` | admin | admin only | 기존 정책 유지 | | `POST/GET/PATCH/DELETE /portfolios/{id}/comments*` | read or trade-write | 적용 | 상위 포트폴리오 ownership을 재사용 | ## 서비스/리포지토리 반영 포인트 - Domain model: - `Portfolio`에 `owner_user_id` 추가 - Schema: - `PortfolioSummary`/`CreatePortfolioRequest` 응답/입력 계약에 owner 노출 정책 정의 - 외부 입력으로 owner_user_id 수동 지정은 금지 - Repository: - create 시 owner_user_id 필수 인자 - list/get 시 요청자 컨텍스트 기반 owner 필터 함수 제공 - Service/API: - request principal을 service 계층으로 전달 - 공통 ownership 가드 함수 도입 ## frontend 연계 방침 - frontend는 credential 저장/검증 자체를 수행하지 않고 portfolio-hub 인증 API를 사용한다. - 브라우저 경로는 HttpOnly + Secure cookie 기반 세션을 우선 고려한다. - 동일 서버 내 별도 컨테이너 운영 시 reverse proxy에서 CORS/Cookie domain 정책을 일관되게 관리한다. ## 테스트 케이스 목록 ## Migration 1. 기존 portfolios row가 존재할 때 owner_user_id 백필 후 null row가 0건인지 검증 2. users가 없는 상태에서 migration 실행 시 실패 동작 검증 3. downgrade 시 인덱스/제약 해제가 순서대로 수행되는지 검증 ## API Authorization 1. 사용자 A 소유 portfolio를 사용자 B가 조회하면 403 2. 사용자 A 소유 portfolio를 사용자 B가 수정/매매/리셋하면 403 3. admin이 타 사용자 portfolio 조회/수정 가능 4. 일반 사용자의 `GET /portfolios`는 본인 소유만 반환 5. user_id 없는 bot principal 요청이 ownership 필요 API 호출 시 403 ## API Creation 1. `POST /portfolios`에서 owner_user_id가 인증 사용자로 저장되는지 검증 2. 요청 body에 owner_user_id를 넣어도 무시되거나 validation 실패하는지 검증 ## Regression 1. 기존 REQ-001 댓글 API가 ownership 정책 적용 후에도 owner 경계 내에서 정상 동작 2. 기존 RBAC 오류(`401`, `403`) 계약이 ownership 도입 후에도 유지 3. admin delete 동작이 ownership 변경과 충돌 없이 유지 ## 추적 항목 - 본 설계 문서 구현 시 `docs/SRS/traceability-matrix.md`의 `REQ-004` 행에 Issue/PR/Test evidence를 갱신한다.