# 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 — 신규