1--- 2title: "HTTP APIs" 3kind: tutorial 4weight: 1 5--- 6 7Anything 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. 8 9## Goals 10 11In this tutorial, you'll use a simple HTTP web server that accepts any HTTP GET 12request that you issue and echoes the OPA decision back as text. Both OPA and 13the web server will be run as containers. 14 15For this tutorial, our desired policy is: 16 17* People can see their own salaries (`GET /finance/salary/{user}` is permitted for `{user}`) 18* A manager can see their direct reports' salaries (`GET /finance/salary/{user}` is permitted for `{user}`'s manager) 19 20## Prerequisites 21 22This tutorial requires [Docker Compose](https://docs.docker.com/compose/install/) to run a demo web server along with OPA. 23 24## Steps 25 26### 1. Bootstrap the tutorial environment using Docker Compose. 27 28First, create a `docker-compose.yml` file that runs OPA and the demo web server. 29 30**docker-compose.yml**: 31 32```yaml 33version: '2' 34services: 35 opa: 36 image: openpolicyagent/opa:{{< current_docker_version >}} 37 ports: 38 - 8181:8181 39 # WARNING: OPA is NOT running with an authorization policy configured. This 40 # means that clients can read and write policies in OPA. If you are 41 # deploying OPA in an insecure environment, be sure to configure 42 # authentication and authorization on the daemon. See the Security page for 43 # details: https://www.openpolicyagent.org/docs/security.html. 44 command: 45 - "run" 46 - "--server" 47 - "--log-format=json-pretty" 48 - "--set=decision_logs.console=true" 49 api_server: 50 image: openpolicyagent/demo-restful-api:0.2 51 ports: 52 - 5000:5000 53 environment: 54 - OPA_ADDR=http://opa:8181 55 - POLICY_PATH=/v1/data/httpapi/authz 56``` 57 58Then run `docker-compose` to pull and run the containers. 59 60```shell 61docker-compose -f docker-compose.yml up 62``` 63 64Every time the demo web server receives an HTTP request, it 65asks OPA to decide whether an HTTP API is authorized or not 66using a single RESTful API call. An example code is [here](https://github.com/open-policy-agent/contrib/blob/master/api_authz/docker/echo_server.py), 67but the crux of the (Python) code is shown below. 68 69```python 70 71# Grab basic information. We assume user is passed on a form. 72http_api_user = request.form['user'] 73 74# Get the path as a list (removing leading and trailing /) 75# Example: "/finance/salary/" will become ["finance", "salary"] 76http_api_path_list = request.path.strip("/").split("/") 77 78input_dict = { # create input to hand to OPA 79 "input": { 80 "user": http_api_user, 81 "path": http_api_path_list, # Ex: ["finance", "salary", "alice"] 82 "method": request.method # HTTP verb, e.g. GET, POST, PUT, ... 83 } 84} 85# ask OPA for a policy decision 86# (in reality OPA URL would be constructed from environment) 87rsp = requests.post("http://127.0.0.1:8181/v1/data/httpapi/authz", json=input_dict) 88if rsp.json()["allow"]: 89 # HTTP API allowed 90else: 91 # HTTP API denied 92 93``` 94 95### 2. Load a policy into OPA. 96 97In another terminal, create a policy that allows users to 98request their own salary as well as the salary of their direct subordinates. 99 100**example.rego**: 101 102```live:example:module:openable 103package httpapi.authz 104 105# bob is alice's manager, and betty is charlie's. 106subordinates = {"alice": [], "charlie": [], "bob": ["alice"], "betty": ["charlie"]} 107 108# HTTP API request 109import input 110 111default allow = false 112 113# Allow users to get their own salaries. 114allow { 115 some username 116 input.method == "GET" 117 input.path = ["finance", "salary", username] 118 input.user == username 119} 120 121# Allow managers to get their subordinates' salaries. 122allow { 123 some username 124 input.method == "GET" 125 input.path = ["finance", "salary", username] 126 subordinates[input.user][_] == username 127} 128``` 129 130Then load the policy via OPA's REST API. 131 132```shell 133curl -X PUT --data-binary @example.rego \ 134 localhost:8181/v1/policies/example 135``` 136 137### 3. Check that `alice` can see her own salary. 138 139The following command will succeed. 140 141```shell 142curl --user alice:password localhost:5000/finance/salary/alice 143``` 144 145The webserver queries OPA to authorize the request. In the query, the webserver 146includes JSON data describing the incoming request. 147 148```live:example:input 149{ 150 "method": "GET", 151 "path": ["finance", "salary", "alice"], 152 "user": "alice" 153} 154``` 155 156When the webserver queries OPA it asks for a specific policy decision. In this 157case, the integration is hardcoded to ask for `/v1/data/httpapi/authz`. OPA 158translates this URL path into a query: 159 160```live:example:query 161data.httpapi.authz 162``` 163 164The answer returned by OPA for the input above is: 165 166```live:example:output 167``` 168 169### 4. Check that `bob` can see `alice`'s salary (because `bob` is `alice`'s manager.) 170 171```shell 172curl --user bob:password localhost:5000/finance/salary/alice 173``` 174 175### 5. Check that `bob` CANNOT see `charlie`'s salary. 176 177`bob` is not `charlie`'s manager, so the following command will fail. 178 179```shell 180curl --user bob:password localhost:5000/finance/salary/charlie 181``` 182 183### 6. Change the policy. 184 185Suppose the organization now includes an HR department. The organization wants 186members of HR to be able to see any salary. Let's extend the policy to handle 187this. 188 189**example-hr.rego**: 190 191```live:hr_example:module:read_only,openable 192package httpapi.authz 193 194import input 195 196# Allow HR members to get anyone's salary. 197allow { 198 input.method == "GET" 199 input.path = ["finance", "salary", _] 200 input.user == hr[_] 201} 202 203# David is the only member of HR. 204hr = [ 205 "david", 206] 207``` 208 209Upload the new policy to OPA. 210 211```shell 212curl -X PUT --data-binary @example-hr.rego \ 213 http://localhost:8181/v1/policies/example-hr 214``` 215 216For the sake of the tutorial we included `manager_of` and `hr` data directly 217inside the policies. In real-world scenarios that information would be imported 218from external data sources. 219 220### 7. Check that the new policy works. 221Check that `david` can see anyone's salary. 222 223```shell 224curl --user david:password localhost:5000/finance/salary/alice 225curl --user david:password localhost:5000/finance/salary/bob 226curl --user david:password localhost:5000/finance/salary/charlie 227curl --user david:password localhost:5000/finance/salary/david 228``` 229 230### 8. (Optional) Use JSON Web Tokens to communicate policy data. 231OPA supports the parsing of JSON Web Tokens via the builtin function `io.jwt.decode`. 232To get a sense of one way the subordinate and HR data might be communicated in the 233real world, let's try a similar exercise utilizing the JWT utilities of OPA. 234 235Shut down your `docker-compose` instance from before with `^C` and then restart it to 236ensure you are working with a fresh instance of OPA. 237 238**example.rego**: 239 240```live:jwt_example:module:openable 241package httpapi.authz 242 243default allow = false 244 245# Allow users to get their own salaries. 246allow { 247 some username 248 input.method == "GET" 249 input.path = ["finance", "salary", username] 250 token.payload.user == username 251 user_owns_token 252} 253 254# Allow managers to get their subordinate' salaries. 255allow { 256 some username 257 input.method == "GET" 258 input.path = ["finance", "salary", username] 259 token.payload.subordinates[_] == username 260 user_owns_token 261} 262 263# Allow HR members to get anyone's salary. 264allow { 265 input.method == "GET" 266 input.path = ["finance", "salary", _] 267 token.payload.hr == true 268 user_owns_token 269} 270 271# Ensure that the token was issued to the user supplying it. 272user_owns_token { input.user == token.payload.azp } 273 274# Helper to get the token payload. 275token = {"payload": payload} { 276 [header, payload, signature] := io.jwt.decode(input.token) 277} 278``` 279 280```live:jwt_example:input:hidden 281{ 282 "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiYWxpY2UiLCJhenAiOiJhbGljZSIsInN1Ym9yZGluYXRlcyI6W10sImhyIjpmYWxzZX0.rz3jTY033z-NrKfwrK89_dcLF7TN4gwCMj-fVBDyLoM", 283 "method": "GET", 284 "path": ["finance", "salary", "alice"], 285 "user": "alice" 286``` 287 288And load it into OPA: 289 290```shell 291curl -X PUT --data-binary @example.rego \ 292 localhost:8181/v1/policies/example 293``` 294 295For convenience, we'll want to store user tokens in environment variables (they're really long). 296 297```shell 298export ALICE_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiYWxpY2UiLCJhenAiOiJhbGljZSIsInN1Ym9yZGluYXRlcyI6W10sImhyIjpmYWxzZX0.rz3jTY033z-NrKfwrK89_dcLF7TN4gwCMj-fVBDyLoM" 299export BOB_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiYm9iIiwiYXpwIjoiYm9iIiwic3Vib3JkaW5hdGVzIjpbImFsaWNlIl0sImhyIjpmYWxzZX0.n_lXN4H8UXGA_fXTbgWRx8b40GXpAGQHWluiYVI9qf0" 300export CHARLIE_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiY2hhcmxpZSIsImF6cCI6ImNoYXJsaWUiLCJzdWJvcmRpbmF0ZXMiOltdLCJociI6ZmFsc2V9.EZd_y_RHUnrCRMuauY7y5a1yiwdUHKRjm9xhVtjNALo" 301export BETTY_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiYmV0dHkiLCJhenAiOiJiZXR0eSIsInN1Ym9yZGluYXRlcyI6WyJjaGFybGllIl0sImhyIjpmYWxzZX0.TGCS6pTzjrs3nmALSOS7yiLO9Bh9fxzDXEDiq1LIYtE" 302export DAVID_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiZGF2aWQiLCJhenAiOiJkYXZpZCIsInN1Ym9yZGluYXRlcyI6W10sImhyIjp0cnVlfQ.Q6EiWzU1wx1g6sdWQ1r4bxT1JgSHUpVXpINMqMaUDMU" 303``` 304 305These 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). 306If 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. 307 308Let's try a few queries (note: you may need to escape the `?` characters in the queries for your shell): 309 310Check that `charlie` can't see `bob`'s salary. 311 312```shell 313curl --user charlie:password localhost:5000/finance/salary/bob?token=$CHARLIE_TOKEN 314``` 315 316Check that `charlie` can't pretend to be `bob` to see `alice`'s salary. 317 318```shell 319curl --user charlie:password localhost:5000/finance/salary/alice?token=$BOB_TOKEN 320``` 321 322Check that `david` can see `betty`'s salary. 323 324```shell 325curl --user david:password localhost:5000/finance/salary/betty?token=$DAVID_TOKEN 326``` 327 328Check that `bob` can see `alice`'s salary. 329 330```shell 331curl --user bob:password localhost:5000/finance/salary/alice?token=$BOB_TOKEN 332``` 333 334Check that `alice` can see her own salary. 335 336```shell 337curl --user alice:password localhost:5000/finance/salary/alice?token=$ALICE_TOKEN 338``` 339 340## Wrap Up 341 342Congratulations for finishing the tutorial! 343 344You learned a number of things about API authorization with OPA: 345 346* OPA gives you fine-grained policy control over APIs once you set up the 347 server to ask OPA for authorization. 348* You write allow/deny policies to control which APIs can be executed by whom. 349* You can import external data into OPA and write policies that depend on 350 that data. 351* You can use OPA data structures to define abstractions over your data. 352 353The code for this tutorial can be found in the 354[open-policy-agent/contrib](https://github.com/open-policy-agent/contrib) 355repository. 356