A bear playing hopscotch

Open Policy Agent Alternatives: OPA vs. Oso

Greg Sarjeant

Open Policy Agent Alternatives

We get a lot of questions about the differences between Oso and Open Policy Agent (OPA).   There are three practical distinctions that affect how you use them:

  1. the primitives that each provides
  2. the data format that each supports
  3. the rubric by which you’ll implement authorization in each

In this post, we summarize these distinctions and then use examples to show how you use each product to solve a similar problem - enforcing hierarchical permissions. In a nutshell, Oso is purpose-built for application authorization, while OPA is a general-purpose policy engine. Let’s see what that means in practice.

Comparing Oso and OPA

Primitives for Writing Authorization Policies

Oso is designed specifically for application authorization. Authorization in applications generally takes the form of an actor requesting permission to perform some action on a given resource.  Because these notions (actor, action, resource, permission) are so pervasive in application authorization, Oso provides primitives for them – that is, they are built in to Oso as entities with intrinsic meaning. For example, Oso provides keywords for actors, resources, and roles.

Open Policy Agent Alternatives: Primitives

These primitives make it straightforward to implement common application authorization models such as role-based access control (RBAC) and relationship-based access control (ReBAC). You can see how this looks below.

  
  # actor keyword
  actor User { }

  # resource keyword
  resource Organization {
    # keywords for roles and permissions
      roles = ["admin", "member"];
      permissions = ["read", "add_member"];

      # define role hierarchy
      # admins inherit all member permissions
      "member" if "admin";

      # assign permissions to roles
      "read" if "member";
      "add_member" if "admin";
  }
  

How Oso Handles Data

Oso accepts data in a format called facts. Facts are closely related to Polar, Oso’s rules language. A fact takes this general form:

fact_name(argument, argument, … , argument)

The facts format is capable of expressing any authorization-relevant data in your application. For instance, you could say that the user alice has the admin role in the acme Organization as follows:

has_role(User:alice, "admin", Organization:acme)

Similarly, you could say that the roadmap repository is public with

is_public(Repository:roadmap)

In an application, the requests that are being authorized are often user-facing actions like placing an order or showing a list of documents. These operations need to be fast so the application can respond without degrading the user experience. With Oso, you transform your application data from its native format to facts when you load the data. Oso then makes authorization decisions by performing a small number of simple lookups against a highly indexed database of facts. This allows Oso to make these decisions quickly, minimizing the impact of authorization on application performance.

Rubric: Model, Data, Enforcement

Open Policy Agent Alternatives: Rubrics

Authorization in Oso consists of three elements: model, data, and enforcement. When you get started with Oso, you’ll first define your authorization model in in terms of the actors, actions, relationships, and attributes that you’ve built into your application. Once you have your model, you’ll load authorization data into Oso as facts. These facts will define the specific actors and resources in your application and the relationships between them. Once Oso has your rules and the authorization-relevant data from your application, it provides enforcement by rendering authorization decisions for requests by a given actor to perform a specific action on a particular resource. For example, it will determine whether the current user can create a new file in a directory by looking for a has_rolefact on the user that is associated by the model with the create permission on the directory.

What is Open Policy Agent (OPA)?

Primitives for Writing Open Policy Agent (OPA) Policies in Rego

Open Policy Agent (OPA) is a general-purpose policy engine that has an emphasis on policy enforcement for cloud infrastructure. Requests for access to cloud resources are often expressed in structured data documents (e.g. JSON, YAML) that define the characteristics of the thing that’s being requested. Because of this, OPA’s policy langage, Rego, provides primitives for inspecting and transforming structured data.

Open Policy Agent Alternatives: Rego Policies

OPA’s data inspection primitives include operators for traversing dictionaries by key, iterating over and indexing into lists, testing membership in a set, and similar operations. For data transformation, OPA supports functions such as concatenating strings, defining new lists, or creating unions of sets. This lets OPA work well with structured data. Some examples are shown below

  
    # Test the http request method and path
    allow if {
	    input.attributes.request.http.method == "GET"
	    input.attributes.request.http.path == "/"
    }

    # Look up a specific user in the user_attributes dictionary
    # and retrieve their title to see if they're an employee
    user_is_employee if data.user_attributes[input.user].title == "employee"

    # Iterate over the union of three sets of objects
    some subject, object_instance in object.union_n([files, teams, organizations])
  

How OPA Handles Data

OPA accepts arbitrary structured data as input. It natively supports JSON and YAML because of their prevalence in modern systems, but is designed for the general case of working with data documents. Data in OPA can be deeply nested, and a value at any level of the document hierarchy may itself be a hierarchical data document. This allows OPA to accept data in its native format from a wide variety of systems. However, this also means that OPA needs to be told how to interpret the data in the context of authorization decisions.

To do this, you transform your data at policy evaluation time using the primitives that Rego provides. This allows you to coerce the input data into a structure that allows you to use OPA’s data inspection primitives to evaluate policy rules, but it adds time to each authorization decision. This can affect performance as the policy becomes more complex or as the data grows. OPA provides guidance for optimizing policy performance to address these issues.

Rubric: Load, Transform, Inspect

Open Policy Agent Alternatives: Rubric Load Transform Inspect

Authorization in OPA starts with loading data. Because authorization in OPA consists of operations on arbitrarily structured data documents, you first need to know the structure of your data before you can define the operations that implement your policy. Once you’ve loaded your data, you’ll write policy rules to transform the data as needed in order to derive the authorization-specific meaning from the source data. When the data is in the right structure, you inspect the data to determine whether to allow or deny a request. For example, in OPA, you can determine whether a given container should be deployed to kubernetes by searching the input data for a spec.containers.image key and testing whether its value exists in a list of approved container registries.

Open Policy Agent Example: Inherited Permissions

Open Policy Agent Alternatives: Permissions example

Inherited permissions are a common pattern in authorization. People often gain access to a resource by means of a permission on some other resource that contains it. For example, a person may have access to a file by means of:

  • a permission on the file itself
  • a role on the folder that contains the file
  • a role on the team or organization that owns the file

Let’s see how inherited permissions work in Oso and OPA.

Inherited Permissions in Oso

In Oso, you model inherited permissions like this:

  
    actor User { }
      
    resource Repository {
        roles = ["reader", "maintainer"];
    }
      
    resource Folder {
        roles = ["reader", "writer"];
        relations = {
            repository: Repository,
            folder: Folder,
        };
      
        "reader" if "reader" on "repository";
        "writer" if "maintainer" on "repository";
        role if role on "folder";
    }
      
    resource File {
        permissions = ["read", "write"];
        roles = ["reader", "writer"];
        relations = {
            folder: Folder,
        };
      
        role if role on "folder";
      
        "read"  if "reader";
        "write" if "writer";
    }
  

The code above is written in Polar, Oso’s policy language. The model is defined in terms of the primitives that Oso provides for application authorization. There is an actor block that defines a User entity, and resource blocks that define Folder and File entities.  Within the resource blocks, we define the roles and relations that apply to that resource, as well as the actions that are allowed to the various roles. Finally, we define how roles on a resource are inherited from roles on that resource’s parent. Oso knows what it means for two resources to be related, so we can express a recursive permission model like this in a couple of lines:

  
    resource Folder {
        roles = ["reader", "writer"];
        relations = {
            folder: Folder,
        };

        role if role on "folder";
    }
  

This says that a given actor:

  • can be assigned the role of “reader” or “writer” on a given folder
  • has the “reader” or “writer” role on a folder if they have that role on the folder’s parent

Once the model is defined in Oso, we can supply data as facts, and use that data to validate enforcement by running a test:

  
    test "folder roles apply to files" {
        setup {
            has_role(User{"alice"}, "reader", Repository{"anvil"});
            has_relation(Folder{"python"}, "repository", Repository{"anvil"});
            has_relation(Folder{"tests"}, "folder", Folder{"python"});
            has_relation(File{"test.py"}, "folder", Folder{"tests"});
        }
    
        assert allow(User{"alice"}, "read", File{"test.py"});
    }
  

This says that a given actor:

  • can be assigned the role of “reader” or “writer” on a given folder
  • has the “reader” or “writer” role on a folder if they have that role on the folder’s parent

Once the model is defined in Oso, we can supply data as facts, and use that data to validate enforcement by running a test:

Inherited Permissions in OPA

In OPA, you model inherited permissions like this (the following code samples come from this blog post):

  
    package app.rebac
      
    import future.keywords.in
      
    # rule to return the resource instances by ids
    files[id] := file_instance {
        # iterate the resource instances of some file_instance in data.files
        some file_instance in data.files
        id := sprintf("file:%s",[file_instance.id])
    }
      
    # rule to return teams by ids
    teams[id] := team_instance {
        # Iterate the teams 
        some team_instance in data.teams
        id := sprintf("team:%s",[team_instance.id])
    }
      
    organizations[id] := organization_instance {
        some organization_instance in data.organizations
        id := sprintf("organization:%s",[organization_instance.id])
    }

    # return a full graph mapping of each subject to the object it has reference to
    full_graph[subject] := ref_object {
        some subject, object_instance in object.union_n([files, teams,organizations])

        # get the parent_id the subject is referring
        ref_object := [object.get(object_instance, "parent_id", null)]
      }
      
    # rule to return users by ids
    users[id] := user {
        some user in data.users
        id := user.id
    }
      
    # the input user
    input_user := users[input.user]
      
    # rule to return a list of allowed assignments
    allowing_assignments[assignment] {
        # iterate the user assignments
        some assignment in input_user.assignments
      
        # check that the required action from the input is allowed by the current role
        input.action in data.roles[assignment.role].grants
      
        # check that the required resource from the input is reachable in the graph
        # by the current team 
        assignment.resource in graph.reachable(full_graph, {input.resource})
    }
      
    # create allow rule with the default of false
    default allow := false
      
    allow {
        # allow the user to perform the action on the resource if they have more than one allowing       assignments
        count(allowing_assignments) > 0
    }
  

The input data looks like this:

  
    {
        "files": [
            {
              "id": "backend-readme.md",
              "parent_id": "team:rnd"
            },
            {
                "id": "frontend-readme.md",
                "parent_id": "team:rnd"
            },
            {
                "id": "gateway-config.yaml",
                "parent_id": "team:rnd"
            },
            {
                "id": "website.js",
                "parent_id": "team:marketing"
            },
            {
                "id": "blog-1.pdf",
                "parent_id": "team:marketing"
            },
            {
                "id": "logo.psd",
                "parent_id": "team:marketing"
            }
        ],
        "organizations": [
            {
                "id": "acme"
            }
        ],
        "roles": {
            "admin": {
                "grants": [
                    "view",
                    "edit"
                ]
            },
            "viewer": {
                "grants": [
                    "view"
                ]
            }
        },
        "teams": [
            {
                "id": "rnd",
                "parent_id": "organization:acme"
            },
            {
                "id": "marketing",
                "parent_id": "organization:acme"
            }
        ],
        "users": [
            {
                "assignments": [
                    {
                        "resource": "organization:acme",
                        "role": "viewer"
                    },
                    {
                        "resource": "team:rnd",
                        "role": "admin"
                    }
                ],
                "id": "sally"
            },
            {
                "assignments": [
                    {
                        "resource": "team:marketing",
                        "role": "viewer"
                    },
                    {
                        "resource": "file:design.psd",
                        "role": "admin"
                    }
                ],
                "id": "tim"
            }
        ]
    }
  

The code is written in Rego, OPA’s policy language. Here, you can see the primitives that OPA provides for manipulating structured data: the some keyword iterates over a list, list items are referenced by a numeric index, dictionary items are referenced by key, etc.

The first half of the policy is spent transforming the input data into a graph. This graph defines the relationships between the files, teams, and organizations in a way that allows OPA to perform authorization operations on the data. We build the graph by extracting the files, teams, and organizations into lists and rewriting their IDs so they match the parent_id field in the source data. We then use the union_n operator to merge these lists into the graph.

Once we’ve transformed the input data to a graph, we can inspect the data to determine whether a given user has permission to perform a requested operation on a file. We first search the user’s roles and the role’s grants to determine whether the user has the requested permission. We then use the graph.reachable operator to determine whether the user is associated with the specified file through parent/child relationships between the user’s organizations and teams. The combination of the two determines whether the user has the necessary grant on the file.

Differences between Oso and OPA

Open Policy Agent Alternatives: Oso and OPA

There are high quality alternatives to OPA, like Oso. The core differences between Oso and OPA are summarized below:

Open Policy Agent Alternatives: Oso

Oso is built for application authorization. Its rules language, Polar, supports application authorization concepts such as actors, roles, and relationships. Its data model, facts, captures authorization-relevant data in terms that can be directly evaluated against the rules. The complementary nature of Polar and facts allow Oso to render authorization decisions quickly. This is a primary consideration for application authorization operations.

OPA, on the other hand, is a general-purpose policy engine. It doesn’t define a data model, but instead supports arbitrary structured data. This allows it to accept data from a variety of systems in its native format. Its rules language, Rego, provides primitives that allow you to transform and inspect its data as needed during evaluation to make authorization decisions.

If you’re getting started with Application Authorization, you can try Oso Cloud by signing up for a free account here.

Want us to remind you?
We'll email you before the event with a friendly reminder.

Write your first policy