29 KiB
0. 문서 상태
이 문서의 현재 상태와 식별 정보를 기록한다.
- 상태: confirmed
- 문서 ID: sdlc-002_20260423_service-token-user-based
- 이슈명: service-token user-based 소유권
- 참조 Planning: docs/sdlc/sdlc-002_20260423_service-token-user-based/2_planning.md
- 작성 시각(KST): 2026-04-23
- 마지막 갱신 시각(KST): 2026-04-23 (confirm-design 실행 — Design 최종 승인, Implementation 진입 허가)
1. Planning 인계 요약
Planning에서 확정된 핵심 내용을 요약한다. 원본은 2_planning.md를 참조하며, 이 섹션은 Design 작업의 입력 기준선이 된다.
이슈 목적
bot_principals 테이블과 auth_audit_logs 테이블을 제거하고, service token을 auth_tokens 단일 엔티티로 직접 발급하도록 재구현한다. CLI 명령 pfhub service-token issue <name> --user <username>으로 사용자 기반 소유권을 명확히 한다.
확정 태스크 목록
| ID | 태스크 제목 | 참조 요구사항 | 담당자 |
|---|---|---|---|
| T-1 | DB 마이그레이션 파일 작성 | FR-3, NFR-2 | copilot |
| T-2 | 도메인/스키마/인터페이스 정리 | FR-1 | copilot |
| T-3 | postgres.py 구현체 정리 | FR-1, FR-4 | copilot |
| T-4 | AuthContext/security.py 변경 | FR-1 (OQ-4) | copilot |
| T-5 | AuthService 재구현 | FR-2, FR-5, FR-6, FR-7 | copilot |
| T-6 | API 경로 정리 | FR-1 | copilot |
| T-7 | CLI 재구현 | FR-5, FR-6, FR-7, NFR-3 | copilot |
| T-8 | 테스트 업데이트 | NFR-1 | copilot |
| T-9 | 기술 문서 업데이트 | - | copilot |
이월된 오픈 질문 / 리스크
없음. Planning 단계에서 모든 OQ 해소 완료.
2. 아키텍처 및 구조 설계
변경이 미치는 전체 구조와 주요 컴포넌트 관계를 설명한다.
전체 구조 개요
시스템은 아래 5개 레이어로 구성되며, 이번 변경은 모든 레이어에 걸친 bot_principal 엔티티 제거와 service token 직접 발급 모델로의 전환이다.
CLI (app/cli/)
↓ service_tokens.py (bot_tokens.py 대체)
Service (app/services/auth_service.py)
↓ issue_service_token() / revoke_service_token()
Repository (app/repositories/postgres.py)
↓ auth_tokens 직접 INSERT, bot_principals/auth_audit_logs 테이블 미사용
DB (migrations/versions/)
auth_tokens (user_id = 실제 user.id, token_type = 'service')
[제거] bot_principals, auth_audit_logs
변경 전: auth_tokens.user_id ← bot_principal.id (봇 자체 UUID, JOIN으로 소유자 추적)
변경 후: auth_tokens.user_id ← users.id (실제 소유 사용자 직접 참조)
주요 컴포넌트
| 컴포넌트 | 역할 | 영향 범위 |
|---|---|---|
app/domain/models.py |
도메인 모델 정의 | BotPrincipal 삭제, AuthToken.bot_principal_id 삭제, TokenPrincipal.bot_principal_id 삭제, token_type Literal 변경 |
app/schemas/auth.py |
API/CLI 요청·응답 스키마 | bot_principal 관련 스키마 삭제, AuthTokenResponse.token_type 갱신, ServiceTokenIssueCliResponse / ServiceTokenRevokeCliResponse 추가 |
app/repositories/portfolio_repository.py |
Repository Protocol 인터페이스 | bot_principal 관련 메서드 삭제, get_service_token_by_name() 추가 |
app/repositories/postgres.py |
DB 접근 구현체 | ensure_schema() bot 관련 DDL 제거, get_token_principal() JOIN 단순화, bot/audit 메서드 삭제, get_service_token_by_name() 추가 |
app/core/auth.py |
인증 컨텍스트 해석 | AuthContext.bot_principal_id 삭제, AuthContext.token_type 추가 |
app/core/security.py |
암호화·토큰 유틸 | build_audit_event() 삭제 |
app/services/auth_service.py |
인증 비즈니스 로직 | bot 관련 메서드 삭제, issue_service_token() / revoke_service_token() 추가, 가드 교체 |
app/api/auth_routes.py |
FastAPI 라우터 | /auth/bots, /auth/me/bots 경로 삭제 |
app/cli/commands/bot_tokens.py |
CLI 명령 (구 파일) | app/cli/commands/service_tokens.py로 교체 |
app/cli/commands/service_tokens.py |
CLI 명령 (신규 파일) | pfhub service-token issue <name> --user <username> / revoke <name> |
app/cli/core/output.py |
CLI 출력 헬퍼 | render_bot_result() → render_service_token_result()로 rename |
app/cli/main.py |
CLI 진입점 | import 경로 변경 (bot_tokens → service_tokens) |
migrations/versions/ |
DB 마이그레이션 | 20260423_0013_remove_bot_principals_service_tokens.py 신규 생성 |
데이터 흐름
service token 발급 흐름 (변경 후)
- CLI:
pfhub service-token issue <name> --user <username>실행 - CLI:
AuthService.issue_service_token(name=name, username=username, ...)호출 - Service:
repository.get_user_by_username(username)→ user 조회 (없으면 ValueError) - Service:
repository.create_auth_token(user.id, name=name, token_type="service", ...)실행- name uniqueness는 DB
partial unique index ON auth_tokens(name) WHERE token_type='service'로 보장
- name uniqueness는 DB
- Service:
IssuedServiceToken(token=raw_token, auth_token=auth_token)반환 - CLI: 결과 출력 (ServiceTokenIssueCliResponse)
service token 인증 흐름 (변경 후)
- HTTP 요청:
Authorization: Bearer <service_token>헤더 포함 _resolve_auth_context():repository.get_token_principal(hash_token(token))호출get_token_principal(): 단순SELECT id, user_id, permission, token_type FROM auth_tokens WHERE token_hash = %s AND ...(JOIN 없음)AuthContext(token_type="service", user_id=<실제 user_id>, ...)생성- service token 가드:
auth_context.token_type == "service"→ 토큰 발급/목록/폐기 조작 차단
3. 상세 설계
태스크(T-N)별로 구체적인 구현/변경 방법을 기술한다.
T-1: DB 마이그레이션 파일 작성
- 설계 방향: Alembic 마이그레이션으로 DB 구조를 변경한다. upgrade 전 사전 검증 쿼리를 포함하여 NULL owner_user_id로 인한 FK 위반을 사전 차단한다.
- 변경 대상:
migrations/versions/20260423_0013_remove_bot_principals_service_tokens.py(신규 생성) - 입력/출력:
- upgrade() 실행 순서:
SELECT COUNT(*) FROM bot_principals WHERE owner_user_id IS NULL→ 0이 아니면RuntimeError로 중단UPDATE auth_tokens t SET user_id = b.owner_user_id FROM bot_principals b WHERE t.bot_principal_id = b.id AND t.bot_principal_id IS NOT NULL— user_id 보정UPDATE auth_tokens SET token_type = 'service' WHERE token_type = 'bot'— token_type renameCREATE UNIQUE INDEX IF NOT EXISTS idx_auth_tokens_service_name_unique ON auth_tokens(name) WHERE token_type = 'service'— service token name 유니크 제약ALTER TABLE auth_tokens DROP COLUMN IF EXISTS bot_principal_idDROP TABLE IF EXISTS auth_audit_logsDROP TABLE IF EXISTS bot_principals
- downgrade() 실행 순서 (역방향 복원):
CREATE TABLE bot_principals (id TEXT PRIMARY KEY, name TEXT NOT NULL, owner_user_id TEXT, permission TEXT NOT NULL, description TEXT, is_active BOOLEAN DEFAULT TRUE, created_at TIMESTAMPTZ, updated_at TIMESTAMPTZ, deactivated_at TIMESTAMPTZ)CREATE TABLE auth_audit_logs (id TEXT PRIMARY KEY, event_type TEXT NOT NULL, action TEXT NOT NULL, actor_id TEXT, bot_id TEXT NOT NULL, token_id_masked TEXT, token_ref_masked TEXT, result TEXT, detail TEXT, payload JSONB, occurred_at TIMESTAMPTZ NOT NULL)ALTER TABLE auth_tokens ADD COLUMN IF NOT EXISTS bot_principal_id TEXTUPDATE auth_tokens SET token_type = 'bot' WHERE token_type = 'service'DROP INDEX IF EXISTS idx_auth_tokens_service_name_unique- (user_id 역방향 보정 불가 — bot_principals 데이터 없음. downgrade는 구조 복원만 수행하며 데이터 보정은 생략)
- upgrade() 실행 순서:
- 예외/엣지 케이스: upgrade 전 bot_principals.owner_user_id NULL 검사가 실패하면 마이그레이션 중단. 이 경우 운영자가 수동으로 NULL 행을 보정해야 한다.
T-2: 도메인/스키마/인터페이스 정리
- 설계 방향: bot_principal 관련 클래스·필드를 일괄 삭제하고 service token용 CLI 응답 스키마를 추가한다.
- 변경 대상:
app/domain/models.py:BotPrincipal데이터클래스 전체 삭제AuthToken.bot_principal_id: str | None필드 삭제TokenPrincipal.bot_principal_id: str | None필드 삭제AuthToken.token_type타입 힌트:str유지 (Literal 미사용 — 기존과 동일)
app/schemas/auth.py:- 삭제 대상:
CreateBotPrincipalRequest,CreateSelfBotPrincipalRequest,BotPrincipalResponse,CreateBotTokenRequest,IssuedBotTokenResponse,BotTokenIssueCliResponse,BotTokenRevokeCliResponse - 변경 대상:
AuthTokenResponse.token_typeLiteral —"bot"제거,"service"추가 (Literal["session", "client", "service"]) - 변경 대상:
AuthTokenResponse.bot_principal_id: str | None필드 삭제 - 추가 대상:
ServiceTokenIssueCliResponse(token_id: str, name: str, expires_at: datetime | None, revoked: bool, token: str | None),ServiceTokenRevokeCliResponse(token_id: str, name: str, revoked: bool, revoked_at: datetime | None)
- 삭제 대상:
app/repositories/portfolio_repository.py:- 삭제 대상 메서드:
create_bot_principal,get_bot_principal,list_bot_principals,deactivate_bot_principal,list_bot_auth_tokens,revoke_bot_auth_token create_auth_token()시그니처에서bot_principal_id: str | None = None파라미터 삭제- 추가 대상 메서드:
get_service_token_by_name(name: str) -> AuthToken | None BotPrincipalimport 삭제
- 삭제 대상 메서드:
- 입력/출력:
python -c "from app.domain.models import AuthToken"실행 시 ImportError 없음
T-3: postgres.py 구현체 정리
- 설계 방향: ensure_schema()에서 bot 관련 DDL을 제거하고, get_token_principal()의 JOIN을 단순화한다. bot/audit 관련 메서드를 삭제하고 get_service_token_by_name()을 추가한다.
- 변경 대상:
app/repositories/postgres.py_SELECT_BOT_PRINCIPAL쿼리 상수 삭제_SELECT_AUTH_TOKEN쿼리에서bot_principal_id컬럼 삭제ensure_schema()내 제거 대상:CREATE TABLE IF NOT EXISTS bot_principals (...)블록ALTER TABLE auth_tokens ADD COLUMN IF NOT EXISTS bot_principal_id TEXTALTER TABLE bot_principals ...블록 전체idx_auth_tokens_bot_created인덱스 생성 구문idx_bot_principals_*인덱스 생성 구문CREATE TABLE IF NOT EXISTS auth_audit_logs (...)블록idx_auth_audit_logs_bot_occurred인덱스 생성 구문
get_token_principal()변경:- 변경 전:
SELECT t.id, COALESCE(b.owner_user_id, t.user_id), t.permission, t.token_type, t.bot_principal_id FROM auth_tokens t LEFT JOIN bot_principals b ... - 변경 후:
SELECT id, user_id, permission, token_type FROM auth_tokens WHERE token_hash = %s AND revoked_at IS NULL AND (expires_at IS NULL OR expires_at > NOW()) TokenPrincipal생성 시bot_principal_id인자 제거
- 변경 전:
create_auth_token()시그니처에서bot_principal_id파라미터 및 INSERT 컬럼 제거- 삭제 대상 메서드:
create_bot_principal,get_bot_principal,list_bot_principals,deactivate_bot_principal,list_bot_auth_tokens,revoke_bot_auth_token,record_audit_event - 추가 대상 메서드:
def get_service_token_by_name(self, name: str) -> AuthToken | None: # SELECT * FROM auth_tokens WHERE name = %s AND token_type = 'service' AND revoked_at IS NULL # → AuthToken 또는 None 반환
- 입력/출력:
ensure_schema()재실행 시bot_principals,auth_audit_logs테이블 미생성.get_token_principal()반환값의bot_principal_id없음.
T-4: AuthContext/security.py 변경
- 설계 방향:
AuthContext에token_type필드를 추가하고bot_principal_id를 제거한다.security.py에서build_audit_event()함수를 삭제한다. - 변경 대상:
app/core/auth.py:AuthContext데이터클래스:bot_principal_id: str | None = None삭제,token_type: str | None = None추가_resolve_auth_context():bot_principal_id=principal.bot_principal_id제거,token_type=principal.token_type추가 (env 토큰은token_type=None유지)
app/core/security.py:build_audit_event()함수 삭제
- 입력/출력:
- env 토큰:
AuthContext(token=..., permission=..., token_source="env", token_type=None) - DB 세션 토큰:
AuthContext(token=..., permission=..., user_id=..., token_id=..., token_source="db", token_type="session") - DB service 토큰:
AuthContext(token=..., permission=..., user_id=..., token_id=..., token_source="db", token_type="service")
- env 토큰:
T-5: AuthService 재구현
- 설계 방향: bot 관련 메서드를 모두 삭제하고
issue_service_token()/revoke_service_token()을 추가한다.bot_principal_id가드를token_type == "service"가드로 교체한다. - 변경 대상:
app/services/auth_service.py- 가드 교체 (3개소):
issue_client_token():if auth_context.bot_principal_id is not None→if auth_context.token_type == "service"list_tokens(): 동일revoke_token(): 동일
- 삭제 대상 메서드:
create_bot_principal,create_owned_bot_principal,list_bot_principals,list_owned_bot_principals,get_bot_principal,deactivate_bot_principal,issue_bot_token,issue_owned_bot_token,list_bot_tokens,list_owned_bot_tokens,revoke_bot_token,revoke_owned_bot_token,_record_audit_event,_build_bot_expiration,_require_db_user_context_require_db_user_context()노트: 모든 호출자가 삭제되므로 함께 삭제 (review-design에서 가드 교체 대상에서 삭제 대상으로 재분류)
- 삭제 대상 import:
build_audit_event,mask_secret,BotPrincipal,CreateBotPrincipalRequest,CreateBotTokenRequest,CreateSelfBotPrincipalRequest IssuedBotToken데이터클래스 →IssuedServiceToken(token: str, auth_token: AuthToken)으로 교체- 추가 대상 메서드:
def issue_service_token( self, name: str, username: str, *, permission: str | None = None, expires_in_days: int | None = None, no_expiry: bool = False, ) -> IssuedServiceToken: user = self._repository.get_user_by_username(username) if user is None: raise ValueError(f"사용자 '{username}'을 찾을 수 없습니다") requested = Permission(permission or user.permission) raw_token = generate_token() expires_at = self._build_expiration( expires_in_days if not no_expiry else None, self._settings.client_token_default_ttl_days if not no_expiry else None, ) auth_token = self._repository.create_auth_token( user.id, name=name, token_hash=hash_token(raw_token), token_prefix=token_prefix(raw_token), permission=requested.value, token_type="service", expires_at=expires_at, ) return IssuedServiceToken(token=raw_token, auth_token=auth_token) def revoke_service_token(self, name: str) -> datetime: token = self._repository.get_service_token_by_name(name) if token is None: raise ValueError(f"service token '{name}'을 찾을 수 없습니다") self._repository.revoke_auth_token(token.user_id, token.id) return datetime.now(timezone.utc)
- 가드 교체 (3개소):
- 예외/엣지 케이스:
issue_service_token():name중복 시 DBUNIQUE constraint위반 → psycopgUniqueViolation예외 →ValueError로 래핑revoke_service_token(): 이미 폐기된 토큰도get_service_token_by_name()에서revoked_at IS NOT NULL필터 → None 반환 → ValueError 발생 (명시적 오류)
T-6: API 경로 정리
- 설계 방향: bot_principal 관련 엔드포인트와 핸들러, 헬퍼 함수를 모두 삭제한다.
- 변경 대상:
app/api/auth_routes.py- 삭제 대상 라우트:
POST /auth/bots(create_bot_principal)POST /auth/me/bots(create_owned_bot_principal)GET /auth/me/bots(list_owned_bot_principals)POST /auth/me/bots/{bot_id}/tokens(issue_owned_bot_token)GET /auth/me/bots/{bot_id}/tokens(list_owned_bot_tokens)DELETE /auth/me/bots/{bot_id}/tokens/{token_id}(revoke_owned_bot_token)DELETE /auth/bots/{bot_id}또는 관련 bot deactivate 라우트 (존재 시)
- 삭제 대상 헬퍼 함수:
_to_bot_principal_response(),_to_issued_bot_token_response() - 삭제 대상 import:
BotPrincipal,BotPrincipalResponse,CreateBotPrincipalRequest,CreateSelfBotPrincipalRequest,CreateBotTokenRequest,IssuedBotTokenResponse,IssuedBotToken _to_auth_token_response():bot_principal_id=token.bot_principal_id인자 삭제
- 삭제 대상 라우트:
- 입력/출력: FastAPI 기동 후
/openapi.json에/auth/bots,/auth/me/bots경로 미노출
T-7: CLI 재구현
- 설계 방향:
bot_tokens.py를service_tokens.py로 교체하고, CLI 인수를 bot_id 기반에서 name + --user 기반으로 변경한다. - 변경 대상:
- 신규 파일:
app/cli/commands/service_tokens.pytoken_app = typer.Typer(help="Service token lifecycle commands.") @token_app.command("issue") def issue_service_token( ctx: typer.Context, name: str = typer.Argument(..., help="Service token name (unique identifier)."), username: str = typer.Option(..., "--user", help="Username to assign the token to."), permission: str | None = typer.Option(None, help="Token permission: read or trade-write."), ttl_days: int | None = typer.Option(None, "--ttl-days", help="Token TTL in days."), no_expiry: bool = typer.Option(False, "--no-expiry", help="Issue token with no expiry."), force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation prompt."), output_format: str = typer.Option("table", "--format"), ) -> None: ... @token_app.command("revoke") def revoke_service_token( ctx: typer.Context, name: str = typer.Argument(..., help="Service token name to revoke."), force: bool = typer.Option(False, "--force", "-f"), output_format: str = typer.Option("table", "--format"), ) -> None: ... - 구 파일
app/cli/commands/bot_tokens.py삭제 app/cli/main.py:from app.cli.commands.bot_tokens import token_app→from app.cli.commands.service_tokens import token_appapp/cli/core/output.py:render_bot_result()→render_service_token_result()rename- 응답 스키마:
BotTokenIssueCliResponse→ServiceTokenIssueCliResponse,BotTokenRevokeCliResponse→ServiceTokenRevokeCliResponse
- 신규 파일:
- 입력/출력:
pfhub service-token issue --help에서NAMEpositional,--user옵션 노출. help 텍스트에 "bot" 문자열 없음.
T-8: 테스트 업데이트
- 설계 방향: bot_principal 참조를 제거하고 service token 시나리오 테스트를 추가한다.
- 변경 대상:
tests/test_auth.py:create_bot_principal,issue_bot_token,revoke_bot_token관련 테스트 함수 삭제issue_service_token()단위 테스트 추가:- 정상 발급 (username 존재, name 신규)
- 존재하지 않는 username → ValueError
- 중복 name → UniqueViolation → ValueError
revoke_service_token()단위 테스트 추가- service token 가드 테스트:
token_type == "service"인 AuthContext로issue_client_token()호출 시 ValueError 확인
tests/test_cli.py:service-token issue <name> --user <username>명령 통합 테스트 추가service-token revoke <name>명령 통합 테스트 추가- bot-token CLI 관련 테스트 삭제 또는 service-token으로 업데이트
tests/test_users.py: bot_principal 참조 제거 (있는 경우)tests/test_portfolio_ownership.py:bot_principal_id기반 mock →token_type="service"기반 mock으로 교체
T-9: 기술 문서 업데이트
- 설계 방향: 4개 기술 문서에서 bot_principal 참조를 제거하고 service token 기반 설명으로 갱신한다.
- 변경 대상:
docs/technical/auth-and-token-lifecycle.md: bot → service 전반 업데이트,AuthContext.token_type설명 추가, bot_principals/auth_audit_logs 삭제 반영docs/technical/api-contracts.md:/auth/bots,/auth/me/bots경로 삭제, 변경 후 token_type 값 갱신docs/technical/management-cli-operations.md:pfhub service-token issue <name> --user <username>명령 예제로 교체, bot-token 관련 내용 삭제README.md: CLI 섹션에서 bot-token 예제 삭제, service-token 예제 추가
4. 인터페이스 정의
외부 시스템, 모듈 간 계약(API, 이벤트, 파일 포맷 등)을 명시한다.
API / 함수 시그니처
AuthService 신규 메서드:
def issue_service_token(
self,
name: str, # auth_tokens.name으로 저장되는 서비스 식별자 (service token 범위 내 유니크)
username: str, # users.username → user_id 조회 후 auth_tokens.user_id로 저장
*,
permission: str | None = None, # None이면 user.permission 상속
expires_in_days: int | None = None,
no_expiry: bool = False,
) -> IssuedServiceToken: ...
def revoke_service_token(
self,
name: str, # service token 이름으로 조회 후 폐기
) -> datetime: ... # revoked_at 반환
PortfolioRepository Protocol 신규 메서드:
def get_service_token_by_name(self, name: str) -> AuthToken | None: ...
# SELECT * FROM auth_tokens WHERE name = %s AND token_type = 'service' AND revoked_at IS NULL
AuthContext 변경 후 필드:
@dataclass(frozen=True)
class AuthContext:
token: str
permission: Permission
user_id: str | None = None
token_id: str | None = None
token_type: str | None = None # "session" | "client" | "service" | None (env 토큰)
token_source: str = "env" # "env" | "db"
데이터 스키마 / 포맷
ServiceTokenIssueCliResponse (새 스키마):
class ServiceTokenIssueCliResponse(BaseModel):
token_id: str
name: str
expires_at: datetime | None = None
revoked: bool
token: str | None = None # 발급 시 일회 노출, 이후 마스킹
ServiceTokenRevokeCliResponse (새 스키마):
class ServiceTokenRevokeCliResponse(BaseModel):
token_id: str
name: str
revoked: bool
revoked_at: datetime | None = None
AuthTokenResponse 변경:
token_type:Literal["session", "client", "bot"]→Literal["session", "client", "service"]bot_principal_id: str | None필드 삭제
이벤트 / 메시지
해당 없음. audit log 테이블 자체 삭제로 이벤트 기록 없음.
5. 보안 고려사항
OWASP Top 10 기준으로 이번 변경에서 주의해야 할 보안 항목을 점검한다.
| 항목 | 해당 여부 | 대응 방안 |
|---|---|---|
| 입력값 검증 (Injection) | 해당 | --user <username> 값은 get_user_by_username()을 통해 파라미터 바인딩으로 DB 조회. SQL injection 없음. name 값도 파라미터 바인딩 사용. |
| 인증/인가 | 해당 | service token은 token_type == "service" 가드로 토큰 발급/목록/폐기 조작 차단. 인가 단계에서 AuthContext.token_type 필드를 명시적으로 확인. env 토큰(token_type=None)은 가드 미적용 — admin env 토큰은 해당 조작 허용 (기존 설계 유지). |
| 민감 데이터 노출 | 해당 | 발급된 raw token은 CLI에서 발급 시 1회만 출력. 저장 시 SHA-256 해시(token_hash)만 DB에 저장. mask_secret() 함수로 출력 시 마스킹 유지. audit log 삭제로 raw token_ref 잔류 위험 제거. |
| 의존성 취약점 | 비해당 | 이번 변경은 새 의존성 추가 없음. 기존 psycopg, fastapi, typer 의존성 유지. |
| 기타 (권한 상승) | 해당 | issue_service_token() 시 permission은 user.permission 상한으로 제한 (_has_permission() 체크). 지정한 user의 권한을 초과하는 service token 발급 차단. |
6. 리스크
Design 단계에서 새로 식별된 리스크를 기록한다.
리스크
- R-1 (High, 이월): 기존
auth_tokens에user_id = bot_principal.id인 행의bot_principal.owner_user_id가 NULL이거나users테이블에 없으면 마이그레이션 FK 위반 발생.- 완화 방안: T-1 upgrade() 첫 번째 스텝에서 NULL 검사 쿼리 실행 후 0이 아니면
RuntimeError로 중단. 운영 전 사전 검증 권장.
- 완화 방안: T-1 upgrade() 첫 번째 스텝에서 NULL 검사 쿼리 실행 후 0이 아니면
- R-2 (Medium, 이월): 약 15개 파일 rename 중 일부 누락 시 런타임
AttributeError/NameError.- 완화 방안: T-8 완료 후
grep -r "bot_principal\|BotPrincipal\|bot_token\|token_type.*bot\|render_bot_result"실행으로 잔류 참조 검사.
- 완화 방안: T-8 완료 후
- R-3 (Medium):
revoke_service_token(name)구현 시get_service_token_by_name()이revoked_at IS NOT NULL필터를 포함하면 이미 폐기된 토큰 revoke 시도 시 "찾을 수 없음" 오류 발생.- 완화 방안: CLI에서 적절한 오류 메시지 출력. 설계상 의도된 동작.
- R-4 (Low):
ensure_schema()DDL 제거 후 기존 테스트 환경이 이전 스키마를 캐시하는 경우 bot_principals 테이블 잔류 가능.- 완화 방안: T-3 DoD에서
ensure_schema()재실행 후 테이블 미존재 확인 포함.
- 완화 방안: T-3 DoD에서
가정
- DA-1:
app/cli/main.py에서name="service-token"등록이 이미 완료되어 있으므로 T-7에서 import 경로(bot_tokens→service_tokens)만 변경하면 된다. - DA-2:
get_user_by_username()메서드가 postgres.py에 이미 구현되어 있다 (auth_service.py의login()메서드에서 사용 중으로 확인됨). - DA-3:
revoke_auth_token(user_id, token_id)메서드가 postgres.py에 구현되어 있어revoke_service_token()내부에서 재사용 가능하다. - DA-4: auth_routes.py에서 삭제 대상 이외의 라우트(
/auth/login,/auth/tokens,/auth/tokens/{id})는 변경 없이 유지된다.
오픈 질문
없음. 모든 설계 결정 완료.
7. Implementation으로 넘기기 전 확인 체크
Implementation 단계로 넘기기 전에 무엇이 확정되어야 하는지 확인한다.
- 전체 컴포넌트 및 태스크별 설계 완성
- 기준: T-1 ~ T-9 모든 태스크에 설계 방향, 변경 대상, 입출력이 명시되어야 한다
- 인터페이스 계약 확정
- 기준:
issue_service_token(),revoke_service_token(),get_service_token_by_name(),AuthContext.token_type시그니처가 명시되어야 한다
- 기준:
- 보안 점검 완료
- 기준: OWASP 점검 항목 중 해당 항목(입력값 검증, 인증/인가, 민감 데이터 노출, 권한 상승)에 대응 방안이 있어야 한다
- 모든 오픈 질문(OQ)
Open상태 없음 확인- 기준: OQ 목록에
Open상태 항목이 0개 — 확인 완료
- 기준: OQ 목록에
- Freeze 확정 담당자(CONFIRMED_BY) 지정
- 기준: 문서 승인 또는 인계 결정을 내릴 담당자가 명시되어야 한다
8. 파일 처리 결과
이 Design 문서 생성/갱신 결과와 참조 문서를 기록한다.
- 처리 결과: 생성
- 생성 경로: docs/sdlc/sdlc-002_20260423_service-token-user-based/3_design.md
- 참조 문서:
- 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 (line 821: get_token_principal, line 124: ensure_schema)
- 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
- app/cli/main.py
- migrations/versions/20260414_0011_bot_principal_owner_user_id.py (최신 마이그레이션)
9. 사용자 결정 필요 항목 요약
사용자나 승인자가 결정해야 하는 항목만 모아 정리한다.
DECIDE
없음. 모든 설계 결정이 Discovery/Planning에서 완료되었다.
CONFIRM
- Freeze 확정 담당자(
CONFIRMED_BY): Justin — confirm-design 단계에서 처리
DATA
- 마이그레이션 실행 전 운영 DB
bot_principals행 수 및owner_user_id NULL행 수 확인 필요 (R-1 완화). T-1 upgrade() 내 사전 검증 쿼리가 이를 자동으로 수행하지만, 사전 확인 권장.
10. Design Freeze
Implementation 인계 시점의 확정 플래그와 섹션 참조만 기록한다.
섹션 참조
- 아키텍처/구조: # 2. 아키텍처 및 구조 설계
- 상세 설계: # 3. 상세 설계
- 인터페이스: # 4. 인터페이스 정의
- 보안: # 5. 보안 고려사항
- 리스크/오픈 질문: # 6. 리스크
- 인계 전 확인: # 7. Implementation으로 넘기기 전 확인 체크
- 사용자 결정 필요: # 9. 사용자 결정 필요 항목 요약
Freeze 플래그
- Handoff Decision: 승인
- Handoff Rationale: T-1~T-9 전체 상세 설계 완성, 주요 인터페이스(
issue_service_token,revoke_service_token,get_service_token_by_name,AuthContext.token_type) 확정, OWASP 해당 4개 항목 대응 방안 명시, OQ 없음, 저장소 현황 대조 이상 없음. - Ready for Implementation: true
- Confirmed By: Justin
- Confirmed At (KST): 2026-04-23