Skip to main content

Attribute-Based Access Control (ABAC) in Rego

Based on our best practice model, we will now implement Attribute-Based Access Control. Note that since there are so many attributes that can be relevant for access control, we limit ourselves to four different kinds of them, and show you how to implement them in Rego.

tip

This how-to assumes basic knowledge of the ABAC model of access control. See the explanation page for a refresher

Common Model

As outlined in the introduction, the actual policy model (ABAC) described here is independent of how the subject/action/resource tuple is determined. We will thus assume that data.common resolves to:

{
"subject": "alice",
"action": "upgrade",
"resource": {
"type": "server",
"fqdn": "db01.corp.com"
}
}

as described in the introduction.

Recap: common.rego and preamble
package common

import rego.v1

subject := input.subject

action := input.action

resource := input.resource
package abac

import rego.v1

subject := data.common.subject

action := data.common.action

resource := data.common.resource

Attributes in policy evaluation

To demonstrate multiple ways of getting attributes to have an effect on policy evaluation, we'll implement the following rule:

A destructive action is authorized if and only if

  1. the subject is on call,
  2. the resource's environment is "production"
  3. it's the weekend.

Note the multiple attributes in bold markup.

A first approximation of this logic in Rego is the following:

default allow := false

allow if {
is_destructive(action)
is_on_call(subject)
is_production(resource)
is_weekend(time.now_ns())
}

This evaluation is likely to be false: While all the attributes of subject, action and resource match, there's a fair chance that it's not the weekend right now. In a complete access control policy, there will be other rules, that may not depend on all the attributes like this one. For example, you could imagine that an action is allowed by any authenticated user, as long as it's not the weekend.

note

See below for test evaluations with mocked time attributes.

Environmental attributes: Time

The easiest of these is is_weekend, checking an attribute of the current time:

is_weekend(ns) if time.weekday(ns) in {"Sunday", "Saturday"}

Statically mapped attributes: Action

Since there may only be a few actions in the application, it could be possible to declare the non-destructive subset statically in Rego:

is_destructive(action) if not action in {"list", "view"}
note

We're declaring the non-destructive subset to err on the safe side: if a new action is added to the application and the Rego policy is not updated, it is generally safer to deny a non-destructive request than to potentially allow a destructive one through.

Query-provided attributes: Resource

If the application is already aware of the resource's attributes when querying OPA for a policy decision, it can send those attributes along via input. Sometimes, it might also be possible to derive the attribute from another one — in this example, we'll determine if a resource belongs to the production environment based on its domain name.

is_production(resource) if glob.match("*.corp.com", ["."], resource.fqdn)

Since input has been presented above, there is not much to do to integrate it with the policy: The application that's asking OPA for a policy decision provides the attributes via input.

With input that follows the Decision Request Input Schema outlined earlier, there is plentiful contextual information that can have an effect on access control.

{
"resource": {
"type": "endpoint",
"id": "/documents/1"
},
"action": {
"name": "POST",
"protocol": "HTTP 1.1",
"headers": {
"Authorization": "bearer [JWT}",
"Content-Type": "application/json"
}
},
"context": {
"type": "http",
"host": "db01.corp.com",
"port": "5678",
"data": {
"environment": "production"
},
"appID": "documents-service"
},
"subject": {
"type": "user",
"id": "alice"
}
}

Our Common Model would be adapted to pull this out of the input request:

package common

import rego.v1

resource := {"type": "server", "fqdn": input.context.host}

Externally-provided attributes: Subject

On-call schedules are fluent by nature and it's conceivable that an external API has to be queried during policy evaluation to get the most recent on-call duty assignments. In this example, we're assuming that the external service responds to a query like this:

GET https://api.majorduty.com/${TENANT}/groups/on-call-now

{
"name": "alice"
}

To make use of http.send to send such a query, we add a helper method:

on_call_now := username if {
url := sprintf("https://api.majorduty.com/%s/groups/on-call-now", [opa.runtime().env.TENANT])
resp := http.send({"method": "GET", "url": url})
username := resp.body.name
}

is_on_call(subject) := on_call_now == subject
Mocked rule

For implementation reasons, we cannot use http.send in the live evaluation examples. So the actual rule that's used for evaluation is this one:

on_call_now := username if {
url := sprintf("https://api.majorduty.com/%s/groups/on-call-now", ["default"])
resp := http_send({"method": "GET", "url": url})
username := resp.body.name
}

http_send(_) := {"body": {"name": "alice"}}

is_on_call(subject) if on_call_now == subject

Test Evaluations

With this Rego policy set up, we can test different scenarios:

test_all_conditions_met if {
allow with subject as "alice"
with action as "delete"
with resource as {"fqdn": "api56.corp.com"}
with time.now_ns as time.add_date(0, 0, 0, 2) # it's the first unix weekend!
}