Open Policy Agentについて
Open Policy Agentとは、オープンソースのポリシーエンジンです。CNCFのGraduatedプロジェクトとして登録されています。
ポリシーをコードで表現・管理することができます。
RBACポリシーの例
package app.rbac
roles_permissions := {
"editor": {"delete", "create", "update", "read"},
"writer": {"create", "update", "read"},
"viewer": {"read"},
}
default allow := false
# ユーザーの持つロールの中に、要求された権限を持つものがあるかチェック
allow if {
some role in input.roles
input.permission in roles_permissions[role]
}
# adminロールを持っている場合は全ての操作を許可
allow if {
"admin" in input.roles
}
ポリシーのテスト手法
ポリシーのテストは、同じくregoで記述することができます。
test_というプレフィックスをつけることで、テストとして認識されます。パッケージに、_testと付けるのは任意です。また、todo_test_のようなプレフィックスの場合、テストがSKIPPEDとして報告されます。
package app.rbac_test
import data.app.rbac
test_admin_can_delete if {
rbac.allow with input as {"roles": ["admin"], "permission": "delete"}
}
# テーブル駆動テストとして書ける
test_rbac[title] if {
some title, tt in {
"viewer can not delete": {
"roles": ["viewer"],
"permission": "delete",
"expect": false,
},
"admin can delete": {
"roles": ["admin"],
"permission": "delete",
"expect": true,
},
}
actual := rbac.allow with input as {"roles": tt.roles, "permission": tt.permission}
tt.expect == actual
}
# これはスキップされる
todo_test_foo if {
false
}
また、テストに必要なデータはYAMLもしくはJSONから読み込むことができます。
例えば、テストケースをYAMLで記述しておき、Regoでテストコードだけを書くことができます。
package app.rbac_test
import data.role_cases # YAMLからテストケースを読み込める
test_import_yaml[title] if {
some title, tt in role_cases
actual := rbac.allow with input as {"roles": tt.roles, "permission": tt.permission}
tt.expect == actual
}
# table.yaml
role_cases:
"viewer can not delete":
roles: [ "viewer" ]
permission: "delete"
expect: false
"admin can delete":
roles: [ "admin" ]
permission: "delete"
expect: true
opa test -v .
テスト結果
rbac_test.rego:
data.app.rbac_test.todo_test_foo: SKIPPED
data.app.rbac_test.test_admin_can_delete: PASS (1.135833ms)
data.app.rbac_test.test_rbac: PASS (1.947334ms)
admin can delete: PASS
viewer can not delete: PASS
Goからの利用
Regoで書いたポリシーをOpenAgentPolicyを利用することで、Goから評価させることができます。
inputは、map[string]any型で渡すことができます。ですが、型の制約などを受けるために、jsonタグを使った structを宣言することを勧めます。
package main
import (
"context"
"fmt"
"github.com/open-policy-agent/opa/v1/rego"
)
type Input struct {
Roles []string `json:"roles"`
Permission string `json:"permission"`
}
func main() {
ctx := context.Background()
query, err := rego.New(
rego.Query("data.app.rbac.allow"),
rego.Load([]string{"./opa/rbac.rego"}, nil),
).PrepareForEval(ctx)
if err != nil {
panic(err)
}
inputs := []Input{
{
Roles: []string{"admin"},
Permission: "read",
}, {
Roles: []string{"admin"},
Permission: "write",
}, {
Roles: []string{"viewer"},
Permission: "create",
},
}
for _, input := range inputs {
results, err := query.Eval(ctx, rego.EvalInput(input))
if err != nil {
panic(err)
}
fmt.Printf(
"roles: %v, permission: %v, result: %v\n",
input.Roles, input.Permission, results.Allowed(),
)
}
}