Files
portfolio-hub/docs/releases/rel-003_20260628_daemon-credential-and-admin-api.md
T
genichinandClaude Opus 4.8 9d98d47e2d docs(rel-003): M3 daemon-scoped 크레덴셜 릴리즈 문서 + Step2 ops 런북
REL-003(dcy-010/011 + WAC #15 동반) draft + Step2 마이그레이션 핸드오프(프리체크·롤백·go/no-go).
내일 ops 윈도우용. 미푸시(사장님 결재 시 release와 함께 push).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VHtM5Ws1HKCKQ4JGi6GRb5
2026-06-28 23:02:35 +09:00

258 lines
14 KiB
Markdown

# REL-003: daemon-scoped 브로커 인증정보 + 관리 API (dcy-010 + dcy-011)
- Status: draft
- Profile: app-service
- Included Batch: BAT-018 (REQ-043), BAT-019 (REQ-044, REQ-045), BAT-020 (REQ-046~051), BAT-021 (REQ-047 Step2 통합 및 런북), BAT-022 (REQ-052~054)
---
## Scope
### Included
- **BAT-018** (dcy-008, batch-lite, release-candidate) — REQ-043: `GET /v1/portfolios/{id}/holdings/cost-basis` 엔드포인트 신설 (WAC/평단 조회)
- **BAT-019** (dcy-009, standard, confirmed) — REQ-044/045: `GET /v1/assets` 권한 완화(require_read) + `GET /v1/daemons` 신설
- **BAT-020** (dcy-010, standard, release-candidate) — REQ-046~051: BrokerCredential daemon_id 도메인 모델 + Step1 마이그레이션 + backfill CLI + `/v1/user_info` daemon-scoped 확장 + CLI `--daemon-id` 옵션 + 보안 격리
- **BAT-021** (dcy-010, standard, release-candidate) — REQ-047: Step2 동반 코드 수정(ensure_schema/upsert ON CONFLICT) + 실 PostgreSQL 통합검증 + Step2 런북 (`docs/runbooks/bat-021-req047-step2-migration-runbook.md`)
- **BAT-022** (dcy-011, standard, release-candidate) — REQ-052~054: 사용자 소유 스코프 인증정보 관리 API + admin 데몬 CRUD API + 인증정보 보안 격리(쓰기전용·secret 미반환·로그 미노출)
- **(동반 배포) dcy-035 WAC #15** (`3a6adc9`, REQ-041 change-req CHG-20260626-01) — `/pnl/realized`·holdings cost-basis의 **매수 수수료를 WAC cost basis에 편입**. 이미 origin/main 머지됨 → 이번 portfolio-hub 코드 배포(main)에 함께 실린다. trex-core 운영리포트(dcy-035, REL-010)와 짝을 이루는 멀티데이 WAC 정확값의 portfolio-hub 측. (별도 배포·재시작 불필요 — 동일 배포에 포함)
### Excluded
- REQ-047 Step2(PK 전환) 운영 DB 실제 적용 — **이번 릴리즈의 코드 배포 이전** ops 윈도우에서 별도 수행 (런북 `docs/runbooks/bat-021-req047-step2-migration-runbook.md` 엄수, 사장님 결재 게이트)
- trex-core 소비자 어댑터 (KISConfig fetch 경로) — 별도 trex-core Discovery (dcy-010 out-of-scope)
- trex-web UI (dcy-009) — trex-web repo 별도 진행
- 서비스 토큰 발급·폐기 (dcy-011 out-of-scope)
---
## 전제 조건 (배포 시작 전 필수 확인)
1. **`CREDENTIAL_ENCRYPTION_KEY`** — 운영 환경 env에 Fernet key 값이 설정돼 있어야 한다. 미설정 시 인증정보 암·복호화 전체가 동작하지 않는다.
- 확인: 서비스 실행 환경에서 `echo $CREDENTIAL_ENCRYPTION_KEY | head -c 10` (공백이면 미설정)
- 최초 설정: `python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"` 결과를 `CREDENTIAL_ENCRYPTION_KEY`에 등록
- **주의**: 기존 운영 인스턴스에 이미 설정돼 있으면 변경 금지 (기존 암호화 payload를 복호화할 수 없게 됨)
2. **Step2 마이그레이션 완료 확인** — 코드 배포 전에 REQ-047 Step2(alembic revision `20260628_0027`) 적용이 완료되어야 한다. Step2 미적용 상태로 BAT-021 코드를 배포하면 `upsert_broker_credential`의 `ON CONFLICT (daemon_id, broker)` 절이 구 스키마(PK=`(user_id, broker)`)와 맞지 않아 인증정보 등록/회전 실패.
- 검증 쿼리: `SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'broker_credentials_pkey';`
- 기대값: `PRIMARY KEY (daemon_id, broker)`
3. **alembic 상태 사전 확인** — 현행 운영 DB의 alembic head가 `20260628_0027` (Step2 완료 후) 인지 확인:
```bash
alembic current
# 기대: 20260628_0027 (head)
```
4. **DB 백업** — Step2 마이그레이션 적용 전 운영 DB 전체 스냅샷 백업 (런북 1단계 참조).
5. **pytest 통과 확인** — main HEAD (`9fe6f3b`) 기준으로 `pytest` 전체가 통과해야 한다.
- BAT-022 기준 565 passed + 핫픽스 커밋 포함 최신 스위트 통과 확인 필요.
---
## Rollout Plan
> **배포 순서 엄수. 특히 Step2 미적용 상태에서 코드 배포하면 upsert 실패가 발생한다.**
```
[ops 윈도우] DB 백업 → Step1 upgrade → backfill CLI → NULL 0건 확인
→ Step2 upgrade (사장님 결재 필수) → Step2 스키마 검증
[코드 배포] → portfolio-hub 서비스 rolling restart
[smoke] → 헬스체크 → 스모크 테스트 체크리스트 실행
```
### 단계별 상세
**[ops 윈도우 전용 — 코드 배포 전 수행]**
1. **DB 백업**: 운영 DB 전체 스냅샷 (`pg_dump`) — 런북 1단계 참조
2. **Step1 upgrade** (`alembic upgrade 20260628_0026`): `broker_credentials`에 `daemon_id TEXT NULL` 컬럼 추가. 기존 PK `(user_id, broker)` 유지.
3. **backfill CLI**: `pfhub credential backfill-daemon-id --user <username> --daemon-id <daemon_slug>` — 기존 user-scoped 인증정보 행에 daemon_id 설정. 사용자별 반복. `--dry-run` 선 확인 권장.
4. **NULL 0건 검증**: `SELECT COUNT(*) FROM broker_credentials WHERE daemon_id IS NULL;` → 반드시 0 확인. 1건이라도 있으면 Step2 진행 금지 (upgrade가 RuntimeError로 자동 거부함).
5. **Step2 upgrade** (`alembic upgrade 20260628_0027`, **사장님 결재 필수**): PK를 `(user_id, broker)` → `(daemon_id, broker)`로 전환. `daemon_id NOT NULL` 제약 추가. **비가역 작업.**
6. **Step2 스키마 검증**: PK 전환·daemon_id NOT NULL·user_id 유지를 SQL 쿼리로 확인 — 런북 4단계 검증 SQL 참조.
**[코드 배포]**
7. **portfolio-hub 서비스 rolling restart**: freeze 커밋 `9fe6f3b` (main HEAD) 이미지로 교체.
- 포함 변경 모듈: `app/repositories/postgres.py` (ensure_schema·upsert ON CONFLICT), `app/api/v1_daemons.py` (admin CRUD·daemon 목록), `app/api/v1_credentials.py` (소유 스코프 관리 API), `app/api/v1_portfolios.py` (holdings/cost-basis), `app/api/v1_user_info.py` (daemon-scoped broker 전달), `app/cli/commands/credential.py` (--daemon-id·backfill), `app/domain/pnl.py` (calculate_current_wac)
**[배포 후 smoke]**
8. 아래 Verification Checklist 항목 순서대로 실행.
---
## Rollback Plan
### 트리거 조건
- `GET /health` HTTP 200 실패
- `GET /v1/user_info` (service token) HTTP 500 또는 broker 필드 누락
- 인증정보 PUT/GET 연속 500 발생
- 기존 API(`/v1/portfolios`, `/v1/assets`, `/v1/daemons`) 회귀 발생
### 코드 롤백 (1순위)
BAT-022/021/020은 기존 기능의 additive 추가 및 REQ-049 동작 변경(brokers→broker 단일화)이 포함된다. 코드 롤백 시 REL-002 freeze 이미지(커밋 `d89f137`)로 복원하면 된다. DB는 Step2 이후 상태 그대로 둬도 무방 (코드가 구 스키마를 참조하지 않는 방향으로 하위호환 불가하여 DB 롤백 필요 여부를 판단).
```
portfolio-hub → REL-002 이미지(커밋 d89f137)로 rolling restart
```
### DB 롤백 (Step2 롤백 — 조건부)
Step2 downgrade는 같은 user의 여러 daemon이 동일 broker를 쓰는 경우 RuntimeError로 자동 거부된다. 단일 daemon 1계좌 환경이면 downgrade 가능:
```bash
alembic downgrade 20260628_0026 # Step2 → Step1 상태
alembic downgrade 20260605_0025 # Step1 → 이전 상태 (daemon_id 컬럼 제거)
```
**경고**: Step2 downgrade는 조건부 가역이다. 백업 복원이 최후 수단.
```bash
pg_restore -h <DB_HOST> -U <DB_USER> -d <DB_NAME> -c backup_pre_bat021_<TIMESTAMP>.dump
```
### 롤백 영향 분석
- REQ-049 (`/v1/user_info` daemon-scoped): brokers 리스트→단일 broker 변경. trex-core 소비자는 `daemon_id`만 읽으므로 무영향(소비자 인벤토리 확인 완료).
- REQ-052/053: HTTP API 추가분 — additive. 롤백 시 엔드포인트 제거만 발생. 기존 운영 영향 없음.
- REQ-043/044/045 (BAT-018/019): additive 추가·권한 완화. 롤백 시 신규 엔드포인트 제거 + GET /v1/assets가 require_admin으로 복원.
---
## Verification Checklist
### 배포 전
- [ ] Step2 완료 확인: `SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'broker_credentials_pkey';` → `PRIMARY KEY (daemon_id, broker)`
- [ ] alembic head 확인: `alembic current` → `20260628_0027 (head)`
- [ ] NULL 잔존 0건: `SELECT COUNT(*) FROM broker_credentials WHERE daemon_id IS NULL;` → `0`
- [ ] `CREDENTIAL_ENCRYPTION_KEY` 운영 환경 설정 확인
- [ ] pytest 전체 통과 확인 (freeze 커밋 `9fe6f3b` 기준)
- [ ] DB 백업 완료 확인 (백업 파일 경로/사이즈 기록)
### 배포 직후
#### 기본 헬스
- [ ] `GET /health` → HTTP 200, `db_enabled: true`
#### admin 데몬 CRUD (REQ-053 — BAT-022)
- [ ] `POST /v1/admin/daemons` (admin-token) — 데몬 생성 → HTTP 201 + `DaemonAdminResponse` (id, owner_user_id, daemon_slug, is_active)
- [ ] `GET /v1/admin/daemons` (admin-token) — 데몬 목록 → HTTP 200 + 데몬 목록 반환
- [ ] `GET /v1/admin/daemons/{daemon_id}` (admin-token) → HTTP 200
- [ ] `PATCH /v1/admin/daemons/{daemon_id}` (admin-token) — display_name 변경 → HTTP 200
- [ ] `PATCH /v1/admin/daemons/{daemon_id}` (admin-token, is_active=false) — 비활성화 → HTTP 200 + is_active=false
- [ ] `DELETE /v1/admin/daemons/{daemon_id}` (admin-token) → HTTP 405 (하드 삭제 금지)
- [ ] 비admin user-token으로 admin CRUD 시도 → HTTP 403
#### 인증정보 관리 API (REQ-052 — BAT-022)
- [ ] `PUT /v1/daemons/{daemon_id}/credential` (user-token, 소유 데몬) → HTTP 200 + 메타데이터(account_ref, credential_version, last_rotated_at, broker) 응답; **app_key/app_secret 미포함 확인 (필드 부재)**
- [ ] `GET /v1/daemons/{daemon_id}/credential` (user-token, 소유 데몬) → HTTP 200 + 메타데이터; **secret 미포함 확인**
- [ ] `DELETE /v1/daemons/{daemon_id}/credential` (user-token, 소유 데몬) → HTTP 204
- [ ] 타 사용자 user-token으로 소유 스코프 외 데몬 인증정보 조회/수정 → HTTP 403
#### service-token `/v1/user_info` daemon-scoped (REQ-049 — BAT-020)
- [ ] service-token(daemon_id 바인딩)으로 `GET /v1/user_info` → `broker` 단일 객체(account_ref, credential_payload 포함), `brokers` 리스트 키 없음 확인
- [ ] `CREDENTIAL_ENCRYPTION_KEY`와 일치하는 키로 인증정보를 등록한 경우 `credential_payload` 복호화 값(app_key 포함) 반환 확인
- [ ] daemon_id 없는 service-token으로 `/v1/user_info` → HTTP 403
- [ ] user-token으로 `/v1/user_info` → HTTP 403 (REQ-051 보안 격리 회귀)
#### WAC 조회 엔드포인트 (REQ-043 — BAT-018)
- [ ] `GET /v1/portfolios/{id}/holdings/cost-basis` (read-token, 소유 portfolio) → HTTP 200 + `items`, `cost_basis_from` 포함
#### 자산/데몬 목록 (REQ-044/045 — BAT-019)
- [ ] `GET /v1/assets` (read-token, non-admin) → HTTP 200 (권한 완화 확인)
- [ ] `GET /v1/daemons` (user-token) → HTTP 200 + 소유 데몬 목록
#### 기존 API 회귀
- [ ] `GET /v1/portfolios` (read-token 이상) → HTTP 200
- [ ] `POST /v1/portfolios/{id}/ledger/trades` (trade-write-token) → 기존 동작 유지
- [ ] `GET /v1/portfolios/{id}/trades` (read-token 이상) → HTTP 200
- [ ] `GET /v1/portfolios/{id}/pnl/realized` (read-token 이상) → HTTP 200
---
## Known Issues (릴리즈 전 인지 사항)
1. **BAT-022 AC-3 통합 수준**: REQ-052 AC-3(CLI·API·service-token 3경로 정합)이 unit-level 검증. 실 DB 통합 환경에서 재확인 권장(비가역 아님, 선택적).
2. ~~owner 재할당 원자성~~ **(해소됨 — 오기 정정)**: `update_daemon_owner()`는 **단일 `conn.transaction()` 트랜잭션**으로 credential DELETE(선행) → owner UPDATE를 원자 처리한다(BAT-022 QC Major fix, 커밋 83e6dde). 어느 한쪽 실패 시 전체 롤백되어 "owner만 바뀌고 구 인증정보 잔존"하는 leak 상태가 발생하지 않는다. 라이브 통합검증(2026-06-28)에서 owner 재할당 → 인증정보 무효화 PASS. **리스크 아님.**
3. **DELETE credential broker 고정**: `DELETE /v1/daemons/{daemon_id}/credential`은 broker="kis" 고정. 다중 broker 지원 시 별도 설계 필요(현재 단일 broker 가정 설계).
4. **REL-002 Known Issues 이월**: symbol 필터 edge 테스트, HTTP_422 deprecated 경고, 성능 미검증 — 여전히 비차단 잔여.
---
## Release Execution Evidence
- Release Status Transition Target: released
### Deployment Facts
- Executed At:
- (미실행 — 배포 전)
- Completed At:
- (미실행)
- Environment:
- portfolio-hub 운영 인스턴스
- Operator:
- (ops 윈도우 담당자 기입)
- Revision or Artifact:
- 배포 커밋: `9fe6f3b` (main HEAD, 2026-06-28)
- alembic head (코드 배포 후 기대): `20260628_0027`
- Commands or Procedure:
- 상세는 위 Rollout Plan 참조
- DB 마이그레이션: `docs/runbooks/bat-021-req047-step2-migration-runbook.md` 엄수
- 코드 배포: portfolio-hub 서비스 rolling restart (freeze 커밋 `9fe6f3b`)
### Profile-Specific Checks
- app-service:
- Health Endpoint:
- (배포 후 기입)
- Startup Log Fields:
- (배포 후 기입)
- Request Log Fields:
- (배포 후 기입)
- Smoke or Manual Verification:
- (배포 후 Verification Checklist 결과 기입)
### Rollback Outcome
- Rollback Required:
- (배포 후 기입)
- Rollback Trigger:
- (배포 후 기입)
- Rollback Action:
- (배포 후 기입)
### Release Decision Evidence
- Released Decision:
- (배포 후 팀장 기입)
- Rationale:
- (배포 후 기입)
- Recorded By:
- (배포 후 기입)
- Recorded At:
- (배포 후 기입)
---
## Feedback Handoff
- Observation Summary:
- (배포 후 운영 관찰 기입)
- Discovery Input:
- trex-core 소비자 어댑터 (KISConfig fetch, dcy-010 out-of-scope) — 별도 trex-core Discovery (dcy-036/BAT-054로 진행, PR #35)
- REQ Input:
- (배포 후 기입)
- Change Request Input:
- (배포 후 기입)