Implementing OPA: Comprehensive Overview and Practical Examples

Implementing OPA: Comprehensive Overview and Practical Examples

In this piece, you will learn the process of implementing the Open Policy Agent (OPA) and understand how it all fits together when building fine-grained permissions.

By Vanessa Osuka ·

implementing-opa

The Open Policy Agent (OPA) offers a high-level declarative language for defining and enforcing policies across various systems, making it a key tool for precise access control and integration in various environments.

Here’s where OPA really shines:

  1. Microservices: Enforces inter-service communication policies to meet organizational standards.
  2. Cloud Infrastructure: Manages cloud resources by enforcing compliance and best practices.
  3. CI/CD Pipelines: Integrates into pipelines to ensure code quality and security through automated policy enforcement.
  4. Kubernetes: Acts as an admission controller, applying custom policies for resource management in clusters.

Table Of Contents

Throughout this guide, we’ll cover:

  1. Understanding Open Policy Agent (OPA) - its purpose, features, and integration capabilities.
  2. A comprehensive introduction to crafting policies using OPA's expressive policy language, Rego.
  3. The practical aspects of setting up OPA, including installation steps, configuration processes, and writing tests.

At the end of this article, you would have gained practical knowledge in implementing ABAC using OPA which can enable you to enhance security and access control in a diverse computing environment.

What is OPA (Open Policy Agent)?

To effectively express these policies, a structured approach to writing rules is necessary. This is what OPA helps us achieve using a declarative language called Rego.

According to the official docs;

The Open Policy Agent is an open source, general-purpose policy engine that unifies policy enforcement across the stack

The primary purpose of OPA is to provide a unified approach to access control and policy decision-making in dynamic environments which includes microservices, Kubernetes, CI/CD pipelines and API gateways.

Integration capabilities of OPA

API Integration

OPA exposes a simple HTTP API that applications can interact with to request policy decisions. This makes it easy to integrate OPA with diverse systems and programming languages.

Kubernetes Integration

OPA has native integrations with Kubernetes, serving as the policy engine for the Kubernetes Policy Controller. This allows organisations to define and enforce policies across their Kubernetes clusters.

Cloud-Native Environments

OPA is well-suited for cloud-native environments and integrates seamlessly with cloud platforms, including AWS, Azure, and Google Cloud. It enables organisations to enforce policies consistently in cloud-based applications.

Custom Application Integration

OPA can be integrated with custom applications and services regardless of the programming language or framework used. By sending requests to OPA's decision endpoint, applications can benefit from centralised policy decision-making.

ABAC-workflow-2

Fundamentals of OPA using ABAC permissions

Defining policies

Let's talk about a key component of ABAC called attributes.

This is the bane of this access control model. Attributes means properties associated with entities such as users, resources, and environment. These attributes play a crucial role in making access control decisions.

Let's explain attributes in ABAC with a practical example:

Attributes in access control: a use case

Consider an online document sharing system where users can access documents based on certain conditions. In this scenario, we will likely have the following kinds of attributes:

  1. User Attributes: Users have attributes like "role," "department," and "clearance level."
  2. Document Attributes: Documents have attributes like "classification," "department access," and "owner."
  3. Environment Attributes: There might be environmental attributes like "time of day" or "location." For example: Current time is "business hours."

Writing rules using attributes

Now, let's create some basic ABAC rules combining our knowledge of attributes using OPA’s expressive policy language format known as Rego.

Step I : Create a new file in any preferred text editor touch rules.rego and define the following rules:

Rule #1
allow {
    user.role == "Manager"
    document.classification == "Confidential"
}

Rule #2
allow {
    user.department == document.departmentAccess
}

Rule #3:
allow {
    user.username == document.owner
}

Rule #4:
allow {
    environment.timeOfDay == "business hours"
}

Rule #1: Here the user has an attribute called "role", so this rule specifies to allow access to the document if the user's role is "Manager" and the document is classified as "Confidential."

Rule #2: Here we allow access if the user's attribute of "department" matches the document's attribute department

Rule #3: We grant access if the username supplied matches the "owner" attribute of the document, in order words if the user is the one who actually created the document.

Rule #4: Allow access during business hours.

We now understand how attributes work in ABAC and what they really do; they help us make rules.

Great. The next step is to weave this in and actually write policies.

But first, let’s get things in place.

How to Set Up OPA

Installation and configuration

To interact with OPA directly on your machine:

  1. Download the corresponding binary from the official website

  2. Next, set the permission mode for the executable

chmod 755 ./opa
  1. Move the binary file to a directory in your system's PATH
chmod +x opa >> sudo mv opa /usr/local/bin/

And that’s it, opa should now be installed on your local machine.

  1. The opa eval command helps you validate rego expression and policies. For example, when we write a policy, we can use opa eval to run these policies through a json file that contains sample input using this command:
opa eval --data example.rego --input input.json "data.main"

where ‘example.rego’ and ‘input.json’ are placeholder filenames, ‘data.main’ is the default entry point of a Rego program.

  1. There’s also a REPL area to play around with. To start the REPL run opa run from your terminal.

Writing Policies with OPA

Here comes the fun part! We are going to write a policy that will allow or deny access based on IP addresses and users’ role. Let’s name it ip-checker policy!

Step I: Create a new policy file and name it demo.rego

Step II : Decide on the input data. The attributes in the use case described above will be user and an array data of IP addresses.

Step III: Express the policy rules with Rego based on our input data. These rules define what is allowed or denied. In our demo.rego file, let’s add the following lines of code;

package main

default allow_access =  false

allow_access {
  input.user.role == "admin"
}

allow_access {
  allowed_ip := {"192.168.1.1", "10.0.0.2", "172.16.0.1"}
  input.request_ip == allowed_ip[_]
  input.user.role == "editor"
}

Here, we are defining two rules besides which everything else should return false.

The first rule allows access if the user is an admin.

The second rule should allow access if the user has a role of editor AND their ip address corresponds to any of those in the array. Note: this array can also be an external list or document.

Next, we want to evaluate our policy by loading input data. We’ll use the OPA CLI to load input data and evaluate our policies against it.

Step IV: Create an input.json file. touch input.json and add the following lines of code:

{
  "user": {
    "username": "Alice",
    "role": "editor"
  },
  "request_ip": "10.0.0.2"
}

Step V: Run the program:

opa eval --data demo.rego --input input.json "data.main.allow_access"

Output:

shell >> opa eval --data demo.rego --input input.json "data.main.allow_access"

{
  "result": [
    {
      "expressions": [
        {
          "value": true,
          "text": "data.main.allow_access",
          "location": {
            "row": 1,
            "col": 1
          }
        }
      ]
    }
  ]
}

The result indicates access is allowed based on the conditions defined in the provided input data.

Try to switch the request_ip numbers in input.json, you’ll get a negation result. You can also extend this policy to include more sophisticated conditions.

Writing Tests

Writing policy tests helps ensure that your policy behaves as expected and provides a way to catch regressions when you make changes.

So let’s write some tests for our demo.rego file. Create a new test file in the same folder touch demo_test.rego and add the following test cases:

package example.authz

# Test case allowing access for an admin user
test_allow_admin {
  file = {
    "user": {"role": "admin"},
    "request_ip": "192.168.1.5"
  }
  allow with input as file
}

# Test case allowing access for an editor user with a valid IP
test_allow_editor_valid_ip {
  file = {
    "user": {"role": "editor"},
    "request_ip": "10.0.0.2"
  }
  allow with input as file
}

# Test case denying access for an editor user with an invalid IP
test_deny_access_editor_invalid_ip {
  file = {
    "user": {"role": "editor"},
    "request_ip": "192.168.1.5"
  }
  not allow with input as file
}

# Test case denying access for a user with an unknown role
test_deny_access_unknown_role {
  file = {
    "user": {"role": "guest"},
    "request_ip": "10.0.0.2"
  }
  not allow with input as file
}

In the test cases above: We define a package name ‘example.authz’ , it also makes a call to a ‘file’ variable that contains our mock data.

  • test_allow_admin tests the scenario where access should be allowed for an admin user.
  • test_allow_editor_valid_ip tests the scenario where access should be allowed for an editor user with a valid IP.
  • test_deny_access_editor_invalid_ip tests the scenario where access should be denied for an editor user with an invalid IP.
  • test_deny_access_unknown_role tests the scenario where access should be denied for a user with an unknown role.

Run the test;

opa test -v .

Here is the output after running tests with the above command showing all our test cases are successful: demo_tests

Practical Examples

Walkthrough of practical use case

For this part, we shall consider a streaming service that is meant to show a particular movie to users depending on their age range and preferred language.

ABAC is a better option for our use case when compared to RBAC and ReBAC since this use case will involve a fusion of changing conditions.

Also, considering that our app will scale to cater for more users in the future, to prevent complications we’ll want to decouple our policies and in this case opt for ABAC, especially if it’s a large enterprise service app catering to global users.

Movie-ss

Restricting access based on attributes

Let's now create a policy file containing the attributes for our ‘fictional’ movie streaming service: touch movie_policy.rego;

package movie_access

default allow = false

# Define user attributes
user = {
    "age": 25,
    "language": "English",
    "subscriptionStatus": "active",
}

# Define movie attributes
movie = {
  "genre": "Action",
    "language": "English",
    "rating": "PG-13",
}

# Define dynamic content access policy
allow {
    input.action == "access"
    input.resource.type == "movie"
    
    # Check age restriction
    input.resource.attributes.rating == "PG-13"
    user.age >= input.resource.attributes.age

    # Check language preference
    user.language == input.resource.attributes.language

    # Check subscription status
    user.subscriptionStatus == "active"
}

Granting permissions dynamically

Let’s breakdown the policy rules;

  1. Default Rule (default allow = false): This means that by default, access is denied. This makes sure that only explicitly defined rules in the policy will allow access.
  2. User Attributes (user): The user object represents dynamic attributes of a user, including age, language, and subscriptionStatus.
  3. Movie Attributes (movie): The movie object represents dynamic attributes of a movie, including genre, language, and rating.
  4. Dynamic Content Access Policy (allow Rule):
  • The allow rule defines conditions for granting access to a movie based on the user's attributes and the movie's attributes.
  • It checks whether the user's age is greater than or equal to the movie's age rating, ensuring age-appropriate access.
  • It verifies that the user's language preference matches the language of the movie.
  • It checks whether the user's subscription status is "active."

You can extend this policy to cover a broader range of attributes depending on your microservices setup.

Recap

So far, this tutorial has provided a comprehensive guide on implementing Open Policy Agent (OPA) with ABAC permissions.

By leveraging OPA's flexible and declarative policy language, Rego, we've explored how to define fine-grained access control rules based on dynamic attributes associated with users, resources, and the environment.

Throughout the tutorial, we've demonstrated the steps to create Rego policy files, understanding attributes, and implementing ABAC with OPA.

Further Exploration

  1. Learn how to seamlessly integrate the power of ABAC to your application layer with Permify
  2. Visit the rego playground
  3. Check out the official OPA docs