In this tutorial, you’ll use a simple HTTP web server that accepts any HTTP GET request that you issue and echoes the OPA decision back as text. OPA will fetch policy bundles from a simple bundle server. Both OPA, the bundle server and the web server will be run as containers.
For this tutorial, our desired policy is:
- People can see their own salaries (
GET /finance/salary/{user}
is permitted for{user}
) - A manager can see their direct reports’ salaries (
GET /finance/salary/{user}
is permitted for{user}
’s manager)
This tutorial requires Docker Compose to run a demo web server along with OPA.
Create a policy that allows users to request their own salary as well as the salary of their direct subordinates.
example.rego:
Then, build a bundle.
opa build example.rego
You should now see a policy bundle (bundle.tar.gz
) in your working directory.
2. Bootstrap the tutorial environment using Docker Compose.
Next, create a docker-compose.yml
file that runs OPA, a bundle server and the demo web server.
docker-compose.yml:
version: '2'
services:
opa:
image: openpolicyagent/opa:0.30.2-rootless
ports:
- 8181:8181
# WARNING: OPA is NOT running with an authorization policy configured. This
# means that clients can read and write policies in OPA. If you are
# deploying OPA in an insecure environment, be sure to configure
# authentication and authorization on the daemon. See the Security page for
# details: https://www.openpolicyagent.org/docs/security.html.
command:
- "run"
- "--server"
- "--log-format=json-pretty"
- "--set=decision_logs.console=true"
- "--set=services.nginx.url=http://bundle_server"
- "--set=bundles.nginx.service=nginx"
- "--set=bundles.nginx.resource=bundles/bundle.tar.gz"
depends_on:
- bundle_server
api_server:
image: openpolicyagent/demo-restful-api:0.3
ports:
- 5000:5000
environment:
- OPA_ADDR=http://opa:8181
- POLICY_PATH=/v1/data/httpapi/authz
depends_on:
bundle_server:
image: nginx:1.20.0-alpine
ports:
- 8888:80
volumes:
- ./bundles:/usr/share/nginx/html/bundles
Then run docker-compose
to pull and run the containers.
NOTE: if running “Docker Desktop” (Mac or Windows) you may instead use the docker compose
command.
docker-compose -f docker-compose.yml up
http_api_user = request.form['user']
# Get the path as a list (removing leading and trailing /)
# Example: "/finance/salary/" will become ["finance", "salary"]
http_api_path_list = request.path.strip("/").split("/")
input_dict = { # create input to hand to OPA
"input": {
"user": http_api_user,
"path": http_api_path_list, # Ex: ["finance", "salary", "alice"]
"method": request.method # HTTP verb, e.g. GET, POST, PUT, ...
}
}
# ask OPA for a policy decision
# (in reality OPA URL would be constructed from environment)
rsp = requests.post("http://127.0.0.1:8181/v1/data/httpapi/authz", json=input_dict)
if rsp.json()["allow"]:
# HTTP API allowed
else:
# HTTP API denied
The following command will succeed.
curl --user alice:password localhost:5000/finance/salary/alice
The webserver queries OPA to authorize the request. In the query, the webserver includes JSON data describing the incoming request.
{
"method": "GET",
"path": ["finance", "salary", "alice"],
"user": "alice"
}
When the webserver queries OPA it asks for a specific policy decision. In this case, the integration is hardcoded to ask for /v1/data/httpapi/authz
. OPA translates this URL path into a query:
The answer returned by OPA for the input above is:
{
"allow": true,
"subordinates": {
"alice": [],
"betty": [
"charlie"
],
"bob": [
],
"charlie": []
}
}
4. Check that bob
can see alice
’s salary (because bob
is alice
’s manager.)
curl --user bob:password localhost:5000/finance/salary/alice
bob
is not charlie
’s manager, so the following command will fail.
curl --user bob:password localhost:5000/finance/salary/charlie
6. Change the policy.
Suppose the organization now includes an HR department. The organization wants members of HR to be able to see any salary. Let’s extend the policy to handle this.
example-hr.rego:
package httpapi.authz
allow {
input.method == "GET"
input.path = ["finance", "salary", _]
input.user == hr[_]
}
# David is the only member of HR.
hr = [
"david",
]
Build a new bundle with the new policy included.
opa build example.rego example-hr.rego
The updated bundle will automatically be served by the bundle server, but note that it might take up to the configured max_delay_seconds
for the new bundle to be downloaded by OPA. If you plan to make frequent policy changes you might want to adjust this value in docker-compose.yml
accordingly.
For the sake of the tutorial we included manager_of
and hr
data directly inside the policies. In real-world scenarios that information would be imported from external data sources.
Check that david
can see anyone’s salary.
curl --user david:password localhost:5000/finance/salary/alice
curl --user david:password localhost:5000/finance/salary/bob
curl --user david:password localhost:5000/finance/salary/charlie
curl --user david:password localhost:5000/finance/salary/david
8. (Optional) Use JSON Web Tokens to communicate policy data.
example-jwt.rego:
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiYWxpY2UiLCJhenAiOiJhbGljZSIsInN1Ym9yZGluYXRlcyI6W10sImhyIjpmYWxzZX0.rz3jTY033z-NrKfwrK89_dcLF7TN4gwCMj-fVBDyLoM",
"method": "GET",
"path": ["finance", "salary", "alice"],
"user": "alice"
Build a new bundle for the new policy.
opa build example-jwt.rego
For convenience, we’ll want to store user tokens in environment variables (they’re really long).
export ALICE_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiYWxpY2UiLCJhenAiOiJhbGljZSIsInN1Ym9yZGluYXRlcyI6W10sImhyIjpmYWxzZX0.rz3jTY033z-NrKfwrK89_dcLF7TN4gwCMj-fVBDyLoM"
export BOB_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiYm9iIiwiYXpwIjoiYm9iIiwic3Vib3JkaW5hdGVzIjpbImFsaWNlIl0sImhyIjpmYWxzZX0.n_lXN4H8UXGA_fXTbgWRx8b40GXpAGQHWluiYVI9qf0"
export CHARLIE_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiY2hhcmxpZSIsImF6cCI6ImNoYXJsaWUiLCJzdWJvcmRpbmF0ZXMiOltdLCJociI6ZmFsc2V9.EZd_y_RHUnrCRMuauY7y5a1yiwdUHKRjm9xhVtjNALo"
export BETTY_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiYmV0dHkiLCJhenAiOiJiZXR0eSIsInN1Ym9yZGluYXRlcyI6WyJjaGFybGllIl0sImhyIjpmYWxzZX0.TGCS6pTzjrs3nmALSOS7yiLO9Bh9fxzDXEDiq1LIYtE"
export DAVID_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiZGF2aWQiLCJhenAiOiJkYXZpZCIsInN1Ym9yZGluYXRlcyI6W10sImhyIjp0cnVlfQ.Q6EiWzU1wx1g6sdWQ1r4bxT1JgSHUpVXpINMqMaUDMU"
These tokens encode the same information as the policies we did before (bob
is alice
’s manager, betty
is charlie
’s, david
is the only HR member, etc). If you want to inspect their contents, start up the OPA REPL and execute io.jwt.decode(<token here>, [header, payload, signature])
or open the example above in the Playground.
Let’s try a few queries (note: you may need to escape the ?
characters in the queries for your shell):
Check that charlie
can’t see bob
’s salary.
curl --user charlie:password "localhost:5000/finance/salary/bob?token=$CHARLIE_TOKEN"
Check that charlie
can’t pretend to be bob
to see alice
’s salary.
curl --user charlie:password "localhost:5000/finance/salary/alice?token=$BOB_TOKEN"
Check that david
can see betty
’s salary.
curl --user david:password "localhost:5000/finance/salary/betty?token=$DAVID_TOKEN"
Check that bob
can see alice
’s salary.
Check that alice
can see her own salary.
curl --user alice:password "localhost:5000/finance/salary/alice?token=$ALICE_TOKEN"
Congratulations for finishing the tutorial!
- OPA gives you fine-grained policy control over APIs once you set up the server to ask OPA for authorization.
- You write allow/deny policies to control which APIs can be executed by whom.
- You can import external data into OPA and write policies that depend on that data.
- You can use OPA data structures to define abstractions over your data.
The code for this tutorial can be found in the repository.