# 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 ` 기반 직접 발급.