Skip to content

Hierarchies & Recursion

When resources live inside nested folders or users belong to nested teams, store only the direct parent-child links as facts and use recursive rules to propagate access across any number of levels.

Suppose Folder:engineering contains Folder:guides, which contains Document:handbook. Record each direct link with a parent(child, container) fact, then define a recursive is_ancestor predicate with two rules:

declare parent(Folder, Folder);
declare parent(Document, Folder);
// Base case: a direct parent folder is an ancestor
is_ancestor(folder: Folder, resource) if
parent(resource, folder);
// Recursive step: an ancestor of a parent folder is also an ancestor
is_ancestor(ancestor: Folder, resource) if
parent(resource, folder: Folder) and
is_ancestor(ancestor, folder);

Verdict evaluates recursive rules to a fixed point and terminates cleanly even if the stored facts contain a cycle.

Joining is_ancestor with has_role lets you control which folder roles propagate down to descendant folders and documents:

declare has_role(User, string, Folder);
// A viewer on any ancestor folder can read descendant folders and documents
has_permission(user: User, "read", resource) if
is_ancestor(folder: Folder, resource) and
has_role(user, "viewer", folder);
allow(actor, action, resource) if
has_permission(actor, action, resource);
test "folder roles propagate two levels deep" {
parent(Folder:guides, Folder:engineering);
parent(Document:handbook, Folder:guides);
has_role(User:alice, "viewer", Folder:engineering);
assert allow(User:alice, "read", Folder:guides);
assert allow(User:alice, "read", Document:handbook);
assert not allow(User:alice, "edit", Document:handbook);
}

If every permission on a parent folder should propagate directly to its children, you can make has_permission itself recursive without introducing is_ancestor:

has_permission(user: User, "read", resource) if
parent(resource, folder: Folder) and
has_permission(user, "read", folder);

Recursion works the same way on the actor side when users belong to nested teams. If User:alice belongs to Team:platform, and Team:platform is a sub-team of Team:engineering, a recursive team_member predicate derives her membership in the parent team automatically:

team_member(user: User, team: Team) if
direct_member(user, team);
team_member(user: User, parent: Team) if
team_member(user, child: Team) and
parent_team(child, parent);
allow(user: User, "read", folder: Folder) if
team_member(user, team: Team) and
has_role(team, "viewer", folder);