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