From 2e1298a978d789bae8bc6b31d9e94027fc63dc66 Mon Sep 17 00:00:00 2001 From: Justin Ko Date: Mon, 11 May 2026 14:17:27 +0000 Subject: [PATCH] fix: enforce trade-write permission check on v1 ledger trade endpoint Security Fix: - Update v1_ledger.py to require trade_write permission instead of read - Fix authentication dependency from require_read to require_trade_write - Ensure trade operations cannot be performed with read-only service tokens Test Coverage: - Add test_v1_ledger_trade_requires_trade_write_permission() to verify permission boundary - Confirm read-only tokens are rejected with 403 Forbidden status - Validate repository methods are not called on permission failure Documentation: - Add new Discovery dcy-005 for production admin bootstrap and operator access - Update docs/discovery/index.md with dcy-005 entry - Update release rel-001 with additional verification notes and issue references --- app/api/v1_ledger.py | 4 +- ...ion-admin-bootstrap-and-operator-access.md | 238 ++++++++++++++++++ docs/discovery/index.md | 3 +- ...001_20260502_trex-basic-v1-api-contract.md | 45 +++- tests/test_v1.py | 28 +++ 5 files changed, 302 insertions(+), 16 deletions(-) create mode 100644 docs/discovery/dcy-005_20260511_production-admin-bootstrap-and-operator-access.md diff --git a/app/api/v1_ledger.py b/app/api/v1_ledger.py index 23bb917..bc15b1a 100644 --- a/app/api/v1_ledger.py +++ b/app/api/v1_ledger.py @@ -6,14 +6,14 @@ from decimal import Decimal from fastapi import APIRouter, Depends, HTTPException, Response, status -from app.core.auth import AuthContext, require_read +from app.core.auth import AuthContext, require_trade_write from app.repositories.factory import get_portfolio_repository_from_settings from app.schemas.v1 import LedgerTradeRequest, LedgerTradeResponse router = APIRouter(prefix="/v1", tags=["v1"]) -def _require_service_token(auth: AuthContext = Depends(require_read)) -> AuthContext: +def _require_service_token(auth: AuthContext = Depends(require_trade_write)) -> AuthContext: if auth.token_source != "db" or auth.token_type != "service": raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, diff --git a/docs/discovery/dcy-005_20260511_production-admin-bootstrap-and-operator-access.md b/docs/discovery/dcy-005_20260511_production-admin-bootstrap-and-operator-access.md new file mode 100644 index 0000000..a986c2d --- /dev/null +++ b/docs/discovery/dcy-005_20260511_production-admin-bootstrap-and-operator-access.md @@ -0,0 +1,238 @@ +# 0. 문서 상태 +이 문서의 현재 상태와 식별 정보를 기록한다. 문서 추적과 승인 흐름에 필요한 최소 메타데이터를 적는다. + +- 상태: draft +- 문서 ID: dcy-005_20260511_production-admin-bootstrap-and-operator-access +- 이슈명: 실배포 admin bootstrap 및 operator access 경로 정립 +- 작성 시각(KST): 2026-05-11 23:12 +- 마지막 갱신 시각(KST): 2026-05-11 23:12 +- 대체됨: 없음 +- 후속 Discovery 참조: 없음 +- 생성된 REQ 참조: 없음 + +# 1. 계획 상태 요약 +현재 Discovery가 어떤 배경에서 작성되고 있는지, 연계 문서가 있는지, 지금 시점의 해석이 무엇인지 짧게 요약한다. + +- 연계 문서 현황: + - 관련 점수 파일: 미생성 + - 관련 TODO/메모: docs/releases/rel-001_20260502_trex-basic-v1-api-contract.md +- 해석: + - REL-001 실배포 smoke에서 public health와 read service-token 경로는 확인됐지만, admin 성공 경로와 trade-write 성공 경로를 완료할 운영 접근 방법이 저장소 문서에 정의돼 있지 않다. + - 이번 Discovery는 활성 release 검증을 막는 운영 문제를 별도 주기로 분리해, 실배포 admin bootstrap, operator auth path, smoke credential provenance, 로그 접근 절차를 독립적으로 정리하기 위한 초안이다. + +# 2. 요구사항 판정 결과 +이번 요청이 기존 요구사항과 비교해 신규인지, 유사한지, 중복인지 판단하고 그 근거를 남긴다. + +- 판정: 신규 +- 근거 + - 기존 dcy-001~004는 DB 검증, trex-basic consumer API, CLI `portfolio list --user`, portfolio UUID 전환을 다루며, 실배포 admin bootstrap 및 operator access를 독립 주제로 다루는 Discovery는 없다. + - 이번 요청은 제품 기능 추가보다 운영 인증 경로, bootstrap 계정/토큰 provenance, release smoke credential 관리라는 별도 문제 정의를 가진다. + +> **판정이 `완전 중복`인 경우**: 이하 섹션 작성을 중단하고 `# 9. 파일 처리 결과`에 중복 판정 사유와 참조 문서 경로를 기록한 뒤 문서를 닫는다. REQ 초안 작성으로 넘기지 않는다. + +# 3. 문제점의 요약 +문제의 현재 상태, 기대 상태, 관련 이해관계자, 제약사항을 정리한다. Discovery의 핵심 맥락을 가장 먼저 이해할 수 있어야 한다. + +- 현재 상태 + - 실배포 URL `https://fourblocks2026.com/portfolio-hub/`에서 `GET /health`, `/docs`, `/openapi.json`은 정상 응답했고, read service-token으로 `GET /v1/assets`, `GET /v1/user_info`까지는 검증됐다. + - 동일 토큰으로 `POST /auth/service-tokens`를 호출하면 `403 Insufficient permission`이 반환됐다. 현재 저장소에는 실배포에서 어떤 경로로 admin auth context를 확보하는지, admin 성공 경로를 어떻게 검증하는지 정리한 runbook이 없다. + - `app/core/auth.py`는 env token 집합 또는 DB `auth_tokens` principal의 `permission`으로 권한을 해석하고, `app/api/auth_routes.py`의 `POST /auth/service-tokens`는 `require_admin`을 선행한다. 즉 운영자가 어떤 credential을 admin으로 다뤄야 하는지가 명확해야 smoke가 가능하다. + - `README.md`와 `app/core/config.py`에는 first-run bootstrap 계정 환경 변수(`PORTFOLIOHUB_BOOTSTRAP_*`)가 있으나, 실배포의 steady-state admin access, smoke용 read/trade-write/admin token 분리, startup/request 로그 접근 경로는 문서화돼 있지 않다. +- 기대 상태 + - 운영자는 실배포에서 admin auth context를 얻는 표준 경로를 알고 있어야 하며, 그 경로로 `POST /auth/service-tokens` 성공 smoke를 반복 가능하게 수행할 수 있어야 한다. + - release 검증에는 public/no-auth, read service-token, trade-write service-token, admin auth context의 용도와 provenance가 문서로 분리돼 있어야 한다. + - startup log, request log, auth failure(401), permission failure(403), resource failure(404)를 운영자가 구분해 증거로 남길 수 있어야 한다. +- 이해관계자 + - repository-maintainers + - 실배포 운영자 / 배포 승인자 + - trex-basic 연동 운영자 + - 보안 또는 secret 관리 담당자 +- 제약사항 + - 실제 credential 원문은 저장소, release 문서, Discovery 문서에 기록하지 않는다. + - 기존 auth model(`read`, `trade-write`, `admin` 권한과 DB-issued service-token 구조)은 유지하되, 필요한 경우 bootstrap/runbook 또는 최소한의 운영 지원 기능만 추가한다. + - 활성 release(REL-001)와 병렬로 진행되므로, 이 Discovery는 운영 접근성과 검증 절차를 정리하는 데 집중하고 기존 consumer contract 범위를 임의로 확장하지 않는다. + +## 주요 사용자 시나리오 +사용자 또는 운영자가 실제로 어떤 상황에서 이 변경을 필요로 하는지 대표 시나리오를 적는다. + +- 운영자가 REL-001 배포 후 smoke를 수행할 때, admin 경로로 service token을 발급하고 read/trade-write 토큰을 분리해 `/auth/service-tokens`, `/v1/assets`, `/v1/user_info`, `/v1/assets/{asset_id}/ledger/trades`를 순서대로 검증한다. +- 장애 또는 신규 환경 bootstrap 시 운영자가 `PORTFOLIOHUB_BOOTSTRAP_*` 기반 초기 admin 계정 생성 경로와 steady-state admin recovery 경로를 구분해 사용한다. +- release evidence 작성 시 운영자가 raw secret 없이 token provenance, 권한 수준, 로그 접근 경로만 기록해 보안 사고 없이 검증 내역을 남긴다. + +# 4. 이번에 정의할 변경 +이번 Discovery에서 다루는 핵심 변경 항목을 목록으로 적는다. 기능, 구조, 규칙 변경을 함께 포함해도 된다. + +- 실배포 admin bootstrap 및 steady-state operator access 경로를 정의한다. bootstrap user, admin login/session, admin token, DB-issued admin service token 중 어떤 경로를 표준으로 쓸지 결정한다. +- release 검증용 credential matrix를 정의한다. public/no-auth, read service-token, trade-write service-token, admin auth context의 용도와 발급/보관/폐기 절차를 분리한다. +- startup/request 로그 접근, auth/permission/resource 오류 판독, release evidence 기록 절차를 운영 runbook과 release 문서에 연결한다. + +## 영향 범위 +변경이 미칠 코드/문서/운영 범위를 명시해 REQ 정제와 batch planning에서 추정 누락을 줄인다. + +- 코드 영향: + - `app/core/config.py` — bootstrap 환경 변수와 운영 환경 가정 재검토 + - `app/main.py` — bootstrap 초기 사용자 생성 경로의 운영 제약 재정리 필요 여부 검토 + - `app/core/auth.py`, `app/api/auth_routes.py`, `app/services/auth_service.py` — admin/read/trade-write auth path의 운영 의미 문서화 또는 최소 지원 기능 추가 가능성 + - `app/cli/commands/*` 또는 운영 보조 스크립트 — 필요 시 token inspection 또는 smoke helper 추가 검토 +- 문서 영향: + - `README.md` — 실배포 bootstrap/admin/operator access runbook 추가 가능성 + - `docs/project-structure.md`, `docs/runtime-flows.md` — auth/bootstrap/runtime smoke 흐름 반영 가능성 + - `docs/releases/rel-001_20260502_trex-basic-v1-api-contract.md` — release evidence와 운영 주의사항 정합화 +- 운영 영향: + - secret manager 또는 배포 환경 변수 관리 절차 명시 필요 + - release smoke에 필요한 token 수명 주기 및 폐기 절차 정의 필요 + - 로그 접근 권한과 증적 수집 절차를 운영자 역할에 연결해야 함 + +# 5. 요구사항 목록 +실제로 구현과 검증으로 이어질 요구사항을 기능/비기능으로 나누어 정리하고, 범위 경계를 명확히 적는다. + +## 기능 요구사항 +- FR-1: 실배포에서 admin auth context를 확보하는 표준 경로를 정의한다. first-run bootstrap과 steady-state 운영 경로를 구분하고, 운영자가 어떤 credential로 `POST /auth/service-tokens`를 성공시켜야 하는지 명시한다. +- FR-2: release 검증에 필요한 credential matrix를 정의한다. public/no-auth, read service-token, trade-write service-token, admin auth context 각각의 발급 주체, 보관 위치, 만료 정책, 폐기 절차를 문서화한다. +- FR-3: REL-001과 같은 app-service release에서 startup log, request log, auth/permission/resource 오류를 수집하는 운영 runbook을 정의한다. +- FR-4: 운영자가 DB 콘솔에 직접 들어가지 않고도 token provenance 또는 권한 수준을 확인할 수 있는 최소 지원 수단이 필요한지 판정하고, 필요 시 구현 또는 문서 경로를 정의한다. + +## 비기능 요구사항 +성능, 운영성, 일관성, 자동화 안정성 등 기능 외 요구사항을 적는다. + +- NFR-1: raw credential, raw token, bootstrap password는 저장소와 release/discovery 문서에 남기지 않는다. 운영 문서에는 provenance와 마스킹된 메타데이터만 남긴다. +- NFR-2: 운영 runbook은 401, 403, 404의 의미를 분리해 재현 가능하게 설명해야 하며, 권한 문제와 리소스 부재 문제를 혼동하지 않도록 해야 한다. +- NFR-3: bootstrap 또는 recovery 경로는 idempotent해야 하며, 이미 사용자가 존재하는 환경에서 first-run bootstrap 로직이 반복 부작용을 만들지 않아야 한다. + +## 범위 경계 +이번 반복에서 반드시 포함할 범위와 명시적으로 제외할 범위를 구분한다. + +- In Scope + - 실배포 admin bootstrap/steady-state access 경로 정의 + - read/trade-write/admin smoke credential 구분 및 운영 절차 정의 + - release evidence용 로그 접근 및 증적 수집 절차 정의 + - 필요한 최소 운영 지원 기능 또는 문서 변경 판정 +- Out of Scope + - 권한 체계(`read`, `trade-write`, `admin`) 자체의 전면 재설계 + - 외부 사용자를 위한 신규 admin UI 또는 공개 운영 포털 추가 + - trex-basic consumer API 스키마 변경 + +# 6. 리스크/가정 목록 +불확실성과 전제 조건을 분리해서 적는다. 리스크는 영향과 완화 방안을, 가정은 현재 판단의 전제를 기록한다. + +## 리스크 +- R-1 (High): admin/bootstrap credential을 ad-hoc 방식으로 공유하면 secret leakage가 발생할 수 있다. + - 완화 방안: secret manager 또는 승인된 보관소만 사용하고, 저장소/문서에는 raw value를 남기지 않는다. +- R-2 (Medium): first-run bootstrap 계정 경로만 정리하고 steady-state 운영 경로를 정의하지 않으면, 이미 사용자가 있는 실배포 환경에서는 같은 문제를 반복한다. + - 완화 방안: bootstrap과 steady-state access/recovery를 별도 요구사항으로 분리해 정의한다. +- R-3 (Medium): 로그 접근 경로가 환경 의존적이면 release 증적 수집이 담당자 개인 지식에 묶인다. + - 완화 방안: 최소한의 환경 계약(예: 어느 시스템에서 startup/request 로그를 확인하는지)을 문서화한다. + +## 가정 +아직 확정되지 않았지만 현재 문서 작성의 기반이 되는 전제를 적는다. + +- A-1: 실배포 환경은 DB-backed `users` / `auth_tokens`를 사용하며, service-token은 DB principal 권한으로 해석된다. +- A-2: admin 성공 경로는 현재 완전히 없는 것이 아니라, 운영자에게 문서화되지 않았거나 credential provenance가 정리되지 않은 상태다. +- A-3: release 검증에는 최소한 public/no-auth, read service-token, trade-write service-token, admin auth context 4개 관점이 필요하다. + +## 오픈 질문 +답을 아직 모르는 항목을 적는다. 추정으로 덮지 말고 REQ 초안 작성 이전에 확인할 질문으로 남긴다. + +- OQ-1: steady-state admin access의 표준 경로는 무엇인가? bootstrap admin 로그인, env admin token, DB-issued admin session/token, 별도 운영 절차 중 무엇을 canonical path로 삼을지. + - 상태: Open + - 처리 방안: 운영자와 승인자가 실제 배포 체계를 확인하고 canonical path를 하나로 선택한다. + - 종료 조건: REL-001 후속 release 검증에 사용할 admin path가 문서화되면 Answered 처리. +- OQ-2: startup/request 로그는 어디에서 확인하는가? 호스팅 환경, reverse proxy, Loki 등 실제 로그 경로와 접근 권한을 어떻게 운영자에게 부여하는가. + - 상태: Open + - 처리 방안: 현재 실배포 로그 스택과 접근 권한 모델을 확인한 뒤 runbook에 반영한다. + - 종료 조건: 실제 로그 수집 절차가 문서에 추가되면 Answered 처리. +- OQ-3: token provenance 확인 수단은 문서화만으로 충분한가, 아니면 최소한의 CLI/관리 기능이 필요한가. + - 상태: Open + - 처리 방안: runbook-only 접근과 지원 기능 추가 비용을 비교해 결정한다. + - 종료 조건: REQ 초안 단계에서 지원 수단이 문서-only인지 구현 포함인지 확정되면 Answered 처리. + +### 오픈 질문 처리 기준 +- `Open`: 아직 답이 없고 다음 단계 진행에 영향이 있는 상태다. 문서에 유지한다. +- `Answered`: 답이 확정된 상태다. 관련 본문/요구사항에 반영한 뒤 오픈 질문 목록에서는 제거하거나 resolved 기록으로 이동한다. +- `Deferred`: 지금 단계에서 결정하지 않고 다음 단계 또는 후속 이슈로 넘기는 상태다. 넘길 대상과 이유를 반드시 적는다. +- `Dropped`: 더 이상 유효하지 않은 질문이다. 삭제 사유를 짧게 남기고 목록에서 제거한다. +- 처리 원칙 1: 질문만 남기지 말고 반드시 `상태`, `처리 방안`, `종료 조건`을 함께 적는다. +- 처리 원칙 2: 답이 나온 질문은 본문 반영 없이 단순 삭제하지 않는다. 반영 위치를 먼저 갱신한다. +- 처리 원칙 3: High 영향 질문은 `Deferred` 처리 후 처리 방안에 REQ 인계 추적 방식을 명시한다. + +# 7. 초기 성공 기준 +이 변경이 성공했다고 판단할 기준을 측정 가능하게 정의한다. 가능하면 데이터 출처까지 연결한다. + +- S-1: 운영자가 admin/read/trade-write/public 검증 경로를 혼동 없이 수행할 수 있는 runbook을 보유한다. +- S-2: release 검증 시 `POST /auth/service-tokens` 성공 경로와 `POST /v1/assets/{asset_id}/ledger/trades` 권한 경로를 적절한 credential로 재현할 수 있다. +- S-3: startup/request 로그 접근 경로와 release evidence 기록 방식이 REL 문서에 반영된다. + +## 측정 방식 및 데이터 출처 +각 성공 기준을 어떻게 측정할지와 어떤 데이터나 문서를 근거로 쓸지 적는다. + +- S-1 측정 방식: 운영 runbook 리뷰 및 smoke rehearsal / 데이터 출처: README.md 또는 신규 운영 문서, release verification 기록 +- S-2 측정 방식: 실배포 또는 staging smoke에서 admin/read/trade-write 경로를 각각 실행 / 데이터 출처: release 문서, HTTP 응답 로그, 운영자가 제공한 검증 기록 +- S-3 측정 방식: release 문서와 baseline 문서에 로그 접근 및 증적 수집 항목 포함 여부 확인 / 데이터 출처: docs/releases/*.md, docs/runtime-flows.md, docs/project-structure.md + +# 8. REQ로 넘기기 전 확인 체크 +REQ 초안 작성 단계로 넘기기 전에 무엇이 확정되어야 하는지 확인한다. 미완료 항목은 후속 결정이 필요함을 뜻한다. + +- [ ] 범위(In Scope/Out of Scope) 확정 + - 기준: admin bootstrap, steady-state access, logging scope의 포함/제외 범위 합의 +- [ ] 성공 지표의 측정 방식/데이터 출처 확정 + - 기준: staging/production 중 어디서 smoke를 수행할지와 evidence 위치 확정 +- [ ] High 영향 리스크 우선순위 및 해소 책임자 지정 + - 기준: secret leakage와 admin recovery 책임자 지정 +- [ ] 모든 오픈 질문(OQ) `Open` 상태 없음 확인 + - 기준: OQ 목록의 `Open` 상태가 0개 +- [ ] Freeze 확정 담당자(CONFIRMED_BY) 지정 + - 기준: 운영 승인자 또는 repository-maintainers 중 확인자 지정 + +# 9. 파일 처리 결과 +이 Discovery 문서 생성/갱신 결과와 참조 문서를 기록한다. 이후 추적과 리뷰 시 근거로 사용한다. + +- 처리 결과: 생성 +- 생성 경로: docs/discovery/dcy-005_20260511_production-admin-bootstrap-and-operator-access.md +- 참조 문서: + - docs/releases/rel-001_20260502_trex-basic-v1-api-contract.md + - app/core/auth.py + - app/api/auth_routes.py + - app/services/auth_service.py + - README.md + +# 10. 사용자 결정 필요 항목 요약 +사용자나 승인자가 결정해야 하는 항목만 모아 정리한다. 결정, 확인, 데이터 요청을 구분해서 적는다. + +## DECIDE +- {{DECIDE_CANONICAL_ADMIN_ACCESS_PATH}} +- {{DECIDE_SMOKE_CREDENTIAL_OWNERSHIP_POLICY}} + +## CONFIRM +- {{CONFIRM_LOG_ACCESS_CHANNEL}} +- {{CONFIRM_BOOTSTRAP_VS_STEADY_STATE_RECOVERY_SCOPE}} + +## DATA +- {{DATA_SECRET_STORE_LOCATION}} +- {{DATA_PRODUCTION_LOG_STACK}} + +# 11. Discovery Freeze +REQ 초안 작성 인계 시점의 확정 플래그와 섹션 참조만 기록한다. 내용 재작성 없이 원본 섹션을 링크로 대신한다. + +## 섹션 참조 +- 범위: [# 5. 요구사항 목록 > 범위 경계](#5-요구사항-목록) +- 리스크/오픈 질문: [# 6. 리스크/가정 목록](#6-리스크가정-목록) +- 성공 기준: [# 7. 초기 성공 기준](#7-초기-성공-기준) +- 인계 전 확인: [# 8. REQ로 넘기기 전 확인 체크](#8-req로-넘기기-전-확인-체크) +- 사용자 결정 필요: [# 10. 사용자 결정 필요 항목 요약](#10-사용자-결정-필요-항목-요약) + +## Freeze 플래그 +- Handoff Decision: 보류 +- Handoff Rationale: 운영 경로의 핵심 선택지(admin canonical path, 로그 접근, smoke credential provenance)가 아직 Open 상태라 REQ 초안 작성 전 사용자 확인이 필요하다. +- Ready for REQ Drafting: false +- Confirmed By: TBD +- Confirmed At (KST): TBD + +# 12. 작성 가이드 +템플릿 사용 시의 기본 규칙을 적는다. 실제 문서를 만들 때는 이 가이드를 기준으로 플레이스홀더를 치환한다. + +- 이 파일은 템플릿이다. 실제 문서 작성 시 `{{...}}` 플레이스홀더를 구체 값으로 교체한다. +- 필요 없는 항목은 삭제하고, 부족한 항목은 같은 형식으로 추가한다. +- Open Questions는 가정으로 대체하지 말고 그대로 유지한다. +- `대체됨`은 현재 Discovery가 더 이상 기준 문서가 아닐 때 대체 Discovery 경로를 기록하고, 해당 사항이 없으면 `없음`으로 둔다. +- `후속 Discovery 참조`는 범위를 이어받는 별도 Discovery가 생성된 경우 그 Discovery 경로를 기록하고, 없으면 `없음`으로 둔다. +- Discovery Freeze는 문서 하단에 반드시 유지한다. \ No newline at end of file diff --git a/docs/discovery/index.md b/docs/discovery/index.md index 9a6b3b9..550a4b8 100644 --- a/docs/discovery/index.md +++ b/docs/discovery/index.md @@ -9,4 +9,5 @@ | dcy-001_20260430_real-db-migration-validation-and-operations-readiness | 20260430 | 실DB 마이그레이션 검증 및 운영 준비 | [Discovery](./dcy-001_20260430_real-db-migration-validation-and-operations-readiness.md) | confirmed | | dcy-002_20260502_trex-basic-consumer-api-contract | 20260502 | trex-basic Consumer-Driven Contract 반영 — service-token runtime_id 발급 및 v1 API 엔드포인트 구현 | [Discovery](./dcy-002_20260502_trex-basic-consumer-api-contract.md) | confirmed | | dcy-003_20260502_portfolio-list-user-required | 20260502 | CLI `pfhub portfolio list` — `--user` 옵션 필수화 및 소유자 기반 조회 강제 | [Discovery](./dcy-003_20260502_portfolio-list-user-required.md) | confirmed | -| dcy-004_20260511_portfolio-id-uuid-migration-and-user-scoped-name-resolution | 20260511 | portfolio_id UUID 교체 및 CLI 사용자 범위 기반 자산 식별 구조 전환 | [Discovery](./dcy-004_20260511_portfolio-id-uuid-migration-and-user-scoped-name-resolution.md) | confirmed | \ No newline at end of file +| dcy-004_20260511_portfolio-id-uuid-migration-and-user-scoped-name-resolution | 20260511 | portfolio_id UUID 교체 및 CLI 사용자 범위 기반 자산 식별 구조 전환 | [Discovery](./dcy-004_20260511_portfolio-id-uuid-migration-and-user-scoped-name-resolution.md) | confirmed | +| dcy-005_20260511_production-admin-bootstrap-and-operator-access | 20260511 | 실배포 admin bootstrap 및 operator access 경로 정립 | [Discovery](./dcy-005_20260511_production-admin-bootstrap-and-operator-access.md) | draft | \ No newline at end of file diff --git a/docs/releases/rel-001_20260502_trex-basic-v1-api-contract.md b/docs/releases/rel-001_20260502_trex-basic-v1-api-contract.md index 5e6b432..27bc99b 100644 --- a/docs/releases/rel-001_20260502_trex-basic-v1-api-contract.md +++ b/docs/releases/rel-001_20260502_trex-basic-v1-api-contract.md @@ -43,18 +43,18 @@ ### 배포 전 -- [ ] `CREDENTIAL_ENCRYPTION_KEY` 환경 변수 설정 완료 -- [ ] `alembic upgrade head` 성공 (migration 0014) -- [ ] pytest 98개 로컬 PASS 확인 (2026-05-01 완료) +- [x] `CREDENTIAL_ENCRYPTION_KEY` 환경 변수 설정 완료 +- [x] `alembic upgrade head` 성공 (migration 0014) +- [x] pytest 로컬 PASS 확인 (`python -m pytest -q` → `129 passed in 4.46s`) ### 배포 후 -- [ ] GET /health → 200, body에 `status`, `service`, `version`, `checked_at` 포함 확인 -- [ ] GET /health → `checked_at`에 `+09:00` 포함 확인 +- [x] GET /health → 200, body에 `status`, `service`, `version`, `checked_at` 포함 확인 +- [x] GET /health → `checked_at`에 `+09:00` 포함 확인 - [ ] POST /auth/service-tokens (admin 토큰) → 201, `runtime_id` 포함 확인 -- [ ] GET /v1/assets (service-token) → 200, `assets[]` 반환 확인 +- [x] GET /v1/assets (service-token) → 200, `assets[]` 반환 확인 - [ ] GET /v1/assets (service-token) → service-token 소유자의 portfolio만 반환되고 다른 사용자 portfolio가 비노출인지 확인 (REQ-006 owner scope 재확인) -- [ ] GET /v1/user_info (service-token) → 200, `trading_enabled`/`brokers[]` 포함 확인 +- [x] GET /v1/user_info (service-token) → 200, `trading_enabled`/`brokers[]` 포함 확인 - [ ] POST /v1/assets/{asset_id}/ledger/trades → 201 (신규), 200 (중복 동일 payload) 확인 - [ ] 서비스 startup 로그에 RuntimeError 없음 확인 @@ -75,19 +75,38 @@ - Revision or Artifact: - (미기록 — 커밋 SHA 배포 후 기록) - Commands or Procedure: - - (미기록) + - `CREDENTIAL_ENCRYPTION_KEY` 설정 확인 완료 + - `python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"` 실행으로 Fernet 키 생성 형식 확인 완료 + - 보안상 실제 환경 변수 값과 생성된 키 원문은 문서에 기록하지 않음 + - `alembic upgrade head` 실행 완료 + - Alembic 출력: `Context impl PostgresqlImpl.` / `Will assume transactional DDL.` + - 추가 revision 실행 로그는 출력되지 않았고, 명시적 오류 메시지는 없었음 + - 로컬 회귀 검증: `python -m pytest -q` → `129 passed in 4.46s` + - 배포 public smoke: `curl https://fourblocks2026.com/portfolio-hub/health` → HTTP 200, `{"status":"ok","service":"portfolio-hub","version":"0.1.0","checked_at":"2026-05-11T21:35:54.686819+09:00"}` + - 배포 public contract 확인: `/portfolio-hub/docs` → HTTP 200, `/portfolio-hub/openapi.json` → HTTP 200 + - 실배포 설정 파일 `temp/real_portfolio-hub_environment`의 service token으로 protected read smoke 실행 + - `GET /v1/assets` → HTTP 200, body 요약: `total=0`, `assets=[]` + - `GET /v1/user_info` → HTTP 200, body 요약: `runtime_id` 존재, `trading_enabled=true`, `permission=read`, `brokers=[]` + - `POST /auth/service-tokens` with same token → HTTP 403 `Insufficient permission` (admin 토큰 미보유) + - `POST /v1/assets/00000000-0000-0000-0000-000000000001/ledger/trades` with same read service token → HTTP 404 `Asset not found ...` + - 위 ledger 결과는 계약상 trade-write 부족이면 403이어야 한다는 기대와 다르므로, write permission enforcement 재확인 필요 + - 현재 보유 credential만으로는 admin 성공 경로와 trade-write 성공 경로를 완료할 수 없음 ### Profile-Specific Checks - app-service: - Health Endpoint: - - (미수행 — 배포 후 확인 필요) + - `GET https://fourblocks2026.com/portfolio-hub/health` → HTTP 200 확인. body에 `status=ok`, `service=portfolio-hub`, `version=0.1.0`, `checked_at=2026-05-11T21:35:54.686819+09:00` 포함. - Startup Log Fields: - - (미수행 — 배포 후 확인 필요) + - 외부 URL만으로는 startup 로그 접근 불가. 배포 환경 로그 접근 권한 필요. - Request Log Fields: - - (미수행 — 배포 후 확인 필요) + - 외부 URL만으로는 request 로그 접근 불가. 배포 환경 로그 접근 권한 필요. - Smoke or Manual Verification: - - (미수행 — Verification Checklist 참조) + - public smoke 완료 (`/health`, `/docs`, `/openapi.json`). + - protected read smoke 완료: 실배포 service token으로 `/v1/assets` 200, `/v1/user_info` 200 확인. + - `/v1/assets` 응답은 빈 목록(`total=0`)이라 owner scope 비노출은 강하게 입증되지 않음. + - same token으로 `/auth/service-tokens`는 403 `Insufficient permission` 확인되어 admin 성공 경로는 미검증. + - same token으로 `/v1/assets/{asset_id}/ledger/trades` 호출 시 404 `Asset not found`가 반환되어, trade-write 미보유 토큰에 대한 403 차단 여부가 계약과 다름. write path는 release blocker로 재확인 필요. ### Rollback Outcome @@ -103,7 +122,7 @@ - Released Decision: - (미기록) - Rationale: - - (미기록) + - public health 및 read service-token smoke는 확인됐으나, admin 성공 경로와 trade-write 성공 경로가 아직 완료되지 않았다. 특히 ledger/trades가 read token에 대해 계약 기대(403) 대신 404를 반환해 permission enforcement 재확인이 필요하므로 released 전환 보류 - Recorded By: - (미기록) - Recorded At: diff --git a/tests/test_v1.py b/tests/test_v1.py index a3d8bfd..482816f 100644 --- a/tests/test_v1.py +++ b/tests/test_v1.py @@ -528,6 +528,34 @@ def test_v1_ledger_trade_asset_not_found(monkeypatch) -> None: assert resp.status_code == 404 +def test_v1_ledger_trade_requires_trade_write_permission(monkeypatch) -> None: + """Read-only service token must be rejected before asset lookup.""" + _make_app_db(monkeypatch) + + principal = TokenPrincipal( + token_id="t1", + user_id="user-1", + permission="read", + token_type="service", + runtime_id="rt-1", + ) + mock_repo = MagicMock() + mock_repo.get_token_principal.return_value = principal + + with patch("app.core.auth.get_portfolio_repository_from_settings", return_value=mock_repo): + client = _make_app(_make_settings(credential_encryption_key=_TEST_ENC_KEY)) + resp = client.post( + "/v1/assets/005930/ledger/trades", + json=_trade_payload(), + headers=_service_token_header(), + ) + + assert resp.status_code == 403 + assert resp.json()["detail"] == "Insufficient permission" + mock_repo.get.assert_not_called() + mock_repo.insert_ledger_trade.assert_not_called() + + # --------------------------------------------------------------------------- # REQ-010: startup validation # ---------------------------------------------------------------------------