README ¶
opa-bundle-api
Proof-of-concept for an API to produce OPA bundles
Service overview
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)
Additional information
Rules
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.
Policy
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
Source code
cmd/opa-bundle-api
File: cmd/opa-bundle-api/main.go
- Entrypoint for the application
- Loads all the clients and the API
pkg/bundle
Directory: pkg/bundle
- Contains the logic around building OPA Bundles.
- Contains static rules for OPA (written in
rego
) which are added to the bundles
pkg/config
Directory: pkg/config
- The application configuration built with urfave
- Overly complex for the proof-of-concept, but copied from another project
pkg/handler
Directory: pkg/handler
- Contains the logic for the REST API, invoked as Echo handlers
pkg/logs
Directory: pkg/logs
- Contains logic around storing and reading OPA Decision logs
pkg/replay
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
pkg/rule
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
pkg/util
Directory: pkg/util
- Just some utils, hashing of data to become
revision
andETag
as an example
API
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.
Endpoints
/rules
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
/logs
GET /logs
: reads all logsPOST /logs
: creates rules (takes decision log array)GET /logs/:decisionID
: reads rule with:decisionID
/replay
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)
/bundle
GET /bundle/bundle.tar.gz
: downloads the current OPA bundle (containing the module + dynamic data)
Running with docker-compose
Start:
docker-compose up
Stop with CTRL+C
Testing API with cURL
Download bundle
curl localhost:8080/bundle/bundle.tar.gz --output /tmp/bundle.tar.gz
Download bundle (with If-None-Match)
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
Test bundle with OPA
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" |
+---+--------------------+
Read All Rules
curl localhost:8080/rules
Read Rule
curl localhost:8080/rules/1
Create Rule
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
Update Rule
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
Delete Rule
curl -X DELETE localhost:8080/rules/1
Create Logs
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
Read Logs
curl localhost:8080/logs
Read Log
curl localhost:8080/logs/4ca636c1-55e4-417a-b1d8-4aceb67960d1
Replay log with current rules
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
}
}
]
}
]
Replay log with new rules
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
}
}
]
}
]
Testing OPA with cURL
Get Policies
curl localhost:8181/v1/policies
Test Policy
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