Roles & access model

Six roles across two scopes. Every admin surface, every API endpoint, every SIP register gate consults the same role slug. This page is the canonical documentation — if you're building a permissions matrix for your tenant, start here.

Two scopes. operator roles bypass per-tenant isolation and live in the bridge's appsettings.json, not in any tenant's user store. per-tenant roles live in App_Data/<tenant>/sip-credentials/<tenant>.json and only see their own tenant. There is exactly one operator role (superuser); everything else is per-tenant.

superuser operator

Cross-tenant break-glass. The only role that can list, provision, delete or read across every tenant materialised on the machine. Credentials live in the bridge's appsettings.json (SuperuserBearer + optional HMAC secret) — NOT in any tenant's user store.

What it unlocks:

  • Cross-tenant view at /superadmin.html (the only page that enumerates all tenants).
  • Tenant provisioning: create a new tenant, bootstrap its first admin, issue an ACME/Let's Encrypt cert via the bridge /acquire-cert endpoint.
  • Bypass of per-tenant isolation on read endpoints. Every gate that says "tenant scope" allows superuser through as the exception.
  • The Stefan-IP allowlist (195.158.111.88) is a superuser-scoped safety valve; hits from that IP are never rate-limited.

Where it's checked (server): RequireSuperuserBearer(context, "<endpoint-name>") in signal.ashx, oidc.ashx, csc.ashx. The check accepts either a Bearer token whose sub matches SuperuserBearer or a request signed with the operator HMAC.

Where it's checked (client): if (p.role === 'superuser') after OIDC.init resolves. Elevates cross-tenant panels; on non-superuser sessions the panel hides entirely and the server returns 403 superuser-required.

How to assign: only the operator with root on the box can grant this role. Add / rotate the SuperuserBearer value in the bridge appsettings.json. Not creatable via any UI. Not discoverable on any customer-facing page (per house rule).

admin per-tenant

Tenant owner / operator. Full control inside one tenant's App_Data directory: users, virtual numbers, trunks, SIP ACL, appsettings, PAdES signing settings, wallet issuance toggles.

What it unlocks:

Where it's checked: the standard admin gate if(p.role !== "admin") in every admin page's AdminFetch.gate({onWrongRole}). Server-side, admin surfaces bind through the OIDC bearer verified against the per-tenant issuer.

How to assign: a tenant admin creates users via /register.html and picks admin from the dropdown. First admin of a fresh tenant is provisioned by a superuser via /superadmin.html.

poweruser per-tenant

Elevated user quotas, no admin. Added 2026-08-12. Sits between admin and user. Can consume more of the tenant's licensed quotas (calls per hour, signatures per day, wallet issuance per week) but cannot change tenant configuration.

What it unlocks:

  • All user-level surfaces (SIP softphone, EU Wallet, meetings, sign.html, csc-v2-api tester).
  • Higher per-hour / per-day rate-limit buckets on API endpoints.
  • No configuration surfaces (admin.html, tenant-settings.html, register.html all deny).

Where it's checked: rate-limit tiering in the bridge's LicenseGate.Check and per-endpoint meter budgets. Admin surfaces treat poweruser the same as user (denied).

How to assign: tenant admin picks poweruser from the role dropdown in /register.html when creating a user, or edits an existing user's role in /superadmin.html.

user per-tenant

Default role. Every newly-registered user gets this role unless the creating admin picks something else.

What it unlocks:

  • SIP soft-phone + registered desk-phone dialling.
  • Personal EU Wallet issuance + verification.
  • PDF signing (via sign.html or csc-v2-api.html Try-it-live).
  • WebRTC meetings.
  • Personal API keys via /api-keys-admin.html (own keys only).
  • OIDC sign-in as a client on any Relying Party wired to the tenant's OP.

Where it's checked: every endpoint that requires "signed-in, any role" passes on user. Rate-limit buckets use the standard tier.

siponly per-tenant

SIP phone only, no browser surfaces. Created to distinguish desk-phone-only users from full app users, so licensing counts stay honest.

What it unlocks: SIP REGISTER + INVITE from the tenant's SIP trunk. Nothing else.

What it doesn't: no browser dashboard, no wallet, no signing, no OIDC sign-in flows. Attempts to sign in via /login.html return role_not_permitted_here.

Typical use: hotel desk phones, kiosk phones, deployed hardware in the field. The tenant admin bulk-provisions these via /register.html.

guest per-tenant

Sign-in only. Can authenticate to prove identity but has no consumable quotas. Used by external users invited to a single meeting or a single document-signing session.

What it unlocks:

  • OIDC sign-in as a subject (returns id_token so an RP can verify who they are).
  • Time-boxed meeting join via personal invite URL.
  • Chat access under the anonymous-invited-only model.

What it doesn't: no SIP registration, no signing, no wallet operations, no persistent identity.

Capability matrix

Capability superuser admin poweruser user siponly guest
Cross-tenant list / provisionyes-----
Tenant configuration (admin.html, tenant-settings)yesyes----
Create / disable tenant usersyesyes----
Elevated API quotasyesyesyesstandard--
Personal EU Walletyesyesyesyes--
Sign PDFs (sign.html + CSC v2)yesyesyesyes--
SIP soft-phone + WebRTC meetingsyesyesyesyes-meeting join only
SIP REGISTER from desk phoneyesyesyesyesyes-
OIDC sign-in (proof of identity)yesyesyesyes-yes
Assignable via register.htmlno (operator only)yesyesyesyesyes

Where roles live on disk

Per-tenant roles (admin, poweruser, user, siponly, guest) are stored in the tenant's SIP credentials file:

App_Data/<tenant-domain>/sip-credentials/<tenant-slug>.json

{
  "realm": "phone.aloaha.com",
  "users": {
    "alice": { "ha1": "<md5-hex>", "role": "admin",     "disabled": false },
    "bob":   { "ha1": "<md5-hex>", "role": "poweruser", "disabled": false },
    "carol": { "ha1": "<md5-hex>", "role": "siponly",   "disabled": false }
  }
}

ha1 = MD5("username:realm:password"). Same file drives SIP REGISTER, HTTP Basic auth (csc.ashx signDocBasic), and OIDC ROPC. Change the role and the change is picked up on next auth — no service restart.

The superuser role lives in the bridge's appsettings.json under SuperuserBearer + optional SuperuserHmacSecret. Only accessible to whoever has root on the machine. Never listed in sip-credentials/*.json. Never surfaced by any customer-facing page.

Cross-references