Skip to main content

Policies & Composition

Policies are serializable discriminated unions composed through algebraic combinators. Every policy is plain JSON data -- no callbacks, no closures, no hidden state.

Policy as Data

A PolicyConstraint is a discriminated union with a _tag field identifying the policy kind. This design enables:

  • Serialization -- policies round-trip through JSON without loss
  • Introspection -- you can walk the policy tree programmatically
  • Explanation -- human-readable descriptions are derived from data
  • Storage -- persist policies in databases, transmit over networks

Leaf Combinators

Leaf policies check a single condition against an AuthSubject.

hasPermission(permission)

Checks that the subject has a specific permission in their permission set.

import { hasPermission } from "@hex-di/guard";

const canRead = hasPermission(ReadUsers);

hasRole(role)

Checks that the subject has a specific role.

import { hasRole } from "@hex-di/guard";

const isAdmin = hasRole(AdminRole);

hasAttribute(key, value)

Checks that a subject attribute matches a specific value.

import { hasAttribute } from "@hex-di/guard";

const isActive = hasAttribute("status", "active");

hasResourceAttribute(key, value)

Checks a resource-level attribute from the EvaluationContext.

import { hasResourceAttribute } from "@hex-di/guard";

const isPublic = hasResourceAttribute("visibility", "public");

hasSignature(signatureType)

Checks that an electronic signature of the specified type is present in the evaluation context.

import { hasSignature } from "@hex-di/guard";

const requiresApproval = hasSignature("approval");

hasRelationship(relationship)

Checks that the subject has a specific relationship to the resource.

import { hasRelationship } from "@hex-di/guard";

const isOwner = hasRelationship("owner");

Logical Combinators

Logical combinators compose policies into trees.

allOf(...policies)

All sub-policies must grant. Short-circuits on the first denial.

import { allOf } from "@hex-di/guard";

const canEdit = allOf(hasPermission(WriteUsers), hasAttribute("status", "active"));

anyOf(...policies)

At least one sub-policy must grant. Short-circuits on the first grant.

import { anyOf } from "@hex-di/guard";

const canAccess = anyOf(hasRole(AdminRole), hasPermission(ReadUsers));

not(policy)

Inverts the decision of a sub-policy.

import { not } from "@hex-di/guard";

const notSuspended = not(hasAttribute("status", "suspended"));

withLabel(label, policy)

Attaches a human-readable label to a policy node for debugging and audit trail readability.

import { withLabel } from "@hex-di/guard";

const policy = withLabel(
"Can edit active users",
allOf(hasPermission(WriteUsers), hasAttribute("status", "active"))
);

anyOfRoles(...roles)

Shorthand for anyOf(hasRole(r1), hasRole(r2), ...).

import { anyOfRoles } from "@hex-di/guard";

const isStaff = anyOfRoles(AdminRole, EditorRole, ModeratorRole);

Policy Composition Tree

Composed policies form a tree structure. The evaluator walks the tree depth-first, short-circuiting where possible.

This tree represents:

allOf(
anyOf(hasRole("Admin"), hasPermission(ReadUsers)),
not(hasAttribute("status", "suspended")),
hasPermission(WriteUsers)
);

Short-Circuit Evaluation

  • allOf stops at the first deny -- remaining sub-policies are not evaluated
  • anyOf stops at the first allow -- remaining sub-policies are not evaluated
  • not always evaluates its single child

This means policy ordering can affect performance but never correctness. Place cheap checks (permission lookups) before expensive ones (attribute resolution).

Combinator Reference

CombinatorDescription
hasPermission(p)Subject has permission p
hasRole(r)Subject has role r
hasAttribute(key, value)Subject attribute matches
hasResourceAttribute(key, value)Resource attribute matches
hasSignature(type)Electronic signature present
hasRelationship(rel)Subject-resource relationship
allOf(...policies)All policies must grant
anyOf(...policies)At least one policy must grant
not(policy)Inverts the decision
withLabel(label, policy)Annotate with label
anyOfRoles(...roles)Any of the listed roles

Field-Level Visibility

Policies can control which fields of a resource are visible using the fields option and fieldStrategy.

const policy = hasPermission(ReadUsers, {
fields: ["name", "email"],
fieldStrategy: "include", // only these fields are visible
});

When evaluation grants access, the Decision includes a visibleFields set that the enforcement layer can use to filter response data.