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

94 lines
5.8 KiB
Markdown

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