Python

OPA for HTTP Authorization

Open Policy Agent[1] is a promising, light weight and very generic policy engine to govern authorization is any type of domain. I found this comparion[2] very attractive in evaluating OPA for a project I am currently working on, where they demonstrate how OPA can cater same functionality defined in RBAC, RBAC with Seperation of Duty, ABAC and XACML.  

Here are the steps to a brief demonstration of OPA used for HTTP API authorization based on the sample [3], taking it another level up.

Running OPA Server

First we need to download OPA from [4], based on the operating system we are running on. 

For linux,
 

1
curl -L -o opa https://github.com/open-policy-agent/opa/releases/download/v0.10.3/opa_linux_amd64

Make it executable,

1
chmod 755 ./opa

Once done, we can start OPA policy engine as a server.

1
./opa run --server

Define Data and Rules

Next we need to load data and authorization rules to the server, so it can make decisions. OPA defines these in files in the format of .rego. Below is a sample file I used.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package httpapi.authz
 
subordinates = {"alice": [], "charlie": [], "bob": ["alice"], "betty": ["charlie"]}
 
# HTTP API request
import input as http_api
# http_api = {
#   "path": ["finance", "salary", "alice"],
#   "user": "alice",
#   "method": "GET"
#   "user_agent": "cURL/1.0"
#   "remote_addr": "127.0.0.1"
# }
 
default allow = false
 
# Allow users to get their own salaries.
allow {
  http_api.method = "GET"
  http_api.path = ["finance", "salary", username]
  username = http_api.user
}
 
# Allow managers to get their subordinates' salaries.
allow {
  http_api.method = "GET"
  http_api.path = ["finance", "salary", username]
  subordinates[http_api.user][_] = username
}
 
# Allow managers to edit their subordinates' salaries only if the request came
# from user agent cURL and address 127.0.0.1.
allow {
  http_api.method = "POST"
  http_api.path = ["finance", "salary", username]
  subordinates[http_api.user][_] = username
  http_api.remote_addr = "127.0.0.1"
  http_api.user_agent = "curl/7.47.0"
}

At first it defines a data set, which represents the relationship subordinates. For example as per this dataset, alice is a subordinate of bob. Then it defines 3 rules that will give feedback as ‘allow’.

  • If user tries to get own salary it is allowed.
  • If a user tries to get the salary of a subordinate it is allowed.
  • If a user tries to modify the salary, it is allowed only if it is of a subordinate, request is initiated from remote address ‘127.0.0.1’ and user agent ‘curl/7.47.0’

To load this policy into the OPA engine we use below call.

1
curl -X PUT --data-binary @salary-example.rego  localhost:8181/v1/policies/example

The above policy is stored into a file named ‘salary-example.rego’ and referred in the above command.

Evaluate at API Invocation

Below is a sample API implementation in python, that consults the OPA engine on the decision whether to provide a response or deny as unauthorized.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#!/usr/bin/env python
 
import base64
import os
 
from flask import Flask
from flask import request
import json
import requests
 
import logging
import sys
logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
 
app = Flask(__name__)
 
opa_url = os.environ.get("OPA_ADDR", "http://localhost:8181")
policy_path = os.environ.get("POLICY_PATH", "/v1/data/httpapi/authz")
 
def check_auth(url, user, method, user_agent, remote_addr,url_as_array, token):
    input_dict = {"input": {
        "user": user,
        "path": url_as_array,
        "method": method,
    "user_agent": user_agent,
    "remote_addr": remote_addr
    }}
    if token is not None:
        input_dict["input"]["token"] = token
 
    logging.info("Checking auth...")
    logging.info(json.dumps(input_dict, indent=2))
    try:
        rsp = requests.post(url, data=json.dumps(input_dict))
    except Exception as err:
        logging.info(err)
        return {}
    if rsp.status_code >= 300:
        logging.info("Error checking auth, got status %s and message: %s" % (j.status_code, j.text))
        return {}
    j = rsp.json()
    logging.info("Auth response:")
    logging.info(json.dumps(j, indent=2))
    return j
 
@app.route('/', defaults={'path': ''}, methods = ['GET', 'POST', 'DELETE'])
@app.route('/<path:path>', methods = ['GET', 'POST'])
def root(path):
    user_encoded = request.headers.get('Authorization', "Anonymous:none")
    logging.info("User Agent: %s" % request.user_agent.string)
    logging.info("Remote Address: %s" % request.remote_addr)
    if user_encoded:
        user_encoded = user_encoded.split("Basic ")[1]
    user, _ = base64.b64decode(user_encoded).decode("utf-8").split(":")
    url = opa_url + policy_path
    path_as_array = path.split("/")
    token = request.args["token"] if "token" in request.args else None
    j = check_auth(url, user, request.method, request.user_agent.string, request.remote_addr, path_as_array, token).get("result", {})
    if j.get("allow", False) == True:
        return "Success: user %s is authorized \n" % user
    return "Error: user %s is not authorized to %s url /%s \n" % (user, request.method, path)
 
if __name__ == "__main__":
    app.run()

The function ‘check_auth’ is responsible to retreive the decision from OPA engine, providing the input details required for authorization. Run the above python script with below command. It uses python modules ‘flask’ and ‘request’.

1
python echo_server.py

Now we can try to call this API served by this python server and see the authorization policy in action.

1
curl --user alice:password localhost:5000/finance/salary/alice

Above is allowed based on the 1st rule, user trying to read own salary.

1
curl --user bob:password localhost:5000/finance/salary/alice

Above is allowed based on the 2nd rule, user trying to read the salary of a subordinate.

1
curl -X POST -d "empoyeeID=100&value=2000" --user bob:password localhost:5000/finance/salary/alice

This will be allowed based on the 3rd rule, if the user agent also matches the exact same cURL client version we have defined in the policy.

1
curl -X POST -d "empoyeeID=100&value=2000" --user bob:password localhost:5000/finance/salary/alice

 Even though the previous request was allowed for bob to edit alice’s salary, the above request is failed as a user cannot modify own salary based on the defined rule.

This was a good excercise to understand the power and the behavior of OPA which enjoyed. Hope you too. Cheers!

[1] –https://www.openpolicyagent.org

[2] –https://www.openpolicyagent.org/docs/comparison-to-other-systems.html

[3] –https://www.openpolicyagent.org/docs/http-api-authorization.html

[4] –https://github.com/open-policy-agent/opa/releases

Published on Web Code Geeks with permission by Pushpalanka, partner at our WCG program. See the original article here: OPA for HTTP Authorization

Opinions expressed by Web Code Geeks contributors are their own.

Pushpalanka

Pushpalanka is an undergraduate in Computer Science and Engineering and working on variety of middle-ware solutions. She is an open-source enthusiastic having interests in the fields of Big Data, Distributed Systems and Web Security.She has successfully participated in Google Summer of Code 2012 program.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Back to top button