refactor(auth): remove bot owner linkage and enforce bot-level permissions
This commit is contained in:
@@ -57,8 +57,8 @@ def _to_auth_token_response(token: AuthToken) -> AuthTokenResponse:
|
||||
def _to_bot_principal_response(bot: BotPrincipal) -> BotPrincipalResponse:
|
||||
return BotPrincipalResponse(
|
||||
id=bot.id,
|
||||
owner_user_id=bot.owner_user_id,
|
||||
name=bot.name,
|
||||
permission=bot.permission,
|
||||
description=bot.description,
|
||||
is_active=bot.is_active,
|
||||
created_at=bot.created_at,
|
||||
|
||||
@@ -56,8 +56,8 @@ class TokenPrincipal:
|
||||
@dataclass
|
||||
class BotPrincipal:
|
||||
id: str
|
||||
owner_user_id: str
|
||||
name: str
|
||||
permission: str
|
||||
description: str | None = None
|
||||
is_active: bool = True
|
||||
created_at: datetime | None = None
|
||||
|
||||
@@ -62,8 +62,8 @@ class PortfolioRepository(Protocol):
|
||||
def create_bot_principal(
|
||||
self,
|
||||
*,
|
||||
owner_user_id: str,
|
||||
name: str,
|
||||
permission: str,
|
||||
description: str | None = None,
|
||||
) -> BotPrincipal: ...
|
||||
|
||||
|
||||
@@ -47,8 +47,8 @@ _SELECT_AUTH_TOKEN = """
|
||||
_SELECT_BOT_PRINCIPAL = """
|
||||
SELECT
|
||||
id,
|
||||
owner_user_id,
|
||||
name,
|
||||
permission,
|
||||
description,
|
||||
is_active,
|
||||
created_at,
|
||||
@@ -158,8 +158,8 @@ class PostgresPortfolioRepository:
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS bot_principals (
|
||||
id TEXT PRIMARY KEY,
|
||||
owner_user_id TEXT NOT NULL,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
permission TEXT NOT NULL,
|
||||
description TEXT,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
@@ -169,6 +169,9 @@ class PostgresPortfolioRepository:
|
||||
"""
|
||||
)
|
||||
cur.execute("ALTER TABLE auth_tokens ADD COLUMN IF NOT EXISTS bot_principal_id TEXT")
|
||||
cur.execute("ALTER TABLE bot_principals ADD COLUMN IF NOT EXISTS permission TEXT")
|
||||
cur.execute("UPDATE bot_principals SET permission = 'read' WHERE permission IS NULL")
|
||||
cur.execute("ALTER TABLE bot_principals ALTER COLUMN permission SET NOT NULL")
|
||||
cur.execute(
|
||||
"""
|
||||
CREATE INDEX IF NOT EXISTS idx_auth_tokens_user_created
|
||||
@@ -304,8 +307,8 @@ class PostgresPortfolioRepository:
|
||||
raise ValueError("봇을 찾을 수 없습니다")
|
||||
return BotPrincipal(
|
||||
id=row[0],
|
||||
owner_user_id=row[1],
|
||||
name=row[2],
|
||||
name=row[1],
|
||||
permission=row[2],
|
||||
description=row[3] if len(row) > 3 else None,
|
||||
is_active=bool(row[4]) if len(row) > 4 else True,
|
||||
created_at=row[5] if len(row) > 5 else None,
|
||||
@@ -515,9 +518,10 @@ class PostgresPortfolioRepository:
|
||||
) -> AuthToken:
|
||||
self.ensure_schema()
|
||||
with self._connect().cursor() as cur:
|
||||
cur.execute("SELECT 1 FROM users WHERE id = %s", (user_id,))
|
||||
if cur.fetchone() is None:
|
||||
raise ValueError("사용자를 찾을 수 없습니다")
|
||||
if token_type != "bot":
|
||||
cur.execute("SELECT 1 FROM users WHERE id = %s", (user_id,))
|
||||
if cur.fetchone() is None:
|
||||
raise ValueError("사용자를 찾을 수 없습니다")
|
||||
if bot_principal_id is not None:
|
||||
cur.execute("SELECT 1 FROM bot_principals WHERE id = %s", (bot_principal_id,))
|
||||
if cur.fetchone() is None:
|
||||
@@ -562,15 +566,12 @@ class PostgresPortfolioRepository:
|
||||
def create_bot_principal(
|
||||
self,
|
||||
*,
|
||||
owner_user_id: str,
|
||||
name: str,
|
||||
permission: str,
|
||||
description: str | None = None,
|
||||
) -> BotPrincipal:
|
||||
self.ensure_schema()
|
||||
with self._connect().cursor() as cur:
|
||||
cur.execute("SELECT 1 FROM users WHERE id = %s", (owner_user_id,))
|
||||
if cur.fetchone() is None:
|
||||
raise ValueError("소유 사용자를 찾을 수 없습니다")
|
||||
cur.execute("SELECT 1 FROM bot_principals WHERE name = %s", (name,))
|
||||
if cur.fetchone() is not None:
|
||||
raise ValueError(f"이미 존재하는 bot name입니다: {name}")
|
||||
@@ -578,11 +579,11 @@ class PostgresPortfolioRepository:
|
||||
bot_id = str(uuid.uuid4())
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO bot_principals (id, owner_user_id, name, description)
|
||||
INSERT INTO bot_principals (id, name, permission, description)
|
||||
VALUES (%s, %s, %s, %s)
|
||||
RETURNING id, owner_user_id, name, description, is_active, created_at, updated_at, deactivated_at
|
||||
RETURNING id, name, permission, description, is_active, created_at, updated_at, deactivated_at
|
||||
""",
|
||||
(bot_id, owner_user_id, name, description),
|
||||
(bot_id, name, permission, description),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
return self._row_to_bot_principal(row)
|
||||
@@ -615,7 +616,7 @@ class PostgresPortfolioRepository:
|
||||
UPDATE bot_principals
|
||||
SET is_active = FALSE, deactivated_at = NOW(), updated_at = NOW()
|
||||
WHERE id = %s
|
||||
RETURNING id, owner_user_id, name, description, is_active, created_at, updated_at, deactivated_at
|
||||
RETURNING id, name, permission, description, is_active, created_at, updated_at, deactivated_at
|
||||
""",
|
||||
(bot_principal_id,),
|
||||
)
|
||||
|
||||
+2
-2
@@ -66,8 +66,8 @@ class IssuedClientTokenResponse(BaseModel):
|
||||
|
||||
|
||||
class CreateBotPrincipalRequest(BaseModel):
|
||||
owner_user_id: str = Field(min_length=1)
|
||||
name: str = Field(min_length=1)
|
||||
permission: Literal["read", "trade-write", "admin"]
|
||||
description: str | None = None
|
||||
|
||||
@field_validator("name")
|
||||
@@ -81,8 +81,8 @@ class CreateBotPrincipalRequest(BaseModel):
|
||||
|
||||
class BotPrincipalResponse(BaseModel):
|
||||
id: str
|
||||
owner_user_id: str
|
||||
name: str
|
||||
permission: Literal["read", "trade-write", "admin"]
|
||||
description: str | None = None
|
||||
is_active: bool
|
||||
created_at: datetime | None = None
|
||||
|
||||
@@ -103,10 +103,9 @@ class AuthService:
|
||||
self._repository.revoke_auth_token(auth_context.user_id, token_id)
|
||||
|
||||
def create_bot_principal(self, payload: CreateBotPrincipalRequest) -> BotPrincipal:
|
||||
self._repository.get_user(payload.owner_user_id)
|
||||
return self._repository.create_bot_principal(
|
||||
owner_user_id=payload.owner_user_id,
|
||||
name=payload.name,
|
||||
permission=payload.permission,
|
||||
description=payload.description,
|
||||
)
|
||||
|
||||
@@ -124,16 +123,15 @@ class AuthService:
|
||||
if not bot.is_active:
|
||||
raise ValueError("비활성화된 봇에는 토큰을 발급할 수 없습니다")
|
||||
|
||||
owner = self._repository.get_user(bot.owner_user_id)
|
||||
owner_permission = Permission(owner.permission)
|
||||
requested = Permission(payload.permission or owner.permission)
|
||||
if not _has_permission(owner_permission, requested):
|
||||
raise ValueError("요청한 토큰 권한이 봇 소유자의 권한을 초과합니다")
|
||||
bot_permission = Permission(bot.permission)
|
||||
requested = Permission(payload.permission or bot.permission)
|
||||
if not _has_permission(bot_permission, requested):
|
||||
raise ValueError("요청한 토큰 권한이 봇 권한을 초과합니다")
|
||||
|
||||
raw_token = generate_token()
|
||||
expires_at = self._build_expiration(payload.expires_in_days, self._settings.bot_token_default_ttl_days)
|
||||
auth_token = self._repository.create_auth_token(
|
||||
owner.id,
|
||||
bot.id,
|
||||
name=payload.name,
|
||||
token_hash=hash_token(raw_token),
|
||||
token_prefix=token_prefix(raw_token),
|
||||
|
||||
Reference in New Issue
Block a user