API Connect with OPA
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';
}