- 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
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/daemonsGET이 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 DaemonAdminResponsePOST /v1/admin/daemons/{daemon_id}/activate→ 200 DaemonAdminResponseDELETE /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
-
소유 스코프 게이트 위치: 라우터 내에서 daemon 조회 후
daemon.owner_user_id != auth.user_id즉시 403. 서비스 레이어 분리 없이 라우터에서 직접 처리(기존 v1_portfolios.py 패턴 동일). -
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 발생). -
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만 담당. -
owner 재할당 + credential 무효화 (REQ-053 AC-4):
update_daemon_owner()에서 먼저 daemon의 owner를 업데이트하고 이후DELETE FROM broker_credentials WHERE daemon_id = %s호출. autocommit 모드이므로 두 연산이 별도. 인증정보 삭제 실패는 로그 경고 후 무시(데몬 업데이트는 이미 완료). -
DELETE 405 (REQ-053 AC-8):
@router.delete("/v1/admin/daemons/{daemon_id}")핸들러를 명시 등록해 HTTP 405를 반환. FastAPI 기본은 미등록 DELETE → 405 (Method Not Allowed)이나 명시 등록이 더 안전. -
slug 중복 → 409 (REQ-053 AC-2): postgres.py create_daemon에서 UniqueViolation → ValueError 변환. 라우터에서 ValueError 메시지 패턴 매칭으로 409 반환.
-
DELETE credential 204 (REQ-052 AC-5): 인증정보가 없어도 204 반환(멱등). 있으면 삭제 후 204.
-
브로커 경로 단순화: 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 — 신규