zynactradocs

Permissions

Resource kinds, attributes, actions, conditions, field-level and sensitive permissions.

A permission is a resource kind and an action: leave.request:approve. Products declare them in resource manifests, one per area of the product (the leave module, the payroll module), so each team owns its own.

access/plugins/leave/access.yaml
source: leave-demo/leave
resources:
  leave.request:
    attributes:
      ownerId: string
      ownerPath: string
      orgUnitId: string
      status: { enum: [pending, approved, rejected] }
      days: number
    actions:
      create:   { scopes: [self, global], description: Request leave }
      read:     { scopes: [self, team, org_unit, global], description: View leave requests }
      approve:
        scopes: [team, org_unit, global]
        description: Approve or reject leave
        condition: R.attr.status == "pending" && R.attr.ownerId != P.id
      read:reason:
        scopes: [self, team, global]
        description: See the reason given for leave
        sensitive: true

Resource kinds

One kind per kind of record: leave.request, employee, payroll.run. Prefix kinds with the area that owns them. access.* is reserved for the built-in administration permissions.

Attributes

The attributes are exactly what policies need to decide: who owns the record, where it sits in the reporting line, which unit it belongs to, its status. Types are string, number and boolean; strings may be an enum; any attribute may be optional. orgId is added to every kind for you.

Every check must send every required attribute, typed as declared, and nothing else. Cerbos rejects anything else (the generated schemas are enforced with reject), and the SDK denies it before it gets there. Build a record's resource in one function per kind, so every check sends the same thing.

What attributes are not: a copy of the record. A field that no rule reads doesn't belong in the manifest.

Actions and their scopes

Each action lists the scopes it may be granted at. create above can be granted for your own records or everywhere, never "for your team". Allow the narrowest set that makes sense; admins can't grant outside it. See Scopes.

Conditions

A condition is for rules that are not about reach: the record's state, or "not your own". It is a CEL expression over:

  • R.attr.<attribute>: the record's declared attributes (and R.attr.orgId);
  • P.id: the caller's user ID;
  • P.attr.orgId and the principal attributes of prefix scopes (P.attr.reportingPath).

Never P.attr.perms: what someone holds is the scope check's job. A condition applies on top of the scope, at every scope, including global. In the example, nobody approves their own leave, not even someone who holds approve globally.

Conditions also apply to lists: they become part of the SQL filter. The translator supports the operators generated policies use (==, !=, <, <=, >, >=, in, startsWith, endsWith, contains, &&, ||, !) and refuses anything else rather than widen a query.

Field-level permissions

A field only some people may see is its own action, not a flag in code: leave.request:read:reason, employee:read:salary. Actions may contain colons; the key splits on the first one. The endpoint returns the record to anyone with read, and includes the field only where a batched check of the field permission allows it. See Field-level permissions.

Sensitive permissions

sensitive: true marks permissions whose reach should be deliberate: salaries, medical notes, the reason for leave. Granting one at global scope needs a reason, and the change is logged with high severity. Granting it at a narrower scope is ordinary.

Constants

zaccess gen writes the permission keys as constants: accessgen.LeaveRequestApprove in Go, P.LEAVE_REQUEST_APPROVE in TypeScript. Product code uses them, never string literals, so a renamed or removed permission is a compile error.

Changing permissions

Edit the manifest, zaccess gen, update the policy tests, zaccess test. On the next start, SyncCatalog (or zaccess sync) brings the database's catalog and system roles in line. A removed permission is deprecated in the catalog rather than deleted, so history keeps pointing at it.

Edit on GitHub

On this page