前回のCasbinと同じ領域のOSSで、Open Policy Agentというものを試してみました。(手順の確認と疑問点の調査など)
参考)
https://qiita.com/KWS_0901/items/4ea7fb634a122d2baa13
https://www.openpolicyagent.org/docs/v0.16.2/http-api-authorization/ (original)
今回もDockerを使いますが、docker-composeで一気にサービスの起動までできるものが用意されています。
docker-compose.yml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
version: '2' services: opa: image: openpolicyagent/opa:0.16.2 ports: - 8181:8181 # WARNING: OPA is NOT running with an authorization policy configured. This # means that clients can read and write policies in OPA. If you are # deploying OPA in an insecure environment, be sure to configure # authentication and authorization on the daemon. See the Security page for # details: https://www.openpolicyagent.org/docs/security.html. command: - "run" - "--server" - "--log-format=json-pretty" - "--set=decision_logs.console=true" api_server: image: openpolicyagent/demo-restful-api:0.2 ports: - 5000:5000 environment: - OPA_ADDR=http://opa:8181 - POLICY_PATH=/v1/data/httpapi/authz |
docker-compose -f docker-compose.yml up
ポリシーはREGOという独自言語で書かれています。
example.rego
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
package httpapi.authz # bob is alice's manager, and betty is charlie's. subordinates = {"alice": [], "charlie": [], "bob": ["alice"], "betty": ["charlie"]} # HTTP API request import input default allow = false # Allow users to get their own salaries. allow { some username input.method == "GET" input.path = ["finance", "salary", username] input.user == username } # Allow managers to get their subordinates' salaries. allow { some username input.method == "GET" input.path = ["finance", "salary", username] subordinates[input.user][_] == username } |
opaサーバとapiサーバが立ち上がります。
$ docker ps
61759fa0d6d6 openpolicyagent/demo-restful-api:0.2 “flask run –host=0.…” About an hour ago Up About an hour 0.0.0.0:5000->5000/tcp opa-api_server-1
e9bad5f0b420 openpolicyagent/opa:0.16.2 “/opa run –server -…” About an hour ago Up About an hour 0.0.0.0:8181->8181/tcp opa-opa-1
まずopaサーバに、ポリシーを登録。
$ curl -X PUT –data-binary @example.rego localhost:8181/v1/policies/example
apiサーバに認可の確認。あとopaサーバにユーザ情報の確認。
paswordはどこで管理しているのかと思いましたが、ここではあればなんでもよいようです。
理解を深めるためapiサーバアプリのコード(Flask FW/Python)をコンテナから抜き出してみました。
docker cp opa-api_server-1:/usr/src/app .
/usr/src/app/echo_server.py
1 2 3 4 5 6 7 8 9 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 |
#!/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, url_as_array, token): input_dict = {"input": { "user": user, "path": url_as_array, "method": method, }} 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': ''}) @app.route('/<path:path>') def root(path): user_encoded = request.headers.get('Authorization', "Anonymous:none") if user_encoded: user_encoded = user_encoded.split("Basic ")[1] user, _ = base64.b64decode(user_encoded).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, 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() |
opaサーバは、docker-composeのあと起動して以下のようにログを表示します。以下はapiサーバにaliceの認可確認と、opaサーバにユーザ情報を取得したところです。
また以下よりPlaygroundに移動すると、REGOのポリシーの確認が容易にできます。
https://www.openpolicyagent.org/
userとownwerが違うためfalseになっています。
あとpathについて、データの構造がちがったりkeyのpetsが違う文字列の場合falseになります。
このように直観的にポリシーを確認できるツールがあるのば便利です。
とりあえずここまで初めて使ったメモでした。
(以下2013/07/12追記)
https://www.openpolicyagent.org/docs/latest/#2-try-opa-eval
opaコマンドをつかった評価をするため、この環境で上記サイトのコマンドをためしてみました。(イメージ内コマンドを外部から実行)
input.json
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
{ "servers": [ {"id": "app", "protocols": ["https", "ssh"], "ports": ["p1", "p2", "p3"]}, {"id": "db", "protocols": ["mysql"], "ports": ["p3"]}, {"id": "cache", "protocols": ["memcache"], "ports": ["p3"]}, {"id": "ci", "protocols": ["http"], "ports": ["p1", "p2"]}, {"id": "busybox", "protocols": ["telnet"], "ports": ["p1"]} ], "networks": [ {"id": "net1", "public": false}, {"id": "net2", "public": false}, {"id": "net3", "public": true}, {"id": "net4", "public": true} ], "ports": [ {"id": "p1", "network": "net1"}, {"id": "p2", "network": "net3"}, {"id": "p3", "network": "net2"} ] } |
example2.rego
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
package example default allow := false # unless otherwise defined, allow is false allow := true { # allow is true if... count(violation) == 0 # there are zero violations. } violation[server.id] { # a server is in the violation set if... some server public_server[server] # it exists in the 'public_server' set and... server.protocols[_] == "http" # it contains the insecure "http" protocol. } violation[server.id] { # a server is in the violation set if... server := input.servers[_] # it exists in the input.servers collection and... server.protocols[_] == "telnet" # it contains the "telnet" protocol. } public_server[server] { # a server exists in the public_server set if... some i, j server := input.servers[_] # it exists in the input.servers collection and... server.ports[_] == input.ports[i].id # it references a port in the input.ports collection and... input.ports[i].network == input.networks[j].id # the port references a network in the input.networks collection and... input.networks[j].public # the network is public. } |
$ docker run openpolicyagent/opa:0.16.2 eval “1*2+3”
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
{ "result": [ { "expressions": [ { "value": 5, "text": "1*2+3", "location": { "row": 1, "col": 1 } } ] } ] } |
ところが次のjsonファイルとregoファイルのがエラーがででうまくいきません。フォーマットなどがまずいようですが、調べていくとバージョンがかなり古いことに気づきました。
docker-compose.yml のopa imageを、0.16.22から0.54.0に書き換え、もう一度最初からやり直しました。
$ docker run -v $PWD:/tmp openpolicyagent/opa:0.54.0 eval -i /tmp/input.json -d /tmp/example2.rego “data.example.violation[x]”
1 2 3 4 5 6 7 8 9 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 |
{ "result": [ { "expressions": [ { "value": "busybox", "text": "data.example.violation[x]", "location": { "row": 1, "col": 1 } } ], "bindings": { "x": "busybox" } }, { "expressions": [ { "value": "ci", "text": "data.example.violation[x]", "location": { "row": 1, "col": 1 } } ], "bindings": { "x": "ci" } } ] } |
–fail-defined オプションをつけると echo $? が1になります。
opaコマンドのテストでした。