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 で使えそうな点も期待しちゃいます。

流れ

request-response
公式ドキュメント引用

流れとしては上図のようにOPAにクエリを投げて、それに対して認可するかどうかの判断がレスポンスで返ってくるというものです。

ポリシー

ポリシーに関してはRegoというDSLで書きます。Regoに関する詳細は割愛します。チュートリアルで雰囲気はつかめると思います。

HTTP API Authorizationチュートリアル

それではHTTP API Authorizationのチュートリアルをやってみましょう。Webサービスを作っていく上で「誰が何をすることができるか」というのはほとんどの場合出て来る課題ではないでしょうか。1から自分でこれを実装するのはなかなか大変ですし、正直なところビジネスロジックのコアなところに集中したいので、こういう部分で時間を消費してしまうのは本意ではないですよね。そのレイヤをわかりやすくヘルプしてくれるのがOPAです。

やりたいこと

やりたいことは下図のとおりです。

SS-2017-12-19-23.29.41

  • 従業員は自分の給料を閲覧できる
    • 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 という意味があります。

ユーザーの関係は下図のような感じです。
SS-2017-12-19-23.54.04

それでは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はマージされたようです

© 2018. SuperSoftware Co., Ltd. All Rights Reserved.