vaultaris /docs

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:

  1. Matching — does this policy apply to this resource and action? Determined by resource_pattern and actions.
  2. Conditions — given that it applies, are the extra conditions met? (user role, IP, time of day, custom attributes, etc.)
  3. Effect — if everything matches, does the policy allow or deny access?

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

FieldTypeDescription
namestringrequiredUnique within the tenant. Between 1 and 100 characters.
display_namestringoptionalHuman-readable label for the UI. Max 255 chars. Not used in evaluation.
descriptionstringoptionalFree-form text. No length limit.
effect"allow" | "deny"requiredallow grants access. deny blocks with absolute priority over any allow.
priorityintegeroptional · default 0Higher number = evaluated first. Any integer, including negative. Policies with equal priority have no guaranteed order.
resource_patternstringrequiredGlob pattern for the resource. Max 255 chars. See resource_pattern.
actionsstring[]requiredActions this policy applies to. Use ["*"] or [] for any action.
conditionsobject | nulloptionalExtra conditions (all must pass — AND). null, undefined, or {} means the policy always fires when resource and action match. See Conditions.
statusstringoptional · 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:

PatternMatchesExample
"*"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"
exactOnly if the resource string is identical to the pattern."oauth_client" → only "oauth_client"

resource_pattern does 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:

  • actions is an empty array ([]) → applies to any action.
  • actions contains "*" → applies to any action.
  • actions contains 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

FieldTypeDescription
subject.iduuidUUID of the authenticated user.
subject.tenant_iduuidUUID of the tenant the user belongs to.
subject.rolesstring[]List of role names assigned to the user. Also accessible as subject.role. Use with in or contains.
subject.groupsstring[]List of group names the user belongs to.
subject.permissionsstring[]Flattened permission strings from the user's roles (e.g. "users:read").
subject.<attribute>stringAny custom attribute on the user. Example: subject.department, subject.clearance_level.

RESOURCE — the resource being acted on

FieldTypeDescription
resource.typestringResource type. Example: "user", "oauth_client", "group".
resource.iduuidUUID of the specific resource instance, if provided in context.
resource.owner_iduuidUUID of the resource owner. Also accessible as resource.owner.
resource.tenant_iduuidUUID of the tenant that owns the resource.
resource.<attribute>stringAny custom attribute on the resource.

ENVIRONMENT — request context

FieldTypeDescription
environment.ipipClient IP address. Also accessible as environment.ip_address. Use with in_cidr.
environment.user_agentstringRaw User-Agent header. Use with contains, starts_with, or matches.
environment.timestringRFC3339 timestamp of the request (UTC). Use between with ISO strings for ranges.
environment.hourstringHour of day in UTC, zero-padded: "00""23".
environment.daystringDay of week, lowercase: "monday""sunday". Also accessible as environment.weekday.
environment.<attribute>stringCustom environment attributes. Example: environment.country.

ACTION — the attempted action

FieldTypeDescription
actionstringThe action being attempted. Useful in policies with resource_pattern: "*" that need to refine by action inside conditions.

Operators

OperatorAliasesExpectsBehavior
equalseqscalarExact equality. Case-sensitive for strings.
not_equalsne, neqscalarInverse of equals.
inarrayExpected 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_inninarrayInverse of in.
containsscalarIf field is a string: true if it contains the given substring. If field is an array: true if it contains the given element.
starts_withscalarStrings only. True if the field starts with the given prefix.
ends_withscalarStrings only. True if the field ends with the given suffix.
gtgreater_thannumberfield > value. Returns false (not an error) if either side is not a number.
ltless_thannumberfield < value.
gtegreater_than_or_equalsnumberfield >= value.
lteless_than_or_equalsnumberfield <= value.
between[min, max]Inclusive range. Exactly two elements. Works for both numbers and strings (lexicographic). field >= min && field <= max.
in_cidrcidr or cidrThe 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.
matchesregexregex stringField 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:

  1. Load active policies. Fetch all policies for the tenant with status = active and deleted_at IS NULL whose resource_pattern and actions match the context resource and action.
  2. 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.
  3. Sort by priority descending. Higher number is evaluated first. Equal-priority policies have no guaranteed relative order.
  4. 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.
  5. First Deny wins immediately. If a policy matches and its effect is deny, evaluation stops right there. No further policies are checked.
  6. Allow is recorded but does not stop evaluation. If a policy matches and is allow, it's added to matched_policies and the engine continues. A Deny policy later in the sorted list can still win.
  7. 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

MethodPathDescription
GET/api/v1/tenants/{tenant_id}/policiesList policies (paginated)
POST/api/v1/tenants/{tenant_id}/policiesCreate a policy
GET/api/v1/tenants/{tenant_id}/policies/{policy_id}Get a policy by ID
POST/api/v1/tenants/{tenant_id}/policies/evaluateEvaluate a full context
GET/api/v1/tenants/{tenant_id}/users/{user_id}/check-accessQuick allow/deny check
GET/api/v1/abac/conditions-schemaConditions 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"
}