Skip to content

Roles

Roles group permissions under named labels such as "viewer", "editor", or "admin". Instead of granting individual actions to each user one by one, your application records which role an actor holds on an organization or resource, and your policy maps each role to the permissions it grants.

In most applications, a role is held on a specific organization, folder, or document rather than globally across the entire system. Use a 3-term has_role(actor, role, resource) fact to scope role assignments to a target resource:

declare has_role(User, string, Document);
declare has_role(User, string, Organization);
// Alice is an editor on Document:handbook, and an admin on Organization:acme
has_role(User:alice, "editor", Document:handbook);
has_role(User:alice, "admin", Organization:acme);

Rather than writing separate allow rules for every role, derive has_permission from has_role and forward has_permission to allow:

has_permission(user: User, "read", document: Document) if
has_role(user, "viewer", document);
has_permission(user: User, "read", document: Document) if
has_role(user, "editor", document);
has_permission(user: User, "edit", document: Document) if
has_role(user, "editor", document);
allow(actor, action, resource) if
has_permission(actor, action, resource);

Always bind the action slot in has_permission rules to a concrete string literal ("read", "edit"). Leaving action as an unbound variable on a role rule (has_permission(user, action, document) if has_role(user, "editor", document);) would grant that role every action string your application ever checks against the document.

When higher roles include every permission of lower roles, repeating every lower-tier permission creates boilerplate. Because has_role can appear in both facts and rule heads, you can derive "viewer" directly from "editor":

// Every editor on a document also holds the viewer role on that document
has_role(user: User, "viewer", document: Document) if
has_role(user, "editor", document);
has_permission(user: User, "read", document: Document) if
has_role(user, "viewer", document);
has_permission(user: User, "edit", document: Document) if
has_role(user, "editor", document);

With that rule in place, recording has_role(User:alice, "editor", Document:handbook) automatically grants Alice every permission assigned to "viewer" as well:

test "editors inherit viewer permissions" {
has_role(User:alice, "editor", Document:handbook);
has_role(User:bob, "viewer", Document:handbook);
assert allow(User:alice, "read", Document:handbook);
assert allow(User:alice, "edit", Document:handbook);
assert allow(User:bob, "read", Document:handbook);
assert not allow(User:bob, "edit", Document:handbook);
}