Files
portfolio-hub/docs/batches/bat-022_20260628_daemon-crud-self-service-credential-api/design.md
T
genichinandClaude Sonnet 4.6 d24058d863 feat(bat-022): daemon CRUD admin API + self-service credential management (REQ-052/053/054)
- POST/PATCH/deactivate/activate /v1/admin/daemons — admin-only daemon CRUD (REQ-053)
- GET/PUT/DELETE /v1/daemons/{daemon_id}/credential — user-owned scoped credential management (REQ-052)
- Owner re-assignment auto-invalidates BrokerCredential via update_daemon_owner() (REQ-053 AC-4)
- Deactivation auto-revokes service tokens via revoke_tokens_by_daemon_id() (REQ-022 reuse, REQ-053 AC-6)
- DELETE /v1/admin/daemons/{id} → 405 (hard delete not provided, REQ-053 AC-8)
- All responses exclude app_key/app_secret; Fernet encryption at rest (REQ-054)
- Service-token → 403 on credential endpoints (REQ-052 AC-10)
- Admin ownership scope enforced: admin cannot write other user's credential (REQ-052 AC-9)
- REQ-051 regression: 4 service-token guard tests pass (REQ-054 AC-7)
- 44 new tests, 565 total passed (521 baseline + 44 new), 0 failures

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VHtM5Ws1HKCKQ4JGi6GRb5
2026-06-28 14:18:37 +09:00

5.8 KiB

BAT-022 Design

  • Status: confirmed
  • Batch: BAT-022

OQ-1 해소: 인증정보 관리 엔드포인트 경로 (REQ-052)

확정: /v1/daemons/{daemon_id}/credential

근거:

  • v1 컨벤션: /v1/daemons/{daemon_id}가 daemon 리소스 계층 루트. credential은 daemon의 하위 리소스이므로 중첩 경로가 REST 의미에 맞는다.
  • 소유 스코프 자연성: daemon_id가 경로에 있어 소유 검증(daemon.owner_user_id == auth.user_id) 경로 파라미터에서 바로 가능.
  • dcy-009 UI 흐름: 사용자가 자기 데몬을 선택한 뒤 인증정보를 관리하는 흐름과 일치.
  • 기존 /v1/daemons GET이 daemon 계층을 쓰므로 일관성 유지.

메서드:

  • GET /v1/daemons/{daemon_id}/credential — 메타데이터 조회 (has_credential, version, last_rotated_at)
  • PUT /v1/daemons/{daemon_id}/credential — 등록/회전 (upsert, 응답 메타데이터만)
  • DELETE /v1/daemons/{daemon_id}/credential — 삭제 (204)

브로커 지정: PUT 요청 body에 broker 필드(기본값 "kis"). 현재 단일 broker 가정이므로 경로에 broker를 두지 않는다. 다중 broker 지원 필요 시 /v1/daemons/{daemon_id}/credentials/{broker} 로 확장 경로로 분리 가능(하위 호환).

OQ-2 해소: admin 데몬 CRUD 경로 (REQ-053)

확정: /v1/admin/daemons

근거:

  • dcy-006 패턴: /v1/admin/... prefix를 admin 전용 관리 API의 네임스페이스로 일관되게 사용한다. dcy-006에서 ledger 회계 통합 경로는 /v1/portfolios/...를 사용했으나, admin 전용 mutate는 다른 서비스(users.py 등)도 /users에 admin 게이트를 두는 방식. 여기서 /v1/daemons 기존 경로에 POST/PATCH를 추가하면 GET(caller-scoped, REQ-045)과 POST(admin-only, REQ-053)가 같은 경로에 혼재되어 권한 경계가 불명확해진다.
  • /v1/admin/daemons는 admin 전용 write 작업임을 경로 자체로 명시.
  • trex-web dcy-009 소비자가 admin 경로를 별도 컨텍스트에서 관리하기 쉽다.

메서드:

  • POST /v1/admin/daemons → 201 DaemonAdminResponse (생성)
  • PATCH /v1/admin/daemons/{daemon_id} → 200 DaemonAdminResponse (display_name 변경, owner 재할당)
  • POST /v1/admin/daemons/{daemon_id}/deactivate → 200 DaemonAdminResponse
  • POST /v1/admin/daemons/{daemon_id}/activate → 200 DaemonAdminResponse
  • DELETE /v1/admin/daemons/{daemon_id} → 405 Method Not Allowed (명시적 미제공)

Architecture Summary

신규 파일

파일 역할
app/api/v1_admin_daemons.py REQ-053 admin daemon CRUD 라우터
app/api/v1_daemon_credential.py REQ-052 소유 스코프 인증정보 관리 라우터

변경 파일

파일 변경 내용
app/schemas/v1.py REQ-052 응답/요청 스키마 + REQ-053 응답/요청 스키마 추가
app/repositories/postgres.py get_broker_credential_by_daemon, delete_credentials_by_daemon_id, update_daemon_owner 추가
app/repositories/portfolio_repository.py Protocol에 신규 메서드 추가
app/main.py 신규 라우터 include

신규 테스트 파일

파일 커버
tests/test_bat022_req052_credential_api.py REQ-052 AC-1~10
tests/test_bat022_req053_admin_daemon_crud.py REQ-053 AC-1~12
tests/test_bat022_req054_security.py REQ-054 AC-1~7

Key Decisions

  1. 소유 스코프 게이트 위치: 라우터 내에서 daemon 조회 후 daemon.owner_user_id != auth.user_id 즉시 403. 서비스 레이어 분리 없이 라우터에서 직접 처리(기존 v1_portfolios.py 패턴 동일).

  2. service-token 403 (REQ-052 AC-10): auth.token_source == "db" and auth.token_type == "service" 이면 403. env-token(token_source="env")은 token_type이 None이므로 이 조건에 해당하지 않는다. env admin token으로 credential 관리는 허용하지 않는다(user_id가 없으므로 소유 스코프 검사에서 403 발생).

  3. admin도 타인 credential write 403 (REQ-052 AC-9): admin role이라도 auth.user_id != daemon.owner_user_id이면 403. 인증정보 소유 스코프는 admin role을 우선시하지 않는다. admin은 REQ-053(/v1/admin/daemons)으로 데몬 provisioning만 담당.

  4. owner 재할당 + credential 무효화 (REQ-053 AC-4): update_daemon_owner()에서 먼저 daemon의 owner를 업데이트하고 이후 DELETE FROM broker_credentials WHERE daemon_id = %s 호출. autocommit 모드이므로 두 연산이 별도. 인증정보 삭제 실패는 로그 경고 후 무시(데몬 업데이트는 이미 완료).

  5. DELETE 405 (REQ-053 AC-8): @router.delete("/v1/admin/daemons/{daemon_id}") 핸들러를 명시 등록해 HTTP 405를 반환. FastAPI 기본은 미등록 DELETE → 405 (Method Not Allowed)이나 명시 등록이 더 안전.

  6. slug 중복 → 409 (REQ-053 AC-2): postgres.py create_daemon에서 UniqueViolation → ValueError 변환. 라우터에서 ValueError 메시지 패턴 매칭으로 409 반환.

  7. DELETE credential 204 (REQ-052 AC-5): 인증정보가 없어도 204 반환(멱등). 있으면 삭제 후 204.

  8. 브로커 경로 단순화: PUT body에 broker: str = "kis". GET/DELETE는 기본 broker(현재 단일 broker 가정).

Changed Areas

  • app/api/v1_admin_daemons.py — 신규
  • app/api/v1_daemon_credential.py — 신규
  • app/schemas/v1.py — CredentialSetRequest, CredentialMetaResponse, DaemonCreateRequest, DaemonUpdateRequest, DaemonAdminResponse 추가
  • app/repositories/postgres.py — get_broker_credential_by_daemon, delete_credentials_by_daemon_id, update_daemon_owner 추가
  • app/repositories/portfolio_repository.py — Protocol 동기화
  • app/main.py — 라우터 include
  • tests/test_bat022_req052_credential_api.py — 신규
  • tests/test_bat022_req053_admin_daemon_crud.py — 신규
  • tests/test_bat022_req054_security.py — 신규