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