53 lines
1.8 KiB
Python
53 lines
1.8 KiB
Python
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
|
|
from fastapi import Depends, Header, HTTPException, status
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.db.models import ApiKey, User
|
|
from app.db.session import get_session
|
|
from app.security.api_keys import authenticate_api_key, has_scope
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class ApiPrincipal:
|
|
api_key: ApiKey
|
|
user: User
|
|
tenant_id: str
|
|
|
|
|
|
def _extract_api_key(authorization: str | None, x_api_key: str | None) -> str | None:
|
|
if x_api_key:
|
|
return x_api_key.strip()
|
|
if authorization and authorization.lower().startswith("bearer "):
|
|
return authorization[7:].strip()
|
|
return None
|
|
|
|
|
|
def get_api_principal(
|
|
session: Session = Depends(get_session),
|
|
authorization: str | None = Header(default=None),
|
|
x_api_key: str | None = Header(default=None, alias="X-API-Key"),
|
|
) -> ApiPrincipal:
|
|
secret = _extract_api_key(authorization, x_api_key)
|
|
if not secret:
|
|
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Missing API key")
|
|
api_key = authenticate_api_key(session, secret)
|
|
if not api_key:
|
|
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid API key")
|
|
user = session.get(User, api_key.user_id)
|
|
if not user or not user.is_active:
|
|
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Inactive user")
|
|
session.commit()
|
|
return ApiPrincipal(api_key=api_key, user=user, tenant_id=api_key.tenant_id)
|
|
|
|
|
|
def require_scope(required_scope: str):
|
|
def dependency(principal: ApiPrincipal = Depends(get_api_principal)) -> ApiPrincipal:
|
|
if not has_scope(principal.api_key, required_scope):
|
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=f"Missing scope: {required_scope}")
|
|
return principal
|
|
|
|
return dependency
|