Admin routes: require_admin gates nestquest-admins group, refuses panel token

WHY: the admin plane was authenticated (any valid JWT passed) but not
authorized, and the panel service token would have been run through
JWT verification like any other credential. require_admin is now the
ONE dependency every admin route uses: it refuses the panel service
token outright with 403 BEFORE any JWT parsing (a refused credential
type, not an invalid one — a 401 challenge would imply a retry could
ever succeed), then verifies the JWT via the existing require_admin_jwt
path (uniform 401 unchanged) and requires the groups claim to name
nestquest-admins (403 for a missing/empty/non-list claim or one
without the group).
This commit is contained in:
2026-09-23 00:44:38 -04:00
parent ca1165de3f
commit 4d697dc025
3 changed files with 229 additions and 26 deletions
+18 -16
View File
@@ -2,17 +2,17 @@
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).
whose ``groups`` claim names ``nestquest-admins``, through the ONE
reusable dependency :func:`api.auth.require_admin` (declared once at
the router level, so each later admin route added to this router
inherits the check). The panel service token is refused outright on
this plane (403), never treated as a JWT.
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.
plus the token's subject.
"""
from __future__ import annotations
@@ -21,13 +21,15 @@ from typing import Annotated
from fastapi import APIRouter, Depends
from pydantic import BaseModel
from api.auth import require_admin_jwt
from api.auth import require_admin
#: All admin-plane routes share this router; the JWT check is attached
#: HERE so every admin route (current and later) requires it.
#: All admin-plane routes share this router; the ONE admin dependency
#: (service-token refusal + JWT verification + nestquest-admins group
#: gate) is attached HERE so every admin route (current and later)
#: requires it.
router = APIRouter(
prefix="/api/v1/admin",
dependencies=[Depends(require_admin_jwt)],
dependencies=[Depends(require_admin)],
)
@@ -42,14 +44,14 @@ class AdminPingResponse(BaseModel):
@router.get("/ping", summary="Admin plane probe")
async def admin_ping(
claims: Annotated[dict[str, object], Depends(require_admin_jwt)],
claims: Annotated[dict[str, object], Depends(require_admin)],
) -> AdminPingResponse:
"""Return 200 for a verified admin JWT (the plane's smoke probe).
"""Return 200 for a verified JWT naming the nestquest-admins group.
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
Requires a valid Authentik JWT with the admin group (checked by the
router's shared :func:`~api.auth.require_admin` dependency — the
ONE 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.
"""