Skip to main content

The permission model

How a permission is shaped, the module-scoped resources, why a catalogue exists at all, and the outage that explains the design better than a definition would.

Written for whoever runs IT9 min readUpdated

Who can see and do what covers the three built-in roles and is the right article for most people. This one is for whoever has to reason about the model itself: because you are building a custom role, because you are debugging a 403, or because you want to know how much you can trust the boundaries you are relying on. It is more detailed and less flattering.

The shape of a permission

A permission is a pair: a resource and an action. The name you see in the interface is simply the two joined by a full stop. Resource siem:alerts plus action read is the permission named siem:alerts.read. Both the name and the pair are unique, so there is exactly one row in the catalogue for any given combination.

A role is a named bundle of those pairs, and a role belongs to one organisation. Your admin and another customer's admin are two different rows that happen to share a name. A person's access is decided by one thing only: does the role attached to their membership of this organisation hold this exact pair?

Four consequences follow, and each of them is the answer to a question somebody eventually asks.

  • There are no wildcards. No *, no siem:*, no prefix matching. Matching is exact string equality on both halves. A role holding siem:alerts.read holds nothing else — not siem:alerts.write, not siem:rules.read. Every permission a role needs is listed explicitly.
  • There is no superuser shortcut in the check. The admin role is powerful because it has been granted every row in the catalogue, not because the code treats it specially. That is a deliberate choice: it means the same one query answers every access question, and there is no second, weaker path to get around.
  • No membership means no access. A user who is not a member of an organisation gets a negative answer for every permission in it. There is no default, no inherited access and no fallback.
  • Permissions are not additive across roles. A membership names exactly one role. Somebody cannot hold member and a custom role at once and receive the union of the two; if they need both, you build a third role that contains both sets.

When a check says no, the request is refused with HTTP 403 and this body. It is the same string everywhere in the platform, which is why it is worth recognising on sight:

A permission denial
403 {"error":"insufficient permissions"}

A different message, permission check failed with HTTP 500, means the check could not be evaluated at all — a database problem on our side rather than anything about your access. If you see that one, it is ours to fix and worth reporting.

The resource strings

Two naming conventions coexist, and the difference is historical rather than meaningful. The original identity and organisation permissions use a bare resource: users, roles, sso. Everything added once the platform grew modules is prefixed with the module and a colon: siem:sources, pam:credentials. Both are ordinary permissions and are checked identically.

The resource strings you will meet most often, and the actions defined on each. The permission name is the resource, a full stop, and the action.
ResourceActionsWhat it governs
usersread, write, deleteUser records, import, search, deprovisioning.
organizationsread, write, deleteOrganisation settings, lifecycle, group rules, profile schemas, password policy — and, as it turns out, the audit log.
membersread, write, deleteWho belongs to the organisation.
rolesread, write, deleteCreating and editing roles.
api_keysread, write, deleteProgrammatic credentials against the tenant.
ssoread, write, deleteSingle sign-on providers and provisioning tokens.
audit_logsreadReading the audit trail. See the caution below.
siem:sources, siem:alerts, siem:rules, siem:dashboards, siem:queries, siem:tokensread, write, deleteLog collection, detections and investigation. siem:tokens has only write.
xdr:endpoints, xdr:rules, xdr:alerts, xdr:incidents, xdr:response_actions, xdr:telemetryread, write, delete, executeEndpoint detection and response. xdr:response_actions.execute is the one that contains a machine.
pam:credentials, pam:sessions, pam:policies, pam:access_requests, pam:rotation, pam:discovery, pam:targets, pam:platformsread, write, delete, testPrivileged access. pam:credentials.read is retrieval of secret material and is the single most sensitive pair in the catalogue.
ai:assistants, ai:conversations, ai:toolsread, writeSentinel-Q and the assistant. Writing a tool definition changes what the AI may reach for.
billing:subscriptions, billing:invoices, licensing:seats, licensing:featuresread, writeMoney and seat allocation.
compliance:frameworks, compliance:evidence, compliance:breachesread, writeEvidence collection and breach records.
integration:webhooks, integration:connectors, integration:instancesread, write, deleteOutbound webhooks and connected systems — all ways to move your data elsewhere.
iam:network_zones, iam:auth_policies, iam:risk, iam:swa_apps, iam:swa_credentials, iam:federationread, write, deleteThe identity administration surfaces added after the original set.
realtime:events, push:devicesread, writeLive event streams and registering a phone for alerts.

The actions are read, write and delete almost everywhere. There are exactly two exceptions and both are deliberate: execute on XDR response actions, because running a containment is a different thing from being allowed to define one, and test on a PAM target, because checking that a connection works is not the same as changing it.

Why there is a catalogue at all

It would be simpler to hard-code the rules — admins can do everything, members can triage — somewhere in the application. We do not, and the reason is a real failure that is more instructive than any definition of role-based access control.

The permission catalogue started as eighteen rows covering identity and organisation administration: users.read, roles.write, sso.delete and so on. That was the whole product at the time. Then SIEM arrived, and XDR, and PAM, and each module's routes guarded themselves with module-scoped resources — siem:sources with read, pam:credentials with read, and dozens more.

Nobody added those rows to the catalogue. The check is exact string equality against a table, so a permission that does not exist as a row cannot be granted to anybody, and a check for it can only ever return no. The result was that a brand-new organisation's owner — the person who had just signed up and paid — could administer identity and nothing else. Every SIEM page, every XDR page, every PAM page, billing, licensing, compliance and integrations all answered the same way:

What a freshly provisioned administrator saw on every module but identity
403 {"error":"insufficient permissions"}

It was not a bug in any one module. It was a gap between two sources of truth — the routes knew what they required, the database did not — and every module added afterwards widened it. Seeding the missing rows would have fixed the customers who existed that day and nobody else, because roles are created per organisation at the moment an organisation is provisioned. The next customer to sign up would have hit the same wall.

Making the grants a property of the schema

So the repair had three parts, and the third is the one that matters.

  1. Seed the missing permissions

    The sixty-seven missing rows were added, bringing the catalogue to eighty-five. That closed the gap for the modules that existed at the time.

  2. Move the built-in role policy into a table

    What viewer and member are supposed to hold is now recorded as data in a lookup table, with a comment on each entry explaining why. The answer to "what does viewer get" is a query with one auditable answer, not something reconstructed by reading application code.

  3. Make the grant happen when the role is created

    A database trigger now fires the moment a role row is created — that is, at the instant a new organisation is provisioned, inside the same transaction. If the new role is named admin, it is granted every row currently in the permissions table, unconditionally, with no lookup. If it is named member or viewer, it is granted whatever the policy table lists for that name. The grants are no longer something a migration has to remember to do; they are a property of the schema.

The catalogue has kept growing since — network zones, authentication policies, risk, SWA applications, federation, PAM targets — and is now in the region of a hundred and thirteen permissions rather than eighty-five. That growth is the system working as intended: each new module adds its rows, and existing administrators keep working without anybody touching their roles.

One consequence of the trigger is worth knowing before you create a role. Matching is on the exact role name, and it is case-sensitive. A role you create called Admin matches nothing in the policy table and is granted no permissions at all. Neither does any name of your own choosing — which is not a defect but the mechanism by which a custom role starts empty and you fill it deliberately. Building a role that grants exactly enough picks up from there.

Boundaries that do not hold

Three places where the model is thinner than the catalogue implies. We would rather you knew than relied on a line that is not there.

Second, several identity surfaces carry no permission check of their own. Groups, the directory synchronisation pages, the lifecycle rules pages and the console branding settings are authenticated and locked to your own organisation, but any member of it can use them. Groups and automatic membership covers what that means in practice.

Third, four permissions in the catalogue control nothing inside identity: iam:users.read, iam:users.write, iam:scim.read and iam:scim.write. They appear in the permission picker and can be granted, and granting them changes nothing here — the provisioning token administration you would expect them to govern is gated on the sso family instead. One of them is genuinely used, but by the XDR module rather than this one. Granting them does no harm; expecting them to do something does.

One boundary that does hold, and is worth knowing about because it is unusual: six permissions require a fresh second factor on every use, and an API key is refused outright at the step-up endpoint. So users.write, roles.write and organizations.write — and, in practice, api_keys.write, members.write and sso.write — cannot be exercised by a script at all. See Step-up authentication.

What to do next

  1. Building a role that grants exactly enough — three worked scenarios with the actual permission names.
  2. Who can see and do what — the three built-in roles and which one to give whom.
  3. Delegated administration — what scoping an administrator to part of your estate does and does not currently achieve.

Was this article wrong?

If a procedure here does not match what you see, or a limit we described has changed, tell us and we will fix the page. Email us about this article, or see how to get help if you need an answer rather than a correction.

Everything in identity and access