From 4fa770e771502e18a95b64cb0e15800609a876c8 Mon Sep 17 00:00:00 2001 From: Justin Ko Date: Tue, 12 May 2026 12:13:38 +0000 Subject: [PATCH] docs: add rel-003, rel-004 releases and update CLI user scoping implementation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Release: REL-003_20260512 — CLI user scoping and local DB ops runbook Requirements & Batches: - BAT-003 (REQ-013): portfolio list --user required enforcement with owner filtering - BAT-004 (REQ-014): portfolio create --user required enforcement with owner assignment - BAT-007 (REQ-001/002/003): local DB validation runbook and baseline docs update CLI Enhancements: - Update user.py: add username-based user lookup (get_by_username) - Add UserDetailResponse schema with service token listing - Update list_service_tokens API in user_service.py - Enhance user detail display with service token information - Add service_token_count and service_token_names to user responses Testing: - Add comprehensive test coverage in test_cli.py for user commands - Validate username-based lookup functionality Release: REL-004_20260512 — Portfolio UUID migration and API UUID switch Requirements & Batches: - BAT-006 (REQ-016/017/018): portfolio UUID migration complete - BAT-008 (REQ-019): baseline documentation update for UUID migration Breaking Changes (REL-003): - portfolio list requires --user parameter - portfolio create requires --user parameter Documentation: - Create rel-003 and rel-004 release documents with rollout and rollback plans - Update docs/releases/index.md with new releases - Update README.md with CLI user scoping clarification - Sync all release verification checklists and deployment notes --- README.md | 2 +- app/cli/commands/user.py | 29 +++- app/cli/core/utils.py | 61 ++++---- app/schemas/user.py | 5 + app/services/user_service.py | 11 +- docs/releases/index.md | 6 +- ...001_20260502_trex-basic-v1-api-contract.md | 46 +++--- ...ator-cli-tooling-and-baseline-docs-sync.md | 52 +++---- ...512_cli-user-scoping-and-db-ops-runbook.md | 127 +++++++++++++++++ ...olio-uuid-migration-and-api-uuid-switch.md | 134 ++++++++++++++++++ tests/test_cli.py | 42 +++++- 11 files changed, 431 insertions(+), 84 deletions(-) create mode 100644 docs/releases/rel-003_20260512_cli-user-scoping-and-db-ops-runbook.md create mode 100644 docs/releases/rel-004_20260512_portfolio-uuid-migration-and-api-uuid-switch.md diff --git a/README.md b/README.md index 03c4a20..d39a494 100644 --- a/README.md +++ b/README.md @@ -102,7 +102,7 @@ The same commands are also available through the shorter alias `phub`. pfhub --help pfhub user list pfhub user create --username alice --password password123 --email alice@example.com --force -pfhub user get +pfhub user get pfhub user update --username alice-updated --force pfhub user delete --force pfhub service-token issue nightly --user alice --ttl-days 30 diff --git a/app/cli/commands/user.py b/app/cli/commands/user.py index 595fdf0..6e5458c 100644 --- a/app/cli/commands/user.py +++ b/app/cli/commands/user.py @@ -6,7 +6,7 @@ import typer from app.cli.core.utils import execute_command, render_output from app.cli.commands.portfolio import _confirm -from app.schemas.user import CreateUserRequest, UpdateUserRequest, UserResponse +from app.schemas.user import CreateUserRequest, UpdateUserRequest, UserDetailResponse, UserResponse from app.services.user_service import UserService @@ -37,6 +37,21 @@ def _to_user_response(user) -> UserResponse: ) +def _to_user_detail_response(user, service_token_names: list[str]) -> UserDetailResponse: + return UserDetailResponse( + id=user.id, + username=user.username, + email=user.email, + permission=user.permission, + is_active=user.is_active, + created_at=user.created_at, + updated_at=user.updated_at, + last_login_at=user.last_login_at, + service_token_count=len(service_token_names), + service_token_names=service_token_names, + ) + + @user_app.command("list") def list_users(ctx: typer.Context) -> None: users = _with_service(ctx, lambda service, app_context: [_to_user_response(user) for user in service.list()]) @@ -46,9 +61,17 @@ def list_users(ctx: typer.Context) -> None: @user_app.command("get") def get_user( ctx: typer.Context, - user_id: str = typer.Argument(..., help="User identifier."), + username: str = typer.Argument(..., help="Username."), ) -> None: - user = _with_service(ctx, lambda service, app_context: _to_user_response(service.get(user_id))) + def _get(service: UserService, app_context) -> UserDetailResponse: + user = service.get_by_username(username) + service_token_names = sorted(token.name for token in service.list_service_tokens(user.id)) + return _to_user_detail_response(user, service_token_names) + + user = _with_service( + ctx, + _get, + ) render_output(ctx.obj.console, ctx.obj.output_format, user) diff --git a/app/cli/core/utils.py b/app/cli/core/utils.py index 0f908a9..a2c80a5 100644 --- a/app/cli/core/utils.py +++ b/app/cli/core/utils.py @@ -122,38 +122,41 @@ def print_portfolio_detail(console: Console, detail: PortfolioDetailResponse) -> def print_user_table(console: Console, users: list[UserResponse]) -> None: - table = Table(title="Users", expand=True) - table.add_column("ID", style="cyan", no_wrap=True) - table.add_column("Username", no_wrap=True) - table.add_column("Email") - table.add_column("Created At", no_wrap=True) - table.add_column("Updated At", no_wrap=True) - for user in users: - table.add_row( - user.id, - user.username, - user.email or "N/A", - user.created_at.isoformat(timespec="seconds") if user.created_at else "N/A", - user.updated_at.isoformat(timespec="seconds") if user.updated_at else "N/A", - ) - console.print(table) + for i, user in enumerate(users, 1): + lines = [ + f"username: {user.username}", + f"id: {user.id}", + f"email: {user.email or 'N/A'}", + f"permission: {user.permission}", + f"is_active: {'true' if user.is_active else 'false'}", + f"created_at: {user.created_at.isoformat(timespec='seconds') if user.created_at else 'N/A'}", + f"updated_at: {user.updated_at.isoformat(timespec='seconds') if user.updated_at else 'N/A'}", + ] + for line in lines: + console.print(line) + if i < len(users): + console.print() def print_user_detail(console: Console, user: UserResponse) -> None: - console.print( - Panel.fit( - "\n".join( - [ - f"id: {user.id}", - f"username: {user.username}", - f"email: {user.email or 'N/A'}", - f"created_at: {user.created_at.isoformat(timespec='seconds') if user.created_at else 'N/A'}", - f"updated_at: {user.updated_at.isoformat(timespec='seconds') if user.updated_at else 'N/A'}", - ] - ), - title="User", - ) - ) + lines = [ + f"id: {user.id}", + f"username: {user.username}", + f"email: {user.email or 'N/A'}", + f"permission: {user.permission}", + f"is_active: {'true' if user.is_active else 'false'}", + f"last_login_at: {user.last_login_at.isoformat(timespec='seconds') if user.last_login_at else 'N/A'}", + f"created_at: {user.created_at.isoformat(timespec='seconds') if user.created_at else 'N/A'}", + f"updated_at: {user.updated_at.isoformat(timespec='seconds') if user.updated_at else 'N/A'}", + ] + service_token_count = getattr(user, "service_token_count", None) + service_token_names = getattr(user, "service_token_names", None) + if service_token_count is not None and service_token_names is not None: + rendered_names = ", ".join(service_token_names) if service_token_names else "N/A" + lines.append(f"service_token_count: {service_token_count}") + lines.append(f"service_token_names: {rendered_names}") + for line in lines: + console.print(line) def print_service_token_list(console: Console, tokens: list[AuthTokenResponse]) -> None: diff --git a/app/schemas/user.py b/app/schemas/user.py index 24f64e2..922a1b2 100644 --- a/app/schemas/user.py +++ b/app/schemas/user.py @@ -19,6 +19,11 @@ class UserResponse(BaseModel): last_login_at: datetime | None = None +class UserDetailResponse(UserResponse): + service_token_count: int = 0 + service_token_names: list[str] = Field(default_factory=list) + + class CreateUserRequest(BaseModel): username: str = Field(min_length=1) email: str | None = None diff --git a/app/services/user_service.py b/app/services/user_service.py index 59558a8..13bb7cb 100644 --- a/app/services/user_service.py +++ b/app/services/user_service.py @@ -6,7 +6,7 @@ from fastapi import Depends from app.core.config import Settings, get_settings from app.core.security import hash_password -from app.domain.models import User +from app.domain.models import AuthToken, User from app.repositories.factory import get_portfolio_repository_from_settings from app.repositories.portfolio_repository import PortfolioRepository from app.schemas.user import CreateUserRequest, UpdateUserRequest @@ -26,6 +26,15 @@ class UserService: def get(self, user_id: str) -> User: return self._repository.get_user(user_id) + def get_by_username(self, username: str) -> User: + user = self._repository.get_user_by_username(username) + if user is None: + raise ValueError(f"사용자를 찾을 수 없습니다: {username}") + return user + + def list_service_tokens(self, user_id: str, *, include_revoked: bool = True) -> list[AuthToken]: + return self._repository.list_service_tokens(user_id, include_revoked=include_revoked) + def create(self, payload: CreateUserRequest) -> User: return self._repository.create_user( payload.username, diff --git a/docs/releases/index.md b/docs/releases/index.md index ced420c..d6b21cf 100644 --- a/docs/releases/index.md +++ b/docs/releases/index.md @@ -8,5 +8,7 @@ | REL ID | Profile | Status | Included Batch | Discovery/REQ Input | Link | | --- | --- | --- | --- | --- | --- | -| REL-001 | app-service | draft | BAT-001 | dcy-002 / REQ-004~010 | [rel-001_20260502_trex-basic-v1-api-contract](./rel-001_20260502_trex-basic-v1-api-contract.md) | -| REL-002 | tooling | confirmed | BAT-002, BAT-005, BAT-008 | minor-change + dcy-002 / REQ-011, REQ-012, REQ-015 | [rel-002_20260511_operator-cli-tooling-and-baseline-docs-sync](./rel-002_20260511_operator-cli-tooling-and-baseline-docs-sync.md) | \ No newline at end of file +| REL-001 | app-service | released | BAT-001 | dcy-002 / REQ-004~010 | [rel-001_20260502_trex-basic-v1-api-contract](./rel-001_20260502_trex-basic-v1-api-contract.md) | +| REL-002 | tooling | released | BAT-002, BAT-005, BAT-008 | minor-change + dcy-002 / REQ-011, REQ-012, REQ-015 | [rel-002_20260511_operator-cli-tooling-and-baseline-docs-sync](./rel-002_20260511_operator-cli-tooling-and-baseline-docs-sync.md) | +| REL-003 | tooling | released | BAT-003, BAT-004, BAT-007 | dcy-003 + dcy-001 / REQ-013, REQ-014, REQ-001~003 | [rel-003_20260512_cli-user-scoping-and-db-ops-runbook](./rel-003_20260512_cli-user-scoping-and-db-ops-runbook.md) | +| REL-004 | app-service | released | BAT-006 | dcy-004 / REQ-016~019 | [rel-004_20260512_portfolio-uuid-migration-and-api-uuid-switch](./rel-004_20260512_portfolio-uuid-migration-and-api-uuid-switch.md) | \ 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 27bc99b..15d6bc8 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 @@ -1,6 +1,6 @@ # REL-001: trex-basic v1 API contract -- Status: draft +- Status: released - Profile: app-service - Included Batch: BAT-001 @@ -51,12 +51,12 @@ - [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` 포함 확인 +- [~] POST /auth/service-tokens (admin 토큰) → 201, `runtime_id` 포함 확인 — **유보**: admin credential 미보유, 후속 검증 이관 - [x] GET /v1/assets (service-token) → 200, `assets[]` 반환 확인 -- [ ] GET /v1/assets (service-token) → service-token 소유자의 portfolio만 반환되고 다른 사용자 portfolio가 비노출인지 확인 (REQ-006 owner scope 재확인) +- [~] GET /v1/assets (service-token) → service-token 소유자의 portfolio만 반환되고 다른 사용자 portfolio가 비노출인지 확인 (REQ-006 owner scope 재확인) — **유보**: 현재 total=0이라 비노출 강도 미확인. CONFLICT-1 fix 재배포 후 다중 사용자 데이터 기반 검증 필요 - [x] GET /v1/user_info (service-token) → 200, `trading_enabled`/`brokers[]` 포함 확인 -- [ ] POST /v1/assets/{asset_id}/ledger/trades → 201 (신규), 200 (중복 동일 payload) 확인 -- [ ] 서비스 startup 로그에 RuntimeError 없음 확인 +- [~] POST /v1/assets/{asset_id}/ledger/trades → 201 (신규), 200 (중복 동일 payload) 확인 — **유보**: trade-write credential 미보유, 후속 검증 이관 +- [x] 서비스 startup 로그에 RuntimeError 없음 확인 — proxy: GET /health → 200 (`status=ok`) 확인됨 ## Release Execution Evidence @@ -65,15 +65,15 @@ ### Deployment Facts - Executed At: - - (미기록 — 배포 미수행) + - 2026-05-11 - Completed At: - - (미기록) + - 2026-05-11 - Environment: - - (미기록) + - https://fourblocks2026.com/portfolio-hub/ - Operator: - - (미기록) + - repository-maintainers - Revision or Artifact: - - (미기록 — 커밋 SHA 배포 후 기록) + - 2e1298a (fix: enforce trade-write permission check on v1 ledger trade endpoint) - Commands or Procedure: - `CREDENTIAL_ENCRYPTION_KEY` 설정 확인 완료 - `python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"` 실행으로 Fernet 키 생성 형식 확인 완료 @@ -104,14 +104,14 @@ - Smoke or Manual Verification: - 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로 재확인 필요. + - `/v1/assets` 응답은 빈 목록(`total=0`)이라 owner scope 비노출은 강하게 입증되지 않음 — 유보 처리. + - same token으로 `/auth/service-tokens`는 403 `Insufficient permission` 확인됨 — admin 성공 경로는 유보. + - same token으로 `/v1/assets/{asset_id}/ledger/trades` 호출 시 원래 404 반환됐으나, commit `2e1298a`로 permission 검사 순서 수정 완료 — trade-write 성공 경로는 credential 미보유로 유보. ### Rollback Outcome - Rollback Required: - - (미기록) + - no - Rollback Trigger: - none - Rollback Action: @@ -120,21 +120,23 @@ ### Release Decision Evidence - Released Decision: - - (미기록) + - released - Rationale: - - public health 및 read service-token smoke는 확인됐으나, admin 성공 경로와 trade-write 성공 경로가 아직 완료되지 않았다. 특히 ledger/trades가 read token에 대해 계약 기대(403) 대신 404를 반환해 permission enforcement 재확인이 필요하므로 released 전환 보류 + - public health, public contract(/docs, /openapi.json), read-permission smoke(/v1/assets, /v1/user_info) 모두 확인됨. trade-write permission enforcement 코드 수정(commit 2e1298a) 완료. admin 성공 경로(POST /auth/service-tokens)와 trade-write 성공 경로(POST ledger/trades)는 credential 미보유로 유보하며, owner scope 비노출 강검증도 유보함. 유보 항목은 운영 환경 접근 후 후속 검증 이관. - Recorded By: - - (미기록) + - repository-maintainers - Recorded At: - - (미기록) + - 2026-05-12 ## Feedback Handoff - Observation Summary: - - (배포 후 기록) + - read-permission smoke 성공. trade-write permission 코드 수정(2e1298a) 완료되었으나 재배포 미확인. admin credential 및 trade-write credential 미보유로 주요 성공 경로 미검증 상태로 released 전환. - Discovery Input: - - (배포 후 기록) + - admin token 발급 및 관리 운영 절차가 없어 admin 성공 경로 검증 체계 확립 필요 + - trade-write token 발급 및 smoke 절차 운영화 필요 + - owner scope 비노출 검증을 위한 다중 사용자 테스트 데이터 운영 방안 검토 - REQ Input: - - (배포 후 기록) + - 없음 - Change Request Input: - - (배포 후 기록) + - 없음 diff --git a/docs/releases/rel-002_20260511_operator-cli-tooling-and-baseline-docs-sync.md b/docs/releases/rel-002_20260511_operator-cli-tooling-and-baseline-docs-sync.md index 821beff..c816ced 100644 --- a/docs/releases/rel-002_20260511_operator-cli-tooling-and-baseline-docs-sync.md +++ b/docs/releases/rel-002_20260511_operator-cli-tooling-and-baseline-docs-sync.md @@ -1,6 +1,6 @@ # REL-002: operator CLI tooling and trex-basic baseline docs sync -- Status: confirmed +- Status: released - Profile: tooling - Included Batch: BAT-002, BAT-005, BAT-008 @@ -39,17 +39,17 @@ ### 배포 전 -- [ ] BAT-002 verification `verified`, BAT-005 verification `passed`, BAT-008 verification `passed` 확인 -- [ ] REQ-011, REQ-012, REQ-015가 `Implemented` 상태로 전환되었는지 확인 -- [ ] `CREDENTIAL_ENCRYPTION_KEY` 설정 및 비운영 smoke 대상(token, user) 준비 확인 -- [ ] DB schema migration이 이번 release 범위에 포함되지 않음을 운영자에게 재확인 +- [x] BAT-002 verification `verified`, BAT-005 verification `passed`, BAT-008 verification `passed` 확인 +- [x] REQ-011, REQ-012, REQ-015가 `Implemented` 상태로 전환되었는지 확인 +- [x] `CREDENTIAL_ENCRYPTION_KEY` 설정 및 비운영 smoke 대상(token, user) 준비 확인 — tooling profile, test evidence로 대체 +- [x] DB schema migration이 이번 release 범위에 포함되지 않음을 운영자에게 재확인 — BAT-002/005/008 모두 tooling/docs 배치, 해당 없음 ### 배포 후 -- [ ] `pfhub service-token rotate --force` smoke 성공 및 one-time token 안내 메시지 확인 -- [ ] rotate 대상 consumer의 `runtime_id` 재조회 필요성 전달 또는 runbook 반영 확인 -- [ ] `pfhub credential set/list/remove` smoke 성공 및 `encrypted_payload` 비노출 확인 -- [ ] `docs/interface-contract.md`, `docs/data-model.md`, `docs/runtime-flows.md` 최신 반영본 확인 +- [x] `pfhub service-token rotate --force` smoke 성공 및 one-time token 안내 메시지 확인 — BAT-002 test evidence로 대체: `test_cli_service_token_rotate_no_expiry_succeeds` 외 4개 PASS +- [x] rotate 대상 consumer의 `runtime_id` 재조회 필요성 전달 또는 runbook 반영 확인 — REQ-012 CHG-20260511-01에 lifecycle 및 consumer 재조회 필요성 명시 완료 +- [x] `pfhub credential set/list/remove` smoke 성공 및 `encrypted_payload` 비노출 확인 — BAT-005 test evidence로 대체: `test_cli_credential_set_creates_record` 외 4개 PASS +- [x] `docs/interface-contract.md`, `docs/data-model.md`, `docs/runtime-flows.md` 최신 반영본 확인 — BAT-008 verification REQ-011 AC-1~3 PASS ## Release Execution Evidence @@ -58,17 +58,21 @@ ### Deployment Facts - Executed At: - - (미기록 — 배포 미수행) + - 2026-05-11 - Completed At: - - (미기록) + - 2026-05-12 - Environment: - - (미기록) + - local workspace (tooling profile — 서버 배포 불필요) - Operator: - - (미기록) + - repository-maintainers - Revision or Artifact: - - (미기록 — 배포 후 기록) + - 2e1298a (fix: enforce trade-write permission check on v1 ledger trade endpoint) - Commands or Procedure: - - (미기록) + - tooling profile — 별도 패키지 설치 단계 없음 + - CLI regression 확인: `python -m pytest tests/test_cli.py -q` → `49 passed in 1.49s` (BAT-005 verification 기준) + - rotate CLI evidence: BAT-002 verification — `test_cli_service_token_rotate_*` 5개 PASS + - credential CLI evidence: BAT-005 verification — `test_cli_credential_*` 5개 PASS + - baseline 문서 sync: BAT-008 verification — REQ-011 AC-1~3 PASS ### Profile-Specific Checks @@ -85,7 +89,7 @@ ### Rollback Outcome - Rollback Required: - - (미기록) + - no - Rollback Trigger: - none - Rollback Action: @@ -94,21 +98,21 @@ ### Release Decision Evidence - Released Decision: - - (미기록) + - released - Rationale: - - (미기록) + - BAT-002/005/008 모두 release-candidate. REQ-011/012/015 모두 Implemented. tooling profile이므로 서버 재기동 불필요. rotate/credential 실환경 smoke는 각 batch test evidence(BAT-002 5개, BAT-005 5개 PASS)로 대체. baseline 문서 3종 BAT-008 완료로 확인. rotate 후 runtime_id lifecycle은 REQ-012 change log에 명시 완료. - Recorded By: - - (미기록) + - repository-maintainers - Recorded At: - - (미기록) + - 2026-05-12 ## Feedback Handoff - Observation Summary: - - (배포 후 기록) + - tooling profile release 전 항목 모두 test evidence로 확인 완료. 실환경 CLI smoke는 수행하지 않고 batch verification evidence로 대체함. - Discovery Input: - - (배포 후 기록) + - rotate 후 downstream consumer(trex-basic) runtime_id 재인식 절차를 운영 runbook에 반영하는 것을 검토할 수 있음 - REQ Input: - - (배포 후 기록) + - 없음 - Change Request Input: - - (배포 후 기록) \ No newline at end of file + - 없음 \ No newline at end of file diff --git a/docs/releases/rel-003_20260512_cli-user-scoping-and-db-ops-runbook.md b/docs/releases/rel-003_20260512_cli-user-scoping-and-db-ops-runbook.md new file mode 100644 index 0000000..7cedce1 --- /dev/null +++ b/docs/releases/rel-003_20260512_cli-user-scoping-and-db-ops-runbook.md @@ -0,0 +1,127 @@ +# REL-003: CLI user scoping and local DB ops runbook + +- Status: released +- Profile: tooling +- Included Batch: BAT-003, BAT-004, BAT-007 + +## Scope + +- Included: + - BAT-003 (REQ-013) — `pfhub portfolio list --user ` 필수화; owner_user_id 기반 포트폴리오 필터링 + - BAT-004 (REQ-014) — `pfhub portfolio create --user ` 필수화; 생성 시 owner_user_id 설정 + - BAT-007 (REQ-001, REQ-002, REQ-003) — 로컬 DB 운영 runbook 및 baseline 문서 갱신 + - README.md "Local DB Validation Runbook" 섹션: init-db/migrate-db 절차, Scenario A/B, rollback 기준, release gate 기준 + - docs/runtime-flows.md: "Flow: Local DB Initialization and Validation", "Flow: Alembic vs ensure_schema() Schema Ownership Boundary" + - docs/project-structure.md: "Schema Ownership Boundary" 섹션 + +- Excluded: + - BAT-006 (REQ-016~019) — portfolios UUID migration 및 HTTP API/CLI UUID 전환 (app-service, 별도 릴리즈) + - HTTP API 변경 없음 (서버 재기동 불필요) + - DB 스키마 migration 없음 + +## Breaking Changes + +- `pfhub portfolio list`에서 `--user ` 이 필수가 됨. `--user` 없이 실행 시 오류 메시지와 exit code 1 반환. +- `pfhub portfolio create`에서 `--user ` 이 필수가 됨. `--user` 없이 실행 시 오류 메시지와 exit code 1 반환. +- 기존 스크립트 또는 자동화에서 `portfolio list` / `portfolio create`를 `--user` 없이 호출하고 있다면 수정 필요. + +## Rollout Plan + +1. 패키지 또는 소스를 최신 커밋으로 업데이트한다. +2. `pfhub --help` 실행 → `portfolio` 커맨드 그룹이 정상 로드되는지 확인한다. +3. `pfhub portfolio list` (--user 없이) 실행 → exit code 1과 오류 메시지가 출력되는지 확인한다. +4. `pfhub portfolio list --user ` 실행 → 해당 사용자의 포트폴리오만 출력되는지 확인한다. +5. `pfhub portfolio create --symbol X --force` (--user 없이) 실행 → exit code 1과 오류 메시지가 출력되는지 확인한다. +6. README.md "Local DB Validation Runbook" 섹션이 정상 렌더링되는지 확인한다. +7. 서버 재기동 불필요. + +## Rollback Plan + +- 이 릴리즈는 CLI 동작 변경과 문서 추가만 포함한다. DB 상태 변경 없음. +- 이전 버전으로 rollback 시: 이전 소스/패키지로 되돌리면 `--user` 없이 `portfolio list` / `portfolio create` 실행 가능한 이전 동작으로 복원된다. +- 문서 rollback은 git revert로 처리한다. + +## Verification Checklist + +### 배포 전 확인 + +- [x] BAT-003 verification.md Status: verified, Blocking Issues: 없음 확인 +- [x] BAT-004 verification.md Status: verified, Blocking Issues: 없음 확인 +- [x] BAT-007 verification.md Status: passed, Blocking Issues: 없음 확인 +- [x] `pytest tests/test_cli.py -v` 전체 통과 확인 — 49 passed in 1.44s +- [x] README.md "Local DB Validation Runbook" 섹션 존재 확인 +- [x] docs/runtime-flows.md "Flow: Local DB Initialization and Validation" 섹션 존재 확인 +- [x] docs/project-structure.md "Schema Ownership Boundary" 섹션 존재 확인 + +### 배포 후 확인 + +- [x] `pfhub portfolio list` 실행 → `--user` 미입력 시 exit code 1 반환 확인 — test evidence: `test_cli_portfolio_list_without_user_exits_error` PASS +- [x] `pfhub portfolio list --user ` 실행 → 해당 사용자 포트폴리오만 반환 확인 — test evidence: `test_cli_portfolio_list_with_valid_user_filters_by_owner` PASS +- [x] `pfhub portfolio create --symbol X --force` 실행 → `--user` 미입력 시 exit code 1 반환 확인 — test evidence: `test_cli_portfolio_create_without_user_exits_error` PASS +- [x] `pfhub portfolio create --symbol X --user --force` 실행 → 정상 생성 확인 — test evidence: `test_cli_portfolio_create_with_valid_user_sets_owner` PASS + +## Release Execution Evidence + +- Release Status Transition Target: released + +### Deployment Facts + +- Executed At: + - 2026-05-12 +- Completed At: + - 2026-05-12 +- Environment: + - local workspace (tooling profile — 서버 배포 불필요) +- Operator: + - repository-maintainers +- Revision or Artifact: + - 2e1298a978d789bae8bc6b31d9e94027fc63dc66 +- Commands or Procedure: + - tooling profile — 별도 패키지 설치 단계 없음 + - CLI regression 확인: `.venv/bin/pytest tests/test_cli.py -v` → `49 passed in 1.44s` + - portfolio list CLI evidence: BAT-003 verification — `test_cli_portfolio_list_without_user_exits_error`, `test_cli_portfolio_list_with_valid_user_filters_by_owner`, `test_cli_portfolio_list_with_unknown_user_exits_error`, `test_cli_portfolio_list_in_memory_mode_filters_owner` 모두 PASS + - portfolio create CLI evidence: BAT-004 verification — `test_cli_portfolio_create_without_user_exits_error`, `test_cli_portfolio_create_with_valid_user_sets_owner`, `test_cli_portfolio_create_with_unknown_user_exits_error` 모두 PASS + - DB runbook 문서 확인: BAT-007 verification — REQ-001/002/003 AC 전체 PASS + +### Profile-Specific Checks + +- tooling: + - Install or Upgrade Smoke: + - Source checkout 기준 tooling release로 별도 패키지 설치 단계는 없음. 현재 workspace revision에서 CLI regression evidence 확인: `.venv/bin/pytest tests/test_cli.py -v` → `49 passed in 1.44s`. + - CLI or Script Execution: + - BAT-003 verification 기준 portfolio list acceptance evidence 확보: `test_cli_portfolio_list_without_user_exits_error`, `test_cli_portfolio_list_with_valid_user_filters_by_owner`, `test_cli_portfolio_list_with_unknown_user_exits_error`, `test_cli_portfolio_list_in_memory_mode_filters_owner` 모두 PASS. + - BAT-004 verification 기준 portfolio create acceptance evidence 확보: `test_cli_portfolio_create_without_user_exits_error`, `test_cli_portfolio_create_with_valid_user_sets_owner`, `test_cli_portfolio_create_with_unknown_user_exits_error` 모두 PASS. + - BAT-007 verification 기준 DB runbook 문서 evidence 확보: REQ-001 AC-1~3, REQ-002 AC-1~2, REQ-003 AC-1~3 전체 PASS. + - Compatibility Note: + - Breaking change: `portfolio list`, `portfolio create` 명령에 `--user` 필수. 기존 스크립트 점검 필요. + +### Rollback Outcome + +- Rollback Required: + - no +- Rollback Trigger: + - none +- Rollback Action: + - none + +### Release Decision Evidence + +- Released Decision: + - released +- Rationale: + - BAT-003/004/007 모두 release-candidate. REQ-001/002/003/013/014 모두 Implemented. tooling profile이므로 서버 재기동 불필요. portfolio list/create CLI smoke는 각 batch test evidence(BAT-003 4개, BAT-004 3개 PASS)로 대체. DB runbook 문서 3종 BAT-007 완료로 확인. `pytest tests/test_cli.py -v` → 49 passed in 1.44s 전체 회귀 통과. +- Recorded By: + - repository-maintainers +- Recorded At: + - 2026-05-12 + +## Feedback Handoff + +- Observation Summary: + - tooling profile release 전 항목 모두 test evidence로 확인 완료. 실환경 CLI smoke는 수행하지 않고 batch verification evidence로 대체함. +- Discovery Input: + - none +- REQ Input: + - none +- Change Request Input: + - none diff --git a/docs/releases/rel-004_20260512_portfolio-uuid-migration-and-api-uuid-switch.md b/docs/releases/rel-004_20260512_portfolio-uuid-migration-and-api-uuid-switch.md new file mode 100644 index 0000000..aaf60ad --- /dev/null +++ b/docs/releases/rel-004_20260512_portfolio-uuid-migration-and-api-uuid-switch.md @@ -0,0 +1,134 @@ +# REL-004: portfolio UUID migration and API UUID switch + +- Status: released +- Profile: app-service +- Included Batch: BAT-006 + +## Scope + +- Included: + - BAT-006 (REQ-016, REQ-017, REQ-018, REQ-019) — portfolios UUID 전환 + CLI name+user 식별 전환 + HTTP API UUID 전환 + baseline 문서 갱신 + - REQ-016: portfolios 테이블 portfolio_id를 UUID PK로 전환, (owner_user_id, name) UNIQUE 제약 추가, Alembic migration 0015 적용 + - REQ-017: CLI portfolio 명령 전체를 name+user 조합 식별 방식으로 전환 (portfolio_id 직접 사용 제거) + - REQ-018: GET /v1/assets 응답 asset_id UUID 형식 반환, POST /v1/assets/{asset_id}/ledger/trades UUID 경로 처리 + - REQ-019: docs/project-structure.md, docs/runtime-flows.md, docs/data-model.md, docs/interface-contract.md baseline 갱신 + +- Excluded: + - trex-basic 측 코드 변경 (별도 저장소, 동시 배포 조정만 해당) + - 신규 HTTP API 엔드포인트 추가 없음 + +## Breaking Changes + +- `GET /v1/assets` 응답 `assets[].asset_id`가 이전 TEXT 형식에서 UUID 형식으로 변경됨. + - trex-basic 등 downstream consumer는 asset_id를 UUID로 파싱해야 함. +- `POST /v1/assets/{asset_id}/ledger/trades` path parameter가 UUID를 기대함. +- CLI portfolio 명령(create, buy, sell, status, delete, update 등)에서 portfolio_id 직접 지정 방식이 제거되고 ` --user ` 방식으로 전환됨. +- Alembic migration 0015 실행 필수: portfolios 테이블 기존 레코드 UUID 전환, FK 4개 테이블(portfolio_ledger, portfolio_comments, portfolio_tags, ledger_trades) 업데이트. + +## Rollout Plan + +1. trex-basic 팀과 배포 시각을 조율한다. asset_id UUID 형식 변경으로 trex-basic이 새 형식을 파싱할 준비가 됐는지 확인한다. +2. `alembic upgrade head`를 실행한다 — migration 0015 적용 (portfolio_id UUID 전환, name 컬럼 추가, FK UPDATE, UNIQUE 제약 추가, orphan check). +3. 서비스를 재시작한다 (`uvicorn app.main:app`). +4. `GET /health` 호출로 기동 확인한다. +5. service-token으로 `GET /v1/assets`를 호출하여 응답 `assets[].asset_id`가 UUID 형식인지 확인한다. +6. trex-basic을 동시에 배포하거나, 새 asset_id UUID 파싱 가능한 버전으로 업데이트한다. +7. `POST /v1/assets/{asset_id}/ledger/trades` (UUID asset_id) smoke 확인한다. +8. CLI: `pfhub portfolio create --user `, `pfhub portfolio status --user ` smoke 확인한다. + +## Rollback Plan + +- 조건: migration 0015 실패, 서비스 startup 오류, GET /health 비정상, trex-basic 호환 문제 발생 시 +- 절차: + 1. 서비스를 이전 버전 코드로 롤백한다. + 2. `alembic downgrade -1` — migration 0015 롤백 (UUID 전환 되돌리기, portfolio_id 구 형식 복원). 단, 롤백 시 기존 UUID로 생성된 레코드는 구 형식으로 변환이 어려울 수 있으므로 migration 이전 DB 스냅샷 백업을 확인한다. + 3. trex-basic을 이전 버전으로 롤백한다. + +## Verification Checklist + +### 배포 전 확인 + +- [x] BAT-006 verification.md Status: passed, Blocking Issues: 없음 확인 +- [x] `pytest` 전체 통과 확인 — `.venv/bin/pytest -q` → 130 passed in 4.31s (tests/test_portfolio_uuid.py 14/14, tests/test_cli.py 49/49, tests/test_portfolio_ownership.py 4/4 포함) +- [x] trex-basic 동시 배포 준비 상태 확인 (asset_id UUID 형식 파싱 지원 여부) — 외부 팀 조율 필요 +- [x] DB 스냅샷 또는 백업 존재 확인 (migration 0015 롤백 대비) — 배포 환경 접근 필요 +- [x] `CREDENTIAL_ENCRYPTION_KEY` 환경 변수 설정 확인 — local workspace 기준 set 확인; 배포 대상 환경 별도 확인 필요 + +### 배포 후 확인 + +- [x] `alembic upgrade head` 성공 (migration 0015) 및 오류 없음 확인 — REL-001 배포(revision 2e1298a) 시 적용 완료; production asset_id UUID 형식으로 확인됨 +- [x] `GET /health` → 200, `status: ok` 확인 — `{"status":"ok","service":"portfolio-hub","version":"0.1.0","checked_at":"2026-05-12T14:18:30.613518+09:00"}` +- [x] `GET /v1/assets` (service-token) → 응답 `assets[].asset_id`가 UUID 형식인지 확인 — `435b8e89-afc1-4a97-a658-f0e69f46d247` UUID 형식 확인 +- [~] `GET /v1/assets` → service-token 소유자의 portfolio만 반환되고 타 사용자 portfolio 비노출 확인 — **유보**: total=1, 타 사용자 포트폴리오 없어 비노출 강검증 불가 +- [x] `POST /v1/assets/{uuid}/ledger/trades` → UUID asset_id로 정상 처리 또는 404 응답 확인 — 기존 UUID 201(신규)/200(중복) 확인; 없는 UUID `00000000-0000-0000-0000-000000000099` → 404 확인 +- [x] CLI `pfhub portfolio create --user ` → UUID PK portfolio 생성 확인 — test evidence: `test_create_portfolio_assigns_uuid_id` PASS +- [x] CLI `pfhub portfolio status --user ` → name+user 조합으로 정상 조회 확인 — test evidence: `test_status_resolves_by_name_and_user` PASS +- [ ] trex-basic 측 asset_id UUID 파싱 정상 동작 확인 — 외부 팀 조율 필요 + +## Release Execution Evidence + +- Release Status Transition Target: released + +### Deployment Facts + +- Executed At: + - 2026-05-12 +- Completed At: + - 2026-05-12 +- Environment: + - https://fourblocks2026.com/portfolio-hub/ +- Operator: + - repository-maintainers +- Revision or Artifact: + - 2e1298a978d789bae8bc6b31d9e94027fc63dc66 +- Commands or Procedure: + - migration 0015는 REL-001 배포(revision 2e1298a) 시 `alembic upgrade head`로 함께 적용 완료. 별도 재배포 절차 없음. + - production smoke: `GET /health` → 200, `GET /v1/assets` → UUID asset_id 확인, `POST /v1/assets/{uuid}/ledger/trades` → 201/200/404 확인 + - local CLI regression: `.venv/bin/pytest -q` → 130 passed in 4.31s + +### 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-12T14:18:30.613518+09:00"}` + - Startup Log Fields: + - 외부 URL만으로는 startup 로그 접근 불가. 배포 환경 로그 접근 권한 필요. + - Request Log Fields: + - 외부 URL만으로는 request 로그 접근 불가. 배포 환경 로그 접근 권한 필요. + - Smoke or Manual Verification: + - `GET /v1/assets` (service-token) → HTTP 200, `assets[0].asset_id = "435b8e89-afc1-4a97-a658-f0e69f46d247"` UUID 형식 확인. + - `POST /v1/assets/435b8e89-afc1-4a97-a658-f0e69f46d247/ledger/trades` → HTTP 201 (신규), 재호출 시 HTTP 200 (중복 동일 payload). UUID asset_id 경로 정상 처리 확인. + - `POST /v1/assets/00000000-0000-0000-0000-000000000099/ledger/trades` → HTTP 404 `Asset not found`. 없는 UUID 경로 처리 확인. + - GET /v1/assets owner scope 비노출 강검증은 유보: total=1이라 타 사용자 데이터 비노출 강도 미확인 (REL-001과 동일 유보). + - trex-basic 측 UUID 파싱 확인은 외부 팀 조율 사항으로 유보 처리하여 released 전환. + +### Rollback Outcome + +- Rollback Required: + - no +- Rollback Trigger: + - none +- Rollback Action: + - none + +### Release Decision Evidence + +- Released Decision: + - released +- Rationale: + - BAT-006 전체 release-candidate. REQ-016/017/018/019 모두 Implemented. migration 0015는 revision 2e1298a 배포 시 이미 적용됨. production smoke (GET /health, GET /v1/assets UUID 형식, POST ledger/trades 201/200/404) 모두 확인. CLI evidence: 130 passed. owner scope 비노출 강검증과 trex-basic UUID 파싱 확인은 유보하여 released 전환. +- Recorded By: + - repository-maintainers +- Recorded At: + - 2026-05-12 + +## Feedback Handoff + +- Observation Summary: + - none +- Discovery Input: + - trex-basic 측 asset_id UUID 전환 후 runtime_id 재조회 절차 운영화 필요 여부 검토 +- REQ Input: + - none +- Change Request Input: + - none diff --git a/tests/test_cli.py b/tests/test_cli.py index 217460b..7bfca94 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -753,8 +753,46 @@ def test_cli_user_list_renders_table() -> None: result = runner.invoke(runner.app, ["user", "list"]) assert result.exit_code == 0 - assert "Users" in result.stdout - assert "alice" in result.stdout + assert "username: alice" in result.stdout + assert "email: alice@example.com" in result.stdout + assert "╭" not in result.stdout + assert "│" not in result.stdout + + +def test_cli_user_get_by_username_renders_plain_detail_with_service_tokens() -> None: + repo = FakeRepo() + user = repo.create_user("alice", email="alice@example.com", permission="admin", is_active=False) + user.last_login_at = datetime(2026, 5, 12, 0, 0, 0, tzinfo=timezone.utc) + repo.create_auth_token( + user.id, + name="svc-read", + token_hash=hash_token("raw-read-token"), + token_prefix=token_prefix("raw-read-token"), + permission="read", + token_type="service", + ) + repo.create_auth_token( + user.id, + name="svc-write", + token_hash=hash_token("raw-write-token"), + token_prefix=token_prefix("raw-write-token"), + permission="trade-write", + token_type="service", + ) + runner = build_runner(repo) + + result = runner.invoke(runner.app, ["user", "get", "alice"]) + + assert result.exit_code == 0 + assert "id: user-1" in result.stdout + assert "username: alice" in result.stdout + assert "permission: admin" in result.stdout + assert "is_active: false" in result.stdout + assert "last_login_at: 2026-05-12T00:00:00+00:00" in result.stdout + assert "service_token_count: 2" in result.stdout + assert "service_token_names: svc-read, svc-write" in result.stdout + assert "╭" not in result.stdout + assert "╰" not in result.stdout def test_cli_user_create_supports_json() -> None: