Skip to main content
Attribute-Based Access Control (ABAC) is the right choice when access decisions depend on properties of the user, resource, or environment rather than on static roles or entity relationships alone. Where RBAC asks “what role does this user have?” and ReBAC asks “how is this user related to this resource?”, ABAC asks “do the attributes of this request satisfy these conditions?” In Permify, ABAC is implemented with two schema constructs: attributes (typed properties attached to entities) and rules (boolean functions that evaluate those attributes, optionally against request context).

When to use ABAC

  • Access depends on a property of a resource (e.g., is_public, min_age, region).
  • Access depends on runtime context supplied in the request (e.g., current IP address, day of the week).
  • You need to express numerical ranges or set membership (e.g., balance limits, allowed locations).
  • Compliance rules require evaluating a combination of user, resource, and environmental attributes.
ABAC composes with RBAC and ReBAC in a single Permify schema. You can, for example, require both a valid IP range (ABAC) and organization membership (ReBAC) in the same action.

Core constructs

Attributes

Attributes define typed properties on an entity. Permify supports the following attribute types:
boolean      // true/false
boolean[]    // array of booleans
string       // single string
string[]     // array of strings
integer      // whole number
integer[]    // array of integers
double       // decimal number
double[]     // array of decimals
Declare an attribute inside an entity block:
entity organization {
    attribute ip_range string[]
}

Rules

Rules are named boolean functions written in Common Expression Language (CEL). They accept the entity attribute (and optionally context.data fields from the request) and return true or false.
rule check_ip_range(ip_range string[]) {
    context.data.ip in ip_range
}
A rule is referenced inside an action (or permission) expression:
entity organization {
    relation admin @user
    attribute ip_range string[]

    permission view = check_ip_range(ip_range) or admin
}
Boolean attributes are the one exception: they can be used directly in a permission expression without a wrapping rule, because their value is the condition. All other attribute types require a rule.

Example: public/private content

A boolean attribute can gate access with no rule required.
entity user {}

entity post {
    relation owner @user

    attribute is_public boolean

    permission view = is_public or owner
    permission edit = owner
}
If post:1$is_public|boolean:true, any user can view it. If false, only the owner can.

Example: age-restricted content

An integer attribute combined with a rule enforces a minimum age requirement supplied in the request context.
entity content {
    attribute min_age integer

    permission view = check_age(min_age)
}

rule check_age(min_age integer) {
    context.data.age >= min_age
}
The caller passes the user’s age in the context.data field of the check request. The rule evaluates context.data.age >= min_age at query time.

Example: IP range restriction

This example combines a ReBAC rule (admin role) with an ABAC rule (IP allow-list) using or.
entity user {}

entity organization {
    relation admin @user

    attribute ip_range string[]

    permission view = check_ip_range(ip_range) or admin
}

rule check_ip_range(ip_range string[]) {
    context.data.ip in ip_range
}

Write the attribute

cURL
curl --location --request POST 'localhost:3476/v1/tenants/t1/data/write' \
--header 'Content-Type: application/json' \
--data-raw '{
  "metadata": {
    "schema_version": ""
  },
  "tuples": [
    {
      "entity": { "type": "organization", "id": "1" },
      "relation": "admin",
      "subject": { "type": "user", "id": "1" }
    }
  ],
  "attributes": [
    {
      "entity": { "type": "organization", "id": "1" },
      "attribute": "ip_range",
      "value": {
        "@type": "type.googleapis.com/base.v1.StringArrayValue",
        "data": ["187.182.51.206", "250.89.38.115"]
      }
    }
  ]
}'

Check with context

Pass the user’s current IP address in context.data:
cURL
curl --location --request POST 'localhost:3476/v1/tenants/t1/permissions/check' \
--header 'Content-Type: application/json' \
--data-raw '{
  "metadata": {
    "snap_token": "",
    "schema_version": "",
    "depth": 20
  },
  "entity": { "type": "organization", "id": "1" },
  "permission": "view",
  "subject": { "type": "user", "id": "2" },
  "context": {
    "data": {
      "ip": "187.182.51.206"
    }
  }
}'
Response
{
  "can": "RESULT_ALLOWED"
}
If the IP were not in the ip_range attribute, and user:2 is not an admin, the response would be RESULT_DENIED.

Example: weekday-based access

A string[] attribute holds an allow-list of valid days; the rule checks the request context.
entity user {}

entity organization {
    relation member @user

    attribute valid_weekdays string[]

    permission view = is_weekday(valid_weekdays) and member
}

entity repository {
    relation organization @organization

    permission view = organization.view
}

rule is_weekday(valid_weekdays string[]) {
    context.data.day_of_week in valid_weekdays
}
To view a repository, the current day must be in valid_weekdays and the user must be a member of the owning organization. Both conditions are required.

Example: banking withdrawal limit

A double attribute stores the account balance; the rule enforces both a balance check and a per-transaction cap.
entity user {}

entity account {
    relation owner @user
    attribute balance double

    permission withdraw = check_balance(balance) and owner
}

rule check_balance(balance double) {
    (balance >= context.data.amount) && (context.data.amount <= 5000)
}
A withdrawal is allowed only if the user owns the account and the requested amount does not exceed both the current balance and the 5,000 per-transaction cap.

Hierarchical ABAC

Attribute rules can depend on permissions from parent entities. This lets you express conditions like “a department is viewable only if its budget exceeds 10,000 and its parent organization was founded after 2000”.
entity employee {}

entity organization {
    attribute founding_year integer

    permission view = check_founding_year(founding_year)
}

entity department {
    relation organization @organization
    attribute budget double

    permission view = check_budget(budget) and organization.view
}

rule check_founding_year(founding_year integer) {
    founding_year > 2000
}

rule check_budget(budget double) {
    budget > 10000
}
department.view requires check_budget to pass and the parent organization.view permission to be allowed. Permify evaluates the full chain.
You can reference a parent entity’s permission (e.g., organization.view) from a child entity’s action. You cannot directly reference the parent entity’s attribute (e.g., organization.founding_year) — only permissions traverse entity boundaries.

Building RBAC Systems

Start with static roles before adding attribute-based conditions.

Building ReBAC Systems

Combine relationship traversal with attribute checks for fine-grained rules.

Modeling guide

Full reference for attributes, rules, and all schema DSL constructs.

Testing

Write validation files that assert permission outcomes including context data.

Build docs developers (and LLMs) love