docs(sdlc): sdlc-002 service-token user-based Discovery/Planning/Index/summary 문서 초안

This commit is contained in:
2026-04-23 07:21:31 +00:00
parent 5d3884fd4a
commit 1e8e3d7ae2
5 changed files with 508 additions and 0 deletions
+22
View File
@@ -0,0 +1,22 @@
# 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 | |
@@ -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 <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>` 기반 직접 발급.
@@ -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 <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 | - | {{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`에서 `<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: 정리 완료 | {{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_<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` 전체 실행 후 결과 확인 | {{VERIFY_OWNER_1:TBD}} |
| S-2: pfhub service-token --help 정상 노출 | 터미널에서 `pfhub service-token issue --help` 실행, `<name>` 및 `--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}}
@@ -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) | `-` |
@@ -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)