opa-bundle-api

module
v0.0.0-...-2d83bcd Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 16, 2021 License: MIT

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:

overview

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 for country, city, building, role and device_type means a wildcard.
  • The keyword undefined for action means it will use the default action which is action = deny
  • Any matches action = allow will allow access as long as there are no matches for action = deny
  • Even if there are multiple rules that gives a user action = allow, a single action = deny will set allow to false
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 and ETag 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
Group /rules
  • GET /rules: reads all rules
  • POST /rules: creates a rule
  • GET /rules/:id: reads rule with :id
  • PUT /rules/:id: updates rule with :id
  • DELETE /rules/:id: deletes rule with :id
Group /logs
  • GET /logs: reads all logs
  • POST /logs: creates rules (takes decision log array)
  • GET /logs/:decisionID: reads rule with :decisionID
Group /replay
  • GET /replay/:decisionID: replays the :decisionID based on the current rules
  • POST /replay/:decisionID: replays the :decisionID based new rules posted (will not change the actual roles, only during the replay)
Group /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

Directories

Path Synopsis
cmd
pkg

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL