Users, roles and permissions
Authorization in Northplane is role-based: a user holds role names, a role holds permissions (and may include other roles), and every API route checks one permission string. API tokens carry permissions directly as scopes and/or through roles. This page is the reference; the conceptual overview lives in Tenancy and RBAC, and how credentials are obtained in Authentication.
The user record
Section titled “The user record”| Field | Meaning |
|---|---|
id |
UUIDv7 |
name, email |
Display name and login e-mail. E-mail is unique across the whole instance, not per tenant. |
subject |
Identity-provider subject: issuer|sub for OIDC users, ldap|<dn or id> for directory users; empty for local users |
tenantId |
Home tenant (empty = Default). A local login lands in this tenant; OIDC and LDAP users always get the Default tenant. |
local |
true = password account (created by /setup, /register, POST /users or default-admin seeding); false = OIDC (just-in-time provisioned) or LDAP-synced |
roles |
Role names. Authoritative for local and LDAP users (LDAP sync writes them); OIDC users get their roles recomputed from IdP groups at every login and usually have an empty list here. |
disabled |
A disabled user cannot log in, and existing sessions are rejected on the next request |
lastSeenAt |
Stamped at most once per minute while the user is active |
version, createdAt, updatedAt |
passHash (argon2id) is never returned by the API.
Managing users in the UI
Section titled “Managing users in the UI”Admin → Users (Benutzer) lists every account with Name (plus an LDAP/OIDC badge for non-local accounts), E-Mail, Rollen, Status and “zuletzt gesehen”. Benutzer anlegen opens a dialog with Name, E-Mail, Passwort (≥ 12 characters), Rollen (with suggestions from the roles list) and a Deaktiviert switch. Each row offers Passwort setzen (local users only), Bearbeiten and Löschen. Two cards at the bottom handle the LDAP directory sync (when configured) and Mein Passwort ändern for the signed-in user.
User endpoints
Section titled “User endpoints”All routes need admin:users unless stated otherwise. New users are created in the caller’s active tenant (the X-Northplane-Tenant header is honoured for admin:tenants holders — this is how a central admin provisions a customer login).
| Endpoint | Behaviour |
|---|---|
GET /api/v1/users |
Lists the users of the effective tenant (home tenant; cross-tenant admins select one via X-Northplane-Tenant), ordered by name |
GET /api/v1/users/{id} |
One user |
POST /api/v1/users |
{name, email, password?, roles?, disabled?} → 201 User. password is optional (≥ 12 characters when given); without it the account can only log in via OIDC until an admin sets one. Duplicate e-mail → 409 np:users/email-in-use. Audit user.create. |
PUT /api/v1/users/{id} |
Partial update {name?, email?, roles?, disabled?} — absent fields stay unchanged. Audit user.update with before/after. |
POST /api/v1/users/{id}:set-password |
{password} (≥ 12); an empty password clears it (OIDC-only account). Audit user.set-password (no values logged). |
POST /api/v1/users/me:change-password |
No permission, but only for a session principal (tokens get 401). {oldPassword, newPassword} → 204; wrong current password → 403 np:auth/bad-password. Audit user.change-password. |
DELETE /api/v1/users/{id} |
204. Audit user.delete. |
curl -s -X POST https://monitoring.example.net/api/v1/users \ -H "Authorization: Bearer np_<48 hex>" -H "Content-Type: application/json" \ -d '{"name":"Jane Doe","email":"[email protected]","password":"<at least 12 characters>","roles":["operator"]}'Role names in roles are not validated against existing roles: an unknown name simply contributes no permissions.
User preferences
Section titled “User preferences”Each actor has one preferences document: {refreshIntervalMs?: int, extra?: map[string]string}. refreshIntervalMs is 0 for “off” or 1000–86400000 (otherwise 422). GET/PUT /api/v1/users/{id}/preferences — {id} may be me or your own actor id without any permission; another id requires admin:users. PUT replaces the whole document (audit preferences.update). The UI uses it for the refresh presets (5 s / 10 s / 30 s / 60 s / off, default 30 s).
Two things that look like preferences but are not: the UI language follows navigator.language (German for de*, otherwise English) and is not stored; the colour theme and mode are instance-wide branding, not per user — see Branding and themes.
Built-in roles
Section titled “Built-in roles”Four system roles (system: true) are seeded into the Default tenant by migration and into every new tenant on creation:
| Role | Permissions |
|---|---|
admin |
*:* |
operator |
objects:read, objects:write, checks:run, alerts:read, alerts:ack, alerts:write, incidents:read, incidents:write, downtimes:write, silences:write, events:read, metrics:read, oncall:read, oncall:write, dashboards:read, dashboards:write, reports:read, reports:render |
viewer |
objects:read, alerts:read, incidents:read, events:read, metrics:read, oncall:read, dashboards:read, reports:read |
ai-agent |
objects:read, alerts:read, alerts:ack, incidents:read, incidents:write, events:read, metrics:read, oncall:read, checks:run, downtimes:write, silences:write, config:propose, reports:render |
Consequences worth knowing:
operatorandviewerhold noadmin:*permission — they cannot list roles, users, tokens, secrets, the audit log or tenants.operatorholds noconfig:write: an operator manages hosts and services but cannot edit templates, check commands, alert rules, channels, event sources, dashboards or reports. Among the built-ins onlyadmincan.- A “tenant-admin” style role (everything except
admin:tenants) is a custom role; see the example in Tenants and sites. - At boot the server reconciles the system role
operatorto includealerts:writein every tenant (only roles withsystem: trueare touched).
Custom roles
Section titled “Custom roles”A role is a tenant-scoped document (kind: role) at /api/v1/roles:
{ "name": "noc-l1", "permissions": ["objects:read", "alerts:read", "alerts:ack", "admin:read"], "includes": ["viewer"], "idpGroups": ["np-noc-l1", "cn=noc-l1,ou=groups,dc=example,dc=net"], "scope": { "tenantId": "", "folder": "/", "selector": "" }, "system": false}| Field | Meaning |
|---|---|
name |
Unique per tenant; used in user role lists, token roles and includes |
permissions |
Permission strings (see below) |
includes |
Names of roles whose permissions are added — expanded recursively, depth ≤ 8, cycle-safe. Unknown names are ignored. |
idpGroups |
Group identifiers from the IdP or directory that map onto this role at OIDC login or LDAP sync. OIDC matching is an exact string compare; LDAP matching is lower-cased and accepts the full group DN or its first RDN value (cn). Only roles in the Default tenant are consulted for mapping. |
scope.tenantId, scope.folder, scope.selector |
Stored and editable, not enforced (see below) |
system |
Built-in marker; the UI hides edit/delete for system roles |
Endpoints: GET /api/v1/roles (?q=&cursor=&limit=, default 500) and GET /api/v1/roles/{name} need admin:read; POST /api/v1/roles, PUT /api/v1/roles/{name} (with If-Match) and DELETE /api/v1/roles/{name} need admin:write. Mutations are audited as role.create|update|delete. Role is also a config bundle kind for apply, but bundle export skips roles.
Admin → Roles (Rollen) shows Name (with a “System” badge), Berechtigungen, Erbt von (includes) and IdP-Gruppen; the dialog edits Name, the permission list, Inherits, IdP groups and the scope fields.
System roles are immutable through the API: PUT/DELETE /api/v1/roles/{name} on a system: true role returns 403 np:rbac/system-role (the seed/reconcile paths write through the store directly). To vary a built-in, create a custom role — optionally with includes — instead.
Permission model
Section titled “Permission model”A permission is a string resource:action. A held permission implies a wanted one when:
- they are equal, or the held one is
*:*or*; - otherwise both contain a colon and the resource part matches (
*or equal) and the action part matches (*or equal).
| Held | Wanted | Result |
|---|---|---|
*:* or * |
anything | allowed |
admin:* |
admin:users |
allowed |
*:read |
objects:read |
allowed |
objects:read |
objects:write |
denied |
objects (no colon) |
objects:read |
denied — a malformed permission only matches itself literally |
The same logic is ported to the UI (web/src/permissions.ts) for hiding controls; the server decides.
How permissions are resolved per request
Section titled “How permissions are resolved per request”- Session principal: the role names stored in the session at login are expanded (including
includes) in the session’s tenant on every request. Editing a role’s permissions therefore applies immediately; changing a user’s role list applies at the next login. - Token principal:
scopes∪ permissions of the token’sroles, roles resolved in the token’s tenant. - The
X-Northplane-Tenantheader changes which tenant a request acts on (foradmin:tenantsholders) but never which permissions the principal has.
Permission reference
Section titled “Permission reference”Every permission string that any route or AI/MCP tool checks:
| Permission | What it allows |
|---|---|
objects:read |
Read hosts, services, objects, problems, overview, effective config, the builtin check list; list downtimes, silences, heartbeats, discovery scans; business-service tree/impact/SLA; report archive; bundle plan and export; sites overview; agent check pull (GET /api/v1/agent/checks); read of all config-document kinds (templates, check commands, time periods, alert rules, alert groups, escalation policies, channels, event sources, business services, dashboards, reports, saved filters, webhooks, IVR menus, sites) |
objects:write |
Create/update/delete hosts and services, POST /objects:batch, passive results POST /results, heartbeat beats |
config:write |
Create/update/delete all config-document kinds listed above, heartbeat definitions, bundle apply, branding, discovery scan start, channel test-notification, report :run, approve AI actions |
checks:run |
check-now, POST /check-commands:test |
alerts:read |
List/get alerts, dead letters, AI action queue, alert-rule tests and escalation-policy simulation |
alerts:ack |
Ack/resolve/snooze alerts, dead-letter replay, deny AI actions |
alerts:write |
Raise alerts manually |
incidents:read |
List/get incidents |
incidents:write |
Create/update/resolve/merge/summarize incidents |
downtimes:write |
Create/cancel downtimes |
silences:write |
Create/expire silences |
events:read |
Event search/export, SSE stream, and all AI chat endpoints (/ai/conversations, /ai/chats, /ai/connections, /ai/tools, /ai/providers, /ai/chat) |
metrics:read |
Metrics query, object metric series |
oncall:read |
On-call now/timeline/ICS/overrides/stats; GET of schedules, contacts, contact groups |
oncall:write |
POST/PUT/DELETE of schedules, contacts, contact groups; schedule overrides |
reports:render |
POST /reports/{name}:render |
admin:read |
List/get roles |
admin:write |
Create/update/delete roles |
admin:users |
Users CRUD, set-password, directory status/sync, other users’ preferences |
admin:tokens |
API tokens create/list/revoke/rotate |
admin:secrets |
Secrets put/list/delete |
admin:audit |
Audit search/export/verify, contact GDPR data export |
admin:tenants |
List/create tenants and the right to act on another tenant via X-Northplane-Tenant |
admin:ai |
AI tool policy get/put |
sites:connect |
Federation edge heartbeat and bundle pull |
dashboards:read, dashboards:write, reports:read |
Granted by built-in roles but checked by no route — dashboard and report CRUD use objects:read / config:write |
config:propose |
In the built-in ai-agent role; checked by no route and no AI tool (the propose_config_change tool requires config:write) |
maintenance:write |
Appears in the MCP tab’s “Read + operate” scope preset; checked by no route (downtimes and silences use downtimes:write / silences:write) |
AI and MCP tools check the same permission names with the same wildcard logic — see Agent chat and MCP server.
Route-to-permission table
Section titled “Route-to-permission table”Every API operation publishes its permission as x-required-permission in /api/openapi.json, and the generated REST API reference shows it per operation. — means no permission check (the handler may still require a login).
| Method and path | Permission |
|---|---|
GET /api/v1/whoami |
— (401 if anonymous) |
GET /api/v1/tenants, POST /api/v1/tenants |
admin:tenants |
GET /api/v1/roles, GET /api/v1/roles/{name} |
admin:read |
POST /api/v1/roles, PUT/DELETE /api/v1/roles/{name} |
admin:write |
POST/GET /api/v1/api-tokens, DELETE /api/v1/api-tokens/{id}, POST /api/v1/api-tokens/{id}:rotate |
admin:tokens |
PUT /api/v1/secrets/{name}, GET /api/v1/secrets, DELETE /api/v1/secrets/{name} |
admin:secrets |
GET /api/v1/audit, GET /api/v1/audit:export, POST /api/v1/audit:verify, GET /api/v1/contacts/{name}:data-export |
admin:audit |
GET /api/v1/notifications/dead-letters / POST …/{id}:replay |
alerts:read / alerts:ack |
POST/DELETE /api/v1/push-subscriptions |
— (principal required) |
GET /api/v1/users, GET /api/v1/users/{id}, POST /api/v1/users, PUT /api/v1/users/{id}, POST /api/v1/users/{id}:set-password, DELETE /api/v1/users/{id} |
admin:users |
POST /api/v1/users/me:change-password |
— (session user only) |
GET/PUT /api/v1/users/{id}/preferences |
— for me/own id; admin:users for others |
GET /api/v1/branding / PUT /api/v1/branding |
— (login required) / config:write |
GET /api/v1/directory/status, POST /api/v1/directory:sync |
admin:users |
GET /api/v1/objects, /hosts, /services, GET /api/v1/objects/{id}, GET …/effective-config, GET /api/v1/problems, GET /api/v1/check-commands:builtins, GET /api/v1/overview |
objects:read |
POST /api/v1/hosts, POST /api/v1/services, PUT/DELETE /api/v1/objects/{id}, POST /api/v1/objects:batch |
objects:write |
POST /api/v1/objects/{id}/check-now, POST /api/v1/check-commands:test |
checks:run |
CRUD of templates, check-commands, time-periods, alert-rules, alert-groups, escalation-policies, channels, event-sources, business-services, dashboards, reports, saved-filters, webhooks, ivr-menus, sites |
objects:read (GET) / config:write (POST/PUT/DELETE) |
CRUD of schedules, contacts, contact-groups |
oncall:read (GET) / oncall:write (POST/PUT/DELETE) |
GET /api/v1/alerts, GET /api/v1/alerts/{id} |
alerts:read |
POST /api/v1/alerts |
alerts:write |
POST /api/v1/alerts/{id}:ack, :resolve, :snooze |
alerts:ack |
GET /api/v1/incidents, GET /api/v1/incidents/{id} |
incidents:read |
POST /api/v1/incidents, PUT /api/v1/incidents/{id}, POST …/{id}:resolve, :merge, :summarize |
incidents:write |
POST /api/v1/alert-rules:test, POST /api/v1/alert-rules/{name}:test, POST /api/v1/escalation-policies/{name}:simulate |
alerts:read |
POST /api/v1/downtimes, DELETE /api/v1/downtimes/{id} / GET /api/v1/downtimes |
downtimes:write / objects:read |
POST /api/v1/silences, DELETE /api/v1/silences/{id} / GET /api/v1/silences |
silences:write / objects:read |
GET /api/v1/oncall/now, GET /api/v1/schedules/{name}/timeline, /ics, /overrides, /stats |
oncall:read |
POST /api/v1/schedules/{name}/overrides, DELETE …/overrides/{id} |
oncall:write |
POST /api/v1/channels/{name}:test-notification |
config:write |
GET /api/v1/events, GET /api/v1/events:export, GET /api/v1/stream |
events:read |
POST /api/v1/metrics/query, GET /api/v1/objects/{id}/metrics |
metrics:read |
POST /api/v1/results |
objects:write |
GET /api/v1/heartbeats / POST /api/v1/heartbeats, DELETE /api/v1/heartbeats/{name} / GET/POST /api/v1/heartbeats/{name}/beat |
objects:read / config:write / objects:write |
POST /api/v1/config/bundles:plan, GET /api/v1/config/bundles:export / POST /api/v1/config/bundles:apply |
objects:read / config:write |
GET /api/v1/business-services:tree, GET /api/v1/objects/{id}/impact, GET /api/v1/business-services/{name}/sla |
objects:read |
POST /api/v1/reports/{name}:render / GET …/archive, GET …/archive/{id} / POST …:run |
reports:render / objects:read / config:write |
POST /api/v1/discovery/scans / GET /api/v1/discovery/scans, GET …/{id} |
config:write / objects:read |
GET /api/v1/agent/checks |
objects:read |
GET /api/v1/system/health, GET /api/v1/system/info |
— (anonymous) |
GET /api/v1/sites:overview |
objects:read |
POST /api/v1/sites/{name}:heartbeat, GET /api/v1/sites/{name}:pull |
sites:connect |
/api/v1/ai/conversations, /ai/providers, /ai/connections (+ :test, /models), /ai/tools, /ai/chats (+ messages), POST /api/v1/ai/chat |
events:read |
GET /api/v1/ai/actions / POST …/{id}:approve / POST …/{id}:deny |
alerts:read / config:write / alerts:ack |
GET/PUT /api/v1/ai/policy |
admin:ai |
Raw routes with their own auth: POST /api/v1/ingest/{source} (+ /alertmanager), GET /api/v1/ack/{token}, POST /api/v1/voice/gather/{token}, POST /api/v1/voice/inbound/{source} (+ /menu, /transcription), POST /api/v1/sms/inbound/{source}, GET /api/openapi.json, GET /api/docs, GET /healthz, GET /readyz, GET /metrics |
— |
The full per-operation list, including request and response schemas, is in the REST API reference.