Skip to content

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.

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.

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 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:

  1. 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 /verify with a first code), receives 8 single-use recovery codes (shown exactly once), and logs in again.
  2. Password accepted, TOTP enrolled → the response carries a 2FA challenge ("type": "2fa-required", a challenge_id valid 5 minutes). The client submits the 6-digit code — or a recovery code — to POST /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.

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.

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.

Two independent filters must both pass:

  1. 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.
  2. 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.

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:write authority 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.

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.

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.
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.
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 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.

  • A carrier NOC: staff users owned by your partition; noc-viewer on the partition for tier-1, operator for tier-2, partition-admin for the switch owners. Give the billing system a service user with billing and an API key.
  • A reseller: your staff hold namespace-admin on your root namespace (applies_to: subtree covers every customer below). A customer admin gets namespace-admin on their namespace only. Your support desk gets helpdesk with applies_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.