🐡

Regal: Rego(OPA)用リンタの導入手順

2024/05/06に公開

初めて使うプログラム言語のプロジェクトを作成する際にテストフレームワーク、フォーマッタ、リンタ、パッケージ管理さえ揃えて運用しておけば、その言語の玄人相手でもそこそこ騙せるとこのブログでは度々力説してきたが、PaC(Policy as Code)界隈においてポリシーを記述するためのプログラム言語であるRegoでも同様の準備を行うことにする。

まずテストフレームワークについてはOPA(Open Policy Agent)のインストールさえ行えばビルトインで提供されることから、特に追加の手続きは不要でテストコードの実装を開始できる。

フォーマッタについても、OPAの以下のコマンドを実行するだけとなる。

# Regoファイルに対してフォーマッタを適用する場合
$ opa fmt -w path/to/policy-dir/

# フォーマットチェックを行う場合
$ opa fmt --fail path/to/policy-dir/

Regoのパッケージ管理ツールについては、自分が欲しいと考えているTerraformのHCLファイル定義のベストプラクティスがまとめられたポリシーバンドルを配布・ダウンロード可能なRegistryのようなものはおそらく現時点では存在しない。

一応OPAの公式ドキュメントでは自社が所有するOCI互換レジストリ(Artifact Registry、Elastic Container Registry等)にバンドルをアップロードおよびダウンロードすることは出来るとは書かれている。

他にもAserto社が提供するpolicyというツールを使えば、よりシンプルな方法でバンドルの作成と運用が可能になるようだが、結局のところ先に述べた自分が求めるものとは異なるため、現時点ではパッケージ管理ツールは不要であると考えている。

そして最後にリンタについてだが、これについてもopa checkという構文チェックコマンドが標準で用意されているものの、OPAが提供するConftestで実行するRegoコードにおいて、Conftest独自のparse_config関数を使用していると構文が理解できずにエラーになるなどの課題がある。

一方でOPAの開発元であるStyra社ではより本格的なRegoの静的解析ツールであるRegalを提供していて、これであれば上記parse_configの問題は起きることがなく、さらにStyraが定めるRegoのコードスタイルガイドに準拠しているかのチェックまで行なってくれる。

Regoに関しては一般的なプログラミング言語とは明らかに質の異なる記法であるため、こういったツールは開発序盤ではかなり助かる。

そこで今回はこのRegalの導入手順について説明したい。

インストール

Macのhomebrewを使用している場合、Regalは以下のコマンドでインストールできる。

$ brew install styrainc/packages/regal

その他のインストール方法はこちら

CI/CD環境での利用ついては、Github ActionsのActionが提供されている。

検証プロジェクト

Regalを実際に動作確認するにあたって、以下のような簡単なサンプルプロジェクトを作成する。

$ mkdir -p /tmp/sample-project/policy
$ cd /tmp/sample-project
$ vi policy/example.rego
# 設定内容は下記

$ vi policy/example_test.rego
# 設定内容は下記

$ mkdir .regal
$ vi .regal/config.yaml
# 設定内容は下記

$ tree -a
.
├── .regal
│   └── config.yaml
└── policy
    ├── example.rego
    └── example_test.rego

3 directories, 3 files

まずexample.regoだが、OPAのHello World的なコードからさらに今回の検証で必要な最低限の部分だけを抜き出したコードとなっている。

/tmp/sample-project/policy/example.rego
package authz

# Regoのメジャーバージョンアップグレード時に使用可能となる機能を今から事前にオプトインするという意味
import rego.v1

# 以下ではルール(ポリシー)を定義している
# ルール内の各行はAND条件で結ばれたif文と考えればよい
# すべての行に一致する場合はallowにtrueが代入され、1行でも一致しない行がある場合はfalseになる
allow if {
  # 入力(input)として渡されるJSONに含まれるpathの値とuser_idの値が一致していることを確認
  # つまりJSON化されたリクエスト情報を元にパーソナルページに別人がアクセスしていないかチェックしている
  # なおOPAは1ms authorizationのようなユースケースも想定したエンジンである
  input.path == ["users", input.user_id]
  # HTTPメソッドがGETであることをチェックしている
  input.method == "GET"
}

このルールに対するテストコードを記述しているexample_test.regoは以下のようになる。

/tmp/sample-project/policy/example_test.rego
package authz

import rego.v1

# user_idのない匿名ユーザからのアクセスは拒否(not allow)される
# `allow with input as {...}`構文は、as以降のJSONをinputに渡してallowルールを実行するという意味
test_get_anonymous_denied if {
  not allow with input as {"path": ["users"], "method": "GET"}
}

# /users/bobのページにbobがアクセスしているので許可
test_get_user_allowed if {
  allow with input as {"path": ["users", "bob"], "method": "GET", "user_id": "bob"}
}

# /users/bobのページにaliceがアクセスしようとしているので拒否
test_get_another_user_denied if {
  not allow with input as {"path": ["users", "bob"], "method": "GET", "user_id": "alice"}
}

ちなみにテストコードの実行は以下のコマンドで行う。

$ opa test -v policy/
policy/example_test.rego:
data.authz.test_get_anonymous_denied: PASS (4.834512ms)
data.authz.test_get_user_allowed: PASS (2.683039ms)
data.authz.test_get_another_user_denied: PASS (166.374µs)
--------------------------------------------------------------------------------
PASS: 3/3

これでRegalを実行する対象となるコードの準備は完了となる。

次にRegalの設定ファイルを作成する必要があるが、公式ドキュメントで設定可能な値を確認した上で自分は以下のような設定を定義した。

/tmp/sample-project/.regal/config.yaml
rules:
  custom:
    # パッケージ名やルール名、関数名の命名規約を定義
    # ref. https://docs.styra.com/regal/rules/custom/naming-convention
    naming-convention:
      level: error
      conventions:
        # パッケージ名は'authz'に固定
        - pattern: "^authz$"
          targets:
            - package
        # ルール名、関数名は英小文字、数字、アンダースコアのみ利用可能
        - pattern: "^[_a-z0-9]+$"
          targets:
            - rule
            - function
    # ワンライナーでルールを記述可能なケースにおいて、ワンライナーで記述していない場合にerrorとなる
    # ref. https://docs.styra.com/regal/rules/custom/one-liner-rule
    one-liner-rule:
      level: error

capabilities:
  from:
    engine: opa
    version: v0.63.0

Regalでは現時点で70近くのルールが存在するが、customカテゴリの4つのルールを除いた全てがデフォルトで有効となっているので、いったんは使用したいcustomのルールを有効にする以外は何も設定せずに使い始めて問題ない。

そして開発を進めていくうちに明らかにプロジェクトの方針に合致しないルールが出てきた場合は個別にignoreを行えばよい。

capabilitiesはどのバージョンのOPAの構文でチェックを行うかを指定する設定だが、現在のOPAの最新バージョンは0.64.1であるのに対して、この設定で指定可能な最新のバージョンは0.63.0と若干のラグがある点には注意。

以上でRegalの検証準備は完了となる。

Regalによる静的チェックの実行は以下のコマンドで行う。

なお設定ファイルはコマンド実行ディレクトリから見て.regal/config.yamlにある場合は自動的に読み込まれるため、--config-file / -cオプションで明示的に指定する必要はない。

$ regal lint policy/
Rule:         	opa-fmt
Description:  	File should be formatted with `opa fmt`
Category:     	style
Location:     	policy/example_test.rego:1:1
Text:         	package authz
Documentation:	https://docs.styra.com/regal/rules/style/opa-fmt

...

2 files linted. 7 violations found in 2 files.

サンプルコードをコピペしただけではあるが、7つのルール違反を指摘される。

まず最初の指摘はフォーマッタが適用されていないとのことなので、以下のコマンドでフォーマッタを適用して再度regalコマンドを実行するとフォーマットに関する違反が解消される。

$ opa fmt -w policy/
$ regal lint policy/
Rule:         	one-liner-rule
Description:  	Rule body could be made a one-liner
Category:     	custom
Location:     	policy/example_test.rego:5:1
Text:         	test_get_anonymous_denied if {
Documentation:	https://docs.styra.com/regal/rules/custom/one-liner-rule

Rule:         	one-liner-rule
Description:  	Rule body could be made a one-liner
Category:     	custom
Location:     	policy/example_test.rego:9:1
Text:         	test_get_user_allowed if {
Documentation:	https://docs.styra.com/regal/rules/custom/one-liner-rule
...

2 files linted. 6 violations found in 2 files.

続いてワンライナールールが2つ出ているが、これはLocationが指すテストコードを以下のようにワンライナーに変更すれば解決する。

package authz

import rego.v1

test_get_anonymous_denied if not allow with input as {"path": ["users"], "method": "GET"}

test_get_user_allowed if allow with input as {"path": ["users", "bob"], "method": "GET", "user_id": "bob"}

test_get_another_user_denied if {
  not allow with input as {"path": ["users", "bob"], "method": "GET", "user_id": "alice"}
}

ちなみに3番目のテストもワンライナー化できるが、ワンライナー化した場合今度は1行あたりの文字列長のルールに違反するためなのか指摘は行われない。

次はtest-outside-test-packageというルールの指摘になるが、これはテストコードのpackage名には末尾に_testを付けるべきとの指摘になる。

$ regal lint policy/
Rule:         	test-outside-test-package
Description:  	Test outside of test package
Category:     	testing
Location:     	policy/example_test.rego:5:1
Text:         	test_get_anonymous_denied if not allow with input as {"path": ["users"], "method": "GET"}
Documentation:	https://docs.styra.com/regal/rules/testing/test-outside-test-package
...

Rule:         	no-defined-entrypoint
Description:  	Missing entrypoint annotation
Category:     	idiomatic
Location:
Documentation:	https://docs.styra.com/regal/rules/idiomatic/no-defined-entrypoint

2 files linted. 4 violations found in 2 files.

しかしこの指摘を守ったところで、開発上のメリットはあまりないどころかテストコードから対象コードの関数などへのアクセスが難しくなるなどのデメリットが生じるルールであることから、個人的には無視したい。

よってこのルールを.regal/config.yamlで以下のように無効化する。

/tmp/sample-project/.regal/config.yaml
rules:
  # テストコードのパッケージ名に_testを付けるメリットを開発時に感じられないためignore
  # ref. https://docs.styra.com/regal/rules/testing/test-outside-test-package
  testing:
    test-outside-test-package:
      level: ignore
  custom:
...

なおignoreはコード上に# regal ignore:test-outside-test-packageのようなコメントを付与することでも可能だが、これは違反している箇所すべてに記述する必要があるのでやはり設定ファイルで一括で設定したほうがよい。

なおignoreの指定方法は、その他にもきめ細かく設定可能なので詳細については公式ドキュメントを確認してほしい。

最後の指摘はMetadataコメントでentrypointを指定する必要があるとのことだが、これはexample.regoに以下のようなコメントを書いて再度実行すればエラーは表示されなくなる。

/tmp/sample-project/policy/example.rego
package authz

import rego.v1

# METADATA
# description: パーソナルページに別人がアクセスしていないかチェック
# entrypoint: true
allow if {
	input.path == ["users", input.user_id]
	input.method == "GET"
}
$ regal lint policy/
2 files linted. No violations found.

おわり

他にもRegalではRegoでルールを自作することでカスタムルールを追加することも出来る。

なお公式ドキュメントの以下の2ページを読むだけで、Regalを単純に利用するだけなら必要な情報のほとんどを把握できるので、もし時間があれば読んでみることをおすすめしたい。

今回は軽めの内容だが次回以降はinfra-testing-google-sampleプロジェクトのv0.4.0で予定している"Conftestを使用した組織内の全インフラプロジェクトのほぼ全ファイルをPaC管理下に置く実装例"について説明していく。

その準備段階として、次回はConftestの解説記事の続編となるOPA実践利用編をtoukousuru予定。

Discussion