Files
portfolio-hub/docs/batches/bat-022_20260628_daemon-crud-self-service-credential-api/planning.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

86 lines
5.6 KiB
Markdown

# BAT-022 Planning
## 구현 의존 순서
1. **REQ-052/053 스키마** — `app/schemas/v1.py`에 `CredentialSetRequest`, `CredentialMetaResponse`, `DaemonCreateRequest`, `DaemonUpdateRequest`, `DaemonAdminResponse` 추가
2. **REQ-053 repository 확장** — `postgres.py`에 `update_daemon_owner()` (owner 변경 + 인증정보 자동 삭제) 신규. `update_daemon()` 재사용 가능한 부분은 그대로.
3. **REQ-052 repository 확장** — `postgres.py`에 `get_broker_credential_meta()` (단일 credential 조회, user_id 소유 검증 포함) 추가. 기존 `upsert_broker_credential`, `delete_broker_credential` 재사용.
4. **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` 사용.
5. **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` + 소유 스코프 검사.
6. **REQ-052/053/054 테스트** — `tests/test_bat022_*.py` 신규 (AC별 pytest 케이스)
7. **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: str`
- `DaemonUpdateRequest` — `display_name: str | None`, `owner_user_id: str | None`
- `DaemonAdminResponse` — `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
- `POST /v1/admin/daemons/{daemon_id}/deactivate` → 200 + `DaemonAdminResponse`
- `update_daemon(is_active=False)` + `revoke_tokens_by_daemon_id()` (REQ-022 재사용)
- 미존재 → 404
- `POST /v1/admin/daemons/{daemon_id}/activate` → 200 + `DaemonAdminResponse`
- `update_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 + `CredentialMetaResponse`
- `app_key`/`app_secret` Fernet 암호화 → `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_router` include
### 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 고정으로 등록해야 함.