diff --git a/docs/sdlc/index.md b/docs/sdlc/index.md index e69de29..c5a2e24 100644 --- a/docs/sdlc/index.md +++ b/docs/sdlc/index.md @@ -0,0 +1,22 @@ +# 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 | | diff --git a/docs/sdlc/sdlc-002_20260423_service-token-user-based/1_discovery.md b/docs/sdlc/sdlc-002_20260423_service-token-user-based/1_discovery.md new file mode 100644 index 0000000..f098a64 --- /dev/null +++ b/docs/sdlc/sdlc-002_20260423_service-token-user-based/1_discovery.md @@ -0,0 +1,241 @@ +# 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 --user ` 명령으로 외부 서비스용 API key를 발급한다. ``은 발급할 토큰의 유니크 식별자이며 어떤 외부 서비스에서 사용 중인지 추적하는 데 쓰인다. `--user`로 지정한 username에서 user_id를 조회하여 `auth_tokens.user_id`에 저장하고, 해당 user의 권한 범위 내에서 동작한다. +- 운영자가 `--user`를 생략하고 `pfhub service-token issue ` 명령을 실행하면 오류가 반환된다. (`--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` 인수 설계: `` = 발급할 토큰의 유니크 식별자(`auth_tokens.name`에 저장, service token 범위 내 중복 불가), `--user ` 필수 옵션(생략 시 오류 반환, 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` 명령 인수 `` = 토큰 이름(`auth_tokens.name`에 저장), `--user ` 필수 옵션 추가 + - `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 ` 명령의 ``은 발급할 토큰의 식별자이며 `auth_tokens.name`에 저장된다. service token의 `name`은 유니크해야 하며, 중복 시 오류를 반환해야 한다. +- FR-6: `pfhub service-token issue --user ` 명령에서 `--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 --user `) + - 기존 테스트 업데이트 및 신규 검증 테스트 추가 + - 기술 문서(`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` 단일 엔티티로 ``(유니크) + `--user ` 기반 직접 발급. diff --git a/docs/sdlc/sdlc-002_20260423_service-token-user-based/2_planning.md b/docs/sdlc/sdlc-002_20260423_service-token-user-based/2_planning.md new file mode 100644 index 0000000..c9c48eb --- /dev/null +++ b/docs/sdlc/sdlc-002_20260423_service-token-user-based/2_planning.md @@ -0,0 +1,217 @@ +# 0. 문서 상태 +이 문서의 현재 상태와 식별 정보를 기록한다. + +- 상태: draft +- 문서 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 + +# 1. Discovery 인계 요약 +Discovery에서 확정된 핵심 내용을 요약한다. 원본은 1_discovery.md를 참조하며, 이 섹션은 Planning 작업의 입력 기준선이 된다. + +## 이슈 목적 +`bot_principals` 테이블과 audit log를 제거하고, service token을 `auth_tokens` 단일 엔티티로 직접 발급하도록 재구현한다. CLI 명령 `pfhub service-token issue --user `으로 사용자 기반 소유권을 명확히 한다. + +## 확정 범위 +- 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 --user `) + - `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 ` → 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 | - | {{TASK_OWNER_1:TBD}} | +| T-2 | 도메인/스키마/인터페이스 정리 | 개발 | FR-1 | - | {{TASK_OWNER_2:TBD}} | +| T-3 | postgres.py 구현체 정리 | 개발 | FR-1, FR-4 | T-2 | {{TASK_OWNER_3:TBD}} | +| T-4 | AuthContext/security.py 변경 | 개발 | FR-1 (OQ-4) | T-2 | {{TASK_OWNER_4:TBD}} | +| T-5 | AuthService 재구현 | 개발 | FR-2, FR-5, FR-6, FR-7 | T-3, T-4 | {{TASK_OWNER_5:TBD}} | +| T-6 | API 경로 정리 | 개발 | FR-1 | T-5 | {{TASK_OWNER_6:TBD}} | +| T-7 | CLI 재구현 | 개발 | FR-5, FR-6, FR-7, NFR-3 | T-5 | {{TASK_OWNER_7:TBD}} | +| T-8 | 테스트 업데이트 | 개발 | NFR-1 | T-5, T-6, T-7 | {{TASK_OWNER_8:TBD}} | +| T-9 | 기술 문서 업데이트 | 문서 | - | T-7, T-8 | {{TASK_OWNER_9:TBD}} | + +## 태스크 완료 조건 (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`에서 ``, `--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: 정리 완료 | {{MILESTONE_1_DATE:TBD}} | T-1, T-2, T-3, T-4 | bot_principals/auth_audit_logs 관련 코드·스키마·DDL 전체 삭제, import 오류 없음 | +| M-2: 구현·검증 완료 | {{MILESTONE_2_DATE:TBD}} | T-5, T-6, T-7, T-8, T-9 | pytest 0 failures, CLI --help 정상 노출, 문서 갱신 완료 | + +## 이터레이션 계획 +- 이터레이션 단위: 없음(단일 배포) +- 이터레이션 1 ({{ITERATION_1_PERIOD:TBD}}): T-1, T-2 (병렬 가능) +- 이터레이션 2 ({{ITERATION_2_PERIOD:TBD}}): T-3, T-4 (T-2 완료 후) +- 이터레이션 3 ({{ITERATION_3_PERIOD:TBD}}): T-5 (T-3, T-4 완료 후) +- 이터레이션 4 ({{ITERATION_4_PERIOD:TBD}}): T-6, T-7 (병렬 가능, T-5 완료 후) +- 이터레이션 5 ({{ITERATION_5_PERIOD:TBD}}): 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_.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` 전체 실행 후 결과 확인 | {{VERIFY_OWNER_1:TBD}} | +| S-2: pfhub service-token --help 정상 노출 | 터미널에서 `pfhub service-token issue --help` 실행, `` 및 `--user` 인수 확인 | {{VERIFY_OWNER_2:TBD}} | +| S-3: auth_tokens.user_id FK 정합성 | 마이그레이션 upgrade 후 `SELECT COUNT(*) FROM auth_tokens WHERE user_id NOT IN (SELECT id FROM users)` = 0 확인 | {{VERIFY_OWNER_3:TBD}} | + +# 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으로 넘기기 전 확인 체크 + +- [ ] 전체 태스크 목록 확정 및 담당자 지정 + - 기준: T-1 ~ T-9 모든 태스크에 담당자(`{{TASK_OWNER_N}}`)가 지정되어야 한다 +- [ ] 공수 추정 완료 및 총 공수 확인 + - 기준: 17 SP — 모든 태스크 추정값 채움 완료 +- [ ] 마일스톤 및 이터레이션 계획 확정 + - 기준: M-1, M-2 목표 날짜(`{{MILESTONE_N_DATE}}`)가 확정되어야 한다 +- [ ] 외부 의존성 해소 계획 확인 + - 기준: DB 접속 환경 및 pytest 실행 환경 확인 +- [ ] 모든 오픈 질문(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 담당자 지정: 현재 전부 `TBD`. 단독 개발자 환경이면 Justin으로 일괄 지정 가능 +- M-1, M-2 목표 날짜 및 이터레이션 기간 확정 + +## DATA +- 마이그레이션 실행 전 운영 DB `bot_principals` 행 수 및 `owner_user_id NULL` 행 수 확인 필요 (R-1 완화) + +# 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}} diff --git a/docs/sdlc/sdlc-002_20260423_service-token-user-based/index.md b/docs/sdlc/sdlc-002_20260423_service-token-user-based/index.md new file mode 100644 index 0000000..ee38373 --- /dev/null +++ b/docs/sdlc/sdlc-002_20260423_service-token-user-based/index.md @@ -0,0 +1,12 @@ +# sdlc-002_20260423_service-token-user-based + +| 단계 | 문서 | 상태 | +|------|------|------| +| 1. Discovery | [1_discovery.md](./1_discovery.md) | `confirmed` | +| 2. Planning | [2_planning.md](./2_planning.md) | `draft` | +| 3. Design | [3_design.md](./3_design.md) | `-` | +| 4. Implementation | [4_implementation.md](./4_implementation.md) | `-` | +| 5. Verification | [5_verification.md](./5_verification.md) | `-` | +| 6. Release | [6_release.md](./6_release.md) | `-` | +| 7. Operations | [7_operations.md](./7_operations.md) | `-` | +| Summary | [summary.md](./summary.md) | `-` | diff --git a/docs/sdlc/sdlc-002_20260423_service-token-user-based/summary.md b/docs/sdlc/sdlc-002_20260423_service-token-user-based/summary.md new file mode 100644 index 0000000..ce536ad --- /dev/null +++ b/docs/sdlc/sdlc-002_20260423_service-token-user-based/summary.md @@ -0,0 +1,16 @@ +# 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)