Proof-of-concept for an API to produce OPA bundles
The proof of concept is to show that it is possible to generate Open Policy Agent bundles dynamically and have OPA periodically download them (if changed).
The whole picture would look something like this:
In the proof of concept, there's no additional services for allowing/denying access, updating the database etcetera but shows how the concept could be.
There's also a feature included to receive OPA Decision Logs and replay them based on current rules or a set of new rules (only testing them)
A rule looks like this in the code:
type Rule struct {
ID ID `json:"id"`
Country string `json:"country"`
City string `json:"city"`
Building string `json:"building"`
Role string `json:"role"`
DeviceType string `json:"device_type"`
Action string `json:"action"`
}
It will look something like this in the bundle (data.json
):
{
"rules": [
{
"action": "allow",
"building": "ANY",
"city": "ANY",
"country": "ANY",
"device_type": "ANY",
"id": 1,
"role": "super_admin"
},
{
"action": "allow",
"building": "ANY",
"city": "ANY",
"country": "Sweden",
"device_type": "ANY",
"id": 2,
"role": "sweden_admin"
}
]
}
Look at pkg/rule/rule.go to dig deeper.
You can find the only current policy here: rule.rego
This file will be embedded in the Golang binary at compilation and used together with the dynamic rules to create the OPA Bundle.
The important parts of the current rule are:
- The keyword
ANY
forcountry
,city
,building
,role
anddevice_type
means a wildcard. - The keyword
undefined
foraction
means it will use the default action which isaction = deny
- Any matches
action = allow
will allow access as long as there are no matches foraction = deny
- Even if there are multiple rules that gives a user
action = allow
, a singleaction = deny
will setallow
tofalse
File: cmd/opa-bundle-api/main.go
- Entrypoint for the application
- Loads all the clients and the API
Directory: pkg/bundle
- Contains the logic around building OPA Bundles.
- Contains static rules for OPA (written in
rego
) which are added to the bundles
Directory: pkg/config
- The application configuration built with urfave
- Overly complex for the proof-of-concept, but copied from another project
Directory: pkg/handler
- Contains the logic for the REST API, invoked as Echo handlers
Directory: pkg/logs
- Contains logic around storing and reading OPA Decision logs
Directory: pkg/replay
- Contains logic around replaying OPA Decisions based on the bundle
- Enables us to test if a change had the desired effect on a previous decision or test how a previous decision would be if we changed the rules
Directory: pkg/rule
- Contains the rule client for all the dynamic rules that are injected into the bundle
- Here is the logic around adding new rules, showing them etcetera
Directory: pkg/util
- Just some utils, hashing of data to become
revision
andETag
as an example
The API (built with Echo
) takes care of everything right now and at start-up populates a few pre-defined rules.
Right now it is self contained, but could just as well read the data about the rules from a database or another API. Using a hashmap for convinience.
GET /rules
: reads all rulesPOST /rules
: creates a ruleGET /rules/:id
: reads rule with:id
PUT /rules/:id
: updates rule with:id
DELETE /rules/:id
: deletes rule with:id
GET /logs
: reads all logsPOST /logs
: creates rules (takes decision log array)GET /logs/:decisionID
: reads rule with:decisionID
GET /replay/:decisionID
: replays the:decisionID
based on the current rulesPOST /replay/:decisionID
: replays the:decisionID
based new rules posted (will not change the actual roles, only during the replay)
GET /bundle/bundle.tar.gz
: downloads the current OPA bundle (containing the module + dynamic data)
Start:
docker-compose up
Stop with CTRL+C
curl localhost:8080/bundle/bundle.tar.gz --output /tmp/bundle.tar.gz
If the header matches the current revision, a status code 304
should be returned.
curl --header "If-None-Match: 476d1f14d83110241366a81f82753523b850e150f55ed51bf5379f40cabc323d" localhost:8080/bundle/bundle.tar.gz --output /tmp/bundle.tar.gz
opa eval --bundle /tmp/bundle.tar.gz --format pretty 'data.rules[i].id == 1; data.rules[i].role'
This should output the first role, something like:
+---+--------------------+
| i | data.rules[i].role |
+---+--------------------+
| 0 | "super_admin" |
+---+--------------------+
curl localhost:8080/rules
curl localhost:8080/rules/1
DATA='{"country": "Iceland", "city": "Reykjavik", "building": "Branch", "role": "user", "device_type": "Printer", "action": "allow"}'
curl -X POST --header "Content-Type: application/json" --data $DATA localhost:8080/rules
DATA='{"country": "Iceland", "city": "Reykjavik", "building": "Branch", "role": "user", "device_type": "Printer", "action": "allow"}'
curl -X PUT --header "Content-Type: application/json" --data $DATA localhost:8080/rules/1
curl -X DELETE localhost:8080/rules/1
DATA='[
{
"labels": {
"app": "my-example-app",
"id": "1780d507-aea2-45cc-ae50-fa153c8e4a5a",
"version": "v0.28.0"
},
"decision_id": "4ca636c1-55e4-417a-b1d8-4aceb67960d1",
"bundles": {
"authz": {
"revision": "W3sibCI6InN5cy9jYXRhbG9nIiwicyI6NDA3MX1d"
}
},
"path": "http/example/authz/allow",
"input": {
"method": "GET",
"path": "/salary/bob"
},
"result": "true",
"requested_by": "[::1]:59943",
"timestamp": "2018-01-01T00:00:00.000000Z"
}
]'
curl --header "Content-Type: application/json" -X POST --data $DATA localhost:8080/logs
curl localhost:8080/logs
curl localhost:8080/logs/4ca636c1-55e4-417a-b1d8-4aceb67960d1
Start api
and opa
, then run the following and you should expect to be Denied
(result = false
):
DATA='{"input":{"user":"Simon","country":"Sweden","city":"Alingsås","building":"HQ","role":"user","device_type":"Printer"}}'
curl -X POST --header "Content-Type: application/json" --data $DATA localhost:8181/v1/data/rule/allow
You should get a response like this:
{
"decision_id":"7b861f17-e1a7-49d5-8660-b13d5d42fd8e",
"result":false
}
Verify that you can replay the decision:
curl localhost:8080/replay/7b861f17-e1a7-49d5-8660-b13d5d42fd8e
Result should look something like this:
[
{
"expressions": [
{
"value": false,
"text": "data.rule.allow",
"location": {
"row": 1,
"col": 1
}
}
]
}
]
Now add a new rule that would allow it:
DATA='{"country": "Sweden", "city": "Alingsås", "building": "ANY", "role": "user", "device_type": "Printer", "action": "allow"}'
curl -X POST --header "Content-Type: application/json" --data $DATA localhost:8080/rules
Replay the event once more:
curl localhost:8080/replay/7b861f17-e1a7-49d5-8660-b13d5d42fd8e
Now the result should have changed:
[
{
"expressions": [
{
"value": true,
"text": "data.rule.allow",
"location": {
"row": 1,
"col": 1
}
}
]
}
]
Start api
and opa
, then run the following and you should expect to be Denied
(result = false
):
Test with the root
account:
DATA='{"input":{"user":"root","country":"Sweden","city":"Alingsås","building":"HQ","role":"super_admin","device_type":"Alarm"}}'
curl -X POST --header "Content-Type: application/json" --data $DATA localhost:8181/v1/data/rule/allow
Result for the root
account:
{
"decision_id": "b2929531-d387-42f1-afb8-4c9177911429",
"result": true
}
Test with John Doe
account:
DATA='{"input":{"user":"John Doe","country":"Sweden","city":"Gothenburg","building":"HQ","role":"guest","device_type":"Alarm"}}'
curl -X POST --header "Content-Type: application/json" --data $DATA localhost:8181/v1/data/rule/allow
Result for the John Doe
account:
{
"decision_id": "746a568b-629a-4e39-933c-14f843821771",
"result": false
}
Now replay the root
account request with a set of new rules:
DATA='[
{
"country": "ANY",
"city": "ANY",
"building": "ANY",
"role": "super_admin",
"device_type": "ANY",
"action": "deny"
},
{
"country": "Sweden",
"city": "Gothenburg",
"building": "HQ",
"role": "guest",
"device_type": "Alarm",
"action": "allow"
}
]'
curl -X POST --header "Content-Type: application/json" --data $DATA localhost:8080/replay/b2929531-d387-42f1-afb8-4c9177911429
The replay result for the root
account should look like this:
[
{
"expressions": [
{
"value": false,
"text": "data.rule.allow",
"location": {
"row": 1,
"col": 1
}
}
]
}
]
Now replay the John Doe
account request with a set of new rules:
DATA='[
{
"country": "ANY",
"city": "ANY",
"building": "ANY",
"role": "super_admin",
"device_type": "ANY",
"action": "deny"
},
{
"country": "Sweden",
"city": "Gothenburg",
"building": "HQ",
"role": "guest",
"device_type": "Alarm",
"action": "allow"
}
]'
curl -X POST --header "Content-Type: application/json" --data $DATA localhost:8080/replay/746a568b-629a-4e39-933c-14f843821771
The replay result for the John Doe
account should look like this:
[
{
"expressions": [
{
"value": true,
"text": "data.rule.allow",
"location": {
"row": 1,
"col": 1
}
}
]
}
]
curl localhost:8181/v1/policies
Allowed
:
DATA='{"input":{"user":"Simon","country":"Sweden","city":"Alingsås","building":"Branch","role":"user","device_type":"Printer"}}'
curl -X POST --header "Content-Type: application/json" --data $DATA localhost:8181/v1/data/rule/allow
Denied
:
DATA='{"input":{"user":"Simon","country":"Sweden","city":"Alingsås","building":"HQ","role":"user","device_type":"Printer"}}'
curl -X POST --header "Content-Type: application/json" --data $DATA localhost:8181/v1/data/rule/allow