Policies & ABAC

Attribute-Based Access Control (ABAC) lets you make fine-grained access decisions based on user attributes, resource properties, and environmental context — not just roles.

Defining Policy Rules

Add policies to your config:

const config = defineAccess({
  roles: ["admin", "editor", "viewer"] as const,
  permissions: {
    admin: ["*"],
    editor: ["documents:read", "documents:write"],
    viewer: ["documents:read"],
  },
  policies: [
    {
      id: "owner-can-edit",
      effect: "allow",
      permissions: ["documents:write"],
      condition: (ctx) => ctx.resource?.ownerId === ctx.user.id,
      priority: 10,
    },
    {
      id: "block-outside-hours",
      effect: "deny",
      permissions: ["documents:write"],
      environments: ["production"],
      condition: (ctx) => {
        const hour = new Date().getHours();
        return hour < 6 || hour > 22; // block writes outside 6am–10pm
      },
      priority: 20,
    },
    {
      id: "enterprise-only-export",
      effect: "allow",
      permissions: ["documents:export"],
      plans: ["enterprise"],
      priority: 5,
    },
  ],
});

Policy Rule Structure

FieldTypeDescription
idstringUnique rule identifier
effect'allow' | 'deny'Whether the rule grants or denies access
permissionsstring[]Which permissions this rule applies to
rolesstring[]Limit to specific roles
plansstring[]Limit to specific plans
environmentsstring[]Limit to specific environments
condition(ctx) => booleanCustom condition function
prioritynumberHigher = evaluated first

Evaluation Order

  1. Rules are sorted by priority (highest first)
  2. The first matching rule determines the outcome
  3. If policies exist but none matchdenied (fail-closed)
  4. If no policies are defined → access is determined by RBAC alone

Using the Condition Engine

For declarative conditions (without writing functions), use the condition engine:

import {
  evaluateCondition,
  evaluateConditions,
  buildConditionContext,
} from "react-access-engine";

// Simple condition
const isOwner = evaluateCondition(
  { field: "resource.ownerId", operator: "equals", value: "user-42" },
  { resource: { ownerId: "user-42" } },
);

// Composite AND/OR
const canAccess = evaluateConditions(
  [
    { field: "user.plan", operator: "in", value: ["pro", "enterprise"] },
    {
      or: [
        { field: "user.roles", operator: "includes", value: "admin" },
        { field: "resource.isPublic", operator: "equals", value: true },
      ],
    },
  ],
  context,
);

Built-In Operators

OperatorDescription
equalsStrict equality
notEqualsStrict inequality
inValue is in array
notInValue is not in array
includesArray includes value
greaterThanNumeric greater than
lessThanNumeric less than
greaterThanOrEqualNumeric greater than or equal
lessThanOrEqualNumeric less than or equal
existsField is not null/undefined

Custom Operators

Register custom operators via the plugin system:

import { createOperatorPlugin } from "react-access-engine";

const geoPlugin = createOperatorPlugin([
  {
    name: "withinRegion",
    evaluate: (fieldValue, targetValue) => {
      // Custom logic: check if user's region is in allowed regions
      return targetValue.includes(fieldValue);
    },
  },
]);

const config = defineAccess({
  // ...
  plugins: [geoPlugin],
});

Using usePolicy Hook

function DocumentEditor({ documentId, ownerId }) {
  const { allowed, matchedRule, reason } = usePolicy("documents:write", {
    ownerId,
    documentId,
  });

  if (!allowed) {
    return <p>Access denied: {reason}</p>;
  }

  return <Editor />;
}

Combining RBAC + ABAC

RBAC and ABAC work together:

  1. RBAC grants base permissions from roles
  2. Policies can allow access the role doesn't explicitly have
  3. Policies can deny access even if the role allows it

A policy deny always overrides an RBAC allow — this is how you implement restrictions like "editors can write, but not during maintenance windows."