Skip to main content

Roles

This page is a reference for role assignment, since roles can attach to three different entities, and any of the other use-cases can also involve roles.

A role can attach to a user within an organization, a group at the tenant level, or a group within an organization. Each attachment point fires its own event pair and writes to a different object. Pick the subsection below that matches how you're assigning the role, then read Static vs. dynamic roles, which applies regardless of which one you picked.

What can hold a role​

A user, within an organization membership​

Triggered by organization.member.role.assigned / organization.member.role.deleted. This is the most common case, and the one B2B via Organizations walks through end to end.

organization.member.deleted's payload only ever carries organization.id and user.user_id, never which roles that user held. So a role assignment is wired through an intermediate organization_membership object instead of directly from user to role: that's what lets removing a user from an organization revoke every role they held in it with a pair of filters keyed on the membership, without knowing what those roles were.

type organization_membership # one object per (organization, user) pair, id "{organization_id}_{user_id}"
relations
define member: [user]

type role
relations
define assignee: [user, organization_membership#member]

type organization
relations
define member: [user]
rules:
- name: "Add user to organization"
when: input.type == "organization.member.added"
variables:
organization_id: input.data.object.organization.id
user_id: input.data.object.user.user_id
tuples:
- user: "user:{{ variables.user_id }}"
relation: "member"
object: "organization:{{ variables.organization_id }}"

- name: "Remove user from organization"
when: input.type == "organization.member.deleted"
variables:
organization_id: input.data.object.organization.id
user_id: input.data.object.user.user_id
tuples:
- action: delete
user: "user:{{ variables.user_id }}"
relation: "member"
object: "organization:{{ variables.organization_id }}"

- name: "Revoke roles held through the membership"
when: input.type == "organization.member.deleted"
variables:
organization_id: input.data.object.organization.id
user_id: input.data.object.user.user_id
tuple_filters:
- user: "organization_membership:{{ variables.organization_id }}_{{ variables.user_id }}#member"
object: "role:"
action: delete
- object: "organization_membership:{{ variables.organization_id }}_{{ variables.user_id }}"
action: delete

- name: "Assign role to user in organization"
when: input.type == "organization.member.role.assigned"
variables:
organization_id: input.data.object.organization.id
user_id: input.data.object.user.user_id
role_id: input.data.object.role.id
tuples:
- user: "user:{{ variables.user_id }}"
relation: "member"
object: "organization_membership:{{ variables.organization_id }}_{{ variables.user_id }}"
- user: "organization_membership:{{ variables.organization_id }}_{{ variables.user_id }}#member"
relation: "assignee"
object: "role:{{ variables.role_id }}"

- name: "Remove role from user in organization"
when: input.type == "organization.member.role.deleted"
variables:
organization_id: input.data.object.organization.id
user_id: input.data.object.user.user_id
role_id: input.data.object.role.id
tuples:
- action: delete
user: "organization_membership:{{ variables.organization_id }}_{{ variables.user_id }}#member"
relation: "assignee"
object: "role:{{ variables.role_id }}"

Assigning a role writes both user --member--> organization_membership:{organization_id}_{user_id} and organization_membership:{organization_id}_{user_id}#member --assignee--> role:{role_id}. "Remove user from organization" removes the user's organization membership tuple, and "Revoke roles held through the membership" cleans up the rest with two filters, both computable from organization_id and user_id, which organization.member.deleted carries: one deletes every role assignment granted to that membership's #member userset, the other deletes the membership's own member tuples. Without the first filter, the user would lose access (their member tuple is gone) but the assignee tuples on each role would remain as orphans. A simpler model, role with define assignee: [user] and the role's assignees granted directly on the resource (for example define billing_manager: [role#assignee] on organization), works when you don't need roles revoked automatically on membership removal: a delete would have to name each role, and the event doesn't say which ones the user held. If the same user holds the same role via a second organization they're still a member of, each organization gets its own organization_membership object, so removing one doesn't affect the other.

A group, at the tenant level​

Triggered by group.role.assigned / group.role.deleted. Covers assigning a role directly to a SCIM group with no organization involved, as in Workforce via SCIM Groups.

type role
relations
define assignee: [user, group#member]

type group
relations
define member: [user, group#member]
rules:
- name: "Assign role to group"
when: input.type == "group.role.assigned"
variables:
group_id: input.data.object.group.id
role_id: input.data.object.role.id
tuples:
- user: "group:{{ variables.group_id }}#member"
relation: "assignee"
object: "role:{{ variables.role_id }}"

- name: "Remove role from group"
when: input.type == "group.role.deleted"
variables:
group_id: input.data.object.group.id
role_id: input.data.object.role.id
tuples:
- action: delete
user: "group:{{ variables.group_id }}#member"
relation: "assignee"
object: "role:{{ variables.role_id }}"

role#assignee accepting group#member (alongside user) is what lets a role reach every current and future member of a group with one tuple, instead of one tuple per member.

group.role.deleted only carries the role's id, never its name. If a rule needs to branch on which role fired, branch on role.id.

A group, within an organization​

Triggered by organization.group.role.assigned / organization.group.role.deleted, distinct from the bare, tenant-level events above. The payload carries organization.id, group.id, and role.id together.

Whether this should write to the same role:{id}#assignee tuple as the tenant-level case, or a separate one, depends on whether a group in your tenant can hold the same role at both scopes and you need to tell the two grants apart. If a group only ever gets a given role one way, write to the same tuple as the tenant-level case:

rules:
- name: "Assign role to group in organization"
when: input.type == "organization.group.role.assigned"
variables:
group_id: input.data.object.group.id
role_id: input.data.object.role.id
tuples:
- user: "group:{{ variables.group_id }}#member"
relation: "assignee"
object: "role:{{ variables.role_id }}"

- name: "Remove role from group in organization"
when: input.type == "organization.group.role.deleted"
variables:
group_id: input.data.object.group.id
role_id: input.data.object.role.id
tuples:
- action: delete
user: "group:{{ variables.group_id }}#member"
relation: "assignee"
object: "role:{{ variables.role_id }}"

If a group can hold the same role both ways and the two need to stay distinguishable, wire this through a separate object instead, the same way user-in-organization role assignment is wired through organization_membership above: an object scoped to (group, organization), with its own member tuple, so each grant can be told apart by which object it's attached to and cleaned up independently of the other.

Static vs. dynamic roles​

Roles fall into two categories, and each needs different modeling. This applies to any of the three attachment points above.

  • Static roles: your application defines these itself, once, as a fixed set of named permissions hardcoded into your model as relations (for example, an "Approver" role your app checks for explicitly). Your mapping can branch on these by identity.
  • Dynamic roles: each customer or organization defines their own roles, with arbitrary names, IDs, and permissions your application can't know ahead of time. Since they're managed by the customer rather than fixed in your model, these have to be modeled generically and managed as tuples.

Some applications need only one category; others need both.

caution

Branch on the role's ID (input.data.object.role.id), not its name. A role's name can be changed by whoever manages roles in your Auth0 tenant (see Create Roles), but its ID is stable for the role's lifetime. Matching by name is fragile: a rename silently breaks the branch, with no error to catch it.

Static roles​

Since your application already knows about these roles, model each as its own named relation directly on organization, for example:

type organization
relations
define member: [user]
define approver: [user] # illustrative: a static role, not an Auth0-provided one

Map it by matching the role's ID, hardcoded to the specific role you created in your own tenant:

- name: "Assign approver role to user in organization"
when: >-
input.type == "organization.member.role.assigned" &&
input.data.object.role.id == "rol_YOUR_APPROVER_ROLE_ID"
variables:
organization_id: input.data.object.organization.id
user_id: input.data.object.user.user_id
tuples:
- user: "user:{{ variables.user_id }}"
relation: "approver"
object: "organization:{{ variables.organization_id }}"

- name: "Remove approver role from user in organization"
when: >-
input.type == "organization.member.role.deleted" &&
input.data.object.role.id == "rol_YOUR_APPROVER_ROLE_ID"
variables:
organization_id: input.data.object.organization.id
user_id: input.data.object.user.user_id
tuples:
- action: delete
user: "user:{{ variables.user_id }}"
relation: "approver"
object: "organization:{{ variables.organization_id }}"

Replace rol_YOUR_APPROVER_ROLE_ID with the real role ID from your tenant. Because this writes a direct relation on organization, no separate cleanup rule is needed: Cleaning up roles on membership removal's filter-by-user+organization technique already reaches it when the membership is removed.

Dynamic roles​

Since these roles' names, IDs, and count aren't known ahead of time, model them generically as role:{id}#assignee. Use whichever attachment point above matches how the role is assigned: through organization_membership for a user in an organization, or directly on group#member for a group.

Combining both​

If your application has both kinds, add two separate rule pairs per event, each guarded by whether the role ID matches your known, hardcoded set. The first pair is the static branch shown above. The second pair reuses the same rules as A user, within an organization membership, with input.data.object.role.id != "rol_YOUR_APPROVER_ROLE_ID" (and any other static role IDs) added to their when guards, so the two branches don't overlap.

Rules are independent, and all matching ones fire for a given event, so ordering between the two pairs doesn't matter as long as their when conditions are mutually exclusive.

Have Feedback?

You can use any of our support channels for any questions or suggestions you may have.