Skip to content

Permissions

A permission represents a specific action an actor can perform on a resource, such as "read" or "edit" on Document:handbook.

Modeling permissions explicitly with has_permission(actor, action, resource) gives your policy a clean foundation: you can store one-off permission grants directly as facts, derive permissions from roles and relationships in rules, and control how permissions become final allow decisions in one place.

Sometimes a user or service needs access to a single action on a resource without holding a broader role—for example, sharing a document read-only with an external guest or issuing a scoped API key.

Declare has_permission as a fact and forward it to allow:

declare has_permission(User, string, Document);
allow(actor, action, resource) if
has_permission(actor, action, resource);

Your application can now grant User:carol read access to Document:handbook by inserting a single fact:

has_permission(User:carol, "read", Document:handbook);

Because the second term binds the exact action string ("read"), that fact grants only "read" on Document:handbook and never grants "edit" or access to other documents.

Funneling multiple models into has_permission

Section titled “Funneling multiple models into has_permission”

In most applications, permissions come from several sources at once: organization roles, resource ownership, and direct shares.

Rather than writing separate allow rules for every access model, write rules that derive has_permission:

declare has_permission(User, string, Document);
declare has_role(User, string, Organization);
declare belongs_to(Document, Organization);
declare is_owner(User, Document);
// 1. Organization viewers can read documents in their organization
has_permission(user: User, "read", document: Document) if
belongs_to(document, org: Organization) and
has_role(user, "viewer", org);
// 2. Document owners can read and edit their documents
has_permission(user: User, "read", document: Document) if
is_owner(user, document);
has_permission(user: User, "edit", document: Document) if
is_owner(user, document);
// 3. Forward all permissions (stored or derived) to allow
allow(actor, action, resource) if
has_permission(actor, action, resource);

Both stored has_permission facts (direct shares) and derived has_permission rules (roles and ownership) now flow through the same allow rule:

Direct grant ------+
Scoped role -------+--> has_permission --> allow
Ownership ---------+

As you add roles, relationships, or hierarchies, each new pattern simply derives has_permission.

Funneling access into has_permission also gives you a single choke point when a universal restriction must apply across every way a permission can be granted (for example, blocking "edit" on archived documents even for owners and direct collaborators).

When adding resource-specific restrictions at the allow boundary, replace the generic allow(actor, action, resource) catch-all rule with resource-qualified allow rules (one per resource type):

// Organization permissions forward directly
allow(user: User, action: string, org: Organization) if
has_permission(user, action, org);
// Documents allow reading freely, but require unarchived state for editing
allow(user: User, "read", document: Document) if
has_permission(user, "read", document);
allow(user: User, "edit", document: Document) if
has_permission(user, "edit", document) and
not is_archived(document);

Qualifying each forwarding rule by resource type (org: Organization, document: Document) is essential: if you instead added a condition like is_published(resource) onto the unqualified allow(actor, action, resource) rule, Verdict would require is_published on every resource type in your policy, breaking checks on Organization or Folder resources.

[!IMPORTANT] Because rules in Verdict are additive, remove the unqualified allow(actor, action, resource) rule when switching to resource-specific forwarding rules. Leaving the catch-all rule in the policy would let edits on archived documents bypass the guard.

Resource-qualified allow rules attach conditions to specific resource actions, like gating edits on unarchived documents. When a restriction must instead block access unconditionally across every resource and action (such as suspending a user or revoking an offboarded user’s access), add an optional deny rule:

declare is_suspended(User);
deny(user: User, action, resource) if
is_suspended(user);

A matching deny rule eliminates any matching allow without requiring you to add suspension checks to each resource forwarding rule or track down every direct grant. Keep deny rule bodies simple, and rely on composing positive paths through has_permission and allow for standard access logic.

  • Group permissions into reusable roles and role hierarchies in Roles.
  • Derive permissions from parent organizations and resource links in Relationships.
  • Propagate permissions down nested folders and teams in Hierarchies & Recursion.
  • Layer property comparisons onto your rules in Attributes.