feat: 주요 CLI, 인증, 서비스 로직 및 문서 개선
This commit is contained in:
@@ -4,12 +4,14 @@ from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Any
|
||||
|
||||
from fastapi import Depends
|
||||
|
||||
from app.core.auth import AuthContext, Permission, _has_permission
|
||||
from app.core.config import Settings, get_settings
|
||||
from app.core.security import generate_token, hash_token, token_prefix, verify_password
|
||||
from app.core.security import build_audit_event, generate_token, hash_token, mask_secret, token_prefix, verify_password
|
||||
from app.domain.services import normalize_expiry_days
|
||||
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
|
||||
@@ -138,7 +140,14 @@ class AuthService:
|
||||
def deactivate_bot_principal(self, bot_principal_id: str) -> BotPrincipal:
|
||||
return self._repository.deactivate_bot_principal(bot_principal_id)
|
||||
|
||||
def issue_bot_token(self, bot_principal_id: str, payload: CreateBotTokenRequest) -> IssuedBotToken:
|
||||
def issue_bot_token(
|
||||
self,
|
||||
bot_principal_id: str,
|
||||
payload: CreateBotTokenRequest,
|
||||
*,
|
||||
actor_permission: Permission = Permission.ADMIN,
|
||||
actor_id: str | None = None,
|
||||
) -> IssuedBotToken:
|
||||
bot = self._repository.get_bot_principal(bot_principal_id)
|
||||
if not bot.is_active:
|
||||
raise ValueError("비활성화된 봇에는 토큰을 발급할 수 없습니다")
|
||||
@@ -149,7 +158,11 @@ class AuthService:
|
||||
raise ValueError("요청한 토큰 권한이 봇 권한을 초과합니다")
|
||||
|
||||
raw_token = generate_token()
|
||||
expires_at = self._build_expiration(payload.expires_in_days, self._settings.bot_token_default_ttl_days)
|
||||
expires_at = self._build_bot_expiration(
|
||||
payload.expires_in_days,
|
||||
no_expiry=payload.no_expiry,
|
||||
actor_permission=actor_permission,
|
||||
)
|
||||
auth_token = self._repository.create_auth_token(
|
||||
bot.id,
|
||||
name=payload.name,
|
||||
@@ -160,6 +173,20 @@ class AuthService:
|
||||
bot_principal_id=bot.id,
|
||||
expires_at=expires_at,
|
||||
)
|
||||
try:
|
||||
self._record_audit_event(
|
||||
build_audit_event(
|
||||
action="bot_token.issue",
|
||||
actor_id=actor_id or bot.owner_user_id,
|
||||
bot_id=bot.id,
|
||||
token_id=auth_token.id,
|
||||
token_ref=mask_secret(raw_token, prefix=6, suffix=4),
|
||||
result="success",
|
||||
)
|
||||
)
|
||||
except Exception as exc:
|
||||
self._repository.revoke_bot_auth_token(bot.id, auth_token.id)
|
||||
raise ValueError("감사 로그 기록에 실패하여 토큰 발급을 취소했습니다") from exc
|
||||
return IssuedBotToken(token=raw_token, auth_token=auth_token)
|
||||
|
||||
def issue_owned_bot_token(
|
||||
@@ -172,7 +199,12 @@ class AuthService:
|
||||
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)
|
||||
return self.issue_bot_token(
|
||||
bot_principal_id,
|
||||
payload,
|
||||
actor_permission=auth_context.permission,
|
||||
actor_id=user_id,
|
||||
)
|
||||
|
||||
def list_bot_tokens(self, bot_principal_id: str) -> list[AuthToken]:
|
||||
self._repository.get_bot_principal(bot_principal_id)
|
||||
@@ -185,16 +217,31 @@ class AuthService:
|
||||
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)
|
||||
def revoke_bot_token(self, bot_principal_id: str, token_id: str, *, actor_id: str | None = None) -> datetime:
|
||||
bot = self._repository.get_bot_principal(bot_principal_id)
|
||||
self._repository.revoke_bot_auth_token(bot_principal_id, token_id)
|
||||
revoked_at = datetime.now(timezone.utc)
|
||||
try:
|
||||
self._record_audit_event(
|
||||
build_audit_event(
|
||||
action="bot_token.revoke",
|
||||
actor_id=actor_id or bot.owner_user_id,
|
||||
bot_id=bot.id,
|
||||
token_id=token_id,
|
||||
token_ref=mask_secret(token_id, prefix=6, suffix=4),
|
||||
result="success",
|
||||
)
|
||||
)
|
||||
except Exception as exc:
|
||||
raise ValueError("토큰은 폐기되었으나 감사 로그 기록에 실패했습니다") from exc
|
||||
return revoked_at
|
||||
|
||||
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)
|
||||
self.revoke_bot_token(bot_principal_id, token_id, actor_id=user_id)
|
||||
|
||||
@staticmethod
|
||||
def _require_db_user_context(auth_context: AuthContext) -> str:
|
||||
@@ -206,9 +253,31 @@ class AuthService:
|
||||
|
||||
@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
|
||||
days = normalize_expiry_days(expires_in_days, default_days=default_days)
|
||||
return datetime.now(timezone.utc) + timedelta(days=days)
|
||||
|
||||
@staticmethod
|
||||
def _build_bot_expiration(
|
||||
expires_in_days: int | None,
|
||||
*,
|
||||
no_expiry: bool,
|
||||
actor_permission: Permission,
|
||||
) -> datetime | None:
|
||||
if no_expiry:
|
||||
if expires_in_days is not None:
|
||||
raise ValueError("만료일 옵션과 무기한 옵션은 동시에 사용할 수 없습니다")
|
||||
if actor_permission is not Permission.ADMIN:
|
||||
raise ValueError("비관리자는 --no-expiry를 사용할 수 없습니다")
|
||||
return None
|
||||
|
||||
days = normalize_expiry_days(expires_in_days, default_days=365)
|
||||
return datetime.now(timezone.utc) + timedelta(days=days)
|
||||
|
||||
def _record_audit_event(self, payload: dict[str, Any]) -> None:
|
||||
recorder = getattr(self._repository, "record_audit_event", None)
|
||||
if callable(recorder):
|
||||
recorder(payload)
|
||||
|
||||
|
||||
def get_auth_service(
|
||||
settings: Settings = Depends(get_settings),
|
||||
|
||||
Reference in New Issue
Block a user