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단일 엔티티로 직접 관리한다.
- CLI 명령 그룹은
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_idJOIN이 제거된다.
- 이해관계자
- 운영자(관리자 권한 사용자): 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_typeLiteral"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_tokenrename, 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_typeLiteral"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()로 renamemigrations/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테이블 DROPtests/: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변경으로 기존 클라이언트 호환성 영향 가능
- DB 마이그레이션 실행 필요 (테이블 rename,
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_principalsJOIN이 제거되고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"renameauth_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이 있으면 오류를 내고 중단한다. 운영 전 사전 검증 쿼리를 마이그레이션 파일에 포함한다.
- 완화 방안: 마이그레이션 upgrade 전
- R-2 (Medium): rename 범위가 약 15개 파일에 걸쳐 광범위하므로 일부 누락 시 런타임
AttributeError/NameError발생 가능.- 완화 방안: rename 완료 후
grep -r "bot_principal\|bot_token\|token_type.*bot"잔류 참조 검사를 CI에서 실행하거나, 전체 테스트 통과를 gate로 설정한다.
- 완화 방안: rename 완료 후
- 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_principalsDROP이 안전하게 진행 가능하다. - 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.pyAuthContext 변경,app/services/auth_service.py가드 교체 — 영향 범위 항목에 반영 완료.
- 반영 위치:
-
OQ-5:auth_audit_logs.bot_idNOT 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 해소 완료
- 기준: 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. 요구사항 목록 > 범위 경계
- 성공 기준: # 7. 초기 성공 기준
- 리스크: # 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>기반 직접 발급.