Integration with OPA

Open Policy Agent, or OPA, is an open source, general purpose policy engine, which decouples authorization, respectively policy decisions from other responsibilities of an application and can be used to implement fine-grained access control for your application. As such it is a very good fit for integrating with heimdall. And indeed, the integration is very simple. It is just a matter of using a Remote Authorizer or a Generic Contextualizer. Which one is better for a particular use case depends on the specific application requirements. Here some examples demonstrating these and how it can be solved with heimdall:

Example 1. Sharing Service

Imagine you have a sharing service, e.g. to let friends share all their photos with each other and the API looks roughly as follows:

  • GET /<user>/photos returns all the photos of the user either to the owner of the photo collection itself or to its friends.

To achieve this using OPA, we would need something like the following Rego policy:

package share_photos

default allow = false

# user is owner. that is, the value of first path
# fragment is equal to the identified user
allow { split(input.path, "/")[1] == input.user }

# user is friend. that is, the user referenced by the
# first path fragment has the identified user in its friends list
allow { data.friends[split(input.path, "/")[1]][_] == input.user }

It expects two pieces of information in the payload to the OPA instance:

  • the actual user, making the request and

  • the path to witch the request is made

So something like this: { "input": { "user": "alice", "path": "bob/photos" } }. Since the Rego policy defined above returns just true or false, the corresponding response from the OPA endpoint would be { "result": true } or { "result": false }. Given this and assuming the above policy can be used by making requests to https://opa.local/v1/data/share_photos/policy/allow, the configuration of the Remote Authorizer would look like follows:

id: photos_access
type: remote
config:
  endpoint:
    url: https://opa.local/v1/data/share_photos/policy/allow
  payload: |
    { "input": { "user": {{ quote .Subject.ID }}, "path": {{ quote .RequestURL.Path }} } }
  expressions:
  - expression: |
      Payload.result == true

Here the entire authorization happens within heimdall and is completely outsourced from the business logic of our sharing service.

Example 2. Membership Verification

Imagine you have a billing service, which requires information about the membership of a user to different groups, which represent different subscription options for you entire offering. Depending on this information your service would create invoices with different amounts. The imaginary API of that service could look like follows:

  • POST /create_invoice to invoice the identified user.

You could perform the required query to OPA entirely in your service and use the retrieved group memberships then. Alternatively, you could outsource the communication to OPA to heimdall and deal only with the group membership information in your service. Compared to the Sharing Service example, this time heimdall would not perform any authorization, but rather enrich the subject information with further information. This way, we’re not going to use a Authorizer, but a Generic Contextualizer instead. As with Sharing Service example, there is a need for a Rego policy, which could look like this:

package invoice

groups_graph[data.groups[subject].name] = edges {
  edges := data.groups[subject].member_of
}

member_of_groups[subject] = groups {
  groups_graph[subject]
  groups := graph.reachable(groups_graph, {subject})
}

groups {
  member_of_groups[input.user][_]
}

It expects just one piece of information, namely the actual user, making the request. So something like this: { "input": { "user": "alice" } }. Since the Rego policy defined above returns a list of groups, the corresponding response from the OPA endpoint would be { "result": ["group1", "group2"] }. Given this and assuming the above policy can be used by making requests to https://opa.local/v1/data/invoice/policy/groups and the authentication to that endpoint requires basic authentication, the configuration of the Generic Contextualizer would look like follows:

id: billing_contextualizer
type: generic
config:
  endpoint:
    url: https://opa.local/v1/data/invoice/policy/groups
    auth:
      type: basic_auth
      config:
        user: MyOpaUser
        password: SuperSecretPassword
  payload: |
    { "input": { "user": {{ quote .Subject.ID }} } }

Upon successful execution of the corresponding request, the response from the OPA endpoint will be stored in the Subject.Attributes["billing_contextualizer"] field. That way, you can use that information in a Unifier to forward the group membership to the billing service API.

Last updated on Dec 19, 2022