feat(auth): add admin-managed bot API keys with expiry and revocation

This commit is contained in:
2026-04-03 13:12:29 +00:00
parent 06b1805b92
commit 6998936858
15 changed files with 935 additions and 34 deletions
+124 -4
View File
@@ -6,11 +6,21 @@ from typing import NoReturn
from fastapi import APIRouter, Depends, HTTPException, Response, status
from app.core.auth import AuthContext, require_read
from app.domain.models import AuthToken, User
from app.schemas.auth import AuthTokenResponse, CreateClientTokenRequest, IssuedClientTokenResponse, LoginRequest, TokenResponse
from app.core.auth import AuthContext, require_admin, require_read
from app.domain.models import AuthToken, BotPrincipal, User
from app.schemas.auth import (
AuthTokenResponse,
BotPrincipalResponse,
CreateBotPrincipalRequest,
CreateBotTokenRequest,
CreateClientTokenRequest,
IssuedBotTokenResponse,
IssuedClientTokenResponse,
LoginRequest,
TokenResponse,
)
from app.schemas.user import UserResponse
from app.services.auth_service import AuthService, IssuedClientToken, LoginResult, get_auth_service
from app.services.auth_service import AuthService, IssuedBotToken, IssuedClientToken, LoginResult, get_auth_service
router = APIRouter(prefix="/auth", tags=["auth"])
@@ -36,6 +46,7 @@ def _to_auth_token_response(token: AuthToken) -> AuthTokenResponse:
token_prefix=token.token_prefix,
permission=token.permission,
token_type=token.token_type,
bot_principal_id=token.bot_principal_id,
expires_at=token.expires_at,
last_used_at=token.last_used_at,
revoked_at=token.revoked_at,
@@ -43,6 +54,19 @@ 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,
description=bot.description,
is_active=bot.is_active,
created_at=bot.created_at,
updated_at=bot.updated_at,
deactivated_at=bot.deactivated_at,
)
def _to_login_response(result: LoginResult) -> TokenResponse:
return TokenResponse(
access_token=result.token,
@@ -59,6 +83,13 @@ def _to_issued_token_response(result: IssuedClientToken) -> IssuedClientTokenRes
)
def _to_issued_bot_token_response(result: IssuedBotToken) -> IssuedBotTokenResponse:
return IssuedBotTokenResponse(
token=result.token,
auth_token=_to_auth_token_response(result.auth_token),
)
def _raise_auth_error(exc: ValueError) -> NoReturn:
detail = str(exc)
if "잘못된 사용자명 또는 비밀번호" in detail:
@@ -111,4 +142,93 @@ def revoke_token(
service.revoke_token(auth_context, token_id)
except ValueError as exc:
_raise_auth_error(exc)
return Response(status_code=status.HTTP_204_NO_CONTENT)
@router.post("/bots", response_model=BotPrincipalResponse, status_code=status.HTTP_201_CREATED, dependencies=[Depends(require_admin)])
def create_bot_principal(
payload: CreateBotPrincipalRequest,
service: AuthService = Depends(get_auth_service),
) -> BotPrincipalResponse:
try:
return _to_bot_principal_response(service.create_bot_principal(payload))
except ValueError as exc:
_raise_auth_error(exc)
@router.get("/bots", response_model=list[BotPrincipalResponse], dependencies=[Depends(require_admin)])
def list_bot_principals(
is_active: bool | None = None,
service: AuthService = Depends(get_auth_service),
) -> list[BotPrincipalResponse]:
try:
return [_to_bot_principal_response(bot) for bot in service.list_bot_principals(is_active=is_active)]
except ValueError as exc:
_raise_auth_error(exc)
@router.get("/bots/{bot_id}", response_model=BotPrincipalResponse, dependencies=[Depends(require_admin)])
def get_bot_principal(
bot_id: str,
service: AuthService = Depends(get_auth_service),
) -> BotPrincipalResponse:
try:
return _to_bot_principal_response(service.get_bot_principal(bot_id))
except ValueError as exc:
_raise_auth_error(exc)
@router.delete("/bots/{bot_id}", response_model=BotPrincipalResponse, dependencies=[Depends(require_admin)])
def deactivate_bot_principal(
bot_id: str,
service: AuthService = Depends(get_auth_service),
) -> BotPrincipalResponse:
try:
return _to_bot_principal_response(service.deactivate_bot_principal(bot_id))
except ValueError as exc:
_raise_auth_error(exc)
@router.post(
"/bots/{bot_id}/tokens",
response_model=IssuedBotTokenResponse,
status_code=status.HTTP_201_CREATED,
dependencies=[Depends(require_admin)],
)
def issue_bot_token(
bot_id: str,
payload: CreateBotTokenRequest,
service: AuthService = Depends(get_auth_service),
) -> IssuedBotTokenResponse:
try:
return _to_issued_bot_token_response(service.issue_bot_token(bot_id, payload))
except ValueError as exc:
_raise_auth_error(exc)
@router.get("/bots/{bot_id}/tokens", response_model=list[AuthTokenResponse], dependencies=[Depends(require_admin)])
def list_bot_tokens(
bot_id: str,
service: AuthService = Depends(get_auth_service),
) -> list[AuthTokenResponse]:
try:
return [_to_auth_token_response(token) for token in service.list_bot_tokens(bot_id)]
except ValueError as exc:
_raise_auth_error(exc)
@router.delete(
"/bots/{bot_id}/tokens/{token_id}",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(require_admin)],
)
def revoke_bot_token(
bot_id: str,
token_id: str,
service: AuthService = Depends(get_auth_service),
) -> Response:
try:
service.revoke_bot_token(bot_id, token_id)
except ValueError as exc:
_raise_auth_error(exc)
return Response(status_code=status.HTTP_204_NO_CONTENT)