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.
| Role | Rank | Reaches |
|---|---|---|
| custodian | 3 | Everything, including Governance and the destructive Terminate action |
| admin | 2 | Tenants, Plans, Nodes, Billing (and Overview) |
| user | 1 | Overview only |
| guest | 0 | Overview only |
| (unknown / empty) | 0 | Overview 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)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: 403Output
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.* function | Minimum role | On deny |
|---|---|---|
| cloud.provision | admin | AccessDenied: cloud role 'admin' or higher required |
| cloud.scale_vertical | admin | AccessDenied |
| cloud.scale_horizontal | admin | AccessDenied |
| cloud.switch_mode | admin | AccessDenied |
| cloud.suspend | admin | AccessDenied |
| cloud.resume | admin | AccessDenied |
| cloud.terminate | custodian | AccessDenied: cloud role 'custodian' or higher required |
| cloud.generate_invoice | admin | AccessDenied |
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 forgeOutput
The cloud.* function reads ctx.role only; a top-level role in the original request body is never consulted.