fix(daemons): block owner transfer while assigned
This commit is contained in:
@@ -13,7 +13,7 @@ from typing import Any
|
||||
import psycopg
|
||||
from psycopg import sql
|
||||
|
||||
from app.domain.errors import DaemonInactiveError, DaemonNotFoundError, StrategyInactiveError, StrategyNotFoundError
|
||||
from app.domain.errors import DaemonInactiveError, DaemonInUseError, DaemonNotFoundError, StrategyInactiveError, StrategyNotFoundError
|
||||
from app.domain.models import Asset, AuthToken, BrokerCredential, Daemon, LedgerEntry, Portfolio, PortfolioComment, PortfolioTag, StrategySet, Tag, TokenPrincipal, User
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -2843,13 +2843,10 @@ class PostgresPortfolioRepository:
|
||||
transaction is rolled back, preventing the state where the owner has changed but
|
||||
the previous owner's credentials remain (credential leak — dcy-011 OQ-3).
|
||||
|
||||
Execution order inside the transaction:
|
||||
1. DELETE FROM broker_credentials WHERE daemon_id = %s (credential first)
|
||||
2. UPDATE daemons SET owner_user_id = %s ... (owner second)
|
||||
This order is also safe if the transaction machinery is somehow bypassed: if
|
||||
only step 1 completes the owner is unchanged and credentials are gone (no leak);
|
||||
if only step 2 completes the old credentials are still present but the caller
|
||||
still owns them (no cross-owner leak).
|
||||
The daemon row is locked FOR UPDATE before checking portfolio references.
|
||||
Portfolio assignment takes the same daemon row FOR SHARE before writing, so
|
||||
either an earlier assignment makes this transfer fail or an earlier transfer
|
||||
makes the pending assignment fail its same-owner validation.
|
||||
"""
|
||||
self.ensure_schema()
|
||||
conn = self._connect()
|
||||
@@ -2858,11 +2855,17 @@ class PostgresPortfolioRepository:
|
||||
with conn.transaction():
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"SELECT id FROM daemons WHERE id = %s",
|
||||
"SELECT id FROM daemons WHERE id = %s FOR UPDATE",
|
||||
(daemon_id,),
|
||||
)
|
||||
if cur.fetchone() is None:
|
||||
raise ValueError("데몬을 찾을 수 없습니다")
|
||||
cur.execute(
|
||||
"SELECT 1 FROM portfolios WHERE managed_by_daemon_id = %s LIMIT 1",
|
||||
(daemon_id,),
|
||||
)
|
||||
if cur.fetchone() is not None:
|
||||
raise DaemonInUseError("배정된 포트폴리오가 있는 daemon의 소유자는 변경할 수 없습니다")
|
||||
# Step 1: delete credentials first (safe-order)
|
||||
cur.execute(
|
||||
"DELETE FROM broker_credentials WHERE daemon_id = %s",
|
||||
|
||||
Reference in New Issue
Block a user