Eviworx
Docs

Permissions & RBAC Architecture

Eviworx enforces authorization through a unified, role-based model: every request authenticates as an actor (user, API key or system), carries its role's permission matrix and is checked per route. This page is the central reference for the permission model; the individual API pages link here.

🛡️
Core Principles
✓ One actor model (user, API key, system)
✓ Per-route permission check (feature.action)
✓ Permission cache per role (5 min for reads)
✓ Modifying requests read permissions fresh from DB
✓ Three check levels (view · edit · UI)
✓ Visibility scoping (mailbox, agent group, substitute)
✓ Links only with visibility on both ends
✓ One catalog from the 4 system roles

The Three Check Levels

Every permission acts on three independent levels:

LevelQuestionEnforcement
ViewMay the actor see this entity?Backend: per-object visibility check, list filter
EditMay they change this field on this entity?Backend: route permission + check of changed fields
UIIs the tab/button rendered/active?UI: hides/disables (usability only, not a security boundary)

For API clients only the backend checks view and edit matter. The UI merely hides elements; enforcement always happens in the backend.

Unified Actor Model

ActorAuthPermissions
userSession cookie (JWT, HttpOnly)Matrix of the assigned role
apiKeyX-API-Key header (Bearer NOT supported)Matrix of the key's role — key WITHOUT an active role → 403 API_KEY_NO_ROLE
systeminternal trusted caller (worker/system user)BYPASS — only when the internal caller explicitly requests it

A request authenticates either via session cookie or via X-API-Key; both go through the same permission check. An API key receives the permissions of its role, optionally limited by expiry, IP whitelist, rate limit and allowedTemplates.

User-only vs. API-key-only: Some endpoints accept only signed-in users and reject API keys with 403, e.g. the cost-center mutations, the license-assignment routes, ticket linking and all approval decisions. Conversely, some endpoints accept API keys only. Read and sync endpoints usually accept both, such as the cost-center reads and POST /api/cost-centers/import for ERP sync.

Permission Check per Request

  • Permission cache: A role's matrix is cached per role for 5 min. Read requests use the cache; modifying requests (POST, PUT, PATCH, DELETE) always load permissions fresh from the database. A role change via the API clears the role's cache immediately and therefore applies to all holders of the role at once.
  • Missing permission: 403 with errorCode FORBIDDEN; required names the permission needed — as a string, or as an array for critical actions and for routes where any one of several permissions suffices. (body examples)
  • No permissions in the token: Permissions are loaded from the role on every request, not read from the token. An older token therefore grants no permissions that have since been removed from the role.
  • Critical actions: critical actions (e.g. delete, approve, manageRoles) always read permissions fresh from the database, so a just-revoked permission applies there without cache delay.
  • Usable role: A missing role, a deactivated role and an invalid permission matrix count the same: they carry no usable permissions. The same rule decides everywhere — at sign-in, on token refresh, when assigning a role and in the directory sync — so that an account is not treated as entitled in one place and rejected in the next. (sign-in)

Route permission and object check: The route permission alone is not enough. The concrete object is additionally checked for whether the caller may see it and change the affected fields. A request can therefore be rejected despite holding the route permission. Status transitions and approvals have their own permissions.

Per-Object Checks

  • View: lists, search and statistics apply the same visibility rule as the detail view.
  • Edit: either a base permission (editAll, or editOwn for own objects) plus an extra permission per changed field (assignment → assign, status → changeStatus, reopen → reopen …), or a separate permission per action where there are no “own” objects.
  • Reopen: dedicated permission reopen / reopenOverride (ticket, incident, problem) — separate from changeStatus/editAll. A reopen requires tickets.reopen (resp. incidents./problems.reopen); a role with editAll but reopen=false CANNOT reopen. reopenOverride bypasses only window/limit, never the reason or approval requirement. (details)
Edit modelDomains (examples)
Own/all + field permissionsTicket, Incident, Problem, Change, Contract, Inventory, Custom-Reports
One permission per actionCostCenter, License, Cronjob, Settings, Notifications, Approvals (management)
Asset type lock (additional permission)Asset (per asset-type grant/override)

Visibility in Detail

Beyond the global viewAll/viewOwn permissions, actual visibility is further restricted. Three restrictions matter in practice:

Mailbox Access (tickets)

  • accessRestricted = false → anyone with tickets.viewAll sees the mailbox tickets.
  • accessRestricted = true → only users/roles/agent groups in the MailboxAccess list (separate flags canViewTickets / canBeAssigned). Even viewAll is constrained.
  • Exceptions: creator (customerId), assigned agent and agents of the assigned group always see the ticket.

Agent Group Scope

The visibility check of a ticket considers the actor's group memberships (assignedGroupId ∈ own groups) — so an agent sees their group's queue without global viewAll. Only active memberships count.

Substitution (substitute)

During an approved absence the substitute sees and edits the covered user's tickets/items, in lists as well as in the detail view. Outside an approved substitution this exception does not apply.

Custom Reports: two permissions

The report builder is a mass-data/export channel and therefore deliberately separates TWO permissions, both required:

PermissionControls
customReports.* (view*/create/edit*/delete*/export/schedule/share)the report DEFINITION — builder access, CRUD, sharing, scheduling
<feature>.reporting (tickets, problems, changes, incidents, assets, contracts, licenses)the DATA level — which domain the user may mass-evaluate at all ("use in reports")

The data sources in the builder follow the same visibility rules as above: a domain appears only if the user holds its reporting permission AND can see any rows of that domain at all. Examples: asset type grants without a global read permission → the source appears; reporting permission on a domain with no visibility → the source does not appear. On execution the report additionally filters every row by the executor's visibility.

  • Sub-entities inherit the domain permission: changes.reporting also unlocks changeTasks, assets.reporting also handovers.
  • Individual fields additionally require their own permission (participant emails → tickets.viewAll, GDPR block → incidents.viewPIR) — making the report stricter here than the single view.
  • Result view = report view + customReports.export: whoever may see a shared report and holds export loads its results in the executor's scope. Sharing with roles is explicit (sharedWithAllRoles flag; an empty role list means shared with nobody, not with everyone).

Full API details, error codes and field gates: Reports & Custom Reports API.

Links Between Objects

Links (e.g. license↔asset/user/contract, asset↔contract) are checked centrally: the route requires the permission of the calling side, and the caller must additionally be able to see both ends, regardless of direction:

allowed(A↔B) = permission of the calling side
            AND canView(A) AND canView(B)     (direction-neutral)

Linked objects appear with full data if the caller may see them, otherwise as a placeholder (id/number/status, restricted:true).

Ticket to ticket: parent ticket and sub-tickets

Tickets can also be linked to each other: one ticket is subordinated to another as a sub-ticket. This has its own permission in the ticket domain, tickets.linkToTickets — on a par with the other linkTo* permissions. It covers both ends of the action: subordinating and releasing the relationship, creating a ticket with a parent ticket, and the list of tickets eligible as a sub-ticket at all. Without the permission the candidate list stays closed, not just the write path. The general rule applies on top: the caller must be allowed to see both tickets.

The internal details of this relationship — the list of sub-tickets, their counters, the activity trail and the reason for an SLA pause — additionally follow tickets.viewInternal, the permission for the internal view of a ticket. Anyone without it (typically a customer) sees the ticket but not its parent/child details. Ticket API details, overview of the linking endpoints.

Permission Catalog & System Roles

The catalog (all modules/actions) is made up of the permissions of the 4 system roles. System roles can neither be deactivated, deleted nor moved, and settings.manageRoles cannot be removed from them; custom roles are freely configurable. Lower priority = higher privileges (decides EntraID conflicts).

RolePriorityisSystem
ADMIN1000true
AGENT2000true
DATA_PROTECTION_OFFICER (DSB)2500false (pre-installed)
APPROVER3000true
END_USER99000true

New permissions after an update: A newly added permission starts as “off” in custom roles. If it replaces an existing permission, it inherits that permission’s value, so existing roles lose nothing.