Project: Open Policy Agent (OPA) (Provisioning / Security & Compliance)
File: http-api-authorization.md

title: "HTTP APIs" kind: tutorial weight: 1 Anything that exposes an HTTP API (whether an individual microservice or an application as a whole) needs to control who can run those APIs and when. OPA makes it easy to write fine-grained, context-aware policies to implement API authorization. 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 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. First create a directory named `bundles` and cd into it. ```sh mkdir bundles cd bundles ``` example.rego: ```live:example:module:openable package httpapi.authz import rego.v1 subordinates := {"alice": [], "charlie": [], "bob": ["alice"], "betty": ["charlie"]} default allow := false allow if { input.method == "GET" input.path == ["finance", "salary", input.user] } allow if { some username input.method == "GET" input.path = ["finance", "salary", username] subordinates == username } ``` Then, build a bundle. ```shell opa build example.rego cd .. ``` You should now see a policy bundle (`bundle.tar.gz`) in your working directory (`./bundles/bundle.tar.gz`). Next, create a `docker-compose.yml` file that runs OPA, a bundle server and the demo web server. docker-compose.yml: ```yaml version: '2' services: opa: image: openpolicyagent/opa:{{< currentdockerversion >}} ports: 8181:8181 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: opa 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. ```shell docker-compose -f docker-compose.yml up ``` Every time the demo web server receives an HTTP request, it asks OPA to decide whether an HTTP API is authorized or not using a single RESTful API call. An example code is , but the crux of the (Python) code is shown below. ```python httpapiuser = request.form['user'] httpapipath_list = request.path.strip("/").split("/") input_dict = { # create input to hand to OPA "input": { "user": httpapiuser, "path": httpapipath_list, # Ex: ["finance", "salary", "alice"] "method": request.method # HTTP verb, e.g. GET, POST, PUT, ... } } rsp = requests.post("http://127.0.0.1:8181/v1/data/httpapi/authz", json=input_dict) if rsp.json()["allow"]: else: ``` The following command will succeed. ```shell 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. ```live:example:input { "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: ```live:example:query data.httpapi.authz ``` The answer returned by OPA for the input above is: ```live:example:output ``` ```shell curl --user bob:password localhost:5000/finance/salary/alice ``` `bob` is not `charlie`'s manager, so the following command will fail. ```shell curl --user bob:password localhost:5000/finance/salary/charlie ``` Suppose the organization now includes an HR department. The organization wants members of HR to be able to see any

Let's extend the policy to handle this. example-hr.rego: ```live:hrexample:module:readonly,openable package httpapi.authz import rego.v1 allow if { input.method == "GET" input.path = ["finance", "salary", _] input.user == hr[_] } hr := ["david"] ``` Build a new bundle with the new policy included. ```shell 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 `maxdelayseconds` 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. ```shell 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 ``` OPA supports the parsing of JSON Web Tokens via the builtin function `io.jwt.decode`. To get a sense of one way the subordinate and HR data might be communicated in the real world, let's try a similar exercise utilizing the JWT utilities of OPA. example-jwt.rego: ```live:jwt_example:module:openable package httpapi.authz import rego.v1 default allow := false allow if { some username input.method == "GET" input.path = ["finance", "salary", username] token.payload.user == username userownstoken } allow if { some username input.method == "GET" input.path = ["finance", "salary", username] token.payload.subordinates[_] == username userownstoken } allow if { input.method == "GET" input.path = ["finance", "salary", _] token.payload.hr == true userownstoken } userownstoken if input.user == token.payload.azp token := {"payload": payload} if { [header, payload, signature] := io.jwt.decode(input.token) } ``` ```live:jwt_example:input:hidden { "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiYWxpY2UiLCJhenAiOiJhbGljZSIsInN1Ym9yZGluYXRlcyI6W10sImhyIjpmYWxzZX0.rz3jTY033z-NrKfwrK89_dcLF7TN4gwCMj-fVBDyLoM", "method": "GET", "path": ["finance", "salary", "alice"], "user": "alice" ``` Build a new bundle for the new policy. ```shell opa build example-jwt.rego ``` For convenience, we'll want to store user tokens in environment variables (they're really long). ```shell export ALICETOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiYWxpY2UiLCJhenAiOiJhbGljZSIsInN1Ym9yZGluYXRlcyI6W10sImhyIjpmYWxzZX0.rz3jTY033z-NrKfwrK89dcLF7TN4gwCMj-fVBDyLoM" export BOBTOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiYm9iIiwiYXpwIjoiYm9iIiwic3Vib3JkaW5hdGVzIjpbImFsaWNlIl0sImhyIjpmYWxzZX0.nlXN4H8UXGA_fXTbgWRx8b40GXpAGQHWluiYVI9qf0" export CHARLIETOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiY2hhcmxpZSIsImF6cCI6ImNoYXJsaWUiLCJzdWJvcmRpbmF0ZXMiOltdLCJociI6ZmFsc2V9.EZdy_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. ```shell 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. ```shell curl --user charlie:password "localhost:5000/finance/salary/alice?token=$BOB_TOKEN" ``` Check that `david` can see `betty`'s salary. ```shell curl --user david:password "localhost:5000/finance/salary/betty?token=$DAVID_TOKEN" ``` Check that `bob` can see `alice`'s salary. ```shell curl --user bob:password "localhost:5000/finance/salary/alice?token=$BOB_TOKEN" ``` Check that `alice` can see her own salary. ```shell curl --user alice:password "localhost:5000/finance/salary/alice?token=$ALICE_TOKEN" ``` Congratulations for finishing the tutorial! You learned a number of things about API authorization with OPA: 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. You can use a remote bundle server for distributing policy and data. The code for this tutorial can be found in the repository.