77 lines
2.4 KiB
Python
77 lines
2.4 KiB
Python
"""API dependencies: current user, RBAC guards, correlation IDs."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import uuid
|
|
from datetime import datetime
|
|
from typing import Annotated
|
|
|
|
from fastapi import Depends, HTTPException, Request, status
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.db import get_db
|
|
from app.models import ManagementRole, ManagementSession, ManagementUser
|
|
|
|
SESSION_COOKIE = "mqtt_mgmt_session"
|
|
|
|
|
|
def _expires_at_aware(mgmt_session: ManagementSession) -> datetime:
|
|
"""Normalize expiry to aware UTC (sqlite returns naive datetimes)."""
|
|
from datetime import UTC
|
|
|
|
expires_at = mgmt_session.expires_at
|
|
if expires_at.tzinfo is None:
|
|
return expires_at.replace(tzinfo=UTC)
|
|
return expires_at
|
|
|
|
|
|
def _get_request_session(
|
|
request: Request, db: Session
|
|
) -> tuple[ManagementUser, ManagementSession]:
|
|
token = request.cookies.get(SESSION_COOKIE)
|
|
if not token:
|
|
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "not authenticated")
|
|
try:
|
|
session_id = uuid.UUID(token)
|
|
except ValueError as exc:
|
|
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "invalid session") from exc
|
|
|
|
from datetime import UTC, datetime
|
|
|
|
mgmt_session = db.get(ManagementSession, session_id)
|
|
if (
|
|
mgmt_session is None
|
|
or mgmt_session.revoked
|
|
or _expires_at_aware(mgmt_session) < datetime.now(UTC)
|
|
):
|
|
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "session expired")
|
|
user = db.get(ManagementUser, mgmt_session.user_id)
|
|
if user is None or not user.enabled:
|
|
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "user disabled")
|
|
return user, mgmt_session
|
|
|
|
|
|
def get_current_user(
|
|
request: Request, db: Annotated[Session, Depends(get_db)]
|
|
) -> ManagementUser:
|
|
user, _ = _get_request_session(request, db)
|
|
return user
|
|
|
|
|
|
def require_roles(*roles: ManagementRole):
|
|
"""Dependency factory enforcing one of the given management roles."""
|
|
|
|
def _guard(
|
|
user: Annotated[ManagementUser, Depends(get_current_user)],
|
|
) -> ManagementUser:
|
|
if user.role == ManagementRole.SUPER_ADMIN:
|
|
return user
|
|
if user.role not in roles:
|
|
raise HTTPException(status.HTTP_403_FORBIDDEN, "insufficient role")
|
|
return user
|
|
|
|
return _guard
|
|
|
|
|
|
CurrentUser = Annotated[ManagementUser, Depends(get_current_user)]
|
|
DbSession = Annotated[Session, Depends(get_db)] |