zynactradocs

Roles and assignments

System and custom roles, assignments with a scope and a validity window, IdP group mappings, break-glass and the built-in admin role.

Roles

A role is a named set of permissions, each with a mode. There are two kinds:

  • System roles come from the product manifest and are seeded into every org. Admins assign them but can't edit or delete them; they change with the code.
  • Custom roles are made by admins from the permission catalog, for needs the product didn't anticipate. An admin can only put a permission into a role if they hold it globally.
access/access.yaml
systemRoles:
  employee:
    name: Employee
    permissions:
      leave.request:create: self
      leave.request:read: self
  line_manager:
    name: Line manager
    description: Assign with scope team.
    permissions:
      leave.request:read: inherit
      leave.request:approve: inherit

"*": global in a role means every permission that may be granted globally; it's for break-glass roles.

A role in use (assigned, or mapped to a group) can't be deleted. Deleting an unused one soft-deletes it, so the change log and revoked assignments keep pointing at it.

Assignments

An assignment gives one user one role at one scope:

Field
userId, roleIdwho, and which role
scopeType, scopeIdsglobal, self, a prefix scope type, or a set scope type with IDs
validFrom, validUntiloptional window; an assignment outside it grants nothing
reasonwhy; required for sensitive global grants and break-glass

Assignments are revoked, never deleted. Revoking one keeps it in history with who revoked it and why.

Group mappings

A group mapping gives a role at a scope to everyone whose token carries an IdP group: (idp, groupName) -> role @ scope. Members hold it while their verified token has the group (Subject.IdP and Subject.Groups). It's how "everyone in all-staff is an employee" works without onboarding code.

Mappings with a prefix scope need the person creating them to hold those permissions globally, because the members aren't known in advance. Break-glass roles can't be mapped to groups: that would make them permanent for a whole group.

Break-glass

A role marked breakGlass: true is for emergencies: "*" at global, for a few hours. Assigning it needs a reason and a validUntil at most 24 hours after validFrom, and is logged with high severity.

  break_glass:
    name: Break glass
    breakGlass: true
    permissions:
      "*": global

The built-in admin role

access_admin is built in and can't be redeclared. It holds every access.* permission:

PermissionLets you
access.role:readsee roles and the permission catalog
access.role:managecreate, edit and delete custom roles
access.assignment:readlist assignments
access.assignment:manageassign and revoke roles
access.group_mapping:managemap IdP groups to roles
access.change_log:readread the change log
access.decision:explainexplain other people's decisions
access.delegation:anygrant beyond your own reach (sensitive, global only)

access.delegation:any is what lets the first admin hand out product roles they don't hold themselves. It lifts the no-escalation rule and nothing else.

The rules the admin service enforces

  1. No escalation. To grant a permission at a scope you must hold it at that scope or wider: global covers everything; self needs self or global; a prefix scope needs the same scope with the target's path inside yours; a set scope needs every expanded ID. Revoking follows the same rule. access.delegation:any lifts this one.
  2. No self-assignment. Nobody assigns roles to themselves, admins included.
  3. Role editing needs each added permission held globally. System roles can't be edited.
  4. Reasons for sensitive global grants and break-glass.
  5. Break-glass limits: at most 24 hours, never mapped to groups.
  6. The last admin: the last active access_admin of an org can't be revoked.
  7. The change log is append-only; the database refuses UPDATE, DELETE and TRUNCATE on it.

They hold for every caller, the admin screens, scripts and service accounts alike, because only the admin service writes to the access tables. See Administering access.

Edit on GitHub

On this page