👷

Regoの基礎(Safety)

2021/12/08に公開

この記事はOPA/Regoアドベントカレンダーの8日目です。

今回はRegoでポリシーを記述する際にはまりがちな "Safety" の概念について解説します(公式ドキュメント)。

rego_unsafe_var_error: var x is unsafe

Regoでポリシーの記述し始めるとおそらく1度は遭遇するであろうエラーです。例えば以下のようなポリシーがこのエラーになります。

example.rego
package example

p := {
    "blue": 1,
    "red": 0,
    "yellow": 2,
}

result[x] {
    not p[x] == 0
}
% opa eval -b . data
{
  "errors": [
    {
      "message": "var x is unsafe",
      "code": "rego_unsafe_var_error",
      "location": {
        "file": "example.rego",
        "row": 9,
        "col": 5
      }
    }
  ]
}

OPAはルールが有限個の入力と出力を持つことを保証するために Safety という概念を持っています。ちゃんと定義された変数のみが
ドキュメントでは "Safety" は以下のように定義されています。

Safety: every variable appearing in the head or in a builtin or inside a negation must appear in a non-negated, non-builtin expression in the body of the rule.

headとは {...} で囲われたルールの外で、上記例だと p および result[x] がその領域にいることになります。builtinは組み込みで用意されているキーワードや関数(例えば with など)、negationは not を指しています。これらに使われる変数はすべて

  • not を使わない式
  • 組み込みキーワードや関数以外の式

のどちらかで定義されている必要がある、ということになります。逆に言うと not の式やbuiltinキーワードを使う式では 変数が決定せず、ルールを安全に評価できない とみなされてエラーになる、というケースが多いと思います。

例えば先程のポリシーだと x が取りうる値は blue, red, yellow の3つのみ[1]ですが、 not x == 0 とだけ記述された場合、0 以外の無数の値を取りうる事になってしまいます。また組み込みの関数やキーワードとだけ組み合わせた変数も未定義のものとして扱われるので、同様に無数の値を取り得ます。先述したとおりOPAは有限の結果を返すことを保証するため、変数の安全性を検証できなかった場合は unsafe のエラーを出力して処理を中断します。

具体的なエラーの例

という説明だけだとなかなか理解しづらいと思うので、具体的な例と解決方法を見ていきたいと思います。

一致しない要素を抜き出したい

エラーになる例

p := {
    "blue": 1,
    "red": 0,
    "yellow": 2,
}

result[x] {
    not p[x] == 0
}

直感的には値が 0 ではないキー、つまり blueyellow を抜き出せそうですが、前述したとおり p にどのような値が入るかが決定的ではないため、 x がunsafeな変数として扱われエラーになります。

解決方法

result = y {
    y := {x | p[x]} - {x | p[x] == 0}
}

いくつか方法が考えられますが、差集合を使う方法が1つ挙げられます。条件に一致する要素の集合を作り、すべての要素を含む別の集合との差分を計算し、それを返り値として渡します。条件に合う集合は内包表記 {要素 | 条件} によって作り出すことができます。

上記の例は result に直接値を渡すため、再代入できなくなってしまいます。もしほかのルールでも result に要素を追加したいという場合は、以下のような書き方ができます。

result[z] {
    y := {x | p[x]} - {x | p[x] == 0}
    z := y[_]
}

result[m] {
    m := "hoge" # ということをしてもエラーにならない
}

すべての要素が一致しないことを確認したい

エラーになる例

p := {
    "blue": 1,
    "red": 0,
    "yellow": 2,
}

result {
    not p[x] == 3
}

オブジェクト型のpの中に値として3が入っていないことを確認したい、という意図で記述されたルールです。こちらも先ほどと同様で、not の中でのみ x [2] を呼び出しているため、unsafeな変数として扱われています。

解決方法1

result {
    count({x | p[x] == 3}) == 0
}

こちらもいくつか方法が考えられますが、1つ目は「一致する要素が0であることを確認する」というアプローチです。先程の例と同様に内包表記で「値が3であるキーの集合」を作成し、組み込み関数 count で集合の要素数を調べます。要素数が 0 なら「値が3であるキー」は存在しないことがわかります。

解決方法2

result {
    not has3
}

has3 {
    p[x] == 3
}

別の解法として、一度 not を含まない式でxの値を決定し、それをもとにnotを含む式を使う、という方法があります。もとのsafetyの定義の通りnotを含まない式で呼び出せばsafe扱いになるので、エラーにはなりません。

組み込み要素+Unificationで変数の値を決めようとする

エラーになる例

result[y] {
    12 = y + 7
}

RegoにはUnificationという機能があり、未定義の変数を含む = を使うと左辺と右辺が等しくなる条件を満たす値が代入されます。例えば [1, 2, x] = [y, 2, 3] とすると、x には 3 が、y には 1 が割り当てられます。上記の式も数学的には5になることは自明なのですが、OPAの処理では「未定義の変数 y を組み込み機能の+を使って7と足そうとしている」と解釈され、yがunsafe扱いになってしまいます。

解決方法

result[y] {
    5 = y
}

普通に式をちゃんと整理すればOKです。

まとめ

最初は混乱しがちなunsafeエラーですが、基本的には「not キーワードを含む式以外で変数を定義した上で使う」ということで回避できることがほとんどです。エラーに遭遇した際はまずその観点でチェックしてみることをお勧めします。

脚注
  1. ポリシーだけで完結する場合は not を使っても値が収束する可能性はあるのですが、それでも外部から with キーワードなどでデータをinjectすることが可能なため、すべからく not だけでの変数定義を禁止していると思われます。 ↩︎

  2. 説明の流れ上 x を置いていますが、本来このように他に影響しないイテレーションをしたい場合は _ にするほうが明示的です。ただし、_ でも同様にunsafeエラーになります。 ↩︎

Discussion