Skip to main content

ABAC Design Patterns

Implement common authorization patterns with attribute-based access control (ABAC) in Permit.io: ownership, group membership, comparisons between two attributes, several condition groups in one set, and dynamic attributes computed with custom Rego code. This page is for developers who know how to create user sets and resource sets, and want a recommended way to model a specific rule. To create user sets and resource sets, see Building your first ABAC policy.

Prerequisites

  • A Permit.io account with a project and an environment. See the Quickstart.
  • A resource with the actions you want to grant. See Create the bicycle resource.
  • A container policy decision point (PDP) running at http://localhost:7766 for the checks on this page. The Cloud PDP doesn't evaluate ABAC policies. See Cloud PDP capabilities.

Every JavaScript example on this page uses this client, and userKey is the key of the user who makes the request. Replace <YOUR_API_KEY> with your environment API key:

import { Permit } from "permitio";

const permit = new Permit({
token: "<YOUR_API_KEY>",
pdp: "http://localhost:7766",
});

Ownership

With ownership, a user can perform actions only on the resources they own. You can implement ownership in Permit in three ways with the Policy Editor, or write the policy as code. The three ways are equally valid:

ApproachWhere ownership is storedUse when
Ownership via tenantsThe tenant of the user and of the resourceEach owner already maps to a tenant
Ownership via a list on the resourceA resource attribute that lists the owner keysYour application knows a resource's owners when it runs the check
Ownership via a list on the user profileA user attribute that lists the resource keys the user ownsYour application stores ownership on the user

To model ownership with relationship-based access control (ReBAC) instead, see Model ownership.

Ownership via tenants

A user owns a resource when the user and the resource belong to the same tenant. This pattern needs no ABAC condition set: Permit scopes every role assignment to a tenant, so a role-based access control (RBAC) role grants its permissions only inside the tenant it was assigned in. Pass the tenant in permit.check(), and the check allows the action only when the user holds a role that permits the action in that tenant.

To learn how tenants isolate users and resources, see Multi-tenant authorization. To assign a role in one tenant, see Create a user and assign the Admin role.

Ownership via list on resource

The resource has an attribute that lists the keys of the users who own it. A resource set checks whether the list contains the key of the user who makes the request.

  1. Add an array attribute to the resource, for example owner.
  2. Create a resource set with the condition resource.owner array contains (ref) user.key.
  3. Grant the resource set its actions in the Policy Editor.

Dynamic Resource (Resource Set) form named owned_file for the file resource type, with the condition resource.owner array contains (ref) user.key

To check permissions, pass the owner list as a resource attribute in permit.check(). In the following example, userKey is the key of the user who makes the request:

const permitted = await permit.check(userKey, "read", {
type: "file", // The resource name
attributes: {
owner: [
"d08c85a349994aeb89a3f02c08bdb340", // user-1
"48fb889360604253a5189580b48694cf", // user-2
],
},
});

The check returns true when userKey is one of the keys in owner.

Ownership via list on user profile

The user has an attribute that lists the keys of the resources the user owns. A user set checks whether that list contains the key of the requested resource, which your application passes as a resource attribute.

  1. Add an array attribute to the user, for example owned_files, and store the keys of the resources the user owns.
  2. Add an attribute to the resource, for example id, that your application fills with the key of the resource it checks.
  3. Create a user set with the condition user.owned_files array contains (ref) resource.id.
  4. Grant the user set its actions in the Policy Editor.

To check permissions, pass both attributes in permit.check():

const permitted = await permit.check(
{
key: "john@permit.io",
attributes: { owned_files: ["readme", "budget"] },
},
"read",
{
type: "file",
attributes: { id: "readme" },
}
);

The check returns true, because owned_files contains readme. With id: "roadmap", the same check returns false.

A user set matches a user in every tenant. To keep this pattern inside one tenant, add the conditions described in the tenant boundary warning of Create the Full-time Stanford Student user set.

Belonging to a group

Permit supports groups natively with ReBAC. To assign roles to a group instead of to each member, use the Groups API or read the ReBAC overview.

You can also represent a group with ABAC, as a tenant or as an attribute of users and resources.

Group by tenants

Use tenants as groups, so several tenants together form a higher-level group. Group the tenants with tenant attributes.

Group by attributes

Assign users and resources to a group with an attribute such as group_id or group_name. The implementation follows the ownership via a list on the resource pattern: compare the group attribute of the resource with the group attribute of the user.

Compare a resource attribute with a user attribute

A condition can compare an attribute with another attribute instead of with a fixed value. The Policy Editor marks these operators with (ref), and the policy decision point (PDP) resolves the referenced attribute when it evaluates the check.

For example, to allow an action only when the risk level of the resource is at or below the risk level the user is cleared for:

  1. Add a risk attribute of type Number to the resource, and a risk attribute of type Number to the user.
  2. Create a resource set on that resource type with the condition resource.risk less-than-equals (ref) user.risk.
  3. Grant the resource set its actions in the Policy Editor.

Dynamic Resource (Resource Set) form named Risk for the Route resource type, with the condition resource.risk less-than-equals (ref) user.risk

Give both attributes a value of the type the operator accepts. Comparison operators lists the operand type of each operator, and Compare two attributes with a reference lists the operators that accept a reference.

Combine several condition groups

A condition set can hold several condition groups. Conditions inside a group are joined by the operator you select on each row (and or or), and the groups are joined by the operator you select between them.

  1. In the user set or resource set form, add the conditions of the first group, and select and or or on each row after the first.
  2. Click Add Condition Group to add another group.
  3. Select the operator between the groups.

Condition set form with three condition groups, each holding three conditions joined by and or or, the groups joined by or, and an Add Condition Group button

For the JSON that the API stores for these groups, see Build ABAC conditions.

Compute dynamic attributes with custom Rego code

Add custom Open Policy Agent (OPA) Rego code to compute dynamic attributes that your ABAC policies use like any other attribute.

How dynamic attributes work

  1. You write custom Rego code that generates attributes.
  2. You use the generated attributes in user set and resource set conditions, as you use stored attributes.
  3. The policy decision point (PDP) runs your custom code with the ABAC code that Permit generates.

Example: Creating a dynamic intersection attribute

The following example computes a has_intersection user attribute that is true when a list attribute on the user and a list attribute on the resource share at least one value.

Add the code to the custom/ directory of your GitOps repository:

package permit.custom
import future.keywords.in

# Default value if no conditions are met
default custom_user_attributes["has_intersection"] = false

# Set to true if there's an intersection between user and resource attributes
custom_user_attributes["has_intersection"] := true {
# Convert attributes to sets for intersection testing
user_attr_set = {x | some x in data.users[input.user.key].attributes["attr_in_user"]}
res_attr_set = {x | some x in input.resource.attributes["attr_in_resource"]}
# Check if there's at least one common value
count(user_attr_set & res_attr_set) > 0
}

The code reads attr_in_user from the stored attributes of the user, and attr_in_resource from the resource attributes in the check input.

Requirements for custom attribute code
  • The package name must be permit.custom.
  • The rule name must be custom_user_attributes.
  • There are no other requirements. The rule body can contain any Rego logic.

Use the has_intersection attribute in a condition

Use user.has_intersection in a user set condition, and pass the resource attribute in the check:

const permitted = await permit.check(userKey, "read", {
type: "document",
attributes: {
attr_in_resource: ["tag1", "tag2"],
},
});

The check returns true when the user's attr_in_user list contains tag1 or tag2, and the user set with the has_intersection condition has read permission on document.

Test the custom Rego code

  1. Test in the OPA Playground. Open this example in the OPA Playground to iterate on the Rego logic without a PDP.

  2. Test with a local PDP. Run a PDP container. Replace permit_key_XXXXX with your environment API key. The command maps the PDP API to port 7767 on your machine, and the OPA API to port 8181.

    docker run -it -p 7767:7000 -p 8181:8181 -e PDP_API_KEY=permit_key_XXXXX permitio/pdp-v2:latest

    With the PDP running, you can:

    • Send POST requests with your input data to http://localhost:8181/v1/data/permit, and read the raw output, including policy debug data.
    • Update a custom policy directly at http://localhost:8181/v1/policies/custom/example.rego.
Development workflow
  1. Iterate on the Rego logic in the OPA Playground.
  2. Test with a local PDP and your real policy data.
  3. Commit the code to your GitOps repository.

Next steps