Appearance
API Reference
The provisioning API is a FastAPI service (cyberrange/src/provisioning/provision_api_fastapi.py, version 1.2.0) bound to 10.115.77.1:5000. The browser never calls it directly; the portal's BFF proxies every request and injects the access token server-side. The terminal bridge calls it to check lab ownership.
Authorization tiers
Every route is classified in ROUTE_POLICY in test_sec01_rbac_matrix.py, and a test fails if a route ships unclassified. There are four tiers.
| Tier | Who passes | Enforced by |
|---|---|---|
| Public | Anyone, including unauthenticated | no auth dependency (browser-score uses a shared secret) |
| Any app role | student, instructor or admin, scoped to their own data | require_app_role on the router, plus require_owner per pod |
| Instructor | instructor or admin | require_role(["instructor","admin"]) |
| Admin | admin only | require_role(["admin"]) |
"Allowed" means the caller passed authorization; a 404, 409 or 422 after that is business logic. require_owner returns 404 (not 403) so a lab's existence is not revealed to non-owners.
Public
| Method | Path | Purpose | Notes |
|---|---|---|---|
| GET | /health | Liveness | Returns {"status":"ok"} |
| GET | /capacity | Free RAM, active pods, limits, whether a lab can be provisioned | Returns available_mb, active_pods, max_pods, pod_ram_mb, ram_buffer_mb, profile, ram_required_mb, can_provision. See F13 (public info). |
| POST | /internal/browser-score | DVWA browser scoring callback (Scenario 2 only) | Requires the X-Browser-Score-Secret header (constant-time compare); 503 if unset. Checks pod exists, ACTIVE, owner match, scenario 06. The body is validated strictly (unknown fields refused, student_id pattern, bounded IDs and label). |
| POST | /internal/browser-diagnostic | Scenario 2 near-miss reason from the DVWA proxy, stored for instructors | Same secret and pod checks. Accepts only an allowlisted reason code; returns {"recorded": false} when Scenario 2 diagnostics are off or the milestone already passed on that lab. |
Any app role (self-scoped)
Provided by pods_router.router, which carries Depends(require_app_role). Pod routes additionally check ownership.
| Method | Path | Purpose |
|---|---|---|
| POST | /pods/provision | Start a lab (identity from the token; scenario_id in the body). Returns 202. |
| GET | /pods | List the caller's labs (admins get all, with an optional student_id filter) |
| GET | /pods/{id}/status | One lab's status (owner or admin), including ttl_minutes and remaining_seconds; updates the heartbeat when ACTIVE |
| GET | /pods/{id}/guac-token | Legacy placeholder (native-xterm-ws); owner only |
| GET | /pods/{id}/lab-urls | DVWA and SIEM URLs for the lab; owner only |
| DELETE | /pods/{id}/destroy | End the caller's own lab |
| POST | /pods/{id}/verify/{scenario}/{milestone} | Manual Check; owner only, lab must be ACTIVE. Once per milestone: a definitive PASS or FAIL is recorded in manual_check_attempts; a later call answers status: "REVIEW" without running the check |
| GET | /pods/{id}/milestones | Milestone history for the lab plus manual_check_locked (milestone IDs locked to instructor review); owner only |
| GET | /pods/{id}/alerts | The lab's Wazuh alerts; owner only |
| GET | /progress | The caller's milestone history across labs |
| DELETE | /progress/{scenario} | Reset (delete) the caller's own rows and used Manual Check attempts for one scenario |
| POST | /progress/{scenario}/flag | Submit a flag. Flag-only tasks (1,5) and (6,5) pass on a correct flag alone; other milestones use hybrid flag + state scoring. Called by the portal's FlagSubmission box. |
| GET | /progress/{scenario}/rubrics | Rubric criteria for a scenario |
| GET / POST | /users/me/orientation | Read ({"completed": bool}) or record the caller's first-login orientation, keyed by the token's sub; 401 if sub is missing (onboarding_router.py, mounted with require_app_role) |
| POST | /reviews/submit | Submit a written report or manual-review request |
| GET | /reviews/{id} | Read a review (owner or staff) |
| POST | /reviews/{id}/resubmit | Resubmit a review sent back as RETRY (owner only) |
| POST / GET | /reviews/{id}/images | Upload / list evidence screenshots (owner) |
| GET / DELETE | /reviews/{id}/images/{image_id} | View (owner or staff) / delete (owner) an image |
Instructor (instructor or admin)
| Method | Path | Purpose |
|---|---|---|
| GET | /instructor/pods | All live labs with progress and ttl_minutes (sensitive fields stripped) |
| GET | /instructor/pods/{id}/alerts | Live SIEM view for a lab |
| GET | /instructor/students/{sid}/labs | A student's lab history |
| GET | /instructor/students/{sid}/labs/{pod}/alerts | Archived alerts for an ended lab (started_at required) |
| GET | /instructor/reviews/{id}/alert-snapshot | Alerts frozen with a report |
| GET | /instructor/students | Students derived from labs, milestones and reviews (not Keycloak) |
| GET | /instructor/students/{sid} | One student's detail; each milestone row includes source (auto, manual, browser, flag, review, or null for rows before v15) |
| GET | /instructor/students/{sid}/diagnostics | Check-failure reason codes for one student (optional pod_id, pod_created_at), plus enabled, enabled_scenarios and retention_days |
| GET | /instructor/reviews | Review queue (filter by status) |
| GET | /instructor/reviews/{id} | Review detail |
| POST | /instructor/reviews/{id}/resolve | Approve, reject or request retry (score 0 to 100, feedback). Approving a milestone-scoped case writes the milestone PASS; changing it to reject or retry removes that instructor-granted PASS |
| GET | /instructor/export/knowledge-gain | Telemetry export (JSON or CSV, scenario_id, status_filter). Anonymized by default; anonymize=false and include_excluded=true are admin only (403 for instructors). Admin-marked test accounts and Keycloak staff accounts are left out; the JSON summary and the X-Excluded-Accounts / X-Staff-Exclusion headers report it. Every call writes KNOWLEDGE_GAIN_EXPORTED |
Admin only
| Method | Path | Purpose |
|---|---|---|
| DELETE | /admin/pods/{id}/force-destroy | Destroy any lab (ACTIVE or FAILED_ROLLBACK_COMPLETE); audited |
| GET | /admin/infra-health | API, LXD, Keycloak and Wazuh health |
| GET / POST | /admin/users | List / create Keycloak users; each user carries research_excluded and research_exclusion_reason |
| PATCH | /admin/users/{id}/enabled | Enable or disable a user |
| PUT | /admin/users/{id}/role | Change a user's single app role |
| PUT | /admin/users/{id}/password | Reset a password (temporary by default; Keycloak's policy can answer 422) |
| DELETE | /admin/users/{id}/mfa | Reset MFA: delete the user's OTP credentials so they re-enrol; 409 on your own account; audited ADMIN_USER_MFA_RESET |
| PUT | /admin/users/{id}/research-exclusion | Body {"excluded": bool, "reason": ...} with reason one of test_account, team_member, other (required when excluding; 422 otherwise). Leaves the account out of, or puts it back into, the knowledge-gain export; allowed on any account, including your own; 503 if the database write fails; audited ADMIN_USER_RESEARCH_EXCLUDE / ADMIN_USER_RESEARCH_INCLUDE |
| GET | /admin/audit-log | Read the append-only audit log: event_type or comma-separated event_types (at most 64), student_id, result (OK and FAILED match outcome groups), before_id, limit (keyset paging, up to 500) |
User-management writes are audited with the actor, block self-targeting and removing the last admin, and revoke the target's cached tokens.
Breaking rename in pull request 167
Pod responses return ttl_minutes instead of ttl_hours. The API and portal must be deployed together.
Conventions
- Auth:
Authorization: Bearer <token>; the API validates it by Keycloak introspection every request (cache at most 60 s). Missing header is 401; introspection failure is 503. - Identity: taken from the token's
preferred_username, never from a query or body parameter, so a caller can only act as themselves. (WhenAUTH_ENABLED=false, a dev-only bypass,student_idmay come from the request.) - Validation: request bodies are Pydantic models (malformed input is 422); user-management, flag submission and the internal browser routes reject unknown fields. For
POST /admin/usersandPUT /admin/users/{id}/passwordthe 422 body is rewritten to fixed messages (Value is too short,Value has an invalid format, ...) so a rejected password is never echoed back (sanitized_validation_error()inprovision_api_fastapi.py). - Errors: JSON
{"detail": ...}; owner checks use 404, capacity uses 503 with a machine-readable code (POD_CAP_REACHED,RAM_FULL,STORAGE_FULL,ALREADY_PROVISIONED). - No CORS: the API has no CORS middleware because only the server-side BFF calls it.
Older docs undercount the API
The network appendix lists only /pods, /capacity, /health, /alerts. The real surface is the 40-plus routes above (D7). The authoritative inventory is ROUTE_POLICY in test_sec01_rbac_matrix.py.
Sources
cyberrange/src/provisioning/provision_api_fastapi.py, pods_router.py, users_router.py, onboarding_router.py, research_exclusions.py, scoring_diagnostics.py, keycloak_admin.py, hybrid_scoring.py, ttl.py, models.py, infra_health.py, audit_router.py, alerts_endpoint.py, auth.py, test_sec01_rbac_matrix.py.