Overview
Vigilo's authorisation model has three layers stacked top-to-bottom: built-in roles (the five tiers everyone gets out of the box), custom roles (workspace-specific variants that inherit from a built-in and add/remove permissions), and deny rules (explicit "never allow this, even if inherited" overrides). On top of that, OIDC group mappings and SCIM let an external identity provider drive role assignment automatically.
Roles live at /ws/{slug}/settings/roles. The permission matrix view renders the full role × permission grid with cells coloured by allow (green), inherit (white), or deny (red).
Why it exists
Every multi-tenant product evolves the same way: ship five roles, hit a customer who needs "viewer but can also approve emergencies", then either flag-ify every role into oblivion or build a real RBAC layer. Vigilo's RBAC is built up front: the five tiers cover 95% of cases, custom roles cover the rest, and deny rules let an admin remove a specific permission from a role without forking the entire role.
Key concepts
Built-in WorkspaceRole
WorkspaceRole is the enum of five tiers:
- owner — full control, including billing, SSO config, and ownership transfer.
- admin — manage members, integrations, custom fields, playbooks, webhooks; cannot change ownership or billing.
- approver — approve and reject changes, schedule, and execute. Read-only on everything else operational.
- engineer — create and edit changes, tasks, incidents, runbooks; cannot approve their own work.
- viewer — read-only across the workspace.
Each tier is bound to a fixed set of permissions in apps/workspaces/permissions.py. The set is stable across releases: adding a new permission to the codebase requires explicit assignment to the relevant tiers, never silent inheritance.
CustomRole
A CustomRole (CustomRole: workspace, name, inherits_from (FK to built-in WorkspaceRole), description, is_active) is a workspace-defined role that inherits the entire permission set of its parent and lets you add (allow) or remove (deny) individual permissions. A custom role is assigned to a member via WorkspaceMembership.custom_role; the WorkspaceMembership.role field still holds the tier hint (used for default-dashboard selection and "what tier am I" UI).
Examples:
- Cert manager — inherits viewer, adds
assets.writeandassets.execute_rotation. - Emergency approver — inherits engineer, adds
changes.approvebut only forchange_type=emergency(the limitation is enforced via a permission predicate, not just role).
CustomRoles are workspace-scoped; cloning across workspaces is a roadmap item.
RoleDenyPermission
The RoleDenyPermission model (a stub from WB.13 — currently roadmap) will allow an admin to explicitly deny a single permission on a built-in role for the workspace without creating a custom role. Today, the same outcome is achieved with a CustomRole that inherits the built-in and removes the permission. The deny model will simplify the common case ("we never want approvers to delete attachments") without proliferating named roles.
Permission matrix UI
The matrix UI at /ws/{slug}/settings/roles/matrix lists every permission in the system as a row and every role (built-in + custom) as a column. Cells render:
- Green check — permission is allowed for the role.
- White circle — permission is inherited from parent (custom roles only) and currently allowed.
- Red X — permission is explicitly denied (custom-role override or deny rule).
Click any cell to toggle (custom roles only — built-in cells are read-only). Hover for the permission's docstring.
OIDCGroupMapping
OIDCGroupMapping (WB.13: workspace, oidc_group (string), target_role (FK or string), priority (int), is_active) maps an Identity Provider group name to a Vigilo role. On login, the OIDC token's groups claim is iterated against the active mappings in priority order (lower number wins), and the first match sets the user's role for the workspace.
If no mapping matches, the user's role falls back to Workspace.settings.oidc_default_role (often viewer).
Multiple mappings supports common IdP layouts where a user is in many groups: e.g. eng-payments → engineer, oncall-payments → approver, vigilo-admins → admin. Highest-power role wins via priority ordering.
SCIM Groups JIT provisioning
When SCIM is configured (WB.12), an external provisioning system (Okta, Azure AD, JumpCloud) pushes membership updates to /scim/v2/Groups and /scim/v2/Users. Vigilo handles the SCIM PATCH ops and:
- Creates a
UserProfileon first contact (Just-In-Time provisioning) if the user does not exist. - Creates or updates the
WorkspaceMembershipwith the role derived from the matchingOIDCGroupMapping. - Revokes membership when the user is removed from the SCIM group.
Workspaces with SCIM enabled typically disable manual invites for SCIM-managed roles to avoid drift; the Members page surfaces a "SCIM-managed" badge on rows owned by the provisioning system.
Common workflows
Create a custom role
- Settings → Roles → New custom role, pick a parent built-in (e.g. engineer), name it ("Cert manager").
- The new role appears in the matrix. Toggle cells to allow or deny specific permissions.
- Activate the role. It now appears in the role picker on the member detail panel.
Assign a custom role to a member
- Open the member detail, click Change role, pick Custom role, then the new role.
- The membership row sets
custom_roleand the tier hint remains.
Map an OIDC group to a role
- Settings → Roles → OIDC mappings → New mapping.
- Type the group name (must exactly match the value in the
groupsclaim), pick the target role, set priority. - Click Save. Next login that includes the group claim will be assigned the target role.
Enable SCIM provisioning
- Settings → Identity → SCIM, generate a bearer token.
- Configure your IdP with
https://{your-host}/scim/v2/and the bearer token. - Map IdP groups to Vigilo roles either via the IdP's role-mapping UI or via Vigilo's OIDCGroupMapping table.
Permissions
- Owners can manage all role configuration including SCIM, OIDC mappings, and ownership.
- Admins can manage custom roles, deny rules, and OIDC mappings but cannot transfer ownership.
- Approvers / engineers / viewers can read the matrix to understand their own permissions, no edits.
Troubleshooting
A user has the wrong role after OIDC login — Inspect their token's groups claim and walk the OIDCGroupMapping table in priority order. The token can be viewed in the user's session details under the per-member panel.
Custom role toggles do not save — Likely a stale role state — the role's is_active=False. Activate it before editing.
Deny rule never takes effect — The deny model (RoleDenyPermission) is roadmap (WB.13). Today, achieve the same effect with a custom role that inherits the built-in and removes the permission cell.
SCIM PATCH returns 4xx for a known user — Check the scim_logs admin view for the exact error. Common cause is a group name in the PATCH op that has no OIDCGroupMapping row and oidc_default_role not set.
Member has both built-in role and custom_role; which wins? — custom_role always wins for permission resolution. The built-in role is only used for tier-based UI hints (default dashboard, "your tier" badge).