ABAC

Whereas RBAC allows you to group users and permissions into predefined buckets, you may also want to represent fine-grained or dynamic permissions based on who the user is and her relation to the resource she wants to access. This is known as attribute-based access control (ABAC).

ABAC Basics

Suppose we want to allow employees to view their own expenses.

We can register our user classes with oso:

abac.py
@polar_class
class User:
    def __init__(self, name, location: str = None):

We can do the same with the resources being requested:

abac.py
@polar_class(from_polar="by_id")
class Expense:
abac.rb
OSO ||= Oso.new

class User
  ...
end

OSO.register_class(User)

We can do the same with the resources being requested:

abac.rb
class Expense
  ...
end

OSO.register_class(Expense)
User.Java
public class User {
    ...
    public static void main(String[] args) {
        oso.registerClass(User.class);
    }
}

We can do the same with the resources being requested:

Expense.Java
public class Expense {
    ...
    public static void main(String[] args) {
        oso.registerClass(Expense.class);
    }
}

An allow rule that checks that the user reading the expense is the same person who submitted the expense, would look like:

abac.polar
allow(actor: User, "view", resource: Expense) if
    resource.submitted_by = actor.name;

This simple example shows the potential for ABAC: we took an intuitive concept of “can see their own expenses” and represented it as a single comparison.

The power of ABAC comes from being able to express these kind of permissions based on who you are and how you are related to the data.

ABAC ❤️ RBAC

As alluded to in the summary on RBAC, provisioning access based on checking whether a user has a particular role is technically a simple variant of ABAC. Putting aside whether this is a relevant distinction, the two are closely related.

The power of RBAC comes from: adding some form of organization to the limitless distinct permissions that a person might have, and exposing those in an intuitive, human-understandable way.

Combine this with what ABAC does best: representing relations between a user and the data, and you get intuitive, but fine-grained permissions. For example, suppose our company has taken off and now spans multiple locations, and now accountants can only view expenses from their own locations. We can combine our previous roles with some simple ABAC conditions to achieve this:

abac.polar
# Accountants can view expenses from their location
allow(actor: User, "view", resource: Expense) if
    role(actor, "accountant") and
    actor.location = resource.location;

This is great when what we need is an intersection of models, and you want to apply both RBAC and ABAC policies simultaneously. However, the ABAC model can be even more powerful when composed with roles. And having the roles themselves include attributes.

For example, an employee might be an administrator of a project, and therefore is allowed to see all expenses related to that project.

abac.polar
# Alice is an admin of Project 1
role(_: User { name: "alice" }, "admin", _: Project { id: 1 });

# Project admins can view expenses of the project
allow(actor: User, "view", resource: Expense) if
    role(actor, "admin", new Project { id: resource.project_id });

What we can see is happening here, is that we are associated roles not just globally to a user, but to a user for some specific resource. Other examples might be team-, or organization- specific roles.

And these can also follow inheritance patterns like we saw with regular roles.

abac.polar
# Bhavik is an admin of ACME
role(_: User { name: "bhavik" }, "admin",  _: Organization { name: "ACME" });

# Team roles inherit from Organization roles
role(actor: User, role, team: Team) if
    role(actor, role, new Organization { id: team.organization_id });

# Project roles inherit from Team roles
role(actor: User, role, project: Project) if
    role(actor, role, new Team { id: project.team_id });

Hierarchies

Up to this point, we’ve made a big deal about ABAC being able to represent relations between users and resources. In the previous example, we even showed how relations between resources permits creating inheritance logic. To expand on that idea, here we look at representing organizational hierarchies in our policy.

Starting out with a simple example, suppose managers can view employees’ expenses:

abac.polar
allow(actor: User, "view", resource: Expense) if
    employee = actor.employees and
    employee.name = resource.submitted_by;

First thing we can do, is extract out the logic for checking whether the user manages someone:

abac.polar
allow(actor: User, "view", resource: Expense) if
    manages(actor, employee) and
    employee.name = resource.submitted_by;

Now if we want this logic to apply for managers, and managers’ managers, and so on… then we need to make sure this logic is evaluated recursively:

abac.polar
# Management hierarchies
manages(manager: User, employee) if
    employee = manager.employees()
    or manages(manager.employees(), employee);


# Now this inline query confirms Cora can view the expense because Cora manages
# Bhavik who manages Alice.
?= allow(new User { name: "cora"}, "view", new Expense { id: 0 });

What’s next