KubeCon + CloudNativeCon 2017 - Austinで「How Netflix Is Solving Authorization Across Their Cloud」 にて紹介されたOpen Policy Agent (OPA)。興味深かったので実際にチュートリアルを実践した内容を共有します。
OPAって?
まだ深くは理解しきれていないのですが、 lightweight general-purpose policy engine
と言われているようにAuthZ(認可)に使える便利な、ポリシーを扱ってくれる人、という感じです。 You can integrate OPA as a sidecar, host-level daemon, or library.
と書いてあって、特に sidecar
として kubernetes
で使えそうな点も期待しちゃいます。
流れ
公式ドキュメント引用
流れとしては上図のようにOPAにクエリを投げて、それに対して認可するかどうかの判断がレスポンスで返ってくるというものです。
ポリシー
ポリシーに関してはRegoというDSLで書きます。Regoに関する詳細は割愛します。チュートリアルで雰囲気はつかめると思います。
HTTP API Authorizationチュートリアル
それではHTTP API Authorizationのチュートリアルをやってみましょう。Webサービスを作っていく上で「誰が何をすることができるか」というのはほとんどの場合出て来る課題ではないでしょうか。1から自分でこれを実装するのはなかなか大変ですし、正直なところビジネスロジックのコアなところに集中したいので、こういう部分で時間を消費してしまうのは本意ではないですよね。そのレイヤをわかりやすくヘルプしてくれるのがOPAです。
やりたいこと
やりたいことは下図のとおりです。
- 従業員は自分の給料を閲覧できる
GET /finance/salary/{user}
は{user}
からのリクエストは許可
- マネージャーは自分の配下の従業員の給料も閲覧できる
GET /finance/salary/{user}
は{user}のマネージャー
からのリクエストを許可
1. 環境構築
docker-compose.ymlを作ります。
# docker-compose.yml
version: '2'
services:
opa:
image: openpolicyagent/opa:0.5.13
ports:
- 8181:8181
command:
- "run"
- "--server"
- "--log-level=debug"
api_server:
image: openpolicyagent/demo-restful-api:0.2
ports:
- 5000:5000
environment:
- OPA_ADDR=http://opa:8181
- POLICY_PATH=/v1/data/httpapi/authz
api_server
はチュートリアル用のデモAPIサーバーです。ソースコードはこちらにありますが、やっていることの概要は下記のとおり:
request = ... # HTTP APIのリクエスト
input_dict = { # OPAに渡す情報を作成
"input": {
"user": request.token.username,
"path": request.path.split("/"), # "/finance/salary" を ["finance", "salary"] に分割
"method": request.method # HTTP のメソッド, e.g. GET, POST, PUT, ...
}}
# OPAに問い合わせ
rsp = requests.post("http://opa:8181/v1/data/httpapi/authz", data=input_dict)
if rsp.json()["allow"] {
# HTTP APIが認可された
} else {
# HTTP APIが認可されなかった
}
docker-compose up
を実行して環境を立ち上げましょう。
2. ポリシーをOPAに読み込ませる
上で述べたようにOPAのポリシーはDSLのRegoで記述します。 example.rego
というファイルを作って、次のように記述します。
package httpapi.authz
# bobはaliceのマネージャーでbettyはcharlieのマネージャー
subordinates = {"alice": [], "charlie": [], "bob": ["alice"], "betty": ["charlie"]}
# HTTP APIリクエスト
import input as http_api
default allow = false
# 自分の給料を閲覧(GET)可能とする
allow {
http_api.method = "GET"
http_api.path = ["finance", "salary", username]
username = http_api.user
}
# マネージャーは、その直下の従業員の給料を閲覧(GET)できる
allow {
http_api.method = "GET"
http_api.path = ["finance", "salary", username]
subordinates[http_api.user][_] = username
}
なんとなく雰囲気でRegoは読めるのではないでしょうか?注意する点としては、Regoでは ==
という比較の演算がありません。変数に値がなければ =
は代入になりますし、変数に値が既にあれば比較になります。なので、 username = http_api.user
の1行は、 APIで渡ってきたユーザーが、URLのユーザーと一致していればtrue
という意味があります。
ユーザーの関係は下図のような感じです。
それではREST APIを使ってOPAにポリシーを読み込ませましょう。
curl -X PUT --data-binary @example.rego \
localhost:8181/v1/policies/example
3. alice -> alice
aliceが自分の給料を閲覧できるか確認しましょう。
下のコマンドを実行します:
curl --user alice:password localhost:5000/finance/salary/alice
下のように成功するはずです。
Success: user alice is authorized
4. bob -> alice
aliceのマネージャーであるbobも、aliceの給料を閲覧できるか確認しましょう。
curl --user bob:password localhost:5000/finance/salary/alice
上のコマンドも成功するはずです。
Success: user bob is authorized
5. bob -> charlie
bobはcharlieのマネージャーではないので、閲覧できないことを確認します。
curl --user bob:password localhost:5000/finance/salary/charlie
以下のように失敗します。
Error: user bob is not authorized to GET url /finance/salary/charlie
6. ポリシーの変更
では、人事部門ができて、すべての従業員の給料が閲覧できるようにしましょう。example-hr.rego
を作ります。
package httpapi.authz
import input as http_api
# 人事部門(hr)はすべての給料を閲覧(GET)できる
allow {
http_api.method = "GET"
http_api.path = ["finance", "salary", _]
hr[_] = http_api.user
}
# davidが人事部門だとする
hr = [
"david",
]
OPAに新しいポリシーを読み込ませます。
curl -X PUT --data-binary @example-hr.rego \
http://localhost:8181/v1/policies/example-hr
※チュートリアルなので manager
とか hr
をハードコートしていますが、本来これは外部のリソースを使って設定するものとのこと(そうじゃないと使いものにならないですよね)
7. 新しいポリシーの確認
davidが全員の給料を閲覧できるか確認しましょう。
curl --user david:password localhost:5000/finance/salary/alice
curl --user david:password localhost:5000/finance/salary/bob
curl --user david:password localhost:5000/finance/salary/charlie
curl --user david:password localhost:5000/finance/salary/david
すべて以下のように成功するはずです。
Success: user david is authorized
8. JSON Web Token(JWT)も使ってみよう
OPAはJWTもサポートしていて io.jwt.decode
関数を使うことでJWTのデコードもできるみたいです。上で作った環境はいったん止めて、新しい環境を作りましょう。
そして新たな example-token.rego
を作ります。
package httpapi.authz
import input as http_api
# io.jwt.decodeにはエンコードされたトークンを渡します。そうすると3つのアウトプットが手に入ります:
# header, payload と signature, です。
# このデモではpayloadがあればいいので、他の2つは無視します
token = {"payload": payload} { io.jwt.decode(http_api.token, [_, payload, _]) }
# APIで渡ってきたユーザーと、トークン内のユーザーが一致すること確認
user_owns_token { http_api.user = token.payload.azp }
default allow = false
# 自分の給料の閲覧(GET)は許可
allow {
http_api.method = "GET"
http_api.path = ["finance", "salary", username]
username = token.payload.user
user_owns_token
}
# マネージャーは、その直下の従業員の給料を閲覧(GET)できる
allow {
http_api.method = "GET"
http_api.path = ["finance", "salary", username]
token.payload.subordinates[_] = username
user_owns_token
}
# 人事部門(hr)はすべての給料を閲覧(GET)できる
allow {
http_api.method = "GET"
http_api.path = ["finance", "salary", _]
token.payload.hr = true
user_owns_token
}
OPAに読み込ませます。
curl -X PUT --data-binary @example-token.rego \
localhost:8181/v1/policies/example
テストしやすいようにJWTを環境変数にexportしましょう。
export ALICE_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiYWxpY2UiLCJhenAiOiJhbGljZSIsInN1Ym9yZGluYXRlcyI6W10sImhyIjpmYWxzZX0.rz3jTY033z-NrKfwrK89_dcLF7TN4gwCMj-fVBDyLoM"
export BOB_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiYm9iIiwiYXpwIjoiYm9iIiwic3Vib3JkaW5hdGVzIjpbImFsaWNlIl0sImhyIjpmYWxzZX0.n_lXN4H8UXGA_fXTbgWRx8b40GXpAGQHWluiYVI9qf0"
export CHARLIE_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiY2hhcmxpZSIsImF6cCI6ImNoYXJsaWUiLCJzdWJvcmRpbmF0ZXMiOltdLCJociI6ZmFsc2V9.EZd_y_RHUnrCRMuauY7y5a1yiwdUHKRjm9xhVtjNALo"
export BETTY_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiYmV0dHkiLCJhenAiOiJiZXR0eSIsInN1Ym9yZGluYXRlcyI6WyJjaGFybGllIl0sImhyIjpmYWxzZX0.TGCS6pTzjrs3nmALSOS7yiLO9Bh9fxzDXEDiq1LIYtE"
export DAVID_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiZGF2aWQiLCJhenAiOiJkYXZpZCIsInN1Ym9yZGluYXRlcyI6W10sImhyIjp0cnVlfQ.Q6EiWzU1wx1g6sdWQ1r4bxT1JgSHUpVXpINMqMaUDMU"
jwt.ioに貼ってみてトークンの中身をみてみましょう。
# aliceのトークン
{
"user": "alice",
"azp": "alice",
"subordinates": [],
"hr": false
}
# bobのトークン
{
"user": "bob",
"azp": "bob",
"subordinates": [
"alice"
],
"hr": false
}
# davidのトークン
{
"user": "david",
"azp": "david",
"subordinates": [],
"hr": true
}
それではリクエストをいくつか投げてみます。
charlieはbobの給料を閲覧できないことを確認します。
curl --user charlie:password localhost:5000/finance/salary/bob\?token=$CHARLIE_TOKEN
Error: user charlie is not authorized to GET url /finance/salary/bob
charlieがbobのトークンを使ってなりすましてもaliceの給料を閲覧できないことを確認します。
curl --user charlie:password localhost:5000/finance/salary/alice\?token=$BOB_TOKEN
Error: user charlie is not authorized to GET url /finance/salary/alice
人事部のdavidはbettyの給料が閲覧できること。
curl --user david:password localhost:5000/finance/salary/betty\?token=$DAVID_TOKEN
Success: user david is authorized
aliceのマネージャーであるbobはaliceの給料を閲覧できること
curl --user bob:password localhost:5000/finance/salary/alice\?token=$BOB_TOKEN
Success: user bob is authorized
aliceは自分の給料を閲覧できること
curl --user alice:password localhost:5000/finance/salary/alice\?token=$ALICE_TOKEN
Success: user alice is authorized
おわりに
ここまででチュートリアル完了です!まだProduction Readyではない部分はあるにせよ、これが固まってきたらかなりいい感じにポリシーをWebアプリに取り込んでいけるのではないかと思います。ちなみにこのOPAはHTTP Authに限らず、SSHだったりKubernetesのAdmission Controlとかも連携できるみたいなので、興味がある人はそちらのチュートリアルもやってみましょう。
IstioのMixer AdapterとしてもPRがあるので、この連携が実現するとさらに面白くなりそうですね。
2017-12-20追記:さっそくPRはマージされたようです