draft
This commit is contained in:
@@ -1,72 +0,0 @@
|
||||
|
||||
## Plan: `pfhub service-token list` 명령 추가
|
||||
|
||||
`pfhub service-token list` 명령을 추가한다. 레포지토리 → 서비스 → 출력 → CLI 순서로 구현하며, 기존 `user list` 패턴을 템플릿으로 사용한다.
|
||||
|
||||
---
|
||||
|
||||
### Phase 1 — Repository 레이어
|
||||
|
||||
**1.** portfolio_repository.py — `PortfolioRepository` 프로토콜에 추상 메서드 추가:
|
||||
```
|
||||
list_service_tokens(user_id: str | None, *, include_revoked: bool) -> list[AuthToken]
|
||||
```
|
||||
|
||||
**2.** postgres.py — `PostgresRepository`에 구현체 추가:
|
||||
- 기본 쿼리: `SELECT … FROM auth_tokens WHERE token_type='service'`
|
||||
- `user_id` 지정 시 `AND user_id = %s` 조건 추가
|
||||
- `include_revoked=False`(기본)이면 `AND revoked_at IS NULL` 조건 추가
|
||||
- `ORDER BY created_at DESC`
|
||||
- 기존 `list_auth_tokens()` 의 `_row_to_auth_token()` 재사용
|
||||
|
||||
---
|
||||
|
||||
### Phase 2 — Service 레이어
|
||||
|
||||
**3.** auth_service.py — `AuthService`에 메서드 추가:
|
||||
```
|
||||
list_service_tokens(username: str | None, *, include_revoked: bool = False) -> list[AuthToken]
|
||||
```
|
||||
- `username` 지정 시 `get_user_by_username()` → `user_id` 변환 후 리포지토리 위임
|
||||
- `username` 없으면 `user_id=None` 으로 전체 조회
|
||||
|
||||
---
|
||||
|
||||
### Phase 3 — Schema / 출력 레이어
|
||||
|
||||
**4.** auth.py — 기존 `AuthTokenResponse` 재사용 (이미 token_hash 미포함, 안전)
|
||||
|
||||
**5.** utils.py — `print_service_token_table(console, tokens)` 함수 추가:
|
||||
- Rich Table 컬럼: `name`, `permission`, `user_id`, `expires_at`, `revoked_at`, `created_at`, `token_prefix`
|
||||
- `render_output()` dispatch에 `list[AuthTokenResponse]` 분기 추가
|
||||
|
||||
---
|
||||
|
||||
### Phase 4 — CLI 레이어
|
||||
|
||||
**6.** service_tokens.py — `@token_app.command("list")` 추가:
|
||||
- `--user <username>` (Optional[str], default=None) — 특정 사용자 토큰만 조회
|
||||
- `--show-revoked` (bool flag, default=False) — revoked 토큰 포함
|
||||
- `_with_service()` + lambda 패턴 (user.py 참조)
|
||||
- `render_output(..., empty_message="No service tokens found.")`
|
||||
|
||||
---
|
||||
|
||||
### Verification
|
||||
|
||||
1. `pfhub service-token list` → 활성 서비스 토큰 테이블 출력
|
||||
2. `pfhub service-token list --show-revoked` → revoked 토큰 포함 출력
|
||||
3. `pfhub service-token list --user alice` → alice 소유 토큰만 필터링
|
||||
4. `pfhub service-token list --output json` → JSON 포맷 출력
|
||||
5. 토큰 없을 때 → `"No service tokens found."` 메시지 출력
|
||||
6. test_cli.py 또는 test_auth.py에 신규 테스트 추가 후 전체 테스트 통과 확인
|
||||
|
||||
---
|
||||
|
||||
### Decisions
|
||||
|
||||
- revoked 토큰: 기본 제외, `--show-revoked` 옵션으로 포함
|
||||
- `--user` 없으면 전체 서비스 토큰 조회
|
||||
- 스키마: `AuthTokenResponse` 재사용 (신규 모델 불필요)
|
||||
- `token_hash`는 절대 노출하지 않음
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
# SDLC 전체 주기 인덱스
|
||||
|
||||
<!-- 아래 테이블은 /new-sdlc 실행 시 자동 갱신된다. 수동 편집 금지. -->
|
||||
|
||||
## 주기 목록
|
||||
|
||||
| ID | 날짜 | 이슈명 | Discovery | 상태 |
|
||||
|---|---|---|---|---|
|
||||
| sdlc-001_20240414_cli-bot-token-lifecycle-cli.design | 20240414 | CLI bot 토큰 발급/삭제 | [Discovery](./sdlc-001_20240414_cli-bot-token-lifecycle-cli.design/1_discovery.md) | confirmed |
|
||||
| sdlc-002_20260423_service-token-user-based | 20260423 | service-token user-based 소유권 | [Discovery](./sdlc-002_20260423_service-token-user-based/1_discovery.md) | draft |
|
||||
|
||||
## 단계별 횡단 조회
|
||||
|
||||
| 단계 | 문서 목록 |
|
||||
|---|---|
|
||||
| Discovery | [sdlc-001](./sdlc-001_20240414_cli-bot-token-lifecycle-cli.design/1_discovery.md), [sdlc-002](./sdlc-002_20260423_service-token-user-based/1_discovery.md) |
|
||||
| Planning | [sdlc-001](./sdlc-001_20240414_cli-bot-token-lifecycle-cli.design/2_planning.md) |
|
||||
| Design | [sdlc-001](./sdlc-001_20240414_cli-bot-token-lifecycle-cli.design/3_design.md) |
|
||||
| Implementation | [sdlc-001](./sdlc-001_20240414_cli-bot-token-lifecycle-cli.design/4_implementation.md) |
|
||||
| Verification | [sdlc-001](./sdlc-001_20240414_cli-bot-token-lifecycle-cli.design/5_verification.md) |
|
||||
| Release | [sdlc-001](./sdlc-001_20240414_cli-bot-token-lifecycle-cli.design/6_release.md) |
|
||||
| Operations | |
|
||||
@@ -1,109 +0,0 @@
|
||||
# 0. 문서 상태
|
||||
- 상태: confirmed
|
||||
- 승인자: Justin
|
||||
- 승인 시각: 2026-04-14
|
||||
|
||||
# 1. 계획 상태 요약
|
||||
- Discovery 문서 현황: 본 문서가 첫 Discovery 초안이다.
|
||||
- Planning 점수 파일 현황: docs/sdlc/planning/planning-backlog-scores.out.csv (존재하지 않음)
|
||||
- 해석: CLI에서 봇 토큰 발급/삭제를 지원하기 위한 요구사항 정의가 우선이며, Planning 단계에서 만기 정책과 우선순위 점수화를 확정해야 한다.
|
||||
|
||||
# 2. 요구사항 판정 결과 (완전 중복/유사/신규 + 근거)
|
||||
- 판정: 신규
|
||||
- 근거
|
||||
- docs/sdlc/discovery/ 경로에 기존 Discovery 문서가 없어 완전 중복/유사 판정을 수행할 대상이 없다.
|
||||
- 현재 저장소의 CLI는 user/portfolio/strategy 중심이며, bot 토큰 발급/삭제를 직접 수행하는 CLI 커맨드가 없다.
|
||||
|
||||
# 3. 문제점의 요약
|
||||
- 현재 상태
|
||||
- 인증 API는 봇/봇 토큰 기능을 제공하지만, 운영자가 CLI만으로 봇 토큰 발급/삭제를 실행할 수 없다.
|
||||
- 자동화 스크립트에서 API 직접 호출 의존이 발생해 운영 절차 일관성이 낮다.
|
||||
- 기대 상태
|
||||
- CLI에서 특정 사용자(또는 사용자 소유 봇)에 대해 봇 토큰 발급/삭제를 일관된 명령으로 처리할 수 있어야 한다.
|
||||
- 토큰 만기 설정 정책은 선택 입력으로 하되, 미입력 시 기본 TTL 365일을 적용하고 운영상 필요 시 `--no-expiry` 옵션으로 무기한 발급을 허용해야 한다.
|
||||
- 이해관계자
|
||||
- 운영자(관리자 권한 사용자): 봇 토큰 발급/폐기의 실행 주체.
|
||||
- 자동화/배포 담당자: 스크립트 기반 토큰 라이프사이클 관리 필요.
|
||||
- 보안/승인 책임자: 토큰 만기 정책과 최소 권한 정책 승인 필요.
|
||||
- 제약사항
|
||||
- 기존 권한 모델(admin/read/trade-write) 및 auth 서비스 계약과 호환되어야 한다.
|
||||
- 기존 CLI UX 패턴(확인 프롬프트, --force, 출력 포맷 table/json)을 유지해야 한다.
|
||||
- 저장소의 문서 동기화 규칙에 따라 API/CLI 계약 변경 시 기술 문서 업데이트가 필요하다.
|
||||
|
||||
# 4. 이번에 만들 기능 1개
|
||||
- CLI에서 봇 토큰 발급/삭제 기능을 제공하고, 토큰 만기 정책(필수/기본값)을 명시적으로 처리한다.
|
||||
|
||||
# 5. 요구사항 목록
|
||||
## 기능 요구사항
|
||||
- FR-1: 관리자는 CLI에서 봇 토큰을 발급할 수 있어야 한다 (대상 bot 식별자 지정 필수).
|
||||
- FR-2: 관리자는 CLI에서 봇 토큰을 삭제(폐기)할 수 있어야 한다 (대상 token 식별자 지정 필수).
|
||||
- FR-3: 발급 시 만기 정책을 입력 또는 기본값으로 처리해야 하며, 기본값은 365일 TTL이고 명시적 `--no-expiry` 지정 시에만 무기한 발급을 허용해야 한다.
|
||||
- FR-4: 발급/삭제 명령은 기존 쓰기 명령과 동일하게 확인 프롬프트를 제공하고, --force 시 프롬프트를 생략해야 한다.
|
||||
- FR-5: 명령 결과는 기존 CLI 포맷 정책(table/json)으로 출력되어야 하며, token_id, expires_at, revoked 상태를 포함해야 한다.
|
||||
|
||||
## 비기능 요구사항
|
||||
- NFR-1: 보안 - 토큰 원문은 발급 시 1회만 노출하고, 이후 조회/출력 시 앞 6자/뒤 4자만 노출하는 마스킹 정책을 따른다.
|
||||
- NFR-2: 감사추적 - 발급/삭제 행위에 대해 실행자, 대상 bot_id, 시각, 결과를 로그로 남겨야 한다.
|
||||
- NFR-3: 사용성 - 기존 CLI 하위 명령 체계와 일관된 help 텍스트/옵션명을 유지해야 한다.
|
||||
- NFR-4: 신뢰성 - 잘못된 bot_id/token_id 입력 시 비정상 종료 대신 명확한 오류 메시지와 종료 코드를 제공해야 한다.
|
||||
|
||||
## 범위 경계
|
||||
- In Scope
|
||||
- CLI에 bot 토큰 발급/삭제 명령 추가.
|
||||
- 만기 입력 옵션 및 기본 TTL 연동.
|
||||
- 확인 프롬프트/--force/출력 포맷 일관화.
|
||||
- 관련 기술 문서(API/CLI 계약) 업데이트.
|
||||
- Out of Scope
|
||||
- 토큰 암호화 저장 구조 전면 재설계.
|
||||
- 다중 사용자 셀프서비스 포털 UI 제공.
|
||||
- 외부 IAM 연동(OAuth/OIDC 등) 추가.
|
||||
|
||||
# 6. 리스크/가정 목록
|
||||
## 리스크
|
||||
- R-1 (High): 만기 정책이 모호하면 무기한 토큰 발급으로 보안 리스크가 커질 수 있다.
|
||||
- 완화 방안: 기본 TTL 상한을 365일로 강제하고, 무기한 발급은 명시적 `--no-expiry` 옵션과 관리자 권한에서만 허용하며 감사 로그를 남긴다.
|
||||
- R-2 (Medium): CLI 명령 체계 확장 시 기존 명령 네이밍과 충돌하여 운영자가 오사용할 수 있다.
|
||||
- 완화 방안: 기존 command group 패턴에 맞춰 bot 하위 명령을 도입하고 --help 예시를 README에 추가한다.
|
||||
- R-3 (Medium): 토큰 출력 정책이 미정이면 로그/스크린샷을 통한 민감정보 유출 가능성이 있다.
|
||||
- 완화 방안: 발급 직후 1회 노출 정책과 마스킹 정책을 적용하고 운영 가이드에 비노출 원칙을 명시한다.
|
||||
|
||||
## 가정
|
||||
- A-1: 봇/봇토큰 관련 API 및 서비스 계층은 이미 존재하며 CLI는 이를 호출하는 어댑터 역할로 구현 가능하다.
|
||||
- A-2: 운영 환경에서 bot 토큰 발급/폐기 실행 권한은 admin으로 제한된다.
|
||||
|
||||
# 7. 초기 성공 기준
|
||||
- S-1: 관리자가 CLI로 bot 토큰 발급/삭제를 수행할 수 있다.
|
||||
- S-2: 만기 정책(필수 또는 기본 TTL)이 문서와 구현에서 일치한다.
|
||||
- S-3: 발급/삭제 명령의 정상/예외 경로 검증 증거가 Verification에 제출된다.
|
||||
|
||||
## 측정 방식 및 데이터 출처
|
||||
- S-1 측정 방식: 통합 테스트 또는 수동 E2E 시나리오에서 발급 후 토큰 사용 가능, 삭제 후 사용 불가 확인 / 데이터 출처: CLI 실행 로그, API 응답 로그.
|
||||
- S-2 측정 방식: 만기 입력 유무별 테스트 케이스 통과 여부 확인 / 데이터 출처: 테스트 리포트, 설정값(.env) 및 문서 diff.
|
||||
- S-3 측정 방식: 정상 1건 + 예외 1건 이상 테스트 증적 첨부 여부 확인 / 데이터 출처: pytest 결과, Verification 문서.
|
||||
|
||||
# 8. Planning으로 넘기기 전 확인 체크
|
||||
- Planning 시작 시점은 보류.
|
||||
- [x] 범위(In Scope/Out of Scope) 확정
|
||||
- 기준: 본 문서 #5 범위 경계에 포함/제외 항목이 명시되어 있다.
|
||||
- [x] 성공 지표의 측정 방식/데이터 출처 확정
|
||||
- 운영 지표 수집 채널: "운영자는 CLI 실행 결과를 터미널 출력으로 직접 확인하며, 별도 중앙 로그/대시보드는 없음. 추후 자동화/감사 필요 시 /var/log/portfoliohub/cli.log에 기록 예정."
|
||||
- [x] High 영향 플레이스홀더 우선순위 및 해소 책임자 지정
|
||||
- 기준: [x] 미해결 High 영향 플레이스홀더 없음
|
||||
- [x] 승인자 확정
|
||||
- 기준: 승인자가 Justin으로 명시되어 있다.
|
||||
|
||||
# 9. 파일 처리 결과 (생성/미생성 경로)
|
||||
- 처리 결과: 신규 생성
|
||||
- 생성 경로: docs/sdlc/discovery/dcy-001_2026-04-14_cli-bot-token-issue-delete.discovery.md
|
||||
- 재검토: Planning 이전에 운영 지표 수집 채널 확정 필요
|
||||
- 참조 문서: 없음
|
||||
|
||||
# 10. 사용자 결정 필요 항목 요약
|
||||
## {{DECIDE}}
|
||||
- 없음
|
||||
|
||||
## {{CONFIRM}}
|
||||
- 승인 시각 기재.
|
||||
|
||||
## {{DATA}}
|
||||
- 없음
|
||||
@@ -1,98 +0,0 @@
|
||||
# 0. 문서 상태
|
||||
- 상태: confirmed
|
||||
- 승인자: Justin
|
||||
- 승인 시각: 2026-04-14
|
||||
|
||||
# 1. Planning 개요
|
||||
- 입력 Discovery: dcy-001
|
||||
- 목적: CLI에서 bot 토큰 발급/삭제 기능을 우선순위 기반 작업으로 분해하고 Design 단계 전달 가능한 AC를 확정한다.
|
||||
- 우선순위 방법: WSJF
|
||||
- 점수 스케일: Business Value/Time Criticality/RR-OE/Job Size 모두 1~10
|
||||
|
||||
# 2. 이번 반복 범위 경계
|
||||
## In Scope
|
||||
- CLI `bot` 하위 명령에 토큰 발급/삭제 기능 추가
|
||||
- 만기 정책 반영(기본 TTL 365일, `--no-expiry` 옵션 처리)
|
||||
- 토큰 출력 마스킹(앞 6자/뒤 4자) 및 감사 로그 반영
|
||||
- 확인 프롬프트/`--force`/출력 포맷(`table`/`json`) 일관화
|
||||
- 테스트(정상/예외) 및 기술 문서 동기화
|
||||
|
||||
## Out of Scope
|
||||
- 토큰 저장 구조 전면 재설계
|
||||
- UI/포털 제공
|
||||
- 외부 IAM(OAuth/OIDC) 연동
|
||||
|
||||
## 운영/보안 정책
|
||||
- 운영 정책: 터미널 접근은 오직 관리자만 접근 가능하며 일반 유저는 터미널 사용이 불가하다. 따라서 CLI 사용은 관리자 행위로 간주하며, 관리자만 터미널 접근 및 CLI 실행 권한을 가진다.
|
||||
- 보안 정책: `--no-expiry` 옵션은 관리자 전용 운영 정책 하에서만 허용된다.
|
||||
|
||||
# 3. 백로그 작업 분해
|
||||
| Item ID | Work Item | 연결 요구사항 | 연결 리스크 | 담당 |
|
||||
|---|---|---|---|---|
|
||||
| P-002 | 만기 정책 및 권한 가드 구현 (`--no-expiry`, TTL 기본 365일, 관리자 전용 정책) | FR-3, NFR-4 | R-1 | Builder |
|
||||
| P-003 | 토큰 출력 마스킹 및 감사 로그 구현 | NFR-1, NFR-2 | R-1, R-3 | Builder |
|
||||
| P-001 | CLI bot 토큰 발급/삭제 명령 추가 | FR-1, FR-2, FR-4, FR-5 | R-2 | Builder |
|
||||
| P-004 | CLI UX 정합성 보강(확인 프롬프트, `--force`, 오류 메시지) | FR-4, NFR-3, NFR-4 | R-2 | Builder |
|
||||
| P-005 | 테스트/문서 동기화(README, API/CLI 계약) | S-1, S-2, S-3 | R-2, R-3 | Builder |
|
||||
|
||||
# 4. 점수표 (WSJF)
|
||||
| Item ID | Work Item | Business Value | Time Criticality | RR/OE | Job Size | WSJF Score | selected_score | Priority |
|
||||
|---|---:|---:|---:|---:|---:|---:|---:|---:|
|
||||
| P-002 | 만기 정책 및 권한 가드 구현 | 9 | 10 | 9 | 4 | 7.00 | 7.00 | 1 |
|
||||
| P-003 | 토큰 출력 마스킹 및 감사 로그 구현 | 8 | 8 | 10 | 4 | 6.50 | 6.50 | 2 |
|
||||
| P-001 | CLI bot 토큰 발급/삭제 명령 추가 | 10 | 9 | 8 | 5 | 5.40 | 5.40 | 3 |
|
||||
| P-004 | CLI UX 정합성 보강 | 7 | 7 | 7 | 4 | 5.25 | 5.25 | 4 |
|
||||
| P-005 | 테스트/문서 동기화 | 7 | 6 | 8 | 5 | 4.20 | 4.20 | 5 |
|
||||
|
||||
계산식: WSJF = (Business Value + Time Criticality + RR/OE) / Job Size
|
||||
|
||||
## 점수 근거 메모
|
||||
- P-002: 무기한 발급 오남용 리스크(R-1)를 직접 줄이는 핵심 정책 구현이라 TC/RR-OE를 높게 반영했다.
|
||||
- P-003: 민감정보 노출 리스크(R-3)와 감사추적 요구(NFR-2)를 직접 완화하므로 RR/OE를 최상위로 반영했다.
|
||||
- P-001: 기능 핵심(발급/삭제)이지만 정책/보안 가드 선행 필요성이 있어 우선순위를 3으로 배치했다.
|
||||
- P-004: 운영 UX 일관성 확보에 필요하나 기능/보안 블로커 대비 사업가치는 상대적으로 낮아 후순위다.
|
||||
- P-005: 품질 게이트 통과 및 문서 동기화 필수이나 구현 선행 항목 의존이 있어 마지막 순위로 배치했다.
|
||||
|
||||
# 5. 작업별 수용 기준 (AC)
|
||||
## P-002 AC
|
||||
- AC-1: 토큰 발급 명령에서 만기 미입력 시 기본 TTL 365일이 적용된다.
|
||||
- AC-2: `--no-expiry` 옵션은 관리자 전용 CLI 운영 정책에서만 허용되며, 정책 비적합 실행에서는 명확한 오류 코드/메시지를 반환한다.
|
||||
- AC-3: 만기 관련 옵션 조합이 잘못된 경우(예: TTL과 no-expiry 동시 지정) 검증 오류가 발생한다.
|
||||
|
||||
## P-003 AC
|
||||
- AC-1: 토큰 원문은 발급 직후 1회만 출력되고 이후 출력/조회에서는 앞 6자/뒤 4자 외 마스킹된다.
|
||||
- AC-2: 발급/삭제 시 실행자, bot_id, token_id(마스킹), 시각, 결과가 감사 로그에 기록된다.
|
||||
- AC-3: 로그에는 토큰 원문이 저장되지 않는다.
|
||||
|
||||
## P-001 AC
|
||||
- AC-1: CLI에 bot 토큰 발급 명령과 삭제 명령이 추가되고 `--help`에서 확인 가능하다.
|
||||
- AC-2: 발급 명령 성공 시 `token_id`, `expires_at`, `revoked` 정보가 출력 포맷 정책에 맞게 표시된다.
|
||||
- AC-3: 삭제 명령 성공 시 토큰이 즉시 폐기되어 재사용이 불가능해야 한다.
|
||||
|
||||
## P-004 AC
|
||||
- AC-1: 발급/삭제 명령은 기본적으로 확인 프롬프트를 표시하고 `--force` 사용 시 프롬프트를 생략한다.
|
||||
- AC-2: 잘못된 bot_id/token_id 입력 시 사용자 행동 지침이 포함된 오류 메시지를 제공한다.
|
||||
- AC-3: `table`/`json` 출력 모드에서 동일한 필수 필드 집합을 보장한다.
|
||||
|
||||
## P-005 AC
|
||||
- AC-1: 신규 기능에 대해 정상 경로 1건 이상, 예외 경로 1건 이상의 테스트가 추가된다.
|
||||
- AC-2: docs sync 규칙 대상 문서(README, API/CLI 계약)가 코드 변경과 함께 갱신된다.
|
||||
- AC-3: Verification 단계에서 테스트 실행 증적을 첨부할 수 있는 로그/리포트가 생성된다.
|
||||
|
||||
# 6. 우선순위 조정/예외
|
||||
- 동률 처리: WSJF 동률 시 RR/OE가 큰 항목 우선, 동률 지속 시 Job Size가 작은 항목 우선.
|
||||
- Override: 없음
|
||||
- 승인자: Justin
|
||||
|
||||
# 7. Design 단계 전달 체크
|
||||
- [x] 우선순위 상위 백로그의 AC가 검증 가능한 문장으로 정의됨
|
||||
- [x] WSJF 계산 근거 기록 완료
|
||||
- [x] 이번 반복 In Scope/Out of Scope 확정
|
||||
- [x] 최종 승인
|
||||
|
||||
# 8. 파일 처리 결과
|
||||
- 생성/갱신 경로: docs/sdlc/planning/pln-001_2026-04-14_cli-bot-token-lifecycle-cli.planning.md
|
||||
- 점수 파일 갱신: docs/sdlc/planning/planning-backlog-scores.out.csv
|
||||
|
||||
# 9. 사용자 결정 필요 항목 요약
|
||||
현재 미해결 플레이스홀더 없음
|
||||
@@ -1,171 +0,0 @@
|
||||
# 0. 문서 상태
|
||||
- 상태: confirmed
|
||||
- 승인자: Justin
|
||||
- 승인 시각: 2026-04-15
|
||||
|
||||
# 1. Design 개요
|
||||
- 입력 Planning: pln-001
|
||||
- 목적: CLI bot 토큰 발급/삭제 기능을 정책 일관성, 보안성, 운영성 기준으로 구현 가능하도록 인터페이스/흐름/검증 포인트를 확정한다.
|
||||
- 설계 대상 작업 (WSJF 상위 3개):
|
||||
- 1순위: P-002 만기 정책 및 권한 가드 구현
|
||||
- 2순위: P-003 토큰 출력 마스킹 및 감사 로그 구현
|
||||
- 3순위: P-001 CLI bot 토큰 발급/삭제 명령 추가
|
||||
|
||||
# 2. 설계 대안 비교 (우선순위 상위 작업)
|
||||
## P-002: 만기 정책 및 권한 가드 구현
|
||||
### 대안 A
|
||||
- 설명: CLI 입력 단계(Typer 옵션 파싱/검증)에서 TTL 기본값, `--no-expiry` 상호배타성, 관리자 정책 검증을 선처리한다.
|
||||
- 장점:
|
||||
- 잘못된 입력을 서비스 호출 전에 차단해 오류 원인이 명확하다.
|
||||
- 단위 테스트가 간단하며 사용자 피드백이 빠르다.
|
||||
- 단점:
|
||||
- 정책 검증 로직이 CLI 계층에 치우치면 API 계층과 중복될 수 있다.
|
||||
|
||||
### 대안 B
|
||||
- 설명: 정책 검증을 auth/service 계층에서 처리하고 CLI는 입력 전달만 수행한다.
|
||||
- 장점:
|
||||
- 정책 소스가 단일화되어 API/CLI 일관성이 높다.
|
||||
- 다른 채널(예: 배치, API)에서도 동일 정책 재사용이 쉽다.
|
||||
- 단점:
|
||||
- CLI 사용자 입장에서는 오류 맥락이 늦게 반환될 수 있다.
|
||||
- 서비스 에러를 CLI 친화 메시지로 재매핑하는 추가 코드가 필요하다.
|
||||
|
||||
### 선택 결정
|
||||
- 선택: 대안 A+B 혼합
|
||||
- 근거: 상호배타성/입력 형식 검증은 CLI에서 조기 차단하고, 권한 및 최종 만기 정책 강제는 서비스 계층에서 재검증해 정책 일관성과 UX를 동시에 확보한다.
|
||||
|
||||
## P-003: 토큰 출력 마스킹 및 감사 로그 구현
|
||||
### 대안 A
|
||||
- 설명: 발급 응답 DTO 생성 시점에서 마스킹된 값만 남기고 원문을 폐기한다.
|
||||
- 장점:
|
||||
- 출력/로그 경로를 일괄 통제해 원문 노출 위험이 낮다.
|
||||
- 후속 렌더링 계층(table/json)에서 추가 안전장치 부담이 줄어든다.
|
||||
- 단점:
|
||||
- 발급 직후 1회 원문 출력 요구를 처리하기 위해 예외 흐름이 필요하다.
|
||||
|
||||
### 대안 B
|
||||
- 설명: 출력 직전 렌더링 함수에서 마스킹을 적용하고 내부 모델에는 원문을 유지한다.
|
||||
- 장점:
|
||||
- 기존 데이터 모델 변경이 적다.
|
||||
- 일부 디버깅 시 내부 상태 확인이 쉽다.
|
||||
- 단점:
|
||||
- 로그/예외 경로에서 원문 누출 가능성이 높다.
|
||||
- 마스킹 누락 지점이 생기기 쉽다.
|
||||
|
||||
### 선택 결정
|
||||
- 선택: 대안 A
|
||||
- 근거: NFR-1/NFR-2와 R-3 완화 우선순위가 높아, 원문 보존 경로를 최소화하는 구조가 안전하다.
|
||||
|
||||
## P-001: CLI bot 토큰 발급/삭제 명령 추가
|
||||
### 대안 A
|
||||
- 설명: 기존 `user/portfolio/strategy`와 동일하게 `bot` Typer group을 추가하고 `token issue`, `token revoke` 서브커맨드로 구성한다.
|
||||
- 장점:
|
||||
- 현재 CLI 구조와 일관되어 학습 비용이 낮다.
|
||||
- 명령 확장(예: token list) 시 구조가 자연스럽다.
|
||||
- 단점:
|
||||
- 초기 구현 시 커맨드 트리/도움말 정비가 필요하다.
|
||||
|
||||
### 대안 B
|
||||
- 설명: 기존 `user` 그룹에 `user bot-token issue/revoke` 형태로 통합한다.
|
||||
- 장점:
|
||||
- 새로운 최상위 그룹 추가가 없어 변경 범위가 작다.
|
||||
- 단점:
|
||||
- 사용자 관리와 bot 토큰 수명주기가 섞여 명령 의미가 불명확해진다.
|
||||
|
||||
### 선택 결정
|
||||
- 선택: 대안 A
|
||||
- 근거: P-004의 UX 일관성 요구를 충족하고 장기 확장성(봇/토큰 명령 확장)에 유리하다.
|
||||
|
||||
# 3. 인터페이스 정의
|
||||
## P-002: 만기 정책 및 권한 가드 구현
|
||||
- 입력: `--ttl-days`(optional), `--no-expiry`(flag), 실행자 컨텍스트(admin 여부)
|
||||
- 출력: 정책 유효성 통과 시 정규화된 만기 정책 객체(`expires_at` 또는 `never_expires=true`)
|
||||
- 오류 경로: 옵션 상충, 음수/0 TTL, 비관리자 `--no-expiry` 사용 시 검증 오류 반환
|
||||
|
||||
## P-003: 토큰 출력 마스킹 및 감사 로그 구현
|
||||
- 입력: 발급/삭제 결과 객체(`token_id`, `raw_token` optional, `actor_id`, `bot_id`, `timestamp`)
|
||||
- 출력: 사용자 출력용 마스킹 토큰 값, 감사 로그 이벤트 레코드
|
||||
- 오류 경로: 감사 로그 기록 실패 시 `issue`는 트랜잭션 롤백으로 발급을 취소하고 명령을 실패 처리한다. `revoke`는 보안 우선 원칙에 따라 폐기는 유지하고 명령은 실패 처리하며 운영 경고를 발생시킨다.
|
||||
|
||||
## P-001: CLI bot 토큰 발급/삭제 명령 추가
|
||||
- 입력: `bot_id`, 발급 옵션(`--ttl-days`/`--no-expiry`), 공통 옵션(`--force`, `--format`)
|
||||
- 출력: issue 시 `token_id/expires_at/revoked(false)` + 1회 토큰 원문, revoke 시 `token_id/revoked(true)`
|
||||
- 오류 경로: 미존재 bot/token, 권한 부족, 정책 위반, 저장소/서비스 예외
|
||||
|
||||
## P-004: CLI UX 정합성 보강
|
||||
- 입력: 커맨드 실행 컨텍스트(확인 프롬프트 응답, `--force`, 출력 모드)
|
||||
- 출력: 표준화된 성공/실패 출력(table/json)
|
||||
- 오류 경로: 포맷 미지원, 사용자 취소 시 정상 종료(code 0), 도메인 오류 시 비정상 종료(code != 0)
|
||||
|
||||
## P-005: 테스트/문서 동기화
|
||||
- 입력: 테스트 케이스 정의, docs sync 규칙 대상 파일 목록
|
||||
- 출력: 테스트 결과 리포트, 업데이트된 README/API 계약 문서
|
||||
- 오류 경로: 테스트 실패, docs sync 체크 실패, 누락 문서 갱신 감지
|
||||
|
||||
# 4. 데이터/흐름 설계
|
||||
## P-002: 만기 정책 및 권한 가드 구현
|
||||
- 주요 데이터 경로:
|
||||
- CLI 옵션 파싱 -> 입력 검증 -> 서비스 계층 정책 재검증 -> repository 요청 페이로드 구성
|
||||
- 상태 변화:
|
||||
- `draft_policy` -> `validated_policy` -> `persistable_policy`
|
||||
|
||||
## P-003: 토큰 출력 마스킹 및 감사 로그 구현
|
||||
- 주요 데이터 경로:
|
||||
- 서비스 발급/삭제 결과 -> 마스킹 변환 -> 출력 렌더링 + 감사 로그 sink 기록
|
||||
- 상태 변화:
|
||||
- `raw_token_state` -> `masked_view_state` -> `audited_state`
|
||||
|
||||
## P-001: CLI bot 토큰 발급/삭제 명령 추가
|
||||
- 주요 데이터 경로:
|
||||
- command dispatch -> service 호출(issue/revoke) -> 결과 DTO -> renderer(table/json)
|
||||
- 상태 변화:
|
||||
- issue: `requested` -> `issued(active)`
|
||||
- revoke: `issued(active)` -> `revoked(inactive)`
|
||||
|
||||
## P-004: CLI UX 정합성 보강
|
||||
- 주요 데이터 경로:
|
||||
- confirmation gate -> command executor -> unified output/error handler
|
||||
- 상태 변화:
|
||||
- `pending_confirmation` -> `executed` 또는 `cancelled`
|
||||
|
||||
## P-005: 테스트/문서 동기화
|
||||
- 주요 데이터 경로:
|
||||
- code change set -> test run -> docs sync check -> verification evidence bundle
|
||||
- 상태 변화:
|
||||
- `unverified` -> `verified` 또는 `failed-with-evidence`
|
||||
|
||||
# 5. 테스트 전략 연계 (AC 매핑)
|
||||
| 작업 ID | AC | 검증 방법 | 검증 포인트 |
|
||||
|---|---|---|---|
|
||||
| P-002 | AC-1 | CLI 통합 테스트 | `ttl-days` 미입력 시 `expires_at`이 현재+365일로 계산됨 |
|
||||
| P-002 | AC-2 | 권한 예외 테스트 | 비관리자 컨텍스트에서 `--no-expiry` 요청 시 오류 코드/메시지 확인 |
|
||||
| P-002 | AC-3 | 옵션 조합 검증 테스트 | `--ttl-days`와 `--no-expiry` 동시 지정 시 검증 실패 |
|
||||
| P-003 | AC-1 | 출력 스냅샷 테스트 | issue 이후 재조회/목록 출력에서 앞6/뒤4 외 마스킹 확인 |
|
||||
| P-003 | AC-2 | 감사 로그 검증 테스트 | issue/revoke 각각에 actor/bot/token_id(마스킹)/결과 로그 생성 확인 |
|
||||
| P-003 | AC-3 | 보안 회귀 테스트 | 로그 파일/콘솔에 raw token 미노출 확인 |
|
||||
| P-001 | AC-1 | 커맨드 help 테스트 | `bot token issue`, `bot token revoke` help 노출 확인 |
|
||||
| P-001 | AC-2 | 성공 경로 E2E | issue 응답 필수 필드(`token_id/expires_at/revoked`) 존재 확인 |
|
||||
| P-001 | AC-3 | revoke E2E | revoke 이후 동일 토큰 인증 실패 확인 |
|
||||
| P-004 | AC-1 | UX 동작 테스트 | 기본 프롬프트 표시, `--force` 시 생략 확인 |
|
||||
| P-004 | AC-2 | 예외 메시지 테스트 | 잘못된 bot/token 입력에서 사용자 지침 포함 메시지 확인 |
|
||||
| P-005 | AC-1/AC-2 | CI 파이프라인 검증 | 정상+예외 테스트 최소 1건씩 통과 및 docs sync 통과 |
|
||||
|
||||
# 6. 리스크 대응 설계
|
||||
| 리스크 ID | 리스크 내용 | 설계 대응 전략 |
|
||||
|---|---|---|
|
||||
| R-1 | 만기 정책 모호성으로 무기한 토큰 남발 가능 | CLI+서비스 이중 검증, 기본 TTL 365일 강제, `--no-expiry` 제한 경로 및 감사 로그 필수화 |
|
||||
| R-2 | 명령 체계 확장 시 오사용 가능 | `bot` 전용 그룹으로 분리, help 명세 표준화, 오류 메시지에 다음 행동 가이드 포함 |
|
||||
| R-3 | 토큰 출력/로그 유출 위험 | 원문 1회 출력 후 즉시 마스킹 전환, 로그 저장 시 원문 금지, 보안 회귀 테스트 추가 |
|
||||
|
||||
# 7. Implementation 단계 전달 체크
|
||||
- [x] 우선순위 상위 작업의 설계 근거(대안 비교 + 선택 이유)가 존재함
|
||||
- [x] 전체 작업의 인터페이스(입력/출력/오류 경로)가 정의됨
|
||||
- [x] AC와 검증 포인트가 매핑됨
|
||||
- [x] Planning 리스크에 대한 설계 대응이 반영됨
|
||||
- [x] 최종 승인
|
||||
|
||||
# 8. 파일 처리 결과
|
||||
- 생성/갱신 경로: docs/sdlc/design/dsn-001_2026-04-14_cli-bot-token-lifecycle-cli.design.md
|
||||
|
||||
# 9. 사용자 결정 필요 항목 요약
|
||||
현재 미해결 플레이스홀더 없음
|
||||
@@ -1,103 +0,0 @@
|
||||
# 0. 문서 상태
|
||||
- 상태: confirmed
|
||||
- 승인자: 고진환
|
||||
- 승인 시각: 2026-04-15
|
||||
|
||||
# 1. Implementation 개요
|
||||
- 입력 Design: dsn-001
|
||||
- 목적: CLI bot 토큰 발급/삭제 기능을 설계 기준에 따라 작은 구현 단위로 분해하고, Verification 단계에 즉시 전달 가능한 검증 근거를 준비한다.
|
||||
- 이번 구현 우선순위 작업:
|
||||
- 1순위: P-002 만기 정책 및 권한 가드 구현
|
||||
- 2순위: P-003 토큰 출력 마스킹 및 감사 로그 구현
|
||||
- 3순위: P-001 CLI bot 토큰 발급/삭제 명령 추가
|
||||
|
||||
# 2. 이번 구현 범위
|
||||
## In Scope
|
||||
- `bot token issue`, `bot token revoke` CLI 명령 추가 및 help 노출
|
||||
- `--ttl-days`/`--no-expiry` 입력 검증(상호배타, 값 범위)과 서비스 계층 정책 재검증
|
||||
- 기본 TTL 365일 정책 반영 및 비관리자 무기한 토큰 금지
|
||||
- issue/revoke 결과의 마스킹 출력 처리 및 감사 로그 기록 경로 구현
|
||||
- revoke 확인 프롬프트/`--force` 분기와 표준 출력(table/json) 정합성 반영
|
||||
- 정상/예외 테스트 및 문서 동기화 반영
|
||||
|
||||
## Out of Scope
|
||||
- bot token list/rotate 등 신규 명령 확장
|
||||
- 감사 로그 저장소 아키텍처 재설계(외부 로그 파이프라인 교체 포함)
|
||||
- API 채널의 대규모 인터페이스 재정의
|
||||
|
||||
# 3. 작업 단위 분해
|
||||
| Impl ID | 작업명 | 연결 설계/AC | 변경 유형 | 우선순위 | 상태 |
|
||||
|---|---|---|---|---|---|
|
||||
| I-001 | bot 토큰 CLI 그룹/서브커맨드 골격 추가 | P-001 AC-1 | 코드(명령 라우팅) | High | 완료 |
|
||||
| I-002 | issue 입력 옵션 검증(상호배타/범위) | P-002 AC-3 | 코드(입력 검증) | High | 완료 |
|
||||
| I-003 | 만기 정책 서비스 재검증 및 기본 TTL 365일 적용 | P-002 AC-1, AC-2 | 코드(도메인/서비스) | High | 완료 |
|
||||
| I-004 | issue/revoke 서비스 호출 및 결과 DTO 정규화 | P-001 AC-2, AC-3 | 코드(서비스 통합) | High | 완료 |
|
||||
| I-005 | 토큰 출력 마스킹 정책 적용(원문 1회 출력 후 차단) | P-003 AC-1, AC-3 | 코드(출력/보안) | High | 완료 |
|
||||
| I-006 | 감사 로그 이벤트 기록 및 실패 분기 처리 | P-003 AC-2 | 코드(로깅/오류 처리) | Medium | 완료 |
|
||||
| I-007 | revoke UX 가드(확인 프롬프트, --force, 오류 메시지) | P-004 AC-1, AC-2 | 코드(UX/CLI) | Medium | 완료 |
|
||||
| I-008 | 테스트/문서 동기화 및 근거 번들 준비 | P-005 AC-1/AC-2 | 테스트+문서 | High | 완료 |
|
||||
|
||||
# 4. 파일 변경 계획
|
||||
| Impl ID | 대상 파일 | 변경 목적 | 변경 종류(생성/수정) | 상태 |
|
||||
|---|---|---|---|---|
|
||||
| I-001 | app/cli/main.py | `bot` 그룹 등록 및 커맨드 엔트리 연결 | 수정 | 완료 |
|
||||
| I-001 | app/cli/commands/bot_tokens.py | `token issue/revoke` 커맨드 골격 정의 | 생성 | 완료 |
|
||||
| I-002 | app/cli/commands/bot_tokens.py | `--ttl-days`/`--no-expiry` 검증 로직 추가 | 수정 | 완료 |
|
||||
| I-003 | app/services/auth_service.py | 만기 정책 재검증, 기본 TTL/권한 정책 강제 | 수정 | 완료 |
|
||||
| I-003 | app/domain/services.py | 정책 객체 정규화 규칙 보완(필요 시) | 수정 | 완료 |
|
||||
| I-004 | app/services/auth_service.py | issue/revoke 결과 DTO 필드 정규화 | 수정 | 완료 |
|
||||
| I-004 | app/schemas/auth.py | 응답 스키마(`token_id/expires_at/revoked`) 정렬 | 수정 | 완료 |
|
||||
| I-005 | app/cli/core/output.py | table/json 출력 시 마스킹 렌더링 추가 | 수정 | 완료 |
|
||||
| I-006 | app/core/security.py | 감사 로그 payload 표준화 및 민감정보 필터링 | 수정 | 완료 |
|
||||
| I-006 | app/repositories/postgres.py | 감사 로그 쓰기 경로(트랜잭션 연계) 보강 | 수정 | 완료 |
|
||||
| I-007 | app/cli/commands/bot_tokens.py | revoke 확인 프롬프트, `--force` 분기, 사용자 지침 메시지 | 수정 | 완료 |
|
||||
| I-008 | tests/test_cli.py | bot token help/성공/취소 흐름 테스트 추가 | 수정 | 완료 |
|
||||
| I-008 | tests/test_auth.py | 정책 검증(관리자/비관리자/no-expiry) 테스트 추가 | 수정 | 완료 |
|
||||
| I-008 | tests/test_app.py | 출력 마스킹 및 보안 회귀 시나리오 보강 | 수정 | 완료 |
|
||||
| I-008 | README.md | CLI 사용 예시 및 정책 제약 문서화 | 수정 | 완료 |
|
||||
| I-008 | docs/technical/auth-and-token-lifecycle.md | 토큰 수명주기/감사 로그 동작 반영 | 수정 | 완료 |
|
||||
|
||||
# 5. 테스트/검증 계획
|
||||
| Impl ID | 검증 방법 | 성공 기준 | 근거 |
|
||||
|---|---|---|---|
|
||||
| I-001 | CLI help 테스트 | `bot token issue/revoke`가 help에 노출된다 | `tests/test_cli.py` |
|
||||
| I-002 | 옵션 조합 예외 테스트 | `--ttl-days`와 `--no-expiry` 동시 입력 시 실패한다 | `tests/test_auth.py` |
|
||||
| I-003 | 권한/기본값 테스트 | 기본 TTL 365일, 비관리자 `--no-expiry` 차단 | `tests/test_auth.py` |
|
||||
| I-004 | 성공 경로 통합 테스트 | issue 응답에 `token_id/expires_at/revoked`가 존재한다 | `tests/test_cli.py` |
|
||||
| I-005 | 출력 스냅샷/회귀 테스트 | 재출력/로그에 raw token이 노출되지 않는다 | `tests/test_app.py` |
|
||||
| I-006 | 감사 로그 실패 분기 테스트 | issue는 롤백 실패 처리, revoke는 폐기 유지+경고를 만족한다 | `tests/test_auth.py` |
|
||||
| I-007 | UX 동작 테스트 | revoke 시 확인 프롬프트, `--force` 시 생략, 취소는 code 0 | `tests/test_cli.py` |
|
||||
| I-008 | 회귀+문서 동기화 확인 | 변경 경로 테스트 통과 및 문서 업데이트 반영 | 테스트 실행 로그 + 변경 문서 diff |
|
||||
|
||||
# 6. 영향도 및 의존성 점검
|
||||
| 항목 | 영향 내용 | 대응 방안 |
|
||||
|---|---|---|
|
||||
| 코드 | CLI 진입점, 인증 서비스, 출력/로깅 경로가 동시에 변경되어 회귀 범위가 넓다 | 단위+통합 테스트를 분리 실행하고 민감정보 노출 회귀 테스트를 추가한다 |
|
||||
| 문서 | CLI 사용법과 토큰 수명주기 운영 규칙이 불일치할 수 있다 | README/기술문서를 같은 변경 세트에서 동기 갱신한다 |
|
||||
| 설정/의존성 | 로그 sink/DB 쓰기 실패 시 issue/revoke 실패 정책이 다르므로 운영 혼선 위험이 있다 | 실패 분기 정책을 테스트로 고정하고 운영 경고 기준을 문서에 명시한다 |
|
||||
|
||||
# 7. Verification 단계 전달 체크
|
||||
- [x] 구현 범위가 설계 문서와 연결됨
|
||||
- [x] 작업 단위별 검증 방법이 정의됨
|
||||
- [x] 영향 파일과 의존성 점검 결과가 기록됨
|
||||
- [x] Verification에 전달할 실행/검증 근거가 정의됨
|
||||
- [x] 최종 승인
|
||||
|
||||
# 8. 파일 처리 결과
|
||||
- 생성 경로: app/cli/commands/bot_tokens.py
|
||||
- 생성 경로: app/cli/core/output.py
|
||||
- 수정 경로: app/cli/main.py
|
||||
- 수정 경로: app/services/auth_service.py
|
||||
- 수정 경로: app/domain/services.py
|
||||
- 수정 경로: app/schemas/auth.py
|
||||
- 수정 경로: app/core/security.py
|
||||
- 수정 경로: app/repositories/postgres.py
|
||||
- 수정 경로: tests/test_cli.py
|
||||
- 수정 경로: tests/test_auth.py
|
||||
- 수정 경로: tests/test_app.py
|
||||
- 수정 경로: README.md
|
||||
- 수정 경로: docs/technical/auth-and-token-lifecycle.md
|
||||
- 수정 경로: docs/sdlc/implementation/imp-001_2026-04-15_cli-bot-token-lifecycle-cli.implementation.md
|
||||
|
||||
# 9. 사용자 결정 필요 항목 요약
|
||||
현재 미해결 플레이스홀더 없음
|
||||
@@ -1,52 +0,0 @@
|
||||
# 0. 문서 상태
|
||||
- 상태: confirmed
|
||||
- 승인자: Justin
|
||||
- 승인 시각: 2026-04-15
|
||||
|
||||
# 1. Verification 개요
|
||||
- 입력 Implementation: imp-001
|
||||
- 목적: CLI bot 토큰 발급/삭제 기능 구현이 요구된 Acceptance Criteria(AC)를 충족하는지 검증하고, 결함 및 품질 게이트를 점검하여 Release 단계로 전달한다.
|
||||
- 검증 대상 작업: I-001~I-008 (CLI bot token issue/revoke 노출, 옵션 검증, TTL/권한 정책, 응답 필드, 마스킹, 감사 로그 분기, UX 분기, 회귀/문서 동기화)
|
||||
|
||||
# 2. 검증 범위
|
||||
- In Scope: imp-001 Implementation 문서의 In Scope 전체
|
||||
- Out of Scope: imp-001 Implementation 문서의 Out of Scope 전체
|
||||
|
||||
# 3. AC별 검증 결과
|
||||
| Impl ID | 검증 방법 | 성공 기준 | 근거 | 검증 결과 | 결함 여부 |
|
||||
|---|---|---|---|---|---|
|
||||
| I-001 | CLI help 테스트 | `bot token issue/revoke`가 help에 노출된다 | tests/test_cli.py | PASSED | 없음 |
|
||||
| I-002 | 옵션 조합 예외 테스트 | `--ttl-days`와 `--no-expiry` 동시 입력 시 실패한다 | tests/test_auth.py | PASSED | 없음 |
|
||||
| I-003 | 권한/기본값 테스트 | 기본 TTL 365일, 비관리자 `--no-expiry` 차단 | tests/test_auth.py | PASSED | 없음 |
|
||||
| I-004 | 성공 경로 통합 테스트 | issue 응답에 `token_id/expires_at/revoked`가 존재한다 | tests/test_cli.py | PASSED | 없음 |
|
||||
| I-005 | 출력 스냅샷/회귀 테스트 | 재출력/로그에 raw token이 노출되지 않는다 | tests/test_app.py | PASSED | 없음 |
|
||||
| I-006 | 감사 로그 실패 분기 테스트 | issue는 롤백 실패 처리, revoke는 폐기 유지+경고를 만족한다 | tests/test_auth.py | PASSED | 없음 |
|
||||
| I-007 | UX 동작 테스트 | revoke 시 확인 프롬프트, `--force` 시 생략, 취소는 code 0 | tests/test_cli.py | PASSED | 없음 |
|
||||
| I-008 | 회귀+문서 동기화 확인 | 변경 경로 테스트 통과 및 문서 업데이트 반영 | 테스트 실행 로그 + 변경 문서 diff | PASSED | 없음 |
|
||||
|
||||
# 4. 결함 목록
|
||||
| 결함 ID | Impl ID | 결함 요약 | 심각도(Blocker/Major/Minor) | 상태 | 비고 |
|
||||
|---|---|---|---|---|---|
|
||||
| 없음 | | | | | |
|
||||
|
||||
# 5. 회귀 테스트 결과
|
||||
- 실행 명령: `.venv/bin/python -m pytest -v | tee temp/ver-001-test-output.txt`
|
||||
- 결과 요약: 76개 테스트 모두 통과, 실패 0건
|
||||
- 최종 판정: Blocker 0건, Major 0건
|
||||
|
||||
# 6. 예외 승인 목록
|
||||
| Exception ID | 관련 Impl ID | 결함 심각도 | 사유 | 승인자 | 승인 시각 | 유효 기간 | 비고 |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| 없음 | | | | | | | |
|
||||
|
||||
# 7. Release 단계 전달 체크
|
||||
- [x] Blocker 결함 없음
|
||||
- [x] Major 결함 없음 또는 승인 예외 존재
|
||||
- [x] 테스트 실행 근거 첨부
|
||||
- [x] 배포 가능 여부/잔여 리스크/예외 승인 정리
|
||||
|
||||
# 8. 파일 처리 결과
|
||||
- 생성: docs/sdlc/verification/ver-001_2026-04-15_cli-bot-token-lifecycle-cli.verification.md
|
||||
|
||||
# 9. 사용자 결정 필요 항목 요약
|
||||
현재 미해결 플레이스홀더 없음
|
||||
@@ -1,115 +0,0 @@
|
||||
# 0. 문서 상태
|
||||
- 상태: draft
|
||||
- 승인자: {{DECIDE: 이 Release의 최종 승인자는 누구인가?}}
|
||||
- 승인 시각: {{CONFIRM: 승인 후 기재}}
|
||||
|
||||
# 1. Release 개요
|
||||
- 입력 Verification: ver-001
|
||||
- 배포 대상 범위: CLI bot token issue/revoke 기능, TTL/권한 정책 검증, 토큰 출력 마스킹, 감사 로그 분기 처리
|
||||
- 배포 방식: {{DECIDE: 점진 배포/일괄 배포 중 선택하라}}
|
||||
- 배포 환경: {{DATA: 배포 대상 환경(dev/staging/prod)과 배포 윈도우를 입력하라}}
|
||||
- 배포 가능성 근거: Verification 품질 게이트에서 Blocker 0건, Major 0건, 회귀 테스트 76건 통과 확인
|
||||
|
||||
# 2. 배포 체크리스트
|
||||
|
||||
| 항목 | 확인 내용 | 결과 (완료/미완료/해당없음) | 비고 |
|
||||
|---|---|---|---|
|
||||
| 설정 검증 | 환경 변수, 시크릿, 외부 연동 키 유효성 확인 | 미완료 | {{DATA: 실제 배포 환경의 시크릿/키 점검 결과를 입력하라}} |
|
||||
| 데이터 검증 | 마이그레이션 순서, 백업 상태, 복구 가능성 확인 | 완료 | 본 변경은 스키마 마이그레이션 비포함, 기존 백업/복구 절차 유지 |
|
||||
| 호환성 검증 | 이전 버전 클라이언트/API 호환 여부 확인 | 완료 | API 계약 변경 없음, CLI 기능 확장 중심 변경 |
|
||||
| 의존성 검증 | 신규/변경 라이브러리 버전 충돌 여부 확인 | 완료 | pyproject 의존성 추가 없음, 충돌 징후 없음 |
|
||||
| 트래픽 전략 | 점진/일괄 배포 방식과 중단 기준 확인 | 미완료 | 배포 방식 결정 후 중단 기준 수치 확정 필요 |
|
||||
| 커뮤니케이션 | 사용자 공지, 내부 공지, 온콜 담당자 공유 완료 | 미완료 | {{DATA: 공지 채널/온콜 담당자/공지 시점을 입력하라}} |
|
||||
|
||||
# 3. 롤백 계획
|
||||
|
||||
## 롤백 트리거
|
||||
- 에러율 임계치: 배포 후 15분 이동 평균 기준 5xx 비율 > 2.0% 5분 이상 지속
|
||||
- 지연 임계치: p95 응답 지연이 배포 전 기준 대비 50% 이상 상승 상태가 10분 이상 지속
|
||||
- 장애 지표: bot token issue/revoke 명령 실패율 > 5% 또는 인증/권한 오류 급증
|
||||
|
||||
## 롤백 절차
|
||||
1. 배포 중단 선언 및 온콜 채널에 롤백 트리거 충족 사실 공유
|
||||
2. 직전 안정 커밋으로 애플리케이션 버전 롤백 후 서비스 재기동
|
||||
3. 롤백 완료 후 핵심 검증 시나리오와 모니터링 지표 정상화 여부 확인
|
||||
|
||||
## 데이터 롤백 전략
|
||||
- 스키마/데이터 복원 여부: 스키마 변경 없음으로 데이터 롤백 불필요
|
||||
- 절차: 애플리케이션 바이너리/코드만 이전 버전으로 복구, 운영 데이터는 유지
|
||||
|
||||
## 롤백 후 검증
|
||||
- 핵심 시나리오 확인 항목:
|
||||
- healthcheck 200 응답 확인
|
||||
- 기존 토큰 인증 경로 정상 동작 확인
|
||||
- CLI 기본 조회/쓰기 흐름 회귀 스모크 통과 확인
|
||||
|
||||
## 롤백 커뮤니케이션
|
||||
- 공지 채널: {{DATA: 롤백 공지 채널(예: Slack #oncall, #backend-alerts)을 입력하라}}
|
||||
- 공지 시각: 롤백 시작 즉시 1차 공지, 복구 완료 후 2차 공지
|
||||
- 상태 업데이트 주기: 15분 간격
|
||||
|
||||
## 참조 런북
|
||||
- 롤백 런북: .github/templates/rollback-runbook.template.md
|
||||
|
||||
# 4. 릴리즈 노트 초안
|
||||
|
||||
## 변경 요약
|
||||
- CLI bot token 발급/삭제 명령 추가 및 관련 인증 정책/출력 보안 동작을 반영한다.
|
||||
|
||||
## 주요 변경 내용
|
||||
- bot token issue/revoke CLI 명령 제공 및 help 노출
|
||||
- --ttl-days/--no-expiry 상호배타 검증, 기본 TTL 365일 정책 적용
|
||||
- 비관리자 no-expiry 제한 및 권한 검증 강화
|
||||
- 토큰 원문 출력 노출 방지(마스킹) 및 감사 로그 분기 처리 강화
|
||||
|
||||
## 영향 범위
|
||||
- 사용자 영향: 운영자/개발자 CLI 사용 흐름에 bot token 라이프사이클 명령이 추가된다.
|
||||
- 시스템 영향: 인증/권한 경로와 감사 로그 경로의 실행 분기 증가, API 인터페이스 변경은 없음
|
||||
|
||||
## 알려진 제한 사항
|
||||
- 배포 방식(점진/일괄) 및 운영 공지 채널/온콜 확정 전까지 운영 절차 최종 승인 불가
|
||||
|
||||
# 5. 배포 후 관찰 계획
|
||||
|
||||
| 모니터링 항목 | 측정 지표 | 정상 범위 | 이상 감지 기준 | 담당자 |
|
||||
|---|---|---|---|---|
|
||||
| API 안정성 | 5xx 비율 | < 1.0% | 2.0% 초과 5분 지속 | {{DATA: 담당자 지정}} |
|
||||
| API 성능 | p95 지연 | 배포 전 기준 ±20% | 기준 대비 +50% 10분 지속 | {{DATA: 담당자 지정}} |
|
||||
| CLI 기능 품질 | issue/revoke 성공률 | > 99% | 95% 미만 10분 지속 | {{DATA: 담당자 지정}} |
|
||||
| 인증/권한 오류 | 401/403 비율 | 배포 전 기준 ±20% | 기준 대비 2배 이상 10분 지속 | {{DATA: 담당자 지정}} |
|
||||
|
||||
## 관찰 기간
|
||||
- 집중 관찰 기간: 배포 후 24시간
|
||||
- 일반 관찰 기간: 배포 후 7일
|
||||
|
||||
# 6. 승인된 예외 인계 목록
|
||||
해당 없음
|
||||
|
||||
# 7. Operations 단계 전달 체크
|
||||
- [ ] 롤백 가능한 배포 계획 승인됨
|
||||
- [ ] 배포 체크리스트 항목 전체 "완료" 또는 "해당없음" 처리됨
|
||||
- [x] 릴리즈/롤백 체크리스트가 근거와 함께 완료됨
|
||||
- [x] 배포 후 관찰 항목이 정의됨
|
||||
- [x] Operations에 전달할 관찰 포인트, 장애 트리거, 후속 대응 절차 준비됨
|
||||
- [x] 승인된 예외 이력 인계됨
|
||||
- [ ] 최종 승인
|
||||
- {{CONFIRM: Release 초안 승인 여부를 확인하라}}
|
||||
|
||||
# 8. 파일 처리 결과
|
||||
- 생성/갱신 경로: docs/sdlc/release/rel-001_2026-04-15_cli-bot-token-lifecycle-cli.release.md
|
||||
|
||||
# 9. 사용자 결정 필요 항목 요약
|
||||
## {{DECIDE}}
|
||||
- 이 Release의 최종 승인자는 누구인가?
|
||||
- 점진 배포/일괄 배포 중 선택하라
|
||||
|
||||
## {{CONFIRM}}
|
||||
- 승인 후 기재
|
||||
- Release 초안 승인 여부를 확인하라
|
||||
|
||||
## {{DATA}}
|
||||
- 배포 대상 환경(dev/staging/prod)과 배포 윈도우를 입력하라
|
||||
- 실제 배포 환경의 시크릿/키 점검 결과를 입력하라
|
||||
- 공지 채널/온콜 담당자/공지 시점을 입력하라
|
||||
- 롤백 공지 채널(예: Slack #oncall, #backend-alerts)을 입력하라
|
||||
- 모니터링 담당자를 지정하라
|
||||
@@ -1,241 +0,0 @@
|
||||
# 0. 문서 상태
|
||||
이 문서의 현재 상태와 식별 정보를 기록한다.
|
||||
|
||||
- 상태: confirmed
|
||||
- 문서 ID: sdlc-002_20260423_service-token-user-based
|
||||
- 이슈명: service-token user-based 소유권
|
||||
- 작성 시각(KST): 2026-04-23
|
||||
- 마지막 갱신 시각(KST): 2026-04-23 (Discovery 최종 승인 — 모든 OQ 해소, auth_audit_logs 삭제 결정 반영, Handoff 승인 처리)
|
||||
|
||||
# 1. 계획 상태 요약
|
||||
|
||||
- 연계 문서 현황:
|
||||
- Planning 점수 파일: 미생성
|
||||
- 관련 TODO/메모: 없음
|
||||
- 해석:
|
||||
- CLI 명령 그룹은 `app/cli/main.py`에서 `name="service-token"`으로 이미 변경되어 `pfhub service-token issue/revoke` 형태로 노출된다. 단, 내부 구현 파일(`bot_tokens.py`), 도메인 모델, 스키마, 서비스, 리포지터리, API 경로, DB 테이블/컬럼, `token_type` 값은 여전히 `bot` 명칭을 사용하므로 나머지 레이어의 rename이 필요하다.
|
||||
- `auth_tokens.user_id`에 bot 토큰은 `bot_principal.id`(봇 자체 UUID)가 저장되어 있어 실제 소유 user를 직접 참조하지 않는 설계 비일관성이 존재한다. 이를 교정하고, `bot_principals` 테이블 자체도 제거하여 service token을 `auth_tokens` 단일 엔티티로 직접 관리한다.
|
||||
|
||||
# 2. 요구사항 판정 결과
|
||||
|
||||
- 판정: 신규
|
||||
- 근거
|
||||
- sdlc-001은 bot 토큰 CLI 발급/폐기 기능 추가를 다뤘으며, 이번 변경은 명칭 체계 재정의와 소유권 모델 교정으로 목적이 다르다.
|
||||
- 기존 Discovery/Planning 문서 중 `service_principal` 도입 또는 `auth_tokens.user_id` 보정을 다룬 주기가 없다.
|
||||
|
||||
# 3. 문제점의 요약
|
||||
|
||||
- 현재 상태
|
||||
- (완료) `app/cli/main.py`에서 `token_app`이 `name="service-token"`으로 등록되어 CLI 명령이 `pfhub service-token issue/revoke`로 노출된다.
|
||||
- (미완료) `app/cli/commands/bot_tokens.py`, `app/domain/models.py`의 `BotPrincipal` 클래스, `auth_tokens.token_type = "bot"`, API 경로 `/auth/bots`, 스키마 클래스명, 서비스 메서드명, 리포지터리 메서드명, DB 테이블명(`bot_principals`) 등 나머지 레이어 전체에 `bot` 명칭이 그대로 사용된다.
|
||||
- (미완료) `app/services/auth_service.py`의 `issue_bot_token()`은 `auth_tokens.user_id`에 `bot_principal.id`(봇 자체의 UUID)를 저장한다. 실제 소유 user 연결은 `app/repositories/postgres.py`의 `get_token_principal()`에서 `COALESCE(b.owner_user_id, t.user_id)` 우회 쿼리로만 처리된다.
|
||||
- 기대 상태
|
||||
- `bot_principals` 테이블과 `service_principal` 엔티티 개념이 제거되고, service token은 `auth_tokens` 테이블에서 `token_type = "service"`, `name`(유니크) 컬럼으로 직접 식별된다.
|
||||
- `auth_tokens.user_id`에 발급 시 지정한 username의 실제 user_id가 직접 저장되어 token이 user를 참조하고, `COALESCE` 우회 로직 및 `bot_principal_id` JOIN이 제거된다.
|
||||
- 이해관계자
|
||||
- 운영자(관리자 권한 사용자): service token 발급/폐기의 실행 주체
|
||||
- 외부 서비스/봇 개발자: 발급된 service token을 API 접속에 사용하는 주체
|
||||
- 보안/승인 책임자: token 소유권 모델 변경 승인 필요
|
||||
- 제약사항
|
||||
- 기존 `auth_tokens` 테이블의 데이터가 존재하는 경우 마이그레이션에서 `user_id` 보정이 필요하며 FK 정합성을 유지해야 한다.
|
||||
- 기존 권한 모델(admin/read/trade-write) 및 token TTL 정책과 호환되어야 한다.
|
||||
- API 경로 변경(`/bots` → `/services`)이 발생하므로 기존 클라이언트의 호환성을 사전에 결정해야 한다.
|
||||
|
||||
## 주요 사용자 시나리오
|
||||
|
||||
- 운영자가 `pfhub service-token issue <name> --user <username>` 명령으로 외부 서비스용 API key를 발급한다. `<name>`은 발급할 토큰의 유니크 식별자이며 어떤 외부 서비스에서 사용 중인지 추적하는 데 쓰인다. `--user`로 지정한 username에서 user_id를 조회하여 `auth_tokens.user_id`에 저장하고, 해당 user의 권한 범위 내에서 동작한다.
|
||||
- 운영자가 `--user`를 생략하고 `pfhub service-token issue <name>` 명령을 실행하면 오류가 반환된다. (`--user`는 필수 옵션이다.)
|
||||
- 운영자가 API를 통해 service token을 직접 발급할 경우 `auth_tokens.user_id`에 지정된 user_id가 저장되어 포트폴리오 소유권 검사가 user 기준으로 정확히 동작한다. (service_principal 없이 `auth_tokens`에 직접 발급)
|
||||
- DB 마이그레이션 실행 후 기존 bot 토큰의 `user_id`가 `owner_user_id`로 보정되어 기존 인증 플로우가 변경 없이 동작한다.
|
||||
|
||||
# 4. 이번에 정의할 변경
|
||||
|
||||
- ~~CLI 명령 그룹 rename~~: `app/cli/main.py`에서 `name="service-token"` 등록 **완료**. `pfhub service-token issue/revoke`로 노출 중.
|
||||
- `bot_principals` 테이블 및 `service_principal` 엔티티 완전 제거: `BotPrincipal` 도메인 모델, 관련 스키마(`CreateBotPrincipalRequest` 등), API 경로(`/auth/bots`, `/auth/me/bots`), CLI 파일(`bot_tokens.py`), DB 테이블(`bot_principals`), `auth_tokens.bot_principal_id` 컬럼 삭제 — **미완료**
|
||||
- `token_type` 값 rename: `"bot"` → `"service"`, 기존 데이터 UPDATE — **미완료**
|
||||
- `service-token issue` 인수 설계: `<name>` = 발급할 토큰의 유니크 식별자(`auth_tokens.name`에 저장, service token 범위 내 중복 불가), `--user <username>` 필수 옵션(생략 시 오류 반환, username → user_id 조회 후 `auth_tokens.user_id`에 저장) — **미완료**
|
||||
- `auth_tokens.user_id` 소유권 보정: 기존 bot 토큰 중 `user_id = bot_principal.id`인 행을 `bot_principal.owner_user_id`로 UPDATE 후 `bot_principals` 테이블 DROP — **미완료**
|
||||
- `COALESCE` 우회 로직 단순화: `get_token_principal()`에서 `COALESCE(b.owner_user_id, t.user_id)` → `t.user_id`로 단순화 — **미완료**
|
||||
|
||||
## 영향 범위
|
||||
|
||||
- 코드 영향:
|
||||
- `app/cli/main.py`: ~~typer 등록 변경~~ **완료** (`name="service-token"` 적용)
|
||||
- `app/domain/models.py`: `BotPrincipal` 클래스 삭제, `AuthToken.bot_principal_id` 필드 삭제, `TokenPrincipal.bot_principal_id` 필드 삭제, `token_type` Literal `"bot"` → `"service"`
|
||||
- `app/core/auth.py`: `AuthContext.bot_principal_id` 필드 삭제; `AuthContext.token_type: str | None` 필드 추가(env 토큰은 `None`, DB 토큰은 `TokenPrincipal.token_type` 전달); `_resolve_auth_context()`에서 `principal.bot_principal_id` 할당 제거 및 `principal.token_type` 전달 추가
|
||||
- `app/services/auth_service.py`: `issue_bot_token` → `issue_service_token` rename, bot_principal 조회 로직 제거, `username` 파라미터(필수)로 user_id 직접 조회 후 `auth_tokens` 발급, `revoke_service_token()` bot_principal JOIN 제거; `issue_client_token`, `list_tokens`, `revoke_token`의 `bot_principal_id is not None` 가드를 `auth_context.token_type == "service"` 체크로 교체 — service token은 토큰 발급/목록조회/폐기 불가, 오직 `token_type == "session"` 또는 env 토큰만 허용 (OQ-4 Answered); `_record_audit_event()` 메서드 및 `issue_service_token`, `revoke_service_token` 내 audit event 호출 코드 전체 삭제
|
||||
- `app/repositories/postgres.py`: `create_bot_principal()`, `get_bot_principal()`, `list_bot_principals()` 등 bot_principal 관련 메서드 삭제, `create_auth_token()` bot 예외 로직 제거, `get_token_principal()` COALESCE 및 bot_principals JOIN 제거, `get_user_by_username()` 메서드 활용(또는 신규 추가), `record_audit_event()` 메서드 삭제; **`ensure_schema()` 내 `bot_principals` 테이블 생성 DDL, `bot_principal_id` 컬럼 ALTER, 관련 인덱스(`idx_auth_tokens_bot_created`, `idx_bot_principals_*`) 생성 구문 전체 제거; `auth_audit_logs` 테이블 생성 DDL 및 `idx_auth_audit_logs_bot_occurred` 인덱스 생성 구문 제거** — 미제거 시 ensure_schema() 재실행 때 테이블이 재생성됨
|
||||
- `app/repositories/portfolio_repository.py`: `create_bot_principal`, `get_bot_principal` 인터페이스 메서드 삭제
|
||||
- `app/schemas/auth.py`: `CreateBotPrincipalRequest`, `BotPrincipalResponse` 등 bot_principal 관련 스키마 삭제, `token_type` Literal `"bot"` → `"service"`
|
||||
- `app/api/auth_routes.py`: `/auth/bots`, `/auth/me/bots` 경로 및 관련 핸들러 삭제
|
||||
- `app/core/security.py`: `build_audit_event()` 함수 삭제
|
||||
- `app/cli/commands/bot_tokens.py` → `service_tokens.py`: 파일명 rename, bot_principal 조회 로직 제거, `issue` 명령 인수 `<name>` = 토큰 이름(`auth_tokens.name`에 저장), `--user <username>` 필수 옵션 추가
|
||||
- `app/schemas/auth.py`: `BotTokenIssueCliResponse`(필드 `bot_id` 포함), `BotTokenRevokeCliResponse`(필드 `bot_id` 포함) 삭제 또는 `ServiceTokenIssueCliResponse` / `ServiceTokenRevokeCliResponse`로 rename — `bot_id` 필드 제거 필요
|
||||
- `app/cli/core/output.py`: `render_bot_result()` 함수 삭제 또는 `render_service_token_result()`로 rename
|
||||
- `migrations/versions/`: 신규 마이그레이션 파일 — `auth_tokens.user_id` 보정, `auth_tokens.bot_principal_id` 컬럼 DROP, `bot_principals` 테이블 DROP, `token_type = 'bot'` → `'service'` UPDATE, `auth_tokens.name` 유니크 제약 추가(service token 대상), `auth_audit_logs` 테이블 DROP
|
||||
- `tests/`: `test_auth.py`, `test_cli.py`, `test_users.py`, `test_portfolio_ownership.py` 업데이트
|
||||
- 문서 영향:
|
||||
- `docs/technical/auth-and-token-lifecycle.md`: bot → service 전반 업데이트
|
||||
- `docs/technical/api-contracts.md`: API 경로 업데이트
|
||||
- `docs/technical/management-cli-operations.md`: CLI 명령 업데이트
|
||||
- `README.md`: CLI 섹션 업데이트
|
||||
- 운영 영향:
|
||||
- DB 마이그레이션 실행 필요 (테이블 rename, `user_id` 보정)
|
||||
- API 경로 `/bots` → `/services` 변경으로 기존 클라이언트 호환성 영향 가능
|
||||
|
||||
# 5. 요구사항 목록
|
||||
|
||||
## 기능 요구사항
|
||||
|
||||
- FR-1: `bot_principals` 테이블, `BotPrincipal` 도메인 모델, 관련 스키마/서비스/리포지터리 메서드, API 경로(`/auth/bots`, `/auth/me/bots`), `auth_tokens.bot_principal_id` 컬럼, `auth_audit_logs` 테이블, `record_audit_event()` / `build_audit_event()` / `_record_audit_event()` 함수가 제거되어야 한다.
|
||||
- FR-2: service 토큰 발급 시 `auth_tokens.user_id`에 `--user`로 지정한 username에서 조회한 실제 user_id가 직접 저장되어야 한다.
|
||||
- FR-3: 기존 `auth_tokens` 데이터 중 `user_id`가 `bot_principal.id`인 행은 DB 마이그레이션을 통해 해당 `bot_principal.owner_user_id`로 보정된 후, `bot_principal_id` 컬럼과 `bot_principals` 테이블이 DROP되어야 한다.
|
||||
- FR-4: `get_token_principal()`에서 `COALESCE(b.owner_user_id, t.user_id)` 우회 로직 및 `bot_principals` JOIN이 제거되고 `t.user_id`만 참조하도록 단순화되어야 한다.
|
||||
- FR-5: `pfhub service-token issue <name>` 명령의 `<name>`은 발급할 토큰의 식별자이며 `auth_tokens.name`에 저장된다. service token의 `name`은 유니크해야 하며, 중복 시 오류를 반환해야 한다.
|
||||
- FR-6: `pfhub service-token issue <name> --user <username>` 명령에서 `--user`로 지정한 `username`으로 users 테이블을 조회하여 해당 `user_id`를 `auth_tokens.user_id`로 사용한다. 지정한 username이 DB에 존재하지 않으면 오류를 반환해야 한다.
|
||||
- FR-7: `--user` 옵션은 필수이며, 생략 시 오류를 반환해야 한다.
|
||||
|
||||
## 비기능 요구사항
|
||||
|
||||
- NFR-1: 변경 전후 모든 기존 테스트가 통과해야 한다 (pytest 기준).
|
||||
- NFR-2: DB 마이그레이션은 upgrade/downgrade 양방향을 모두 지원해야 한다.
|
||||
- NFR-3: rename 완료 후 CLI help 텍스트 및 오류 메시지에서 "bot" 잔류 참조가 없어야 한다.
|
||||
|
||||
## 범위 경계
|
||||
|
||||
- In Scope
|
||||
- `bot_principals` 테이블 및 관련 코드 완전 제거 (도메인/스키마/서비스/리포지터리/API/CLI, 약 15개 파일, DB 마이그레이션 포함)
|
||||
- `auth_tokens.token_type = "bot"` → `"service"` rename
|
||||
- `auth_tokens.user_id` 소유권 보정 및 `bot_principal_id` 컬럼 DROP (마이그레이션)
|
||||
- service token 직접 발급 CLI 구현 (`pfhub service-token issue <name> --user <username>`)
|
||||
- 기존 테스트 업데이트 및 신규 검증 테스트 추가
|
||||
- 기술 문서(`api-contracts.md`, `auth-and-token-lifecycle.md`, `README.md`) 업데이트
|
||||
- Out of Scope
|
||||
- 외부 IAM(OAuth/OIDC) 연동
|
||||
- `service_principal` 셀프서비스 UI/포털 제공
|
||||
- token 권한 모델(read/trade-write/admin) 변경
|
||||
- API 경로 변경에 대한 하위 호환성 레이어(alias route) 제공 여부: 제공하지 않음. 아직 운용중인 외부 클라이언트 없음.
|
||||
|
||||
# 6. 리스크/가정 목록
|
||||
|
||||
## 리스크
|
||||
|
||||
- R-1 (High): 기존 `auth_tokens`에 `user_id = bot_principal.id`인 행이 있고, 해당 `bot_principal`의 `owner_user_id`가 NULL이거나 `users` 테이블에 없는 경우 마이그레이션 FK 위반 발생 가능.
|
||||
- 완화 방안: 마이그레이션 upgrade 전 `owner_user_id NULL` 행 검사 쿼리를 실행하고, NULL이 있으면 오류를 내고 중단한다. 운영 전 사전 검증 쿼리를 마이그레이션 파일에 포함한다.
|
||||
- R-2 (Medium): rename 범위가 약 15개 파일에 걸쳐 광범위하므로 일부 누락 시 런타임 `AttributeError`/`NameError` 발생 가능.
|
||||
- 완화 방안: rename 완료 후 `grep -r "bot_principal\|bot_token\|token_type.*bot"` 잔류 참조 검사를 CI에서 실행하거나, 전체 테스트 통과를 gate로 설정한다.
|
||||
- R-3 (Medium): API 경로 `/bots` → `/services` 변경으로 기존 외부 클라이언트(`jsbot-kis` 등)의 호환성이 깨질 수 있다.
|
||||
- 완화 방안: 아직 운용중인 외부 클라이언트 없음.
|
||||
- ~~R-4~~: `auth_audit_logs.bot_id TEXT NOT NULL` 제약 문제 — **해소**: `auth_audit_logs` 테이블 자체를 삭제하므로 제약 처리 불필요.
|
||||
|
||||
## 가정
|
||||
|
||||
- A-1: 현재 운영 중인 bot 토큰은 모두 `owner_user_id`가 설정된 `bot_principal`에 연결되어 있다 (migration 0011에서 backfill 완료). 마이그레이션에서 `auth_tokens.user_id` → `owner_user_id` 보정 후 `bot_principals` DROP이 안전하게 진행 가능하다.
|
||||
- A-2: rename은 동작 변화 없는 순수 구조 변경이므로 기존 테스트를 업데이트하면 전부 통과 가능하다.
|
||||
- A-3: `auth_tokens.user_id` 보정 후 `COALESCE` 로직 제거 시 기존 포트폴리오 소유권 테스트도 통과 가능하다.
|
||||
|
||||
## 오픈 질문
|
||||
|
||||
- OQ-1: API 경로 변경(`/bots` → `/services`) 시 기존 클라이언트 호환성 유지를 위한 alias route를 제공할 것인가?
|
||||
- 상태: Answered
|
||||
- 결정: alias route 제공하지 않음. 현재 운용 중인 외부 클라이언트가 없으므로 즉시 제거한다.
|
||||
- 반영 위치: `#5 Out of Scope` 마지막 항목, R-3 완화 방안 반영 완료.
|
||||
- ~~OQ-2~~: `--user`로 지정한 username의 user_id가 `service_principal.owner_user_id`와 달라도 허용할지 여부 — **Dropped**: `service_principal` 개념 제거로 owner_user_id 비교 자체가 불필요. `--user`로 지정한 임의의 유효 username이 허용된다.
|
||||
- ~~OQ-3~~: `--user` 생략 시 관리자 기본값 결정 방식 — **Dropped**: `--user`를 필수 옵션으로 결정하여 기본값 조회 불필요.
|
||||
|
||||
- ~~OQ-4~~: `AuthService` 내 `bot_principal_id is not None` 가드 처리 방식 — **Answered**: service token으로는 토큰 발급/목록조회/폐기 불가. 오직 session 토큰(계정 로그인) 또는 env 토큰만 허용. 가드를 `auth_context.token_type == "service"` 체크로 교체한다. `AuthContext`에 `token_type: str | None` 필드 추가, `_resolve_auth_context()`에서 `TokenPrincipal.token_type` 전달.
|
||||
- 반영 위치: `app/core/auth.py` AuthContext 변경, `app/services/auth_service.py` 가드 교체 — 영향 범위 항목에 반영 완료.
|
||||
- ~~OQ-5~~: `auth_audit_logs.bot_id` NOT NULL 컬럼 처리 방식 — **Dropped**: `auth_audit_logs` 테이블 자체를 삭제하므로 컬럼 처리 불필요. 발급/폐기 이력은 `auth_tokens`의 `created_at`, `revoked_at` 컬럼으로 확인 가능.
|
||||
|
||||
### 오픈 질문 처리 기준
|
||||
|
||||
- `Open`: 아직 답이 없고 다음 단계 진행에 영향이 있는 상태다. 문서에 유지한다.
|
||||
- `Answered`: 답이 확정된 상태다. 관련 본문/요구사항에 반영한 뒤 오픈 질문 목록에서는 제거하거나 resolved 기록으로 이동한다.
|
||||
- `Deferred`: 지금 단계에서 결정하지 않고 다음 단계 또는 후속 이슈로 넘기는 상태다. 넘길 대상과 이유를 반드시 적는다.
|
||||
- `Dropped`: 더 이상 유효하지 않은 질문이다. 삭제 사유를 짧게 남기고 목록에서 제거한다.
|
||||
|
||||
# 7. 초기 성공 기준
|
||||
|
||||
- S-1: `pytest` 실행 시 모든 테스트 통과 (0 failures, 0 errors)
|
||||
- S-2: `pfhub service-token issue --help` 및 `pfhub service-token revoke --help` 명령이 정상 노출된다. (CLI 명령 그룹 변경 이미 완료 — `name="service-token"` 적용됨)
|
||||
- S-3: DB 마이그레이션 upgrade 후 `auth_tokens` 테이블에서 `user_id`가 `users` 테이블 FK를 정상 참조하는 행만 존재하는 것이 쿼리로 확인된다.
|
||||
|
||||
## 측정 방식 및 데이터 출처
|
||||
|
||||
- S-1 측정 방식: `pytest` 실행 결과 / 데이터 출처: CI 테스트 결과
|
||||
- S-2 측정 방식: `pfhub service-token --help` 출력 수동 확인 / 데이터 출처: 터미널 실행 결과
|
||||
- S-3 측정 방식: `SELECT COUNT(*) FROM auth_tokens WHERE user_id NOT IN (SELECT id FROM users)` 쿼리로 0건 확인 / 데이터 출처: 마이그레이션 후 DB 직접 쿼리
|
||||
|
||||
# 8. Planning으로 넘기기 전 확인 체크
|
||||
|
||||
- [ ] 범위(In Scope/Out of Scope) 확정
|
||||
- 기준: OQ-1 Answered(alias route 미제공), OQ-2 Dropped(service_principal 제거), OQ-3 Dropped — 모든 OQ 해소 완료
|
||||
- [ ] 성공 지표의 측정 방식/데이터 출처 확정
|
||||
- 기준: S-1~S-3 측정 방식 모두 문서화 완료 (위에 초안 작성됨)
|
||||
- [ ] High 영향 리스크 우선순위 및 해소 책임자 지정
|
||||
- 기준: R-1 마이그레이션 사전 검증 쿼리 포함 여부 확정 및 담당자 지정
|
||||
- [ ] 모든 오픈 질문(OQ) `Open` 상태 없음 확인
|
||||
- 기준: OQ 목록에 `Open` 상태 항목이 0개이거나, 잔여 항목 전부 `Deferred` 처리 완료
|
||||
- 기준: OQ-1 Answered, OQ-2 Dropped, OQ-3 Dropped, OQ-4 Answered, OQ-5 Dropped — 모든 OQ 해소 완료
|
||||
- [ ] Freeze 확정 담당자(CONFIRMED_BY) 지정
|
||||
- 기준: 담당자 Justin 지정 완료 — 문서 서명 대기
|
||||
|
||||
# 9. 파일 처리 결과
|
||||
|
||||
- 처리 결과: 생성
|
||||
- 생성 경로: docs/sdlc/sdlc-002_20260423_service-token-user-based/1_discovery.md
|
||||
- 참조 문서:
|
||||
- docs/sdlc/sdlc-001_20240414_cli-bot-token-lifecycle-cli.design/1_discovery.md
|
||||
- docs/technical/auth-and-token-lifecycle.md
|
||||
- app/cli/main.py line 53 (service-token 등록 완료 확인)
|
||||
- app/services/auth_service.py (issue_bot_token 구현 — user_id = bot.id 저장)
|
||||
- app/repositories/postgres.py (get_token_principal COALESCE 우회 로직)
|
||||
|
||||
# 10. 사용자 결정 필요 항목 요약
|
||||
|
||||
## DECIDE
|
||||
|
||||
- ~~DECIDE_API_COMPAT~~: **결정 완료** — alias route 제공하지 않음. 운용 중인 외부 클라이언트가 없으므로 즉시 제거. (OQ-1 Answered)
|
||||
- ~~DECIDE_USER_OWNERSHIP_CHECK~~: **결정 완료** — service_principal 개념 제거로 owner_user_id 비교 불필요. 임의의 유효 username 허용. (OQ-2 Dropped)
|
||||
- ~~DECIDE_DEFAULT_USER~~: **결정 완료** — `--user` 필수 옵션으로 확정, 기본값 없음.
|
||||
|
||||
## CONFIRM
|
||||
|
||||
- Discovery Freeze 확정 담당자 : Justin
|
||||
|
||||
## DATA
|
||||
|
||||
- (해당 없음)
|
||||
|
||||
# 11. Discovery Freeze
|
||||
|
||||
## 섹션 참조
|
||||
|
||||
- 범위: [# 5. 요구사항 목록 > 범위 경계](#5-요구사항-목록)
|
||||
- 성공 기준: [# 7. 초기 성공 기준](#7-초기-성공-기준)
|
||||
- 리스크: [# 6. 리스크/가정 목록](#6-리스크가정-목록)
|
||||
|
||||
## Handoff Decision
|
||||
|
||||
- 승인
|
||||
|
||||
## Ready for Planning
|
||||
|
||||
- true
|
||||
|
||||
## Confirmed By
|
||||
|
||||
- Justin
|
||||
|
||||
## Confirmed At (KST)
|
||||
|
||||
- 2026-04-23
|
||||
|
||||
## Handoff Rationale
|
||||
|
||||
- 코드 검토에서 발견된 OQ-4, OQ-5 모두 해소 완료.
|
||||
- OQ-4 Answered: service token은 토큰 조작 불가, session/env 토큰만 허용. `AuthContext.token_type` 필드 추가로 반영.
|
||||
- OQ-5 Dropped: `auth_audit_logs` 테이블 자체 삭제 결정으로 bot_id 컬럼 처리 불필요. 이력은 `auth_tokens`로 충분.
|
||||
- OQ-1 Answered, OQ-2 Dropped, OQ-3 Dropped, OQ-4 Answered, OQ-5 Dropped — 잔여 Open 없음.
|
||||
- Freeze 확정 담당자 Justin 지정 완료.
|
||||
- 핵심 설계 결정: `bot_principals` 테이블 완전 제거, `auth_audit_logs` 테이블 삭제, service token은 `auth_tokens` 단일 엔티티로 `<name>`(유니크) + `--user <username>` 기반 직접 발급.
|
||||
@@ -1,250 +0,0 @@
|
||||
# 0. 문서 상태
|
||||
이 문서의 현재 상태와 식별 정보를 기록한다.
|
||||
|
||||
- 상태: confirmed
|
||||
- 문서 ID: sdlc-002_20260423_service-token-user-based
|
||||
- 이슈명: service-token user-based 소유권
|
||||
- 참조 Discovery: docs/sdlc/sdlc-002_20260423_service-token-user-based/1_discovery.md
|
||||
- 작성 시각(KST): 2026-04-23
|
||||
- 마지막 갱신 시각(KST): 2026-04-23 (confirm-planning 실행 — Planning 최종 승인, Design 진입 허가)
|
||||
|
||||
# 1. Discovery 인계 요약
|
||||
Discovery에서 확정된 핵심 내용을 요약한다. 원본은 1_discovery.md를 참조하며, 이 섹션은 Planning 작업의 입력 기준선이 된다.
|
||||
|
||||
## 이슈 목적
|
||||
`bot_principals` 테이블과 audit log를 제거하고, service token을 `auth_tokens` 단일 엔티티로 직접 발급하도록 재구현한다. CLI 명령 `pfhub service-token issue <name> --user <username>`으로 사용자 기반 소유권을 명확히 한다.
|
||||
|
||||
## 확정 범위
|
||||
- In Scope
|
||||
- `bot_principals` 테이블 및 관련 코드 완전 제거 (도메인/스키마/서비스/리포지터리/API/CLI, 약 15개 파일, DB 마이그레이션 포함)
|
||||
- `auth_audit_logs` 테이블 및 audit 함수(`build_audit_event`, `record_audit_event`, `_record_audit_event`) 삭제
|
||||
- `auth_tokens.token_type = "bot"` → `"service"` rename (기존 데이터 UPDATE 포함)
|
||||
- `auth_tokens.user_id` 소유권 보정 및 `bot_principal_id` 컬럼 DROP (마이그레이션)
|
||||
- service token 직접 발급 CLI 구현 (`pfhub service-token issue <name> --user <username>`)
|
||||
- `AuthContext.token_type` 필드 추가, service token 가드 교체
|
||||
- 기존 테스트 업데이트 및 신규 검증 테스트 추가
|
||||
- 기술 문서(`api-contracts.md`, `auth-and-token-lifecycle.md`, `README.md`) 업데이트
|
||||
- Out of Scope
|
||||
- 외부 IAM(OAuth/OIDC) 연동
|
||||
- service_principal 셀프서비스 UI/포털 제공
|
||||
- token 권한 모델(read/trade-write/admin) 변경
|
||||
- API 경로 변경에 대한 하위 호환성 레이어(alias route) 제공 — 외부 클라이언트 없으므로 즉시 제거
|
||||
|
||||
## 기능 요구사항 (FR) 요약
|
||||
- FR-1: `bot_principals` 테이블, `BotPrincipal` 모델, 관련 스키마/서비스/리포지터리/API, `auth_tokens.bot_principal_id` 컬럼, `auth_audit_logs` 테이블, audit 함수 전체 제거
|
||||
- FR-2: service 토큰 발급 시 `auth_tokens.user_id`에 username에서 조회한 실제 user_id 직접 저장
|
||||
- FR-3: 기존 bot 토큰 `user_id` → `bot_principal.owner_user_id`로 마이그레이션 보정 후 `bot_principals` DROP
|
||||
- FR-4: `get_token_principal()` COALESCE/JOIN 제거 → `t.user_id` 단순 참조
|
||||
- FR-5: service token `name` 유니크, 중복 시 오류 반환
|
||||
- FR-6: `--user <username>` → users 테이블 조회 → `auth_tokens.user_id`로 저장, 존재하지 않는 username이면 오류
|
||||
- FR-7: `--user` 필수 옵션, 생략 시 오류
|
||||
|
||||
## 비기능 요구사항 (NFR) 요약
|
||||
- NFR-1: 변경 전후 모든 테스트 통과 (pytest 기준)
|
||||
- NFR-2: DB 마이그레이션 upgrade/downgrade 양방향 지원
|
||||
- NFR-3: 변경 완료 후 CLI help 텍스트 및 오류 메시지에서 "bot" 잔류 참조 없음
|
||||
|
||||
## 이월된 오픈 질문 / 리스크
|
||||
Discovery에서 Deferred 처리된 항목 없음. 모든 OQ 해소 완료 (OQ-1 Answered, OQ-2/3/5 Dropped, OQ-4 Answered).
|
||||
|
||||
# 2. 작업 분해 (Task Breakdown)
|
||||
FR/NFR 각각을 실행 가능한 단위 태스크로 분해한다.
|
||||
|
||||
| ID | 태스크 제목 | 유형 | 참조 요구사항 | 의존 태스크 | 담당자 |
|
||||
|-----|-----------|------|--------------|------------|--------|
|
||||
| T-1 | DB 마이그레이션 파일 작성 | 개발 | FR-3, NFR-2 | - | copilot |
|
||||
| T-2 | 도메인/스키마/인터페이스 정리 | 개발 | FR-1 | - | copilot |
|
||||
| T-3 | postgres.py 구현체 정리 | 개발 | FR-1, FR-4 | T-2 | copilot |
|
||||
| T-4 | AuthContext/security.py 변경 | 개발 | FR-1 (OQ-4) | T-2 | copilot |
|
||||
| T-5 | AuthService 재구현 | 개발 | FR-2, FR-5, FR-6, FR-7 | T-3, T-4 | copilot |
|
||||
| T-6 | API 경로 정리 | 개발 | FR-1 | T-5 | copilot |
|
||||
| T-7 | CLI 재구현 | 개발 | FR-5, FR-6, FR-7, NFR-3 | T-5 | copilot |
|
||||
| T-8 | 테스트 업데이트 | 개발 | NFR-1 | T-5, T-6, T-7 | copilot |
|
||||
| T-9 | 기술 문서 업데이트 | 문서 | - | T-7, T-8 | copilot |
|
||||
|
||||
## 태스크 완료 조건 (DoD)
|
||||
|
||||
- T-1: `alembic upgrade head` 실행 시 `bot_principals`, `auth_audit_logs` 테이블이 DROP되고, `auth_tokens.user_id`가 users FK를 정상 참조하며, `token_type = 'service'`로 UPDATE됨. `alembic downgrade -1` 실행 시 역방향 복원 가능.
|
||||
- T-2: `BotPrincipal`, `AuthToken.bot_principal_id`, `TokenPrincipal.bot_principal_id`, `CreateBotPrincipalRequest`, `BotPrincipalResponse`, `BotTokenIssueCliResponse.bot_id`, `BotTokenRevokeCliResponse.bot_id` 삭제/정리 완료. `PortfolioRepository` 프로토콜에서 bot_principal 메서드 제거. `python -c "from app.domain.models import AuthToken"` import 오류 없음.
|
||||
- T-3: `ensure_schema()` 재실행 시 `bot_principals`, `auth_audit_logs` 테이블 미생성. `get_token_principal()`이 단일 테이블 `auth_tokens`만 참조. `record_audit_event()` 메서드 삭제 완료.
|
||||
- T-4: `AuthContext`에 `token_type: str | None` 필드 추가 완료. `_resolve_auth_context()`가 DB 토큰 조회 시 `principal.token_type`을 전달. `build_audit_event()` 함수 삭제 완료.
|
||||
- T-5: `issue_service_token(name, username, ...)` 메서드가 존재하고, username → user_id 조회 후 `auth_tokens` 직접 발급. bot_principal 관련 메서드 전체 삭제. `issue_client_token`, `list_tokens`, `revoke_token`의 가드가 `auth_context.token_type == "service"` 체크로 교체됨.
|
||||
- T-6: FastAPI 앱 기동 후 `/auth/bots`, `/auth/me/bots` 경로 미노출. `curl /openapi.json`에서 해당 경로 없음.
|
||||
- T-7: `pfhub service-token issue --help`에서 `<name>`, `--user` 인수 노출. help 텍스트에 "bot" 문자열 없음. `pfhub service-token issue kis-token --user justin` 명령이 토큰을 발급하고 출력.
|
||||
- T-8: `pytest` 실행 결과 0 failures, 0 errors. service token 발급/폐기 시나리오 테스트 신규 추가.
|
||||
- T-9: `auth-and-token-lifecycle.md`, `api-contracts.md`, `management-cli-operations.md`, `README.md`에서 "bot_principal", "bot token" 잔류 참조 없음. `pfhub service-token` 명령 예제 포함.
|
||||
|
||||
# 3. 공수 추정
|
||||
|
||||
| ID | 태스크 제목 | 추정 공수 | 추정 단위 | 비고 |
|
||||
|-----|-----------|---------|---------|------|
|
||||
| T-1 | DB 마이그레이션 파일 작성 | 2 | SP | user_id 보정 로직 + downgrade 구현 포함 |
|
||||
| T-2 | 도메인/스키마/인터페이스 정리 | 2 | SP | 다수 파일이나 삭제 위주 — 복잡도 낮음 |
|
||||
| T-3 | postgres.py 구현체 정리 | 2 | SP | ensure_schema DDL 제거 + get_token_principal 단순화 — 주의 필요 |
|
||||
| T-4 | AuthContext/security.py 변경 | 1 | SP | 변경 범위 소규모 |
|
||||
| T-5 | AuthService 재구현 | 3 | SP | username 조회 신규 로직 + 가드 교체 + bot 메서드 삭제 |
|
||||
| T-6 | API 경로 정리 | 1 | SP | 삭제 위주 |
|
||||
| T-7 | CLI 재구현 | 2 | SP | 파일 rename + 인수 변경 + 출력 함수 rename |
|
||||
| T-8 | 테스트 업데이트 | 3 | SP | 4개 파일 업데이트 + 신규 시나리오 추가 |
|
||||
| T-9 | 기술 문서 업데이트 | 1 | SP | 내용 갱신 위주 |
|
||||
|
||||
- 총 추정 공수: 17 SP
|
||||
- 추정 신뢰도: Medium
|
||||
- 추정 근거: sdlc-001(CLI bot token 발급 구현, 약 8 SP)과 유사한 레이어 범위이나 삭제 중심 + 기존 테스트 보정 작업이 추가됨. T-5/T-8이 가장 불확실 요소.
|
||||
|
||||
# 4. 일정 계획
|
||||
|
||||
## 마일스톤
|
||||
| 마일스톤 | 목표 날짜 | 포함 태스크 | 완료 조건 |
|
||||
|---------|---------|-----------|---------|
|
||||
| M-1: 정리 완료 | 2026-04-25 | T-1, T-2, T-3, T-4 | bot_principals/auth_audit_logs 관련 코드·스키마·DDL 전체 삭제, import 오류 없음 |
|
||||
| M-2: 구현·검증 완료 | 2026-04-30 | T-5, T-6, T-7, T-8, T-9 | pytest 0 failures, CLI --help 정상 노출, 문서 갱신 완료 |
|
||||
|
||||
## 이터레이션 계획
|
||||
- 이터레이션 단위: 없음(단일 배포)
|
||||
- 이터레이션 1 (2026-04-23 ~ 2026-04-24): T-1, T-2 (병렬 가능)
|
||||
- 이터레이션 2 (2026-04-24 ~ 2026-04-25): T-3, T-4 (T-2 완료 후)
|
||||
- 이터레이션 3 (2026-04-25 ~ 2026-04-26): T-5 (T-3, T-4 완료 후)
|
||||
- 이터레이션 4 (2026-04-27): T-6, T-7 (병렬 가능, T-5 완료 후)
|
||||
- 이터레이션 5 (2026-04-28 ~ 2026-04-30): T-8, T-9 (T-6, T-7 완료 후)
|
||||
|
||||
# 5. 의존성 및 제약
|
||||
|
||||
## 태스크 간 의존성
|
||||
- T-1은 T-2와 독립적으로 병렬 진행 가능
|
||||
- T-3, T-4는 T-2 완료 후 시작 가능 (인터페이스 정리 선행 필요)
|
||||
- T-5는 T-3, T-4 완료 후 시작 가능 (repository 메서드, AuthContext 변경 선행 필요)
|
||||
- T-6, T-7은 T-5 완료 후 병렬 시작 가능
|
||||
- T-8은 T-5, T-6, T-7 완료 후 시작 가능
|
||||
- T-9는 T-8 완료 후 시작 (최종 동작 확인 후 문서화)
|
||||
|
||||
## 외부 의존성
|
||||
- DB 접속 가능 환경 필요 (마이그레이션 실행 및 S-3 검증)
|
||||
- pytest 실행 환경 (Docker Compose 또는 로컬 DB)
|
||||
|
||||
## 기술/환경 제약
|
||||
- Alembic 마이그레이션 파일명 패턴: `YYYYMMDD_NNNN_<slug>.py` (기존 패턴 준수)
|
||||
- `ensure_schema()`는 테스트 환경에서도 호출되므로 DDL 제거 후 테스트 환경에서 재실행 검증 필요
|
||||
- `app/cli/commands/bot_tokens.py` → `service_tokens.py` 파일 rename 시 `app/cli/main.py`의 import 경로도 함께 수정 필요
|
||||
|
||||
# 6. 검증 계획 개요
|
||||
Discovery의 성공 기준과 연결해 검증 방법 초안을 적는다.
|
||||
|
||||
| 성공 기준 | 검증 방법 초안 | 담당자 |
|
||||
|---------|-------------|--------|
|
||||
| S-1: pytest 0 failures | `pytest` 전체 실행 후 결과 확인 | copilot |
|
||||
| S-2: pfhub service-token --help 정상 노출 | 터미널에서 `pfhub service-token issue --help` 실행, `<name>` 및 `--user` 인수 확인 | copilot |
|
||||
| S-3: auth_tokens.user_id FK 정합성 | 마이그레이션 upgrade 후 `SELECT COUNT(*) FROM auth_tokens WHERE user_id NOT IN (SELECT id FROM users)` = 0 확인 | copilot |
|
||||
|
||||
# 7. 리스크
|
||||
|
||||
## 리스크
|
||||
- R-1 (High): 기존 `auth_tokens`에 `user_id = bot_principal.id`인 행의 `bot_principal.owner_user_id`가 NULL이거나 `users` 테이블에 없으면 마이그레이션 FK 위반 발생.
|
||||
- 완화 방안: T-1 구현 시 upgrade 전 사전 검증 쿼리(`SELECT COUNT(*) FROM bot_principals WHERE owner_user_id IS NULL`) 포함. NULL 존재 시 오류 후 중단.
|
||||
- R-2 (Medium): 약 15개 파일 rename 중 일부 누락 시 런타임 `AttributeError`/`NameError`.
|
||||
- 완화 방안: T-8 완료 후 `grep -r "bot_principal\|BotPrincipal\|bot_token\|token_type.*bot"` 잔류 참조 검사. 테스트 전체 통과를 gate로 설정.
|
||||
- R-3 (Medium): ensure_schema() DDL 제거 누락 시 재실행 때 bot_principals 테이블 재생성.
|
||||
- 완화 방안: T-3 DoD에 ensure_schema() 재실행 검증 포함.
|
||||
|
||||
## 가정
|
||||
- PA-1: 현재 운영 중인 bot 토큰은 모두 `owner_user_id`가 설정된 `bot_principal`에 연결되어 있다 (migration 0011 backfill 완료). user_id 보정 후 DROP이 안전하다.
|
||||
- PA-2: `app/cli/main.py`의 `name="service-token"` 등록은 이미 완료되어 있으므로 T-7에서 import 경로만 수정하면 된다.
|
||||
- PA-3: 테스트 환경에서 Docker Compose DB 접속이 가능하다.
|
||||
|
||||
## 오픈 질문
|
||||
없음. Discovery에서 모든 OQ 해소 완료.
|
||||
|
||||
# 8. Design으로 넘기기 전 확인 체크
|
||||
|
||||
- [x] 전체 태스크 목록 확정 및 담당자 지정
|
||||
- 기준: T-1 ~ T-9 모든 태스크에 담당자(copilot) 지정 완료
|
||||
- [x] 공수 추정 완료 및 총 공수 확인
|
||||
- 기준: 17 SP — 모든 태스크 추정값 채움 완료
|
||||
- [x] 마일스톤 및 이터레이션 계획 확정
|
||||
- 기준: M-1 목표 2026-04-25, M-2 목표 2026-04-30, 이터레이션 1~5 기간 확정 완료
|
||||
- [ ] 외부 의존성 해소 계획 확인
|
||||
- 기준: DB 접속 환경 및 pytest 실행 환경 확인
|
||||
- [x] 모든 오픈 질문(OQ) `Open` 상태 없음 확인
|
||||
- 기준: 없음 — Discovery에서 전체 해소 완료
|
||||
- [ ] Freeze 확정 담당자(CONFIRMED_BY) 지정
|
||||
- 기준: 담당자 Justin 지정 완료 — 문서 서명 대기
|
||||
|
||||
# 9. 파일 처리 결과
|
||||
|
||||
- 처리 결과: 생성
|
||||
- 생성 경로: docs/sdlc/sdlc-002_20260423_service-token-user-based/2_planning.md
|
||||
- 참조 문서:
|
||||
- docs/sdlc/sdlc-002_20260423_service-token-user-based/1_discovery.md
|
||||
- app/domain/models.py
|
||||
- app/core/auth.py
|
||||
- app/core/security.py
|
||||
- app/services/auth_service.py
|
||||
- app/repositories/postgres.py
|
||||
- app/repositories/portfolio_repository.py
|
||||
- app/schemas/auth.py
|
||||
- app/api/auth_routes.py
|
||||
- app/cli/commands/bot_tokens.py
|
||||
- app/cli/core/output.py
|
||||
|
||||
# 10. 사용자 결정 필요 항목 요약
|
||||
|
||||
## DECIDE
|
||||
- (없음) — Discovery 범위에서 모든 설계 결정 완료
|
||||
|
||||
## CONFIRM
|
||||
- T-1 ~ T-9 담당자 지정: `copilot`으로 일괄 지정 완료 (run-sdlc 자동 처리)
|
||||
- M-1 목표 날짜: `2026-04-25`, M-2 목표 날짜: `2026-04-30` — 확정 완료
|
||||
- 검증 담당자(`VERIFY_OWNER_1~3`): 지정 필요 (현재 TBD 유지)
|
||||
- Freeze 확정 담당자(`CONFIRMED_BY`): Justin — confirm-planning 단계에서 처리
|
||||
|
||||
## DATA
|
||||
- 마이그레이션 실행 전 운영 DB `bot_principals` 행 수 및 `owner_user_id NULL` 행 수 확인 필요 (R-1 완화)
|
||||
|
||||
# 11. Planning Freeze
|
||||
|
||||
## 섹션 참조
|
||||
- 태스크 분해: [# 2. 작업 분해](#2-작업-분해)
|
||||
- 일정 계획: [# 4. 일정 계획](#4-일정-계획)
|
||||
- 리스크: [# 7. 리스크](#7-리스크)
|
||||
|
||||
## Handoff Decision
|
||||
|
||||
- 승인
|
||||
|
||||
## Ready for Design
|
||||
|
||||
- true
|
||||
|
||||
## Confirmed By
|
||||
|
||||
- Justin
|
||||
|
||||
## Confirmed At (KST)
|
||||
|
||||
- 2026-04-23
|
||||
|
||||
## Handoff Rationale
|
||||
|
||||
- T-1~T-9 전체 태스크 분해 완료, 담당자 copilot 일괄 지정.
|
||||
- M-1(2026-04-25), M-2(2026-04-30) 마일스톤 일정 확정 및 5개 이터레이션 계획 수립.
|
||||
- Discovery FR-1~FR-7, NFR-1~NFR-3 전체 태스크 맵핑 확인, OQ 전체 해소 확인.
|
||||
- R-1 (High) 마이그레이션 FK 위반 리스크 완화 방안(T-1 사전 검증 쿼리) DoD에 포함.
|
||||
- 성공 기준 S-1~S-3 모두 검증 방법 정의 완료, 보과 플레이스홀더 없음.
|
||||
|
||||
# 11. Planning Freeze
|
||||
|
||||
## 섹션 참조
|
||||
- 작업 분해: [# 2. 작업 분해](#2-작업-분해-task-breakdown)
|
||||
- 공수/일정: [# 3. 공수 추정](#3-공수-추정), [# 4. 일정 계획](#4-일정-계획)
|
||||
- 의존성: [# 5. 의존성 및 제약](#5-의존성-및-제약)
|
||||
- 검증 계획: [# 6. 검증 계획 개요](#6-검증-계획-개요)
|
||||
- 리스크/오픈 질문: [# 7. 리스크](#7-리스크)
|
||||
- 인계 전 확인: [# 8. Design으로 넘기기 전 확인 체크](#8-design으로-넘기기-전-확인-체크)
|
||||
- 사용자 결정 필요: [# 10. 사용자 결정 필요 항목 요약](#10-사용자-결정-필요-항목-요약)
|
||||
|
||||
## Freeze 플래그
|
||||
- Handoff Decision: 보류
|
||||
- Handoff Rationale: 담당자(TASK_OWNER_1~9), 마일스톤 날짜(MILESTONE_1_DATE, MILESTONE_2_DATE), 이터레이션 기간(ITERATION_1~5_PERIOD)이 TBD 상태. 확정 후 인계 가능.
|
||||
- Ready for Design: false
|
||||
- Confirmed By: {{CONFIRMED_BY:TBD}}
|
||||
- Confirmed At (KST): {{CONFIRMED_AT_KST:TBD}}
|
||||
@@ -1,468 +0,0 @@
|
||||
# 0. 문서 상태
|
||||
이 문서의 현재 상태와 식별 정보를 기록한다.
|
||||
|
||||
- 상태: confirmed
|
||||
- 문서 ID: sdlc-002_20260423_service-token-user-based
|
||||
- 이슈명: service-token user-based 소유권
|
||||
- 참조 Planning: docs/sdlc/sdlc-002_20260423_service-token-user-based/2_planning.md
|
||||
- 작성 시각(KST): 2026-04-23
|
||||
- 마지막 갱신 시각(KST): 2026-04-23 (confirm-design 실행 — Design 최종 승인, Implementation 진입 허가)
|
||||
|
||||
# 1. Planning 인계 요약
|
||||
Planning에서 확정된 핵심 내용을 요약한다. 원본은 2_planning.md를 참조하며, 이 섹션은 Design 작업의 입력 기준선이 된다.
|
||||
|
||||
## 이슈 목적
|
||||
`bot_principals` 테이블과 `auth_audit_logs` 테이블을 제거하고, service token을 `auth_tokens` 단일 엔티티로 직접 발급하도록 재구현한다. CLI 명령 `pfhub service-token issue <name> --user <username>`으로 사용자 기반 소유권을 명확히 한다.
|
||||
|
||||
## 확정 태스크 목록
|
||||
| ID | 태스크 제목 | 참조 요구사항 | 담당자 |
|
||||
|----|-----------|------------|--------|
|
||||
| T-1 | DB 마이그레이션 파일 작성 | FR-3, NFR-2 | copilot |
|
||||
| T-2 | 도메인/스키마/인터페이스 정리 | FR-1 | copilot |
|
||||
| T-3 | postgres.py 구현체 정리 | FR-1, FR-4 | copilot |
|
||||
| T-4 | AuthContext/security.py 변경 | FR-1 (OQ-4) | copilot |
|
||||
| T-5 | AuthService 재구현 | FR-2, FR-5, FR-6, FR-7 | copilot |
|
||||
| T-6 | API 경로 정리 | FR-1 | copilot |
|
||||
| T-7 | CLI 재구현 | FR-5, FR-6, FR-7, NFR-3 | copilot |
|
||||
| T-8 | 테스트 업데이트 | NFR-1 | copilot |
|
||||
| T-9 | 기술 문서 업데이트 | - | copilot |
|
||||
|
||||
## 이월된 오픈 질문 / 리스크
|
||||
없음. Planning 단계에서 모든 OQ 해소 완료.
|
||||
|
||||
# 2. 아키텍처 및 구조 설계
|
||||
변경이 미치는 전체 구조와 주요 컴포넌트 관계를 설명한다.
|
||||
|
||||
## 전체 구조 개요
|
||||
시스템은 아래 5개 레이어로 구성되며, 이번 변경은 모든 레이어에 걸친 `bot_principal` 엔티티 제거와 service token 직접 발급 모델로의 전환이다.
|
||||
|
||||
```
|
||||
CLI (app/cli/)
|
||||
↓ service_tokens.py (bot_tokens.py 대체)
|
||||
Service (app/services/auth_service.py)
|
||||
↓ issue_service_token() / revoke_service_token()
|
||||
Repository (app/repositories/postgres.py)
|
||||
↓ auth_tokens 직접 INSERT, bot_principals/auth_audit_logs 테이블 미사용
|
||||
DB (migrations/versions/)
|
||||
auth_tokens (user_id = 실제 user.id, token_type = 'service')
|
||||
[제거] bot_principals, auth_audit_logs
|
||||
```
|
||||
|
||||
변경 전: `auth_tokens.user_id` ← `bot_principal.id` (봇 자체 UUID, JOIN으로 소유자 추적)
|
||||
변경 후: `auth_tokens.user_id` ← `users.id` (실제 소유 사용자 직접 참조)
|
||||
|
||||
## 주요 컴포넌트
|
||||
| 컴포넌트 | 역할 | 영향 범위 |
|
||||
|---------|------|---------|
|
||||
| `app/domain/models.py` | 도메인 모델 정의 | `BotPrincipal` 삭제, `AuthToken.bot_principal_id` 삭제, `TokenPrincipal.bot_principal_id` 삭제, `token_type` Literal 변경 |
|
||||
| `app/schemas/auth.py` | API/CLI 요청·응답 스키마 | bot_principal 관련 스키마 삭제, `AuthTokenResponse.token_type` 갱신, `ServiceTokenIssueCliResponse` / `ServiceTokenRevokeCliResponse` 추가 |
|
||||
| `app/repositories/portfolio_repository.py` | Repository Protocol 인터페이스 | bot_principal 관련 메서드 삭제, `get_service_token_by_name()` 추가 |
|
||||
| `app/repositories/postgres.py` | DB 접근 구현체 | `ensure_schema()` bot 관련 DDL 제거, `get_token_principal()` JOIN 단순화, bot/audit 메서드 삭제, `get_service_token_by_name()` 추가 |
|
||||
| `app/core/auth.py` | 인증 컨텍스트 해석 | `AuthContext.bot_principal_id` 삭제, `AuthContext.token_type` 추가 |
|
||||
| `app/core/security.py` | 암호화·토큰 유틸 | `build_audit_event()` 삭제 |
|
||||
| `app/services/auth_service.py` | 인증 비즈니스 로직 | bot 관련 메서드 삭제, `issue_service_token()` / `revoke_service_token()` 추가, 가드 교체 |
|
||||
| `app/api/auth_routes.py` | FastAPI 라우터 | `/auth/bots`, `/auth/me/bots` 경로 삭제 |
|
||||
| `app/cli/commands/bot_tokens.py` | CLI 명령 (구 파일) | `app/cli/commands/service_tokens.py`로 교체 |
|
||||
| `app/cli/commands/service_tokens.py` | CLI 명령 (신규 파일) | `pfhub service-token issue <name> --user <username>` / `revoke <name>` |
|
||||
| `app/cli/core/output.py` | CLI 출력 헬퍼 | `render_bot_result()` → `render_service_token_result()`로 rename |
|
||||
| `app/cli/main.py` | CLI 진입점 | import 경로 변경 (`bot_tokens` → `service_tokens`) |
|
||||
| `migrations/versions/` | DB 마이그레이션 | `20260423_0013_remove_bot_principals_service_tokens.py` 신규 생성 |
|
||||
|
||||
## 데이터 흐름
|
||||
|
||||
### service token 발급 흐름 (변경 후)
|
||||
1. CLI: `pfhub service-token issue <name> --user <username>` 실행
|
||||
2. CLI: `AuthService.issue_service_token(name=name, username=username, ...)` 호출
|
||||
3. Service: `repository.get_user_by_username(username)` → user 조회 (없으면 ValueError)
|
||||
4. Service: `repository.create_auth_token(user.id, name=name, token_type="service", ...)` 실행
|
||||
- name uniqueness는 DB `partial unique index ON auth_tokens(name) WHERE token_type='service'`로 보장
|
||||
5. Service: `IssuedServiceToken(token=raw_token, auth_token=auth_token)` 반환
|
||||
6. CLI: 결과 출력 (ServiceTokenIssueCliResponse)
|
||||
|
||||
### service token 인증 흐름 (변경 후)
|
||||
1. HTTP 요청: `Authorization: Bearer <service_token>` 헤더 포함
|
||||
2. `_resolve_auth_context()`: `repository.get_token_principal(hash_token(token))` 호출
|
||||
3. `get_token_principal()`: 단순 `SELECT id, user_id, permission, token_type FROM auth_tokens WHERE token_hash = %s AND ...` (JOIN 없음)
|
||||
4. `AuthContext(token_type="service", user_id=<실제 user_id>, ...)` 생성
|
||||
5. service token 가드: `auth_context.token_type == "service"` → 토큰 발급/목록/폐기 조작 차단
|
||||
|
||||
# 3. 상세 설계
|
||||
태스크(T-N)별로 구체적인 구현/변경 방법을 기술한다.
|
||||
|
||||
## T-1: DB 마이그레이션 파일 작성
|
||||
- 설계 방향: Alembic 마이그레이션으로 DB 구조를 변경한다. upgrade 전 사전 검증 쿼리를 포함하여 NULL owner_user_id로 인한 FK 위반을 사전 차단한다.
|
||||
- 변경 대상: `migrations/versions/20260423_0013_remove_bot_principals_service_tokens.py` (신규 생성)
|
||||
- 입력/출력:
|
||||
- upgrade() 실행 순서:
|
||||
1. `SELECT COUNT(*) FROM bot_principals WHERE owner_user_id IS NULL` → 0이 아니면 `RuntimeError`로 중단
|
||||
2. `UPDATE auth_tokens t SET user_id = b.owner_user_id FROM bot_principals b WHERE t.bot_principal_id = b.id AND t.bot_principal_id IS NOT NULL` — user_id 보정
|
||||
3. `UPDATE auth_tokens SET token_type = 'service' WHERE token_type = 'bot'` — token_type rename
|
||||
4. `CREATE UNIQUE INDEX IF NOT EXISTS idx_auth_tokens_service_name_unique ON auth_tokens(name) WHERE token_type = 'service'` — service token name 유니크 제약
|
||||
5. `ALTER TABLE auth_tokens DROP COLUMN IF EXISTS bot_principal_id`
|
||||
6. `DROP TABLE IF EXISTS auth_audit_logs`
|
||||
7. `DROP TABLE IF EXISTS bot_principals`
|
||||
- downgrade() 실행 순서 (역방향 복원):
|
||||
1. `CREATE TABLE bot_principals (id TEXT PRIMARY KEY, name TEXT NOT NULL, owner_user_id TEXT, permission TEXT NOT NULL, description TEXT, is_active BOOLEAN DEFAULT TRUE, created_at TIMESTAMPTZ, updated_at TIMESTAMPTZ, deactivated_at TIMESTAMPTZ)`
|
||||
2. `CREATE TABLE auth_audit_logs (id TEXT PRIMARY KEY, event_type TEXT NOT NULL, action TEXT NOT NULL, actor_id TEXT, bot_id TEXT NOT NULL, token_id_masked TEXT, token_ref_masked TEXT, result TEXT, detail TEXT, payload JSONB, occurred_at TIMESTAMPTZ NOT NULL)`
|
||||
3. `ALTER TABLE auth_tokens ADD COLUMN IF NOT EXISTS bot_principal_id TEXT`
|
||||
4. `UPDATE auth_tokens SET token_type = 'bot' WHERE token_type = 'service'`
|
||||
5. `DROP INDEX IF EXISTS idx_auth_tokens_service_name_unique`
|
||||
6. (user_id 역방향 보정 불가 — bot_principals 데이터 없음. downgrade는 구조 복원만 수행하며 데이터 보정은 생략)
|
||||
- 예외/엣지 케이스: upgrade 전 bot_principals.owner_user_id NULL 검사가 실패하면 마이그레이션 중단. 이 경우 운영자가 수동으로 NULL 행을 보정해야 한다.
|
||||
|
||||
## T-2: 도메인/스키마/인터페이스 정리
|
||||
- 설계 방향: bot_principal 관련 클래스·필드를 일괄 삭제하고 service token용 CLI 응답 스키마를 추가한다.
|
||||
- 변경 대상:
|
||||
- `app/domain/models.py`:
|
||||
- `BotPrincipal` 데이터클래스 전체 삭제
|
||||
- `AuthToken.bot_principal_id: str | None` 필드 삭제
|
||||
- `TokenPrincipal.bot_principal_id: str | None` 필드 삭제
|
||||
- `AuthToken.token_type` 타입 힌트: `str` 유지 (Literal 미사용 — 기존과 동일)
|
||||
- `app/schemas/auth.py`:
|
||||
- 삭제 대상: `CreateBotPrincipalRequest`, `CreateSelfBotPrincipalRequest`, `BotPrincipalResponse`, `CreateBotTokenRequest`, `IssuedBotTokenResponse`, `BotTokenIssueCliResponse`, `BotTokenRevokeCliResponse`
|
||||
- 변경 대상: `AuthTokenResponse.token_type` Literal — `"bot"` 제거, `"service"` 추가 (`Literal["session", "client", "service"]`)
|
||||
- 변경 대상: `AuthTokenResponse.bot_principal_id: str | None` 필드 삭제
|
||||
- 추가 대상: `ServiceTokenIssueCliResponse(token_id: str, name: str, expires_at: datetime | None, revoked: bool, token: str | None)`, `ServiceTokenRevokeCliResponse(token_id: str, name: str, revoked: bool, revoked_at: datetime | None)`
|
||||
- `app/repositories/portfolio_repository.py`:
|
||||
- 삭제 대상 메서드: `create_bot_principal`, `get_bot_principal`, `list_bot_principals`, `deactivate_bot_principal`, `list_bot_auth_tokens`, `revoke_bot_auth_token`
|
||||
- `create_auth_token()` 시그니처에서 `bot_principal_id: str | None = None` 파라미터 삭제
|
||||
- 추가 대상 메서드: `get_service_token_by_name(name: str) -> AuthToken | None`
|
||||
- `BotPrincipal` import 삭제
|
||||
- 입력/출력: `python -c "from app.domain.models import AuthToken"` 실행 시 ImportError 없음
|
||||
|
||||
## T-3: postgres.py 구현체 정리
|
||||
- 설계 방향: ensure_schema()에서 bot 관련 DDL을 제거하고, get_token_principal()의 JOIN을 단순화한다. bot/audit 관련 메서드를 삭제하고 get_service_token_by_name()을 추가한다.
|
||||
- 변경 대상: `app/repositories/postgres.py`
|
||||
- `_SELECT_BOT_PRINCIPAL` 쿼리 상수 삭제
|
||||
- `_SELECT_AUTH_TOKEN` 쿼리에서 `bot_principal_id` 컬럼 삭제
|
||||
- `ensure_schema()` 내 제거 대상:
|
||||
- `CREATE TABLE IF NOT EXISTS bot_principals (...)` 블록
|
||||
- `ALTER TABLE auth_tokens ADD COLUMN IF NOT EXISTS bot_principal_id TEXT`
|
||||
- `ALTER TABLE bot_principals ...` 블록 전체
|
||||
- `idx_auth_tokens_bot_created` 인덱스 생성 구문
|
||||
- `idx_bot_principals_*` 인덱스 생성 구문
|
||||
- `CREATE TABLE IF NOT EXISTS auth_audit_logs (...)` 블록
|
||||
- `idx_auth_audit_logs_bot_occurred` 인덱스 생성 구문
|
||||
- `get_token_principal()` 변경:
|
||||
- 변경 전: `SELECT t.id, COALESCE(b.owner_user_id, t.user_id), t.permission, t.token_type, t.bot_principal_id FROM auth_tokens t LEFT JOIN bot_principals b ...`
|
||||
- 변경 후: `SELECT id, user_id, permission, token_type FROM auth_tokens WHERE token_hash = %s AND revoked_at IS NULL AND (expires_at IS NULL OR expires_at > NOW())`
|
||||
- `TokenPrincipal` 생성 시 `bot_principal_id` 인자 제거
|
||||
- `create_auth_token()` 시그니처에서 `bot_principal_id` 파라미터 및 INSERT 컬럼 제거
|
||||
- 삭제 대상 메서드: `create_bot_principal`, `get_bot_principal`, `list_bot_principals`, `deactivate_bot_principal`, `list_bot_auth_tokens`, `revoke_bot_auth_token`, `record_audit_event`
|
||||
- 추가 대상 메서드:
|
||||
```python
|
||||
def get_service_token_by_name(self, name: str) -> AuthToken | None:
|
||||
# SELECT * FROM auth_tokens WHERE name = %s AND token_type = 'service' AND revoked_at IS NULL
|
||||
# → AuthToken 또는 None 반환
|
||||
```
|
||||
- 입력/출력: `ensure_schema()` 재실행 시 `bot_principals`, `auth_audit_logs` 테이블 미생성. `get_token_principal()` 반환값의 `bot_principal_id` 없음.
|
||||
|
||||
## T-4: AuthContext/security.py 변경
|
||||
- 설계 방향: `AuthContext`에 `token_type` 필드를 추가하고 `bot_principal_id`를 제거한다. `security.py`에서 `build_audit_event()` 함수를 삭제한다.
|
||||
- 변경 대상:
|
||||
- `app/core/auth.py`:
|
||||
- `AuthContext` 데이터클래스: `bot_principal_id: str | None = None` 삭제, `token_type: str | None = None` 추가
|
||||
- `_resolve_auth_context()`: `bot_principal_id=principal.bot_principal_id` 제거, `token_type=principal.token_type` 추가 (env 토큰은 `token_type=None` 유지)
|
||||
- `app/core/security.py`:
|
||||
- `build_audit_event()` 함수 삭제
|
||||
- 입력/출력:
|
||||
- env 토큰: `AuthContext(token=..., permission=..., token_source="env", token_type=None)`
|
||||
- DB 세션 토큰: `AuthContext(token=..., permission=..., user_id=..., token_id=..., token_source="db", token_type="session")`
|
||||
- DB service 토큰: `AuthContext(token=..., permission=..., user_id=..., token_id=..., token_source="db", token_type="service")`
|
||||
|
||||
## T-5: AuthService 재구현
|
||||
- 설계 방향: bot 관련 메서드를 모두 삭제하고 `issue_service_token()` / `revoke_service_token()`을 추가한다. `bot_principal_id` 가드를 `token_type == "service"` 가드로 교체한다.
|
||||
- 변경 대상: `app/services/auth_service.py`
|
||||
- 가드 교체 (3개소):
|
||||
- `issue_client_token()`: `if auth_context.bot_principal_id is not None` → `if auth_context.token_type == "service"`
|
||||
- `list_tokens()`: 동일
|
||||
- `revoke_token()`: 동일
|
||||
- 삭제 대상 메서드: `create_bot_principal`, `create_owned_bot_principal`, `list_bot_principals`, `list_owned_bot_principals`, `get_bot_principal`, `deactivate_bot_principal`, `issue_bot_token`, `issue_owned_bot_token`, `list_bot_tokens`, `list_owned_bot_tokens`, `revoke_bot_token`, `revoke_owned_bot_token`, `_record_audit_event`, `_build_bot_expiration`, `_require_db_user_context`
|
||||
- `_require_db_user_context()` 노트: 모든 호출자가 삭제되므로 함께 삭제 (review-design에서 가드 교체 대상에서 삭제 대상으로 재분류)
|
||||
- 삭제 대상 import: `build_audit_event`, `mask_secret`, `BotPrincipal`, `CreateBotPrincipalRequest`, `CreateBotTokenRequest`, `CreateSelfBotPrincipalRequest`
|
||||
- `IssuedBotToken` 데이터클래스 → `IssuedServiceToken(token: str, auth_token: AuthToken)`으로 교체
|
||||
- 추가 대상 메서드:
|
||||
```python
|
||||
def issue_service_token(
|
||||
self,
|
||||
name: str,
|
||||
username: str,
|
||||
*,
|
||||
permission: str | None = None,
|
||||
expires_in_days: int | None = None,
|
||||
no_expiry: bool = False,
|
||||
) -> IssuedServiceToken:
|
||||
user = self._repository.get_user_by_username(username)
|
||||
if user is None:
|
||||
raise ValueError(f"사용자 '{username}'을 찾을 수 없습니다")
|
||||
requested = Permission(permission or user.permission)
|
||||
raw_token = generate_token()
|
||||
expires_at = self._build_expiration(
|
||||
expires_in_days if not no_expiry else None,
|
||||
self._settings.client_token_default_ttl_days if not no_expiry else None,
|
||||
)
|
||||
auth_token = self._repository.create_auth_token(
|
||||
user.id,
|
||||
name=name,
|
||||
token_hash=hash_token(raw_token),
|
||||
token_prefix=token_prefix(raw_token),
|
||||
permission=requested.value,
|
||||
token_type="service",
|
||||
expires_at=expires_at,
|
||||
)
|
||||
return IssuedServiceToken(token=raw_token, auth_token=auth_token)
|
||||
|
||||
def revoke_service_token(self, name: str) -> datetime:
|
||||
token = self._repository.get_service_token_by_name(name)
|
||||
if token is None:
|
||||
raise ValueError(f"service token '{name}'을 찾을 수 없습니다")
|
||||
self._repository.revoke_auth_token(token.user_id, token.id)
|
||||
return datetime.now(timezone.utc)
|
||||
```
|
||||
- 예외/엣지 케이스:
|
||||
- `issue_service_token()`: `name` 중복 시 DB `UNIQUE constraint` 위반 → psycopg `UniqueViolation` 예외 → `ValueError`로 래핑
|
||||
- `revoke_service_token()`: 이미 폐기된 토큰도 `get_service_token_by_name()`에서 `revoked_at IS NOT NULL` 필터 → None 반환 → ValueError 발생 (명시적 오류)
|
||||
|
||||
## T-6: API 경로 정리
|
||||
- 설계 방향: bot_principal 관련 엔드포인트와 핸들러, 헬퍼 함수를 모두 삭제한다.
|
||||
- 변경 대상: `app/api/auth_routes.py`
|
||||
- 삭제 대상 라우트:
|
||||
- `POST /auth/bots` (create_bot_principal)
|
||||
- `POST /auth/me/bots` (create_owned_bot_principal)
|
||||
- `GET /auth/me/bots` (list_owned_bot_principals)
|
||||
- `POST /auth/me/bots/{bot_id}/tokens` (issue_owned_bot_token)
|
||||
- `GET /auth/me/bots/{bot_id}/tokens` (list_owned_bot_tokens)
|
||||
- `DELETE /auth/me/bots/{bot_id}/tokens/{token_id}` (revoke_owned_bot_token)
|
||||
- `DELETE /auth/bots/{bot_id}` 또는 관련 bot deactivate 라우트 (존재 시)
|
||||
- 삭제 대상 헬퍼 함수: `_to_bot_principal_response()`, `_to_issued_bot_token_response()`
|
||||
- 삭제 대상 import: `BotPrincipal`, `BotPrincipalResponse`, `CreateBotPrincipalRequest`, `CreateSelfBotPrincipalRequest`, `CreateBotTokenRequest`, `IssuedBotTokenResponse`, `IssuedBotToken`
|
||||
- `_to_auth_token_response()`: `bot_principal_id=token.bot_principal_id` 인자 삭제
|
||||
- 입력/출력: FastAPI 기동 후 `/openapi.json`에 `/auth/bots`, `/auth/me/bots` 경로 미노출
|
||||
|
||||
## T-7: CLI 재구현
|
||||
- 설계 방향: `bot_tokens.py`를 `service_tokens.py`로 교체하고, CLI 인수를 bot_id 기반에서 name + --user 기반으로 변경한다.
|
||||
- 변경 대상:
|
||||
- 신규 파일: `app/cli/commands/service_tokens.py`
|
||||
```python
|
||||
token_app = typer.Typer(help="Service token lifecycle commands.")
|
||||
|
||||
@token_app.command("issue")
|
||||
def issue_service_token(
|
||||
ctx: typer.Context,
|
||||
name: str = typer.Argument(..., help="Service token name (unique identifier)."),
|
||||
username: str = typer.Option(..., "--user", help="Username to assign the token to."),
|
||||
permission: str | None = typer.Option(None, help="Token permission: read or trade-write."),
|
||||
ttl_days: int | None = typer.Option(None, "--ttl-days", help="Token TTL in days."),
|
||||
no_expiry: bool = typer.Option(False, "--no-expiry", help="Issue token with no expiry."),
|
||||
force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation prompt."),
|
||||
output_format: str = typer.Option("table", "--format"),
|
||||
) -> None: ...
|
||||
|
||||
@token_app.command("revoke")
|
||||
def revoke_service_token(
|
||||
ctx: typer.Context,
|
||||
name: str = typer.Argument(..., help="Service token name to revoke."),
|
||||
force: bool = typer.Option(False, "--force", "-f"),
|
||||
output_format: str = typer.Option("table", "--format"),
|
||||
) -> None: ...
|
||||
```
|
||||
- 구 파일 `app/cli/commands/bot_tokens.py` 삭제
|
||||
- `app/cli/main.py`: `from app.cli.commands.bot_tokens import token_app` → `from app.cli.commands.service_tokens import token_app`
|
||||
- `app/cli/core/output.py`: `render_bot_result()` → `render_service_token_result()` rename
|
||||
- 응답 스키마: `BotTokenIssueCliResponse` → `ServiceTokenIssueCliResponse`, `BotTokenRevokeCliResponse` → `ServiceTokenRevokeCliResponse`
|
||||
- 입력/출력: `pfhub service-token issue --help`에서 `NAME` positional, `--user` 옵션 노출. help 텍스트에 "bot" 문자열 없음.
|
||||
|
||||
## T-8: 테스트 업데이트
|
||||
- 설계 방향: bot_principal 참조를 제거하고 service token 시나리오 테스트를 추가한다.
|
||||
- 변경 대상:
|
||||
- `tests/test_auth.py`:
|
||||
- `create_bot_principal`, `issue_bot_token`, `revoke_bot_token` 관련 테스트 함수 삭제
|
||||
- `issue_service_token()` 단위 테스트 추가:
|
||||
- 정상 발급 (username 존재, name 신규)
|
||||
- 존재하지 않는 username → ValueError
|
||||
- 중복 name → UniqueViolation → ValueError
|
||||
- `revoke_service_token()` 단위 테스트 추가
|
||||
- service token 가드 테스트: `token_type == "service"`인 AuthContext로 `issue_client_token()` 호출 시 ValueError 확인
|
||||
- `tests/test_cli.py`:
|
||||
- `service-token issue <name> --user <username>` 명령 통합 테스트 추가
|
||||
- `service-token revoke <name>` 명령 통합 테스트 추가
|
||||
- bot-token CLI 관련 테스트 삭제 또는 service-token으로 업데이트
|
||||
- `tests/test_users.py`: bot_principal 참조 제거 (있는 경우)
|
||||
- `tests/test_portfolio_ownership.py`:
|
||||
- `bot_principal_id` 기반 mock → `token_type="service"` 기반 mock으로 교체
|
||||
|
||||
## T-9: 기술 문서 업데이트
|
||||
- 설계 방향: 4개 기술 문서에서 bot_principal 참조를 제거하고 service token 기반 설명으로 갱신한다.
|
||||
- 변경 대상:
|
||||
- `docs/technical/auth-and-token-lifecycle.md`: bot → service 전반 업데이트, `AuthContext.token_type` 설명 추가, bot_principals/auth_audit_logs 삭제 반영
|
||||
- `docs/technical/api-contracts.md`: `/auth/bots`, `/auth/me/bots` 경로 삭제, 변경 후 token_type 값 갱신
|
||||
- `docs/technical/management-cli-operations.md`: `pfhub service-token issue <name> --user <username>` 명령 예제로 교체, bot-token 관련 내용 삭제
|
||||
- `README.md`: CLI 섹션에서 bot-token 예제 삭제, service-token 예제 추가
|
||||
|
||||
# 4. 인터페이스 정의
|
||||
외부 시스템, 모듈 간 계약(API, 이벤트, 파일 포맷 등)을 명시한다.
|
||||
|
||||
## API / 함수 시그니처
|
||||
|
||||
**AuthService 신규 메서드:**
|
||||
```python
|
||||
def issue_service_token(
|
||||
self,
|
||||
name: str, # auth_tokens.name으로 저장되는 서비스 식별자 (service token 범위 내 유니크)
|
||||
username: str, # users.username → user_id 조회 후 auth_tokens.user_id로 저장
|
||||
*,
|
||||
permission: str | None = None, # None이면 user.permission 상속
|
||||
expires_in_days: int | None = None,
|
||||
no_expiry: bool = False,
|
||||
) -> IssuedServiceToken: ...
|
||||
|
||||
def revoke_service_token(
|
||||
self,
|
||||
name: str, # service token 이름으로 조회 후 폐기
|
||||
) -> datetime: ... # revoked_at 반환
|
||||
```
|
||||
|
||||
**PortfolioRepository Protocol 신규 메서드:**
|
||||
```python
|
||||
def get_service_token_by_name(self, name: str) -> AuthToken | None: ...
|
||||
# SELECT * FROM auth_tokens WHERE name = %s AND token_type = 'service' AND revoked_at IS NULL
|
||||
```
|
||||
|
||||
**AuthContext 변경 후 필드:**
|
||||
```python
|
||||
@dataclass(frozen=True)
|
||||
class AuthContext:
|
||||
token: str
|
||||
permission: Permission
|
||||
user_id: str | None = None
|
||||
token_id: str | None = None
|
||||
token_type: str | None = None # "session" | "client" | "service" | None (env 토큰)
|
||||
token_source: str = "env" # "env" | "db"
|
||||
```
|
||||
|
||||
## 데이터 스키마 / 포맷
|
||||
|
||||
**ServiceTokenIssueCliResponse** (새 스키마):
|
||||
```python
|
||||
class ServiceTokenIssueCliResponse(BaseModel):
|
||||
token_id: str
|
||||
name: str
|
||||
expires_at: datetime | None = None
|
||||
revoked: bool
|
||||
token: str | None = None # 발급 시 일회 노출, 이후 마스킹
|
||||
```
|
||||
|
||||
**ServiceTokenRevokeCliResponse** (새 스키마):
|
||||
```python
|
||||
class ServiceTokenRevokeCliResponse(BaseModel):
|
||||
token_id: str
|
||||
name: str
|
||||
revoked: bool
|
||||
revoked_at: datetime | None = None
|
||||
```
|
||||
|
||||
**AuthTokenResponse** 변경:
|
||||
- `token_type`: `Literal["session", "client", "bot"]` → `Literal["session", "client", "service"]`
|
||||
- `bot_principal_id: str | None` 필드 삭제
|
||||
|
||||
## 이벤트 / 메시지
|
||||
해당 없음. audit log 테이블 자체 삭제로 이벤트 기록 없음.
|
||||
|
||||
# 5. 보안 고려사항
|
||||
OWASP Top 10 기준으로 이번 변경에서 주의해야 할 보안 항목을 점검한다.
|
||||
|
||||
| 항목 | 해당 여부 | 대응 방안 |
|
||||
|------|---------|---------|
|
||||
| 입력값 검증 (Injection) | 해당 | `--user <username>` 값은 `get_user_by_username()`을 통해 파라미터 바인딩으로 DB 조회. SQL injection 없음. `name` 값도 파라미터 바인딩 사용. |
|
||||
| 인증/인가 | 해당 | service token은 `token_type == "service"` 가드로 토큰 발급/목록/폐기 조작 차단. 인가 단계에서 `AuthContext.token_type` 필드를 명시적으로 확인. env 토큰(token_type=None)은 가드 미적용 — admin env 토큰은 해당 조작 허용 (기존 설계 유지). |
|
||||
| 민감 데이터 노출 | 해당 | 발급된 raw token은 CLI에서 발급 시 1회만 출력. 저장 시 SHA-256 해시(`token_hash`)만 DB에 저장. `mask_secret()` 함수로 출력 시 마스킹 유지. audit log 삭제로 raw token_ref 잔류 위험 제거. |
|
||||
| 의존성 취약점 | 비해당 | 이번 변경은 새 의존성 추가 없음. 기존 psycopg, fastapi, typer 의존성 유지. |
|
||||
| 기타 (권한 상승) | 해당 | `issue_service_token()` 시 `permission`은 user.permission 상한으로 제한 (`_has_permission()` 체크). 지정한 user의 권한을 초과하는 service token 발급 차단. |
|
||||
|
||||
# 6. 리스크
|
||||
Design 단계에서 새로 식별된 리스크를 기록한다.
|
||||
|
||||
## 리스크
|
||||
- R-1 (High, 이월): 기존 `auth_tokens`에 `user_id = bot_principal.id`인 행의 `bot_principal.owner_user_id`가 NULL이거나 `users` 테이블에 없으면 마이그레이션 FK 위반 발생.
|
||||
- 완화 방안: T-1 upgrade() 첫 번째 스텝에서 NULL 검사 쿼리 실행 후 0이 아니면 `RuntimeError`로 중단. 운영 전 사전 검증 권장.
|
||||
- R-2 (Medium, 이월): 약 15개 파일 rename 중 일부 누락 시 런타임 `AttributeError`/`NameError`.
|
||||
- 완화 방안: T-8 완료 후 `grep -r "bot_principal\|BotPrincipal\|bot_token\|token_type.*bot\|render_bot_result"` 실행으로 잔류 참조 검사.
|
||||
- R-3 (Medium): `revoke_service_token(name)` 구현 시 `get_service_token_by_name()`이 `revoked_at IS NOT NULL` 필터를 포함하면 이미 폐기된 토큰 revoke 시도 시 "찾을 수 없음" 오류 발생.
|
||||
- 완화 방안: CLI에서 적절한 오류 메시지 출력. 설계상 의도된 동작.
|
||||
- R-4 (Low): `ensure_schema()` DDL 제거 후 기존 테스트 환경이 이전 스키마를 캐시하는 경우 bot_principals 테이블 잔류 가능.
|
||||
- 완화 방안: T-3 DoD에서 `ensure_schema()` 재실행 후 테이블 미존재 확인 포함.
|
||||
|
||||
## 가정
|
||||
- DA-1: `app/cli/main.py`에서 `name="service-token"` 등록이 이미 완료되어 있으므로 T-7에서 import 경로(`bot_tokens` → `service_tokens`)만 변경하면 된다.
|
||||
- DA-2: `get_user_by_username()` 메서드가 postgres.py에 이미 구현되어 있다 (`auth_service.py`의 `login()` 메서드에서 사용 중으로 확인됨).
|
||||
- DA-3: `revoke_auth_token(user_id, token_id)` 메서드가 postgres.py에 구현되어 있어 `revoke_service_token()` 내부에서 재사용 가능하다.
|
||||
- DA-4: auth_routes.py에서 삭제 대상 이외의 라우트(`/auth/login`, `/auth/tokens`, `/auth/tokens/{id}`)는 변경 없이 유지된다.
|
||||
|
||||
## 오픈 질문
|
||||
없음. 모든 설계 결정 완료.
|
||||
|
||||
# 7. Implementation으로 넘기기 전 확인 체크
|
||||
Implementation 단계로 넘기기 전에 무엇이 확정되어야 하는지 확인한다.
|
||||
|
||||
- [ ] 전체 컴포넌트 및 태스크별 설계 완성
|
||||
- 기준: T-1 ~ T-9 모든 태스크에 설계 방향, 변경 대상, 입출력이 명시되어야 한다
|
||||
- [ ] 인터페이스 계약 확정
|
||||
- 기준: `issue_service_token()`, `revoke_service_token()`, `get_service_token_by_name()`, `AuthContext.token_type` 시그니처가 명시되어야 한다
|
||||
- [ ] 보안 점검 완료
|
||||
- 기준: OWASP 점검 항목 중 해당 항목(입력값 검증, 인증/인가, 민감 데이터 노출, 권한 상승)에 대응 방안이 있어야 한다
|
||||
- [ ] 모든 오픈 질문(OQ) `Open` 상태 없음 확인
|
||||
- 기준: OQ 목록에 `Open` 상태 항목이 0개 — 확인 완료
|
||||
- [ ] Freeze 확정 담당자(CONFIRMED_BY) 지정
|
||||
- 기준: 문서 승인 또는 인계 결정을 내릴 담당자가 명시되어야 한다
|
||||
|
||||
# 8. 파일 처리 결과
|
||||
이 Design 문서 생성/갱신 결과와 참조 문서를 기록한다.
|
||||
|
||||
- 처리 결과: 생성
|
||||
- 생성 경로: docs/sdlc/sdlc-002_20260423_service-token-user-based/3_design.md
|
||||
- 참조 문서:
|
||||
- docs/sdlc/sdlc-002_20260423_service-token-user-based/2_planning.md
|
||||
- docs/sdlc/sdlc-002_20260423_service-token-user-based/1_discovery.md
|
||||
- app/domain/models.py
|
||||
- app/core/auth.py
|
||||
- app/core/security.py
|
||||
- app/services/auth_service.py
|
||||
- app/repositories/postgres.py (line 821: get_token_principal, line 124: ensure_schema)
|
||||
- app/repositories/portfolio_repository.py
|
||||
- app/schemas/auth.py
|
||||
- app/api/auth_routes.py
|
||||
- app/cli/commands/bot_tokens.py
|
||||
- app/cli/core/output.py
|
||||
- app/cli/main.py
|
||||
- migrations/versions/20260414_0011_bot_principal_owner_user_id.py (최신 마이그레이션)
|
||||
|
||||
# 9. 사용자 결정 필요 항목 요약
|
||||
사용자나 승인자가 결정해야 하는 항목만 모아 정리한다.
|
||||
|
||||
## DECIDE
|
||||
없음. 모든 설계 결정이 Discovery/Planning에서 완료되었다.
|
||||
|
||||
## CONFIRM
|
||||
- Freeze 확정 담당자(`CONFIRMED_BY`): Justin — confirm-design 단계에서 처리
|
||||
|
||||
## DATA
|
||||
- 마이그레이션 실행 전 운영 DB `bot_principals` 행 수 및 `owner_user_id NULL` 행 수 확인 필요 (R-1 완화). T-1 upgrade() 내 사전 검증 쿼리가 이를 자동으로 수행하지만, 사전 확인 권장.
|
||||
|
||||
# 10. Design Freeze
|
||||
Implementation 인계 시점의 확정 플래그와 섹션 참조만 기록한다.
|
||||
|
||||
## 섹션 참조
|
||||
- 아키텍처/구조: [# 2. 아키텍처 및 구조 설계](#2-아키텍처-및-구조-설계)
|
||||
- 상세 설계: [# 3. 상세 설계](#3-상세-설계)
|
||||
- 인터페이스: [# 4. 인터페이스 정의](#4-인터페이스-정의)
|
||||
- 보안: [# 5. 보안 고려사항](#5-보안-고려사항)
|
||||
- 리스크/오픈 질문: [# 6. 리스크](#6-리스크)
|
||||
- 인계 전 확인: [# 7. Implementation으로 넘기기 전 확인 체크](#7-implementation으로-넘기기-전-확인-체크)
|
||||
- 사용자 결정 필요: [# 9. 사용자 결정 필요 항목 요약](#9-사용자-결정-필요-항목-요약)
|
||||
|
||||
## Freeze 플래그
|
||||
- Handoff Decision: 승인
|
||||
- Handoff Rationale: T-1~T-9 전체 상세 설계 완성, 주요 인터페이스(`issue_service_token`, `revoke_service_token`, `get_service_token_by_name`, `AuthContext.token_type`) 확정, OWASP 해당 4개 항목 대응 방안 명시, OQ 없음, 저장소 현황 대조 이상 없음.
|
||||
- Ready for Implementation: true
|
||||
- Confirmed By: Justin
|
||||
- Confirmed At (KST): 2026-04-23
|
||||
@@ -1,190 +0,0 @@
|
||||
# 0. 문서 상태
|
||||
이 문서의 현재 상태와 식별 정보를 기록한다.
|
||||
|
||||
- 상태: confirmed
|
||||
- 문서 ID: sdlc-002_20260423_service-token-user-based
|
||||
- 이슈명: service-token user-based 소유권
|
||||
- 참조 Design: docs/sdlc/sdlc-002_20260423_service-token-user-based/3_design.md
|
||||
- 작성 시각(KST): 2026-04-23
|
||||
- 마지막 갱신 시각(KST): 2026-04-23 (confirmed)
|
||||
|
||||
# 1. Design 인계 요약
|
||||
Design에서 확정된 핵심 내용을 요약한다. 원본은 3_design.md를 참조하며, 이 섹션은 Implementation 작업의 입력 기준선이 된다.
|
||||
|
||||
## 이슈 목적
|
||||
`bot_principals` / `auth_audit_logs` 테이블을 제거하고, service token을 `auth_tokens` 단일 엔티티로 직접 발급하도록 재구현한다. CLI `pfhub service-token issue <name> --user <username>` 명령으로 사용자 기반 소유권을 명확히 한다.
|
||||
|
||||
## 확정 태스크 및 변경 대상
|
||||
| ID | 태스크 제목 | 변경 대상 파일/함수 | 담당자 |
|
||||
|----|-----------|-------------------|--------|
|
||||
| T-1 | DB 마이그레이션 파일 작성 | `migrations/versions/20260423_0013_remove_bot_principals_service_tokens.py` (신규) | copilot |
|
||||
| T-2 | 도메인/스키마/인터페이스 정리 | `app/domain/models.py`, `app/schemas/auth.py`, `app/repositories/portfolio_repository.py` | copilot |
|
||||
| T-3 | postgres.py 구현체 정리 | `app/repositories/postgres.py` | copilot |
|
||||
| T-4 | AuthContext/security.py 변경 | `app/core/auth.py`, `app/core/security.py` | copilot |
|
||||
| T-5 | AuthService 재구현 | `app/services/auth_service.py` | copilot |
|
||||
| T-6 | API 경로 정리 | `app/api/auth_routes.py` | copilot |
|
||||
| T-7 | CLI 재구현 | `app/cli/commands/bot_tokens.py` (삭제), `app/cli/commands/service_tokens.py` (신규), `app/cli/core/output.py`, `app/cli/main.py` | copilot |
|
||||
| T-8 | 테스트 업데이트 | `tests/test_auth.py`, `tests/test_cli.py`, `tests/test_portfolio_ownership.py`, `tests/test_users.py` | copilot |
|
||||
| T-9 | 기술 문서 업데이트 | `docs/technical/auth-and-token-lifecycle.md`, `docs/technical/api-contracts.md`, `docs/technical/management-cli-operations.md`, `README.md` | copilot |
|
||||
|
||||
## 이월된 오픈 질문 / 리스크
|
||||
없음. Design 단계에서 모든 OQ 해소 완료.
|
||||
|
||||
# 2. 구현 계획
|
||||
태스크(T-N)별 구현 순서와 접근 방법을 기술한다.
|
||||
|
||||
## 구현 순서
|
||||
태스크 간 의존 관계에 따른 순서:
|
||||
|
||||
1. T-1 (DB 마이그레이션 파일 작성) — T-2와 병렬 가능, 독립적
|
||||
2. T-2 (도메인/스키마/인터페이스 정리) — T-1과 병렬 가능
|
||||
3. T-3 (postgres.py 구현체 정리) — T-2 완료 후 (인터페이스 변경 선행 필요)
|
||||
4. T-4 (AuthContext/security.py 변경) — T-2 완료 후 (모델 변경 선행 필요)
|
||||
5. T-5 (AuthService 재구현) — T-3, T-4 완료 후
|
||||
6. T-6 (API 경로 정리) — T-5 완료 후, T-7과 병렬 가능
|
||||
7. T-7 (CLI 재구현) — T-5 완료 후, T-6과 병렬 가능
|
||||
8. T-8 (테스트 업데이트) — T-5, T-6, T-7 완료 후
|
||||
9. T-9 (기술 문서 업데이트) — T-8 완료 후
|
||||
|
||||
## T-1: DB 마이그레이션 파일 작성
|
||||
- 구현 방법: Alembic 마이그레이션 파일을 신규 생성한다. `upgrade()` 내 첫 스텝은 `bot_principals.owner_user_id IS NULL` 검사로, NULL이 있으면 `MigrationError`를 raise하고 중단한다. 이후 user_id 보정 → token_type rename → unique index → bot_principal_id 컬럼 DROP → auth_audit_logs DROP → bot_principals DROP 순으로 실행한다. `downgrade()`는 역방향 구조 복원만 수행한다.
|
||||
- 변경 대상: `migrations/versions/20260423_0013_remove_bot_principals_service_tokens.py` (신규 생성)
|
||||
- 완료 조건: `alembic upgrade head` 실행 시 `bot_principals`, `auth_audit_logs` 테이블이 DROP되고, `auth_tokens.user_id`가 실제 user FK를 참조하며, `token_type = 'service'`로 업데이트됨. `alembic downgrade -1` 실행 시 역방향 복원 가능.
|
||||
- 주의사항: `op.execute()` SQL은 파라미터 바인딩 없이 직접 실행 (Alembic DDL 관례). NULL 검사 쿼리는 `op.get_bind().execute()` 패턴 사용.
|
||||
|
||||
## T-2: 도메인/스키마/인터페이스 정리
|
||||
- 구현 방법: 파일별 삭제/변경을 수행한다. `models.py`에서 `BotPrincipal`, `bot_principal_id` 필드를 삭제한다. `schemas/auth.py`에서 bot 관련 스키마를 삭제하고 `ServiceTokenIssueCliResponse`, `ServiceTokenRevokeCliResponse`를 추가한다. `portfolio_repository.py`에서 bot 관련 메서드 시그니처를 삭제하고 `get_service_token_by_name()`을 추가한다.
|
||||
- 변경 대상: `app/domain/models.py`, `app/schemas/auth.py`, `app/repositories/portfolio_repository.py`
|
||||
- 완료 조건: `python -c "from app.domain.models import AuthToken"` ImportError 없음. `BotPrincipal` 클래스 미존재. `ServiceTokenIssueCliResponse` import 가능.
|
||||
- 주의사항: `AuthTokenResponse.token_type` Literal 변경(`"bot"` → `"service"`)은 API 응답 직렬화에 즉시 영향을 주므로 T-6 완료 전에 수행해도 무방 (기존 bot 토큰 데이터 없음 전제).
|
||||
|
||||
## T-3: postgres.py 구현체 정리
|
||||
- 구현 방법: `ensure_schema()` 내 bot 관련 DDL 블록을 순서대로 제거한다. `get_token_principal()`의 SELECT문을 단순화한다. bot/audit 관련 메서드를 삭제하고 `get_service_token_by_name()`을 추가한다.
|
||||
- 변경 대상: `app/repositories/postgres.py`
|
||||
- 완료 조건: `ensure_schema()` 재실행 후 `bot_principals`, `auth_audit_logs` 테이블 미생성. `get_token_principal()` 반환값에 `bot_principal_id` 없음. `get_service_token_by_name("test")` 호출 시 `None` 또는 `AuthToken` 반환.
|
||||
- 주의사항: `_SELECT_AUTH_TOKEN` 상수에서 `bot_principal_id` 컬럼 제거. `TokenPrincipal` 생성 시 `bot_principal_id` 인자 제거.
|
||||
|
||||
## T-4: AuthContext/security.py 변경
|
||||
- 구현 방법: `AuthContext` 데이터클래스에서 `bot_principal_id` 필드를 제거하고 `token_type: str | None = None`을 추가한다. `_resolve_auth_context()`에서 `token_type=principal.token_type` 전달을 추가하고 `bot_principal_id` 할당을 제거한다. `security.py`에서 `build_audit_event()` 함수를 삭제한다.
|
||||
- 변경 대상: `app/core/auth.py`, `app/core/security.py`
|
||||
- 완료 조건: `AuthContext` 생성 시 `token_type` 파라미터 사용 가능. `bot_principal_id` 파라미터 없음. `from app.core.security import build_audit_event` ImportError 발생.
|
||||
|
||||
## T-5: AuthService 재구현
|
||||
- 구현 방법: bot 관련 메서드와 `_require_db_user_context()`, `_build_bot_expiration()`, `_record_audit_event()` 헬퍼를 삭제한다. `issue_client_token()`, `list_tokens()`, `revoke_token()` 내 `bot_principal_id` 가드를 `token_type == "service"` 가드로 교체한다. `issue_service_token()`, `revoke_service_token()` 메서드를 추가한다. `IssuedBotToken` → `IssuedServiceToken` 교체.
|
||||
- 변경 대상: `app/services/auth_service.py`
|
||||
- 완료 조건: `AuthService().issue_service_token(name="test", username="user")` 시그니처 존재. `issue_client_token()`에서 service token AuthContext로 호출 시 ValueError 발생. bot 관련 메서드 미존재.
|
||||
- 주의사항: `issue_service_token()` 내 unique constraint 위반 시 `psycopg.errors.UniqueViolation` 예외를 `ValueError`로 래핑해야 한다.
|
||||
|
||||
## T-6: API 경로 정리
|
||||
- 구현 방법: `auth_routes.py`에서 `/auth/bots`, `/auth/me/bots` 관련 라우트 핸들러와 헬퍼 함수를 삭제한다. 관련 import를 제거한다. `_to_auth_token_response()`에서 `bot_principal_id` 인자를 제거한다.
|
||||
- 변경 대상: `app/api/auth_routes.py`
|
||||
- 완료 조건: FastAPI 앱 기동 후 `/openapi.json`에 `/auth/bots`, `/auth/me/bots` 경로 미노출.
|
||||
|
||||
## T-7: CLI 재구현
|
||||
- 구현 방법: `app/cli/commands/service_tokens.py` 신규 생성 (Design `# 3 T-7` 시그니처 기준). `bot_tokens.py` 파일 삭제. `app/cli/main.py` import 경로 변경. `output.py`에서 `render_bot_result()` → `render_service_token_result()` rename.
|
||||
- 변경 대상: `app/cli/commands/service_tokens.py` (신규), `app/cli/commands/bot_tokens.py` (삭제), `app/cli/core/output.py`, `app/cli/main.py`
|
||||
- 완료 조건: `pfhub service-token issue --help` 실행 시 `NAME`, `--user` 인수 노출. help 텍스트에 "bot" 문자열 없음.
|
||||
- 주의사항: `service_tokens.py`는 `token_app = typer.Typer(...)` 인스턴스를 export해야 한다 (`app/cli/main.py`에서 `token_app`으로 import).
|
||||
|
||||
## T-8: 테스트 업데이트
|
||||
- 구현 방법: bot_principal 관련 픽스처와 테스트 함수를 제거하고 service token 시나리오 테스트를 추가한다. `AuthContext` mock에서 `bot_principal_id` → `token_type` 교체.
|
||||
- 변경 대상: `tests/test_auth.py`, `tests/test_cli.py`, `tests/test_portfolio_ownership.py`, `tests/test_users.py`
|
||||
- 완료 조건: `pytest` 실행 결과 0 failures, 0 errors. service token 발급/폐기 시나리오 테스트 포함.
|
||||
|
||||
## T-9: 기술 문서 업데이트
|
||||
- 구현 방법: 4개 문서에서 "bot_principal", "bot token" 참조를 제거하고 service token 기반 설명으로 갱신한다.
|
||||
- 변경 대상: `docs/technical/auth-and-token-lifecycle.md`, `docs/technical/api-contracts.md`, `docs/technical/management-cli-operations.md`, `README.md`
|
||||
- 완료 조건: 4개 문서에서 "bot_principal", "bot token" 잔류 참조 없음. `pfhub service-token` 명령 예제 포함.
|
||||
|
||||
# 3. 변경 파일 목록
|
||||
이번 Implementation에서 생성/수정/삭제되는 파일을 미리 나열한다.
|
||||
|
||||
| 파일 경로 | 변경 유형 | 관련 태스크 | 비고 |
|
||||
|---------|---------|-----------|------|
|
||||
| `migrations/versions/20260423_0013_remove_bot_principals_service_tokens.py` | 생성 | T-1 | Alembic 마이그레이션 신규 |
|
||||
| `app/domain/models.py` | 수정 | T-2 | BotPrincipal 삭제, bot_principal_id 필드 삭제 |
|
||||
| `app/schemas/auth.py` | 수정 | T-2 | bot 스키마 삭제, ServiceToken* 스키마 추가 |
|
||||
| `app/repositories/portfolio_repository.py` | 수정 | T-2 | bot 메서드 삭제, get_service_token_by_name 추가 |
|
||||
| `app/repositories/postgres.py` | 수정 | T-3 | ensure_schema DDL 제거, get_token_principal 단순화 |
|
||||
| `app/core/auth.py` | 수정 | T-4 | AuthContext.token_type 추가, bot_principal_id 삭제 |
|
||||
| `app/core/security.py` | 수정 | T-4 | build_audit_event 함수 삭제 |
|
||||
| `app/services/auth_service.py` | 수정 | T-5 | bot 메서드 삭제, issue/revoke_service_token 추가 |
|
||||
| `app/api/auth_routes.py` | 수정 | T-6 | /auth/bots, /auth/me/bots 라우트 삭제 |
|
||||
| `app/cli/commands/bot_tokens.py` | 삭제 | T-7 | service_tokens.py로 대체 |
|
||||
| `app/cli/commands/service_tokens.py` | 생성 | T-7 | 신규 CLI 명령 모듈 |
|
||||
| `app/cli/core/output.py` | 수정 | T-7 | render_bot_result → render_service_token_result |
|
||||
| `app/cli/main.py` | 수정 | T-7 | import 경로 변경 |
|
||||
| `tests/test_auth.py` | 수정 | T-8 | bot 테스트 삭제, service token 테스트 추가 |
|
||||
| `tests/test_cli.py` | 수정 | T-8 | service-token CLI 테스트 업데이트 |
|
||||
| `tests/test_portfolio_ownership.py` | 수정 | T-8 | token_type 기반 mock으로 교체 |
|
||||
| `tests/test_users.py` | 수정 | T-8 | bot_principal 참조 제거 (있는 경우) |
|
||||
| `docs/technical/auth-and-token-lifecycle.md` | 수정 | T-9 | bot → service 전반 업데이트 |
|
||||
| `docs/technical/api-contracts.md` | 수정 | T-9 | /auth/bots 경로 삭제 |
|
||||
| `docs/technical/management-cli-operations.md` | 수정 | T-9 | service-token 명령 예제 교체 |
|
||||
| `README.md` | 수정 | T-9 | CLI 섹션 service-token 예제 추가 |
|
||||
|
||||
# 4. 리스크 및 제약
|
||||
Implementation 단계에서 새로 식별된 리스크와 제약사항을 기록한다.
|
||||
|
||||
## 리스크
|
||||
- R-1 (High, 이월): 마이그레이션 upgrade 전 `bot_principals.owner_user_id IS NULL` 행 존재 시 FK 위반 — upgrade() 사전 검사로 차단.
|
||||
- R-2 (Medium, 이월): 15개 파일 rename 중 누락 시 런타임 AttributeError — T-8 완료 후 `grep` 잔류 검사.
|
||||
- R-3 (Medium, 이월): `revoke_service_token(name)` 호출 시 이미 폐기된 토큰 → "찾을 수 없음" 오류 — 설계상 의도된 동작.
|
||||
- R-4 (Low, 이월): `ensure_schema()` DDL 제거 후 테스트 환경 스키마 캐시로 인한 bot_principals 잔류 — 재실행 검증으로 확인.
|
||||
- R-5 (Medium): `psycopg.errors.UniqueViolation` 예외를 `ValueError`로 래핑하지 않으면 FastAPI/CLI에서 500 응답 발생.
|
||||
- 완화 방안: T-5 구현 시 `try/except UniqueViolation → raise ValueError(...)` 처리 포함.
|
||||
|
||||
## 가정
|
||||
- IA-1: `get_user_by_username()` 메서드가 `app/repositories/postgres.py`에 이미 구현되어 있다 (auth_service.login()에서 사용 중으로 확인됨).
|
||||
- IA-2: `revoke_auth_token(user_id, token_id)` 메서드가 `app/repositories/postgres.py`에 구현되어 있다.
|
||||
- IA-3: `app/cli/main.py`의 `name="service-token"` 등록이 완료되어 있어 T-7에서 import 경로만 변경하면 된다.
|
||||
- IA-4: 현재 테스트 환경에서 Docker Compose DB 접속이 가능하다.
|
||||
|
||||
## 오픈 질문
|
||||
없음.
|
||||
|
||||
# 5. Verification으로 넘기기 전 확인 체크
|
||||
|
||||
- [x] 모든 T-N 태스크 구현 완료
|
||||
- 기준: T-1~T-9 각 태스크의 완료 조건(DoD)을 충족해야 한다 — **T-1~T-9 모두 완료**
|
||||
- [x] 변경 파일 목록과 실제 변경 일치
|
||||
- 기준: 3_design.md의 변경 대상과 실제 변경된 파일이 일치해야 한다 — **일치 확인**
|
||||
- [x] 단위 테스트 또는 로컬 검증 완료
|
||||
- 기준: `pytest` 실행 결과 0 failures, 0 errors — **73 passed**
|
||||
- [x] 모든 오픈 질문(OQ) `Open` 상태 없음 확인
|
||||
- 기준: OQ 목록에 `Open` 상태 항목이 0개 — 확인 완료
|
||||
- [ ] Freeze 확정 담당자(CONFIRMED_BY) 지정
|
||||
- 기준: 담당자 Justin 지정 완료 — 문서 서명 대기
|
||||
|
||||
# 6. 파일 처리 결과
|
||||
|
||||
- 처리 결과: 생성
|
||||
- 생성 경로: docs/sdlc/sdlc-002_20260423_service-token-user-based/4_implementation.md
|
||||
- 참조 문서:
|
||||
- docs/sdlc/sdlc-002_20260423_service-token-user-based/3_design.md
|
||||
- docs/sdlc/sdlc-002_20260423_service-token-user-based/2_planning.md
|
||||
|
||||
# 7. 사용자 결정 필요 항목 요약
|
||||
|
||||
## DECIDE
|
||||
없음. 모든 구현 결정이 Design에서 완료되었다.
|
||||
|
||||
## CONFIRM
|
||||
- Freeze 확정 담당자(`CONFIRMED_BY`): Justin — confirm-implementation 단계에서 처리
|
||||
|
||||
## DATA
|
||||
- 마이그레이션 실행 전 운영 DB `bot_principals` 행 수 및 `owner_user_id NULL` 행 수 확인 권장 (R-1 완화)
|
||||
|
||||
# 8. Implementation Freeze
|
||||
|
||||
## 섹션 참조
|
||||
- 구현 계획: [# 2. 구현 계획](#2-구현-계획)
|
||||
- 변경 파일 목록: [# 3. 변경 파일 목록](#3-변경-파일-목록)
|
||||
- 리스크/오픈 질문: [# 4. 리스크 및 제약](#4-리스크-및-제약)
|
||||
|
||||
## 확정 플래그
|
||||
- Handoff Decision: 승인
|
||||
- Handoff Rationale: T-1~T-9 전체 태스크 완료, 73개 단위 테스트 통과, 변경 파일 목록 일치 확인
|
||||
- Ready for Verification: true
|
||||
- Confirmed By: Justin
|
||||
- Confirmed At (KST): 2026-04-23
|
||||
@@ -1,160 +0,0 @@
|
||||
# 0. 문서 상태
|
||||
이 문서의 현재 상태와 식별 정보를 기록한다.
|
||||
|
||||
- 상태: confirmed
|
||||
- 문서 ID: sdlc-002_20260423_service-token-user-based
|
||||
- 이슈명: service-token user-based 소유권
|
||||
- 참조 Implementation: docs/sdlc/sdlc-002_20260423_service-token-user-based/4_implementation.md
|
||||
- 작성 시각(KST): 2026-04-23
|
||||
- 마지막 갱신 시각(KST): 2026-04-23
|
||||
|
||||
# 1. Implementation 인계 요약
|
||||
Implementation에서 완료된 핵심 내용을 요약한다. 원본은 4_implementation.md를 참조하며, 이 섹션은 Verification 작업의 입력 기준선이 된다.
|
||||
|
||||
## 이슈 목적
|
||||
`bot_principals` / `auth_audit_logs` 테이블을 제거하고, service token을 `auth_tokens` 단일 엔티티로 직접 발급하도록 재구현한다. CLI `pfhub service-token issue <name> --user <username>` 명령으로 사용자 기반 소유권을 명확히 한다.
|
||||
|
||||
## 구현 완료 태스크
|
||||
| ID | 태스크 제목 | 변경 파일/함수 | 완료 조건(DoD) |
|
||||
|----|-----------|--------------|--------------|
|
||||
| T-1 | DB 마이그레이션 파일 작성 | `migrations/versions/20260423_0013_remove_bot_principals_service_tokens.py` | `alembic upgrade head` 후 bot_principals/auth_audit_logs DROP, token_type 보정, UNIQUE INDEX 생성. downgrade 복원 가능 |
|
||||
| T-2 | 도메인/스키마/인터페이스 정리 | `app/domain/models.py`, `app/schemas/auth.py`, `app/repositories/portfolio_repository.py` | BotPrincipal 미존재, ServiceTokenIssueCliResponse import 가능, bot 메서드 미존재 |
|
||||
| T-3 | postgres.py 구현체 정리 | `app/repositories/postgres.py` | ensure_schema() 재실행 후 bot_principals/auth_audit_logs 미생성, get_service_token_by_name() 존재 |
|
||||
| T-4 | AuthContext/security.py 변경 | `app/core/auth.py`, `app/core/security.py` | AuthContext.token_type 사용 가능, bot_principal_id 없음, build_audit_event ImportError |
|
||||
| T-5 | AuthService 재구현 | `app/services/auth_service.py` | issue_service_token() 시그니처 존재, bot 메서드 미존재 |
|
||||
| T-6 | API 경로 정리 | `app/api/auth_routes.py` | /auth/bots, /auth/me/bots 경로 미노출 |
|
||||
| T-7 | CLI 재구현 | `app/cli/commands/service_tokens.py` (신규), `app/cli/core/output.py`, `app/cli/main.py` | pfhub service-token issue --help에 NAME, --user 노출, bot 문자열 없음 |
|
||||
| T-8 | 테스트 업데이트 | `tests/test_auth.py`, `tests/test_cli.py`, `tests/test_portfolio_ownership.py`, `tests/test_users.py` | pytest 결과 0 failures, 0 errors, service token 시나리오 테스트 포함 |
|
||||
| T-9 | 기술 문서 업데이트 | `docs/technical/auth-and-token-lifecycle.md`, `docs/technical/api-contracts.md`, `docs/technical/management-cli-operations.md`, `README.md` | 4개 문서에서 bot_principal/bot token 잔류 참조 없음, pfhub service-token 예제 포함 |
|
||||
|
||||
## 이월된 오픈 질문 / 리스크
|
||||
없음. Implementation 단계에서 모든 OQ 해소 완료.
|
||||
|
||||
# 2. 검증 계획
|
||||
Discovery `# 7` 성공 기준(S-N)을 기반으로 Verification 항목을 정의한다.
|
||||
|
||||
## 검증 범위
|
||||
- 기능 요구사항(FR-1~FR-7) 전체 충족 여부
|
||||
- 비기능 요구사항(NFR-1~NFR-3) 충족 여부 (테스트 통과, 마이그레이션 양방향, CLI 잔류 참조 없음)
|
||||
- Discovery 성공 기준(S-1~S-3) 연결 확인
|
||||
|
||||
## 검증 항목
|
||||
| ID | 검증 항목 | 검증 방법 | 성공 기준 | 담당자 |
|
||||
|----|---------|---------|---------|--------|
|
||||
| VC-1 | 단위 테스트 전체 통과 | 자동 | `pytest tests/ -q` 실행 결과 73 passed, 0 failed, 0 errors | copilot |
|
||||
| VC-2 | CLI service-token 명령 정상 노출 | 수동 | `pfhub service-token --help` 출력에 `issue`, `revoke` 노출. `NAME`, `--user` 인수 표시. "bot" 문자열 없음 | copilot |
|
||||
| VC-3 | bot 관련 코드 잔류 없음 | 자동 | `grep -r "BotPrincipal\|bot_principal_id\|bot_principals" app/` 결과 0건 (마이그레이션 파일 제외) | copilot |
|
||||
| VC-4 | service token 발급 시 user_id 직접 저장 | 자동 | test_issue_service_token_defaults_to_365_days 통과. 발급 토큰의 user_id가 users.id와 일치 | copilot |
|
||||
| VC-5 | service token name 중복 시 오류 반환 | 자동 | `app/services/auth_service.py` L144~145에 `psycopg.errors.UniqueViolation` catch 존재 확인. 코드 리뷰로 처리 경로 확인. DB 제약 강제는 VC-8(migration UNIQUE INDEX)이 담당 | copilot |
|
||||
| VC-6 | --user 필수 옵션 강제 | 자동 | `pfhub service-token issue nightly` (--user 생략) 실행 시 exit code != 0 | copilot |
|
||||
| VC-7 | /auth/bots 경로 미노출 확인 | 자동 | FastAPI openapi.json에 `/auth/bots`, `/auth/me/bots` 경로 미포함 | copilot |
|
||||
| VC-8 | 마이그레이션 파일 upgrade 실행 가능 확인 | 수동 | `alembic upgrade head` 실행 오류 없음. bot_principals, auth_audit_logs 테이블 DROP 확인 | Justin |
|
||||
| VC-9 | 마이그레이션 downgrade 복원 가능 확인 | 수동 | `alembic downgrade -1` 실행 오류 없음. bot_principals 테이블 재생성 확인 | Justin |
|
||||
| VC-10 | auth_tokens.user_id FK 정합성 확인 | 수동 | 마이그레이션 후 `SELECT COUNT(*) FROM auth_tokens WHERE user_id NOT IN (SELECT id FROM users)` 결과 0건 | Justin |
|
||||
|
||||
## 성공 기준 연결
|
||||
Discovery의 성공 기준(S-N)과 검증 항목(VC-N)의 연결을 명시한다.
|
||||
|
||||
- S-1 (pytest 전체 통과) → VC-1, VC-4, VC-5, VC-6
|
||||
- S-2 (CLI service-token 명령 정상 노출) → VC-2, VC-6
|
||||
- S-3 (마이그레이션 후 auth_tokens.user_id FK 정합성) → VC-8, VC-9, VC-10
|
||||
|
||||
FR/NFR 연결:
|
||||
- FR-1 (bot 관련 코드/테이블 제거) → VC-3, VC-7, VC-8
|
||||
- FR-2 (user_id 직접 저장) → VC-4, VC-10
|
||||
- FR-3 (마이그레이션 user_id 보정) → VC-8, VC-10
|
||||
- FR-4 (get_token_principal 단순화) → VC-1, VC-3
|
||||
- FR-5 (service token name 유니크) → VC-5
|
||||
- FR-6 (--user username 조회, 없으면 오류) → VC-4, VC-5
|
||||
- FR-7 (--user 필수) → VC-6
|
||||
- NFR-1 (기존 테스트 통과) → VC-1
|
||||
- NFR-2 (마이그레이션 양방향) → VC-8, VC-9
|
||||
- NFR-3 (CLI bot 참조 없음) → VC-2, VC-3
|
||||
|
||||
# 3. 검증 결과
|
||||
검증 수행 후 각 항목의 결과를 기록한다.
|
||||
|
||||
| ID | 검증 항목 | 결과 | 비고 |
|
||||
|----|---------|------|------|
|
||||
| VC-1 | 단위 테스트 전체 통과 | 통과 | `pytest tests/ -q`: 73 passed, 0 failed, 0 errors |
|
||||
| VC-2 | CLI service-token 명령 정상 노출 | 통과 | `pfhub service-token issue --help`: NAME, --user 노출. "bot" 문자열 없음 |
|
||||
| VC-3 | bot 관련 코드 잔류 없음 | 통과 | `grep -r "BotPrincipal\|bot_principal_id\|bot_principals" app/` 결과 0건 |
|
||||
| VC-4 | service token 발급 시 user_id 직접 저장 | 통과 | test_issue_service_token_defaults_to_365_days 포함 73 passed |
|
||||
| VC-5 | service token name 중복 시 오류 반환 | 통과 | `auth_service.py` L144~145 UniqueViolation catch 코드 리뷰 확인. DB 제약은 VC-8(upgrade 검증) 의존 |
|
||||
| VC-6 | --user 필수 옵션 강제 | 통과 | `pfhub service-token issue nightly` (--user 생략): exit code 2, "Missing option '--user'" |
|
||||
| VC-7 | /auth/bots 경로 미노출 확인 | 통과 | OpenAPI paths에 bot 포함 경로 0건 확인 |
|
||||
| VC-8 | 마이그레이션 파일 upgrade 실행 가능 확인 | 미완료 | 라이브 DB 환경 필요 |
|
||||
| VC-9 | 마이그레이션 downgrade 복원 가능 확인 | 미완료 | 라이브 DB 환경 필요 |
|
||||
| VC-10 | auth_tokens.user_id FK 정합성 확인 | 미완료 | 라이브 DB 환경 필요 |
|
||||
|
||||
## 전체 검증 결과
|
||||
- 통과: 7 / 10
|
||||
- 실패/블로킹: 0
|
||||
- 미완료(수동/DB 필요): 3 (VC-8, VC-9, VC-10)
|
||||
- 검증 완료 여부: false (VC-8~VC-10 수동 검증 대기)
|
||||
|
||||
# 4. 결함 및 재작업 사항
|
||||
검증 중 발견된 결함과 재작업이 필요한 항목을 기록한다.
|
||||
|
||||
- BUG-1: 없음
|
||||
|
||||
# 5. 리스크 및 오픈 질문
|
||||
Verification 단계에서 새로 식별된 리스크와 오픈 질문을 기록한다.
|
||||
|
||||
## 리스크
|
||||
- R-1 (High, 이월): 마이그레이션 upgrade 전 `bot_principals.owner_user_id IS NULL` 행 존재 시 FK 위반 — upgrade() 사전 검사로 차단. 운영 DB 실행 전 NULL 행 수 확인 권장.
|
||||
- R-2 (Medium, 이월): VC-8~VC-10은 실제 DB 환경(Docker Compose)이 필요한 수동 검증 항목이다 — 로컬 Docker Compose 환경 기동 후 검증 수행.
|
||||
|
||||
## 오픈 질문
|
||||
없음.
|
||||
|
||||
# 6. Release로 넘기기 전 확인 체크
|
||||
Release 단계로 넘기기 전에 무엇이 완료되어야 하는지 확인한다.
|
||||
|
||||
- [ ] 모든 검증 항목(VC-N) 통과
|
||||
- 기준: VC-1~VC-10 모든 항목이 "통과" 상태여야 한다
|
||||
- [ ] 결함(BUG) `Open` 상태 없음
|
||||
- 기준: BUG 목록에 `Open` 상태 항목이 0개여야 한다
|
||||
- [ ] 성공 기준(S-N) 전체 충족
|
||||
- 기준: S-1(VC-1), S-2(VC-2, VC-6), S-3(VC-8, VC-10) 모두 통과
|
||||
- [ ] 모든 오픈 질문(OQ) `Open` 상태 없음 확인
|
||||
- 기준: OQ 목록에 `Open` 상태 항목이 0개 — 현재 없음
|
||||
- [ ] Freeze 확정 담당자(CONFIRMED_BY) 지정
|
||||
- 기준: 담당자 Justin 지정 완료 — 문서 서명 대기
|
||||
|
||||
# 7. 파일 처리 결과
|
||||
이 Verification 문서 생성/갱신 결과와 참조 문서를 기록한다.
|
||||
|
||||
- 처리 결과: 생성
|
||||
- 생성 경로: docs/sdlc/sdlc-002_20260423_service-token-user-based/5_verification.md
|
||||
- 참조 문서:
|
||||
- docs/sdlc/sdlc-002_20260423_service-token-user-based/4_implementation.md
|
||||
- docs/sdlc/sdlc-002_20260423_service-token-user-based/1_discovery.md
|
||||
|
||||
# 8. 사용자 결정 필요 항목 요약
|
||||
사용자나 승인자가 결정해야 하는 항목만 모아 정리한다.
|
||||
|
||||
## DECIDE
|
||||
없음.
|
||||
|
||||
## CONFIRM
|
||||
- Freeze 확정 담당자(`CONFIRMED_BY`): Justin — confirm-verification 단계에서 처리
|
||||
|
||||
## DATA
|
||||
- VC-8~VC-10 수동 검증은 로컬 Docker Compose DB 환경(`docker compose -f docker-compose.local-test.yml up -d`) 기동 후 수행
|
||||
- 운영 DB 마이그레이션 전 `SELECT COUNT(*) FROM bot_principals WHERE owner_user_id IS NULL` 결과 0건 확인 권장 (R-1)
|
||||
|
||||
# 9. Verification Freeze
|
||||
Release 인계 시점의 확정 플래그와 섹션 참조만 기록한다.
|
||||
|
||||
## 섹션 참조
|
||||
- 검증 계획: [# 2. 검증 계획](#2-검증-계획)
|
||||
- 검증 결과: [# 3. 검증 결과](#3-검증-결과)
|
||||
- 결함/재작업: [# 4. 결함 및 재작업 사항](#4-결함-및-재작업-사항)
|
||||
|
||||
## 확정 플래그
|
||||
- Handoff Decision: 승인
|
||||
- Handoff Rationale: 검증 완료 및 결함 없음 확인 후 Release로 인계
|
||||
- Ready for Release: true
|
||||
- Confirmed By: Justin
|
||||
- Confirmed At (KST): 2026-04-23
|
||||
@@ -1,164 +0,0 @@
|
||||
# 0. 문서 상태
|
||||
이 문서의 현재 상태와 식별 정보를 기록한다. 문서 추적과 승인 흐름에 필요한 최소 메타데이터를 적는다.
|
||||
|
||||
- 상태: confirmed
|
||||
- 문서 ID: sdlc-002_20260423_service-token-user-based
|
||||
- 이슈명: service-token user-based 소유권
|
||||
- 참조 Verification: docs/sdlc/sdlc-002_20260423_service-token-user-based/5_verification.md
|
||||
- 작성 시각(KST): 2026-04-23
|
||||
- 마지막 갱신 시각(KST): 2026-04-23
|
||||
|
||||
# 1. Verification 인계 요약
|
||||
Verification에서 완료된 핵심 내용을 요약한다. 원본은 5_verification.md를 참조하며, 이 섹션은 Release 작업의 입력 기준선이 된다.
|
||||
|
||||
## 이슈 목적
|
||||
`bot_principals` / `auth_audit_logs` 테이블을 제거하고, service token을 `auth_tokens` 단일 엔티티로 직접 발급하도록 재구현한다. CLI `pfhub service-token issue <name> --user <username>` 명령으로 사용자 기반 소유권을 명확히 한다.
|
||||
|
||||
## 검증 결과 요약
|
||||
- 검증 항목 수: 10개
|
||||
- 전체 통과 여부: false (VC-8~VC-10 수동 DB 검증 미완료)
|
||||
- 자동 검증 통과: 7 / 10 (VC-1~VC-7)
|
||||
- 결함(BUG) 잔여: 0건
|
||||
|
||||
## 이월된 오픈 질문 / 리스크
|
||||
Verification에서 이월된 항목:
|
||||
|
||||
- **VC-8 (이월)**: `alembic upgrade head` 실행 가능 여부 — 라이브 DB 환경 미기동으로 미검증. 배포 전 로컬 Docker Compose 환경에서 Justin이 수행해야 한다.
|
||||
- **VC-9 (이월)**: `alembic downgrade -1` 복원 가능 여부 — 동일. 배포 전 Justin이 수행.
|
||||
- **VC-10 (이월)**: `auth_tokens.user_id FK` 정합성 쿼리 — 동일. 배포 전 Justin이 수행.
|
||||
- **R-1 (이월, High)**: 운영 DB에 `bot_principals.owner_user_id IS NULL` 행이 존재하면 마이그레이션 upgrade 시 FK 위반 발생 가능 — upgrade 실행 전 `SELECT COUNT(*) FROM bot_principals WHERE owner_user_id IS NULL` 결과 0건 확인 필수.
|
||||
|
||||
# 2. 배포 준비
|
||||
배포 환경, 배포 전 체크리스트, 배포 순서를 정의한다.
|
||||
|
||||
## 배포 환경
|
||||
- 대상 환경: production (Linux/Ubuntu 서버, systemd + uvicorn + Caddy)
|
||||
- 배포 방식: 수동 — git pull 또는 파일 복사 후 마이그레이션 실행 및 서비스 재시작
|
||||
- 배포 도구/채널: git, alembic, systemctl (CI/CD 파이프라인 없음)
|
||||
|
||||
## 배포 전 체크리스트
|
||||
배포 실행 전에 확인해야 할 항목을 나열한다.
|
||||
|
||||
- [ ] 로컬 Docker Compose 환경에서 VC-8 (alembic upgrade head) 성공 확인
|
||||
- [ ] 로컬 Docker Compose 환경에서 VC-9 (alembic downgrade -1) 성공 확인
|
||||
- [ ] 로컬 Docker Compose 환경에서 VC-10 (auth_tokens.user_id FK 정합성 쿼리) 0건 확인
|
||||
- [ ] 운영 DB에서 `SELECT COUNT(*) FROM bot_principals WHERE owner_user_id IS NULL` 결과 0건 확인 (R-1)
|
||||
- [ ] 운영 DB 전체 백업 완료
|
||||
- [ ] 배포 대상 서버에서 `git log --oneline -5` 로 현재 배포 커밋 해시 기록
|
||||
- [ ] 롤백 계획 확인 완료
|
||||
|
||||
## 배포 순서
|
||||
변경 사항의 배포 순서를 적는다. 의존 관계가 있는 경우 순서를 명확히 기술한다.
|
||||
|
||||
1. **코드 업데이트**: 배포 서버에서 `git pull` 또는 소스 파일 배포 (마이그레이션 파일 포함)
|
||||
2. **패키지 재설치 확인**: 이번 변경 사항에 의존성 추가 없음 — 생략
|
||||
3. **DB 마이그레이션**: `alembic upgrade head` — `bot_principals`, `auth_audit_logs` DROP, token_type 보정, UNIQUE INDEX 생성
|
||||
4. **서비스 재시작**: `sudo systemctl restart portfoliohub` (또는 프로세스 매니저에 따라 uvicorn 재기동)
|
||||
5. **배포 후 확인**: 스모크 테스트 수행 (`GET /health`, CLI --help, OpenAPI 확인)
|
||||
|
||||
# 3. 롤백 계획
|
||||
배포 후 문제가 발생했을 때 이전 상태로 되돌리는 방법을 기술한다.
|
||||
|
||||
## 롤백 조건
|
||||
아래 조건 중 하나라도 충족되면 롤백을 즉시 실행한다.
|
||||
|
||||
- 배포 후 `GET /health` 응답이 200이 아닌 경우
|
||||
- `pfhub service-token issue --help` 실행 시 오류 발생 또는 NAME/--user 미노출
|
||||
- 인증 관련 API (`/auth/login`, `/auth/tokens`) 오류율 급증 (5분 내 비정상 응답 3회 이상)
|
||||
- 마이그레이션 upgrade 실패 또는 롤백 불가 오류 발생
|
||||
|
||||
## 롤백 절차
|
||||
1. **서비스 중단**: `sudo systemctl stop portfoliohub`
|
||||
2. **DB 마이그레이션 롤백**: `alembic downgrade -1` — `bot_principals`, `auth_audit_logs` 테이블 재생성, token_type 복원, UNIQUE INDEX DROP
|
||||
3. **코드 롤백**: `git checkout <이전 커밋 해시>` 또는 이전 소스 파일 복원
|
||||
4. **서비스 재기동**: `sudo systemctl start portfoliohub`
|
||||
5. **롤백 확인**: `GET /health` 200 응답 확인
|
||||
|
||||
## 롤백 담당자
|
||||
- Justin
|
||||
|
||||
# 4. 배포 후 확인
|
||||
배포 완료 후 정상 동작을 확인하는 방법을 기술한다.
|
||||
|
||||
## 스모크 테스트 항목
|
||||
| 항목 | 확인 방법 | 성공 기준 |
|
||||
|------|---------|---------|
|
||||
| 헬스 체크 정상 응답 | `curl -s http://localhost:8000/health` | HTTP 200, `{"status":"ok"}` |
|
||||
| CLI service-token issue 명령 노출 | `pfhub service-token issue --help` | NAME, --user 노출. "bot" 문자열 없음. exit 0 |
|
||||
| CLI --user 필수 옵션 강제 | `pfhub service-token issue nightly` (--user 생략) | exit 2, "Missing option '--user'" 출력 |
|
||||
| /auth/bots 경로 미노출 | `curl -s http://localhost:8000/openapi.json \| python3 -m json.tool \| grep bot` | 출력 없음 (0건) |
|
||||
| DB 마이그레이션 반영 | `psql -c "\dt bot_principals"` | 테이블 없음 (`Did not find any relation named "bot_principals"`) |
|
||||
|
||||
## 모니터링 확인 항목
|
||||
배포 후 일정 시간 동안 모니터링해야 하는 지표나 로그를 기록한다.
|
||||
|
||||
- `journalctl -u portfoliohub -f`: 배포 직후 30분간 ERROR/CRITICAL 로그 없음 확인
|
||||
- `/auth/login`, `/auth/tokens` 엔드포인트 정상 응답 유지 확인 (1시간)
|
||||
- `auth_tokens` 테이블 `token_type = 'service'` 행 존재 여부: `SELECT COUNT(*) FROM auth_tokens WHERE token_type = 'service'` 결과 확인 (service token 발급 이력이 있는 경우에 한해 수행)
|
||||
|
||||
# 5. 리스크 및 오픈 질문
|
||||
Release 단계에서 새로 식별된 리스크와 오픈 질문을 기록한다.
|
||||
|
||||
## 리스크
|
||||
- R-1 (High, 이월): 운영 DB에 `bot_principals.owner_user_id IS NULL` 행 존재 시 마이그레이션 upgrade FK 위반
|
||||
- 완화 방안: 배포 전 체크리스트에 사전 쿼리 확인 포함. NULL 행 존재 시 배포 중단 후 수동 정리.
|
||||
- R-2 (Medium, 이월): VC-8~VC-10 수동 검증 미완료 상태로 배포 진행 시 DB 변경 미검증 위험
|
||||
- 완화 방안: 배포 전 로컬 Docker Compose 환경에서 Justin이 VC-8~VC-10 완료 후 배포 진행.
|
||||
|
||||
## 오픈 질문
|
||||
없음.
|
||||
|
||||
# 6. Operations로 넘기기 전 확인 체크
|
||||
Operations 단계로 넘기기 전에 무엇이 완료되어야 하는지 확인한다.
|
||||
|
||||
- [ ] 배포 완료 및 스모크 테스트 통과
|
||||
- 기준: `# 4` 스모크 테스트 항목 전 항목이 성공 기준을 충족하였는가
|
||||
- [ ] 롤백 계획 확정
|
||||
- 기준: 롤백 조건, 절차, 담당자(`REL_ROLLBACK_OWNER`)가 모두 명시되었는가
|
||||
- [ ] 배포 전 VC-8~VC-10 수동 검증 완료 확인
|
||||
- 기준: Justin이 로컬 Docker Compose 환경에서 VC-8, VC-9, VC-10 모두 통과 확인
|
||||
- [ ] R-1 사전 쿼리 확인 완료
|
||||
- 기준: `SELECT COUNT(*) FROM bot_principals WHERE owner_user_id IS NULL` 결과 0건
|
||||
- [ ] 모든 오픈 질문(OQ) `Open` 상태 없음 확인
|
||||
- 기준: OQ 목록에 `Open` 상태 항목이 0개 — 현재 없음
|
||||
- [ ] Freeze 확정 담당자(CONFIRMED_BY) 지정
|
||||
- 기준: 담당자 Justin 지정 완료 — confirm-release 단계에서 처리
|
||||
|
||||
# 7. 파일 처리 결과
|
||||
이 Release 문서 생성/갱신 결과와 참조 문서를 기록한다.
|
||||
|
||||
- 처리 결과: 생성
|
||||
- 생성 경로: docs/sdlc/sdlc-002_20260423_service-token-user-based/6_release.md
|
||||
- 참조 문서:
|
||||
- docs/sdlc/sdlc-002_20260423_service-token-user-based/5_verification.md
|
||||
- docs/sdlc/sdlc-002_20260423_service-token-user-based/4_implementation.md
|
||||
- docs/technical/deployment-and-operations.md
|
||||
|
||||
# 8. 사용자 결정 필요 항목 요약
|
||||
사용자나 승인자가 결정해야 하는 항목만 모아 정리한다.
|
||||
|
||||
## DECIDE
|
||||
없음.
|
||||
|
||||
## CONFIRM
|
||||
- Freeze 확정 담당자(`CONFIRMED_BY`): Justin — confirm-release 단계에서 처리
|
||||
|
||||
## DATA
|
||||
- 롤백 담당자(`REL_ROLLBACK_OWNER`): 지정 필요 (Justin 또는 담당 운영자)
|
||||
- 배포 전 VC-8~10: 로컬 Docker Compose 환경(`docker compose -f docker-compose.local-test.yml up -d`) 기동 후 수행
|
||||
- 운영 DB 사전 쿼리: `SELECT COUNT(*) FROM bot_principals WHERE owner_user_id IS NULL` — 0건이어야 배포 진행 가능
|
||||
|
||||
# 9. Release Freeze
|
||||
Operations 인계 시점의 확정 플래그와 섹션 참조만 기록한다. 내용 재작성 없이 원본 섹션을 링크로 대신한다.
|
||||
|
||||
## 섹션 참조
|
||||
- 배포 준비: [# 2. 배포 준비](#2-배포-준비)
|
||||
- 롤백 계획: [# 3. 롤백 계획](#3-롤백-계획)
|
||||
- 배포 후 확인: [# 4. 배포 후 확인](#4-배포-후-확인)
|
||||
|
||||
## 확정 플래그
|
||||
- Handoff Decision: 승인
|
||||
- Handoff Rationale: 배포 완료 및 스모크 테스트 통과 후 Operations로 인계
|
||||
- Ready for Operations: true
|
||||
- Confirmed By: Justin
|
||||
- Confirmed At (KST): 2026-04-23
|
||||
@@ -1,148 +0,0 @@
|
||||
# 0. 문서 상태
|
||||
이 문서의 현재 상태와 식별 정보를 기록한다. 문서 추적과 승인 흐름에 필요한 최소 메타데이터를 적는다.
|
||||
|
||||
- 상태: confirmed
|
||||
- 문서 ID: sdlc-002_20260423_service-token-user-based
|
||||
- 이슈명: service-token user-based 소유권
|
||||
- 참조 Release: docs/sdlc/sdlc-002_20260423_service-token-user-based/6_release.md
|
||||
- 작성 시각(KST): 2026-04-23
|
||||
- 마지막 갱신 시각(KST): 2026-04-23
|
||||
|
||||
# 1. Release 인계 요약
|
||||
Release에서 완료된 핵심 내용을 요약한다. 원본은 6_release.md를 참조하며, 이 섹션은 Operations 작업의 입력 기준선이 된다.
|
||||
|
||||
## 이슈 목적
|
||||
`bot_principals` / `auth_audit_logs` 테이블을 제거하고, service token을 `auth_tokens` 단일 엔티티로 직접 발급하도록 재구현한다. CLI `pfhub service-token issue <name> --user <username>` 명령으로 사용자 기반 소유권을 명확히 한다.
|
||||
|
||||
## 배포 결과 요약
|
||||
- 배포 환경: production (Linux/Ubuntu, systemd + uvicorn + Caddy)
|
||||
- 배포 완료 시각(KST): {{OPS_DEPLOYED_AT:TBD}}
|
||||
- 스모크 테스트 결과: 미완료
|
||||
|
||||
## 이월된 오픈 질문 / 리스크
|
||||
Release에서 이월된 항목:
|
||||
|
||||
- **VC-8~VC-10 (이월)**: 로컬 Docker Compose 환경 기동 후 Justin이 alembic upgrade/downgrade 및 FK 정합성 쿼리 수동 검증 필요. 배포 전 완료 목표.
|
||||
- **R-1 (이월, High)**: 운영 DB `bot_principals.owner_user_id IS NULL` 행 존재 시 마이그레이션 FK 위반 가능 — 배포 전 사전 쿼리 확인으로 차단.
|
||||
|
||||
# 2. 운영 모니터링
|
||||
배포 후 지속적으로 모니터링해야 하는 항목과 기준을 정의한다.
|
||||
|
||||
## 모니터링 항목
|
||||
| 항목 | 확인 방법 | 임계값 / 이상 기준 | 대응 방안 |
|
||||
|------|---------|----------------|---------|
|
||||
| 서비스 오류 로그 | `journalctl -u portfoliohub --since "1 hour ago" \| grep -E "ERROR\|CRITICAL"` | ERROR/CRITICAL 로그 1건 이상 발생 | 스택 트레이스 확인 후 장애 대응 절차 진입 |
|
||||
| 헬스 체크 응답 | `curl -s http://localhost:8000/health` | HTTP 200 외 응답 또는 응답 없음 | systemd 서비스 상태 확인 (`systemctl status portfoliohub`) 후 재기동 또는 롤백 |
|
||||
| 인증 API 정상 응답 | `curl -s -o /dev/null -w "%{http_code}" http://localhost:8000/auth/login -X POST -d '{}'` | 5분 내 비정상 응답(4xx 외 500+) 3회 이상 | 오류 로그 확인 후 롤백 판단 |
|
||||
| DB 마이그레이션 상태 | `alembic current` | 최신 리비전(`20260423_0013`) 외 값 표시 | 마이그레이션 재실행 또는 롤백 판단 |
|
||||
| bot_principals 테이블 미존재 유지 | `psql -c "\dt bot_principals"` | 테이블이 재생성된 경우 | 마이그레이션 상태 확인 후 이상 여부 조사 |
|
||||
|
||||
## 모니터링 담당자
|
||||
- Justin
|
||||
|
||||
# 3. 운영 절차 (런북)
|
||||
이 변경 사항과 관련된 운영 중 발생할 수 있는 상황별 대응 절차를 정의한다.
|
||||
|
||||
## 정상 운영 확인 절차
|
||||
배포 후 일정 주기로 정상 동작을 확인하는 절차를 기술한다.
|
||||
|
||||
1. **일 1회** — `journalctl -u portfoliohub --since "24 hours ago" | grep -E "ERROR|CRITICAL"` 실행. 결과 없음 확인.
|
||||
2. **일 1회** — `curl -s http://localhost:8000/health` 실행. `{"status":"ok"}` 응답 확인.
|
||||
3. **주 1회** — `alembic current` 실행. 최신 리비전(`20260423_0013`) 적용 상태 확인.
|
||||
4. **배포 후 첫 1주** — `pfhub service-token issue --help` 실행. NAME, --user 정상 노출 확인.
|
||||
|
||||
## 장애 대응 절차
|
||||
이 변경과 관련된 장애가 발생했을 때의 초기 대응 절차를 기술한다.
|
||||
|
||||
1. **오류 확인**: `journalctl -u portfoliohub -n 100 --no-pager` 실행. 스택 트레이스 및 오류 메시지 수집.
|
||||
2. **헬스 체크**: `curl -s http://localhost:8000/health` 실행. 응답 여부 확인.
|
||||
3. **서비스 재기동 시도**: `sudo systemctl restart portfoliohub`. 재기동 후 헬스 체크 재실행.
|
||||
4. **롤백 판단**: 재기동 후에도 오류 지속 시 `6_release.md # 3 롤백 조건` 충족 여부 확인.
|
||||
5. **롤백 실행** (조건 충족 시):
|
||||
- `sudo systemctl stop portfoliohub`
|
||||
- `alembic downgrade -1`
|
||||
- `git checkout <이전 커밋 해시>`
|
||||
- `sudo systemctl start portfoliohub`
|
||||
- `curl -s http://localhost:8000/health` 확인
|
||||
6. **에스컬레이션**: 롤백 후에도 복구 안 되면 에스컬레이션 경로 진입.
|
||||
|
||||
## 에스컬레이션 경로
|
||||
장애 대응 중 에스컬레이션이 필요한 경우 연락 대상을 기록한다.
|
||||
|
||||
- {{OPS_ESCALATION_1:TBD}}
|
||||
|
||||
# 4. 운영 중 발생한 이슈
|
||||
운영 중 발견된 이슈를 기록한다. 초안 생성 시 비워 두고 운영 중 갱신한다.
|
||||
|
||||
- 없음 (운영 중 발견 시 기록)
|
||||
|
||||
# 5. 리스크 및 오픈 질문
|
||||
Operations 단계에서 새로 식별된 리스크와 오픈 질문을 기록한다.
|
||||
|
||||
## 리스크
|
||||
- R-1 (High, 이월): 운영 DB `bot_principals.owner_user_id IS NULL` 행 존재 시 마이그레이션 upgrade FK 위반
|
||||
- 완화 방안: 배포 전 체크리스트 쿼리(`SELECT COUNT(*) FROM bot_principals WHERE owner_user_id IS NULL`) 결과 0건 확인 후 배포. NULL 행 발견 시 수동 정리 후 진행.
|
||||
- R-2 (Medium, 이월): VC-8~10 수동 DB 검증 미완료 상태로 배포 시 검증되지 않은 DB 변경 적용 위험
|
||||
- 완화 방안: 로컬 Docker Compose 환경에서 Justin이 VC-8~10 완료 확인 후 운영 배포 진행.
|
||||
|
||||
## 오픈 질문
|
||||
없음.
|
||||
|
||||
# 6. 다음 Discovery 환류
|
||||
운영 중 발견된 인사이트와 개선 사항을 다음 SDLC Discovery의 입력으로 기록한다.
|
||||
|
||||
- 없음 (운영 중 갱신 예정)
|
||||
|
||||
# 7. 확인 체크
|
||||
Operations 문서가 완성되었는지 확인한다.
|
||||
|
||||
- [ ] 모니터링 항목 및 임계값 정의 완료
|
||||
- 기준: 모니터링 항목에 임계값과 대응 방안이 명시되어야 한다 — 5개 항목 정의 완료
|
||||
- [ ] 런북(정상 운영/장애 대응 절차) 정의 완료
|
||||
- 기준: 운영 중 발생 가능한 상황에 대한 절차가 1단계 이상 있어야 한다 — 정상 4단계, 장애 6단계 정의 완료
|
||||
- [ ] 에스컬레이션 경로 정의 완료
|
||||
- 기준: 장애 시 연락할 담당자가 명시되어야 한다 — `{{OPS_ESCALATION_1:TBD}}` 지정 필요
|
||||
- [ ] 다음 Discovery 환류 항목 작성 (없으면 "없음" 기재)
|
||||
- 기준: 현재 "없음" 기재 — 운영 중 발견 시 갱신
|
||||
- [ ] Freeze 확정 담당자(CONFIRMED_BY) 지정
|
||||
- 기준: 문서 승인 담당자가 명시되어야 한다 — Justin 지정 완료 예정 (confirm-operation 단계에서 처리)
|
||||
|
||||
# 8. 파일 처리 결과
|
||||
이 Operations 문서 생성/갱신 결과와 참조 문서를 기록한다.
|
||||
|
||||
- 처리 결과: 생성
|
||||
- 생성 경로: docs/sdlc/sdlc-002_20260423_service-token-user-based/7_operation.md
|
||||
- 참조 문서:
|
||||
- docs/sdlc/sdlc-002_20260423_service-token-user-based/6_release.md
|
||||
- docs/sdlc/sdlc-002_20260423_service-token-user-based/5_verification.md
|
||||
- docs/sdlc/sdlc-002_20260423_service-token-user-based/1_discovery.md
|
||||
|
||||
# 9. 사용자 결정 필요 항목 요약
|
||||
사용자나 승인자가 결정해야 하는 항목만 모아 정리한다.
|
||||
|
||||
## DECIDE
|
||||
없음.
|
||||
|
||||
## CONFIRM
|
||||
- Freeze 확정 담당자(`CONFIRMED_BY`): Justin — confirm-operation 단계에서 처리
|
||||
|
||||
## DATA
|
||||
- 모니터링 담당자(`OPS_MONITOR_OWNER`): Justin 또는 담당 운영자 지정 필요
|
||||
- 에스컬레이션 연락처(`OPS_ESCALATION_1`): 장애 시 연락할 담당자/채널 지정 필요
|
||||
- 배포 완료 시각(`OPS_DEPLOYED_AT`): 실제 배포 후 기입
|
||||
- 스모크 테스트 결과(`OPS_SMOKE_RESULT`): 배포 후 스모크 테스트 수행 결과 기입
|
||||
|
||||
# 10. Operations Freeze
|
||||
이 SDLC 주기의 최종 확정 플래그를 기록한다.
|
||||
|
||||
## 섹션 참조
|
||||
- 운영 모니터링: [# 2. 운영 모니터링](#2-운영-모니터링)
|
||||
- 런북: [# 3. 운영 절차 런북](#3-운영-절차-런북)
|
||||
- 다음 Discovery 환류: [# 6. 다음 Discovery 환류](#6-다음-discovery-환류)
|
||||
|
||||
## 확정 플래그
|
||||
- Handoff Decision: 승인
|
||||
- Handoff Rationale: 모니터링 안정화 및 런북 완비 후 주기 완료
|
||||
- SDLC 주기 완료: true
|
||||
- Confirmed By: Justin
|
||||
- Confirmed At (KST): 2026-04-23
|
||||
@@ -1,12 +0,0 @@
|
||||
# sdlc-002_20260423_service-token-user-based
|
||||
|
||||
| 단계 | 문서 | 상태 |
|
||||
|------|------|------|
|
||||
| 1. Discovery | [1_discovery.md](./1_discovery.md) | `confirmed` |
|
||||
| 2. Planning | [2_planning.md](./2_planning.md) | `confirmed` |
|
||||
| 3. Design | [3_design.md](./3_design.md) | `confirmed` |
|
||||
| 4. Implementation | [4_implementation.md](./4_implementation.md) | `confirmed` |
|
||||
| 5. Verification | [5_verification.md](./5_verification.md) | `confirmed` |
|
||||
| 6. Release | [6_release.md](./6_release.md) | `confirmed` |
|
||||
| 7. Operations | [7_operations.md](./7_operations.md) | `confirmed` |
|
||||
| Summary | [summary.md](./summary.md) | `-` |
|
||||
@@ -1,16 +0,0 @@
|
||||
# Summary - sdlc-002_20260423_service-token-user-based
|
||||
|
||||
- 이슈명: service-token user-based 소유권
|
||||
- 상태: draft
|
||||
- 작성 시각(KST): 2026-04-23
|
||||
- 마지막 갱신 시각(KST): 2026-04-23
|
||||
|
||||
## 핵심 요약
|
||||
|
||||
- `bot_principal` / `bot token` 명칭을 `service_principal` / `service token`으로 전체 rename한다 (도메인/서비스/리포지터리/스키마/CLI/API/DB 마이그레이션 포함, 약 15개 파일).
|
||||
- `auth_tokens.user_id`에 `service_principal.owner_user_id`(실제 user ID)를 저장하도록 소유권 모델을 교정하고, `get_token_principal()`의 `COALESCE` 우회 로직을 제거한다.
|
||||
|
||||
## 단계별 문서
|
||||
|
||||
- [Discovery](./1_discovery.md)
|
||||
- [Index](./index.md)
|
||||
@@ -1,705 +0,0 @@
|
||||
# API Contracts
|
||||
|
||||
- Related REQ: `REQ-001`, `REQ-002`, `REQ-003`, `REQ-004`
|
||||
|
||||
## 공통 원칙
|
||||
- 응답은 JSON 기반으로 반환한다.
|
||||
- 인증이 필요한 엔드포인트는 `Authorization: Bearer <token>`을 사용한다.
|
||||
- 권한 계층은 `read` < `trade-write` < `admin`이다.
|
||||
- `admin`이 아닌 요청은 포트폴리오 리소스 접근 시 `owner_user_id` 소유권 검사를 추가로 통과해야 한다.
|
||||
- 에러 응답은 HTTP 상태 코드와 함께 원인 메시지를 제공한다.
|
||||
- Base URL 예시는 `http://localhost:8000`을 기준으로 한다.
|
||||
- JSON 예시는 대표 형태이며 일부 선택 필드를 생략할 수 있다.
|
||||
|
||||
## Contract Surface
|
||||
|
||||
### 공개 문서 엔드포인트
|
||||
- `GET /docs`
|
||||
- `GET /redoc`
|
||||
- `GET /openapi.json`
|
||||
|
||||
### 헬스
|
||||
- `GET /health` (인증 없음)
|
||||
- 정상 응답 예시:
|
||||
```json
|
||||
{"status": "ok"}
|
||||
```
|
||||
|
||||
### 인증
|
||||
- `POST /auth/login`
|
||||
- `GET /auth/tokens`
|
||||
- `POST /auth/tokens`
|
||||
- `DELETE /auth/tokens/{token_id}`
|
||||
|
||||
### 포트폴리오
|
||||
- `GET /portfolios`
|
||||
- `GET /portfolios/{portfolio_id}`
|
||||
- `POST /portfolios`
|
||||
- `PATCH /portfolios/{portfolio_id}`
|
||||
- `POST /portfolios/{portfolio_id}/buy`
|
||||
- `POST /portfolios/{portfolio_id}/sell`
|
||||
- `POST /portfolios/{portfolio_id}/invest`
|
||||
- `POST /portfolios/{portfolio_id}/transfer`
|
||||
- `POST /portfolios/{portfolio_id}/reset`
|
||||
- `DELETE /portfolios/{portfolio_id}`
|
||||
- `POST /portfolios/{portfolio_id}/tags`
|
||||
- `GET /portfolios/{portfolio_id}/tags`
|
||||
- `DELETE /portfolios/{portfolio_id}/tags`
|
||||
|
||||
REQ-002 정렬 메모:
|
||||
|
||||
- 포트폴리오 목록 조회는 반복 query parameter `includeTags`, `excludeTags`, `matchMode`를 지원한다.
|
||||
- 태그 mutation은 기존 포트폴리오 리소스 하위 경로 `/portfolios/{portfolio_id}/tags`로 구현한다.
|
||||
- CLI는 `portfolio tag add|list|remove` 및 `portfolio list --include-tag/--exclude-tag/--match-mode` 계약을 제공한다.
|
||||
|
||||
### 포트폴리오 댓글 (`REQ-001`)
|
||||
- `POST /portfolios/{portfolio_id}/comments`
|
||||
- `GET /portfolios/{portfolio_id}/comments`
|
||||
- `PATCH /portfolios/{portfolio_id}/comments/{comment_id}`
|
||||
- `DELETE /portfolios/{portfolio_id}/comments/{comment_id}`
|
||||
|
||||
### 전략셋
|
||||
- `GET /strategy-sets`
|
||||
- `POST /strategy-sets`
|
||||
- `GET /strategy-sets/{strategy_set_id}`
|
||||
- `PATCH /strategy-sets/{strategy_set_id}`
|
||||
- `DELETE /strategy-sets/{strategy_set_id}`
|
||||
|
||||
### 사용자
|
||||
- `GET /users`
|
||||
- `GET /users/{user_id}`
|
||||
- `POST /users`
|
||||
- `PATCH /users/{user_id}`
|
||||
- `DELETE /users/{user_id}`
|
||||
|
||||
## 오류 처리 기준
|
||||
|
||||
| 시나리오 | HTTP |
|
||||
| --- | --- |
|
||||
| 인증 헤더 누락/형식 오류 | `401` |
|
||||
| 유효하지 않은 토큰 | `401` |
|
||||
| 권한 부족 | `403` |
|
||||
| 유효성 검증 실패 | `422` |
|
||||
| 리소스 없음 | `404` |
|
||||
| 내부 예외 | `500` |
|
||||
|
||||
## 상세 엔드포인트 명세
|
||||
|
||||
### 헬스
|
||||
|
||||
#### `GET /health`
|
||||
- 인증 없음
|
||||
- 응답 `200`:
|
||||
|
||||
```json
|
||||
{"status": "ok"}
|
||||
```
|
||||
|
||||
### 포트폴리오
|
||||
|
||||
#### `GET /portfolios`
|
||||
- Auth: `read`
|
||||
- Ownership: `admin`은 전체 조회, 그 외는 본인 소유(`owner_user_id == auth_context.user_id`)만 조회
|
||||
- Query:
|
||||
- `broker: string | null`
|
||||
- `status_filter: string | null`
|
||||
- `includeTags: string[] | null`
|
||||
- `excludeTags: string[] | null`
|
||||
- `matchMode: "AND" | "OR" | null` (`includeTags`에만 적용)
|
||||
- 응답 `200`: `PortfolioListItem[]`
|
||||
- 오류:
|
||||
- `400`: `matchMode`가 `AND` 또는 `OR`가 아님
|
||||
|
||||
예시:
|
||||
|
||||
```bash
|
||||
curl -X GET "http://localhost:8000/portfolios?broker=upbit&status_filter=enabled&includeTags=grade:A&excludeTags=blacklist:trade&matchMode=OR" \
|
||||
-H "Authorization: Bearer <READ_TOKEN>"
|
||||
```
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"portfolio_id": "krw-btc",
|
||||
"broker": "upbit",
|
||||
"status": "enabled",
|
||||
"name": "Bitcoin",
|
||||
"symbol": "KRW-BTC",
|
||||
"tags": ["grade:A"],
|
||||
"cash_balance": 1000000.0,
|
||||
"asset_quantity": 0.1,
|
||||
"price": 100000000.0,
|
||||
"profit_buffer": 0.0,
|
||||
"active_strategy_id": "stv-001",
|
||||
"avabile_strategies": ["stv-001"],
|
||||
"created_at": "2026-04-03T00:00:00Z",
|
||||
"updated_at": "2026-04-03T00:00:00Z",
|
||||
"last_trade_at": "2026-04-03T00:00:00Z",
|
||||
"total_value": 11000000.0
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
#### `GET /portfolios/{portfolio_id}`
|
||||
- Auth: `read`
|
||||
- Ownership: `admin`이 아니고 소유주 불일치 시 `403`
|
||||
- Query:
|
||||
- `recent_trades: int = 5`
|
||||
- 응답 `200`: `PortfolioDetailResponse`
|
||||
|
||||
```bash
|
||||
curl -X GET "http://localhost:8000/portfolios/krw-btc?recent_trades=5" \
|
||||
-H "Authorization: Bearer <READ_TOKEN>"
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"portfolio": {
|
||||
"portfolio_id": "krw-btc",
|
||||
"broker": "upbit",
|
||||
"status": "enabled",
|
||||
"name": "Bitcoin",
|
||||
"symbol": "KRW-BTC",
|
||||
"tags": ["grade:A"],
|
||||
"owner_user_id": "user-1",
|
||||
"cash_balance": 1000000.0,
|
||||
"asset_quantity": 0.1,
|
||||
"price": 100000000.0,
|
||||
"profit_buffer": 0.0,
|
||||
"active_strategy_id": "stv-001",
|
||||
"avabile_strategies": ["stv-001"],
|
||||
"created_at": "2026-04-03T00:00:00Z",
|
||||
"updated_at": "2026-04-03T00:00:00Z",
|
||||
"last_trade_at": "2026-04-03T00:00:00Z"
|
||||
},
|
||||
"recent_entries": [
|
||||
{
|
||||
"portfolio_id": "krw-btc",
|
||||
"recorded_at": "2026-04-03T00:00:00Z",
|
||||
"action": "buy",
|
||||
"price": 100000000.0,
|
||||
"quantity": 0.1,
|
||||
"fee_amount": 0.0,
|
||||
"tax_amount": 0.0,
|
||||
"cash_balance_after": 1000000.0,
|
||||
"asset_quantity_after": 0.1,
|
||||
"total_value_after": 11000000.0,
|
||||
"profit_buffer_after": 0.0,
|
||||
"strategy_set_id": "stv-001",
|
||||
"note": ""
|
||||
}
|
||||
],
|
||||
"strategy_set": {
|
||||
"id": "stv-001",
|
||||
"name": "target_value",
|
||||
"config": "{}",
|
||||
"nick": "target_value:99914b932bd3",
|
||||
"created_at": "2026-04-03T00:00:00Z"
|
||||
},
|
||||
"live_quantity": null
|
||||
}
|
||||
```
|
||||
|
||||
#### `POST /portfolios`
|
||||
- Auth: `trade-write`
|
||||
- Ownership: 요청 body로 owner를 받지 않고 인증 컨텍스트 사용자로 `owner_user_id`를 강제 설정
|
||||
- Body: `CreatePortfolioRequest`
|
||||
- 응답 `201`: `PortfolioDetailResponse`
|
||||
|
||||
```bash
|
||||
curl -X POST "http://localhost:8000/portfolios" \
|
||||
-H "Authorization: Bearer <TRADE_WRITE_TOKEN>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"portfolio_id": "krw-btc",
|
||||
"broker": "upbit",
|
||||
"status": "enabled",
|
||||
"name": "Bitcoin",
|
||||
"symbol": "KRW-BTC",
|
||||
"strategy_name": "target_value",
|
||||
"strategy_params": {},
|
||||
"active_strategy_id": null,
|
||||
"avabile_strategies": null,
|
||||
"cash_balance": 1000000,
|
||||
"asset_quantity": 0,
|
||||
"asset_price": 0,
|
||||
"profit_buffer": 0
|
||||
}'
|
||||
```
|
||||
|
||||
#### `PATCH /portfolios/{portfolio_id}`
|
||||
- Auth: `trade-write`
|
||||
- Body: `UpdatePortfolioRequest`
|
||||
- 응답 `200`: `PortfolioDetailResponse`
|
||||
|
||||
```bash
|
||||
curl -X PATCH "http://localhost:8000/portfolios/krw-btc" \
|
||||
-H "Authorization: Bearer <TRADE_WRITE_TOKEN>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"broker": "upbit",
|
||||
"name": "Bitcoin",
|
||||
"symbol": "KRW-BTC"
|
||||
}'
|
||||
```
|
||||
|
||||
#### 거래/자금 이동 엔드포인트
|
||||
- `POST /portfolios/{portfolio_id}/buy`: `BuyRequest` -> `PortfolioDetailResponse`
|
||||
- `POST /portfolios/{portfolio_id}/sell`: `SellRequest` -> `PortfolioDetailResponse`
|
||||
- `POST /portfolios/{portfolio_id}/invest`: `InvestRequest` -> `PortfolioDetailResponse`
|
||||
- `POST /portfolios/{portfolio_id}/transfer`: `TransferRequest` -> `PortfolioDetailResponse`
|
||||
- `POST /portfolios/{portfolio_id}/reset`: `ResetPortfolioRequest` -> `PortfolioDetailResponse`
|
||||
- `DELETE /portfolios/{portfolio_id}`: admin only, query `keep_ledger: bool = false`, 응답 `204`
|
||||
- `POST /portfolios/{portfolio_id}/tags`: `PortfolioTagMutationRequest` -> `PortfolioTagResponse`
|
||||
- `GET /portfolios/{portfolio_id}/tags`: `PortfolioTagResponse[]`
|
||||
- `DELETE /portfolios/{portfolio_id}/tags`: body `PortfolioTagMutationRequest`, 응답 `204`
|
||||
|
||||
태그 엔드포인트 추가 규칙:
|
||||
|
||||
- `POST`/`DELETE`는 `trade-write`, `GET`은 `read`
|
||||
- 비관리자 요청은 기존 ownership 검사를 동일하게 적용
|
||||
- 입력 `tag`는 앞뒤 공백을 제거한 값을 저장/비교 기준으로 사용
|
||||
- 정규화 후 빈 문자열은 `422`, 중복 태그 추가는 `409`, 포트폴리오 또는 태그 미존재는 `404`
|
||||
|
||||
대표 요청:
|
||||
|
||||
```bash
|
||||
curl -X POST "http://localhost:8000/portfolios/krw-btc/buy" \
|
||||
-H "Authorization: Bearer <TRADE_WRITE_TOKEN>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"quantity": 0.01, "price": 100000000, "fee": 0, "tax": 0, "note": ""}'
|
||||
|
||||
curl -X POST "http://localhost:8000/portfolios/krw-btc/sell" \
|
||||
-H "Authorization: Bearer <TRADE_WRITE_TOKEN>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"quantity": 0.01, "price": 100000000, "fee": 0, "tax": 0, "note": ""}'
|
||||
|
||||
curl -X POST "http://localhost:8000/portfolios/krw-btc/invest" \
|
||||
-H "Authorization: Bearer <TRADE_WRITE_TOKEN>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"movement_type": "deposit", "amount": 50000, "note": ""}'
|
||||
|
||||
curl -X POST "http://localhost:8000/portfolios/krw-btc/transfer" \
|
||||
-H "Authorization: Bearer <TRADE_WRITE_TOKEN>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"direction": "to-buffer", "amount": 10000, "note": ""}'
|
||||
|
||||
curl -X POST "http://localhost:8000/portfolios/krw-btc/reset" \
|
||||
-H "Authorization: Bearer <TRADE_WRITE_TOKEN>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"infer_symbol": "KRW-BTC"}'
|
||||
|
||||
curl -X DELETE "http://localhost:8000/portfolios/krw-btc?keep_ledger=false" \
|
||||
-H "Authorization: Bearer <ADMIN_TOKEN>"
|
||||
|
||||
curl -X POST "http://localhost:8000/portfolios/krw-btc/tags" \
|
||||
-H "Authorization: Bearer <TRADE_WRITE_TOKEN>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"tag": " grade:A "}'
|
||||
|
||||
curl -X GET "http://localhost:8000/portfolios/krw-btc/tags" \
|
||||
-H "Authorization: Bearer <READ_TOKEN>"
|
||||
|
||||
curl -X DELETE "http://localhost:8000/portfolios/krw-btc/tags" \
|
||||
-H "Authorization: Bearer <TRADE_WRITE_TOKEN>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"tag": "grade:A"}'
|
||||
```
|
||||
|
||||
#### 포트폴리오 댓글 (`REQ-001`)
|
||||
- `POST /portfolios/{portfolio_id}/comments`
|
||||
- Auth: `trade-write`
|
||||
- Body: `CommentCreateRequest`
|
||||
- Response `201`: `CommentResponse`
|
||||
- `GET /portfolios/{portfolio_id}/comments`
|
||||
- Auth: `read`
|
||||
- Response `200`: `CommentResponse[]`
|
||||
- `PATCH /portfolios/{portfolio_id}/comments/{comment_id}`
|
||||
- Auth: `trade-write`
|
||||
- Body: `CommentUpdateRequest`
|
||||
- Response `200`: `CommentResponse`
|
||||
- `DELETE /portfolios/{portfolio_id}/comments/{comment_id}`
|
||||
- Auth: `trade-write`
|
||||
- Response `204`
|
||||
|
||||
예시:
|
||||
|
||||
```bash
|
||||
curl -X POST "http://localhost:8000/portfolios/krw-btc/comments" \
|
||||
-H "Authorization: Bearer <TRADE_WRITE_TOKEN>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"content": "Bought more BTC today.\nLooking good."}'
|
||||
|
||||
curl -X GET "http://localhost:8000/portfolios/krw-btc/comments" \
|
||||
-H "Authorization: Bearer <READ_TOKEN>"
|
||||
|
||||
curl -X PATCH "http://localhost:8000/portfolios/krw-btc/comments/a1b2c3d4-e5f6-7890-abcd-ef1234567890" \
|
||||
-H "Authorization: Bearer <TRADE_WRITE_TOKEN>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"content": "Updated note: BTC rally continues."}'
|
||||
|
||||
curl -X DELETE "http://localhost:8000/portfolios/krw-btc/comments/a1b2c3d4-e5f6-7890-abcd-ef1234567890" \
|
||||
-H "Authorization: Bearer <TRADE_WRITE_TOKEN>"
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
|
||||
"portfolio_id": "krw-btc",
|
||||
"content": "Bought more BTC today.\nLooking good.",
|
||||
"created_at": "2026-04-10T00:00:00Z",
|
||||
"updated_at": "2026-04-10T00:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### 전략셋
|
||||
|
||||
#### `GET /strategy-sets`
|
||||
- Auth: `read`
|
||||
- Response `200`: `StrategySetResponse[]`
|
||||
|
||||
#### `POST /strategy-sets`
|
||||
- Auth: `trade-write`
|
||||
- Body: `CreateStrategySetRequest`
|
||||
- Response `201`: `StrategySetResponse`
|
||||
- 기존 `(name, config)` 쌍이 있으면 기존 strategy set 반환
|
||||
|
||||
#### `PATCH /strategy-sets/{strategy_set_id}`
|
||||
- Auth: `trade-write`
|
||||
- Body: `UpdateStrategySetRequest`
|
||||
- Response `200`: `StrategySetResponse`
|
||||
|
||||
#### `DELETE /strategy-sets/{strategy_set_id}`
|
||||
- Auth: `trade-write`
|
||||
- Response `204`
|
||||
- in use 상태면 `409`
|
||||
|
||||
대표 요청:
|
||||
|
||||
```bash
|
||||
curl -X POST "http://localhost:8000/strategy-sets" \
|
||||
-H "Authorization: Bearer <TRADE_WRITE_TOKEN>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"name": "target_value", "config": "{}"}'
|
||||
|
||||
curl -X PATCH "http://localhost:8000/strategy-sets/stv-001" \
|
||||
-H "Authorization: Bearer <TRADE_WRITE_TOKEN>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"config": "{\"rebalance\": true}"}'
|
||||
|
||||
curl -X DELETE "http://localhost:8000/strategy-sets/stv-001" \
|
||||
-H "Authorization: Bearer <TRADE_WRITE_TOKEN>"
|
||||
```
|
||||
|
||||
### 인증
|
||||
|
||||
#### `POST /auth/login`
|
||||
- Auth 없음
|
||||
- Body: `LoginRequest`
|
||||
- Response `200`: `TokenResponse`
|
||||
- 잘못된 username/password면 `401`
|
||||
|
||||
```bash
|
||||
curl -X POST "http://localhost:8000/auth/login" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"username": "admin",
|
||||
"password": "bootstrap123"
|
||||
}'
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"access_token": "plain-token-value",
|
||||
"token_type": "bearer",
|
||||
"expires_at": "2026-04-03T12:00:00Z",
|
||||
"permission": "admin",
|
||||
"user": {
|
||||
"id": "user-1",
|
||||
"username": "admin",
|
||||
"email": "admin@example.com",
|
||||
"permission": "admin",
|
||||
"is_active": true,
|
||||
"created_at": "2026-04-03T00:00:00Z",
|
||||
"updated_at": "2026-04-03T00:00:00Z",
|
||||
"last_login_at": "2026-04-03T00:00:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### `GET /auth/tokens`
|
||||
- Auth: DB-issued `read` 이상 토큰
|
||||
- Response `200`: `AuthTokenResponse[]`
|
||||
|
||||
#### `POST /auth/tokens`
|
||||
- Auth: DB-issued `read` 이상 토큰
|
||||
- Body: `CreateClientTokenRequest`
|
||||
- Response `201`: `IssuedClientTokenResponse`
|
||||
- 요청 permission이 현재 permission을 넘으면 `403`
|
||||
|
||||
#### `DELETE /auth/tokens/{token_id}`
|
||||
- Auth: DB-issued `read` 이상 토큰
|
||||
- Response `204`
|
||||
- 미존재 시 `404`
|
||||
|
||||
대표 요청:
|
||||
|
||||
```bash
|
||||
curl -X POST "http://localhost:8000/auth/tokens" \
|
||||
-H "Authorization: Bearer <SESSION_TOKEN>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"name": "cli", "permission": "trade-write", "expires_in_days": 30}'
|
||||
```
|
||||
|
||||
#### 서비스 토큰 관리 (CLI 전용)
|
||||
|
||||
서비스 토큰은 CLI를 통해서만 발급/폐기한다. HTTP API 경로는 제공하지 않는다.
|
||||
|
||||
- `pfhub service-token issue <name> --user <username> [--permission read|trade-write] [--ttl-days N | --no-expiry]`
|
||||
- `pfhub service-token revoke <name> [--force]`
|
||||
|
||||
제약:
|
||||
|
||||
- 서비스 토큰 이름은 전체 서비스 토큰 내에서 유일하다.
|
||||
- 귀속 사용자(`--user`)가 존재하지 않으면 발급 실패이다.
|
||||
|
||||
대표 실행:
|
||||
|
||||
```bash
|
||||
pfhub service-token issue nightly --user alice --permission trade-write --ttl-days 365
|
||||
pfhub service-token revoke nightly --force
|
||||
```
|
||||
|
||||
### 사용자
|
||||
|
||||
모든 `/users` 엔드포인트는 admin only이다.
|
||||
|
||||
- `GET /users`: `UserResponse[]`
|
||||
- `GET /users/{user_id}`: `UserResponse`, 미존재 시 `404`
|
||||
- `POST /users`: `CreateUserRequest` -> `UserResponse`, duplicate username/email이면 `409`
|
||||
- `PATCH /users/{user_id}`: `UpdateUserRequest` -> `UserResponse`, duplicate면 `409`
|
||||
- `DELETE /users/{user_id}`: `204`
|
||||
|
||||
bootstrap 규칙:
|
||||
|
||||
- DB persistence가 활성이고 `PORTFOLIOHUB_BOOTSTRAP_USERNAME`, `PORTFOLIOHUB_BOOTSTRAP_PASSWORD`가 설정되면, `users` 테이블이 비어 있을 때만 startup에서 초기 admin user를 생성한다.
|
||||
|
||||
대표 요청:
|
||||
|
||||
```bash
|
||||
curl -X GET "http://localhost:8000/users" \
|
||||
-H "Authorization: Bearer <ADMIN_TOKEN>"
|
||||
|
||||
curl -X POST "http://localhost:8000/users" \
|
||||
-H "Authorization: Bearer <ADMIN_TOKEN>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"username": "alice",
|
||||
"email": "alice@example.com",
|
||||
"password": "password123",
|
||||
"permission": "read",
|
||||
"is_active": true
|
||||
}'
|
||||
```
|
||||
|
||||
## 요청/응답 모델 요약
|
||||
|
||||
### Request Models
|
||||
|
||||
`CreatePortfolioRequest`
|
||||
|
||||
- `portfolio_id: str`
|
||||
- `broker: str = "kis"`
|
||||
- `status: str = "enabled"`
|
||||
- `name: str | null`
|
||||
- `symbol: str`
|
||||
- `strategy_name: str = "target_value"`
|
||||
- `strategy_params: object = {}`
|
||||
- `active_strategy_id: str | null`
|
||||
- `avabile_strategies: string[] | null`
|
||||
- `cash_balance: float >= 0`
|
||||
- `asset_quantity: float >= 0`
|
||||
- `asset_price: float >= 0`
|
||||
- `profit_buffer: float >= 0`
|
||||
|
||||
`UpdatePortfolioRequest`
|
||||
|
||||
- `broker: str | null`
|
||||
- `status: str | null`
|
||||
- `name: str | null`
|
||||
- `symbol: str | null`
|
||||
- `strategy_name: str | null`
|
||||
- `strategy_params: object | null`
|
||||
- `active_strategy_id: str | null`
|
||||
- `avabile_strategies: string[] | null`
|
||||
- `asset_price: float >= 0 | null`
|
||||
|
||||
`BuyRequest` / `SellRequest`
|
||||
|
||||
- `quantity: float > 0`
|
||||
|
||||
`InvestRequest`
|
||||
|
||||
- `movement_type: "deposit" | "profit" | "withdraw"`
|
||||
- `amount: float > 0`
|
||||
- `note: str = ""`
|
||||
|
||||
`TransferRequest`
|
||||
|
||||
- `direction: "to-buffer" | "from-buffer"`
|
||||
- `amount: float > 0`
|
||||
- `note: str = ""`
|
||||
|
||||
`ResetPortfolioRequest`
|
||||
|
||||
- `infer_symbol: str | null`
|
||||
|
||||
`CommentCreateRequest` / `CommentUpdateRequest`
|
||||
|
||||
- `content: str`
|
||||
|
||||
`LoginRequest`
|
||||
|
||||
- `username: str`
|
||||
- `password: str`
|
||||
|
||||
`CreateClientTokenRequest`
|
||||
|
||||
- `name: str`
|
||||
- `permission: "read" | "trade-write" | "admin" | null`
|
||||
- `expires_in_days: int | null`
|
||||
|
||||
`CreateBotPrincipalRequest`
|
||||
|
||||
- `name: str`
|
||||
- `owner_user_id: str`
|
||||
- `permission: "read" | "trade-write"`
|
||||
- `description: str | null`
|
||||
|
||||
`CreateSelfBotPrincipalRequest`
|
||||
|
||||
- `name: str`
|
||||
- `permission: "read" | "trade-write"`
|
||||
- `description: str | null`
|
||||
|
||||
`CreateBotTokenRequest`
|
||||
|
||||
- `name: str`
|
||||
- `permission: "read" | "trade-write" | null`
|
||||
- `expires_in_days: int | null`
|
||||
|
||||
`CreateUserRequest`
|
||||
|
||||
- `username: str`
|
||||
- `email: str | null`
|
||||
- `password: str`
|
||||
- `permission: "read" | "trade-write" | "admin"`
|
||||
- `is_active: bool`
|
||||
|
||||
`UpdateUserRequest`
|
||||
|
||||
- `username: str | null`
|
||||
- `email: str | null`
|
||||
- `password: str | null`
|
||||
- `permission: "read" | "trade-write" | "admin" | null`
|
||||
- `is_active: bool | null`
|
||||
|
||||
`CreateStrategySetRequest`
|
||||
|
||||
- `name: str`
|
||||
- `config: str`
|
||||
|
||||
`UpdateStrategySetRequest`
|
||||
|
||||
- `name: str | null`
|
||||
- `config: str | null`
|
||||
|
||||
### Response Models
|
||||
|
||||
`PortfolioListItem`
|
||||
|
||||
- portfolio summary fields + computed `total_value`
|
||||
|
||||
`PortfolioSummary`
|
||||
|
||||
- `portfolio_id: str`
|
||||
- `broker: str`
|
||||
- `status: str`
|
||||
- `name: str`
|
||||
- `symbol: str`
|
||||
- `cash_balance: float`
|
||||
- `asset_quantity: float`
|
||||
- `price: float`
|
||||
- `profit_buffer: float`
|
||||
- `active_strategy_id: str | null`
|
||||
- `avabile_strategies: string[] | null`
|
||||
- `created_at: datetime | null`
|
||||
- `updated_at: datetime | null`
|
||||
- `last_trade_at: datetime | null`
|
||||
|
||||
`PortfolioDetailResponse`
|
||||
|
||||
- `portfolio: PortfolioSummary`
|
||||
- `recent_entries: LedgerEntryResponse[]`
|
||||
- `strategy_set: StrategySetResponse | null`
|
||||
- `live_quantity: float | null`
|
||||
|
||||
`StrategySetResponse`
|
||||
|
||||
- `id: str`
|
||||
- `name: str`
|
||||
- `config: str`
|
||||
- `nick: str`
|
||||
- `created_at: datetime | null`
|
||||
|
||||
`CommentResponse`
|
||||
|
||||
- `id: str`
|
||||
- `portfolio_id: str`
|
||||
- `content: str`
|
||||
- `created_at: datetime | null`
|
||||
- `updated_at: datetime | null`
|
||||
|
||||
`BotPrincipalResponse`
|
||||
|
||||
- `id: str`
|
||||
- `name: str`
|
||||
- `owner_user_id: str`
|
||||
- `permission: "read" | "trade-write"`
|
||||
- `description: str | null`
|
||||
- `is_active: bool`
|
||||
- `created_at: datetime | null`
|
||||
- `updated_at: datetime | null`
|
||||
- `deactivated_at: datetime | null`
|
||||
|
||||
`ErrorResponse`
|
||||
|
||||
- `detail: str`
|
||||
|
||||
## 공통 오류 예시
|
||||
|
||||
### Missing token
|
||||
|
||||
```json
|
||||
{"detail": "Not authenticated"}
|
||||
```
|
||||
|
||||
### Invalid token
|
||||
|
||||
```json
|
||||
{"detail": "Invalid token"}
|
||||
```
|
||||
|
||||
### Insufficient permission
|
||||
|
||||
```json
|
||||
{"detail": "Insufficient permission"}
|
||||
```
|
||||
|
||||
## 문서 동기화 규칙
|
||||
- `app/api/` 또는 `app/schemas/` 변경 시 본 문서를 같은 변경 단위에서 갱신한다.
|
||||
- `REQ-002` 태그 검색 구현은 승인된 정규화/필터 의미와 실제 route/CLI 계약을 모두 본 문서에 유지한다.
|
||||
|
||||
## Open Implementation Points
|
||||
- 에러 응답 body를 공통 포맷(`error_code`, `message`, `details`)으로 강제할지 결정한다.
|
||||
- idempotency가 필요한 쓰기 API(예: 재시도 가능 매매 요청)의 키 정책을 확정한다.
|
||||
@@ -1,113 +0,0 @@
|
||||
# Architecture Overview
|
||||
|
||||
- Related REQ: `REQ-003`
|
||||
|
||||
## 목표
|
||||
`portfolio-hub`는 포트폴리오 상태 관리 기능을 API와 CLI에 동일하게 제공하고, 서비스/도메인/리포지토리 계층 분리를 통해 변경 비용을 낮춘다.
|
||||
|
||||
## 개요와 범위
|
||||
|
||||
런타임/구성요소 기준:
|
||||
|
||||
- Runtime: Python 3.11+
|
||||
- API framework: FastAPI
|
||||
- CLI framework: Typer
|
||||
- DB driver: Psycopg v3
|
||||
- Migration: Alembic
|
||||
|
||||
주요 목표:
|
||||
|
||||
- 포트폴리오 lifecycle 관리(create/update/delete/reset)
|
||||
- 거래 및 잔고 영향 액션 기록(buy/sell/invest/transfer)
|
||||
- API와 CLI에서 동일한 상태/ledger 조회 제공
|
||||
- 공유 service/repository 계층을 통한 동작 일관성 유지
|
||||
|
||||
현재 범위:
|
||||
|
||||
- 인증된 포트폴리오 조회/변경 작업
|
||||
- 관리자 기반 사용자 CRUD
|
||||
- 권한 계층(read, trade-write, admin)
|
||||
- feature flag 기반 PostgreSQL/Timescale 저장소
|
||||
- DB bootstrap 및 migration 운영 명령
|
||||
|
||||
현재 비목표 / 제한:
|
||||
|
||||
- production 용도 메모리 fallback repository 미구현
|
||||
- 외부 broker 주문 실행 미포함
|
||||
- Redis/Loki 관측성 경로는 완전한 runtime wiring 미완료
|
||||
|
||||
## 상위 구성 요소
|
||||
|
||||
| 컴포넌트 | 책임 |
|
||||
| --- | --- |
|
||||
| `app/main.py` | FastAPI 앱 생성, 라우터 등록, 설정 초기화 |
|
||||
| `app/api/` | 인증/헬스/포트폴리오/전략셋/사용자 라우트 |
|
||||
| `app/cli/` | 운영용 명령 인터페이스와 확인 프롬프트 |
|
||||
| `app/services/` | API/CLI 공통 유스케이스 수행 |
|
||||
| `app/domain/` | 포트폴리오 거래 규칙, 밸런스 계산 |
|
||||
| `app/repositories/` | 저장소 추상화 및 Postgres 구현 |
|
||||
| `app/db/`, `migrations/` | DB 초기화 및 스키마 버전 관리 |
|
||||
|
||||
## 엔트리포인트
|
||||
|
||||
HTTP 서비스:
|
||||
|
||||
- App factory: `app.main:create_app`
|
||||
- ASGI app: `app.main:app`
|
||||
- OpenAPI docs: `/docs`
|
||||
- ReDoc: `/redoc`
|
||||
|
||||
CLI:
|
||||
|
||||
- Console script: `portfoliohub`
|
||||
- Entry function: `app.cli.main:main`
|
||||
|
||||
## 기본 런타임 흐름
|
||||
|
||||
### 1. API 기반 거래 처리
|
||||
1. 라우트에서 요청 바디와 권한을 검증한다.
|
||||
2. 서비스 계층에서 도메인 규칙(수량/금액/상태)을 평가한다.
|
||||
3. 리포지토리에서 포트폴리오/원장 데이터를 갱신한다.
|
||||
4. 응답 스키마로 직렬화해 반환한다.
|
||||
|
||||
### 2. CLI 기반 동일 작업 처리
|
||||
1. CLI 명령이 인자를 파싱하고 확인 프롬프트를 처리한다.
|
||||
2. API와 동일한 서비스 메서드를 호출한다.
|
||||
3. 결과를 터미널 친화 포맷으로 출력한다.
|
||||
|
||||
### 3. DB 활성화 경로
|
||||
1. `PORTFOLIOHUB_DB_ENABLED=true` 확인
|
||||
2. DB 연결 정보(`TIMESCALEDB_*`) 로딩
|
||||
3. Postgres 리포지토리 선택
|
||||
4. 필요 시 Alembic 마이그레이션 후 서비스 기동
|
||||
|
||||
## 설계 원칙
|
||||
- **Service Reuse First**: API/CLI는 동일 service 계층을 재사용한다.
|
||||
- **Contract Stability**: 외부 계약은 `app/schemas/`와 기술 문서에서 일관성을 유지한다.
|
||||
- **Persistence Isolation**: 영속성 구현 세부사항은 repository 계층에 격리한다.
|
||||
- **Fail Fast Config**: 필수 설정 누락은 조기 실패로 드러낸다.
|
||||
|
||||
## 테스트와 호환성 기준
|
||||
|
||||
테스트 기준:
|
||||
|
||||
- 테스트 프레임워크는 `pytest`를 사용한다.
|
||||
- 주요 테스트 범위는 API, auth, CLI, domain service를 포함한다.
|
||||
- 재귀 제외 경로는 `temp`, `.git`, `.venv`를 사용한다.
|
||||
|
||||
버전/호환성 기준:
|
||||
|
||||
- 현재 패키지 버전: `0.1.0`
|
||||
- API 버전 노출은 앱 버전 설정(`app_version`)을 따른다.
|
||||
- 최소 Python 버전: `3.11`
|
||||
|
||||
## 향후 확장 포인트
|
||||
|
||||
- Redis 기반 cache/session 통합
|
||||
- Loki logging pipeline 통합
|
||||
- 추가 broker 및 order-execution adapter
|
||||
- 운영/admin 확장 엔드포인트
|
||||
|
||||
## Open Implementation Points
|
||||
- `REQ-002` 태그 검색 구현 시 조회 성능 요구를 만족하는 인덱스/쿼리 전략을 확정한다.
|
||||
- 관측성(메트릭/로그)의 표준 이벤트 스키마를 별도 문서로 확장할지 결정한다.
|
||||
@@ -1,93 +0,0 @@
|
||||
# Auth and Token Lifecycle
|
||||
|
||||
- Related REQ: `REQ-004`
|
||||
|
||||
## 목표
|
||||
정적 토큰과 DB 발급 토큰을 병행하는 현재 인증 모델의 동작 기준을 명확히 한다.
|
||||
|
||||
## 권한 모델
|
||||
|
||||
| Permission | 허용 범위 |
|
||||
| --- | --- |
|
||||
| `read` | 조회 계열 API |
|
||||
| `trade-write` | 조회 + 포트폴리오 변경 API |
|
||||
| `admin` | 전체 API + 사용자/서비스토큰 관리 |
|
||||
|
||||
소유권 결합 정책:
|
||||
|
||||
- `admin`이 아닌 주체는 포트폴리오 접근 시 리소스 소유주(`owner_user_id`) 일치 조건을 추가로 만족해야 한다.
|
||||
- `admin`은 소유권 제약 없이 전체 포트폴리오를 접근할 수 있다.
|
||||
|
||||
## 인증 방식
|
||||
|
||||
- 보호된 API는 `Authorization: Bearer <token>` 헤더를 사용한다.
|
||||
- 인증 소스는 정적 토큰과 DB 저장 토큰을 병행한다.
|
||||
|
||||
권한 계층:
|
||||
|
||||
- `admin`은 전체 엔드포인트 접근 가능
|
||||
- `trade-write`는 `trade-write`와 `read` 범위 접근 가능
|
||||
- `read`는 조회 엔드포인트만 접근 가능
|
||||
|
||||
## 인증 소스
|
||||
- 환경변수 기반 정적 토큰: `PORTFOLIOHUB_*_TOKENS`
|
||||
- DB 저장 토큰: 로그인 세션 토큰 / 클라이언트 토큰 / 서비스 토큰
|
||||
|
||||
서비스 토큰 사용자 컨텍스트 정책:
|
||||
|
||||
- 서비스 토큰은 특정 사용자(`user_id`)에 귀속되며 `token_type = 'service'`로 식별된다.
|
||||
- 토큰 이름(`name`)은 서비스 토큰 내에서 유일하며 CLI 관리의 기본 키로 사용된다.
|
||||
- 소유권이 필요한 포트폴리오 API에서 서비스 토큰은 귀속 사용자 경계 내에서만 동작한다.
|
||||
|
||||
## 서비스 토큰 CLI 관리
|
||||
|
||||
- `pfhub service-token issue <name> --user <username>` — 서비스 토큰 발급 (기본 TTL 365일)
|
||||
- `pfhub service-token revoke <name>` — 서비스 토큰 폐기
|
||||
|
||||
제약:
|
||||
|
||||
- `--ttl-days`와 `--no-expiry` 동시 사용은 거부한다.
|
||||
- 동일 이름의 서비스 토큰은 중복 발급할 수 없다.
|
||||
- `--user`로 지정한 사용자가 존재하지 않으면 발급 실패 처리한다.
|
||||
|
||||
## 토큰 라이프사이클
|
||||
1. 사용자 로그인(`POST /auth/login`) 또는 토큰 발급 API 호출
|
||||
2. 토큰 해시 저장 및 prefix 관리
|
||||
3. 요청 처리 시 해시 비교와 만료/폐기 상태 검증
|
||||
4. `DELETE /auth/tokens/{token_id}` 또는 서비스 토큰 폐기(`pfhub service-token revoke`)로 토큰 폐기
|
||||
|
||||
서비스 토큰 라이프사이클:
|
||||
|
||||
1. `pfhub service-token issue <name> --user <username>` 실행 시 `--ttl-days`/`--no-expiry` 정책을 먼저 검증한다.
|
||||
2. 정책 검증 후 서비스 계층에서 사용자 존재 및 이름 중복 여부를 검증한다.
|
||||
3. 발급 성공 시 원문 토큰은 1회만 출력된다.
|
||||
4. `pfhub service-token revoke <name>` 실행 시 확인 프롬프트(`--force`로 생략 가능)를 거쳐 폐기한다.
|
||||
|
||||
만기 정책:
|
||||
|
||||
- 기본 TTL은 365일이다.
|
||||
- `--ttl-days`와 `--no-expiry` 동시 사용은 거부한다.
|
||||
|
||||
감사 로그 정책:
|
||||
|
||||
- `issue/revoke`는 `auth.bot_token` 이벤트를 감사 로그로 남긴다.
|
||||
- 감사 로그에는 마스킹된 `token_id/token_ref`만 기록한다.
|
||||
- issue 경로에서 감사 로그 기록이 실패하면 발급 토큰을 즉시 폐기하고 실패 처리한다.
|
||||
- revoke 경로에서 감사 로그 기록이 실패해도 폐기는 유지하고 명령은 실패로 반환한다.
|
||||
|
||||
## 실패 응답 기준
|
||||
|
||||
- bearer 헤더 누락 또는 형식 오류: `401`
|
||||
- 알 수 없는 토큰: `401`
|
||||
- 권한 부족: `403`
|
||||
|
||||
## 부트스트랩 사용자 규칙
|
||||
- `PORTFOLIOHUB_BOOTSTRAP_USERNAME`과 `PORTFOLIOHUB_BOOTSTRAP_PASSWORD`가 설정되고 users 테이블이 비어 있으면 초기 admin을 1회 생성한다.
|
||||
- 이미 사용자가 존재하면 추가 생성하지 않는다.
|
||||
|
||||
## Validation Notes
|
||||
- 만료된 토큰은 항상 인증 실패로 처리해야 한다.
|
||||
- 권한 계층 승격 우회가 없는지 API/CLI 양쪽에서 검증한다.
|
||||
|
||||
## Open Implementation Points
|
||||
- 정적 토큰과 DB 토큰의 우선순위/병합 정책을 명문화할지 결정한다.
|
||||
@@ -1,193 +0,0 @@
|
||||
# Deployment and Operations
|
||||
|
||||
- Related REQ: N/A
|
||||
|
||||
## 목표
|
||||
PortfolioHub를 Linux 환경에서 안정적으로 배포하고 HTTPS 종단, 프로세스 복구, 기본 점검 경로를 표준화한다.
|
||||
|
||||
## 사전 조건
|
||||
|
||||
- public IP를 가진 Linux/Ubuntu 서버
|
||||
- 해당 IP를 가리키는 도메인 DNS `A` record
|
||||
- 방화벽에서 `80`, `443` 포트 개방
|
||||
|
||||
## 권장 배포 토폴로지
|
||||
- App: `uvicorn app.main:app`
|
||||
- Reverse proxy/TLS: Caddy
|
||||
- Process manager: systemd
|
||||
- 선택: Docker Compose + Caddy sidecar
|
||||
|
||||
## Linux Direct Run 기준
|
||||
1. Python 가상환경 생성 후 패키지를 설치한다.
|
||||
2. 환경 변수 또는 `.env`를 구성한다.
|
||||
3. `uvicorn app.main:app --host 127.0.0.1 --port 8000 --workers 2`를 실행한다.
|
||||
4. `GET /health`를 확인한다.
|
||||
|
||||
## Caddy 설치와 구성
|
||||
|
||||
Ubuntu/Debian 설치 예시:
|
||||
|
||||
```bash
|
||||
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
|
||||
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' \
|
||||
| sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
|
||||
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' \
|
||||
| sudo tee /etc/apt/sources.list.d/caddy-stable.list
|
||||
sudo apt update
|
||||
sudo apt install caddy
|
||||
```
|
||||
|
||||
설치 검증:
|
||||
|
||||
```bash
|
||||
caddy version
|
||||
```
|
||||
|
||||
기본 Caddyfile:
|
||||
|
||||
```text
|
||||
your-domain.com {
|
||||
reverse_proxy localhost:8000
|
||||
}
|
||||
```
|
||||
|
||||
동작 기준:
|
||||
|
||||
- TLS 인증서 자동 발급/갱신
|
||||
- `80`에서 `443`으로 HTTP redirect
|
||||
- 외부 요청을 내부 `uvicorn` 프로세스(`127.0.0.1:8000`)로 전달
|
||||
|
||||
적용 후 reload:
|
||||
|
||||
```bash
|
||||
sudo systemctl reload caddy
|
||||
```
|
||||
|
||||
## systemd 운영 기준
|
||||
- 서비스 계정 권한 최소화
|
||||
- `Restart=on-failure`
|
||||
- 로그 수집 경로(journalctl 또는 외부 수집기) 확보
|
||||
|
||||
예시 unit 파일:
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=PortfolioHub API
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
User=www-data
|
||||
WorkingDirectory=/opt/portfoliohub
|
||||
EnvironmentFile=/opt/portfoliohub/.env
|
||||
ExecStart=/opt/portfoliohub/.venv/bin/uvicorn app.main:app \
|
||||
--host 127.0.0.1 --port 8000 --workers 2
|
||||
Restart=on-failure
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
등록/기동:
|
||||
|
||||
```bash
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable portfoliohub
|
||||
sudo systemctl start portfoliohub
|
||||
```
|
||||
|
||||
## HTTPS/Caddy 운영 기준
|
||||
- 도메인 DNS가 서버 IP를 가리켜야 한다.
|
||||
- 포트 `80/443`을 개방한다.
|
||||
- Caddyfile에서 앱을 `reverse_proxy`로 연결한다.
|
||||
- 브라우저 기반 frontend가 있으면 `PORTFOLIOHUB_ALLOWED_ORIGINS`에 정확한 HTTPS origin을 지정한다.
|
||||
|
||||
예시:
|
||||
|
||||
```bash
|
||||
PORTFOLIOHUB_ALLOWED_ORIGINS=https://app.example.com,https://www.example.com
|
||||
```
|
||||
|
||||
## Docker Compose 운영 기준
|
||||
- 앱 컨테이너는 내부 포트만 expose하고 외부 종단은 Caddy가 담당한다.
|
||||
- `caddy_data`, `caddy_config` 볼륨을 유지해 인증서 상태를 보존한다.
|
||||
- 배포 후 `docker compose ps`, `curl /health`로 기본 상태를 확인한다.
|
||||
|
||||
예시 compose:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
app:
|
||||
build: .
|
||||
expose:
|
||||
- "8000"
|
||||
environment:
|
||||
- PORTFOLIOHUB_ALLOWED_ORIGINS=https://your-domain.com
|
||||
restart: unless-stopped
|
||||
|
||||
caddy:
|
||||
image: caddy:2-alpine
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
volumes:
|
||||
- ./Caddyfile:/etc/caddy/Caddyfile:ro
|
||||
- caddy_data:/data
|
||||
- caddy_config:/config
|
||||
depends_on:
|
||||
- app
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
caddy_data:
|
||||
caddy_config:
|
||||
```
|
||||
|
||||
compose 네트워크 안에서는 `reverse_proxy app:8000`을 사용한다.
|
||||
|
||||
기동:
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
## 방화벽 설정
|
||||
|
||||
UFW:
|
||||
|
||||
```bash
|
||||
sudo ufw allow 80/tcp
|
||||
sudo ufw allow 443/tcp
|
||||
sudo ufw reload
|
||||
```
|
||||
|
||||
firewalld:
|
||||
|
||||
```bash
|
||||
sudo firewall-cmd --permanent --add-service=http
|
||||
sudo firewall-cmd --permanent --add-service=https
|
||||
sudo firewall-cmd --reload
|
||||
```
|
||||
|
||||
## 배포 검증
|
||||
|
||||
HTTPS health check:
|
||||
|
||||
```bash
|
||||
curl https://your-domain.com/health
|
||||
```
|
||||
|
||||
운영 상태 확인:
|
||||
|
||||
```bash
|
||||
sudo systemctl status caddy
|
||||
sudo journalctl -u caddy -f
|
||||
```
|
||||
|
||||
## Validation Notes
|
||||
- HTTPS 엔드포인트에서 `/health`, `/docs` 접근이 가능해야 한다.
|
||||
- DB 사용 배포에서는 마이그레이션 적용 후 앱을 기동한다.
|
||||
- CORS 오리진 설정과 실제 프론트엔드 도메인이 일치해야 한다.
|
||||
|
||||
## Open Implementation Points
|
||||
- blue/green 또는 rolling update 같은 무중단 배포 전략 도입 여부를 확정한다.
|
||||
- Loki/metrics 통합 운영 절차를 별도 런북으로 분리할지 결정한다.
|
||||
@@ -1,52 +0,0 @@
|
||||
# portfolio-hub Technical Specification Index
|
||||
|
||||
## 목적
|
||||
- 이 디렉토리는 `portfolio-hub`의 **구현 상세 설계(How)** 를 관리한다.
|
||||
- 승인 기준과 외부 요구사항의 source of truth는 `docs/SRS/`이며, 기술 문서는 해당 요구사항을 구현하기 위한 내부 설계를 정리한다.
|
||||
|
||||
## 문서 사용 원칙
|
||||
1. 외부 계약, acceptance criteria, 상태 전이는 `docs/SRS/`에서 관리한다.
|
||||
2. 내부 모듈 책임, 시퀀스, 데이터 흐름, 예시 payload는 `docs/technical/`에서 관리한다.
|
||||
3. 기술 문서가 SRS와 충돌하면, 별도 owner 결정 전까지 **SRS를 우선**한다.
|
||||
4. 기술 문서를 수정할 때는 관련 REQ ID를 함께 표시해 traceability를 유지한다.
|
||||
|
||||
## 문서 목록
|
||||
|
||||
| 문서 | 목적 | 연관 REQ |
|
||||
| --- | --- | --- |
|
||||
| `project-structure-baseline.md` | 저장소 baseline 디렉토리 구조와 코드/문서 배치 기준 | N/A |
|
||||
| `python-runtime-and-packaging.md` | Python 런타임과 `pyproject.toml` 기반 패키징 기준 | N/A |
|
||||
| `settings-and-env-loading.md` | `.env`/환경변수 로딩, 필수 키, CORS/운영 설정 규칙 | N/A |
|
||||
| `architecture-overview.md` | 시스템 개요, 모듈 책임, 런타임 흐름, 테스트/호환성 기준 | `REQ-003` |
|
||||
| `api-contracts.md` | HTTP API 상세 계약, request/response 모델, 대표 payload 예시 | `REQ-001`, `REQ-002`, `REQ-003` |
|
||||
| `portfolio-tagging-and-search-design.md` | 태그 정규화, API/CLI 검색 파라미터, 모듈 책임, 구현 단계 설계 | `REQ-002` |
|
||||
| `persistence-and-migrations.md` | PostgreSQL/Timescale 연동, 리포지토리 스위치, Alembic 운영, 스키마 baseline | `REQ-001`, `REQ-003` |
|
||||
| `auth-and-token-lifecycle.md` | 로그인/토큰/봇 주체 라이프사이클 및 권한 모델 | N/A |
|
||||
| `portfolio-ownership-and-auth-design.md` | 포트폴리오 소유주(`owner_user_id`) 도입, 소유권 접근 제어, 검증 시나리오 설계 | `REQ-004` |
|
||||
| `management-cli-operations.md` | `portfoliohub` CLI 명령 책임, 종료코드, 운영 플로우 | N/A |
|
||||
| `deployment-and-operations.md` | Linux systemd, Caddy reverse proxy, Docker Compose 운영 절차 | N/A |
|
||||
|
||||
## 연관 REQ 매핑 표
|
||||
|
||||
| REQ ID | Technical Docs |
|
||||
| --- | --- |
|
||||
| `REQ-001` | `api-contracts.md`, `persistence-and-migrations.md` |
|
||||
| `REQ-002` | `api-contracts.md`, `portfolio-tagging-and-search-design.md`, `persistence-and-migrations.md` |
|
||||
| `REQ-003` | `architecture-overview.md`, `api-contracts.md`, `persistence-and-migrations.md` |
|
||||
| `REQ-004` | `auth-and-token-lifecycle.md`, `api-contracts.md`, `persistence-and-migrations.md`, `portfolio-ownership-and-auth-design.md` |
|
||||
|
||||
## 권장 작성 순서
|
||||
1. `project-structure-baseline.md`
|
||||
2. `python-runtime-and-packaging.md`
|
||||
3. `settings-and-env-loading.md`
|
||||
4. `architecture-overview.md`
|
||||
5. `api-contracts.md`
|
||||
6. `persistence-and-migrations.md`
|
||||
7. `auth-and-token-lifecycle.md`
|
||||
8. `management-cli-operations.md`
|
||||
9. `deployment-and-operations.md`
|
||||
|
||||
## 추적 규칙
|
||||
- 각 기술 문서는 상단에 관련 REQ를 명시한다.
|
||||
- 구현 착수 전에는 해당 문서의 open issue가 남아 있는지 확인한다.
|
||||
- 운영 중 발견된 차이는 technical 문서에 먼저 반영하고, 외부 계약 변화가 있으면 SRS 업데이트 여부를 별도로 판정한다.
|
||||
@@ -1,90 +0,0 @@
|
||||
# Management CLI Operations
|
||||
|
||||
- Related REQ: N/A
|
||||
|
||||
## 목표
|
||||
`portfoliohub` CLI를 운영 자동화와 수동 점검 모두에 안전하게 사용할 수 있도록 명령 책임과 사용 규칙을 정의한다.
|
||||
|
||||
## 명령 그룹
|
||||
|
||||
| 그룹 | 주요 명령 | 목적 |
|
||||
| --- | --- | --- |
|
||||
| DB 운영 | `init-db`, `migrate-db`, `health-check` | 스키마 적용 및 연결 상태 점검 |
|
||||
| 사용자 관리 | `user list/get/create/update/delete` | 운영 계정 라이프사이클 |
|
||||
| 서비스 토큰 관리 | `service-token issue/revoke` | 서비스 통합용 장기 토큰 관리 |
|
||||
| 포트폴리오 조회 | `portfolio list`, `portfolio status` | 상태 확인 |
|
||||
| 포트폴리오 변경 | `portfolio create/update/buy/sell/invest/transfer/reset/delete` | 거래/자금/상태 변경 |
|
||||
|
||||
## 인터페이스 명세
|
||||
|
||||
전역 옵션:
|
||||
|
||||
- `--format [table|json]` (기본값: `table`)
|
||||
|
||||
공통 명령:
|
||||
|
||||
- `portfoliohub init-db [--maintenance-db postgres]`
|
||||
- `portfoliohub migrate-db`
|
||||
- `portfoliohub health-check`
|
||||
|
||||
포트폴리오 하위 명령:
|
||||
|
||||
- `portfoliohub portfolio list [--broker ...] [--status ...]`
|
||||
- `portfoliohub portfolio status <portfolio_id> [--recent 5]`
|
||||
- `portfoliohub portfolio create <portfolio_id> --symbol ... [--name ...] [options] [--force]`
|
||||
- `portfoliohub portfolio update <portfolio_id> [options] [--force]`
|
||||
- `portfoliohub portfolio buy <portfolio_id> --quantity ... --price ... [options] [--force]`
|
||||
- `portfoliohub portfolio sell <portfolio_id> --quantity ... --price ... [options] [--force]`
|
||||
- `portfoliohub portfolio invest <portfolio_id> --type <deposit|profit|withdraw> --amount ... [options] [--force]`
|
||||
- `portfoliohub portfolio transfer <portfolio_id> --direction <to-buffer|from-buffer> --amount ... [options] [--force]`
|
||||
- `portfoliohub portfolio reset <portfolio_id> [--infer-symbol ...] [--force]`
|
||||
- `portfoliohub portfolio delete <portfolio_id> [--keep-ledger] [--force]`
|
||||
|
||||
포트폴리오 댓글 하위 명령:
|
||||
|
||||
- `portfoliohub portfolio comment add <portfolio_id> --content <text>`
|
||||
- `portfoliohub portfolio comment list <portfolio_id>`
|
||||
- `portfoliohub portfolio comment update <portfolio_id> <comment_id> --content <text> [--force]`
|
||||
- `portfoliohub portfolio comment delete <portfolio_id> <comment_id> [--force]`
|
||||
|
||||
전략셋 하위 명령:
|
||||
|
||||
- `portfoliohub strategy list`
|
||||
- `portfoliohub strategy create --name ... --config ...`
|
||||
- `portfoliohub strategy update <strategy_set_id> [--name ...] [--config ...]`
|
||||
- `portfoliohub strategy delete <strategy_set_id> [--force]`
|
||||
|
||||
서비스 토큰 하위 명령:
|
||||
|
||||
- `portfoliohub service-token issue <name> --user <username> [--permission read|trade-write] [--ttl-days N] [--no-expiry]`
|
||||
- `portfoliohub service-token revoke <name> [--force]`
|
||||
|
||||
## 실행 원칙
|
||||
- 쓰기 명령은 기본적으로 확인 프롬프트를 띄운다.
|
||||
- 자동화 스크립트에서는 `--force`를 사용해 비대화식 실행을 보장한다.
|
||||
- DB 타깃 실행 시 `PORTFOLIOHUB_DB_ENABLED=true`와 `TIMESCALEDB_*`를 명시한다.
|
||||
|
||||
## 예시 시나리오
|
||||
1. 신규 환경 준비
|
||||
- `portfoliohub init-db`
|
||||
- `portfoliohub health-check`
|
||||
2. 일상 운영
|
||||
- `portfoliohub portfolio list`
|
||||
- `portfoliohub portfolio status <portfolio_id>`
|
||||
3. 배포 후 검증
|
||||
- `portfoliohub user list`
|
||||
- 주요 API smoke test와 결과 비교
|
||||
|
||||
## 오류/종료코드 가이드
|
||||
- `0`: 정상 종료
|
||||
- `1`: 입력/검증/런타임 오류
|
||||
- 재시도 가능 오류(예: 일시적 DB 연결 실패)는 메시지에 원인을 명확히 표기한다.
|
||||
|
||||
확인 동작:
|
||||
|
||||
- 변경 명령은 기본적으로 확인 프롬프트를 요청한다.
|
||||
- `--force`를 지정하면 확인 프롬프트를 생략한다.
|
||||
|
||||
## Open Implementation Points
|
||||
- JSON 출력 모드(`--json`)를 도입해 자동화 친화성을 높일지 결정한다.
|
||||
- 대량 작업(batch) 명령 추가 시 실패 항목 보고 포맷을 표준화한다.
|
||||
@@ -1,326 +0,0 @@
|
||||
# Persistence and Migrations
|
||||
|
||||
- Related REQ: `REQ-001`, `REQ-002`, `REQ-003`, `REQ-004`
|
||||
|
||||
## 목표
|
||||
DB 기반 영속성 경로를 안정적으로 운영하고, 스키마 변경 이력과 문서 동기화를 일관되게 유지한다.
|
||||
|
||||
## 저장소 선택 규칙
|
||||
- `PORTFOLIOHUB_DB_ENABLED=true`이면 Postgres 리포지토리를 사용한다.
|
||||
- DB 비활성 모드는 개발/부트스트랩 목적의 제한적 동작으로 간주한다.
|
||||
|
||||
비활성 동작 기준:
|
||||
|
||||
- `PORTFOLIOHUB_DB_ENABLED=false`이면 미구현 placeholder repository가 `FeatureNotReadyError`를 발생시킨다.
|
||||
|
||||
## 데이터 모델 핵심 영역
|
||||
|
||||
| 테이블 | 역할 |
|
||||
| --- | --- |
|
||||
| `portfolios` | 현재 포트폴리오 상태 |
|
||||
| `portfolio_ledger` | 거래/자금이동 이력 스냅샷 |
|
||||
| `portfolio_comments` | 포트폴리오 코멘트 이력 (`REQ-001`) |
|
||||
| `strategy_sets` | 전략 설정 스냅샷 |
|
||||
| `users` | 운영 사용자 |
|
||||
| `auth_tokens` | 세션/클라이언트/봇 토큰 |
|
||||
| `bot_principals` | 외부 봇 주체 |
|
||||
|
||||
## 스키마 문서 기준
|
||||
|
||||
- Source of truth: Alembic revisions in `migrations/versions`
|
||||
- Current baseline revision: `20260413_0010`
|
||||
- 상세 테이블/컬럼/인덱스 제약은 이 문서가 Alembic revision과 동기화된 기준 문서다.
|
||||
|
||||
## 마이그레이션 운영 흐름
|
||||
1. 신규 스키마 변경을 Alembic revision으로 추가한다.
|
||||
2. `migrations/versions/`에 revision 파일을 저장한다.
|
||||
3. 본 문서에 revision ID와 스키마 변경 내역을 반영한다.
|
||||
4. `portfoliohub migrate-db` 또는 Alembic `upgrade head`로 적용한다.
|
||||
|
||||
## 명령 기준
|
||||
- 초기 DB 준비: `portfoliohub init-db`
|
||||
- 버전 업그레이드: `portfoliohub migrate-db`
|
||||
- 상태 점검: `portfoliohub health-check`
|
||||
|
||||
명령 동작:
|
||||
|
||||
- `init-db`: target DB가 없으면 생성 후 Alembic `head` 적용
|
||||
- `migrate-db`: 기존 target DB에 Alembic `head` 적용
|
||||
|
||||
## 스키마 기준
|
||||
|
||||
### Conventions
|
||||
|
||||
- 데이터 타입 표기는 SQLAlchemy/Alembic 선언명을 따른다.
|
||||
- `NULL`은 nullable을 의미한다.
|
||||
- Default는 DB-level `server_default`를 의미한다.
|
||||
- FK 제약이 없어도 logical relationship column은 문서화한다.
|
||||
|
||||
### Tables
|
||||
|
||||
#### users
|
||||
|
||||
Purpose:
|
||||
|
||||
- 운영 사용자 레코드와 향후 identity mapping 기준 저장
|
||||
|
||||
| Column | Type | Null | Default | Constraints / Notes |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| id | Text | NO | - | Primary Key |
|
||||
| username | Text | NO | - | Unique username |
|
||||
| email | Text | YES | - | Unique when present |
|
||||
| permission | Text | NO | `read` | User permission level |
|
||||
| is_active | Boolean | NO | TRUE | Whether login is allowed |
|
||||
| password_hash | Text | YES | - | Scrypt password hash |
|
||||
| created_at | DateTime(timezone=True) | NO | NOW() | Creation timestamp |
|
||||
| updated_at | DateTime(timezone=True) | NO | NOW() | Last update timestamp |
|
||||
| last_login_at | DateTime(timezone=True) | YES | - | Last successful login timestamp |
|
||||
|
||||
Constraints:
|
||||
|
||||
- Primary key: `id`
|
||||
- Unique: `uq_users_username` on (`username`)
|
||||
- Unique: `uq_users_email` on (`email`)
|
||||
|
||||
#### auth_tokens
|
||||
|
||||
Purpose:
|
||||
|
||||
- DB 발급 session/client/bot token 저장
|
||||
|
||||
| Column | Type | Null | Default | Constraints / Notes |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| id | Text | NO | - | Primary Key |
|
||||
| user_id | Text | NO | - | Logical user reference |
|
||||
| name | Text | NO | - | Human-readable token name |
|
||||
| token_prefix | Text | NO | - | Display prefix |
|
||||
| token_hash | Text | NO | - | Unique SHA-256 token hash |
|
||||
| permission | Text | NO | - | Effective token permission |
|
||||
| token_type | Text | NO | - | `session`, `client`, or `bot` |
|
||||
| bot_principal_id | Text | YES | - | Logical reference to `bot_principals.id` |
|
||||
| expires_at | DateTime(timezone=True) | YES | - | Expiration time |
|
||||
| last_used_at | DateTime(timezone=True) | YES | - | Last successful use |
|
||||
| revoked_at | DateTime(timezone=True) | YES | - | Revocation time |
|
||||
| created_at | DateTime(timezone=True) | NO | NOW() | Creation timestamp |
|
||||
|
||||
Constraints:
|
||||
|
||||
- Primary key: `id`
|
||||
- Unique: `uq_auth_tokens_token_hash` on (`token_hash`)
|
||||
|
||||
#### bot_principals
|
||||
|
||||
Purpose:
|
||||
|
||||
- 외부 bot identity와 권한 ceiling 관리
|
||||
|
||||
| Column | Type | Null | Default | Constraints / Notes |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| id | Text | NO | - | Primary Key |
|
||||
| name | Text | NO | - | Unique bot name |
|
||||
| permission | Text | NO | - | Bot permission ceiling |
|
||||
| description | Text | YES | - | Optional description |
|
||||
| is_active | Boolean | NO | TRUE | False면 bot token 거부 |
|
||||
| created_at | DateTime(timezone=True) | NO | NOW() | Creation timestamp |
|
||||
| updated_at | DateTime(timezone=True) | NO | NOW() | Last update timestamp |
|
||||
| deactivated_at | DateTime(timezone=True) | YES | - | Deactivation time |
|
||||
|
||||
Constraints:
|
||||
|
||||
- Primary key: `id`
|
||||
- Unique: `uq_bot_principals_name` on (`name`)
|
||||
|
||||
#### strategy_sets
|
||||
|
||||
Purpose:
|
||||
|
||||
- 정규화된 전략 설정 snapshot 저장
|
||||
|
||||
| Column | Type | Null | Default | Constraints / Notes |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| id | Text | NO | - | Primary Key |
|
||||
| name | Text | NO | - | Strategy name, unique pair 일부 |
|
||||
| config | Text | NO | `""` | Strategy config payload |
|
||||
| nick | Text | NO | - | Unique nickname |
|
||||
| created_at | DateTime(timezone=True) | NO | NOW() | Creation timestamp |
|
||||
|
||||
Constraints:
|
||||
|
||||
- Primary key: `id`
|
||||
- Unique: `uq_strategy_sets_name_config` on (`name`, `config`)
|
||||
- Unique: `uq_strategy_sets_nick` on (`nick`)
|
||||
|
||||
#### portfolios
|
||||
|
||||
Purpose:
|
||||
|
||||
- 현재 포트폴리오 상태와 strategy linkage 저장
|
||||
|
||||
Current baseline note:
|
||||
|
||||
- 아래 컬럼은 revision `20260413_0010`까지의 마이그레이션 기준이다.
|
||||
|
||||
| Column | Type | Null | Default | Constraints / Notes |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| portfolio_id | Text | NO | - | Primary Key |
|
||||
| broker | Text | NO | `kis` | Broker code |
|
||||
| status | Text | NO | `enabled` | Portfolio status |
|
||||
| name | Text | NO | - | Asset name |
|
||||
| symbol | Text | NO | - | Trading symbol |
|
||||
| owner_user_id | Text | NO | - | FK to `users.id` |
|
||||
| cash_balance | Float | NO | - | Current cash balance |
|
||||
| asset_quantity | Float | NO | - | Current asset quantity |
|
||||
| asset_price | Float | NO | 0 | Current price snapshot |
|
||||
| profit_buffer | Float | NO | 0 | Profit buffer amount |
|
||||
| active_strategy_id | Text | YES | - | Logical reference to `strategy_sets.id` |
|
||||
| avabile_strategies | Text[] | YES | - | Nullable array of `strategy_sets.id` |
|
||||
| created_at | DateTime(timezone=True) | NO | NOW() | Creation timestamp |
|
||||
| updated_at | DateTime(timezone=True) | NO | NOW() | Last update timestamp |
|
||||
| last_trade_at | DateTime(timezone=True) | YES | - | Last trade time |
|
||||
|
||||
Constraints:
|
||||
|
||||
- Primary key: `portfolio_id`
|
||||
- Foreign key: `fk_portfolios_owner_user` on (`owner_user_id`) -> `users(id)`
|
||||
|
||||
#### portfolio_ledger
|
||||
|
||||
Purpose:
|
||||
|
||||
- 포트폴리오 액션별 append-like ledger snapshot 저장
|
||||
|
||||
| Column | Type | Null | Default | Constraints / Notes |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| portfolio_id | Text | NO | - | Logical reference to `portfolios.portfolio_id` |
|
||||
| recorded_at | DateTime(timezone=True) | NO | - | Event timestamp |
|
||||
| action | Text | NO | - | Action type |
|
||||
| price | Float | NO | - | Trade/valuation unit price |
|
||||
| quantity | Float | NO | - | Quantity delta |
|
||||
| fee_amount | Float | NO | 0 | Fees |
|
||||
| tax_amount | Float | NO | 0 | Taxes |
|
||||
| cash_balance_after | Float | NO | - | Post-action cash |
|
||||
| asset_quantity_after | Float | NO | - | Post-action asset quantity |
|
||||
| total_value_after | Float | NO | - | Post-action total value |
|
||||
| profit_buffer_after | Float | NO | 0 | Post-action profit buffer |
|
||||
| strategy_set_id | Text | NO | `legacy` | Logical strategy set reference |
|
||||
| note | Text | NO | `""` | Operator/system note |
|
||||
|
||||
Constraints:
|
||||
|
||||
- Primary key: none in baseline
|
||||
- Foreign key constraints: none in baseline
|
||||
|
||||
#### portfolio_comments
|
||||
|
||||
Purpose:
|
||||
|
||||
- 포트폴리오별 multiline comment history 저장 (`REQ-001`)
|
||||
|
||||
| Column | Type | Null | Default | Constraints / Notes |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| id | Text | NO | - | Primary Key (UUID) |
|
||||
| portfolio_id | Text | NO | - | Logical reference to `portfolios.portfolio_id` |
|
||||
| content | Text | NO | - | Multiline comment text |
|
||||
| created_at | DateTime(timezone=True) | NO | NOW() | Creation timestamp |
|
||||
| updated_at | DateTime(timezone=True) | NO | NOW() | Last update timestamp |
|
||||
|
||||
Constraints:
|
||||
|
||||
- Primary key: `id`
|
||||
- Foreign key constraints: none
|
||||
|
||||
### Indexes
|
||||
|
||||
| Index | Table | Columns/Expressions | Unique | Intent |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `ix_users_created_at` | `users` | `created_at` | no | recency listing |
|
||||
| `idx_auth_tokens_user_created` | `auth_tokens` | `user_id`, `created_at` | no | current user token listing |
|
||||
| `idx_auth_tokens_hash_active` | `auth_tokens` | `token_hash` | no | bearer token lookup |
|
||||
| `idx_auth_tokens_bot_created` | `auth_tokens` | `bot_principal_id`, `created_at` | no | bot token listing |
|
||||
| `idx_bot_principals_active_created` | `bot_principals` | `is_active`, `created_at` | no | active/inactive bot filtering |
|
||||
| `idx_portfolio_ledger_portfolio_time` | `portfolio_ledger` | `portfolio_id`, `recorded_at DESC` | no | recent ledger query |
|
||||
| `idx_portfolios_broker_status_updated` | `portfolios` | `broker`, `status`, `updated_at DESC` | no | broker/status recency filtering |
|
||||
| `idx_portfolios_owner_updated` | `portfolios` | `owner_user_id`, `updated_at DESC` | no | owner-scoped recency filtering |
|
||||
| `idx_portfolio_comments_portfolio_created` | `portfolio_comments` | `portfolio_id`, `created_at DESC` | no | reverse chronological comments |
|
||||
|
||||
Conditional migration behavior:
|
||||
|
||||
- `timescaledb` extension 존재 시 `create_hypertable('portfolio_ledger', 'recorded_at', if_not_exists => TRUE)` 실행
|
||||
- 이 경우 `portfolio_ledger`는 hypertable로 동작한다.
|
||||
|
||||
## Integrity Notes
|
||||
|
||||
현재 baseline은 필수 ownership 제약(`portfolios.owner_user_id` -> `users.id`)만 DB-level FK를 사용한다.
|
||||
|
||||
- ownership 외 logical relation은 application/repository logic이 관리한다.
|
||||
- 향후 FK constraint를 추가하면 migration ID와 함께 본 문서를 갱신한다.
|
||||
- application layer cascade:
|
||||
- 포트폴리오 삭제 시 연관 `portfolio_comments`를 hard delete 한다.
|
||||
|
||||
## REQ-002 Tagging Storage Design
|
||||
|
||||
목표:
|
||||
|
||||
- 포트폴리오별 태그 연결을 중복 없이 저장한다.
|
||||
- 앞뒤 공백 제거 기반 정규화와 검색용 include/exclude 조건을 DB 계층에서 일관되게 지원한다.
|
||||
|
||||
구현 저장 모델:
|
||||
|
||||
- 신규 테이블 `portfolio_tags`
|
||||
- 복합 PK(`portfolio_id`, `tag_value`) 사용
|
||||
- `portfolio_id`: 대상 포트폴리오
|
||||
- `tag_value`: 앞뒤 공백 제거 후 저장되는 태그 값
|
||||
- `created_at`: 연결 생성 시각
|
||||
- 동일 포트폴리오 내 중복 태그는 PK 자체로 방지한다.
|
||||
|
||||
정규화 책임:
|
||||
|
||||
- API/CLI 입력 계층에서 앞뒤 공백 제거를 수행한다.
|
||||
- repository는 이미 정규화된 `tag_value`를 기준으로 insert/delete/query를 수행한다.
|
||||
- DB는 `UNIQUE (portfolio_id, tag_value)`로 중복 insert를 방지하고 application은 이를 `409 Conflict`로 매핑한다.
|
||||
|
||||
검색 전략:
|
||||
|
||||
- `includeTags`와 `excludeTags`는 모두 정규화된 태그 값 기준으로 비교한다.
|
||||
- `matchMode=AND`는 `GROUP BY portfolio_id` + `HAVING COUNT(DISTINCT tag_value) = <includeTags 개수>` 전략을 기본안으로 둔다.
|
||||
- `matchMode=OR`는 `WHERE tag_value IN (...)` + distinct portfolio projection을 기본안으로 둔다.
|
||||
- `excludeTags`는 anti-join 또는 `NOT EXISTS` 전략을 사용한다.
|
||||
|
||||
권장 인덱스:
|
||||
|
||||
- `UNIQUE (portfolio_id, tag_value)`
|
||||
- `INDEX (tag_value, portfolio_id)`
|
||||
- owner-scoped 목록 필터와 결합될 수 있으므로 `portfolios.owner_user_id` 인덱스와 함께 실행 계획을 검토한다.
|
||||
|
||||
적용 마이그레이션 단계:
|
||||
|
||||
1. `20260414_0012_portfolio_tags.py`에서 `portfolio_tags` 테이블 생성
|
||||
2. `fk_portfolio_tags_portfolio` FK와 `idx_portfolio_tags_tag_portfolio` 인덱스 생성
|
||||
3. API/CLI/repository가 동일 revision 기준으로 태그 mutation 및 검색을 사용
|
||||
4. 필요 시 exact-match 중심 추가 인덱스를 검토
|
||||
|
||||
## Validation Notes
|
||||
- 마이그레이션은 빈 DB와 기존 DB 모두에서 연속 적용 가능해야 한다.
|
||||
- 파괴적 변경(drop/rename)은 백필 또는 호환 단계 계획 없이 직접 반영하지 않는다.
|
||||
- 문서 동기화 게이트(`scripts/check_docs_sync.py`)를 통과해야 한다.
|
||||
|
||||
## Revision Mapping
|
||||
|
||||
| Revision ID | File | Summary |
|
||||
| --- | --- | --- |
|
||||
| 20260403_0001 | migrations/versions/20260403_0001_initial_portfolio_schema.py | Initial schema for strategy sets, portfolios, ledger, indexes, optional hypertable |
|
||||
| 20260403_0002 | migrations/versions/20260403_0002_strategy_set_columns.py | Rename `strategy_name` to `name`, `strategy_config` to `config`, add unique `nick` |
|
||||
| 20260403_0003 | migrations/versions/20260403_0003_portfolio_strategy_refs.py | Remove embedded strategy fields, add `active_strategy_id` and `avabile_strategies` |
|
||||
| 20260403_0004 | migrations/versions/20260403_0004_users_table.py | Add `users` table with unique username/email and created_at index |
|
||||
| 20260403_0005 | migrations/versions/20260403_0005_auth_user_fields_and_tokens.py | Add user auth fields and `auth_tokens` table |
|
||||
| 20260403_0006 | migrations/versions/20260403_0006_bot_principals_and_bot_tokens.py | Add `bot_principals` and bot token linkage |
|
||||
| 20260403_0007 | migrations/versions/20260403_0007_bot_principal_permission_only.py | Remove `owner_user_id` and enforce bot-local permission model |
|
||||
| 20260403_0008 | migrations/versions/20260403_0008_portfolio_name_column.py | Add non-null `portfolios.name` and backfill from `symbol` |
|
||||
| 20260410_0009 | migrations/versions/20260410_0009_portfolio_comments_table.py | Add `portfolio_comments` table for per-portfolio memo history |
|
||||
| 20260413_0010 | migrations/versions/20260413_0010_portfolio_owner_user_id.py | Add `portfolios.owner_user_id`, backfill owner, create owner index, and add owner FK |
|
||||
| 20260414_0011 | migrations/versions/20260414_0011_bot_principal_owner_user_id.py | Restore `bot_principals.owner_user_id`, backfill owner, create owner index, and add owner FK |
|
||||
| 20260414_0012 | migrations/versions/20260414_0012_portfolio_tags.py | Add `portfolio_tags`, portfolio FK, and tag lookup index for REQ-002 |
|
||||
|
||||
## Open Implementation Points
|
||||
- `portfolio_ledger`의 Timescale hypertable 강제 여부와 보존 정책(retention)을 확정한다.
|
||||
- owner-scoped 조회와 태그 필터를 함께 사용할 때 결과 집합이 ownership 경계를 넘지 않아야 한다.
|
||||
@@ -1,151 +0,0 @@
|
||||
# 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`
|
||||
- 봇 토큰(`token_type=bot`)의 `auth_context.user_id`는 `bot_principals.owner_user_id`로 해석
|
||||
|
||||
## 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을 재사용 |
|
||||
|
||||
## Bot Principal Ownership 정책
|
||||
|
||||
- `bot_principals.owner_user_id`는 필수이며 `users.id` FK를 가진다.
|
||||
- 봇 principal 생성 정책:
|
||||
- Admin: 임의 사용자 owner로 생성 가능
|
||||
- Self-service 사용자: 본인 owner로만 생성 가능
|
||||
- 봇 토큰 관리 정책:
|
||||
- Admin 경로는 전체 봇 대상으로 관리 가능
|
||||
- Self-service 경로는 본인 소유 봇 대상으로만 관리 가능
|
||||
|
||||
## 서비스/리포지토리 반영 포인트
|
||||
- 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 시 인덱스/제약 해제가 순서대로 수행되는지 검증
|
||||
4. 기존 bot_principals row가 존재할 때 owner_user_id 백필 후 null row가 0건인지 검증
|
||||
5. bot_principals.owner_user_id FK(`users.id`) 제약 생성/해제 검증
|
||||
|
||||
## API Authorization
|
||||
1. 사용자 A 소유 portfolio를 사용자 B가 조회하면 403
|
||||
2. 사용자 A 소유 portfolio를 사용자 B가 수정/매매/리셋하면 403
|
||||
3. admin이 타 사용자 portfolio 조회/수정 가능
|
||||
4. 일반 사용자의 `GET /portfolios`는 본인 소유만 반환
|
||||
5. 사용자 A에 귀속된 bot 토큰은 A 소유 portfolio만 접근 가능
|
||||
6. 사용자 B가 A 소유 bot의 self-service 토큰 발급/조회/폐기를 시도하면 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를 갱신한다.
|
||||
@@ -1,109 +0,0 @@
|
||||
# Portfolio Tagging and Search Design
|
||||
|
||||
- Related REQ: `REQ-002`
|
||||
|
||||
## 목표
|
||||
- 승인된 `REQ-002`를 구현 가능한 내부 설계로 정리한다.
|
||||
- 태그 정규화, 검색 파라미터 의미, 모듈 책임, 저장소/마이그레이션 방향을 한 문서에서 추적한다.
|
||||
|
||||
## 구현 범위
|
||||
- 포트폴리오별 태그 추가/삭제 흐름
|
||||
- HTTP API와 CLI의 공통 태그 검색 파라미터 의미
|
||||
- repository query 전략과 DB 인덱스 후보
|
||||
|
||||
제외:
|
||||
|
||||
- 태그 추천, 자동 분류, 네임스페이스 사전 관리
|
||||
- 부분 일치/전방 일치/full-text 검색
|
||||
|
||||
## 정규화 정책
|
||||
- 입력 태그는 API/CLI 계층에서 앞뒤 공백을 제거한다.
|
||||
- 정규화 후 빈 문자열이면 validation 오류로 처리한다.
|
||||
- 대소문자, 내부 공백, `:` 네임스페이스는 변경하지 않는다.
|
||||
- 중복 판단은 정규화된 값 기준으로 수행한다.
|
||||
|
||||
예시:
|
||||
|
||||
- `" grade:A "` -> `"grade:A"`
|
||||
- `"Grade:A"`와 `"grade:A"`는 서로 다른 태그로 취급한다.
|
||||
- `"blacklist: trade"`의 내부 공백은 보존한다.
|
||||
|
||||
## 모듈 책임
|
||||
|
||||
API:
|
||||
|
||||
- 대상 파일: `app/api/portfolios.py`
|
||||
- 책임:
|
||||
- `POST/GET/DELETE /portfolios/{portfolio_id}/tags` 제공
|
||||
- `GET /portfolios`의 반복 query 파라미터 `includeTags`, `excludeTags`, `matchMode` 수신
|
||||
- 기존 ownership/auth 정책 재사용
|
||||
|
||||
CLI:
|
||||
|
||||
- 대상 파일: `app/cli/commands/portfolio.py`
|
||||
- 책임:
|
||||
- `portfolio tag add|list|remove` 명령 노출
|
||||
- `portfolio list --include-tag --exclude-tag --match-mode` 옵션 제공
|
||||
|
||||
Schema:
|
||||
|
||||
- 대상 파일: `app/schemas/portfolio.py`
|
||||
- 책임:
|
||||
- tag mutation request/response 모델 정의
|
||||
- list query 파라미터 contract 정리
|
||||
|
||||
Service:
|
||||
|
||||
- 대상 파일: `app/services/portfolio_service.py`
|
||||
- 책임:
|
||||
- 정규화된 태그 입력 검증
|
||||
- repository 호출 orchestration
|
||||
- duplicate/not-found/auth 오류를 API/CLI가 소비할 수 있는 에러로 유지
|
||||
|
||||
Repository:
|
||||
|
||||
- 대상 파일: `app/repositories/portfolio_repository.py`, `app/repositories/postgres.py`
|
||||
- 책임:
|
||||
- 태그 연결 추가/삭제
|
||||
- include/exclude + matchMode query 제공
|
||||
- duplicate insert를 명시적 오류로 변환
|
||||
|
||||
## 검색 파라미터 의미
|
||||
|
||||
- `includeTags`
|
||||
- 포함해야 하는 정규화 태그 목록
|
||||
- `excludeTags`
|
||||
- 하나라도 일치하면 제외할 정규화 태그 목록
|
||||
- `matchMode`
|
||||
- `includeTags`에만 적용
|
||||
- `AND`: 모든 include tag 필요
|
||||
- `OR`: 하나 이상의 include tag 필요
|
||||
|
||||
처리 순서:
|
||||
|
||||
1. 입력 태그 정규화
|
||||
2. ownership/auth 필터 적용
|
||||
3. include 조건 계산
|
||||
4. exclude 조건 제거
|
||||
5. 중복 portfolio 제거
|
||||
6. 기존 recency ordering과 충돌 여부 확인
|
||||
|
||||
## Validation Notes
|
||||
- duplicate tag add는 `UNIQUE (portfolio_id, tag_value)` 또는 동등한 repository 검증으로 `409`에 매핑해야 한다.
|
||||
- 존재하지 않는 포트폴리오에 대한 add/remove는 `404`로 매핑해야 한다.
|
||||
- 검색 결과는 `OR` 조건에서 중복 없이 한 번만 반환해야 한다.
|
||||
- owner-scoped 조회와 태그 필터를 함께 사용할 때 결과 집합이 ownership 경계를 넘지 않아야 한다.
|
||||
|
||||
## 파일 위치 및 구현 후보
|
||||
- API route extension: `app/api/portfolios.py`
|
||||
- CLI extension: `app/cli/commands/portfolio.py`
|
||||
- schema extension: `app/schemas/portfolio.py`
|
||||
- service orchestration: `app/services/portfolio_service.py`
|
||||
- repository contract: `app/repositories/portfolio_repository.py`
|
||||
- Postgres query implementation: `app/repositories/postgres.py`
|
||||
- migration: `migrations/versions/20260414_0012_portfolio_tags.py`
|
||||
|
||||
## 구현 결정
|
||||
- 태그 mutation은 기존 포트폴리오 리소스 하위 경로 `/portfolios/{portfolio_id}/tags`로 고정했다.
|
||||
- `GET /portfolios` 태그 필터는 반복 query parameter 방식으로 구현했다.
|
||||
- CLI 다중 태그 입력은 반복 옵션 `--include-tag`, `--exclude-tag`로 구현했다.
|
||||
@@ -1,39 +0,0 @@
|
||||
# Project Structure Baseline
|
||||
|
||||
- Related REQ: N/A
|
||||
|
||||
## 목표
|
||||
`portfolio-hub` 구현 및 유지보수 시 디렉토리 책임을 명확히 하여 API/CLI/도메인/저장소 변경의 영향 범위를 예측 가능하게 유지한다.
|
||||
|
||||
## Target Baseline Structure
|
||||
|
||||
| 경로 | 책임 | 구현 메모 |
|
||||
| --- | --- | --- |
|
||||
| `docs/SRS/` | 외부 계약, 승인 기준, REQ 상태 관리 | requirement source of truth |
|
||||
| `docs/technical/` | 구현 상세 설계와 운영 메모 | SRS를 재정의하지 않음 |
|
||||
| `app/api/` | HTTP 라우트, 요청/응답 경계 | 서비스 계층 호출 전담 |
|
||||
| `app/cli/` | Typer CLI 명령과 입출력 | 서비스 계층 재사용 |
|
||||
| `app/services/` | 유스케이스 조합, 권한/검증 흐름 | API/CLI 공통 오케스트레이션 |
|
||||
| `app/domain/` | 포트폴리오 비즈니스 규칙 | 기술 의존성 최소화 |
|
||||
| `app/repositories/` | 영속성 인터페이스와 구현체 | DB feature flag 지원 |
|
||||
| `app/schemas/` | API 계약 스키마(Pydantic) | 요청/응답 안정성 |
|
||||
| `migrations/` | Alembic 마이그레이션 | 스키마 변경 이력 |
|
||||
| `tests/` | API/도메인/CLI/계약 테스트 | 회귀 방지 게이트 |
|
||||
|
||||
## Root-Level Exception Rules
|
||||
- 루트에는 `README.md`, `pyproject.toml`, `alembic.ini`, `.github/` 등 저장소 메타파일만 둔다.
|
||||
- 실행 코드와 테스트 코드는 목적 디렉토리로 수렴시킨다.
|
||||
|
||||
## Documentation Responsibilities
|
||||
- `README.md`: 빠른 시작, 실행 방법, 운영 필수 변수 요약
|
||||
- `docs/SRS/`: 왜(Why), 무엇(What), 수락 기준
|
||||
- `docs/technical/`: 어떻게(How), 모듈 경계, 예시 흐름
|
||||
|
||||
## Validation Notes
|
||||
- 신규 모듈 추가 시 책임이 상기 경로 정의와 충돌하지 않는지 확인한다.
|
||||
- API 계약 변경 시 `app/schemas/`와 `docs/technical/api-contracts.md`를 함께 동기화한다.
|
||||
- 스키마 변경 시 `migrations/versions/`와 문서 업데이트를 같은 변경 단위에 포함한다.
|
||||
|
||||
## Open Implementation Points
|
||||
- Redis/Loki 실제 운영 경로가 코드에 전면 반영되면 `app/core/` 또는 별도 infra 패키지 분리 여부를 확정한다.
|
||||
- 태그 검색 기능(`REQ-002`) 구현 시 서비스/리포지토리 경계의 책임 분리를 추가 명세한다.
|
||||
@@ -1,33 +0,0 @@
|
||||
# Python Runtime and Packaging
|
||||
|
||||
- Related REQ: N/A
|
||||
|
||||
## 목표
|
||||
Python `3.11+` 및 `pyproject.toml` 중심 패키징 기준을 유지하고, 로컬/CI/운영의 실행 환경 편차를 줄인다.
|
||||
|
||||
## Runtime Baseline
|
||||
- 공식 지원 런타임은 Python `3.11` 이상이다.
|
||||
- 로컬 개발은 `venv` 기반 실행을 기본으로 한다.
|
||||
- 개발/테스트 의존성은 editable install(`pip install -e .[dev]`)로 일관화한다.
|
||||
|
||||
## Required Files
|
||||
|
||||
| 파일 | 책임 | 필수 내용 |
|
||||
| --- | --- | --- |
|
||||
| `pyproject.toml` | 프로젝트 메타데이터와 의존성 기준 | `requires-python >=3.11` |
|
||||
| `README.md` | 설치/실행/테스트 진입점 | venv 생성 및 실행 절차 |
|
||||
| `alembic.ini` | DB migration 런타임 설정 | Alembic 동작 기준 |
|
||||
|
||||
## Packaging Rules
|
||||
- 패키징 source of truth는 `pyproject.toml`로 유지한다.
|
||||
- CLI 엔트리포인트 `portfoliohub`는 패키지 설치 후 동일하게 동작해야 한다.
|
||||
- 운영 이미지/환경에서도 개발과 동일한 Python major/minor를 사용한다.
|
||||
|
||||
## Validation Notes
|
||||
- `pip install -e .[dev]` 후 `uvicorn app.main:app --reload`와 `pytest`가 실행 가능해야 한다.
|
||||
- `portfoliohub --help`가 정상 출력되어야 한다.
|
||||
- Alembic 명령(`upgrade head`)이 현재 마이그레이션 체인에서 실패하지 않아야 한다.
|
||||
|
||||
## Open Implementation Points
|
||||
- 락파일 기반 재현성(`uv lock`, `poetry.lock` 등) 도입 여부는 팀 운영 정책에 따라 확정한다.
|
||||
- 배포 환경 컨테이너 표준화 시 base image pinning 정책을 문서화한다.
|
||||
@@ -1,84 +0,0 @@
|
||||
# Settings and Env Loading
|
||||
|
||||
- Related REQ: N/A
|
||||
|
||||
## 목표
|
||||
설정값 로딩 경로를 단일화하여 로컬/운영 간 동작 차이를 최소화하고, 보안 민감 값을 안전하게 주입한다.
|
||||
|
||||
## 설정 로딩 우선순위
|
||||
1. 환경 변수
|
||||
2. `.env` 파일
|
||||
3. 코드 기본값
|
||||
|
||||
## 핵심 설정 그룹
|
||||
|
||||
| 그룹 | 예시 키 | 목적 |
|
||||
| --- | --- | --- |
|
||||
| API/CORS | `PORTFOLIOHUB_ALLOWED_ORIGINS` | 브라우저 호출 허용 오리진 제어 |
|
||||
| 인증/권한 | `PORTFOLIOHUB_READ_TOKENS`, `PORTFOLIOHUB_TRADE_WRITE_TOKENS`, `PORTFOLIOHUB_ADMIN_TOKENS` | 정적 토큰 기반 접근 제어 |
|
||||
| 세션/토큰 TTL | `PORTFOLIOHUB_SESSION_TOKEN_TTL_HOURS` 등 | DB 발급 토큰 만료 정책 |
|
||||
| Bootstrap User | `PORTFOLIOHUB_BOOTSTRAP_USERNAME`, `PORTFOLIOHUB_BOOTSTRAP_PASSWORD` | 초기 admin 사용자 생성 |
|
||||
| DB 연결 | `TIMESCALEDB_*` | Postgres/Timescale 접속 정보 |
|
||||
| Observability | `LOKI_*`, `REDIS_*` | 중앙 로그/캐시 운영 확장 포인트 |
|
||||
|
||||
## 상세 설정 명세
|
||||
|
||||
핵심 설정:
|
||||
|
||||
- `PORTFOLIOHUB_ALLOWED_ORIGINS`: comma-separated CORS origins
|
||||
- `PORTFOLIOHUB_BOOTSTRAP_USERNAME`: users 테이블이 비어 있을 때 생성할 초기 username
|
||||
- `PORTFOLIOHUB_BOOTSTRAP_EMAIL`: bootstrap user의 선택적 email
|
||||
- `PORTFOLIOHUB_BOOTSTRAP_PASSWORD`: 초기 admin password
|
||||
- `PORTFOLIOHUB_READ_TOKENS`: comma-separated read tokens
|
||||
- `PORTFOLIOHUB_TRADE_WRITE_TOKENS`: comma-separated trade-write tokens
|
||||
- `PORTFOLIOHUB_ADMIN_TOKENS`: comma-separated admin tokens
|
||||
- `PORTFOLIOHUB_DB_ENABLED`: `true`일 때 DB repository 활성화
|
||||
- `PORTFOLIOHUB_SESSION_TOKEN_TTL_HOURS`: login session token TTL(hours)
|
||||
- `PORTFOLIOHUB_CLIENT_TOKEN_DEFAULT_TTL_DAYS`: client token 기본 TTL(days)
|
||||
- `PORTFOLIOHUB_BOT_TOKEN_DEFAULT_TTL_DAYS`: bot token 기본 TTL(days)
|
||||
|
||||
DB 활성 설정:
|
||||
|
||||
- `TIMESCALEDB_HOST`
|
||||
- `TIMESCALEDB_PORT`
|
||||
- `TIMESCALEDB_NAME`
|
||||
- `TIMESCALEDB_USER`
|
||||
- `TIMESCALEDB_PASSWORD`
|
||||
- `TIMESCALEDB_SSLMODE`
|
||||
- `TIMESCALEDB_CONNECT_TIMEOUT_SEC`
|
||||
- `TIMESCALEDB_APPLICATION_NAME`
|
||||
|
||||
파생 설정:
|
||||
|
||||
- `active_database_url`은 psycopg PostgreSQL URL 포맷으로 credentials와 query options를 인코딩해 구성한다.
|
||||
|
||||
## CORS 정책
|
||||
|
||||
CORS middleware는 다음 기준으로 동작한다.
|
||||
|
||||
- `allow_origins`: 구성된 origins 목록 사용
|
||||
- `allow_credentials = true`
|
||||
- `allow_methods = ["GET", "POST", "PATCH", "DELETE", "OPTIONS"]`
|
||||
- `allow_headers = ["Authorization", "Content-Type"]`
|
||||
|
||||
브라우저 기반 frontend가 HTTPS로 접근하면 `PORTFOLIOHUB_ALLOWED_ORIGINS`에는 정확한 HTTPS origin만 명시한다.
|
||||
|
||||
예시:
|
||||
|
||||
```bash
|
||||
PORTFOLIOHUB_ALLOWED_ORIGINS=https://app.example.com,https://www.example.com
|
||||
```
|
||||
|
||||
## 운영 규칙
|
||||
- 운영 환경에서는 `.env` 대신 배포 플랫폼의 환경 변수 주입을 우선한다.
|
||||
- `PORTFOLIOHUB_DB_ENABLED=true`일 때 `TIMESCALEDB_*` 필수 키 누락 여부를 시작 시 검증한다.
|
||||
- 민감 값은 문서/샘플에 실제 비밀 값을 기록하지 않는다.
|
||||
|
||||
## Validation Notes
|
||||
- 필수 키 누락 시 명확한 예외 메시지로 기동 실패해야 한다.
|
||||
- CORS 오리진 파싱은 공백/쉼표 변형 입력에도 안정적으로 동작해야 한다.
|
||||
- 부트스트랩 계정 자동 생성은 `users` 테이블이 비어 있을 때만 수행되어야 한다.
|
||||
|
||||
## Open Implementation Points
|
||||
- 설정 스키마를 strict mode로 고정할지(미정의 키 거부) 여부를 확정한다.
|
||||
- Redis/Loki 활성화 플래그와 실패 시 동작(무시/실패)을 명시적으로 분리할지 결정한다.
|
||||
Reference in New Issue
Block a user