Regoは宣言型のポリシー記述言語である。「どのように処理するか」ではなく、「何を満たすべきか」を記述する。
命令型(Go, JavaScript等) | 宣言型(Rego) |
---|---|
for文でループして条件判定 | some i; input.groups[i] == "admin" |
if/else による分岐処理 | 複数のルールによる条件記述 |
手続き的な実行フロー | 条件の成立/不成立の判定 |
Regoは2種類のデータを扱う:
種類 | 説明 | 例 |
---|---|---|
input |
評価時に与えられる動的データ | リクエスト内容 |
data |
参照用の静的・共有データ | ユーザー情報、ロール定義 |
この分離により、ポリシーが汎用的で再利用可能になる。
Regoの基本単位はルールである:
true
default
がない場合、マッチしない場合はundefined
(結果として評価されない)package authz # 名前空間を定義
import data.roles # データの参照
import input.user as u # エイリアス使用
# 一行コメント
allow { # インラインコメント
input.user.role == "admin"
}
データ型 | 例 | 説明 |
---|---|---|
文字列 | "admin" , "user" |
ダブルクォート |
数値 | 42 , 3.14 |
整数・浮動小数点 |
ブール値 | true , false |
真偽値 |
配列 | [1, 2, 3] |
順序あり |
オブジェクト | {"name": "Alice", "age": 30} |
キー・値のペア |
セット | {"admin", "user"} |
重複なし |
package input_example
import rego.v1
# リクエスト情報など
allow if {
input.method == "GET"
input.user.role == "admin"
}
# テスト用のサンプル実行
# Input: {"method": "GET", "user": {"role": "admin"}}
# 結果: allow = true
# Input: {"method": "POST", "user": {"role": "admin"}}
# 結果: allow = false
package data_example
import rego.v1
# 設定情報やマスターデータ
allow if {
data.roles[input.user.role].permissions[_] == "read"
}
# テスト用のサンプル実行
# Input: {"user": {"role": "admin"}}
# 結果: allow = true
# Input: {"user": {"role": "guest"}}
# 結果: allow = false
Dataセクションに貼り付けるJSON:
{
"roles": {
"admin": {
"permissions": ["read", "write", "delete"]
},
"user": {
"permissions": ["read"]
}
}
}
Regoのルールには複数のパターンがある:
パターン | 構文例 | 結果の型 | 説明 |
---|---|---|---|
<name> := <value> |
pi := 3.14 |
単一値 | 完全ルール |
<name> if <body> |
valid if input.age >= 18 |
boolean | 条件付き完全ルール |
<name> contains <key> if <body> |
users contains name if ... |
セット | セットルール |
<name>[<key>] := <value> if <body> |
scores[user] := score if ... |
オブジェクト | オブジェクトルール |
単一の値を返すルール。
package quota
import rego.v1
# 定数値
default request_quota := 100
# 条件付き値
request_quota := 1000 if input.user.internal
request_quota := 50 if input.user.plan == "trial"
# テスト用のサンプル実行
# Input: {"user": {"plan": "basic"}}
# 結果: request_quota = 100
# Input: {"user": {"internal": true}}
# 結果: request_quota = 1000
# Input: {"user": {"plan": "trial"}}
# 結果: request_quota = 50
評価順序: 複数のルールがマッチした場合、最後に定義されたルールの値が採用される。
セット(集合)を生成するルール。
package paths
import rego.v1
# 基本形
allowed_paths contains "/public"
# 条件付きセット生成
allowed_paths contains path if {
some team in input.user.teams
path := sprintf("/teams/%v/*", [team])
}
# テスト用のサンプル実行
# Input: {"user": {"teams": ["engineering", "design"]}}
# 結果: allowed_paths = {"/public", "/teams/engineering/*", "/teams/design/*"}
# Input: {"user": {}}
# 結果: allowed_paths = {"/public"}
オブジェクト(マップ)を生成するルール。
package object_rules
import rego.v1
# プレフィックスごとのパスグループ化
paths_by_prefix[prefix] := paths if {
some path in input.paths
parts := split(path, "/")
count(parts) > 0
prefix := parts[0] # prefixを具体的な値に束縛
paths := [p |
some p in input.paths
p_parts := split(p, "/")
count(p_parts) > 0
p_parts[0] == prefix
]
}
# テスト用のサンプル実行
# Input: {"paths": ["admin/users", "admin/settings", "public/docs", "public/help"]}
# 結果: paths_by_prefix = {"admin": ["admin/users", "admin/settings"], "public": ["public/docs", "public/help"]}
# Input: {"paths": ["home/dashboard"]}
# 結果: paths_by_prefix = {"home": ["home/dashboard"]}
演算子 | 説明 | 例 |
---|---|---|
== |
等しい | input.role == "admin" |
!= |
異なる | input.status != "banned" |
< |
小なり | input.age < 65 |
<= |
以下 | input.score <= 100 |
> |
大なり | input.salary > 50000 |
>= |
以上 | input.age >= 18 |
package logical_and
import rego.v1
default allow := false
allow if {
input.user.role == "admin" # AND
input.user.active == true # AND
input.method == "GET" # AND
}
# テスト用のサンプル実行
# Input: {"user": {"role": "admin", "active": true}, "method": "GET"}
# 結果: allow = true
# Input: {"user": {"role": "admin", "active": false}, "method": "GET"}
# 結果: allow = false
package logical_or
import rego.v1
default allow := false
# 複数ルールでORを表現
allow if {
input.user.role == "admin"
}
allow if {
input.user.role == "manager"
input.action == "read"
}
# テスト用のサンプル実行
# Input: {"user": {"role": "admin"}}
# 結果: allow = true
# Input: {"user": {"role": "manager"}, "action": "read"}
# 結果: allow = true
# Input: {"user": {"role": "user"}, "action": "read"}
# 結果: allow = false
package logical_not
import rego.v1
default allow := false
allow if {
input.user.role == "user"
not input.user.banned # bannedでない場合
}
# テスト用のサンプル実行
# Input: {"user": {"role": "user", "banned": false}}
# 結果: allow = true
# Input: {"user": {"role": "user", "banned": true}}
# 結果: allow = false
# Input: {"user": {"role": "user"}}
# 結果: allow = true
「少なくとも1つが条件を満たす」場合にtrue。
package quantification_some
import rego.v1
# 基本形
default has_admin_role := false
has_admin_role if {
some i
input.user.roles[i] == "admin"
}
# 簡略形
default has_admin_role_short := false
has_admin_role_short if {
input.user.roles[_] == "admin"
}
# 変数束縛と組み合わせ
default allowed_action := false
allowed_action if {
some role in input.user.roles
role == "admin" # 簡略化のため
}
# テスト用のサンプル実行
# Input: {"user": {"roles": ["user", "admin"]}}
# 結果: has_admin_role = true, has_admin_role_short = true, allowed_action = true
# Input: {"user": {"roles": ["user", "guest"]}}
# 結果: has_admin_role = false, has_admin_role_short = false, allowed_action = false
「すべてが条件を満たす」場合にtrue。
package quantification_every
import rego.v1
default all_files_owned := false
all_files_owned if {
# 入力データの存在確認
input.paths
input.user.id
is_array(input.paths)
every path in input.paths {
startswith(path, sprintf("/users/%v/", [input.user.id]))
}
}
# テスト用のサンプル実行
# Input: {"user": {"id": "u123"}, "paths": ["/users/u123/file1.txt", "/users/u123/file2.txt"]}
# 結果: all_files_owned = true
# Input: {"user": {"id": "u123"}, "paths": ["/users/u123/file1.txt", "/public/file2.txt"]}
# 結果: all_files_owned = false
package in_operator
import rego.v1
# セットのmembership
valid_method if {
input.method in {"GET", "POST", "PUT"}
}
# 配列のmembership
valid_role if {
input.user.role in ["admin", "manager", "user"]
}
# テスト用のサンプル実行
# Input: {"method": "GET", "user": {"role": "admin"}}
# 結果: valid_method = true, valid_role = true
# Input: {"method": "DELETE", "user": {"role": "guest"}}
# 結果: valid_method = false, valid_role = false
内包表記は既存のデータから新しいコレクションを生成する構文である。
種類 | 構文 | 特徴 | 用途 |
---|---|---|---|
配列内包 | [term | body] |
順序あり、重複可能 | リスト処理 |
セット内包 | {term | body} |
順序なし、重複除去 | ユニークな値の集合 |
オブジェクト内包 | {key: value | body} |
キー・値のペア | 構造化データの変換 |
package array_comprehension
import rego.v1
# 基本形:数値を2倍にする
doubled := [result |
some x in input.numbers
result := x * 2
]
# 条件付き:偶数のみを2倍
doubled_evens := [result |
some x in input.numbers
x % 2 == 0
result := x * 2
]
# 複雑な変換:ユーザー名の抽出
user_names := [name |
some user in input.users
user.active == true
name := user.name
]
# テスト用のサンプル実行
# Input: {"numbers": [1, 2, 3, 4, 5], "users": [{"name": "Alice", "active": true}, {"name": "Bob", "active": false}, {"name": "Carol", "active": true}]}
# 結果:
# doubled = [2, 4, 6, 8, 10]
# doubled_evens = [4, 8]
# user_names = ["Alice", "Carol"]
package set_comprehension
import rego.v1
# 重複を自動除去
unique_roles := {role |
some user in input.users
some role in user.roles
}
# 条件付きフィルタ
admin_users := {user.name |
user := input.users[_]
user.role == "admin"
}
# テスト用のサンプル実行
# Input: {"users": [{"name": "Alice", "role": "admin", "roles": ["admin", "user"]}, {"name": "Bob", "role": "user", "roles": ["user"]}, {"name": "Carol", "role": "admin", "roles": ["admin"]}]}
# 結果:
# unique_roles = {"admin", "user"}
# admin_users = {"Alice", "Carol"}
package object_comprehension
import rego.v1
# ユーザーIDをキーとしたマップ作成
users_by_id := {user.id: user |
user := input.users[_]
}
# 部署ごとの人数
dept_counts := {dept: count(members) |
some dept in {user.department | user := input.users[_]}
members := [user |
user := input.users[_]
user.department == dept
]
count(members) > 0
}
# テスト用のサンプル実行
# Input: {"users": [{"id": "u1", "name": "Alice", "department": "eng"}, {"id": "u2", "name": "Bob", "department": "eng"}, {"id": "u3", "name": "Carol", "department": "sales"}]}
# 結果:
# users_by_id = {"u1": {"id": "u1", "name": "Alice", "department": "eng"}, "u2": {"id": "u2", "name": "Bob", "department": "eng"}, "u3": {"id": "u3", "name": "Carol", "department": "sales"}}
# dept_counts = {"eng": 2, "sales": 1}
package nested_comprehension
import rego.v1
# 部署ごとのアクティブユーザー
active_by_dept := {dept: active_users |
some dept in {user.department | user := input.users[_]}
active_users := [user.name |
user := input.users[_]
user.department == dept
user.active == true
]
}
# テスト用のサンプル実行
# Input: {"users": [{"name": "Alice", "department": "eng", "active": true}, {"name": "Bob", "department": "eng", "active": false}, {"name": "Carol", "department": "sales", "active": true}]}
# 結果:
# active_by_dept = {"eng": ["Alice"], "sales": ["Carol"]}
Regoには豊富な組み込み関数が用意されている。実行環境(Go SDK、WebAssembly等)によって利用可能な関数が異なるため、詳細は公式リファレンスを参照すること。
Regoのテストはtest_
で始まる名前の関数として記述する。
package example
import rego.v1
# テスト対象のポリシー
default allow := false
allow if {
input.user.role == "admin"
}
# === テストコード ===
# 成功するテスト
test_admin_is_allowed if {
allow with input as {"user": {"role": "admin"}}
}
# 失敗するテスト(notを使用)
test_guest_is_denied if {
not allow with input as {"user": {"role": "guest"}}
}
# テスト結果
# allow = false
# test_admin_is_allowed = true
# test_guest_is_denied = true
with
キーワードを使用して、テスト時にinput
やdata
を差し替えできる。
package with_example
import rego.v1
# テスト対象のポリシー
allow if {
input.user.role == "admin"
data.config.strict_mode == false
}
# === テストコード ===
# inputの差し替え
test_with_input if {
allow with input as {"user": {"role": "admin"}}
with data.config.strict_mode as false
}
# dataの差し替え
test_with_data if {
allow with input as {"user": {"role": "admin"}}
with data as {"config": {"strict_mode": false}}
}
# 複数の値を同時に差し替え
test_with_multiple if {
allow with input as {"user": {"role": "admin"}}
with data.config.strict_mode as false
}
# テスト結果
# allow = false
# test_with_input = true
# test_with_data = true
# test_with_multiple = true
package authz_test
import data.authz
# 正常系のテスト
test_admin_user_allowed {
authz.allow with input as {
"user": {"role": "admin", "active": true},
"action": "read"
}
}
test_user_with_permission_allowed {
authz.allow with input as {
"user": {"role": "user", "active": true},
"action": "read"
} with data as {
"permissions": {
"user": ["read"]
}
}
}
# 異常系のテスト
test_inactive_user_denied {
not authz.allow with input as {
"user": {"role": "admin", "active": false},
"action": "read"
}
}
test_insufficient_permission_denied {
not authz.allow with input as {
"user": {"role": "user", "active": true},
"action": "delete"
} with data as {
"permissions": {
"user": ["read"]
}
}
}
# エッジケースのテスト
test_empty_input_denied {
not authz.allow with input as {}
}
test_missing_role_denied {
not authz.allow with input as {
"user": {"active": true},
"action": "read"
}
}
allow if {
user_role := input.user.role
print("User role:", user_role) # デバッグ出力
permissions := data.roles[user_role].permissions
print("Permissions:", permissions) # デバッグ出力
permissions[_] == input.action
}
allow if {
trace(sprintf("Evaluating access for user: %v", [input.user.name]))
input.user.role == "admin"
}
# 全テストの実行
opa test .
# 詳細出力
opa test . -v
# 特定のファイルのテスト
opa test policy_test.rego
# カバレッジ情報付き
opa test . --coverage
関連書籍