Appearance
User Roles & RBAC Overview
The platform has exactly three application roles: Student, Instructor and Admin. This page explains where roles come from, how they are checked, and what happens when someone tries something they are not allowed to do. The role pages (Student, Instructor, Admin) and the comparison matrix describe what each role can actually do, traced from the code rather than inferred from the role names.
Authentication vs authorization
| Authentication | Authorization | |
|---|---|---|
| Question it answers | Who are you? | What are you allowed to do? |
| Handled by | Keycloak (username and password), NextAuth in the portal | Portal middleware and pages (user experience), FastAPI guards (the real boundary) |
| Result | A signed access token and a portal session cookie | Allow, redirect, or an HTTP error (401, 403, 404) |
| Code | cyberrange/portal/src/lib/auth.ts, cyberrange/src/provisioning/auth.py (verify_token) | cyberrange/portal/src/middleware.ts, lib/routeRoles.ts, cyberrange/src/provisioning/auth.py (require_app_role, require_role, require_owner, require_owner_or_admin) |
Plain language
Authentication is the security guard checking your ID at the door. Authorization is which rooms your badge opens. This platform checks the badge twice: the portal will not show you a room you cannot enter, and the API refuses the request anyway if you try to reach it another way.
The three roles at a glance
| Role | Who it is for | Lands on | Summary of powers |
|---|---|---|---|
| Student | BSIT Network & Cybersecurity learners | /dashboard | Run one lab at a time, use the terminal, get scored, submit reports and screenshots, see only their own data |
| Instructor | Faculty supervising labs | /instructor | Everything a student can do for themselves, plus read-only oversight of every live lab, every student's progress, SIEM alerts and check-failure reasons, grading of reviews, and the anonymized research export |
| Admin | Platform operators | /admin | Everything an instructor can do, plus account and role management in Keycloak, marking test accounts out of research data, the identified research export, force-destroying any lab, system health and the audit log |
Source for landing pages: landingPathForRole() in cyberrange/portal/src/lib/routeRoles.ts. The landing page is also enforced afterwards: an instructor or admin who opens /dashboard is sent to /instructor or /admin (redirectPathForRole(), pull request 176). On first sign-in, students and instructors see a one-time orientation at /orientation with steps for their role; admins skip it (pull requests 184, 188 and 311, cyberrange/portal/src/app/orientation/page.tsx).
Where roles come from
- Roles are Keycloak realm roles named exactly
student,instructorandadminin the realmcyber-range(cyberrange/deploy/host/create_demo_accounts.sh,cyberrange/docs/AUTH-03-role-contract.md). - Each account is meant to hold exactly one application role. The demo accounts
student_demo,instructor_demoandadmin_demoare created with one role each, and the Admin Users page assigns a single role (roleis one of the three values inCreateUserRequestandSetRoleRequest,cyberrange/src/provisioning/users_router.py). - Every token also contains Keycloak's own roles (
offline_access,uma_authorization,default-roles-cyber-range). These are ignored by every check. - There is no self-registration. Accounts are created by an admin or by the setup scripts (
users_router.pydocstring).
How roles are read during authentication
| Where | How roles are read | Code |
|---|---|---|
| Portal | After sign-in, NextAuth stores Keycloak's access token in the encrypted session. decodeRoles() reads realm_access.roles from the token payload (the token was already validated during the code exchange). Roles are re-read on every token refresh, so a role change appears at the next refresh. | cyberrange/portal/src/lib/auth.ts |
| API | Every request's bearer token is sent to Keycloak's introspection endpoint. Roles are the union of realm_access.roles and resource_access.portal.roles. Results are cached for at most 60 seconds, and the cache is cleared for a user when an admin disables them or changes their role. | cyberrange/src/provisioning/auth.py, introspect_cache.py, users_router.py |
| Terminal bridge | Same introspection (shared cache module), then an ownership check through the API | cyberrange/bridge-src/bridge.py |
RBAC architecture
Authentication to authorization, end to end
What happens on an unauthorized attempt
| Situation | Where it is stopped | Result the user sees | Code |
|---|---|---|---|
| Not signed in, opens a protected page | Edge middleware | Redirect to /login?callbackUrl=... | cyberrange/portal/src/middleware.ts |
| Signed in but no application role | Edge middleware and login routing | Redirect to /no-role | middleware.ts, lib/routeRoles.ts |
Student opens /instructor or /admin | Edge middleware (and AuthGate on the client) | Redirect to /dashboard | lib/routeRoles.ts |
Instructor opens /admin, or an instructor or admin opens /dashboard | Edge middleware (and AuthGate) | Redirect to their own landing page (/instructor or /admin) | redirectPathForRole() in lib/routeRoles.ts |
Instructor asks for an identified knowledge-gain export (anonymize=false) or for excluded accounts | export_knowledge_gain() | 403, audited as KNOWLEDGE_GAIN_EXPORTED DENIED | pods_router.py |
| BFF call without a session | proxyToApi() | 401 Unauthorized | cyberrange/portal/src/lib/apiProxy.ts |
| Missing or malformed bearer token at the API | verify_token() | 401 | cyberrange/src/provisioning/auth.py |
| Expired or revoked token | verify_token() | 401 Invalid or expired token | auth.py |
| Keycloak unreachable | verify_token() | 503 Auth service unavailable | auth.py |
| Authenticated, no application role, calls the API directly | require_app_role on the router | 403 | auth.py, pods_router.py |
| Student calls an instructor or admin endpoint | require_role() | 403 Forbidden: Insufficient privileges | auth.py |
| Someone touches another user's lab or review | require_owner() / owner-or-staff checks | 404 (existence is hidden) | auth.py, pods_router.py |
| Admin tries to disable, re-role or reset their own account | _reject_self() | Request refused and audited as DENIED | users_router.py |
| Non-admin attempts a force-destroy | require_role(["admin"]) runs first | 403, and nothing is written to the audit log | pods_router.py |
| Terminal WebSocket with a bad token or someone else's lab | bridge.py | Error message in the terminal, connection closed with code 1008 | cyberrange/bridge-src/bridge.py |
Multi-role and no-role policy
- No application role: treated as unauthorized everywhere (portal redirect to
/no-role, API 403). - More than one application role: not expected, and not prevented by the type system. The portal lands the user on the highest role (admin, then instructor, then student). The API grants the union of all roles.
Source: cyberrange/docs/AUTH-03-role-contract.md, landingPathForRole() in cyberrange/portal/src/lib/routeRoles.ts.
Planned / Not Yet Implemented
| Capability | Status | Evidence |
|---|---|---|
| Instructor or admin editing scenarios, milestones or points in the UI | Not implemented | Scenario catalog is static in cyberrange/portal/src/hooks/useScenarios.ts; no API route writes scenarios |
| Deleting user accounts | Not implemented | users_router.py has create, enable/disable, role, password, MFA reset and research-exclusion routes only |
/settings, /profile, /progress pages | Not implemented | Listed in the middleware matcher, but no pages exist |
| Flag submission for hybrid (non-final) milestones | API only | FlagSubmission is shown only on the flag-only final tasks (Scenario 1 and 2 Task 5); other milestones' flags can only be submitted through the API |
| Self-service MFA recovery | Not implemented | A user who loses their authenticator needs an Admin Reset MFA; Keycloak recovery codes are a preview feature and there is no SMTP for email reset |
| Admin terminal into a student lab from the UI | Backend only | The bridge accepts admin tokens for any lab (see Admin), but no admin page offers it |