The admin plane had no authentication: any caller could reach its routes once they existed. This adds the plane's ONE reusable FastAPI dependency (require_admin_jwt in api/auth.py) that verifies an Authentik OIDC JWT — Bearer extraction, RS256-only signature check against the provider's JWKS, issuer, audience, required exp, and enforced nbf — and raises the SAME 401 for every failure so the response never leaks which check failed. The JWKS is fetched over httpx once per app (JwksCache, 5-minute TTL, asyncio-lock-guarded, failed fetches never cached) and tests stub the fetcher, so the suite proves signature/expiry/audience enforcement with locally generated RSA key pairs and zero network access. ApiConfig grows the three required OIDC settings (NESTQUEST_OIDC_ISSUER/AUDIENCE/JWKS_URL) under the existing fail-fast discipline: an unconfigured issuer must not silently leave the admin plane open, so from_env raises rather than inventing a default. This is authentication only — the verified claims are the dependency's return value, so the nestquest-admins group check is the next task's addition, not a rewrite. PyJWT[crypto] joins the dev dependency group (the API deployable's runtime manifest lands in Feature 17). Maestro task: e70e6bfb-085a-4781-a3f4-276355bbbd19
64 lines
2.3 KiB
Python
64 lines
2.3 KiB
Python
"""Admin-plane routes for the NestQuest API service.
|
|
|
|
The admin plane (``/api/v1/admin/*``, D-012) serves the admin PWA.
|
|
Every route on :data:`router` requires a valid Authentik OIDC JWT
|
|
through the ONE reusable dependency :func:`api.auth.require_admin_jwt`
|
|
(declared once at the router level, so each later admin route added to
|
|
this router inherits the check).
|
|
|
|
This task is the AUTH probe only: the ping route is a thin adapter
|
|
that performs NO business logic and NO auth of its own — it reuses the
|
|
router's already-verified claims (FastAPI caches a dependency's result
|
|
per request, so the check runs exactly once) and returns the status
|
|
plus the token's subject. Authorization — membership in the
|
|
``nestquest-admins`` group — is the NEXT task, layered onto
|
|
:func:`api.auth.require_admin_jwt` so every admin route inherits it.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from typing import Annotated
|
|
|
|
from fastapi import APIRouter, Depends
|
|
from pydantic import BaseModel
|
|
|
|
from api.auth import require_admin_jwt
|
|
|
|
#: All admin-plane routes share this router; the JWT check is attached
|
|
#: HERE so every admin route (current and later) requires it.
|
|
router = APIRouter(
|
|
prefix="/api/v1/admin",
|
|
dependencies=[Depends(require_admin_jwt)],
|
|
)
|
|
|
|
|
|
class AdminPingResponse(BaseModel):
|
|
"""The probe body: the status plus the verified token's subject."""
|
|
|
|
status: str
|
|
#: The JWT's ``sub`` claim — the subject the admin PWA authenticated
|
|
#: as. ``None`` only for a verified token that carries no ``sub``.
|
|
sub: str | None
|
|
|
|
|
|
@router.get("/ping", summary="Admin plane probe")
|
|
async def admin_ping(
|
|
claims: Annotated[dict[str, object], Depends(require_admin_jwt)],
|
|
) -> AdminPingResponse:
|
|
"""Return 200 for a verified admin JWT (the plane's smoke probe).
|
|
|
|
Requires a valid Authentik JWT (checked by the router's shared
|
|
:func:`~api.auth.require_admin_jwt` dependency — the ONE token
|
|
check; this handler performs NO auth of its own). ``claims`` is
|
|
the SAME verified-claims dict that dependency produced (FastAPI's
|
|
per-request dependency cache), so the token is verified exactly
|
|
once per request.
|
|
"""
|
|
sub = claims.get("sub")
|
|
return AdminPingResponse(
|
|
status="ok",
|
|
sub=sub if isinstance(sub, str) else None,
|
|
)
|
|
|
|
|
|
__all__ = ["router"]
|