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