zynactradocs

Field-level permissions

Return a record to everyone who may read it, and a sensitive field only to those who may see that field.

Some fields need more than read: the reason for leave, a salary, a medical note. Model each as its own action, read:<field>, and reveal the field per row with one batched check.

Declare the field permission

access/plugins/leave/access.yaml
    actions:
      read:
        scopes: [self, team, org_unit, global]
        description: View leave requests
      read:reason:
        scopes: [self, team, global]        # HR by org unit sees requests, not reasons
        description: See the reason given for leave
        sensitive: true                     # granting it globally needs a reason

Then give it to the roles that should see it: employees for their own requests (self), managers for their team (inherit).

access/access.yaml
  employee:
    permissions:
      leave.request:read:reason: self
  line_manager:
    permissions:
      leave.request:read:reason: inherit

Reveal it per row

The list endpoint filters rows with read, then asks once, for the whole page, which rows may show the field:

items := make([]access.CheckItem, len(rows))
for i, l := range rows {
	items[i] = access.CheckItem{Resource: l.resource(), Permissions: []string{accessgen.LeaveRequestReadReason}}
}
res, err := a.CheckMany(ctx, s, items) // one Cerbos call
for i, l := range rows {
	if err == nil && res[i].Allowed[accessgen.LeaveRequestReadReason] {
		l.Reason = &l.reason
	}
}
const results = await access.checkMany(
  s,
  rows.map((r) => ({ resource: leaveResource(r), permissions: [P.LEAVE_REQUEST_READ_REASON] })),
);
return rows.map((r, i) => ({
  ...publicFields(r),
  ...(results[i]?.allowed[P.LEAVE_REQUEST_READ_REASON] ? { reason: r.reason } : {}),
}));

Rules

  • Absent, not empty. Leave the field out of the response (omitempty, a missing key) when it isn't allowed. An empty string or null reads as "no reason given".
  • On every endpoint that returns it: the list, the detail view, exports, search results, webhooks. Put the reveal in one function per kind and call it everywhere.
  • Never in the list filter. Someone who may read a request but not its reason still sees the request.
  • Errors hide the field. If the batched check fails, return the rows without it; don't fail the page, and don't show it.
  • Test both sides: present for someone with the field permission, absent for someone with only read.

In the UI, useCan(P.LEAVE_REQUEST_READ_REASON, id) can decide whether to show a "Reason" column, but the server's response is what keeps the field private.

Edit on GitHub

On this page