Appearance
Admin Role & Workflow
Purpose
Admins operate the platform. They manage accounts, roles and lost authenticators in Keycloak, mark test accounts out of research data, watch service health and capacity, force-destroy stuck or abandoned labs, and read the audit log. Admins also have every instructor capability. Admins skip the first-login orientation and always land on /admin; opening /dashboard sends them back there (onboardingRedirectPath() and redirectPathForRole() in cyberrange/portal/src/lib/routeRoles.ts).
Pages and navigation
Sidebar: Dashboard, Users, Pods, System, Audit log and Instructor view (adminNavItems in cyberrange/portal/src/lib/navigation.ts). When an admin is in the instructor view, the sidebar shows a Back to Admin link (cyberrange/portal/src/components/layout/Sidebar.tsx). /admin/* requires the admin role; /instructor/* accepts admins too (cyberrange/portal/src/lib/routeRoles.ts).
| Page | What it shows and does | Backend calls |
|---|---|---|
/admin Dashboard | Service Status (API, LXD, Keycloak, Wazuh), users and pods summary, Refresh, and the Knowledge-gain export with two admin-only options: identified usernames (anonymized is the default) and including test and staff accounts (pull request 272) | GET /admin/infra-health, GET /instructor/export/knowledge-gain |
/admin/users Users | Keycloak users with username, email, name, enabled flag, role and creation time. Search (server-side), plus role (All, Student, Instructor, Admin, No role) and status (Active, Disabled) filters that work on the loaded page and show "N of M users" (pull request 239). Row actions on every row except the admin's own: Enable / Disable and the role dropdown, and a More actions menu with Reset password, Reset MFA and, for student accounts, Exclude from reports… / Include in reports (pull request 274). Excluded students carry a "Not in reports" badge with the reason. After a create or password reset, a credential handoff dialog shows the username and password for at most 2 minutes so the admin can copy them to an approved channel; nothing is emailed (pull request 198). If user management is not set up on the host, the list and the Create dialog show a hint naming manual 04, Step 9 (UserAdminSetupHint) | GET/POST /admin/users, PATCH /admin/users/{id}/enabled, PUT /admin/users/{id}/role, PUT /admin/users/{id}/password, DELETE /admin/users/{id}/mfa, PUT /admin/users/{id}/research-exclusion |
/admin/pods Pods | All live labs; force-destroy with confirmation, then polling until the lab is gone | GET /pods (admin sees all), DELETE /admin/pods/{id}/force-destroy |
/admin/pods/[id] Pod detail | One lab's details; force-destroy | GET /pods/{id}/status, DELETE /admin/pods/{id}/force-destroy |
/admin/system System | System health, pod capacity, host memory, service status, audit and telemetry information | GET /admin/infra-health |
/admin/audit Audit log | Newest-first audit events in category tabs (Admin actions, Lab activity, Scoring & reviews, All), with columns When, Event, By, Affected user, Result and Details (pull request 207), filters (event, result, affected user), Load older events and Download CSV of the filtered events, capped at the newest 5,000 (pull request 217) | GET /admin/audit-log (with event_types for a tab) |
/instructor/* | Every instructor page | instructor routes |
Admin workflow
User management in detail
All user routes call _require_admin() first and talk to Keycloak's admin REST API through a dedicated service-account client (cyberrange-user-admin by default) that holds only the view-users, query-users and manage-users realm-management roles. The portal's own client secret is deliberately not reused for this (cyberrange/src/provisioning/keycloak_admin.py, cyberrange/deploy/host/setup_user_admin_client.sh).
| Rule | Detail | Source |
|---|---|---|
| No self-registration | Accounts are only created here or by setup scripts | users_router.py docstring |
| Username format | Lowercase, ^[a-z0-9][a-z0-9_.-]{0,31}$, because the username becomes the student ID and part of container names (pod-<username>-kali) | USERNAME_PATTERN |
| Single role | role must be student, instructor or admin; setting a role replaces the previous app role | CreateUserRequest, SetRoleRequest |
| Password | 8 to 128 characters; temporary by default, so Keycloak forces a change at first login. Keycloak's realm policy (SEC-04) also rejects common passwords, the username, the email and (on reset) the last 3 passwords; a rejection comes back as 422 and leaves no half-built account (only a rejection at the password step is reported as a policy failure; later failures are audited with step=role or step=readback). The dialogs invite the admin to "Choose your own unique password or passphrase (8–128 characters)" and show an advisory strength meter with "Must" rows for length, username and email; Generate is optional and makes a 24-character digits-and-symbols password with Web Crypto. Password fields use autocomplete="new-password" and the name and email fields autocomplete="off", so the browser does not fill in the admin's own details (pull request 265). For these two routes the API's 422 body carries only fixed messages, never the rejected value | CreateUserRequest, SetPasswordRequest, CreateUserModal.tsx, ResetPasswordModal.tsx, PasswordStrengthMeter.tsx, lib/generatePassword.ts, sanitized_validation_error() in provision_api_fastapi.py |
| Reset MFA | Deletes only the user's otp credentials (never the password), ends their sessions and evicts cached tokens; the user re-enrols an authenticator at next sign-in. Idempotent (removed=0 when there was none); 409 on the admin's own account | remove_otp_credentials() in keycloak_admin.py, users_router.py |
| No hidden fields | Request models forbid extra fields, so raw Keycloak attributes cannot be smuggled in | extra="forbid" |
| Self-protection | An admin cannot disable, re-role, reset the password of or reset MFA on their own account (research exclusion is allowed on any account) | _reject_self() |
| Research exclusion | PUT /admin/users/{id}/research-exclusion with excluded and, when excluding, a reason (test_account, team_member or other). It writes a research_exclusions row keyed by username; no scores are deleted, and the account can be included again. Staff accounts are left out of the export automatically, so the page offers this only on student accounts (pull requests 273 and 301) | users_router.py, research_exclusions.py, schema/v16.sql |
| Last-admin protection | The last remaining admin cannot be demoted or disabled | _reject_last_admin() |
| Actor re-check | The acting admin's admin role is re-confirmed in Keycloak before a change | _confirm_actor_still_admin() |
| Immediate revocation | Disabling a user or changing a role clears that user's cached API tokens | _revoke_cached_tokens() → introspect_cache.invalidate_user() |
| Audit | ADMIN_USER_CREATE, ADMIN_USER_ENABLE, ADMIN_USER_DISABLE, ADMIN_USER_ROLE_SET, ADMIN_USER_PASSWORD_RESET, ADMIN_USER_MFA_RESET, ADMIN_USER_RESEARCH_EXCLUDE, ADMIN_USER_RESEARCH_INCLUDE, each with the actor and result (OK, DENIED, FAILED) | _audit() |
| Setup check | At startup the API logs whether the user-admin client is configured; if not, it names setup_user_admin_client.sh and manual 04, Step 9 (warn-only; student flows are unaffected) | log_configuration_status() in keycloak_admin.py |
Lab and infrastructure management
- See all labs:
GET /podsreturns every live lab for an admin (others see only their own). - Force-destroy:
DELETE /admin/pods/{id}/force-destroyworks on labs that areACTIVEorFAILED_ROLLBACK_COMPLETE. The admin role check runs before anything is read, so a refused attempt leaves no trace; a permitted attempt writesADMIN_POD_FORCE_DESTROYwith the actor and the lab's previous status, then runs the normal teardown in the background (cyberrange/src/provisioning/pods_router.py). - System health:
GET /admin/infra-healthprobes the API, LXD (including storage pool free space), Keycloak (an introspection probe) and Wazuh (the manager agent's status), each with a 2-second timeout, and reports Healthy, Degraded or Unavailable (cyberrange/src/provisioning/infra_health.py). - Audit log:
GET /admin/audit-logis read-only (no route edits or deletes rows), filterable by one event type or a comma-separatedevent_typesset (at most 64), student and result, paged by ID (up to 500 per page). The OK and Failed result filters cover outcomes rather than one exact value: OK matchesOKandPASS, Failed matchesFAILED,FAILandERROR, and both also match events whose result is empty but whose name ends in_OK/_SUCCESSor_FAIL/_FAILED(cyberrange/src/provisioning/audit_router.py). The CSV download pages through the same route in the browser (cyberrange/portal/src/lib/auditCsv.ts). - Research export: from the Admin dashboard an admin can download the knowledge-gain export with real usernames (
anonymize=false) and with excluded test and staff accounts (include_excluded=true); instructors get 403 for both. Every export, allowed or denied, writesKNOWLEDGE_GAIN_EXPORTEDwith the actor, format, scope and counts, never student IDs (export_knowledge_gain()inpods_router.py).
Capability that exists only in the backend
Admin terminal access to any lab
The terminal bridge authorizes a WebSocket by calling GET /pods/{id}/status with the caller's token. That endpoint uses require_owner_or_admin, so an admin's token is accepted for any student's lab, including a shell on kali, meta or dvwa. No admin page links to this, but anyone holding an admin token could open it by building the WebSocket URL. The team should decide whether this is an intended support feature (and document it) or restrict the bridge to owners (cyberrange/bridge-src/bridge.py, cyberrange/src/provisioning/auth.py).
Allowed and restricted routes
| Allowed | Restricted |
|---|---|
All admin and instructor pages, and /scenarios and /scenario/[id] for their own lab | /dashboard and /orientation redirect to /admin (not a restriction, a landing rule) |
| All API routes: admin, instructor, self-scoped | Acting on their own account through user management (refused by design) |
| Status of any lab; force-destroy any lab | Destroying another user's lab through the student destroy route (owner-only; use force-destroy) |
Data an admin can view or modify
| Data | View | Modify |
|---|---|---|
| Keycloak users and roles | Yes | Create, enable/disable, change role, reset password, reset MFA |
| Research exclusions (test accounts left out of the export) | Yes (badge on the Users page) | Exclude or include again |
| Knowledge-gain export | Yes, anonymized or identified | No |
| All labs | Yes | Force-destroy |
| System health, capacity, RAM | Yes | No (configuration is in files on the host) |
| Audit log | Yes | No (read-only by design) |
| Everything an instructor sees | Yes | Resolve reviews |
Not implemented for admins
- Deleting user accounts (disable instead).
- Changing platform settings such as
MAX_PODS, the per-scenario time limits (SCENARIO_TTL_MINUTESis in code) or profiles from the UI. - Changing the realm's MFA, password policy or lockout settings from the UI (host scripts only).
- Managing scenarios, golden images, Wazuh rules or Keycloak clients from the UI.
- Viewing or ending users' Keycloak sessions from the UI.
Sources
cyberrange/portal/src/app/admin/**, cyberrange/portal/src/components/admin/*, cyberrange/portal/src/lib/navigation.ts, lib/adminUserFilter.ts, lib/auditEvents.ts, lib/auditCsv.ts, components/instructor/KnowledgeGainExport.tsx, cyberrange/src/provisioning/users_router.py, research_exclusions.py, keycloak_admin.py, pods_router.py, infra_health.py, audit_router.py, auth.py, cyberrange/bridge-src/bridge.py.