「誰」が「何」をできるかをOPAのレスポンスで返す

OPA(Open Policy Agent)で「○○をやっていいですか?」に対して「Allow/Deny」の返答がくるパターンについていくつか記事にしました。

今回は、 「誰」が「何」をしていいか をOPAからのレスポンスで知る方法について検証したので共有します。

どういった場面でこれを知りたくなるかと言うと、

例えばSPA(Single Page Application)で、 ログイン中のユーザー が「何」をして良いか知りたいときが考えられます。

この情報があることによって、

  • 新規作成ボタンを表示する
  • 編集ボタンを表示する
  • 削除ボタンを表示する
  • 特定のメニューを表示する

などの判断をSPA側で行うことができるようになります。

今回の記事で作成するPolicyもOPAのSlackでやりとりがされていた内容をちょっとカスタマイズさせてもらったものになります。

ドキュメントには登場しないようなユースケースがコアメンバーによってPlaygroundでシェアされていたりするので、OPAに興味がある方はぜひSlackに参加することをおすすめします!

slack.openpolicyagent.org

権限の定義

今回の例では以下のような状況があると仮定します。

  • article というリソースがある
  • alicearticleread できるし write もできる権限がある
  • bobarticleread する権限しかもっていない

これをOPAのRegoで表現すると次のようになります。

package foo.bar

permission["alice"] = [
    { "resources": ["article"], "actions": ["read", "write"] }
]
permission["bob"] = [
    { "resources": ["article"], "actions": ["read"]}
]

実際はこの permission をハードコードすることは無く、データベースなりどこかに保持されているところからOPAに読みこませることになりますが、Playground上で表現するために直でデータを書いておきます。

指定したユーザーの権限を取得

では、データが用意されたのでここから aliceの権限を取得 したいとなったらどうすれば良いか。 欲しいのは以下の情報です。

[
    { "resources": ["article"], "actions": ["read", "write"] }
]

OPAでは「Allow/Deny」な答えを返すことが多いのですが、オブジェクトを返すこともできます。 これがドキュメントにも書かれている「Generating Objects」をする方法です。

www.openpolicyagent.org

OPAへのリクエストの input{ "user": "alice" } とした場合に、 alice の権限が返ってくるようにするには次のようなルールを定義します。

user_permission = perm {
    perm := permission[input.user]
}

ここでポイントとなる点は user_permission = perm の、 = perm の部分です。 普通のルールの場合は key だけ指定して value 側を指定することはありません。

value 側を指定(ここで言う perm )し、その値をルールの中身で用意することでレスポンスとして返すことができます。

実際に試してみましょう!下図はPlayground実行している様子です。

PlaygroundにはちょっとしたTipsがあって、図のように、選択した部分のルールのみを実行してOutputを確認することができます。

f:id:kenev:20190506222034p:plain

下記リンクからPlaygroundが実行できるので、ぜひ試してみてください!

play.openpolicyagent.org

Inputを

{
  "user": "alice"
}

としたことで、 user_permission のみを実行した結果が

[
  {
    "actions": [
      "read",
      "write"
    ],
    "resources": [
      "article"
    ]
  }
]

となり、 alice の権限がレスポンスとしてオブジェクトで返ってきているのがわかります。

また、 bob であれば以下のように、 article に対して read しか許可されていないのがわかります。

[
  {
    "actions": [
      "read"
    ],
    "resources": [
      "article"
    ]
  }
]

このように、「Allow/Deny」のレスポンス以外にも、オブジェクトでレスポンスを返すことができるということもわかりました!

そして、例えばこのレスポンスを使ってUI側では

  • write があるから新規作成、編集、削除ボタンを表示する

という処理が書けて、ログインしているユーザーの権限に応じて画面の見た目を変えることができます。

やっていいかどうかの判断

「誰」が「何」をしていいか を確認する方法についてはこれでOKですが、実際に「やっていいかどうか」の判断も必要になります。

具体的には以下の質問への答えが必要になります。

  • alicearticlewrite できますか?
  • bobarticlewrite できますか?

この質問をするときのInputはきっとこんなJSONになると思います。

{
  "user": "alice",
  "action": "write",
  "resource": "article"
}

そしてこの質問に対して、「Allow/Deny」の答えを出すためのRuleはRegoで以下のように表現することができます。

default allow = false
allow {
    permission[input.user][i].resources[_] == input.resource
    permission[input.user][i].actions[_] == input.action
}
  • permission["alice"] の中のデータを順番にチェック
  • i番目の情報の resourcesarticle が含まれるかチェック
  • i番目の情報の actionswrite が含まれるかチェック

結果として、 alice{ "resources": ["article"], "actions": ["read", "write"] } という permission を持っているので、すべて true となるので、結果も true です!

逆に bob の情報を下のように渡してみるとどうなるでしょう?

{
  "user": "bob",
  "action": "write",
  "resource": "article"
}

bob には { "resources": ["article"], "actions": ["read"]} という permission しか定義されていないので、 write できるかという質問に対しては false になります。

よって、このリクエストの結果は false になります。

まとめ

  • OPAは「Allow/Deny」以外にもオブジェクトをレスポンスとして返すことができる
  • 「誰」が「何」をできるか、を返すことでUIでその情報を使うことができる(ボタンの表示・非表示など)

『JSJ 358: Pickle.js, Tooling, and Developer Happiness with Anatoliy Zaslavskiy』を聴いて

僕のお気に入りのPodcastの一つにJavaScript Jabberというものがあります。

devchat.tv

JavaScript」とPodcastのタイトルには書いてありますが、ゴリッとJavaScriptのテクニカルな話をするわけではないです(昔はそうだったかも)。 どちらかと言うとJavaScript界隈に何かしら関わりのある人物をゲストに呼んで、その人と一つのテーマについて1時間ほどトークをするという流れです。 ホストは Charles Max Wood氏 です。Ruby Rogues, Adventures in Angularなど様々なPodcastを配信している方です。

なんとなーくジョギングなりしながら聴いているのですが、先日聴いたエピソードが結構面白かったので紹介したいと思います。英語ですが、トピックに興味がある方はぜひ聴いてください!

Pickle.js, Tooling, and Developer Happiness

今回のゲストはAnatoliy Zaslavskiy氏というHover Inc.所属のSenior JavaScript R&D Developerでした。

devchat.tv

当初のテーマはきっとAnatoliy氏が作ったPickle.jsと自動テストに関する話だったと思うのですが、途中からDeveloper Happinessに話が逸れ(?)ます。

このDeveloper Happinessに関する議論が特に興味深かったです。

目次

  • [00:00] イントロ(Pickle.jsの紹介)
  • [11:38] テスト文化にまつわるよくある問題
  • [17:30] 自動テストの意義
  • [28:24] Employee Retention(従業員をどう定着させるか)
  • [38:24] なぜSeniorよりJunior Developerを雇うほうがスケールするのか
  • [47:10] ツールやライブラリを作っていく上で注意すること
  • [54:15] Picks

時間は、Podcast内でトピックが話されているだいたいの時間を示してます。Picksとは、毎回Podcastの最後に参加者が全員何かしら記事だったり技術ネタだったり、視聴者にシェアしたい内容を紹介するコーナーです。

Pickle.js

ゲストのAnatoliy氏が作っているPickle.jsというのはe2eテストフレームワークです。

www.picklejs.com

僕もまだ触れていないので、このPodcastと公式サイトから得た情報をまとめると

  • Cucumber っぽく(Gherkin Syntaxで)ノンプログラマーでもテストを書ける
  • 既存のテストフレームワーク(執筆時点ではCypressだけ)を拡張して実装しているので、理論上はテストフレームワークを変えてもテストが動く
  • ページ内の要素を選択するためのセレクターは一箇所にKey Valueで定義し、そのKeyをテストの文章内で使用できる

ポイントは以下のスライドでも簡潔に紹介されています。

僕の中ではpython製の Robot Framework に似てるのかなという印象があります。

robotframework.org

自動テストがなぜ必要か?

なぜ自動テストが必要なのかというトピックについて、「自動テストの無い大企業でのプロジェクトの問題」が例に出されています。

  • プロジェクトに多数のエンジニア(数100)が関わっている
  • ある機能を開発者が実装する
  • QAがその機能をテストして、1週間後にバグなどフィードバックする
  • 開発者はバグを修正してQAに再び渡す
  • また1週間後に、今度はさらに発生したバグなどフィードバックする

ここでの大きな問題としては開発者が、自分の実装した範囲が他にどのような影響を及ぼすのかということを知る術が無い(コード全体を熟知しない限り)。

そして、何かを壊してしまった場合に即座にフィードバックを得ることができないという点です。

流れが悪いために時間はどんどん過ぎていき、エンジニアとしてはほとんどバグを直していることしかしていないという負のスパイラルに陥ります。

自動テストを導入することで、フィードバックのスピードが劇的に早くなるため、上の問題の改善に大きく貢献するというトークをしています。

自分が行った変更が 既存の機能を壊していない ということが 一定のレベル保証される ことで、開発者も変更を加えるときに心理的安全性を保つことができるようになります。

トーク内でも「Eliminate the fear of change(変更することへの恐怖を排除する)」することで「Innovative(革新的)」になれると言っています。

この意見には僕もとても賛同しているのですが、「一定のレベル保証される」というところが、「良いテスト」を書いていることが前提になっていることを忘れちゃいけないのかなとは思います。

「良いテスト」の定義がまた難しいのと、トーク内にも特に触れられていない部分なのですが、以下の書籍の内容が僕は好きです。

The Way of the Web Tester: A Beginner's Guide to Automating Tests

The Way of the Web Tester: A Beginner's Guide to Automating Tests

日本語版はこちら

  • 意味のあるテストになっているか
  • 壊れやすいテストを書いていないか
  • 本当に意図したテストを行っているか(テスト自体がバグってないか)

などいろいろな観点がありますが、「ただテストをたくさん書けばいい」というわけではないという点がポイントです。

Developer HappinessとEmployee Retention

後半はDeveloper HappinessとEmployee Retentionに関する話題になります。ここが結構ヒートアップして前半の内容が薄れちゃいます(笑)

なぜエンジニアは会社を辞めるのか?

エンジニアが会社を辞める理由としてCharles氏は2つの観点を述べています。

  • 人(People)
  • プロジェクトそのもの(Project)

自分と「合わない」人と毎日仕事を続けるのはしんどいですし、そもそもプロジェクトが解決しようとしている問題に興味が無ければモチベーションも上がりません。 これには僕も同意見です。これに加えて「給料」も結構大きな理由なのではないかと思います。

同じ仕事条件で、より給料の高いオファーがあったとしたら会社を変えるのは結構自然だと思います。

ただし、「仕事条件」は僕の中では「仕事場の環境(人)」も含まれていると思うので、 単純な比較にはならない とも思います。

Anatoliy氏は「辞める」のは「Unhappy」だからであり、「Unhappiness」は「Cognitive dissonance(認知的不協和)」から来ると述べています。

ja.wikipedia.org

  • 自分がやるべきこと
  • 自分が実際にやっていること

のギャップに苦しんでいるが、その「現状の状態」に慣れ過ぎちゃっているがために、どうしたら改善できるか 想像ができない状況に陥ってしまう と。

この状況に陥ったときにどうすべきかの意見がCharles氏とAnatoliy氏で異なった点が面白いです。

IT業界は今のところ人手不足なので、転職してしまうのが一番シンプルだとCharles氏は述べています。

それに対してAnatoliy氏は恋愛を例に出します。

「好きだと思った女性と付き合うが、付き合ってみたらなんか違ったので別れてまた違う女性と付き合う。」

ということを繰り返して良いのかどうかという点に注目します。

「問題は付き合っている女性側にあるのではなく、自分にあるのではないか。」

という視点の切り替えが必要なのではないかと問います。

この視点の切り替えとは「課題の分離」を行うということなのかなと僕は感じました。

  • 自分の課題
  • 他者の課題

を整理することで、自分の課題に気づいたり、他者の課題に踏み込んでしまっていることに気づいたりできると思います。

「課題の分離」について以下2つの書籍を最近読んで僕も「なるほどー」とうなづく点が多かったので合わせて載せておきます!

booth.pm

嫌われる勇気―――自己啓発の源流「アドラー」の教え

嫌われる勇気―――自己啓発の源流「アドラー」の教え

「課題の分離」を行うことで、「現状」を改善する道筋が見えてくるかもしれないというのは確かにその通りだなと感じました。

ただし、それでも尚道筋が見えない環境、抜け出した方が良い環境(Toxic Environment)もあるというのは双方で一致している意見でしたし、僕もそう思います。

補足:僕は付き合う相手を変えることについて否定的ではないです。自分に本当に合う人をそうやって見つけられるとも思います!

エンジニアのRetentionを上げる

エンジニアのRetentionを上げるポイントとして述べられているのは「エンジニアは多種多様であると理解すること」と言っています。

  • いろんな感情を持っている
  • 興味の対象は人それぞれ
  • ワークスタイルも人それぞれ

人それぞれで異なるものをうまく組み合わせて活用(utilize)できるようにする、と述べていますがこれはとても難しいと思います。

人・組織・リーダーシップに関わってくる話だと僕は捉えていますが、まだまだ僕はこの点について勉強中で、特に以下の書籍を読んで整理したいと思っています。

スモール・リーダーシップ チームを育てながらゴールに導く「協調型」リーダー

スモール・リーダーシップ チームを育てながらゴールに導く「協調型」リーダー

ピープルウエア 第3版

ピープルウエア 第3版

また、 僕のブログメンターでもある カックさん の以下記事も参考になる情報満載なので要チェックです!

kakakakakku.hatenablog.com

まとめ

Developer Happinessの議論がとても勉強になりました。「これだ!」という単純な答えが無いのが難しいんですけど、様々な人のこう言った話を聴きながら、自分の生き方も改善していきたいと強く感じました。

後半のトークが濃すぎてPickle.jsの話がだいぶ薄れた印象があります(笑)Pickle.jsのコンセプトは面白いので試してみたいです。

参考

  • Testabilityという観点で画面(View)の状態をカタログのように可視化して管理できるStorybookが紹介されました

storybook.js.org

  • Picksで紹介されたDevOpsの本。物語になっていて読みやすくて面白い本です。

The DevOps 逆転だ!

The DevOps 逆転だ!

OPAでANDとORの条件を組み合わせる

OPAのSlackでやりとりされている内容とかが流れていってしまうのが勿体無いので残していこうと思います。(将来的に索引にできたら良いかなと)

f:id:kenev:20190331164752p:plain

上図のようなパターンを考えてみましょう。文で表現すると以下のとおりです。

p1, p2, p3, additional_pがすべてtrueの場合に許可する。additional_pはp9またはp10がtrueであれば良い。

これをRegoで書くと次のようになります。

package play

default allow = false

# this rule says "allow is true if p1 and p2 and p3 and additional_p"
allow {
    p1
    p2
    p3
    additional_p
}

p1 { input.x == 1 }
p2 { input.y == 2 }
p3 { input.z == 3 }

# additional_p is true if p9 or p10
additional_p {
    p9
}

additional_p {
    p10
}

p9 {
    input.a == "foo"
}

p10 {
    input.a == "bar"
}

ポイントは以下のとおりかと!

  • ルールから別のルールを参照できる
  • ルールの中身に書いた式は暗黙的にANDとして扱われる(allowの中のp1, p2, p3, additional_p)
  • 同名のルールは暗黙的にORとして扱われる(additional_p)

試してみましょう!

次のJSONをInputに入れてみます。

{
  "a": "bar",
  "x": 1,
  "y": 2,
  "z": 3
}

そうするとOutputは次のようになります。

{
  "result": {
    "additional_p": true,
    "allow": true,
    "p1": true,
    "p10": true,
    "p2": true,
    "p3": true
  }
}

true の結果が全部返ってくるのでちょっと見づらいですけど、 allowtrue なのがわかります。

Inputの "a": "foo" とした場合でも allowtrue です! なぜなら p9 OR p10 であれば( input.afoo または bar であれば ) additional_ptrue だからです。

Rego Playgroundは下になりますので実際にInputを変えてみてOutputを確認してみると動きの違いを実際にみることができます!

play.openpolicyagent.org

おわりに

  • PolicyのORやANDは暗黙的に行われる
  • ルールの中からルールを呼び出すことでルールを整理できる

参考

kenfdev.hateblo.jp

CircleCIで実行バイナリをDLしてCI内でキャッシュしながら使う

CI内でどこかしらから実行バイナリをダウンロードして、それを使うことがあると思います。 最近では僕はOPAのテストをCircleCIで実行したかったので、OPAのバイナリをダウンロードしてテストを実行しました。

kenfdev.hateblo.jp

毎回バイナリをダウンロードしてCircleCIを回すのもコストが高いので、CircleCIのキャッシュを使う方法について共有します!

実行バイナリをDockerイメージに入れてそれをCIで使う方法もありますが、今回は実行バイナリをダウンロードして使う方法でやります!

キャッシュせずにやった場合

まずはキャッシュのことを考えずにCIしてみると、以下のような流れになります。(OPAの例です)

version: 2
jobs:
  build:
    docker:
      - image: circleci/buildpack-deps:jessie

    steps:
      - checkout
      - run:
          name: Download OPA
          command: |
            mkdir .bin
            wget -O .bin/opa https://github.com/open-policy-agent/opa/releases/download/v0.10.6/opa_linux_amd64
            sudo chmod +x .bin/opa
      - run:
          name: Test Policies
          command: .bin/opa test .
  • チェックアウト
  • OPAのバイナリをGitHubからダウンロードして実行権限付与
  • OPAのテスト実行

という感じです。特につまるところは無いと思います。

ただしこれにはパフォーマンスの問題があって、CIの度にOPAのバイナリをGitHubからダウンロードしてくるのが無駄です。 このバイナリもそんなにしょっちゅう変わるものでも無いので、ダウンロードが一度で済ませられたら良いですよね。

そこで使えるのがCircleCIのキャッシュです!

キャッシュして効率化

CircleCIには save_cacherestore_cache という便利な機能があります。

circleci.com

キーを基にCircleCI側にファイルやディレクトリなどをキャッシュしておくことができます。 よく見る場面としては、依存パッケージなどをキャッシュしておくときです。( node_modulesvendors ディレクトリなど)

ポイントとなるのがキャッシュの「キーの決め方」です。

まずはあまり工夫をせずにキャッシュの手順を加えて見ます。

version: 2
jobs:
  build:
    docker:
      - image: circleci/buildpack-deps:jessie

    steps:
      - checkout
      - restore_cache:
          key: opa-cache-0.10.6
      - run:
          name: Download OPA
          command: |
            if [ ! -f .bin/opa ]; then
              mkdir .bin
              wget -O .bin/opa https://github.com/open-policy-agent/opa/releases/download/v0.10.6/opa_linux_amd64
              sudo chmod +x .bin/opa
            fi
      - run:
          name: Test Policies
          command: .bin/opa test .
      - save_cache:
          key: opa-cache-0.10.6
          paths:
            - .bin
  • チェックアウト
  • キャッシュがあれば復元
  • OPAのバイナリが 無ければ GitHubからダウンロードして実行権限付与
  • OPAのテスト実行
  • OPAのバイナリをキャッシュしておく

restore_cachesave_cache のキーには opa-cache-0.10.6 を使っています。

これは、OPAのバイナリのバージョンが変わったらキャッシュもちゃんと変わるようにするためです。 ただし、これってバージョンが変わるたびに少なくとも3箇所( 0.10.6 となってる部分)は変えないといけないので微妙ですよね。

ということでどうにか1箇所に集約できないかと考えてしまいます。

ちょっとトリッキーなのが、環境変数restore_cachesave_cachekey に使えない点です(少なくとも僕の2019/04/19時点での認識では)。

      - restore_cache:
          key: opa-cache-${OPA_VERSION}

上記のように書けるとすっきりしそうなんですけど、これができません。ではどうするかというと、以下のフォーラムでやり方を見つけることができました。

discuss.circleci.com

次のように書き換えることができます。

      - run:
          name: Setup Environment Variables and Version file
          command: |
            echo 'export OPA_VERSION="0.10.6"' >> $BASH_ENV
            echo "${OPA_VERSION}" > _opa_version_
      - restore_cache:
          key: opa-cache-{{ checksum "_opa_version_" }}
  • 環境変数OPA_VERSIONにバージョン番号を入れて
  • バージョン番号を _opa_version_ というファイルにリダイレクト
  • _opa_version_checksum をキャッシュのキーとして使う

言われてみれば「あ、その手があったか!」ってなります。

ということでこの方法で config.yml を書き換えると下のようになります。

version: 2
jobs:
  build:
    docker:
      - image: circleci/buildpack-deps:jessie

    steps:
      - checkout
      - run:
          name: Setup Environment Variables and Version file
          command: |
            echo 'export OPA_VERSION="0.10.6"' >> $BASH_ENV
            echo "${OPA_VERSION}" > _opa_version_
      - restore_cache:
          keys:
            - opa-cache-{{ checksum "_opa_version_" }}
      - run:
          name: Download OPA
          command: |
            if [ ! -f .bin/opa ]; then
              mkdir .bin
              wget -O .bin/opa https://github.com/open-policy-agent/opa/releases/download/v${OPA_VERSION}/opa_linux_amd64
              sudo chmod +x .bin/opa
            fi
      - run:
          name: Test Policies
          command: .bin/opa test .
      - save_cache:
          key: opa-cache-{{ checksum "_opa_version_" }}
          paths:
            - .bin

こうすることで下の一行を変えるだけで、OPAの実行バイナリのバージョンを切り替えることができます!

echo 'export OPA_VERSION="0.10.6"' >> $BASH_ENV

今の所の僕の最適解はこのやり方になるのですが、他にも良い方法があればぜひとも知りたいところです!

おわりに

  • 依存ライブラリ以外にもCircleCIの save_cacheresotre_cache は使える
  • 一工夫すればキャッシュのキーに環境変数で設定したバージョン番号を指定できる

参考

ちょうど「CircleCI 福岡ミートアップ」が開催されて、その中でもキャッシュについて触れている発表があったようです!

OPAのテストをCircleCIに乗せる

OPAのPolicyをテストする方法について前回は紹介しました!

kenfdev.hateblo.jp

OPAのバイナリさえあれば opa test コマンドで簡単にテストが実行できます。

ということでOPAのPolicyをCIに乗せて継続的にPolicyのテストができるようにしてみます。

どんなCIでもできますが、今回はCircleCIで試してみたのでその方法を共有します。

リポジトリの準備

Policyを書いたRegoファイルは 前回 のものをそのまま使います。

ファイルの構成は以下のとおり。これがGitHubリポジトリに置いてあることを前提にします。

.
├── iam.rego
└── iam_test.rego

ここに .circleci/config.yml を作ります。

mkdir .circleci

# エディタで編集
vim .circleci/config.yml

YAMLの中身は↓

version: 2
jobs:
  build:
    docker:
      - image: circleci/buildpack-deps:jessie

    steps:
      - checkout
      - run:
          name: Setup Environment Variables and Version file
          command: |
            echo 'export OPA_VERSION="0.10.6"' >> $BASH_ENV
            echo "${OPA_VERSION}" > _opa_version_
      - restore_cache:
          keys:
            - opa-cache-{{ checksum "_opa_version_" }}
      - run:
          name: Download OPA
          command: |
            if [ ! -f .bin/opa ]; then
              mkdir .bin
              wget -O .bin/opa https://github.com/open-policy-agent/opa/releases/download/v${OPA_VERSION}/opa_linux_amd64
              sudo chmod +x .bin/opa
            fi
      - run:
          name: Test Policies
          command: .bin/opa test .
      - save_cache:
          key: opa-cache-{{ checksum "_opa_version_" }}
          paths:
            - .bin

これで概ね準備完了です!

書いてる内容はちょっと多いのですがやってることは下の通り。

  • リポジトリのチェックアウト
  • ダウンロードするOPAのバージョンを環境変数に設定
  • OPAバイナリのキャッシュがあれば復元する
  • キャッシュが無ければダウンロードして実行権限与える
  • Policyを opa test でテストする
  • 次から再利用できるようにOPAバイナリをキャッシュしておく

このYAMLリポジトリにPushします。

CircleCIを設定

以下の公式ドキュメントにもありますが、CircleCIに対象となるリポジトリを追加します。

circleci.com

下の画面に表示されている「Start building」をクリックすることでCIが走ります!

f:id:kenev:20190409220122p:plain

正常にCIが走ればテストも通るはずなので、下のように成功画面になります。

f:id:kenev:20190412225659p:plain

では、わざとテストが失敗するように下のように iam_test.rego を編集しましょう!

f:id:kenev:20190409220846p:plain

新しいbranchで変更し、pushしてからPRを作ってみます。

pushした時点でCIが走るのでPRをGitHub上で作るころには失敗して、下のような画面になっているはずです!

f:id:kenev:20190409221622p:plain

このように、CIに乗せて継続的にPolicyのテストも実行していくことができます。

テスト好きにはたまらないですね!僕はテスト大好きなのでワクワクしてしまいます。

今回のコードは以下リポジトリに置いてますので、参考になればと思います!

github.com

おまけ

バッジもREADMEに載せてちょっと今風にしてみました!

CircleCIでプロジェクトの Settings に行って、Status Badgesに移動します。

すると、README.mdとかに貼り付けられるCircleCIのステータスバッジが下図のように記載されています。

f:id:kenev:20190412201757p:plain

これを貼り付ければ、下のようにCIの状況が表示されるバッジを載せることができます!

f:id:kenev:20190412202440p:plain

こういう取り組みも地味にモチベーションに繋がったりするので大事にしたいです。

OPAでテストコードを書く

OPAでPolicyをRegoで書いていくという話は何度かしてきました。

では、そのPolicyを正しく書けたかどうかはどう判断するのでしょう? これまで扱ってきた記事ではInputを用意してOutputをチェックしてました。 それって大変ですし、Policyが鬼のようにたくさんあった場合、デグレチェックも大変です。

そこで登場するのがOPAのテストコードの仕組みです!公式ドキュメントでも詳しい説明があります。

www.openpolicyagent.org

本記事では、以前とりあげたIAM Policyの例を使ってテストコードを書いてみます!

kenfdev.hateblo.jp

上の記事でテストしたパターンは以下の4つです。

  • iam:ChangePasswordが 許可 されるパターン
  • s3:ListAllMyBucketsが 許可 されるパターン
  • s3:ListBucketが 許可 されるパターン
  • s3:CreateBucketが 拒否 されるパターン

対象となるRegoは以下のとおり。

package aws

allow {
    input.action == "iam:ChangePassword"
}

allow {
    input.action == "s3:ListAllMyBuckets"
}

allow {
    actions_match
    resources_match
}

actions_match {
    actions := ["s3:List.*","s3:Get.*"]
    action := actions[_]
    regex.globs_match(input.action, action)
}

resources_match {
    resources := ["arn:aws:s3:::confidential-data","arn:aws:s3:::confidential-data/.*"]
    resource := resources[_]
    regex.globs_match(input.resource, resource)
}

テストの書き方

Regoのテストは(当たり前かもしれませんが)Regoで書きます! ルールに test_ というprefixをつけることでOPAが暗黙的にテストだと認識します。 opa test を実行した場合もOPAは test_ で始まるルールを集めてテストを実行するようになってます。

では、さっそくテストを順に書いていってみましょう! テスト用のルールはどこに書いてもいいんですけど、 iam_test.rego ファイルを作ってそこに書きましょう。 パッケージは同じにしておきたいので、先頭に package aws を忘れずに書きます。

iam:ChangePasswordが許可されるパターン

まずは以下のルールをテストしてみましょう。

allow {
    input.action == "iam:ChangePassword"
}

これはすごくシンプルで、 input.actionがiam:ChangePasswordだったら許可する です。 そのままですね!テストも実はかなりそのまま書くことができます。

test_change_password_action_allowed {
  allow with input as { "action": "iam:ChangePassword" }
}

test_ 以降はわかりやすい、好きな名前をつけると良いです。 で、テストしたいルール(この場合 allow )を書いて、 input の中身を指定したいので withas を使ってテストデータを注入します。 as 以降にはJSONをそのまま書くことができます!

OPAのバイナリを持ってる場合は以下のコマンドでテストが実行できます。

注意:iam.regoとiam_test.regoがあること

opa test .

VSCodeで拡張を入れている方は OPA: Test Workspace というコマンドが用意されているので、 それを実行することで結果を見ることも可能です。

f:id:kenev:20190402223652p:plain

s3:ListAllMyBucketsが許可されるパターン

次にテストしたいパターンも上のパターン同様です(actionが違うだけ)。

allow {
    input.action = "s3:ListAllMyBuckets"
}

先程と同様な考えでテストを書くと、次のようになります。

test_s3_ListAllMyBuckets_allowed {
  allow with input as { "action": "s3:ListAllMyBuckets" }
}

s3:ListBucketが許可されるパターン

次にテストしたいのは以下のパターンです。

allow {
    actions_match
    resources_match
}

actions_match {
    actions = ["s3:List.*","s3:Get.*"]
    action = actions[_]
    regex.globs_match(input.action, action)
}

resources_match {
    resources = ["arn:aws:s3:::confidential-data","arn:aws:s3:::confidential-data/.*"]
    resource = resources[_]
    regex.globs_match(input.resource, resource)
}

言葉で表すと arn:aws:s3:::confidential-data配下のresourceであればs3:List系あるいはs3:Get系のactionを許可 になります。

それでは input を調整してテストしてみましょう!

test_s3_ListBucket_to_confidential_data_allowed {
  allow with input as { 
    "action": "s3:ListBucket",
    "resource": "arn:aws:s3:::confidential-data/12345678"
  }
}

これもわかりやすいですね!

s3:ListBucketarn:aws:s3:::confidential-data/12345678 に対して許可されることをテストしてます。

opa test . してテストが通ることを確認しましょう。

s3:CreateBucketが拒否されるパターン

最後に拒否されるパターンも見てみましょう!

test_s3_CreateBucket_to_confidential_data_not_allowed {
  not allow with input as { 
    "action": "s3:CreateBucket",
    "resource": "arn:aws:s3:::confidential-data/12345678"
  }
}

actions3:CreateBucket にしてみました。 ここのポイントは、 拒否されること をテストするということです!

テストをよく見ると not allow になっていることに気づきましたでしょうか?

そうです、 not を使うことで拒否されることのテストができます。

テストを実行してみましょう!

$ opa test .
data.aws.test_change_password_action_allowed: PASS (7.772µs)
data.aws.test_s3_ListAllMyBuckets_allowed: PASS (1.735µs)
data.aws.test_s3_ListBucket_to_confidential_data_allowed: PASS (1.239µs)
data.aws.test_s3_CreateBucket_to_confidential_data_not_allowed: PASS (978ns)
--------------------------------------------------------------------------------
PASS: 4/4

全部PASSしているはずです!

そして、VSCodeでやっている人は OPA: Toggle Workspace Coverage を実行するとカバレッジも見れますよ!

f:id:kenev:20190404201527p:plain

おわりに

  • OPAのRegoで書いたPolicyはテストできる!
  • input のようなデータを簡単に注入できる!
  • VSCode拡張機能と組み合わせるとテスト実行からカバレッジの表示までできる!

ということがわかりましたー。CIと組み合わせてPolicyのロバストネスを上げれそうですね!

2019-04-12追記:下の記事でCI連携についても書きました!

kenfdev.hateblo.jp

参考

kenfdev.hateblo.jp

OPAのVisual Studio Code拡張機能

OPAにはVSCode用の拡張機能があります!

marketplace.visualstudio.com

機能

できるようになることは次のとおり(2019/03/31時点)

  • 保存時に構文チェック(現時点でエディタ上で表示はされません)
  • OPAのpackageを評価
  • 選択中の箇所を評価
  • 選択中の箇所を部分評価(Partial Evaluation)
  • 選択中の箇所を評価して、トレースを表示
  • 選択中の箇所を評価してプロファイルを表示
  • ワークスペース内のテストを実行
  • ワークスペース内のカバレッジ表示の切り替え
  • 選択中の箇所のカバレッジ表示切り替え

準備するもの

OPAのバイナリを忘れずに!

opa コマンドが叩けることが前提になっているので、ちゃんとインストールしておきましょう。

github.com

opaPATH を通しておくか、VSCodeの設定で Opa: Path にバイナリへのパスを直接指定することもできます!

お試し

拡張機能ページでGIFの紹介アニメーションがあるんですけど、サラッとどんどん進んじゃうので、一個一個見ていきます!

まず下の内容で simple.rego を書きます。

package http.authz

default allow = false

allow {
  input.method = "GET"
  input.path = ["salary", user]
  input.user = user
}

文でこのポリシーを言い表すと

  • デフォルトは許可しない
  • 下が全て満たされていれば許可
    • 入力 method"GET"
    • 入力 path["salary", user] である
    • 入力 userpathuser と一致

となります。

ワークスペースのルートに input.json を置くと、その中身をOPAの入力としてお試し実行することができます。 まずは次の内容にしてみましょう!

{
  "user": "bob",
  "method": "GET",
  "path": ["salary", "bob"]
}

bobbob 自身の salary を見ようとしてるので、これは許可されるはずです!

f:id:kenev:20190331115415p:plain

Regoの allow の部分をvscodeで選択して Cmd + Shift + p して、OPA: Evaluate Selection すると、この部分だけ評価できます。 やってみましょう。

f:id:kenev:20190331115632p:plain

期待通り、この結果は true となります!

では、 pathbob から alice に変えてみるとどうなるでしょ。

{
  "user": "bob",
  "method": "GET",
  "path": ["salary", "alice"]
}

もう一度 allow を選択して実行すると次の通り。

f:id:kenev:20190331120019p:plain

自分の salary じゃなくなってるので false になってますね!

次はルールのBodyを選択して評価します!

f:id:kenev:20190331120658p:plain

input.methodGET なので結果は true ですね!もう一行増やして実行してみましょう。

f:id:kenev:20190331120950p:plain

この結果の output.json は、複数行関わった場合に何を基準にしているかちょっとわかっていないです。 予想としては ["salary", "alice"] = ["salary", user] になって、 useralice が代入されているのかなと思っています。そしてその結果が output.json に出ているのではないかと。(この評価方法が正しいかは別途確認したいです)

では最終行も含めて実行してみましょう!

f:id:kenev:20190331122143p:plain

これは最終的に "bob" = "alice" しようとして、 true にならないので、結果は空っぽになります。

input.jsonpathalice から bob に戻しましょう!

{
  "user": "bob",
  "method": "GET",
  "path": ["salary", "bob"]
}

そして実行してみると次のとおり!

f:id:kenev:20190331123356p:plain

何かしら戻り値が返ってくるのがわかります。 上でも返ってきたのと同様 user に関するJSONなのですが、なぜこの値になるのかは現段階でちょっとわからないです。 allow を選択して実行すると直感的な true が返ってくるのですが。。。

おわりに

本記事ではOPAのVSCode拡張機能を紹介しました! 個人的にかなりうれしいのは下の2項目です。

まだまだ使いこなせていないのでこの記事もわかったことが増え次第更新していきたいです!