Skip to content

Authorization Enforcement ​

Authorization is enforced in two independent layers. The portal layer is for user experience (do not show or render what you cannot use); the API layer is the security boundary (refuse the request no matter how it arrives).

Role and authorization flow ​

Layer 1: the portal ​

MechanismWhat it doesSource
Edge middlewareRedirects on missing session, missing role, or wrong role for the pathcyberrange/portal/src/middleware.ts
Shared rule table/admin requires admin; /instructor requires instructor or admin; segment-aware matching so /instructor-notes would not inherit /instructor rules. One function, redirectPathForRole(), makes the decision for both middleware and AuthGate: no app role goes to /no-role; a wrong role for the path, or an instructor or admin opening /dashboard, goes to the role's landing page (pull request 176)cyberrange/portal/src/lib/routeRoles.ts
AuthGateClient-side mirror of the same rules while rendering; keeps the query string in callbackUrl and uses router.replace for role redirectscyberrange/portal/src/components/auth/AuthGate.tsx
OnboardingGateWraps every page; sends a student or instructor who has not completed orientation to /orientation?returnTo=..., and blocks the content (with a Retry) if the check fails. Admins are exempt. User experience only: the API does not require orientationcyberrange/portal/src/components/auth/OnboardingGate.tsx, onboardingRedirectPath() in lib/routeRoles.ts
Role navigationSidebar shows only the role's linkscyberrange/portal/src/lib/navigation.ts
BFF session checkRoute handlers refuse requests without a server-side sessioncyberrange/portal/src/lib/apiProxy.ts

Known limitation (documented in the code)

The middleware reads roles from the session cookie. A role removed in Keycloak is still honored by the portal until the next token refresh. The API does not have this delay beyond its 60-second cache, and the Admin page clears that cache immediately.

Layer 2: the API ​

GuardApplies toResult on failureSource
verify_token()every protected route401 (or 503 if Keycloak is down)cyberrange/src/provisioning/auth.py
require_app_rolewhole pods_router, alerts_router and onboarding_router (router-level dependency), so a new route cannot forget it403auth.py, pods_router.py, onboarding_router.py, provision_api_fastapi.py
require_role(["instructor", "admin"])instructor routes (including scoring diagnostics), staff alert routes403pods_router.py, alerts_endpoint.py
require_role(["admin"])user management (including research exclusion), infra health, audit log, force-destroy403users_router.py, infra_health.py, audit_router.py, pods_router.py
Admin-only options on a staff routeGET /instructor/export/knowledge-gain with anonymize=false or include_excluded=true; the refusal is audited as KNOWLEDGE_GAIN_EXPORTED DENIED (pull request 272)403export_knowledge_gain() in pods_router.py
require_owner(pod)destroy, lab URLs, Manual Check, milestones, student alerts404auth.py
require_owner_or_admin(pod)lab status (also used by the terminal bridge)404auth.py
Owner-or-staff (reviews, images)read a review or its screenshots404pods_router.py (_review_row_for_access)
Identity from tokenprovision, progress, reset, flag, submit reviewuser can only act as themselvescaller_identity() in auth.py
Subject from tokenorientation state (/users/me/orientation), keyed by the token's sub, not a request parameter401 if sub is missingonboarding_router.py
Shared secretPOST /internal/browser-score, POST /internal/browser-diagnostic401 / 503 when unsetpods_router.py

Why 404 instead of 403 for ownership ​

Returning 404 when a user touches someone else's lab or review means the API never confirms that a resource with that ID exists. This prevents enumerating other students' labs by trying IDs 1 to 6.

Proving every route is classified ​

cyberrange/src/provisioning/test_sec01_rbac_matrix.py declares every route in a ROUTE_POLICY table as public, any application role, instructor, or admin, then checks that unauthenticated calls get 401, role-less calls get 403 and each role gets exactly the expected access. If a route is added without being classified, the test fails. A companion test, test_sec02_instructor_admin_denial.py, checks that denied admin actions leave the audit log unchanged.

Where roles are evaluated ​

Role sourcePortalAPI
realm_access.rolesYes (decoded from the access token)Yes (from introspection)
resource_access.portal.roles (client roles)NoYes (union with realm roles)
Keycloak's own roles (offline_access, uma_authorization, default-roles-cyber-range)IgnoredIgnored

Both sides check membership (includes("admin")), never array equality, because every token also carries Keycloak's own roles (cyberrange/docs/AUTH-03-role-contract.md).

Revocation timing ​

ChangePortal pagesAPI
Admin disables a user in the Admin pageUntil the next refresh (the refresh then fails and the user must sign in)Immediately (cache cleared for that user)
Admin changes a role in the Admin pageAt the next token refreshImmediately
Change made directly in the Keycloak consoleAt the next token refreshWithin 60 seconds (cache expiry)

Bypass switch ​

AUTH_ENABLED=false makes verify_token() return empty claims, caller_identity() trust the client-supplied student ID, and every role check pass. The API logs a CRITICAL warning at startup saying this is a full authorization bypass, intended only for local bootstrap with the API bound to a non-routable address (validate_auth_config() in auth.py). The bridge has the same switch with its own warning.

Sources

cyberrange/portal/src/middleware.ts, lib/routeRoles.ts, components/auth/AuthGate.tsx, components/auth/OnboardingGate.tsx, lib/apiProxy.ts, cyberrange/src/provisioning/auth.py, pods_router.py, onboarding_router.py, alerts_endpoint.py, users_router.py, infra_health.py, audit_router.py, test_sec01_rbac_matrix.py, test_sec02_instructor_admin_denial.py.

Cyber Range capstone documentation (Capstone 2 · Group 2 · A4102). Documents main at commit 6971373.