diff --git a/docs/batches/bat-022_20260628_daemon-crud-self-service-credential-api/implementation.md b/docs/batches/bat-022_20260628_daemon-crud-self-service-credential-api/implementation.md new file mode 100644 index 0000000..4dd7781 --- /dev/null +++ b/docs/batches/bat-022_20260628_daemon-crud-self-service-credential-api/implementation.md @@ -0,0 +1,59 @@ +# BAT-022 Implementation + +- Status: completed +- Commit: 6a0e14d + +## 변경 파일 목록 + +### 신규 파일 + +| 파일 | 역할 | +| --- | --- | +| `app/api/v1_admin_daemons.py` | REQ-053 admin daemon CRUD 라우터 (`/v1/admin/daemons`) | +| `app/api/v1_daemon_credential.py` | REQ-052 소유 스코프 credential 관리 라우터 (`/v1/daemons/{id}/credential`) | +| `tests/test_bat022_req052_credential_api.py` | REQ-052 AC-1~10 테스트 (17 케이스) | +| `tests/test_bat022_req053_admin_daemon_crud.py` | REQ-053 AC-1~12 테스트 (15 케이스) | +| `tests/test_bat022_req054_security.py` | REQ-054 AC-1~7 + REQ-051 회귀 (12 케이스) | + +### 변경 파일 + +| 파일 | 변경 내용 | +| --- | --- | +| `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에 신규 3개 메서드 추가 | +| `app/main.py` | `v1_admin_daemons_router`, `v1_daemon_credential_router` include | + +## REQ별 구현 요약 + +### REQ-052 (소유 스코프 인증정보 관리 API) + +- `GET /v1/daemons/{daemon_id}/credential` — 소유 스코프 검증 후 BrokerCredential 메타데이터 반환 (has_credential, version, last_rotated_at). secret 없음. +- `PUT /v1/daemons/{daemon_id}/credential` — `app_key`/`app_secret` Fernet 암호화 후 `upsert_broker_credential(daemon_id=...)` 호출. 응답 메타데이터만. +- `DELETE /v1/daemons/{daemon_id}/credential` — `delete_broker_credential(daemon_id=...)` 호출, 204 반환(멱등). +- 소유 스코프 게이트: `daemon.owner_user_id != auth.user_id` → 403. admin role도 동일 검사(AC-9). +- service-token 거부: `token_source == "db" and token_type == "service"` → 403(AC-10). +- 기존 `upsert_broker_credential`, `delete_broker_credential`(dcy-010 자산) 재사용. CLI와 동일 repo 메서드. + +### REQ-053 (admin 데몬 CRUD API) + +- `POST /v1/admin/daemons` → 기존 `create_daemon()` 재사용, 201. +- `PATCH /v1/admin/daemons/{id}` → owner 변경 시 `update_daemon_owner()`(새 메서드), display_name만 변경 시 기존 `update_daemon()`. +- `POST .../deactivate` → `update_daemon(is_active=False)` + `revoke_tokens_by_daemon_id()`(REQ-022 재사용). +- `POST .../activate` → `update_daemon(is_active=True)`. +- `DELETE .../` → 명시적 405 핸들러 등록. +- `require_admin` dependency 사용 (기존 `app/core/auth.py`). + +### REQ-054 (보안 격리) + +- 응답 스키마: `CredentialMetaResponse`/`DaemonAdminResponse` 모두 secret 필드 부재. +- 로그: `PUT credential` 처리 시 broker/daemon_id만 기록 (app_key/app_secret 값 미포함). +- Fernet at-rest: `encrypt_payload()` 호출 후 `upsert_broker_credential()` — 평문 미저장(AC-5). +- REQ-051 decrypt 경로 불변: `/v1/user_info` 코드 무변경, 회귀 4개 테스트 PASS(AC-7). + +## 설계 결정 트레이스 (design.md 참조) + +- OQ-1 해소: `/v1/daemons/{daemon_id}/credential` — daemon-scoped, REST 정합. +- OQ-2 해소: `/v1/admin/daemons` — admin 전용 prefix, 기존 GET /v1/daemons(caller-scoped)와 분리. +- service-token 403: `token_source == "db" and token_type == "service"` 조건. env-token은 token_type=None이므로 해당 없음. +- admin + 소유 스코프: admin role도 ownership 검사 통과 → 403(REQ-052 AC-9). diff --git a/docs/batches/bat-022_20260628_daemon-crud-self-service-credential-api/index.md b/docs/batches/bat-022_20260628_daemon-crud-self-service-credential-api/index.md index 1cd868e..9574606 100644 --- a/docs/batches/bat-022_20260628_daemon-crud-self-service-credential-api/index.md +++ b/docs/batches/bat-022_20260628_daemon-crud-self-service-credential-api/index.md @@ -2,7 +2,7 @@ - Batch ID: BAT-022 - Profile: standard -- Status: in-progress +- Status: draft-batch-verification - Discovery: dcy-011 - Included REQ: REQ-052, REQ-053, REQ-054 - Folder: docs/batches/bat-022_20260628_daemon-crud-self-service-credential-api/ @@ -35,11 +35,11 @@ dcy-010 자산(BrokerCredential 모델, Fernet 암호화, upsert/delete 메서 ## DoD (완료 조건) -- [ ] REQ-052 AC-1~10 테스트 통과 -- [ ] REQ-053 AC-1~12 테스트 통과 -- [ ] REQ-054 AC-1~7 테스트 통과 (caplog 비노출, 응답 secret 부재, REQ-051 회귀) -- [ ] 기존 521 테스트 회귀 없음 -- [ ] draft-batch-verification 문서에 AC별 evidence 기록 +- [x] REQ-052 AC-1~10 테스트 통과 (17 케이스) +- [x] REQ-053 AC-1~12 테스트 통과 (15 케이스) +- [x] REQ-054 AC-1~7 테스트 통과 (12 케이스, caplog 비노출, 응답 secret 부재, REQ-051 회귀) +- [x] 기존 521 테스트 회귀 없음 (565 passed, 0 failures) +- [x] draft-batch-verification 문서에 AC별 evidence 기록 ## 비가역 결재 게이트 diff --git a/docs/batches/bat-022_20260628_daemon-crud-self-service-credential-api/verification.md b/docs/batches/bat-022_20260628_daemon-crud-self-service-credential-api/verification.md new file mode 100644 index 0000000..0749007 --- /dev/null +++ b/docs/batches/bat-022_20260628_daemon-crud-self-service-credential-api/verification.md @@ -0,0 +1,92 @@ +# BAT-022 Verification + +- Status: draft +- Batch: BAT-022 +- Verification Date: 2026-06-28 + +## 테스트 실행 결과 + +### 신규 테스트 (44 케이스) + +``` +tests/test_bat022_req052_credential_api.py — 17 passed +tests/test_bat022_req053_admin_daemon_crud.py — 15 passed +tests/test_bat022_req054_security.py — 12 passed +``` + +### 전체 테스트 스위트 + +``` +565 passed, 11 warnings in 32.53s +``` + +기존 521 테스트 회귀: **0건** (521 → 565, +44 신규만 추가) + +## REQ-052 AC별 검증 Evidence + +| AC | 테스트 | 결과 | +| --- | --- | --- | +| AC-1: PUT → 200 + 메타데이터, secret 없음 | `test_put_credential_returns_meta_without_secret` | PASS | +| AC-2: 회전 시 credential_version 증가 | `test_put_credential_rotation_increments_version` | PASS | +| AC-3: CLI·API·service-token 정합 (upsert daemon_id 전달) | `test_put_credential_upsert_called_with_correct_daemon_id` | PASS | +| AC-4: GET → 200 + 메타데이터, secret 없음 | `test_get_credential_meta_returns_metadata_without_secret` | PASS | +| AC-5: DELETE → 204 + 이후 has_credential=False | `test_delete_credential_returns_204`, `test_get_after_delete_returns_has_credential_false` | PASS | +| AC-6 (음성): 타 사용자 토큰 → 403 | `test_put/get/delete_credential_other_user_returns_403` (3개) | PASS | +| AC-7: 비인증 → 401 | `test_credential_no_token_returns_401` | PASS | +| AC-8: 미존재 daemon → 404 | `test_credential_nonexistent_daemon_returns_404`, `test_put_nonexistent_daemon_returns_404` | PASS | +| AC-9 (음성): admin으로 타인 daemon write → 403 | `test_admin_cannot_write/read_other_user_credential` (2개) | PASS | +| AC-10 (음성): service-token → 403 | `test_service_token_credential_get/put/delete_returns_403` (3개) | PASS | + +## REQ-053 AC별 검증 Evidence + +| AC | 테스트 | 결과 | +| --- | --- | --- | +| AC-1: POST 생성 → 201 + DaemonAdminResponse | `test_admin_create_daemon_returns_201` | PASS | +| AC-2: 중복 slug → 409 | `test_admin_create_daemon_duplicate_slug_returns_409` | PASS | +| AC-3: display_name 변경 → BrokerCredential 유지 | `test_admin_patch_display_name_credential_preserved` | PASS | +| AC-4: owner 재할당 → BrokerCredential 자동 삭제 | `test_admin_patch_owner_invalidates_credentials` | PASS | +| AC-5: 재할당 후 이전 owner 인증정보 무효화 | `test_admin_patch_owner_then_old_owner_loses_credential` | PASS | +| AC-6: 비활성화 → is_active=False + service token revoke | `test_admin_deactivate_daemon_revokes_tokens` | PASS | +| AC-7: 재활성화 → is_active=True | `test_admin_activate_daemon_returns_active` | PASS | +| AC-8 (음성): DELETE → 405 | `test_admin_delete_daemon_returns_405` | PASS | +| AC-9 (음성): user-token(비admin) → 403 | `test_non_admin_user_token_create/patch/deactivate` (3개) | PASS | +| AC-10 (음성): service-token → 403 | `test_service_token_admin_crud_returns_403` | PASS | +| AC-11: 비인증 → 401 | `test_admin_crud_no_token_returns_401` | PASS | +| AC-12: 미존재 daemon → 404 | `test_admin_patch/deactivate_nonexistent_daemon_returns_404` (2개) | PASS | + +## REQ-054 AC별 검증 Evidence + +| AC | 테스트 | 결과 | +| --- | --- | --- | +| AC-1: PUT 응답에 app_key/app_secret 없음 | `test_put_credential_response_has_no_secret_fields` | PASS | +| AC-2: GET 응답에 app_key/app_secret 없음 | `test_get_credential_meta_response_has_no_secret_fields` | PASS | +| AC-3: admin CRUD 응답에 credential 필드 없음 | `test_admin_create/patch_response_has_no_credential_fields` (2개) | PASS | +| AC-4: caplog — PUT 처리 중 app_key/app_secret 값 미포함 | `test_put_credential_log_does_not_expose_secret` | PASS | +| AC-5: Fernet 암호화 저장 확인 (gAAAA... 형태) | `test_put_credential_stores_encrypted_payload` | PASS | +| AC-6: PUT 후 service-token user_info → 동일 account_ref | `test_put_then_service_token_user_info_returns_correct_account_ref` | PASS | +| AC-7: REQ-051 회귀 (4개 테스트) | `test_req051_regression_*` (4개) | PASS | + +## 음성 테스트 체크리스트 + +| 음성 시나리오 | 케이스 수 | 결과 | +| --- | --- | --- | +| 소유 스코프 403 (AC-6: 타 사용자 토큰) | 3 | PASS | +| admin 소유 스코프 403 (AC-9) | 2 | PASS | +| service-token 403 (AC-10, REQ-052) | 3 | PASS | +| service-token 403 (AC-10, REQ-053) | 1 | PASS | +| DELETE 405 (AC-8, REQ-053) | 1 | PASS | +| secret 필드 부재 (AC-1/2/3, REQ-054) | 4 | PASS | +| caplog secret 미노출 (AC-4, REQ-054) | 1 | PASS | + +## 회귀 테스트 + +- REQ-051 AC-1~3, AC-5: 신규 API 추가 후 GET /v1/user_info 보안 격리 불변 확인 (4개 테스트 PASS) +- 기존 521 테스트 전건 회귀 없음 + +## 잔여 위험 / 미해결 + +1. **AC-3 통합 수준**: REQ-052 AC-3(CLI·API·service-token 3경로 정합)은 이 batch에서 unit-level로 검증(upsert daemon_id 전달 확인). 실 DB 통합 검증은 dev DB 환경 필요(비가역 아님, 선택적 추후 확인 가능). + +2. **DELETE credential 기본 broker "kis"**: GET/DELETE 에서 broker 파라미터 없이 "kis" 고정. 다중 broker 지원이 필요해지면 경로 파라미터 추가 필요 — 현재 단일 broker 가정 설계 일치. + +3. **update_daemon_owner autocommit 부분 실패**: postgres.py `update_daemon_owner()`는 daemon UPDATE + credential DELETE를 autocommit 2회 연산으로 처리. daemon 업데이트 성공 후 credential 삭제 실패 케이스는 테스트 범위 밖(네트워크 장애 등). 실 운영에서 발생 시 수동 정리 필요. diff --git a/docs/batches/index.md b/docs/batches/index.md index 01cdb03..ba7dfa9 100644 --- a/docs/batches/index.md +++ b/docs/batches/index.md @@ -29,3 +29,4 @@ | BAT-019 | standard | confirmed | REQ-044, REQ-045 | dcy-009 | [bat-019](./bat-019_20260627_assets-read-permission-and-daemon-list/) | | BAT-020 | standard | release-candidate | REQ-046, REQ-047, REQ-048, REQ-049, REQ-050, REQ-051 | dcy-010 | [bat-020](./bat-020_20260628_daemon-scoped-broker-credential/) | | BAT-021 | standard | in-progress | REQ-047 | dcy-010 | [bat-021](./bat-021_20260628_req047-step2-integration-and-runbook/) | +| BAT-022 | standard | draft-batch-verification | REQ-052, REQ-053, REQ-054 | dcy-011 | [bat-022](./bat-022_20260628_daemon-crud-self-service-credential-api/) |