Users and RBAC¶
Otherix uses fixed roles defined in code. There are no custom roles and no dynamic permission editing through the API. This guide covers the four roles, how to manage users and API tokens, and the visibility rule that governs 404 vs 403. For the complete permission table, see the RBAC matrix.
The four roles¶
A user has exactly one role, stored in users.role and carried in the JWT
role claim:
| Role | What it can do |
|---|---|
admin |
Full system access, including users, nodes, firmware, storage pools, and networks. Sees everything. |
operator |
Full VM and network management on behalf of users; node maintenance (cordon/uncordon). Not user management, node lifecycle, firmware, or storage-pool lifecycle. |
developer |
Manages own VMs and own snapshots; reads infrastructure for context. Cannot operate other users' VMs and cannot migrate. |
viewer |
Read-only across visible resources. No mutation, no console access. |
Scopes: own vs any¶
Permissions on resources that carry an owner_id (VMs, snapshots) come in two
scopes:
own- the resource'sowner_idmust equal the caller's user id.any- all matching resources, regardless of ownership.
For example a developer holds vm:read at any scope (they can list every
VM's name and metadata) but vm:lifecycle and vm:delete only at own scope
(they can start or delete only their own VMs). Resources without an owner
(networks, storage pools, firmwares, nodes) have a single meaningful scope -
the permission is simply held or not. The full per-permission table is in the
RBAC matrix.
Managing users: REST only¶
There is no otherix user CLI command
User CRUD is not exposed through the CLI. Create, list, update, and
delete users via the Control Plane REST API directly. All of these
endpoints require an admin bearer token.
The examples below use a bearer token in the Authorization header. That token
can be an admin JWT (from POST /v1/auth/login) or an admin otx_* API token;
the server selects the scheme by the otx_ prefix.
Create a user (admin)¶
POST /v1/users takes email, password, role, and an optional
display_name:
curl -sS -X POST https://cp.example.com/v1/users \
-H "Authorization: Bearer $OTHERIX_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"email": "dev@example.com",
"password": "correct-horse-battery",
"role": "developer",
"display_name": "Dev User"
}'
Passwords are 12..256 characters (no composition rules). Valid role values are
admin, operator, developer, viewer.
List, read, update, delete¶
# List users (cursor-paginated; optional ?email= exact-match filter)
curl -sS https://cp.example.com/v1/users \
-H "Authorization: Bearer $OTHERIX_TOKEN"
# Get one user by UUID
curl -sS https://cp.example.com/v1/users/<uuid> \
-H "Authorization: Bearer $OTHERIX_TOKEN"
# Update (display_name, password, or - admin only - role)
curl -sS -X PATCH https://cp.example.com/v1/users/<uuid> \
-H "Authorization: Bearer $OTHERIX_TOKEN" \
-H "Content-Type: application/json" \
-d '{"role": "operator"}'
# Soft-delete a user (refused while they still own resources)
curl -sS -X DELETE https://cp.example.com/v1/users/<uuid> \
-H "Authorization: Bearer $OTHERIX_TOKEN"
Any user can read and update themselves through GET /v1/users/me and
PATCH /v1/users/me (the latter for display_name and password; a user
cannot change their own role).
API tokens¶
API tokens are long-lived otx_* credentials, an alternative to short-lived
JWTs for CLI and automation. The plaintext is shown exactly once on creation;
the server stores only its SHA-256.
Via the CLI¶
otherix config add cluster runs a one-time bootstrap: it logs in with an
email and password, creates a long-lived API token named
otherix-cli-<cluster> via POST /v1/users/me/api-tokens, and stores the
(server, token) pair in your CLI config so subsequent commands authenticate
automatically:
otherix config add cluster \
--name prod \
--server https://cp.example.com \
--login me@example.com
# password prompted interactively, or pass --password / OTHERIX_PASSWORD
Re-run with --force to replace an existing cluster entry (it best-effort
revokes the previous token server-side first).
Via the REST API¶
POST /v1/users/me/api-tokens issues a token for the calling user. The token
field in the response is the plaintext, returned only this once:
curl -sS -X POST https://cp.example.com/v1/users/me/api-tokens \
-H "Authorization: Bearer $OTHERIX_JWT" \
-H "Content-Type: application/json" \
-d '{"name": "ci-runner", "expires_at": null}'
# => { "id": "...", "name": "ci-runner", "prefix": "otx_abcd", "token": "otx_...", ... }
List your tokens with GET /v1/users/me/api-tokens and revoke one with
DELETE /v1/users/me/api-tokens/{token_id}. Admins can manage tokens on behalf
of any user through the /v1/users/{id}/api-tokens endpoints.
The 404-not-403 visibility rule¶
Otherix never leaks the existence of a resource a caller cannot see. When a
resource exists but belongs to another user and the caller's scope is own, the
API returns 404 not_found, not 403. Returning 403 in that case would
reveal that the resource exists.
403 permission_denied is reserved for the case where the caller can see a
resource but their role lacks the capability to act on it - for example a
developer viewing their own VM but lacking vm:migrate. The error payload's
details.required_permission names the missing permission.
In short:
- Resource you should not even know about ->
404 not_found. - Resource you can see but cannot act on ->
403 permission_denied.