- 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.6 KiB
5.6 KiB
BAT-022 Planning
구현 의존 순서
- REQ-052/053 스키마 —
app/schemas/v1.py에CredentialSetRequest,CredentialMetaResponse,DaemonCreateRequest,DaemonUpdateRequest,DaemonAdminResponse추가 - REQ-053 repository 확장 —
postgres.py에update_daemon_owner()(owner 변경 + 인증정보 자동 삭제) 신규.update_daemon()재사용 가능한 부분은 그대로. - REQ-052 repository 확장 —
postgres.py에get_broker_credential_meta()(단일 credential 조회, user_id 소유 검증 포함) 추가. 기존upsert_broker_credential,delete_broker_credential재사용. - REQ-053 라우터 —
app/api/v1_admin_daemons.py신규.POST /v1/admin/daemons,PATCH /v1/admin/daemons/{daemon_id},POST /v1/admin/daemons/{daemon_id}/activate,POST /v1/admin/daemons/{daemon_id}/deactivate.require_admin사용. - REQ-052 라우터 —
app/api/v1_daemon_credential.py신규.GET /v1/daemons/{daemon_id}/credential,PUT /v1/daemons/{daemon_id}/credential,DELETE /v1/daemons/{daemon_id}/credential.require_read+ 소유 스코프 검사. - REQ-052/053/054 테스트 —
tests/test_bat022_*.py신규 (AC별 pytest 케이스) - REQ-054 보안 격리 테스트 — caplog, 응답 JSON 필드 열거, REQ-051 회귀
Task Breakdown
T-1: 스키마 추가 (app/schemas/v1.py)
REQ-052 스키마:
CredentialSetRequest—app_key: str,app_secret: str,account_ref: str,broker: str = "kis"CredentialMetaResponse—daemon_id: str,broker: str,account_ref: str,has_credential: bool,credential_version: int,last_rotated_at: datetime | None(secret 필드 없음)
REQ-053 스키마:
DaemonCreateRequest—daemon_slug: str,display_name: str | None,owner_user_id: strDaemonUpdateRequest—display_name: str | None,owner_user_id: str | NoneDaemonAdminResponse—id: str,daemon_slug: str,display_name: str | None,owner_user_id: str,is_active: bool(credential 필드 없음)
T-2: repository 메서드 추가 (app/repositories/postgres.py + protocol)
get_broker_credential_by_daemon—(daemon_id, broker)로 단일 BrokerCredential 조회(없으면 None). 소유 검증은 서비스/라우터 레이어에서.delete_credentials_by_daemon_id— daemon_id에 속한 모든 BrokerCredential 삭제(owner 재할당 시 무효화용). REQ-053 AC-4 지원.update_daemon_owner— owner_user_id 변경 + 해당 daemon의 모든 broker_credentials 삭제 (단일 메서드 atomic). owner 변경 없이 display_name만 변경하면 기존update_daemon()재사용.- Protocol(
portfolio_repository.py) 동기화.
T-3: admin daemon 라우터 (app/api/v1_admin_daemons.py)
경로: /v1/admin/daemons
POST /v1/admin/daemons→DaemonCreateRequest→ 201 +DaemonAdminResponse- 중복 slug → 409 Conflict
- 비admin/service-token → 403, 비인증 → 401
PATCH /v1/admin/daemons/{daemon_id}→DaemonUpdateRequest→ 200 +DaemonAdminResponse- owner 변경 시
update_daemon_owner()호출(인증정보 자동 무효화) - owner 변경 없으면
update_daemon(display_name=...)호출 - 미존재 → 404
- owner 변경 시
POST /v1/admin/daemons/{daemon_id}/deactivate→ 200 +DaemonAdminResponseupdate_daemon(is_active=False)+revoke_tokens_by_daemon_id()(REQ-022 재사용)- 미존재 → 404
POST /v1/admin/daemons/{daemon_id}/activate→ 200 +DaemonAdminResponseupdate_daemon(is_active=True)호출- 미존재 → 404
DELETE /v1/admin/daemons/{daemon_id}→ 405 Method Not Allowed (하드 삭제 미제공)
T-4: credential 라우터 (app/api/v1_daemon_credential.py)
경로: /v1/daemons/{daemon_id}/credential
공통 소유 스코프 게이트:
- 비인증(토큰 없음) → 401 (require_read가 처리)
- service-token(
token_type == "service") → 403 - auth.user_id가 daemon.owner_user_id와 불일치 → 403
- 미존재 daemon_id → 404
엔드포인트:
GET /v1/daemons/{daemon_id}/credential→ 200 +CredentialMetaResponse- 인증정보 없으면
has_credential: false+ 나머지 null
- 인증정보 없으면
PUT /v1/daemons/{daemon_id}/credential→ 200 +CredentialMetaResponseapp_key/app_secretFernet 암호화 →upsert_broker_credential()호출- 응답에 app_key/app_secret 미포함
DELETE /v1/daemons/{daemon_id}/credential→ 204 No Content (인증정보 없어도 204)
T-5: main.py 라우터 등록
v1_admin_daemons_router,v1_daemon_credential_routerinclude
T-6: 테스트 신규 (tests/test_bat022_*.py)
test_bat022_req052_credential_api.py— REQ-052 AC-1~10 (소유 스코프 음성 포함)test_bat022_req053_admin_daemon_crud.py— REQ-053 AC-1~12 (owner 재할당 + 인증정보 무효화, DELETE 405 포함)test_bat022_req054_security.py— REQ-054 AC-1~7 (caplog, secret 필드 부재, REQ-051 회귀)
Risks / Constraints
- service-token 403 (REQ-052 AC-10):
token_type == "service"체크는auth.token_source == "db"전제. env-token은token_type=None이므로 별도 분기 필요. - admin role 검증 (REQ-053):
require_admin은 기존 permission hierarchy(admin > trade-write > read)를 사용. env admin 토큰도 처리. - owner 변경 + 인증정보 무효화(REQ-053 AC-4):
update_daemon_owner()는 단일 DB 트랜잭션이 아님(psycopg3 autocommit). partial failure(daemon 업데이트 성공 + 인증정보 삭제 실패)는 드문 케이스이나 로그 경고 추가. - DELETE 405 (REQ-053 AC-8): FastAPI에서 미등록 경로의 DELETE는 404를 반환. 명시적 405 반환하려면 라우터에
DELETE핸들러를 405 고정으로 등록해야 함.