feat(auth): owner-bound bot tokens and self-service bot management

This commit is contained in:
2026-04-14 00:34:44 +09:00
parent 9787cf3159
commit 2dab17cbb1
16 changed files with 576 additions and 35 deletions
+55 -1
View File
@@ -13,7 +13,13 @@ from app.core.security import generate_token, hash_token, token_prefix, verify_p
from app.domain.models import AuthToken, BotPrincipal, User
from app.repositories.factory import get_portfolio_repository_from_settings
from app.repositories.portfolio_repository import PortfolioRepository
from app.schemas.auth import CreateBotPrincipalRequest, CreateBotTokenRequest, CreateClientTokenRequest, LoginRequest
from app.schemas.auth import (
CreateBotPrincipalRequest,
CreateBotTokenRequest,
CreateClientTokenRequest,
CreateSelfBotPrincipalRequest,
LoginRequest,
)
@dataclass
@@ -105,6 +111,16 @@ class AuthService:
def create_bot_principal(self, payload: CreateBotPrincipalRequest) -> BotPrincipal:
return self._repository.create_bot_principal(
name=payload.name,
owner_user_id=payload.owner_user_id,
permission=payload.permission,
description=payload.description,
)
def create_owned_bot_principal(self, auth_context: AuthContext, payload: CreateSelfBotPrincipalRequest) -> BotPrincipal:
user_id = self._require_db_user_context(auth_context)
return self._repository.create_bot_principal(
name=payload.name,
owner_user_id=user_id,
permission=payload.permission,
description=payload.description,
)
@@ -112,6 +128,10 @@ class AuthService:
def list_bot_principals(self, *, is_active: bool | None = None) -> list[BotPrincipal]:
return self._repository.list_bot_principals(is_active=is_active)
def list_owned_bot_principals(self, auth_context: AuthContext, *, is_active: bool | None = None) -> list[BotPrincipal]:
user_id = self._require_db_user_context(auth_context)
return self._repository.list_bot_principals(is_active=is_active, owner_user_id=user_id)
def get_bot_principal(self, bot_principal_id: str) -> BotPrincipal:
return self._repository.get_bot_principal(bot_principal_id)
@@ -142,14 +162,48 @@ class AuthService:
)
return IssuedBotToken(token=raw_token, auth_token=auth_token)
def issue_owned_bot_token(
self,
auth_context: AuthContext,
bot_principal_id: str,
payload: CreateBotTokenRequest,
) -> IssuedBotToken:
user_id = self._require_db_user_context(auth_context)
bot = self._repository.get_bot_principal(bot_principal_id)
if bot.owner_user_id != user_id:
raise ValueError("봇 접근 권한이 없습니다")
return self.issue_bot_token(bot_principal_id, payload)
def list_bot_tokens(self, bot_principal_id: str) -> list[AuthToken]:
self._repository.get_bot_principal(bot_principal_id)
return self._repository.list_bot_auth_tokens(bot_principal_id)
def list_owned_bot_tokens(self, auth_context: AuthContext, bot_principal_id: str) -> list[AuthToken]:
user_id = self._require_db_user_context(auth_context)
bot = self._repository.get_bot_principal(bot_principal_id)
if bot.owner_user_id != user_id:
raise ValueError("봇 접근 권한이 없습니다")
return self._repository.list_bot_auth_tokens(bot_principal_id)
def revoke_bot_token(self, bot_principal_id: str, token_id: str) -> None:
self._repository.get_bot_principal(bot_principal_id)
self._repository.revoke_bot_auth_token(bot_principal_id, token_id)
def revoke_owned_bot_token(self, auth_context: AuthContext, bot_principal_id: str, token_id: str) -> None:
user_id = self._require_db_user_context(auth_context)
bot = self._repository.get_bot_principal(bot_principal_id)
if bot.owner_user_id != user_id:
raise ValueError("봇 접근 권한이 없습니다")
self._repository.revoke_bot_auth_token(bot_principal_id, token_id)
@staticmethod
def _require_db_user_context(auth_context: AuthContext) -> str:
if auth_context.bot_principal_id is not None:
raise ValueError("봇 토큰으로는 사용자 전용 봇 관리를 수행할 수 없습니다")
if auth_context.user_id is None:
raise ValueError("DB 사용자 컨텍스트에서만 사용자 전용 봇 관리를 수행할 수 있습니다")
return auth_context.user_id
@staticmethod
def _build_expiration(expires_in_days: int | None, default_days: int) -> datetime:
days = expires_in_days if expires_in_days is not None else default_days