batch drift 정합 — REL-001(BAT-001~016)·REL-002(BAT-017) released에 맞춰 batches/index 및 개별 index 상태를 release-candidate→released 동기화(17건). README: bootstrap 문구 제거, dcy-007 엔드포인트(GET /trades·/pnl/realized) 추가, planned-next 현행화. 코드 무변경. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VHtM5Ws1HKCKQ4JGi6GRb5
568 lines
25 KiB
Markdown
568 lines
25 KiB
Markdown
# Portfolio Hub
|
|
|
|
Portfolio Hub is a FastAPI service that extracts portfolio management from `jsbot-kis` and exposes it through an authenticated HTTP API.
|
|
|
|
## Current status
|
|
|
|
- FastAPI application entrypoint is available.
|
|
- Token-based authentication and role checks are implemented.
|
|
- Portfolio and user management API contracts are implemented.
|
|
- Core portfolio domain services are ported from `jsbot-kis`.
|
|
- Postgres-backed portfolio and user persistence is implemented and gated behind `pfhub_DB_ENABLED=true`.
|
|
- Alembic-managed schema migrations are applied in production (head: `20260605_0025`).
|
|
- Trade history and realized PnL query API is implemented (`GET /v1/portfolios/{id}/trades`, `GET /v1/portfolios/{id}/pnl/realized`).
|
|
|
|
## Configuration
|
|
|
|
Copy `.env.example` to `.env` and edit values as needed:
|
|
|
|
```bash
|
|
cp .env.example .env
|
|
```
|
|
|
|
The application loads configuration in this order (higher priority first):
|
|
1. Environment variables
|
|
2. Variables from `.env` file
|
|
3. Default values in code
|
|
|
|
When deploying to production, set environment variables directly instead of using `.env`.
|
|
|
|
### Key configuration options
|
|
|
|
- **DB repository switch**: `pfhub_DB_ENABLED` (enables DB-backed persistence)
|
|
- **Initial user bootstrap**: `pfhub_BOOTSTRAP_USERNAME`, `pfhub_BOOTSTRAP_EMAIL`
|
|
- **Initial user password**: `pfhub_BOOTSTRAP_PASSWORD`
|
|
- **Session/token TTL**: `pfhub_SESSION_TOKEN_TTL_HOURS`, `pfhub_CLIENT_TOKEN_DEFAULT_TTL_DAYS`, `pfhub_SERVICE_TOKEN_DEFAULT_TTL_DAYS`
|
|
- **TimescaleDB / PostgreSQL**: `TIMESCALEDB_*` (single source of DB connection settings)
|
|
- **Redis**: `REDIS_*` (for caching and sessions)
|
|
- **Loki**: `LOKI_*` (for centralized logging)
|
|
- **Authentication**: `pfhub_*_TOKENS` (bearer tokens with role-based access)
|
|
|
|
For complete configuration options, see `.env.example`.
|
|
|
|
## Run locally
|
|
|
|
```bash
|
|
python -m venv .venv
|
|
source .venv/bin/activate
|
|
python -m pip install -e '.[dev]'
|
|
|
|
# Configure .env
|
|
cp .env.example .env
|
|
|
|
# Start server
|
|
uvicorn app.main:app --reload
|
|
|
|
# In another terminal, run tests
|
|
pytest
|
|
```
|
|
|
|
If you need local infrastructure for TimescaleDB, Redis, and Loki, start them with Docker Compose before running the app:
|
|
|
|
```bash
|
|
docker compose -f docker-compose.local-test.yml up -d
|
|
docker compose -f docker-compose.local-test.yml ps
|
|
```
|
|
|
|
Default endpoints exposed by `docker-compose.local-test.yml`:
|
|
|
|
- TimescaleDB: `127.0.0.1:5432`
|
|
- Redis: `127.0.0.1:6379` (RedisInsight UI: `127.0.0.1:8001`)
|
|
- Loki: `127.0.0.1:3100`
|
|
|
|
Typical `.env` values for this local setup:
|
|
|
|
```env
|
|
pfhub_DB_ENABLED=true
|
|
TIMESCALEDB_HOST=127.0.0.1
|
|
TIMESCALEDB_PORT=5432
|
|
TIMESCALEDB_USER=portfolio_hub_user
|
|
TIMESCALEDB_PASSWORD=change-me
|
|
TIMESCALEDB_NAME=portfolio_hub
|
|
|
|
REDIS_HOST=127.0.0.1
|
|
REDIS_PORT=6379
|
|
|
|
LOKI_HOST=127.0.0.1
|
|
LOKI_PORT=3100
|
|
```
|
|
|
|
To stop and remove the local containers:
|
|
|
|
```bash
|
|
docker compose -f docker-compose.local-test.yml down
|
|
```
|
|
|
|
## CLI
|
|
|
|
The repository now includes a `pfhub` CLI that mirrors the main portfolio flows from `jsbot-kis`.
|
|
The same commands are also available through the shorter alias `phub`.
|
|
|
|
```bash
|
|
pfhub --help
|
|
pfhub user list
|
|
pfhub user create --username alice --password password123 --email alice@example.com --force
|
|
pfhub user get <username>
|
|
pfhub user update <username> --username alice-updated --force
|
|
pfhub user delete <username> --force
|
|
pfhub service-token list --user alice
|
|
pfhub service-token issue nightly --user alice --ttl-days 30
|
|
pfhub service-token issue permanent --user alice --no-expiry
|
|
pfhub service-token issue my-daemon-token --user alice --daemon-id <daemon_id>
|
|
pfhub service-token revoke nightly --force
|
|
pfhub service-token rotate permanent --force
|
|
pfhub daemon create alpha-bot --user alice --display-name "Alpha Trading Bot"
|
|
pfhub daemon list --user alice
|
|
pfhub daemon update <daemon_id> --display-name "Updated Name"
|
|
pfhub daemon update <daemon_id> --deactivate
|
|
pfhub portfolio list --user <username>
|
|
pfhub portfolio status 005930:tv --recent 10
|
|
pfhub portfolio create 005930:tv --symbol 005930 --user <username> --cash-balance 10000000 --force
|
|
pfhub portfolio buy 005930:tv --quantity 10 --price 71000 --fee 5000 --force
|
|
pfhub portfolio sell 005930:tv --quantity 5 --price 72000 --tax 3000 --force
|
|
pfhub portfolio invest 005930:tv --type deposit --amount 50000 --force
|
|
pfhub portfolio transfer 005930:tv --direction to-buffer --amount 100000 --force
|
|
pfhub portfolio reset 005930:tv --force
|
|
pfhub portfolio delete 005930:tv --keep-ledger --force
|
|
```
|
|
|
|
Supported commands:
|
|
|
|
- `init-db`
|
|
- `migrate-db`
|
|
- `user list`
|
|
- `user get`
|
|
- `user create`
|
|
- `user update`
|
|
- `user delete`
|
|
- `service-token list`
|
|
- `service-token issue`
|
|
- `service-token revoke`
|
|
- `service-token rotate`
|
|
- `daemon create`
|
|
- `daemon list`
|
|
- `daemon update`
|
|
- `portfolio list`
|
|
- `portfolio status`
|
|
- `portfolio create`
|
|
- `portfolio update`
|
|
- `portfolio buy`
|
|
- `portfolio sell`
|
|
- `portfolio invest`
|
|
- `portfolio transfer`
|
|
- `portfolio reset`
|
|
- `portfolio delete`
|
|
- `health-check`
|
|
|
|
Write commands ask for confirmation by default. Use `--force` to skip the prompt for scripts and automation.
|
|
|
|
Service token issue policy:
|
|
|
|
- 기본값은 365일 TTL이다.
|
|
- `--ttl-days`와 `--no-expiry`는 동시에 사용할 수 없다.
|
|
- `--no-expiry`는 관리자 권한에서만 허용된다.
|
|
- 토큰 원문은 발급 응답 1회 이후 마스킹된 값으로만 출력/기록된다.
|
|
|
|
### runtime_id vs daemon_id
|
|
|
|
Two stable identifiers are attached to service tokens:
|
|
|
|
- **`runtime_id`**: per-token opaque ID, regenerated on every `service-token rotate`. Use to track which token generation is currently active (`GET /v1/user_info` returns it for live verification).
|
|
- **`daemon_id`**: stable logical identity for a trading daemon process. Bound at token issuance and preserved across rotates (rotate invariant). Use to scope daemon-owned assets and authorize ledger writes.
|
|
|
|
A token with `daemon_id=null` is a legacy/unbound token. `GET /v1/assets` and `POST /v1/assets/{asset_id}/ledger/trades` return HTTP 403 for null-daemon tokens (fail-closed). `GET /v1/user_info` returns 200 with `daemon_id: null` as a diagnostic (not an error).
|
|
|
|
The CLI uses the same repository and settings layer as the HTTP API. To target the external TimescaleDB, set `pfhub_DB_ENABLED=true` and provide `TIMESCALEDB_*` values in the environment or `.env`.
|
|
|
|
Initialize a new database explicitly before the first use:
|
|
|
|
```bash
|
|
pfhub_DB_ENABLED=true pfhub init-db
|
|
pfhub_DB_ENABLED=true pfhub health-check
|
|
pfhub_DB_ENABLED=true pfhub portfolio list
|
|
```
|
|
|
|
`init-db` will:
|
|
|
|
- connect to the maintenance database, defaulting to `postgres`
|
|
- create the target database if it does not exist
|
|
- run Alembic `upgrade head`
|
|
|
|
`migrate-db` will:
|
|
|
|
- connect to the target database
|
|
- run Alembic `upgrade head`
|
|
- apply any future schema revisions in order
|
|
|
|
This repository now uses Alembic for versioned schema migrations. Future schema changes should be added under `migrations/versions`.
|
|
|
|
## Daemon Provisioning Runbook
|
|
|
|
이 섹션은 daemon registry 등록, service-token 바인딩, asset 할당, 그리고 daemon-scoped enforcement 활성화 절차를 정의한다.
|
|
|
|
### 1. Daemon 등록
|
|
|
|
```bash
|
|
# 1. daemon 생성 (daemon_slug는 per-user unique)
|
|
pfhub_DB_ENABLED=true pfhub daemon create alpha-bot --user alice --display-name "Alpha Trading Bot"
|
|
# 출력: daemon_id (UUID), daemon_slug, is_active=True
|
|
|
|
# 2. daemon 목록 확인
|
|
pfhub_DB_ENABLED=true pfhub daemon list --user alice
|
|
```
|
|
|
|
### 2. Service Token을 Daemon에 바인딩
|
|
|
|
```bash
|
|
# daemon_id는 위 create 출력에서 확인
|
|
pfhub_DB_ENABLED=true pfhub service-token issue alpha-bot-token --user alice \
|
|
--daemon-id <daemon_id> --permission trade-write --no-expiry
|
|
|
|
# 발급된 토큰 원문은 1회만 표시됨. daemon_id와 runtime_id를 기록해 둘 것.
|
|
```
|
|
|
|
바인딩 규칙:
|
|
- daemon은 존재하고 `is_active=True`여야 한다.
|
|
- daemon의 `owner_user_id`가 토큰 발급 대상 user와 일치해야 한다.
|
|
- 비활성 daemon에 대한 token 발급은 즉시 거부된다.
|
|
|
|
### 3. Asset을 Daemon에 할당
|
|
|
|
현재 `managed_by_daemon_id` 할당은 DB 직접 수정 또는 배포 시 마이그레이션으로 수행한다(CLI portfolio update를 통한 daemon 할당은 향후 추가 예정).
|
|
|
|
```sql
|
|
UPDATE portfolios SET managed_by_daemon_id = '<daemon_id>' WHERE name = 'my-portfolio' AND owner_user_id = '<user_id>';
|
|
```
|
|
|
|
### 4. Daemon-Scoped Enforcement 활성화 게이트
|
|
|
|
daemon-scoped enforcement(NULL token → 403)는 코드 레벨에서 이미 활성화되어 있다. 프로덕션 전환 전 아래 4가지 증거를 확보해야 한다:
|
|
|
|
| # | 항목 | 충족 기준 |
|
|
|---|------|-----------|
|
|
| 1 | Active NULL daemon_id token inventory 완료 | `SELECT COUNT(*) FROM auth_tokens WHERE daemon_id IS NULL AND token_type='service' AND revoked_at IS NULL` = 0 (또는 의도된 레거시 토큰만 남음) |
|
|
| 2 | Dry-run 결과 zero active-NULL | 대상 환경에서 위 쿼리 실행, 결과 스크린샷 또는 텍스트 캡처 |
|
|
| 3 | Dry-run 결과 zero mismatch | `SELECT COUNT(*) FROM portfolios p JOIN auth_tokens t ON p.managed_by_daemon_id != t.daemon_id WHERE t.token_type='service' AND t.revoked_at IS NULL` = 0 |
|
|
| 4 | Release approver 승인 | 위 증거 3가지를 release 문서에 첨부하고 release approver 서명 확보 |
|
|
|
|
마이그레이션 20260513_0016이 적용된 환경에서만 enforcement가 정상 동작한다.
|
|
|
|
### 5. Service Token Rotate (daemon_id invariant)
|
|
|
|
```bash
|
|
pfhub_DB_ENABLED=true pfhub service-token rotate alpha-bot-token --force
|
|
# runtime_id는 새로 발급됨; daemon_id는 유지됨
|
|
```
|
|
|
|
rotate 후 `GET /v1/user_info`로 새 `runtime_id`와 기존 `daemon_id`를 교차 확인한다:
|
|
|
|
```bash
|
|
curl -X GET "<BASE_URL>/v1/user_info" \
|
|
-H "Authorization: Bearer <NEW_TOKEN>"
|
|
# 응답: runtime_id=<new_id>, daemon_id=<same_daemon_id>
|
|
```
|
|
|
|
## Local DB Validation Runbook
|
|
|
|
이 섹션은 로컬 docker compose 환경에서 DB 초기화, 마이그레이션, health-check, smoke flow를 반복 검증하기 위한 절차를 정의한다.
|
|
|
|
### 준비 — 컨테이너 시작
|
|
|
|
```bash
|
|
docker compose -f docker-compose.local-test.yml up -d
|
|
docker compose -f docker-compose.local-test.yml ps
|
|
# portfolio-hub-timescaledb 상태가 (healthy) 인지 확인
|
|
```
|
|
|
|
기본 접속 정보 (`.env` 설정 기준):
|
|
- TimescaleDB: `127.0.0.1:5432`
|
|
- DB: `portfolio_hub`, User: `portfolio_hub_user`
|
|
|
|
### Scenario A — Fresh DB (DB 미존재 상태)
|
|
|
|
```bash
|
|
# 1. DB 생성 + Alembic upgrade head
|
|
pfhub_DB_ENABLED=true pfhub init-db
|
|
# 기대: database: portfolio_hub / created: True / schema_initialized: True / migration_applied: head
|
|
|
|
# 2. DB 연결 health 확인
|
|
pfhub_DB_ENABLED=true pfhub health-check
|
|
# 기대: db_enabled: True / repository: PostgresPortfolioRepository
|
|
|
|
# 3. Smoke flow — 사용자 생성 + portfolio 생성 + 목록 확인
|
|
pfhub_DB_ENABLED=true pfhub user create --username alice --password password123 --force
|
|
pfhub_DB_ENABLED=true pfhub portfolio create my-portfolio --symbol 005930 --user alice --cash-balance 1000000 --force
|
|
pfhub_DB_ENABLED=true pfhub portfolio list --user alice
|
|
# 기대: my-portfolio가 목록에 포함
|
|
```
|
|
|
|
**pass 기준**: 각 명령 exit code 0 / 기대 필드 출력 확인
|
|
**fail 기준**: connection refused / Alembic revision 오류 / exit code ≠ 0
|
|
|
|
### Scenario B — runtime-precreated schema (App 선행 기동)
|
|
|
|
```bash
|
|
# 1. App 먼저 기동 (ensure_schema() 실행)
|
|
pfhub_DB_ENABLED=true uvicorn app.main:app &
|
|
|
|
# 2. migrate-db로 Alembic 관리 상태로 전환
|
|
pfhub_DB_ENABLED=true pfhub migrate-db
|
|
# 기대: migration_applied: head
|
|
|
|
# 3. health-check 재확인
|
|
pfhub_DB_ENABLED=true pfhub health-check
|
|
```
|
|
|
|
**동작 원리**: `bootstrap_or_upgrade_to_head()`가 `alembic_version` 테이블 없음 → 기존 핵심 테이블 존재 → legacy stamp → head upgrade 순서로 실행한다.
|
|
|
|
### 반복 실행 (재현성 확인)
|
|
|
|
```bash
|
|
# 컨테이너 + 볼륨 초기화
|
|
docker compose -f docker-compose.local-test.yml down -v
|
|
|
|
# 위 Scenario A 또는 B 재실행
|
|
```
|
|
|
|
출력이 이전 실행과 동일한지 비교한다. JSON 출력 수집:
|
|
|
|
```bash
|
|
pfhub_DB_ENABLED=true pfhub init-db --output json
|
|
pfhub_DB_ENABLED=true pfhub health-check --output json
|
|
```
|
|
|
|
로그 수집 위치: `docker compose -f docker-compose.local-test.yml logs portfolio-hub-timescaledb`
|
|
|
|
### Rollback / 재시도 판단 기준
|
|
|
|
| 증상 | 판단 | 조치 |
|
|
|------|------|------|
|
|
| `init-db`: `connection refused` | DB 컨테이너 미준비 | `docker compose ps` → `(healthy)` 대기 후 재시도 |
|
|
| `migrate-db`: `Alembic revision 오류` | revision 파일 손상 또는 순서 오류 | `migrations/versions/` revision 확인 |
|
|
| `migrate-db` 후 v1 API 오류 | ensure_schema() 선행 후 Alembic 미실행 | `migrate-db` 재실행 |
|
|
| 데이터 오염으로 스키마 불일치 | 심각한 데이터 오류 | 볼륨 초기화(`down -v`) + Scenario A 재시작 |
|
|
|
|
### Release Gate Criteria
|
|
|
|
다음 조건을 모두 만족해야 로컬 검증 완료로 간주한다:
|
|
|
|
1. Scenario A: `init-db` exit code 0 + `health-check` exit code 0 + smoke flow 정상 완료
|
|
2. `migrate-db` 반복 실행 시 오류 없음 (멱등성 확인)
|
|
3. `docker compose logs`에 ERROR 레벨 로그 없음
|
|
|
|
## Docs Sync Guardrail
|
|
|
|
To keep implementation and specification documents in sync, this repository provides an automated check.
|
|
|
|
Rules enforced:
|
|
|
|
- If files under `app/` change, at least one `docs/technical/` document must be updated in the same commit/PR.
|
|
- If files under `app/api/` or `app/schemas/` change, `docs/technical/api-contracts.md` must be updated in the same commit/PR.
|
|
- If files under `migrations/versions/` change, `docs/technical/persistence-and-migrations.md` must be updated in the same commit/PR.
|
|
- Changed migration revision IDs must appear in `docs/technical/persistence-and-migrations.md`.
|
|
|
|
Enable local pre-commit check:
|
|
|
|
```bash
|
|
pip install -e .[dev]
|
|
pre-commit install
|
|
```
|
|
|
|
Run manually:
|
|
|
|
```bash
|
|
python3 scripts/check_docs_sync.py --staged
|
|
```
|
|
|
|
CI check:
|
|
|
|
- GitHub Actions workflow: `.github/workflows/docs-sync-check.yml`
|
|
- Runs automatically on pull requests.
|
|
|
|
## Authentication model
|
|
|
|
pfhub currently supports bearer tokens with three permission levels.
|
|
|
|
- `read`: list and status endpoints
|
|
- `trade-write`: mutation endpoints such as buy, sell, invest, transfer, reset
|
|
- `admin`: delete, user management, and future operational actions
|
|
|
|
## User management
|
|
|
|
User management is exposed through both HTTP API and CLI.
|
|
|
|
- API base route: `/users`
|
|
- Permission: `admin` only
|
|
- Supported operations: list, get, create, update, delete
|
|
|
|
Frontend and app authentication is exposed through `/auth`.
|
|
|
|
- `POST /auth/login`: username/password login, returns bearer session token
|
|
- `GET /auth/tokens`: list current user's issued tokens
|
|
- `POST /auth/tokens`: issue a client token for app/script usage
|
|
- `DELETE /auth/tokens/{token_id}`: revoke a previously issued token
|
|
|
|
Service tokens (long-lived integration tokens) are managed via CLI:
|
|
|
|
```bash
|
|
pfhub service-token issue <name> --user <username> [--permission read|trade-write] [--ttl-days N | --no-expiry]
|
|
pfhub service-token revoke <name> [--force]
|
|
```
|
|
|
|
Example API calls:
|
|
|
|
```bash
|
|
curl -X GET "http://localhost:8000/users" \
|
|
-H "Authorization: Bearer <ADMIN_TOKEN>"
|
|
|
|
curl -X POST "http://localhost:8000/users" \
|
|
-H "Authorization: Bearer <ADMIN_TOKEN>" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"username": "alice", "email": "alice@example.com"}'
|
|
```
|
|
|
|
The default `.env.example` includes example tokens for development.
|
|
|
|
When `pfhub_DB_ENABLED=true` and both `pfhub_BOOTSTRAP_USERNAME` and `pfhub_BOOTSTRAP_PASSWORD` are set, the app creates that admin user automatically on startup only if the `users` table is empty. This is intended for first-run bootstrap, not recurring synchronization.
|
|
|
|
## Portfolio API
|
|
|
|
Portfolio management and ledger access are exposed through `/v1/portfolios`.
|
|
|
|
- `POST /v1/portfolios`: create a portfolio (requires `trade-write` or session token)
|
|
- `GET /v1/portfolios`: list portfolios for the authenticated user
|
|
- `PATCH /v1/portfolios/{id}`: update portfolio metadata
|
|
- `DELETE /v1/portfolios/{id}`: delete a portfolio (requires `admin`)
|
|
- `POST /v1/portfolios/{id}/ledger/trades`: record a trade entry with accounting (`cash_balance_after`, `asset_quantity_after`)
|
|
- `GET /v1/portfolios/{id}/tags`: list tags on a portfolio
|
|
- `POST /v1/portfolios/{id}/tags`: apply a tag to a portfolio
|
|
- `DELETE /v1/portfolios/{id}/tags/{tag_id}`: remove a tag from a portfolio
|
|
- `POST /v1/portfolios/{id}/comments`: add a comment
|
|
- `GET /v1/portfolios/{id}/comments`: list comments
|
|
- `PATCH /v1/portfolios/{id}/comments/{comment_id}`: update a comment
|
|
- `DELETE /v1/portfolios/{id}/comments/{comment_id}`: delete a comment
|
|
- `GET /v1/portfolios/{id}/trades`: query trade history with date filter (requires `read` or higher)
|
|
- `GET /v1/portfolios/{id}/pnl/realized`: compute WAC-based realized PnL over a date range (requires `read` or higher)
|
|
|
|
### Trade history query (`GET /v1/portfolios/{id}/trades`)
|
|
|
|
Supports single-date and range queries via query parameters. At least one date parameter is required.
|
|
|
|
```bash
|
|
# Single date
|
|
curl -X GET "http://localhost:8000/v1/portfolios/<id>/trades?date=2026-06-19" \
|
|
-H "Authorization: Bearer <READ_TOKEN>"
|
|
|
|
# Date range
|
|
curl -X GET "http://localhost:8000/v1/portfolios/<id>/trades?from=2026-06-01&to=2026-06-19" \
|
|
-H "Authorization: Bearer <READ_TOKEN>"
|
|
```
|
|
|
|
Response fields per trade item: `trade_id`, `side`, `quantity`, `price`, `executed_at`, `fee_amount`, `tax_amount`.
|
|
|
|
### Realized PnL query (`GET /v1/portfolios/{id}/pnl/realized`)
|
|
|
|
Computes weighted-average cost (WAC) based realized PnL over the specified date range. `from` and `to` are required. `symbol` is an optional filter.
|
|
|
|
```bash
|
|
curl -X GET "http://localhost:8000/v1/portfolios/<id>/pnl/realized?from=2026-06-01&to=2026-06-19" \
|
|
-H "Authorization: Bearer <READ_TOKEN>"
|
|
```
|
|
|
|
Response fields: `total_realized_pnl`, `cost_basis_from`, `items` (per-sell realized PnL), `data_completeness_warning` (set when buy history before `from` is incomplete).
|
|
|
|
## Production Operator Runbook
|
|
|
|
이 섹션은 실배포 환경에서 admin 접근, release smoke credential 관리, provenance 확인, 로그 증적 수집을 반복 가능한 절차로 정리한다.
|
|
|
|
### 1. Canonical admin access path
|
|
|
|
- steady-state 운영 경로는 운영자 개인 소유 admin 사용자 계정으로 로그인해 얻은 session token 또는 그 컨텍스트에서 발급한 client token이다.
|
|
- `POST /auth/login`으로 admin auth context를 확보한 뒤, 같은 컨텍스트로 `POST /auth/service-tokens`를 호출해 smoke용 service-token을 발급한다.
|
|
- bootstrap 환경 변수 기반 admin 생성은 `users` 테이블이 비어 있는 신규 환경 또는 disaster recovery 직후 admin 사용자 부재가 확인된 경우에만 허용한다.
|
|
- 환경 변수 기반 admin token은 `/opt/portfoliohub/.env`에서만 꺼내는 break-glass fallback으로만 사용하고, 정기 release smoke의 표준 경로로 사용하지 않는다.
|
|
- 현재 프로덕션 secret material location은 `portfoliohub` systemd service의 `EnvironmentFile=/opt/portfoliohub/.env` 이다. 로컬 워크스페이스 파일이나 release 문서는 canonical storage가 아니다.
|
|
|
|
### 2. Smoke credential ownership policy
|
|
|
|
역할 책임:
|
|
- **deployment secret custodian** (repository-maintainers 역할 내): secret material 보관 위치(`/opt/portfoliohub/.env`) 및 접근 통제 관리 책임자.
|
|
- **production operator / release approver**: 실배포 smoke 수행 및 release 승인 책임자. read/trade-write smoke service-token 공동 소유.
|
|
|
|
정책:
|
|
- admin 로그인 자격증명은 named operator가 개인별로 소유하고 공유하지 않는다.
|
|
- read/trade-write smoke service-token은 개인 소유가 아니라 환경 단위 운영 credential로 취급하고, release approver 또는 repository-maintainers 역할이 공동 소유한다.
|
|
- raw password, raw session token, raw client token, raw service-token은 현재 프로덕션 기준 `/opt/portfoliohub/.env`와 동일 접근 통제 하의 운영 저장소에만 저장한다. 관리 주체: deployment secret custodian.
|
|
- release 문서와 운영 메모에는 token name, token_prefix, permission, runtime_id, issued_at, revoked_at만 남기고 raw secret은 남기지 않는다.
|
|
- 토큰은 release smoke 전 발급 또는 rotate하고, 노출 의심, 담당자 변경, 장애 대응 후에는 revoke 또는 rotate한다.
|
|
|
|
### 3. Release credential matrix
|
|
|
|
| Credential | 용도 | 발급 주체 | 보관 위치 | 소유 형태 | 폐기 조건 |
|
|
|---|---|---|---|---|---|
|
|
| public/no-auth | `GET /health`, `/docs`, `/openapi.json` | 없음 | 없음 | 해당 없음 | 해당 없음 |
|
|
| read service-token | `GET /v1/assets`, `GET /v1/user_info` | admin context로 `POST /auth/service-tokens` | `/opt/portfoliohub/.env` 관리 경로 | 환경 단위 공동 소유 (release approver / repository-maintainers) | 노출 의심, 담당자 변경, 장애 후 revoke/rotate |
|
|
| trade-write service-token | `POST /v1/assets/{asset_id}/ledger/trades` | admin context로 `POST /auth/service-tokens` | `/opt/portfoliohub/.env` 관리 경로 | 환경 단위 공동 소유 (release approver / repository-maintainers) | 노출 의심, 담당자 변경, 장애 후 revoke/rotate |
|
|
| admin auth context | `POST /auth/service-tokens` (token 발급) | `/auth/login` (steady-state) 또는 break-glass env token | 개인 소유 (공유 금지) | named operator 개인 소유 | 담당자 퇴임, 노출 의심 |
|
|
|
|
### 4. Recommended production smoke flow
|
|
|
|
1. 운영자 개인 admin 계정으로 로그인해 admin session token을 확보한다.
|
|
2. 필요하면 동일 컨텍스트로 client token을 발급해 짧은 작업 단위 credential로 전환한다.
|
|
3. admin auth context로 `POST /auth/service-tokens`를 호출해 read smoke token과 trade-write smoke token을 분리 발급한다.
|
|
4. raw token은 즉시 `/opt/portfoliohub/.env` 기준 운영 저장소에 저장하고, release evidence에는 token name, token_prefix, permission, runtime_id만 기록한다.
|
|
5. public/no-auth, read service-token, trade-write service-token, admin auth context를 분리해 smoke를 수행한다.
|
|
|
|
예시:
|
|
|
|
```bash
|
|
curl -X POST "<BASE_URL>/auth/login" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"username":"<ADMIN_USERNAME>","password":"<ADMIN_PASSWORD>"}'
|
|
|
|
curl -X POST "<BASE_URL>/auth/service-tokens" \
|
|
-H "Authorization: Bearer <ADMIN_SESSION_OR_CLIENT_TOKEN>" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"name":"prod-smoke-read","username":"<SMOKE_USERNAME>","permission":"read"}'
|
|
|
|
curl -X POST "<BASE_URL>/auth/service-tokens" \
|
|
-H "Authorization: Bearer <ADMIN_SESSION_OR_CLIENT_TOKEN>" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"name":"prod-smoke-trade-write","username":"<SMOKE_USERNAME>","permission":"trade-write"}'
|
|
```
|
|
|
|
### 5. Provenance check procedure
|
|
|
|
- `pfhub service-token list --user <username>`로 token name, token_prefix, token_id, user_id, permission, created_at, last_used_at, revoked_at를 확인한다.
|
|
- `pfhub service-token rotate <name> --force`로 무기한 token을 rotate하고 새 runtime_id를 확보한다.
|
|
- `pfhub user get <username>`로 사용자에 연결된 service token 이름 목록을 교차 확인한다.
|
|
- `GET /v1/user_info`를 live service-token으로 호출해 runtime_id와 permission을 확인하고, 발급 또는 rotate 시 기록한 runtime_id와 대조한다.
|
|
|
|
### 6. Log and evidence collection
|
|
|
|
**Canonical smoke environment**: production을 canonical acceptance 환경으로 사용한다. staging smoke는 optional preflight로만 사용하며 release acceptance 기준이 되지 않는다. 절차의 source of truth는 이 README이며, 실행 evidence의 canonical record는 해당 release 문서에 남긴다.
|
|
|
|
**로그 접근 경로:**
|
|
- 현재 production log stack은 Linux/Ubuntu host에서 systemd로 구동되는 `portfoliohub` uvicorn 서비스와 Caddy reverse proxy다.
|
|
- startup 및 애플리케이션 로그는 `sudo journalctl -u portfoliohub -n 200 --no-pager` 또는 `sudo journalctl -u portfoliohub -f`로 확인한다.
|
|
- reverse proxy 또는 access 수준 로그는 `sudo journalctl -u caddy -n 200 --no-pager` 또는 `sudo journalctl -u caddy -f`로 확인한다.
|
|
- Loki는 `LOKI_ENABLED`와 실제 수집 파이프라인이 확인된 환경에서만 보조 집계 경로로 취급하며, 현재 canonical production path는 아니다.
|
|
- 외부 URL만으로는 로그 접근이 되지 않으므로, 운영자는 production host의 systemd journal 조회 권한을 가져야 한다.
|
|
|
|
**HTTP 오류 분류:**
|
|
- `401 Unauthorized`: bearer token 미제공 또는 유효하지 않은 token. 인증 정보 자체가 없거나 만료된 경우.
|
|
- `403 Forbidden`: token은 유효하나 해당 엔드포인트에 필요한 permission 미보유. read token으로 trade-write 엔드포인트 호출 시 발생.
|
|
- `404 Not Found`: 인증/권한은 통과했으나 대상 리소스(portfolio, asset 등)가 DB에 존재하지 않음. 권한 문제와 혼동하지 않도록 주의.
|
|
|
|
**Release evidence 기록 필드:**
|
|
- 포함: 시각(executed_at, completed_at), endpoint, HTTP status code, source system(journalctl unit name), token name, token_prefix, runtime_id
|
|
- 제외: raw secret, raw token 값
|
|
|
|
## Planned next steps
|
|
|
|
1. Add integration tests for DB-backed portfolio and user flows.
|
|
2. Add unrealized PnL (current price integration) — deferred from dcy-007.
|
|
3. Add pagination for trade history and PnL endpoints.
|
|
4. Remove legacy `POST /portfolios/{id}/buy` and `POST /portfolios/{id}/sell` endpoints (currently deprecated-only).
|
|
5. Fix `pfhub tag apply asset` / `pfhub tag remove asset` CLI mismatch (targets portfolios table instead of assets).
|