fix(daemons): block owner transfer while assigned
This commit is contained in:
+3
-1
@@ -167,6 +167,7 @@
|
||||
- `status="disabled"` (soft delete): `managed_by_daemon_id` is set to NULL; portfolio is excluded from `GET /v1/portfolios` by default. Dependent data (`portfolio_comments`, `tag_ids`) is preserved — never deleted on soft delete. READ operations on sub-resources (GET tags, GET comments) remain allowed; write mutations raise HTTP 409.
|
||||
- Status restoration: PATCH with `{"status": "enabled"}` re-activates a disabled portfolio; no other fields may be set in the same request.
|
||||
- Daemon assignment: a non-null `managed_by_daemon_id` on create or PATCH must reference an existing, active daemon owned by the target portfolio owner (admin `user_id` override uses the final target owner). The daemon `FOR SHARE` row lock and the portfolio INSERT/UPDATE share one transaction, so validation and write form a single atomic decision boundary; a concurrently deactivated daemon is rejected (HTTP 409) rather than committed. Deactivation after an assignment commits preserves the portfolio reference and follows the existing token-revocation contract.
|
||||
- Daemon owner transfer: `update_daemon_owner()` locks the daemon row `FOR UPDATE`, rejects a referenced daemon with a dedicated conflict, and deletes broker credentials only after the no-reference check. Assignment and owner transfer therefore serialize on the same daemon row: transfer-first makes the old owner's pending assignment foreign, while assignment-first makes the transfer conflict. Neither path auto-unassigns, migrates, or reassigns portfolios.
|
||||
|
||||
## Entity: `User`
|
||||
|
||||
@@ -257,6 +258,7 @@
|
||||
|
||||
- A daemon must exist and be active before a service token can be bound to it at issuance.
|
||||
- A daemon must exist, be active, and be owned by the target portfolio owner before a portfolio can be assigned to it (create or PATCH). Assignment validation locks the daemon row `FOR SHARE` inside the portfolio write transaction (Issue #27 / dcy-0004).
|
||||
- Owner transfer locks the daemon row `FOR UPDATE` and is rejected when any portfolio references the daemon. The rejection preserves the daemon owner, all portfolio assignments, and broker credentials; an unreferenced transfer retains the existing credential invalidation policy.
|
||||
- Deactivating a daemon (is_active=false) immediately revokes all bound service tokens via `revoke_tokens_by_daemon_id` (REQ-022/BAT-007) and blocks new token issuance. It does not auto-remove existing portfolio references; deactivation racing a new assignment is rejected with HTTP 409 when the deactivation commits first.
|
||||
- daemon_slug is immutable after creation; only display_name and is_active can be updated.
|
||||
|
||||
@@ -383,4 +385,4 @@
|
||||
- 2026-05-13 KST: bat-010 — REQ-009/023/028: added Entity: Daemon; added portfolios.managed_by_daemon_id; added auth_tokens.daemon_id; updated AuthToken State Rules (runtime_id vs daemon_id distinction, fail-closed policy); updated Portfolio Relationships; updated Model Summary.
|
||||
- 2026-05-11 KST: bat-008 — REQ-011 AC-2: added portfolios.market column; added Entity: AuthToken (runtime_id field); added Entity: BrokerCredential; added Entity: LedgerTrade; updated Model Summary.
|
||||
- 2026-05-11 KST: bat-006 — portfolios.portfolio_id changed to server-generated UUID PK; name column added with UNIQUE (owner_user_id, name) constraint; uq_portfolios_owner_name constraint documented; state rules updated.
|
||||
- 2026-04-30 KST: Bootstrap baseline created from repository observation for initial StagePilot adoption.
|
||||
- 2026-04-30 KST: Bootstrap baseline created from repository observation for initial StagePilot adoption.
|
||||
|
||||
@@ -126,6 +126,7 @@
|
||||
- Each asset (portfolio) may be assigned to exactly one daemon via `portfolios.managed_by_daemon_id`. Mismatch or unassigned asset → HTTP 403 on ledger writes.
|
||||
- **Token rebind** (BAT-007/REQ-023): `pfhub service-token rebind <name> --daemon-id <id>` — daemon_id 변경, runtime_id 불변. 대상 daemon은 active이어야 하고 token 소유 user와 일치해야 한다.
|
||||
- **Daemon deactivation** (BAT-007/REQ-022): `pfhub daemon update <daemon_id> --deactivate --force` — daemon is_active=false + 바인딩된 모든 service token 즉시 revoke.
|
||||
- **Admin daemon owner transfer**: `PATCH /v1/admin/daemons/{daemon_id}` with a changed `owner_user_id` is allowed only for an unassigned daemon. A referenced daemon returns HTTP 409; no daemon owner, portfolio assignment, or broker credential is changed. Owner transfer locks the daemon row `FOR UPDATE`, while portfolio assignment locks it `FOR SHARE`, so a concurrent operation either transfers first and rejects the old owner's assignment as HTTP 404 or assigns first and rejects the transfer as HTTP 409.
|
||||
- **NULL daemon migration CLI** (BAT-007/REQ-023): `pfhub service-token migrate-null-daemon --dry-run` — NULL daemon_id active service token inventory 출력 + user별 candidate daemon 목록. `--dry-run` 없이 실행 시 exit code 1 + 수동 안내.
|
||||
|
||||
### Endpoint: `GET /v1/assets`
|
||||
@@ -570,4 +571,4 @@
|
||||
- 2026-05-13 KST: bat-010 — REQ-006/007/008/011/023: added daemon authorization model section; updated GET /v1/assets (daemon-scoped filter, NULL 403); updated GET /v1/user_info (daemon_id field, NULL runtime_id → 200+null diagnostic); updated POST /v1/assets/{asset_id}/ledger/trades (daemon assignment authorization, fail-closed policy); added daemon CLI subcommand to CLI surface; updated v1 purpose and source discovery.
|
||||
- 2026-05-11 KST: bat-008 — REQ-011 AC-1: expanded v1 Consumer API section with authentication contract, detailed request/response schemas for GET /v1/assets, GET /v1/user_info, POST /v1/assets/{asset_id}/ledger/trades; updated GET /health response schema (REQ-004); added idempotency contract.
|
||||
- 2026-05-11 KST: bat-006 — Added v1 consumer API interface section (trex-basic); noted asset_id UUID change and simultaneous deployment constraint; updated CLI portfolio inputs to reflect name+user identification.
|
||||
- 2026-04-30 KST: Bootstrap baseline created from repository observation for initial StagePilot adoption.
|
||||
- 2026-04-30 KST: Bootstrap baseline created from repository observation for initial StagePilot adoption.
|
||||
|
||||
+10
-1
@@ -325,6 +325,15 @@
|
||||
4. `repo.revoke_tokens_by_daemon_id(daemon_id)` — 바인딩된 모든 active service token에 `revoked_at=NOW()` 설정.
|
||||
5. Console에 revoked count 출력.
|
||||
|
||||
## Flow: `PATCH /v1/admin/daemons/{daemon_id}` — Owner Transfer
|
||||
|
||||
1. Admin authentication is required; an unknown daemon returns HTTP 404.
|
||||
2. For an effective `owner_user_id` change, `update_daemon_owner()` starts one transaction and locks the daemon row `FOR UPDATE`.
|
||||
3. It checks `portfolios.managed_by_daemon_id` while holding that lock.
|
||||
- A reference exists: raise the dedicated conflict and return HTTP 409. Daemon owner, portfolio assignment, and broker credentials remain unchanged.
|
||||
- No reference exists: delete credentials, update the daemon owner, and commit the existing atomic credential-invalidation flow.
|
||||
4. Portfolio assignment uses `FOR SHARE` on the same daemon row before INSERT/UPDATE. Thus transfer-first makes the old owner's pending assignment fail as foreign; assignment-first makes the transfer fail HTTP 409. No automatic unassignment, portfolio owner migration, or alternate-daemon reassignment occurs.
|
||||
|
||||
## Flow: `Service-Token Rotate — daemon_id Invariant`
|
||||
|
||||
1. Operator calls `pfhub service-token rotate <name> --force`.
|
||||
@@ -492,4 +501,4 @@
|
||||
- 2026-05-11 KST: bat-007 — Added "Local DB Initialization and Validation" flow (Scenario A/B, repeatable run criteria); added "Alembic vs ensure_schema() Schema Ownership Boundary" flow.
|
||||
- 2026-05-11 KST: bat-006 — Added CLI portfolio name+user resolution flow; updated GET /v1/assets flow to note asset_id is UUID after BAT-006 migration.
|
||||
- 2026-05-01 KST: bat-001 — Added v1 API flows (service-token auth, GET /v1/assets, GET /v1/user_info, POST ledger/trades, POST /auth/service-tokens), startup validation flow.
|
||||
- 2026-04-30 KST: Bootstrap baseline created from repository observation for initial StagePilot adoption.
|
||||
- 2026-04-30 KST: Bootstrap baseline created from repository observation for initial StagePilot adoption.
|
||||
|
||||
Reference in New Issue
Block a user