Files
TJWaterServerBinary/app/auth/metadata_dependencies.py
T
jiang c7947a7481 feat(auth)!: migrate customer metadata auth
Remove local auth and user-management endpoints in favor of Keycloak-backed metadata users, project context, admin metadata APIs, and agent auth context.
2026-06-13 15:00:58 +08:00

105 lines
3.2 KiB
Python

from dataclasses import dataclass
from uuid import UUID
import logging
from fastapi import Depends, HTTPException, status
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.ext.asyncio import AsyncSession
from app.auth.keycloak_dependencies import get_current_keycloak_payload
from app.infra.db.metadb.database import get_metadata_session
from app.infra.db.metadb.repositories.metadata_repository import MetadataRepository
logger = logging.getLogger(__name__)
async def get_metadata_repository(
session: AsyncSession = Depends(get_metadata_session),
) -> MetadataRepository:
return MetadataRepository(session)
def _keycloak_sub_from_payload(payload: dict) -> UUID:
sub = payload.get("sub")
if not sub:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Missing subject claim",
headers={"WWW-Authenticate": "Bearer"},
)
try:
return UUID(str(sub))
except ValueError as exc:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid subject claim",
headers={"WWW-Authenticate": "Bearer"},
) from exc
def _username_from_payload(payload: dict) -> str | None:
username = payload.get("preferred_username") or payload.get("username")
return str(username) if username else None
def _email_from_payload(payload: dict) -> str | None:
email = payload.get("email")
return str(email) if email else None
async def get_current_metadata_user(
keycloak_payload: dict = Depends(get_current_keycloak_payload),
metadata_repo: MetadataRepository = Depends(get_metadata_repository),
):
keycloak_sub = _keycloak_sub_from_payload(keycloak_payload)
try:
user = await metadata_repo.get_user_by_keycloak_id(keycloak_sub)
except SQLAlchemyError as exc:
logger.error(
"Metadata DB error while resolving current user",
exc_info=True,
)
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail=f"Metadata database error: {exc}",
) from exc
if not user or not user.is_active:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, detail="Inactive user"
)
try:
user = await metadata_repo.refresh_user_keycloak_snapshot(
user,
username=_username_from_payload(keycloak_payload),
email=_email_from_payload(keycloak_payload),
)
except SQLAlchemyError as exc:
logger.error(
"Metadata DB error while refreshing current user snapshot",
exc_info=True,
)
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail=f"Metadata database error: {exc}",
) from exc
return user
async def get_current_metadata_admin(
user=Depends(get_current_metadata_user),
):
if user.is_superuser or user.role == "admin":
return user
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, detail="Admin access required"
)
@dataclass(frozen=True)
class _AuthBypassUser:
id: UUID = UUID(int=0)
role: str = "admin"
is_superuser: bool = True
is_active: bool = True