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).
66 lines
2.3 KiB
Python
66 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
|
|
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.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from typing import Annotated
|
|
|
|
from fastapi import APIRouter, Depends
|
|
from pydantic import BaseModel
|
|
|
|
from api.auth import require_admin
|
|
|
|
#: 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)],
|
|
)
|
|
|
|
|
|
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)],
|
|
) -> AdminPingResponse:
|
|
"""Return 200 for a verified JWT naming the nestquest-admins group.
|
|
|
|
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.
|
|
"""
|
|
sub = claims.get("sub")
|
|
return AdminPingResponse(
|
|
status="ok",
|
|
sub=sub if isinstance(sub, str) else None,
|
|
)
|
|
|
|
|
|
__all__ = ["router"]
|