Rego 102:クエリのAND/ORとカスタムメッセージの組み合わせ
2023年11月9日
0 分で読めますこのブログシリーズでは、Open Policy Agent(OPA)エンジンの開発元が作ったポリシー言語、Regoをやさしく解説します。Rego初心者で、ポリシーをコードとして記述する方法を学びたい方は、ぜひご覧ください。
全3回のシリーズで、次の内容を取り上げます。
第2回(この記事):Regoの中級構文
第3回:値とルールの種類
おさらいすると、Regoは、Open Policy Agent(OPA)フレームワークの開発元が作った宣言型クエリ言語です。Cloud Native Computing Foundation(CNCF)は2019年4月、OPAをインキュベーションレベルのホストプロジェクトとして受け入れ、2021年にインキュベーション段階を卒業しました。
Regoはポリシー・アズ・コードの記述に使われます。これにより、バージョン管理やモジュール設計といったプログラミングの手法を、クラウドやInfrastructure as Code(IaC)リソースの評価に適用できます。OPAは、Regoで記述されたポリシー・アズ・コードを評価するエンジンです。Snykでは、カスタムルールにRego言語を使用しています。
第1回のおさらい
このブログシリーズの第1回では、Regoのルールとは条件付きの割り当てであると説明しました。ルールは入力をクエリして条件に一致するものを探し、一致が見つかると変数に値が割り当てられます。
ルールは次のように読みます。
以下は、ネットワーク管理者のAliceだけがprodアカウントで仮想ネットワークを作成・削除できるという社内ポリシーを表した例です。
OPAはJSONまたはYAMLの入力ドキュメントをルールに照らして評価し、ポリシー判断を行います。次の入力ドキュメントは、現在ログインしているユーザーを表します。
OPAで入力をこのルールに照らして評価すると、クエリ input.user == "alice"との一致が見つかります。そのため、ルールヘッドの変数allowには、ルールヘッドの値trueが割り当てられます。これを示す出力は次のとおりです。
OPAは、現在ログイン中のユーザーAliceがprodアカウントで仮想ネットワークを作成・削除することを許可すると判断しました。この入力はルールに準拠しています。
ANDとOR
ここまでは、クエリが1つだけのルールを見てきました。ルールには複数のクエリを含めることもできます。その場合、変数に値を割り当てるには、複数の条件をすべて満たす必要があります。暗黙のAND、つまり「この条件ANDあの条件も満たす」ということです。
たとえば、以下のルールでは、変数allowにtrueを割り当てるために、input.user == "alice" AND input.environment == "prod"の両方がtrueでなければなりません。
場合によっては、ORが適切なこともあります。同じヘッドを持つ複数のルールを使うと、ORを表せます。
このルールの組み合わせは、次のように読めます。
allowは、userが"alice" OR userが"bob"ならtrueです。
厳密には、どちらもヘッドが同じなので、このルール群は1つのルールを構成します。ルールを複数の段階に分けて定義するため、これをインクリメンタルルールと呼びます。
2つ目のヘッドをなくして、本文をまとめることもできます。上の例をより簡潔に書くと、次のようになります。
なぜ単一化演算子 =を使い、代入演算子 :=を使わないのか、疑問に思うかもしれません。Regoでは変数が不変だからです。同じヘッドを持つルールは1つのインクリメンタルルールとして扱われますが、代入演算子を使おうとすると、実質的に同じ変数を複数回「代入」することになり、Regoでは許可されません。そのためルールヘッドでは単一化演算子を使います。これは同じ名前の複数のルールを単一化するためです。
ルールヘッドでどちらの演算子を使うべきか覚えるのが難しい場合は、デフォルト値と構文糖衣を使うと簡単です。
ルールヘッドのデフォルト値
ルールヘッドで変数に設定されるデフォルト値はtrueです。そのためRegoでは、ここで構文糖衣を使えます。ルールで変数にtrueを割り当てる場合、ルールヘッドの:= trueを省略できます。
つまり、このANDルールは……
……次のANDルールと同じです。
同様に、このORルールは……
……次のORルールと同じです。
まさに「いい感じ」ですね!
defaultキーワード
第1回で説明したように、ルールのクエリに一致する入力がない場合、ルールヘッドの変数にはヘッドの値が割り当てられません。
これを説明するために、ネットワーク管理者のAliceだけがprodアカウントで仮想ネットワークを作成・削除できるという例に戻りましょう。
ここでは、現在ログインしているユーザーがBobである入力ドキュメントを用意します。
input.userは"alice"ではないため、入力に対してルールを評価しても、OPAは一致を見つけません。そのため、allowにはtrueが割り当てられず、評価結果は空集合になります。
この場合、allowの値は未定義であると言います。OPAはルールを評価するために入力をクエリするとき、一致する値のみを返します。一致する値がなければ返すものがないため、空集合となります。
allowが明示的にtrueでない場合に、OPAからfalseを返すにはどうすればよいでしょうか。defaultキーワードを使って、デフォルト値を設定できます。つまり、ルールの評価結果が明示的にtrueでない場合、空の結果セットではなく、指定した値(ここではfalse)を返します。そのためには、同じくallowを使うルールを追加します。
これで、OPAがinput.user は"alice"ではないと判断した場合も、allowの評価結果は空集合になりません。代わりに、宣言したデフォルト値falseになります。
defaultキーワードを指定するときは、デフォルト値を定義するルールと条件付き割り当てを定義するルールの両方で、代入演算子:=ではなく単一化演算子=を使います。繰り返しになりますが、変数は不変だからです。代入演算子を使うと、同じ変数を複数回「代入」することになり、Regoでは許可されません。代わりに単一化演算子を使います。
もちろん、先ほど説明した構文糖衣を使い、条件付き割り当てのルールで= trueを省略することもできます。これは問題なく使えますし、条件付き割り当てでどの演算子を使うか覚える必要がないため、より簡単かもしれません。
カスタムメッセージ
単純な合否、つまりtrueやfalse/未定義の結果ではなく、複数のメッセージを返したい場合があります。その場合、ルールヘッドdeny[msg]を使い、必要なメッセージを変数msgに割り当てます。以下のルールは、ユーザーがAliceでないかを確認し、そうであれば文字列「User is denied access」をmsgに割り当て、それをdenyのセットに追加します(セットについては第3回で詳しく説明します)。
これを試すために、入力ドキュメントに現在ログインしているユーザー名が含まれているとしましょう。
OPAのRego Playground、またはopa eval -i input.json -d check_user.rego "data.rules.check_user" --format prettyコマンドを使って、上記の入力に対してルールを評価すると(手順はブログ記事第1回をご覧ください)、次のような結果になります。
ここでは何が起きているのでしょうか。実際にはセットルールを作成しています。これは、このシリーズの次回の記事で取り上げる概念です。今は、trueまたはfalse/未定義という単一の結果ではなく、deny変数に割り当てられたメッセージのセットを返していることを理解してください。Regoのセットは、整数{ 1, 2, 3 }、文字列{ "alice", "bob", "carlotta" }、あるいは他のセット{ { 1, 2}, {3, 4} }など、一意の要素からなる順序なしリストです。Regoでサポートされている任意の型からセットを作成でき、1つのセット内で型を混在させることもできます。
この例のルールでは、denyセットの各要素がメッセージを含む文字列です。この入力ドキュメントではセットの要素は1つだけですが、複数になることもあります。この記事の後半で例を紹介します。
メッセージに追加情報を含めるにはどうすればよいでしょうか。組み込み関数sprintfを使うと、denyの結果につながったinput.userフィールドの値を表示できます。
sprintf関数は、文字列と値の配列という2つの引数を取ります。この例では、配列内の唯一の要素はinput.userで表される文字列です。最初の引数にプレースホルダーとして%vを指定すると、ルールの評価時に配列内の値がそこに入ります。
次の入力を使ってルールを評価すると……
……次の結果が得られます。
notキーワード
式の前にnotキーワードを付けると、意味を反転させて否定できます。多くの場合、入力にプロパティが存在しないことを指定するクエリで使います。たとえば、次のクエリは……
……「入力ドキュメントにtags.environmentプロパティがあり、その値がfalseではない」という意味です。また、次のクエリは……
……「入力ドキュメントにtags.environmentプロパティがない、またはtags.environmentがfalseに設定されている」という意味です。重なりや中間の状態はありません。式とその否定形は、互いに排他的です。
入力にdepartment タグがない場合、denyにtrueを割り当てるルールの例を見てみましょう。
次の入力を使います。
この入力を上のルールに照らして評価すると、必要なdepartmentプロパティがないため、denyはtrueを返します。
OPAでルールの例を評価する
この記事で説明した概念を試すために、ルールの例を評価してみましょう。第1回と同様、OPAを使う方法は次の2つです。
Rego Playgroundを使う
OPAのコマンドラインツールを使う
これらのインターフェースの使い方は、第1回をご覧ください。
今回は、Kubernetes Podを使った、より実践的な例を見てみましょう。入力として使うJSONマニフェストは次のとおりです。
そして、次のルールを評価します。これは「Kubernetes Podにはreleaseラベルとenvironmentラベルを付ける」という社内ポリシーを適用するために記述したものです。
このルールには、この記事で取り上げた次の概念が使われています。
deny[msg]を使って、trueまたはfalse/undefinedではなく、カスタムメッセージのセットを返すANDとORの両方を使ったルール構造:
KubernetesオブジェクトがPodでAND releaseラベルがない場合、OR:
KubernetesオブジェクトがPodでAND environmentラベルがない場合に拒否する
プロパティが存在しないことを確認する
notキーワード準拠していないPodの名前を含むメッセージを返すsprintf関数
すぐに試せるよう、この内容を設定済みのPlaygroundを用意しました:https://play.openpolicyagent.org/p/KNVK9kEvIT
PlaygroundでEvaluateボタンを選択してルールを評価するか、OPAをローカルで実行している場合はopa eval -i input.json -d check_pod.rego "data.rules.check_pod" --format prettyのようなコマンドを実行すると、次の出力が表示されます。
確認した Kubernetes Pod は、入力に labels.environment プロパティが含まれていないため、ルールに準拠していません。
次に、labels.releaseプロパティを削除してみましょう。入力のlabelsセクションは次のようになります。
ここでルールを評価すると、denyセットに2つのメッセージが含まれていることがわかります。
最後に、入力にlabels.releaseとlabels.environmentの両方のプロパティを追加して、次のようにします。
もう一度ルールを評価すると、どうなるでしょうか?denyセットが空になっていることがわかります。
これは、OPAがdenyセットにメッセージを追加しなかったため、Podが準拠していることを意味します。やりました!
次のステップ
ぜひブログに戻って「Rego for Beginners Part 3」をお読みください。次回は、セットルール、オブジェクトルール、関数、反復処理について解説します。
その間に、役立つリソースをいくつかご紹介します。
Regoを使ってSnyk IaCのカスタムルールを作成する方法に関心がある方は、こちらのドキュメントをご覧ください。Snykに組み込まれたセキュリティルールやコンプライアンス対応ルールセットに加え、IaC+のカスタムルールを使えば、SDLC全体にわたってセキュリティコントロールをカスタマイズできます。
IaC+では、課題を確認するUI、ルールセット、ポリシーエンジンを通じて、IDE、SCM、CLI、CI/CD、Terraform Cloudから、AWS、Azure、Google Cloudなどのデプロイ済みクラウド環境まで、コードからクラウドにわたる構成上の問題を一元的に確認し、管理できます。
