Role Levels
This page defines the shared Octopus role-level policy.
Role Matrix
Role |
Level |
|---|---|
Viewer |
1 |
User |
2 |
Operator |
3 |
Engineer |
4 |
Admin |
5 |
Default for setting-level gates:
If a setting does not define
level, it is treated aslevel: 0(visible to all logged-in users).
Role naming and ROLE_PREFIX
By default, the token role names expected by the backend are:
octopus-viewer, octopus-user, octopus-operator, octopus-engineer, octopus-admin
Set the ROLE_PREFIX key in config.js (and the matching env var on the backend) to scope roles to a specific telescope or site:
|
Expected role names |
|---|---|
(empty) |
|
|
|
|
|
|
|
ROLE_PREFIX affects the default names used for Keycloak role matching. For MSAL, role resolution is done server-side — the backend ROLE_PREFIX is authoritative. The frontend prefix must match the backend so Keycloak role derivation from JWT claims stays in sync.
Selection rules when a user holds multiple roles:
Roles belonging to a different prefix are ignored — only this deployment’s prefix is matched.
When a user holds multiple roles under the same prefix (e.g.
octopus-low-operatorandoctopus-low-admin), the highest level wins.
Per-provider overrides — individual role names can be pinned via KEYCLOAK_ROLE_* keys in config.js. Explicit overrides always take precedence over ROLE_PREFIX.
Semantics
A user can access a feature/setting only when:
userRoleLevel >= requiredLevel.Role names are canonicalized case-insensitively (
admin,ADMIN->Admin).Unknown/invalid role names are treated as
Viewer.Role level must be enforced server-side for security. Frontend checks are UX only.
Security enforcement chain
All authorization decisions for mutations are made server-side from the signed Entra/Keycloak token. The frontend role state (localStorage) controls UI visibility only and is never trusted by the backend.
Per-request flow
Token arrives — every GraphQL request carries an Entra Bearer token in the
Authorizationheader.Signature verified — the backend fetches Microsoft’s public JWKS and cryptographically verifies the token. A forged or tampered token is rejected with HTTP 401 before any claims are read.
Role derived from claims — the backend reads either:
rolesclaim (Entra app roles, matched againstMSENTRA_ROLE_*env vars, e.g.octopus-stfc-operator)groupsclaim (security group Object IDs, matched againstOCTOPUS_ROLE_*_IDenv vars)
This produces a
Userobject withroleandrole_levelthat is authoritative for the lifetime of the request.Operation gate — each resolver calls
_require_role_level(user, minimum, detail). Ifuser.role_level < minimumthe request fails with HTTP 403.
Operation minimums
Operation |
Minimum level |
Minimum role |
|---|---|---|
Read own preferences / workspaces |
1 |
Viewer |
Switch current workspace |
1 |
Viewer |
Write limited own preferences ( |
1 |
Viewer |
Create / rename / save / delete own workspaces |
2 |
User |
Write unrestricted own preferences (layouts, panels, etc.) |
2 |
User |
Send commands (dynamic mutations) |
3 |
Operator |
Read or write another user’s preferences / workspaces |
5 |
Admin |
Viewers are blocked server-side from modifying any preference key outside the allowed set. Any other key returns HTTP 403 for Viewer-level requests.
Admin access to another user’s workspaces is enforced in the backend and exposed in the UI via the Impersonated Workspaces pane (Admin-only). There is no UI path for a non-Admin user to touch another user’s data.
userId matching for preference mutations
Preference and workspace mutations include a userId parameter that must match the authenticated user’s username. The backend enforces this with _same_principal(user.username, userId) (case-insensitive). A mismatch returns HTTP 403 even if the role level is sufficient.
user.username is derived from the verified token claims in priority order: preferred_username → upn → sub. Entra access tokens sometimes omit preferred_username, causing the backend to fall back to sub (an Object ID GUID). The frontend uses the useCurrentUserId() hook, which reads the username echoed back by /auth/me after login rather than reading it directly from the MSAL account object. This guarantees the userId sent in mutations always matches what _same_principal() sees, regardless of which claim the access token carries.
Backend
Source-of-truth mapping lives in
auth/roles.py.Auth session/token claims include
roleandrole_level.GraphQL authorization enforces minimum levels for sensitive operations.
Dynamic endpoint-defined GraphQL mutations require Operator (level 3) or higher. Queries are available to any authenticated user.
Preference access is user-scoped by default:
A user can read/update only their own preferences (
userIdmust match the authenticated user).If
userIdbelongs to someone else, the request is denied.Only
Admin(level 5) can read/update another user’s preferences.
Frontend
Frontend reads the role echoed by
/auth/meand caches it inlocalStoragefor UI behavior.Frontend fetches canonical role levels from backend
/auth/rolesand uses them as the primary mapping.If backend role levels are temporarily unavailable, frontend uses cached values (or built-in defaults) as fallback.
Widget settings UI hides fields where
field.level > userRoleLevel.Frontend filtering is not a security boundary. The backend independently enforces every operation regardless of what the frontend sends.
SDK
Widget config fields support optional
level?: number.JSON-schema style widget settings can define
levelper property.SDK schema conversion preserves
levelso host apps can enforce visibility.
Example
const schema = {
type: 'object',
properties: {
useLiveData: {
type: 'boolean',
title: 'Use Live Data',
level: 5,
default: true
},
title: {
type: 'string',
title: 'Title',
default: '' // implicit level 0
}
}
};