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