Skip to content

User Administration

This page walks through the day-to-day user-management operations. For what the roles and scopes mean, read RBAC & Authentication first. The full endpoint reference (request/response schemas, error shapes) is in the API appendix; this guide covers the operations an administrator actually performs, in the order they usually come up.

The examples assume:

  • API: $VSS is your API base URL and $TOKEN a session JWT. Errors come back as RFC 7807 problem documents (application/problem+json).
  • vsscli: a configured profile (vsscli profile create, or just log in — the profile is saved for you). Most commands default their --scope / --owner-scope to the profile’s active namespace, so the examples pass scopes explicitly where it matters. Add --output json to any command for machine-readable output.

With an API key (service accounts, automation)

Section titled “With an API key (service accounts, automation)”
Terminal window
TOKEN=$(curl -s -X POST "$VSS/v1/login" \
-H "Authorization: Bearer $VSS_API_KEY" | jq -r .token)

The JWT is valid for 1 hour; re-run the login when you receive a 401.

Password login is scoped to the partition or namespace that owns your user account, and always involves the second factor.

Terminal window
# Step 1: password → 2FA challenge
curl -s -X POST "$VSS/v1/pt/$PTID/login" \
-d '{"username":"alice","password":"…"}'
# → {"success":true,"challenge_id":"…","expires_at":"…","type":"2fa-required"}
# Step 2: TOTP code (or a recovery code) → full token
TOKEN=$(curl -s -X POST "$VSS/v1/pt/$PTID/login/2fa" \
-d '{"challenge_id":"…","code":"123456"}' | jq -r .token)

A brand-new account (no TOTP enrolled yet) instead returns "type":"bootstrap" in step 1 — a 5-minute token valid only for the /v1/me/totp/enroll endpoints. Enroll, then repeat the login. Namespace-owned users use /v1/ns/$NSID/login and /v1/ns/$NSID/login/2fa.

To check who you currently are:

Terminal window
curl -s "$VSS/v1/me" -H "Authorization: Bearer $TOKEN"

Creating a staff or subscriber account is three separate steps by design: create the record, attach a credential, add grants. A freshly created user exists but can neither log in (no credential) nor do anything (no grants).

Terminal window
curl -s -X POST "$VSS/v1/user" \
-H "Authorization: Bearer $TOKEN" \
-d '{
"username": "bob",
"email": "bob@example.net",
"display_name": "Bob Ops",
"owner_scope": {"st": "partition", "si": "'$PTID'"}
}'
# → {"success":true,"user_id":"<uuid>"}

owner_scope.st is partition or namespace, with si the scope’s UUID. Remember: the owner scope is immutable — pick the scope whose admins should own this account.

Terminal window
curl -s -X PUT "$VSS/v1/user/$USER_ID/password" \
-H "Authorization: Bearer $TOKEN" \
-d '{"new_password":"a-strong-initial-password"}'

Hand the initial password to the user out of band; their first login forces TOTP enrollment before they get a working session. For a service account, skip the password and mint an API key instead (below).

Terminal window
curl -s -X PUT "$VSS/v1/user/$USER_ID/grant" \
-H "Authorization: Bearer $TOKEN" \
-d '{
"scope_type": "namespace",
"scope_id": "'$NSID'",
"role": "operator",
"applies_to": "subtree"
}'

Optional: "expiry": "2026-10-01T00:00:00Z" (RFC 3339) for a self-revoking grant.

Remember the two-sided delegation rule: you need user-management authority over the user’s owner scope and you must hold the role (or a full-admin role) over the grant’s scope. A 401 on grant-add almost always means the second condition failed.

API keys are named per-user credentials: PUT mints (or rotates — same name replaces the key), GET lists metadata, DELETE revokes.

Terminal window
# Mint / rotate (plaintext returned exactly once)
curl -s -X PUT "$VSS/v1/user/$USER_ID/apikey/ci-deploy" \
-H "Authorization: Bearer $TOKEN" \
-d '{"label":"CI deploy key","expiry":"2027-01-01T00:00:00Z"}'
# → {"success":true,"credential_id":"ci-deploy","api_key":"vss_…"}
# List (metadata only — no secrets)
curl -s "$VSS/v1/user/$USER_ID/apikeys" -H "Authorization: Bearer $TOKEN"
# Revoke
curl -s -X DELETE "$VSS/v1/user/$USER_ID/apikey/ci-deploy" \
-H "Authorization: Bearer $TOKEN"

Users can manage their own keys without any admin grant — the same endpoints with your own user id are self-permitted. A sensible automation pattern: one service user per integration, one named key per environment, expiries set, rotation = re-PUT the same name.

Terminal window
# One user, full admin view (grants + credential metadata)
curl -s "$VSS/v1/user/$USER_ID" -H "Authorization: Bearer $TOKEN"
# All users owned by a scope (cursor-paginated, default 100/max 500)
curl -s "$VSS/v1/users?scope_type=partition&scope_id=$PTID&limit=200" \
-H "Authorization: Bearer $TOKEN"
# → {"users":[…],"next_cursor":"<user-id>"} — pass back as &after=

Note that listing is by owner scope — “everyone owned by this partition” — not by grant. To answer “who has access to this namespace?”, query grants by scope instead:

Terminal window
curl -s "$VSS/v1/grants?scope_type=namespace&scope_id=$NSID" \
-H "Authorization: Bearer $TOKEN"
# → {"holders":[{"user_id":"…","role":"operator","granted_by":"…","granted_at":"…"}]}

This is the access-review view: it includes who granted each role and when. Remember its blind spot — a user can also reach this namespace via a grant on an ancestor namespace or on the partition, so review those scopes too.

Terminal window
# What does this user hold? (includes granted_by / granted_at provenance)
curl -s "$VSS/v1/user/$USER_ID/grants" -H "Authorization: Bearer $TOKEN"
# Revoke: the same (scope, role) tuple used to create it
curl -s -X DELETE "$VSS/v1/user/$USER_ID/grant" \
-H "Authorization: Bearer $TOKEN" \
-d '{"scope_type":"namespace","scope_id":"'$NSID'","role":"operator"}'

Revocation takes effect on the user’s next login — an existing JWT carries the grants it was minted with, for up to the remaining hour of its life. To cut access immediately, disable the user (below): disabled users fail credential lookup on every new login, and you can re-enable after the token horizon has passed.

Terminal window
curl -s -X PUT "$VSS/v1/user/$USER_ID/password" \
-H "Authorization: Bearer $TOKEN" \
-d '{"new_password":"new-temporary-password"}'

No old password is needed on the admin path. Users change their own with POST /v1/me/change-password / vsscli me password change, which does require the current one.

First choice: the user logs in with one of their recovery codes in place of the TOTP code, then re-enrolls at leisure (vsscli me totp delete then enroll). When the recovery codes are gone too, an admin removes the TOTP credential — this also invalidates the remaining recovery codes, and the user’s next password login lands in the enrollment flow again:

Terminal window
curl -s -X DELETE "$VSS/v1/user/$USER_ID/totp" \
-H "Authorization: Bearer $TOKEN"
Terminal window
# Disable (reversible; blocks all new logins)
curl -s -X PUT "$VSS/v1/user/$USER_ID" \
-H "Authorization: Bearer $TOKEN" \
-d '{"disabled": true}'
# Delete (permanent; removes credentials, grants, API keys)
curl -s -X DELETE "$VSS/v1/user/$USER_ID" \
-H "Authorization: Bearer $TOKEN"

The update endpoint has patch semantics — only the fields you send change (username, email, display_name, disabled).

Offboarding pattern: disable immediately (kills new logins now), then delete after your retention window. Deleting or disabling your own account is refused with a 409 Conflict — an admin cannot lock themselves out mid-session (re-enabling yourself is allowed, should another admin have disabled you). There is, however, no last-admin-standing guard between different accounts: one admin can still delete or disable the only other admin of a partition, so keep at least two admin accounts (or a break-glass API-key service user) per partition.

The helpdesk role is built for tier-1 subscriber support: it can read users, edit profile fields, and reset passwords/credentials — but only on self-service accounts (users whose every grant is a feature role such as voicemail-owner or line-owner). Pointing it at a staff account fails with 401 even though the role nominally has user:credential:write.

Terminal window
# The subscriber account a portal would create:
vsscli user create --username sub-1001 --owner-scope ns:$NSID
vsscli user grant add <sub-user-id> --role line-owner \
--scope "resource:$NSID!line!1001"
vsscli user grant add <sub-user-id> --role voicemail-owner \
--scope "resource:$NSID!vm_box!default!1001"
# The helpdesk operator who may reset such accounts (and nothing else):
vsscli user create --username helpdesk-dana --owner-scope ns:$NSID
vsscli user grant add <dana-user-id> --role helpdesk --scope ns:$NSID

Dana can now vsscli user password set <sub-user-id> but gets a 401 attempting the same against your operator accounts.

Both “who are you” and “you may not” failures surface as 401 problem documents, so work through them in order:

  1. Token problems — expired (1-hour lifetime), or a bootstrap token being used outside TOTP enrollment. Re-login; vsscli auth status shows the expiry.
  2. Coverage — none of your grants reaches the target: wrong partition, a namespace grant used against a partition-level endpoint, an applies_to: descendants grant used on the granted node itself, or an expired grant.
  3. Policy — your role reaches the target but doesn’t allow the action (an editor calling apply, a viewer writing). The role catalog on the RBAC page lists each role’s action surface.
  4. Special rules — grant delegation (you don’t control the grant’s landing scope), helpdesk constraint (target isn’t a self-service user), or a self-mutation exclusion (disabling/deleting yourself).

The server logs every denial with the candidate roles it considered, which pinpoints whether you failed at coverage (no roles) or policy (roles listed, action refused).