Skip to content

Qirava Cloud docs

RBAC: deny-by-default role gates

Qirava Cloud governs the fleet through a deny-by-default, ranked role system that is a completely separate authority domain from any tenant's own governance. A cloud custodian or admin operates plans, nodes, customers, caps, billing, and placement — never a tenant's data. The same access predicate is enforced in three layers: the sidebar hides what a role cannot reach (UX), each page handler re-checks and serves a real 403 on a forbidden direct navigation (defense), and the cloud.* function re-checks ctx.role one last time before it writes (the actual security boundary). Below that, the planner gates every _cp_* write. The Console is just a client; it has no privileged path.

A separate authority domain#

Cloud governance (custodian > admin > user) is distinct from every tenant's own custodian hierarchy. The cloud operators are rows in the control DMS's own _sys_users table. A cloud custodian governs the fleet — plans, nodes, customers, caps, billing, placement, migration — but never reaches into a tenant's data. The control plane only allocates resources and tracks lifecycle in its _cp_* catalogs.

Ranked roles#

Cloud roles are ranked by privilege. A capability is satisfied when the operator's role rank is greater than or equal to the capability's minimum rank. Unknown, empty, or guest roles rank 0, so they can never out-rank a real role — that is what makes the system deny-by-default.

RoleRankReaches
custodian3Everything, including Governance and the destructive Terminate action
admin2Tenants, Plans, Nodes, Billing (and Overview)
user1Overview only
guest0Overview only
(unknown / empty)0Overview only

Note a subtlety: the page/sidebar layer treats Overview as the Authed capability (min rank 0), so any signed-in operator — even guest — can see the Overview screen. The cloud.* mutation layer is stricter: its own role_rank maps only custodian/admin/user and treats everything else as 0, and its lowest mutation gate is admin. There is no mutation a user or guest can perform.

Capability::Authed
Any authenticated cloud operator (min rank 0). Used by the Overview screen.
Capability::Admin
At least admin (min rank 2). Manages tenants, plans, nodes, billing.
Capability::Custodian
custodian only (min rank 3). The top tier; destructive ops and Governance.

Three-layer enforcement#

The same predicate — screen.can_access(role), i.e. the screen's capability satisfied by the role's rank — is applied in two of the three layers by construction, and the function layer applies an equivalent require_role check. Hiding nav is convenience; the per-handler 403 and the function re-check are the enforcement.

Layer 1  SIDEBAR        sidebar() renders a screen only if screen.can_access(role)
           (UX hide)      -> a forbidden screen simply does not appear in nav

Layer 2  PAGE HANDLER   require_access(user, screen): same can_access predicate
           (403 page)     -> a forbidden DIRECT navigation returns a real 403 page

Layer 3  cloud.* FN     require_role(ctx, minimum): deny-by-default rank check
           (BOUNDARY)     -> runs server-side regardless of what the client sent
                          -> below it, the planner gates every _cp_* write (L3)
The same access decision, applied three times. The boundary is the function (and the planner under it), not the UI.

Layer 1 lives in the shell's sidebar(): it skips any screen where !screen.can_access(role). Layer 2 lives in each page handler's gated() helper, which runs require_user (session) then require_access (RBAC) and renders a 403 page — with a clear message naming the screen, the required capability, and the operator's actual role — rather than a blank screen. Layer 3 lives inside every cloud.* function as require_role, which is the real boundary.

Request

GET /cloud/tenants HTTP/1.1
Cookie: qdms_session=<token-for-a-user-role-operator>

Response

HTTP/1.1 403 Forbidden
Content-Type: text/html; charset=utf-8
Cache-Control: no-store
X-Status: 403

Output

Access denied
403 — access denied. "Tenants" requires cloud role 'admin' or higher; you are signed in as 'user'.
You do not have permission to view this screen. Ask a cloud custodian or admin if you believe this is an error.

The Capability gate in cloud.* functions#

Each cloud.* function begins with require_role(rec, minimum). It reads ctx.role (injected by the auth.session before-fn the same way the DMS funnel injects it), ranks it, and compares against the minimum for that action. A missing or unknown role ranks 0 and is denied. On failure the function returns an AccessDenied error before any write happens.

cloud.* functionMinimum roleOn deny
cloud.provisionadminAccessDenied: cloud role 'admin' or higher required
cloud.scale_verticaladminAccessDenied
cloud.scale_horizontaladminAccessDenied
cloud.switch_modeadminAccessDenied
cloud.suspendadminAccessDenied
cloud.resumeadminAccessDenied
cloud.terminatecustodianAccessDenied: cloud role 'custodian' or higher required
cloud.generate_invoiceadminAccessDenied

Request

runtime.execute("cloud.provision", { ctx: { user: "op", role: "user" }, id: "t1" })

Response

{ "code": "AccessDenied", "message": "cloud role 'admin' or higher required" }

Output

No _cp_tenants row is written. The same call from the Console redirects back to /cloud/tenants?error=cloud+role+'admin'+or+higher+required.

Why the client gate cannot be the boundary#

The Console performs a cheap client-side rank pre-check (require_min) before calling a function, purely so the UI can fail fast and show a friendly error. This is explicitly NOT the boundary: the function re-checks ctx.role regardless of what the client sent. Because the role is injected into a ctx map server-side (by auth.session) and the action handler injects it into ctx — never at the request top level — a forged top-level role field is ignored, so a client cannot spoof a higher role.

Request

cloud_record(caller=custodian, fields=[("id", "t1")])

Response

Record {
  ctx: { role: "custodian", user: "op" },   // role lives here, set server-side
  id: "t1"
}
// rec.get("role") is None — there is no top-level role to forge

Output

The cloud.* function reads ctx.role only; a top-level role in the original request body is never consulted.