ABAC Policies
Attribute-based access control policies let you define who can do what, on which resources, and under which conditions. This guide covers the full policy structure, condition operators, and how the evaluation engine works.
What is a policy?
A policy is a rule the engine evaluates every time a user attempts an action. It has three parts:
- Matching — does this policy apply to this resource and action? Determined by
resource_patternandactions. - Conditions — given that it applies, are the extra conditions met? (user role, IP, time of day, custom attributes, etc.)
- Effect — if everything matches, does the policy
allowordenyaccess?
Every policy belongs to a tenant_id. It can optionally be scoped to a specific application via application_id.
Creating a policy
POST /api/v1/tenants/{tenant_id}/policies
{
"name": "admin-full-access",
"display_name": "Full Admin Access",
"description": "Ops team only",
"effect": "allow",
"priority": 100,
"resource_pattern": "users:*",
"actions": ["read", "write", "delete"],
"conditions": {
"subject.roles": { "in": ["admin"] }
},
"status": "active"
}
Fields
| Field | Type | Description | |
|---|---|---|---|
name | string | required | Unique within the tenant. Between 1 and 100 characters. |
display_name | string | optional | Human-readable label for the UI. Max 255 chars. Not used in evaluation. |
description | string | optional | Free-form text. No length limit. |
effect | "allow" | "deny" | required | allow grants access. deny blocks with absolute priority over any allow. |
priority | integer | optional · default 0 | Higher number = evaluated first. Any integer, including negative. Policies with equal priority have no guaranteed order. |
resource_pattern | string | required | Glob pattern for the resource. Max 255 chars. See resource_pattern. |
actions | string[] | required | Actions this policy applies to. Use ["*"] or [] for any action. |
conditions | object | null | optional | Extra conditions (all must pass — AND). null, undefined, or {} means the policy always fires when resource and action match. See Conditions. |
status | string | optional · default "active" | active — evaluated normally. inactive — completely ignored. testing — evaluated but result does not affect real access (safe to test before activating). |
resource_pattern
The engine compares resource_pattern against the resource type in the evaluation context. There are exactly four matching forms:
| Pattern | Matches | Example |
|---|---|---|
"*" | Any resource, no exceptions. | "*" → everything |
"prefix/*" | Any resource starting with prefix/. The slash is part of the prefix. | "groups/admin/*" → "groups/admin/ops" |
"prefix:*" | Any resource starting with prefix:. The colon is part of the prefix. | "users:*" → "users:read", "users:delete" |
| exact | Only if the resource string is identical to the pattern. | "oauth_client" → only "oauth_client" |
resource_patterndoes not support?, character ranges, or advanced glob syntax — only these four forms. A pattern like"user*"(no:or/) only matches the literal string"user*".
actions
A policy applies to an action if any of these is true:
actionsis an empty array ([]) → applies to any action.actionscontains"*"→ applies to any action.actionscontains the exact action string (case-sensitive).
Actions are arbitrary strings. The system imposes no fixed vocabulary — you define what strings you use ("read", "publish", "impersonate", etc.) both when creating policies and when calling the evaluate endpoint.
Conditions
The conditions field is a JSON object where each key is a context field path and its value is an object with one or more operators. All conditions are evaluated with AND — every one must pass for the policy to fire.
// Canonical form: field → { operator: value }
{
"subject.roles": { "in": ["admin", "auditor"] },
"environment.ip": { "in_cidr": "10.0.0.0/8" },
"environment.hour": {
"gte": "09",
"lte": "17"
}
}
// Shorthand: a direct value is treated as "equals"
{
"subject.id": "some-uuid"
}
Multiple operators on the same field are also ANDed together.
Available context fields
SUBJECT — the authenticated user
| Field | Type | Description |
|---|---|---|
subject.id | uuid | UUID of the authenticated user. |
subject.tenant_id | uuid | UUID of the tenant the user belongs to. |
subject.roles | string[] | List of role names assigned to the user. Also accessible as subject.role. Use with in or contains. |
subject.groups | string[] | List of group names the user belongs to. |
subject.permissions | string[] | Flattened permission strings from the user's roles (e.g. "users:read"). |
subject.<attribute> | string | Any custom attribute on the user. Example: subject.department, subject.clearance_level. |
RESOURCE — the resource being acted on
| Field | Type | Description |
|---|---|---|
resource.type | string | Resource type. Example: "user", "oauth_client", "group". |
resource.id | uuid | UUID of the specific resource instance, if provided in context. |
resource.owner_id | uuid | UUID of the resource owner. Also accessible as resource.owner. |
resource.tenant_id | uuid | UUID of the tenant that owns the resource. |
resource.<attribute> | string | Any custom attribute on the resource. |
ENVIRONMENT — request context
| Field | Type | Description |
|---|---|---|
environment.ip | ip | Client IP address. Also accessible as environment.ip_address. Use with in_cidr. |
environment.user_agent | string | Raw User-Agent header. Use with contains, starts_with, or matches. |
environment.time | string | RFC3339 timestamp of the request (UTC). Use between with ISO strings for ranges. |
environment.hour | string | Hour of day in UTC, zero-padded: "00"–"23". |
environment.day | string | Day of week, lowercase: "monday" … "sunday". Also accessible as environment.weekday. |
environment.<attribute> | string | Custom environment attributes. Example: environment.country. |
ACTION — the attempted action
| Field | Type | Description |
|---|---|---|
action | string | The action being attempted. Useful in policies with resource_pattern: "*" that need to refine by action inside conditions. |
Operators
| Operator | Aliases | Expects | Behavior |
|---|---|---|---|
equals | eq | scalar | Exact equality. Case-sensitive for strings. |
not_equals | ne, neq | scalar | Inverse of equals. |
in | — | array | Expected value must be an array. If the field is an array (e.g. subject.roles): true if any element of the field is in the expected array. If the field is a scalar: true if that scalar is in the array. |
not_in | nin | array | Inverse of in. |
contains | — | scalar | If field is a string: true if it contains the given substring. If field is an array: true if it contains the given element. |
starts_with | — | scalar | Strings only. True if the field starts with the given prefix. |
ends_with | — | scalar | Strings only. True if the field ends with the given suffix. |
gt | greater_than | number | field > value. Returns false (not an error) if either side is not a number. |
lt | less_than | number | field < value. |
gte | greater_than_or_equals | number | field >= value. |
lte | less_than_or_equals | number | field <= value. |
between | — | [min, max] | Inclusive range. Exactly two elements. Works for both numbers and strings (lexicographic). field >= min && field <= max. |
in_cidr | — | cidr or cidr | The field (an IP address) must belong to the given CIDR range. Accepts a single string ("10.0.0.0/8") or an array of CIDR strings. True if the IP falls in any of them. Full IPv4 subnet mask arithmetic. |
matches | regex | regex string | Field must be a string. Value is a regular expression. Returns an API error if the pattern is invalid. |
Condition examples
Only users with role admin or auditor:
{
"subject.roles": { "in": ["admin", "auditor"] }
}
Internal network only, on weekdays:
{
"environment.ip": { "in_cidr": "10.0.0.0/8" },
"environment.day": { "in": ["monday","tuesday","wednesday","thursday","friday"] }
}
Multiple operators on one field (all must pass):
{
"environment.hour": { "gte": "09", "lte": "17" }
}
Custom user attributes:
{
"subject.department": { "equals": "finance" },
"subject.clearance_level": { "gte": 3 },
"environment.ip": { "in_cidr": "10.0.0.0/8" }
}
Multiple CIDR ranges:
{
"environment.ip": {
"in_cidr": ["192.168.0.0/16", "172.16.0.0/12", "10.0.0.0/8"]
}
}
Evaluation engine
When a client requests access, the engine runs this exact algorithm:
- Load active policies. Fetch all policies for the tenant with
status = activeanddeleted_at IS NULLwhoseresource_patternandactionsmatch the context resource and action. - No policies found → default deny. Response includes
"reason": "No matching policy found (default deny)". The system is deny-by-default: if nothing explicitly allows, access is denied. - Sort by priority descending. Higher number is evaluated first. Equal-priority policies have no guaranteed relative order.
- Evaluate conditions. For each policy in order: check that all conditions pass (AND). If any condition fails, that policy is skipped and the engine moves to the next.
- First Deny wins immediately. If a policy matches and its
effectisdeny, evaluation stops right there. No further policies are checked. - Allow is recorded but does not stop evaluation. If a policy matches and is
allow, it's added tomatched_policiesand the engine continues. A Deny policy later in the sorted list can still win. - Final result. After the loop: if at least one Allow matched (and no Deny was hit),
allowed: true. If nothing matched at all,allowed: false(default deny).
Key rule: Deny always wins over Allow. To block an exception, give the Deny a higher priority than the Allows you want it to override.
Evaluation response
{
"allowed": true,
"effect": "allow",
"matched_policies": [
{
"policy_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"policy_name": "admin-full-access",
"effect": "allow",
"priority": 100
}
],
"reason": null
}
Evaluating a context
Send a complete context to the evaluate endpoint. You construct this in your backend on every access check.
POST /api/v1/tenants/{tenant_id}/policies/evaluate
{
"subject": {
"id": "uuid-of-the-user",
"tenant_id": "uuid-of-the-tenant",
"roles": ["admin", "viewer"],
"groups": ["ops-team"],
"permissions": ["users:read", "users:write"],
"attributes": { "department": "engineering" }
},
"resource": {
"resource_type": "user",
"resource_id": "uuid-of-the-resource",
"owner_id": "uuid-of-the-owner",
"tenant_id": "uuid-of-the-tenant",
"attributes": { "sensitivity": "high" }
},
"action": "write",
"environment": {
"ip_address": "10.0.0.42",
"user_agent": "Mozilla/5.0 ...",
"time": "2026-06-24T14:30:00Z",
"attributes": { "country": "MX" }
}
}
All fields inside resource and environment are optional. If time is omitted it defaults to the current server time.
Endpoints
| Method | Path | Description |
|---|---|---|
GET | /api/v1/tenants/{tenant_id}/policies | List policies (paginated) |
POST | /api/v1/tenants/{tenant_id}/policies | Create a policy |
GET | /api/v1/tenants/{tenant_id}/policies/{policy_id} | Get a policy by ID |
POST | /api/v1/tenants/{tenant_id}/policies/evaluate | Evaluate a full context |
GET | /api/v1/tenants/{tenant_id}/users/{user_id}/check-access | Quick allow/deny check |
GET | /api/v1/abac/conditions-schema | Conditions schema for UI editors |
Full examples
Full access for superadmins
{
"name": "superadmin-all",
"effect": "allow",
"priority": 1000,
"resource_pattern": "*",
"actions": ["*"],
"conditions": {
"subject.roles": { "in": ["superadmin"] }
}
}
Read-only on weekdays
{
"name": "read-only-weekdays",
"effect": "allow",
"priority": 50,
"resource_pattern": "*",
"actions": ["read"],
"conditions": {
"environment.day": {
"in": ["monday","tuesday","wednesday","thursday","friday"]
}
}
}
Finance reports — scoped by department and seniority
{
"name": "finance-reports-senior",
"effect": "allow",
"priority": 200,
"resource_pattern": "reports/finance/*",
"actions": ["read", "export"],
"conditions": {
"subject.department": { "equals": "finance" },
"subject.clearance_level": { "gte": 3 },
"environment.ip": { "in_cidr": "10.0.0.0/8" }
}
}
Block delete for contractors
{
"name": "block-delete-contractors",
"effect": "deny",
"priority": 800,
"resource_pattern": "*",
"actions": ["delete"],
"conditions": {
"subject.groups": { "in": ["contractors"] }
}
}
Testing a new role before going live
{
"name": "new-role-experiment",
"effect": "allow",
"priority": 10,
"resource_pattern": "documents:*",
"actions": ["read"],
"conditions": {
"subject.roles": { "in": ["new-experimental-role"] }
},
"status": "testing"
}