Today is the day I first learnt about OPA…. I am unsure how this has never come to my attention before. For those of you who are not aware OPA is a OpenPolicyAgent that provides a standardised ways to enforce policies, at Kubernetes, Envoys or Application level. Please go to https://openpolicyagent.org/ for more details.

This article is not going to be an introduction to OPA as there are many out there already. Instead, I will describe the building blocks that I used to meet a simple scenario. These can easily be extended for more complex use cases.

The Scenario

The scenario I was building to was I wanted the ability to add a second authorization layer to validate my consumers. This would be used if you wanted to grant permissions to consumers to use certain APIs or Operations. The first layer is the consumer would sign up in the developer portal. Once they had signed up they would be approved by their client id being added to a whitelist for the appropriate path.

Rego

The OPA rego, I included the whitelist in here for simplicity but in truth this could easily be extracted to a separate json file.

package play

default allow := false

whitelist ={
    "/httpbingetip/" : {
        "580590b40d4045f334b457d462359",
        "aaaa-aaaa-aaaa-bbbb"
    }
}

allow {
     whitelist[input.path][_] == input.clientid
}

message[msg] {

     not allow
     msg = sprintf("You have not been approved for this API yet: '%v' with client-id '%v'", [input.path,input.clientid])
}


message[msg] {
     allow
     msg = sprintf("You have  been approved for this API %v",[input.path])
}

This was loaded into my OPA via a config map and mounted on to the OPA pod as instructed https://www.openpolicyagent.org/docs/latest/deployments/#http-proxies:~:text=deployment%2Dopa.yaml%3A

This policy would take a payload similar to

{
  "input":
  {
      "clientid": "aaaa-aaaa-aaaa-aaaa",
      "path": "/bob/bob"
  }
}

and return a response if the client id is incorrect

{
  "decision_id": "796ac231-6ae6-4e25-8123-c864eb8a23aa",
  "result": {
     "allow": false,
     "message": [ "You have not been approved for this API yet: \"/httpbingetip/\" with client-id \"580590b40d4045f334b457d4623f959b\"" ],
     "whitelist": {
        "/httpbingetip/": [ "WRONG"]
     }
  }
}

or return the following response if the client is correct

{
  "decision_id": "796ac231-6ae6-4e25-8123-c864eb8423aa",
  "result": {
     "allow": true,
     "message": [ "You have  been approved for this API  \"/httpbingetip/\"" ]
  }
}

The API Connect Code

The following assembly can easily be placed in an API, Custom Policy or a Global Policy. I am putting it here to show the rough logic that I used. This would need changing for more complex use cases and so I am not going to publish this as a Global Policy or a Custom Policy. An example of a Global Policy and Custom Policy can be found here https://chrisphillips-cminion.github.io/apiconnect/2023/04/15/hmac-apic.html

assembly:
  execute:
  - gatewayscript:
      version: 2.0.0
      title: gatewayscript
      source: |-
        let payload =  {
            input  :
                {
              clientid: context.get('client.app.id'),
              path: context.get("request.path")
          }
        }
        console.info(payload)
        context.set('message.body',payload)
  - invoke:
      version: 2.2.0
      title: invoke
      backend-type: detect
      header-control:
        type: blocklist
        values: []
      parameter-control:
        type: allowlist
        values: []
      http-version: HTTP/1.1
      timeout: 60
      verb: POST
      chunked-uploads: true
      persistent-connection: true
      cache-response: protocol
      cache-ttl: 900
      stop-on-error: []
      graphql-send-type: detect
      websocket-upgrade: false
      target-url: http://opa.opa.svc.cluster.local/v1/data/play
      output: opavar
  - parse:
      version: 2.1.0
      title: parse
      parse-settings-reference:
        default: apic-default-parsesettings
      output: opavar
      input: opavar
  - gatewayscript:
      version: 2.0.0
      title: gatewayscript
      source: |-
        let dd = context.get('opavar.body')
        console.info(dd)
        if (dd.result.allow==false) {
            context.reject('CustomError', dd.result.message[0]);
            context.message.statusCode = '403';
        }