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
| Field | Type | Description |
|---|---|---|
id | string | Unique rule identifier |
effect | 'allow' | 'deny' | Whether the rule grants or denies access |
permissions | string[] | Which permissions this rule applies to |
roles | string[] | Limit to specific roles |
plans | string[] | Limit to specific plans |
environments | string[] | Limit to specific environments |
condition | (ctx) => boolean | Custom condition function |
priority | number | Higher = evaluated first |
Evaluation Order
- Rules are sorted by priority (highest first)
- The first matching rule determines the outcome
- If policies exist but none match → denied (fail-closed)
- 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
| Operator | Description |
|---|---|
equals | Strict equality |
notEquals | Strict inequality |
in | Value is in array |
notIn | Value is not in array |
includes | Array includes value |
greaterThan | Numeric greater than |
lessThan | Numeric less than |
greaterThanOrEqual | Numeric greater than or equal |
lessThanOrEqual | Numeric less than or equal |
exists | Field 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:
- RBAC grants base permissions from roles
- Policies can allow access the role doesn't explicitly have
- 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."