agnes-the-ai-analyst/app/auth/dependencies.py
ZdenekSrotyr a3918d3833 feat: add FastAPI server with auth, RBAC, and all API endpoints
- JWT auth with role-based access control (viewer/analyst/admin/km_admin)
- Endpoints: health, sync manifest, data download, query, users CRUD,
  corporate memory, session/artifact upload
- 18 API tests covering auth, RBAC, all endpoints
2026-03-27 15:19:18 +01:00

88 lines
2.5 KiB
Python

"""FastAPI auth dependencies — current user, role checking."""
from enum import Enum
from typing import Optional
import duckdb
from fastapi import Depends, HTTPException, Header, status
from app.auth.jwt import verify_token
from src.db import get_system_db
from src.repositories.users import UserRepository
class Role(str, Enum):
VIEWER = "viewer"
ANALYST = "analyst"
ADMIN = "admin"
KM_ADMIN = "km_admin"
ROLE_HIERARCHY = {
Role.VIEWER: 0,
Role.ANALYST: 1,
Role.KM_ADMIN: 2,
Role.ADMIN: 3,
}
def _get_db():
conn = get_system_db()
try:
yield conn
finally:
conn.close()
async def get_current_user(
authorization: Optional[str] = Header(None),
conn: duckdb.DuckDBPyConnection = Depends(_get_db),
) -> dict:
"""Extract and validate JWT from Authorization header. Returns user dict."""
if not authorization or not authorization.startswith("Bearer "):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Missing or invalid Authorization header",
)
token = authorization.removeprefix("Bearer ")
payload = verify_token(token)
if not payload:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid or expired token",
)
repo = UserRepository(conn)
user = repo.get_by_id(payload.get("sub", ""))
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="User not found",
)
return user
async def get_optional_user(
authorization: Optional[str] = Header(None),
conn: duckdb.DuckDBPyConnection = Depends(_get_db),
) -> Optional[dict]:
"""Like get_current_user but returns None instead of 401 if no token."""
if not authorization or not authorization.startswith("Bearer "):
return None
try:
return await get_current_user(authorization, conn)
except HTTPException:
return None
def require_role(minimum_role: Role):
"""Dependency factory: require user has at least the given role."""
async def _check(user: dict = Depends(get_current_user)):
user_role = Role(user.get("role", "viewer"))
if ROLE_HIERARCHY.get(user_role, 0) < ROLE_HIERARCHY.get(minimum_role, 0):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=f"Requires role {minimum_role.value} or higher",
)
return user
return _check