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.
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
- the subject is on call,
- the resource's environment is "production"
- 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.
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"}
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!
}