Skip to main content

conditional permissions

grantWhen() — attach conditions to a single permission. Use for owner checks, attribute matching, and role-local logic.

grantWhen

grantWhen() attaches conditions to a permission:

const author = defineRole('author')
  .name('Author')
  .grant('create', 'post')
  .grant('read', 'post')
  .grantWhen('update', 'post', (w) => w.isOwner())
  .grantWhen('delete', 'post', (w) => w.isOwner())
  .build()
const author = defineRole('author')
  .name('Author')
  .grant('create', 'post')
  .grant('read', 'post')
  .grantWhen('update', 'post', (w) => w.isOwner())
  .grantWhen('delete', 'post', (w) => w.isOwner())
  .build()

Authors can create and read any post, but only update or delete posts they own. isOwner() produces resource.attributes.ownerId eq $subject.id. $subject.id is a variable reference resolved at evaluation time.


Complex conditional permissions

The grantWhen() callback gets a full When builder:

const teamLead = defineRole('team-lead')
  .name('Team Lead')
  .grant('read', 'report')
  .grantWhen('approve', 'expense', (w) =>
    w
      .attr('department', 'eq', 'engineering')
      .resourceAttr('amount', 'lte', 10000),
  )
  .build()
const teamLead = defineRole('team-lead')
  .name('Team Lead')
  .grant('read', 'report')
  .grantWhen('approve', 'expense', (w) =>
    w
      .attr('department', 'eq', 'engineering')
      .resourceAttr('amount', 'lte', 10000),
  )
  .build()

This grants approve on expenses only when the subject is in engineering AND the amount is at most 10,000.


Combining grantWhen with scope

Both stack — the resulting condition is (scope match) AND (your when conditions):

const orgApprover = defineRole('org-approver')
  .scope('org-1')
  .grantWhen('approve', 'expense', (w) => w.resourceAttr('amount', 'lte', 10000))
  .build()
const orgApprover = defineRole('org-approver')
  .scope('org-1')
  .grantWhen('approve', 'expense', (w) => w.resourceAttr('amount', 'lte', 10000))
  .build()

Effective rule: only fires when scope is org-1 AND amount ≤ 10,000.


When to use grantWhen vs. a standalone policy

Quick decision:

SituationUse
Condition belongs to one role's natural meaninggrantWhen() on the role
Condition spans many rolesStandalone policy
Global deny layerStandalone policy
Different combining algorithm or operational lifecycleStandalone policy

Examples:

  • "Authors can edit their own posts" — grantWhen('update', 'post', w => w.isOwner()) on the author role
  • "Block all writes during maintenance mode" — standalone policy with target({ actions: [...] })
  • "Require GDPR consent for any user-profile read" — standalone policy targeting user-profile resource

How grantWhen interacts with role inheritance

Conditional permissions are inherited like any other. If author has grantWhen('update', 'post', isOwner) and editor inherits author, the editor also has the conditional update permission.

You can override by re-granting unconditionally:

const editor = defineRole('editor')
  .inherits('author')
  .grant('update', 'post') // unconditional — wins under allow-overrides
  .build()
const editor = defineRole('editor')
  .inherits('author')
  .grant('update', 'post') // unconditional — wins under allow-overrides
  .build()

Both rules end up in the synthetic RBAC policy. Under allow-overrides, the unconditional rule grants access regardless of ownership. Under deny-overrides cross-policy, an explicit deny elsewhere can still block.


Built-in shortcuts vs. condition builders

grantWhen() accepts a callback for full control. For very common patterns, use a shortcut directly:

// Shortcut form:
.grantWhen('update', 'post', (w) => w.isOwner())
 
// Equivalent explicit form:
.grantWhen('update', 'post', (w) =>
  w.check('resource.attributes.ownerId', 'eq', '$subject.id'),
)
// Shortcut form:
.grantWhen('update', 'post', (w) => w.isOwner())
 
// Equivalent explicit form:
.grantWhen('update', 'post', (w) =>
  w.check('resource.attributes.ownerId', 'eq', '$subject.id'),
)

See conditions for the full operator reference and $-variables for cross-field comparisons.