RBAC & Authentication
Every management operation — the REST API, vsscli, and any portal built on
them — is performed by a user presenting a short-lived session token, and
is allowed or denied by role-based access control (RBAC). This page
explains the model; User Administration
shows the concrete commands.
The model in one paragraph: a user is owned by exactly one scope (a partition or a namespace). What the user may do is decided entirely by their grants — each grant binds one role (from a fixed catalog) to one scope, optionally narrowed or time-limited. At request time the system collects the roles from those of the user’s grants that cover the resource being touched, and a policy engine decides whether any of those roles permits the specific action. No grant, no access: everything is deny-by-default.
Authentication
Section titled “Authentication”Users and credentials
Section titled “Users and credentials”A user record carries a username and/or email, a display name, an enabled flag, and its immutable owner scope. A user may hold several credentials, of different types:
| Credential | Used for |
|---|---|
| Password | Interactive login (portal, vsscli), always combined with TOTP |
| TOTP | The second factor for password login (authenticator app) |
| Recovery codes | Single-use fallback when the authenticator is unavailable |
| API keys | Non-interactive login for service accounts and automation |
Usernames and emails are unique within an owner scope, not globally — two
namespaces can each have their own alice, and each logs in through her own
namespace’s login endpoint.
API-key login (service accounts)
Section titled “API-key login (service accounts)”POST /v1/login with the API key as a bearer token returns a session JWT:
Authorization: Bearer <api-key> → { "token": "<jwt>", "type": "full" }The JWT is then presented as Authorization: Bearer <jwt> on every
subsequent call. Tokens are valid for 1 hour (shorter if the key itself
expires sooner); clients are expected to re-login when they receive a 401.
API keys are named per-user credentials with an optional label and expiry. The plaintext key is returned exactly once, at mint time; only metadata is ever readable afterwards.
Password + TOTP login (interactive)
Section titled “Password + TOTP login (interactive)”Password login is scoped: you log in to a partition or namespace —
POST /v1/pt/{ptid}/login or POST /v1/ns/{nsid}/login — with a username
(or email) and password. Two-factor authentication is not optional; the flow
enforces enrollment on first login:
- Password accepted, no TOTP enrolled yet → the response carries a
bootstrap token (
"type": "bootstrap"), valid 5 minutes, that authorizes only the TOTP-enrollment endpoints. The client enrolls an authenticator (POST /v1/me/totp/enroll→ secret +otpauth://QR URI, then/verifywith a first code), receives 8 single-use recovery codes (shown exactly once), and logs in again. - Password accepted, TOTP enrolled → the response carries a
2FA challenge (
"type": "2fa-required", achallenge_idvalid 5 minutes). The client submits the 6-digit code — or a recovery code — toPOST /v1/pt/{ptid}/login/2fa, and receives the full JWT.
A challenge allows 3 failed attempts before it is invalidated; a recovery code is consumed on use. Failed logins are timing-equalized and return the same 401 whether the account exists or not.
vsscli login drives this whole flow interactively, including first-time
TOTP enrollment with a terminal QR code — see
User Administration.
The authorization model
Section titled “The authorization model”Owner scope: where a user lives
Section titled “Owner scope: where a user lives”Every user is owned by exactly one scope, fixed at creation:
partition:<uuid>— a customer account’s own staff,namespace:<uuid>— users belonging to one tenant namespace.
The owner scope determines which administrators manage the user (the admins whose authority covers that scope), which login endpoint finds it, and what disappears in a cascade delete (deleting a namespace or partition deletes the users it owns). It deliberately grants the user no access by itself, and it never changes — access comes only from grants, and granting a user a role somewhere else does not move the account.
Grants: role × scope
Section titled “Grants: role × scope”A grant is a (role, scope) pair with two optional modifiers:
{ "scope_type": "namespace", "scope_id": "0197f9d2-…", "role": "operator", "applies_to": "subtree", "expiry": "2026-10-01T00:00:00Z"}| Scope type | scope_id |
A grant here covers |
|---|---|---|
partition |
partition UUID | that partition and everything in it |
namespace |
namespace UUID | that namespace subtree (see applies_to) |
resource |
<nsid>!<type>!<name> |
one single resource (feature roles only) |
applies_to refines a namespace grant’s breadth over the tree:
subtree (the namespace and all descendants — the default), self (only
that namespace), or descendants (everything below, but not the namespace
itself — useful for a reseller support role that manages customers but not
the reseller’s own switch).
expiry makes a grant self-revoking: an expired grant simply stops
contributing at check time. Use it for contractors and temporary
escalations.
How a request is authorized
Section titled “How a request is authorized”Two independent filters must both pass:
- Coverage — which of the caller’s grants reach the target at all?
A partition grant covers the partition
and every namespace in it; a namespace grant covers its target per
applies_to, including targets below it in the tree (the ancestor walk); a resource grant covers only its exact resource. Grants never reach up or sideways: a namespace grant does not authorize anything at partition level, and a partition grant does nothing in a sibling partition. - Policy — do the covering roles permit this action? Every endpoint
authorizes a named action (
config:apply,user:grant:write,cdr:purge, …) against a central role→action policy. The role list that survives the coverage filter is checked; any one allowing role suffices.
If either filter comes up empty the request fails with 401 and the denial is logged with the candidate roles.
A small set of self-service actions bypass grants entirely when the
target is the caller’s own account: reading /v1/me, changing your own
password, managing your own TOTP enrollment, and managing your own API
keys. Everything else — including reading your own admin view or changing
your own profile fields — goes through the normal grant check.
Delegation and anti-escalation
Section titled “Delegation and anti-escalation”Grant management is itself governed, with a rule designed to make privilege escalation structurally impossible:
- To write a grant on a user, you need
user:grant:writeauthority over the user’s owner scope (their home admins manage their access), and - you must have authority over the grant’s own landing scope: you must hold a full-admin role there, or already hold the very role you are granting there.
So a namespace-admin can delegate editor within their namespace, an
operator can hand operator (but nothing more) to a colleague within
scope, and nobody can grant a role on a partition or namespace they do not
themselves control.
Two structural rules back this up:
- There is no all-scope admin role. The broadest grantable authority is
partition-admin, confined to one partition — no role in the catalog spans accounts, so a single leaked credential can never reach beyond the partition it was granted in. - Role placement is enforced. Each role is grantable only at its declared scope types (the catalog below); feature roles exist only at resource scope, and administrative roles can never be widened beyond it.
Role catalog
Section titled “Role catalog”Capability (what a role may do) is deliberately orthogonal to breadth
(where the grant applies): operator at partition scope is the same job as
operator at namespace scope, just wider.
Full administration
Section titled “Full administration”| Role | Grantable at | What it does |
|---|---|---|
partition-admin |
partition | Everything within the partition: all switch data and config, namespace lifecycle, users/grants, CDR purge. The role minted with a new partition. |
namespace-admin |
namespace | Everything within a namespace subtree except namespace deletion and CDR/route-log purge (those stay with the partition). |
Switch operations (no user administration)
Section titled “Switch operations (no user administration)”| Role | Grantable at | What it does |
|---|---|---|
operator |
partition, namespace | Read/write switch state and config, including config:apply (can promote a version to running). |
editor |
partition, namespace | operator minus config:apply — can stage config but cannot promote it, so cannot cause a config-driven outage. |
config-publisher |
partition, namespace | Only config:read + config:apply — the separation-of-duties partner to editor (one drafts, the other publishes). |
viewer |
partition, namespace | Read-only over switch state plus users/grants/credential metadata. |
Access administration
Section titled “Access administration”| Role | Grantable at | What it does |
|---|---|---|
user-admin |
partition, namespace | Full user lifecycle including grants — but no switch data at all. |
helpdesk |
partition, namespace | Constrained: profile edits and password/credential resets, and only on self-service users (accounts holding nothing but feature roles). Cannot touch staff accounts, cannot manage grants, cannot create or delete users. |
Specialists
Section titled “Specialists”| Role | Grantable at | What it does |
|---|---|---|
provisioner |
partition, namespace | Line-level provisioning: managed resources, child-namespace creation, device credentials (peer:setpass), voicemail admin — no config authoring. |
tenant-provisioner |
partition | Creates root namespaces in a partition (reseller onboarding automation). |
ratedeck-admin |
partition, namespace | LCR tariff tables only, including uploads and versioning. |
billing |
partition, namespace | Read-only CDR extraction. |
cdr-compliance |
partition, namespace | CDR + route-log read and purge (retention enforcement). |
noc-viewer |
partition, namespace | Read running config + route forensics for call debugging; nothing else. |
Feature (self-service) roles
Section titled “Feature (self-service) roles”Feature roles bind one end subscriber to one resource, at resource scope only — they are how a portal gives a subscriber control of their mailbox or line without any administrative reach:
| Role | Bound to | What it does |
|---|---|---|
voicemail-owner |
<nsid>!vm_box!<context>!<box> |
Read/manage messages, greetings, and PIN of that one mailbox. |
line-owner |
<nsid>!line!<line> |
Read/set the calling features (forwards, DND, speed dials) of that one line. |
Accounts holding only feature roles are what the constrained helpdesk
role is allowed to manage.
Choosing roles: three worked patterns
Section titled “Choosing roles: three worked patterns”- A carrier NOC: staff users owned by your partition;
noc-vieweron the partition for tier-1,operatorfor tier-2,partition-adminfor the switch owners. Give the billing system a service user withbillingand an API key. - A reseller: your staff hold
namespace-adminon your root namespace (applies_to: subtreecovers every customer below). A customer admin getsnamespace-adminon their namespace only. Your support desk getshelpdeskwithapplies_to: descendants— they can reset customer subscriber credentials but cannot touch your own switch or staff. - Config change control: give engineers
editor, and a release manager (or a CI service account)config-publisher. Nobody can single-handedly draft and promote a dial-plan change.