Files
portfolio-hub/docs/sdlc/sdlc-002_20260423_service-token-user-based/1_discovery.md
T

20 KiB

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

섹션 참조

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