介绍

OPA简单的说就是一个功能强大的策略规则引擎,开发的时候多少会遇到一些多样的规则配置,这些配置不足以写到数据库,所以都写到了代码中、配置文件中。项目做大的时候,如果需要修改规则,那么只能重新修改代码,打包发布,相对比较麻烦,还增加了业务的复杂度,这个时候OPA的作用就出来了,它可以把这些配置的的东西独立出来,让规则脱离主业务逻辑。

使用

下载

按照系统或指定的版本,下载对应的命令行执行文件, 如下(我的是mac):

curl -L -o opa https://openpolicyagent.org/downloads/v0.37.2/opa_darwin_amd64
chmod +x opa
ln -s /usr/opa /usr/local/bin

模版

input/input1.json

{
    "action": {
        "operation": "read",
        "resource": "widgets"
    },
    "subject": {
        "user": "inspector-alice"
    }
}

rego/test.rego

package example_rbac

default allow = false

# allow will be true when user has role and role has permission
allow {
	user_has_role[role_name]
	role_has_permission[role_name]
}

# check user role binding exist
user_has_role[role_name] {
	role_binding = data.bindings[_] with data.bindings as data_context.bindings
	role_binding.role = role_name
	role_binding.user = input.subject.user
}

# check role permission exist
role_has_permission[role_name] {
	role = data.roles[_] with data.roles as data_context.roles
	role.name = role_name
	role.operation = input.action.operation
	role.resource = input.action.resource
}

data_context = {
	"roles": [
		{
			"operation": "read",
			"resource": "widgets",
			"name": "widget-reader",
		},
		{
			"operation": "write",
			"resource": "widgets",
			"name": "widget-writer",
		},
	],
	"bindings": [
		{
			"user": "inspector-alice",
			"role": "widget-reader",
		},
		{
			"user": "maker-bob",
			"role": "widget-writer",
		},
	],
}

方式1: 把所有规则写在一个目录下, 切换到规则目录-b绑定,后面引用需要执行的规则:

opa eval -b . -i ../input/input1.json "data.example_rbac.allow" -f json

方式2: 指定规则执行:

opa eval -i input/input1.json -d rego/test.rego "data.example_rbac.allow"

方式3: 直接把rego规则和data.json数据写在同一目录下,使用:

opa eval --bundle . -i tommytest/input.json 'data.tommytest.result'

OPA的Rego基本语法如下表:

语法例子
上下文data
输入input
索引取值data.bindings[0]
比较“alice” == input.subject.user
赋值user := input.subject.user
规则< Header > { < Body > }
规则头< Name > = < Value > { … } 或者 < Name > { … }
规则体And运算的一个个描述
多条同名规则Or运算的一个规则
规则默认值default allow = false
函数fun(x) { … }
虚拟文档doc[x] { … }

OPA以daemon方式运行支持HTTP RestAPI, 业务应用可以调用相关API完成集成,详细文档可以参考:
https://www.openpolicyagent.org/docs/latest/rest-api/

go依赖使用

package main

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"log"
	"os"
	"reflect"
	"strings"

	"github.com/open-policy-agent/opa/ast"
	"github.com/open-policy-agent/opa/logging"
	"github.com/open-policy-agent/opa/rego"
	"github.com/open-policy-agent/opa/storage"
	"github.com/open-policy-agent/opa/storage/disk"
	"github.com/open-policy-agent/opa/storage/inmem"
	"github.com/open-policy-agent/opa/topdown"
	"github.com/open-policy-agent/opa/types"
	"github.com/open-policy-agent/opa/util"
)

//go:generate go run main.go test.rego "data" < input.json
func demo1() {
	ctx := context.Background()
	// Construct a Rego object that can be prepared or evaluated.
	r := rego.New(rego.Query(os.Args[2]), rego.Load([]string{os.Args[1]}, nil))

	// Create a prepared query that can be evaluated.
	query, err := r.PrepareForEval(ctx)
	if err != nil {
		log.Fatal(err)
	}

	// Load the input document from stdin.
	var input interface{}
	dec := json.NewDecoder(os.Stdin)
	dec.UseNumber()
	if err := dec.Decode(&input); err != nil {
		log.Fatal(err)
	}

	rs, err := query.Eval(ctx, rego.EvalInput(input))
	if err != nil {
		log.Fatal(err)
	}
	result := rs[0].Expressions[0].Value.(map[string]interface{})
	fmt.Println(result["example_rbac"].(map[string]interface{})["allow"], reflect.TypeOf(result["example_rbac"].(map[string]interface{})["allow"]))
}

// demo2
func demo2() {
	ctx := context.Background()

	// Raw input data that will be used in evaluation.
	raw := `{"users": [{"id": "bob"}, {"id": "alice"}]}`
	d := json.NewDecoder(bytes.NewBufferString(raw))

	// Numeric values must be represented using json.Number.
	d.UseNumber()

	var input interface{}

	if err := d.Decode(&input); err != nil {
		panic(err)
	}

	// Create a simple query over the input.
	rego := rego.New(
		rego.Query("input.users[idx].id = user_id"),
		rego.Input(input))

	//Run evaluation.
	rs, err := rego.Eval(ctx)

	if err != nil {
		// Handle error.
	}

	// Inspect results.
	fmt.Println("result:", rs)
	fmt.Println("len:", len(rs))
	fmt.Println("bindings.idx:", rs[1].Bindings["idx"])
	fmt.Println("bindings.user_id:", rs[1].Bindings["user_id"])
}

// demo3
func demo3() {
	ctx := context.Background()

	// Create query that produces multiple bindings for variable.
	rego := rego.New(
		rego.Query(`a = ["ex", "am", "ple"]; x = a[_]; not p[x]`),
		rego.Package(`example`),
		rego.Module("example.rego", `package example
		p["am"] { true }
		`),
	)

	// Run evaluation.
	rs, err := rego.Eval(ctx)

	// Inspect results.
	fmt.Println("len:", len(rs))
	fmt.Println("err:", err)
	for i := range rs {
		fmt.Printf("bindings[\"x\"]: %v (i=%d)\n", rs[i].Bindings["x"], i)
	}

	// Output:
	//
	// len: 2
	// err: <nil>
	// bindings["x"]: ex (i=0)
	// bindings["x"]: ple (i=1)
}

func demo5() {

	ctx := context.Background()

	// Create query that returns a single boolean value.
	rego := rego.New(
		rego.Query("data.authz.allow"),
		rego.Module("example.rego",
			`package authz
default allow = false
allow {
	input.open == "sesame"
}`,
		),
		rego.Input(map[string]interface{}{"open": "sesame"}),
	)

	// Run evaluation.
	rs, err := rego.Eval(ctx)
	if err != nil {
		panic(err)
	}

	// Inspect result.
	fmt.Println("allowed:", rs.Allowed())

	// Output:
	//

}
func ExampleRego_Eval_multipleDocuments() {

	ctx := context.Background()

	// Create query that produces multiple documents.
	rego := rego.New(
		rego.Query("data.example.p[x]"),
		rego.Module("example.rego",
			`package example
p = {"hello": "alice", "goodbye": "bob"} { true }`,
		))

	// Run evaluation.
	rs, err := rego.Eval(ctx)

	// Inspect results.
	fmt.Println("len:", len(rs))
	fmt.Println("err:", err)
	for i := range rs {
		fmt.Printf("bindings[\"x\"]: %v (i=%d)\n", rs[i].Bindings["x"], i)
		fmt.Printf("value: %v (i=%d)\n", rs[i].Expressions[0].Value, i)
	}

	// Output:
	//
	// len: 2
	// err: <nil>
	// bindings["x"]: goodbye (i=0)
	// value: bob (i=0)
	// bindings["x"]: hello (i=1)
	// value: alice (i=1)
}

func ExampleRego_Eval_compiler() {

	ctx := context.Background()

	// Define a simple policy.
	module := `
		package example
		default allow = false
		allow {
			input.identity = "admin"
		}
		allow {
			input.method = "GET"
		}
	`

	// Compile the module. The keys are used as identifiers in error messages.
	compiler, err := ast.CompileModules(map[string]string{
		"example.rego": module,
	})

	// Create a new query that uses the compiled policy from above.
	rego := rego.New(
		rego.Query("data.example.allow"),
		rego.Compiler(compiler),
		rego.Input(
			map[string]interface{}{
				"identity": "bob",
				"method":   "GET",
			},
		),
	)

	// Run evaluation.
	rs, err := rego.Eval(ctx)

	if err != nil {
		// Handle error.
	}

	// Inspect results.
	fmt.Println("len:", len(rs))
	fmt.Println("value:", rs[0].Expressions[0].Value)
	fmt.Println("allowed:", rs.Allowed()) // helper method

	// Output:
	//
	// len: 1
	// value: true
	// allowed: true
}

func ExampleRego_Eval_storage() {

	ctx := context.Background()

	data := `{
        "example": {
            "users": [
                {
                    "name": "alice",
                    "likes": ["dogs", "clouds"]
                },
                {
                    "name": "bob",
                    "likes": ["pizza", "cats"]
                }
            ]
        }
    }`

	var json map[string]interface{}

	err := util.UnmarshalJSON([]byte(data), &json)
	if err != nil {
		// Handle error.
	}

	// Manually create the storage layer. inmem.NewFromObject returns an
	// in-memory store containing the supplied data.
	store := inmem.NewFromObject(json)

	// Create new query that returns the value
	rego := rego.New(
		rego.Query("data.example.users[0].likes"),
		rego.Store(store))

	// Run evaluation.
	rs, err := rego.Eval(ctx)
	if err != nil {
		// Handle error.
	}

	// Inspect the result.
	fmt.Println("value:", rs[0].Expressions[0].Value)

	// Output:
	//
	// value: [dogs clouds]
}

func ExampleRego_Eval_persistent_storage() {

	ctx := context.Background()

	data := `{
        "example": {
            "users": {
				"alice": {
					"likes": ["dogs", "clouds"]
				},
				"bob": {
					"likes": ["pizza", "cats"]
				}
			}
        }
    }`

	var json map[string]interface{}

	err := util.UnmarshalJSON([]byte(data), &json)
	if err != nil {
		// Handle error.
	}

	// Manually create a persistent storage-layer in a temporary directory.
	rootDir, err := ioutil.TempDir("", "rego_example")
	if err != nil {
		panic(err)
	}

	defer os.RemoveAll(rootDir)

	// Configure the store to partition data at `/example/users` so that each
	// user's data is stored on a different row. Assuming the policy only reads
	// data for a single user to process the policy query, OPA can avoid loading
	// _all_ user data into memory this way.
	store, err := disk.New(ctx, logging.NewNoOpLogger(), nil, disk.Options{
		Dir:        rootDir,
		Partitions: []storage.Path{{"example", "user"}},
	})
	if err != nil {
		// Handle error.
	}

	err = storage.WriteOne(ctx, store, storage.AddOp, storage.Path{}, json)
	if err != nil {
		// Handle error
	}

	// Run a query that returns the value
	rs, err := rego.New(
		rego.Query(`data.example.users["alice"].likes`),
		rego.Store(store)).Eval(ctx)
	if err != nil {
		// Handle error.
	}

	// Inspect the result.
	fmt.Println("value:", rs[0].Expressions[0].Value)

	// Re-open the store in the same directory.
	store.Close(ctx)

	store2, err := disk.New(ctx, logging.NewNoOpLogger(), nil, disk.Options{
		Dir:        rootDir,
		Partitions: []storage.Path{{"example", "user"}},
	})
	if err != nil {
		// Handle error.
	}

	// Run the same query with a new store.
	rs, err = rego.New(
		rego.Query(`data.example.users["alice"].likes`),
		rego.Store(store2)).Eval(ctx)
	if err != nil {
		// Handle error.
	}

	// Inspect the result and observe the same result.
	fmt.Println("value:", rs[0].Expressions[0].Value)

	// Output:
	//
	// value: [dogs clouds]
	// value: [dogs clouds]
}

func ExampleRego_Eval_transactions() {

	ctx := context.Background()

	// Create storage layer and load dummy data.
	store := inmem.NewFromReader(bytes.NewBufferString(`{
		"favourites": {
			"pizza": "cheese",
			"colour": "violet"
		}
	}`))

	// Open a write transaction on the store that will perform write operations.
	txn, err := store.NewTransaction(ctx, storage.WriteParams)
	if err != nil {
		// Handle error.
	}

	// Create rego query that uses the transaction created above.
	inside := rego.New(
		rego.Query("data.favourites.pizza"),
		rego.Store(store),
		rego.Transaction(txn),
	)

	// Create rego query that DOES NOT use the transaction created above. Under
	// the hood, the rego package will create it's own transaction to
	// ensure it evaluates over a consistent snapshot of the storage layer.
	outside := rego.New(
		rego.Query("data.favourites.pizza"),
		rego.Store(store),
	)

	// Write change to storage layer inside the transaction.
	err = store.Write(ctx, txn, storage.AddOp, storage.MustParsePath("/favourites/pizza"), "pepperoni")
	if err != nil {
		// Handle error.
	}

	// Run evaluation INSIDE the transaction.
	rs, err := inside.Eval(ctx)
	if err != nil {
		// Handle error.
	}

	fmt.Println("value (inside txn):", rs[0].Expressions[0].Value)

	// Run evaluation OUTSIDE the transaction.
	rs, err = outside.Eval(ctx)
	if err != nil {
		// Handle error.
	}

	fmt.Println("value (outside txn):", rs[0].Expressions[0].Value)

	if err := store.Commit(ctx, txn); err != nil {
		// Handle error.
	}

	// Run evaluation AFTER the transaction commits.
	rs, err = outside.Eval(ctx)
	if err != nil {
		// Handle error.
	}

	fmt.Println("value (after txn):", rs[0].Expressions[0].Value)

	// Output:
	//
	// value (inside txn): pepperoni
	// value (outside txn): cheese
	// value (after txn): pepperoni
}

func ExampleRego_Eval_errors() {

	ctx := context.Background()

	r := rego.New(
		rego.Query("data.example.p"),
		rego.Module("example_error.rego",
			`package example
p = true { not q[x] }
q = {1, 2, 3} { true }`,
		))

	_, err := r.Eval(ctx)

	switch err := err.(type) {
	case ast.Errors:
		for _, e := range err {
			fmt.Println("code:", e.Code)
			fmt.Println("row:", e.Location.Row)
			fmt.Println("filename:", e.Location.File)
		}
	default:
		// Some other error occurred.
	}

	// Output:
	//
	// code: rego_unsafe_var_error
	// row: 3
	// filename: example_error.rego
}

func ExampleRego_PartialResult() {

	ctx := context.Background()

	// Define a role-based access control (RBAC) policy that decides whether to
	// allow or deny requests. Requests are allowed if the user is bound to a
	// role that grants permission to perform the operation on the resource.
	module := `
		package example
		import data.bindings
		import data.roles
		default allow = false
		allow {
			user_has_role[role_name]
			role_has_permission[role_name]
		}
		user_has_role[role_name] {
			b = bindings[_]
			b.role = role_name
			b.user = input.subject.user
		}
		role_has_permission[role_name] {
			r = roles[_]
			r.name = role_name
			match_with_wildcard(r.operations, input.operation)
			match_with_wildcard(r.resources, input.resource)
		}
		match_with_wildcard(allowed, value) {
			allowed[_] = "*"
		}
		match_with_wildcard(allowed, value) {
			allowed[_] = value
		}
	`

	// Define dummy roles and role bindings for the example. In real-world
	// scenarios, this data would be pushed or pulled into the service
	// embedding OPA either from an external API or configuration file.
	store := inmem.NewFromReader(bytes.NewBufferString(`{
		"roles": [
			{
				"resources": ["documentA", "documentB"],
				"operations": ["read"],
				"name": "analyst"
			},
			{
				"resources": ["*"],
				"operations": ["*"],
				"name": "admin"
			}
		],
		"bindings": [
			{
				"user": "bob",
				"role": "admin"
			},
			{
				"user": "alice",
				"role": "analyst"
			}
		]
	}`))

	// Prepare and run partial evaluation on the query. The result of partial
	// evaluation can be cached for performance. When the data or policy
	// change, partial evaluation should be re-run.
	r := rego.New(
		rego.Query("data.example.allow"),
		rego.Module("example.rego", module),
		rego.Store(store),
	)

	pr, err := r.PartialResult(ctx)
	if err != nil {
		// Handle error.
	}

	// Define example inputs (representing requests) that will be used to test
	// the policy.
	examples := []map[string]interface{}{
		{
			"resource":  "documentA",
			"operation": "write",
			"subject": map[string]interface{}{
				"user": "bob",
			},
		},
		{
			"resource":  "documentB",
			"operation": "write",
			"subject": map[string]interface{}{
				"user": "alice",
			},
		},
		{
			"resource":  "documentB",
			"operation": "read",
			"subject": map[string]interface{}{
				"user": "alice",
			},
		},
	}

	for i := range examples {

		// Prepare and run normal evaluation from the result of partial
		// evaluation.
		r := pr.Rego(
			rego.Input(examples[i]),
		)

		rs, err := r.Eval(ctx)

		if err != nil || len(rs) != 1 || len(rs[0].Expressions) != 1 {
			// Handle erorr.
		} else {
			fmt.Printf("input %d allowed: %v\n", i+1, rs[0].Expressions[0].Value)
		}
	}

	// Output:
	//
	// input 1 allowed: true
	// input 2 allowed: false
	// input 3 allowed: true
}

func ExampleRego_Partial() {

	ctx := context.Background()

	// Define a simple policy for example purposes.
	module := `package test
	allow {
		input.method = read_methods[_]
		input.path = ["reviews", user]
		input.user = user
	}
	allow {
		input.method = read_methods[_]
		input.path = ["reviews", _]
		input.is_admin
	}
	read_methods = ["GET"]
	`

	r := rego.New(rego.Query("data.test.allow == true"), rego.Module("example.rego", module))
	pq, err := r.Partial(ctx)
	if err != nil {
		// Handle error.
	}

	// Inspect result.
	for i := range pq.Queries {
		fmt.Printf("Query #%d: %v\n", i+1, pq.Queries[i])
	}

	// Output:
	//
	// Query #1: "GET" = input.method; input.path = ["reviews", _]; input.is_admin
	// Query #2: "GET" = input.method; input.path = ["reviews", user3]; user3 = input.user
}

func ExampleRego_Eval_trace_simple() {

	ctx := context.Background()

	// Create very simple query that binds a single variable and enables tracing.
	r := rego.New(
		rego.Query("x = 1"),
		rego.Trace(true),
	)

	// Run evaluation.
	r.Eval(ctx)

	// Inspect results.
	rego.PrintTraceWithLocation(os.Stdout, r)

	// Output:
	//
	// query:1     Enter x = 1
	// query:1     | Eval x = 1
	// query:1     | Exit x = 1
	// query:1     Redo x = 1
	// query:1     | Redo x = 1
}

func ExampleRego_Eval_tracer() {

	ctx := context.Background()

	buf := topdown.NewBufferTracer()

	// Create very simple query that binds a single variable and provides a tracer.
	rego := rego.New(
		rego.Query("x = 1"),
		rego.QueryTracer(buf),
	)

	// Run evaluation.
	rego.Eval(ctx)

	// Inspect results.
	topdown.PrettyTraceWithLocation(os.Stdout, *buf)

	// Output:
	//
	// query:1     Enter x = 1
	// query:1     | Eval x = 1
	// query:1     | Exit x = 1
	// query:1     Redo x = 1
	// query:1     | Redo x = 1
}

func ExampleRego_PrepareForEval() {
	ctx := context.Background()

	// Create a simple query
	r := rego.New(
		rego.Query("input.x == 1"),
	)

	// Prepare for evaluation
	pq, err := r.PrepareForEval(ctx)

	if err != nil {
		// Handle error.
	}

	// Raw input data that will be used in the first evaluation
	input := map[string]interface{}{"x": 2}

	// Run the evaluation
	rs, err := pq.Eval(ctx, rego.EvalInput(input))

	if err != nil {
		// Handle error.
	}

	// Inspect results.
	fmt.Println("initial result:", rs[0].Expressions[0])

	// Update input
	input["x"] = 1

	// Run the evaluation with new input
	rs, err = pq.Eval(ctx, rego.EvalInput(input))

	if err != nil {
		// Handle error.
	}

	// Inspect results.
	fmt.Println("updated result:", rs[0].Expressions[0])

	// Output:
	//
	// initial result: false
	// updated result: true
}

func ExampleRego_PrepareForPartial() {

	ctx := context.Background()

	// Define a simple policy for example purposes.
	module := `package test
	allow {
		input.method = read_methods[_]
		input.path = ["reviews", user]
		input.user = user
	}
	allow {
		input.method = read_methods[_]
		input.path = ["reviews", _]
		input.is_admin
	}
	read_methods = ["GET"]
	`

	r := rego.New(
		rego.Query("data.test.allow == true"),
		rego.Module("example.rego", module),
	)

	pq, err := r.PrepareForPartial(ctx)
	if err != nil {
		// Handle error.
	}

	pqs, err := pq.Partial(ctx)
	if err != nil {
		// Handle error.
	}

	// Inspect result
	fmt.Println("First evaluation")
	for i := range pqs.Queries {
		fmt.Printf("Query #%d: %v\n", i+1, pqs.Queries[i])
	}

	// Evaluate with specified input
	exampleInput := map[string]string{
		"method": "GET",
	}

	// Evaluate again with different input and unknowns
	pqs, err = pq.Partial(ctx,
		rego.EvalInput(exampleInput),
		rego.EvalUnknowns([]string{"input.user", "input.is_admin", "input.path"}),
	)
	if err != nil {
		// Handle error.
	}

	// Inspect result
	fmt.Println("Second evaluation")
	for i := range pqs.Queries {
		fmt.Printf("Query #%d: %v\n", i+1, pqs.Queries[i])
	}

	// Output:
	//
	// First evaluation
	// Query #1: "GET" = input.method; input.path = ["reviews", _]; input.is_admin
	// Query #2: "GET" = input.method; input.path = ["reviews", user3]; user3 = input.user
	// Second evaluation
	// Query #1: input.path = ["reviews", _]; input.is_admin
	// Query #2: input.path = ["reviews", user3]; user3 = input.user
}

func ExampleRego_custom_functional_builtin() {

	r := rego.New(
		// An example query that uses a custom function.
		rego.Query(`x = trim_and_split("/foo/bar/baz/", "/")`),

		// A custom function that trims and splits strings on the same delimiter.
		rego.Function2(
			&rego.Function{
				Name: "trim_and_split",
				Decl: types.NewFunction(
					types.Args(types.S, types.S), // two string inputs
					types.NewArray(nil, types.S), // variable-length string array output
				),
			},
			func(_ rego.BuiltinContext, a, b *ast.Term) (*ast.Term, error) {

				str, ok1 := a.Value.(ast.String)
				delim, ok2 := b.Value.(ast.String)

				// The function is undefined for non-string inputs. Built-in
				// functions should only return errors in unrecoverable cases.
				if !ok1 || !ok2 {
					return nil, nil
				}

				result := strings.Split(strings.Trim(string(str), string(delim)), string(delim))

				arr := make([]*ast.Term, len(result))
				for i := range result {
					arr[i] = ast.StringTerm(result[i])
				}

				return ast.ArrayTerm(arr...), nil
			},
		),
	)

	rs, err := r.Eval(context.Background())
	if err != nil {
		// handle error
	}

	fmt.Println(rs[0].Bindings["x"])

	// Output:
	//
	// [foo bar baz]
}

func ExampleRego_custom_function_caching() {
	i := 0

	r := rego.New(
		// An example query that uses a custom function.
		rego.Query(`x = mycounter("foo"); y = mycounter("foo")`),

		// A custom function that uses caching.
		rego.FunctionDyn(
			&rego.Function{
				Name:    "mycounter",
				Memoize: true,
				Decl: types.NewFunction(
					types.Args(types.S), // one string input
					types.N,             // one number output
				),
			},
			func(_ topdown.BuiltinContext, args []*ast.Term) (*ast.Term, error) {
				i++
				return ast.IntNumberTerm(i), nil
			},
		),
	)

	rs, err := r.Eval(context.Background())
	if err != nil {
		// handle error
	}

	fmt.Println("x:", rs[0].Bindings["x"])
	fmt.Println("y:", rs[0].Bindings["y"])

	// Output:
	//
	// x: 1
	// y: 1
}

func ExampleRego_custom_function_global() {

	decl := &rego.Function{
		Name: "trim_and_split",
		Decl: types.NewFunction(
			types.Args(types.S, types.S), // two string inputs
			types.NewArray(nil, types.S), // variable-length string array output
		),
	}

	impl := func(_ rego.BuiltinContext, a, b *ast.Term) (*ast.Term, error) {

		str, ok1 := a.Value.(ast.String)
		delim, ok2 := b.Value.(ast.String)

		// The function is undefined for non-string inputs. Built-in
		// functions should only return errors in unrecoverable cases.
		if !ok1 || !ok2 {
			return nil, nil
		}

		result := strings.Split(strings.Trim(string(str), string(delim)), string(delim))

		arr := make([]*ast.Term, len(result))
		for i := range result {
			arr[i] = ast.StringTerm(result[i])
		}

		return ast.ArrayTerm(arr...), nil
	}

	// The rego package exports helper functions for different arities and a
	// special version of the function that accepts a dynamic number.
	rego.RegisterBuiltin2(decl, impl)

	r := rego.New(
		// An example query that uses a custom function.
		rego.Query(`x = trim_and_split("/foo/bar/baz/", "/")`),
	)

	rs, err := r.Eval(context.Background())
	if err != nil {
		// handle error
	}

	fmt.Println(rs[0].Bindings["x"])

	// Output:
	//
	// [foo bar baz]
}

func ExampleRego_print_statements() {

	var buf bytes.Buffer

	r := rego.New(
		rego.Query("data.example.rule_containing_print_call"),
		rego.Module("example.rego", `
			package example
			rule_containing_print_call {
				print("input.foo is:", input.foo, "and input.bar is:", input.bar)
			}
		`),
		rego.Input(map[string]interface{}{
			"foo": 7,
		}),
		rego.EnablePrintStatements(true),
		rego.PrintHook(topdown.NewPrintHook(&buf)),
	)

	_, err := r.Eval(context.Background())
	if err != nil {
		// handle error
	}

	fmt.Println("buf:", buf.String())

	// Output:
	//
	// buf: input.foo is: 7 and input.bar is: <undefined>
}

func main() {
	// demo1()
	// demo2()
	demo5()
}

// enovy policy demo :https://www.openpolicyagent.org/docs/latest/envoy-primer/


 package http

        default allow = true

        # Allow may also be an object and include other properties

        # For example, if you wanted to redirect on a policy failure, you could set the status code to 301 and set the location header on the response:
        allow = {
            "status_code": 301,
            "additional_headers": {
                "location": "https://my.site/authorize"
            }
        } {
            not jwt.payload["my-claim"]
        }

        # You can also allow the request and add additional headers to it:
        allow = {
            "allow": true,
            "additional_headers": {
                "x-my-claim": my_claim
            }
        } {
            my_claim := jwt.payload["my-claim"]
        }
        jwt = { "payload": payload } {
            auth_header := input.request.headers["authorization"]
            [_, jwt] := split(auth_header, " ")
            [_, payload, _] := io.jwt.decode(jwt)
        }

简单的demo

package main

import (
	"fmt"
	"strings"

	"github.com/gin-gonic/gin"
	"github.com/open-policy-agent/opa/rego"
)

func ttt() gin.HandlerFunc {
	return func(c *gin.Context) {
		method := strings.ToLower(c.Request.Method)
		uri := c.Request.RequestURI
		fmt.Println(method, uri)
		// Create query that returns a single boolean value.
		rego := rego.New(
			rego.Query("data.authz.allow"),
			rego.Module("example.rego",
				`package authz
default allow = false
allow {
	input.method == "get"
	input.uri != "/"
}`,
			),
			rego.Input(map[string]interface{}{"method": method, "uri": uri}),
		)

		// Run evaluation.
		rs, err := rego.Eval(c)
		if err != nil {
			panic(err)
		}

		// Inspect result.
		fmt.Println("allowed:", rs.Allowed())

		if rs.Allowed() {
			c.Next()
		} else {
			c.JSON(401, "error")
			c.Abort()
		}
	}
}

func main() {
	r := gin.Default()
	r.Use(ttt())
	r.GET("/", func(c *gin.Context) {
		c.JSON(200, gin.H{
			"message": "Hello golang!",
		})
	})
	r.Run("0.0.0.0:8080") // listen and serve on 0.0.0.0:8080
}

// package http

// default allow = true

// # Allow may also be an object and include other properties

// # For example, if you wanted to redirect on a policy failure, you could set the status code to 301 and set the location header on the response:
// allow = {
// 	"status_code": 301,
// 	"additional_headers": {
// 		"location": "https://my.site/authorize"
// 	}
// } {
// 	not jwt.payload["my-claim"]
// }

// # You can also allow the request and add additional headers to it:
// allow = {
// 	"allow": true,
// 	"additional_headers": {
// 		"x-my-claim": my_claim
// 	}
// } {
// 	my_claim := jwt.payload["my-claim"]
// }
// jwt = { "payload": payload } {
// 	auth_header := input.request.headers["authorization"]
// 	[_, jwt] := split(auth_header, " ")
// 	[_, payload, _] := io.jwt.decode(jwt)
// }

复杂点的操作

package utils

import (
	"context"
	"encoding/json"
	"fmt"
	"reflect"
	"strings"
	"sync"
	"testing"

	"github.com/open-policy-agent/opa/ast"
	"github.com/open-policy-agent/opa/rego"
)

type Enforcer struct {
	mu    sync.Mutex
	cache map[interface{}]*rego.Rego
	rules *ast.Compiler
}

func NewEnforcer() (*Enforcer, error) {
	// Define a simple policy.
	module := `package testing

default allow = false

allow {
	input.path = ["user", user_id]
	input.method == "GET"
	input.user.id != null
	user_id == input.user.id
}`

	// Compile the module. The keys are used as identifiers in error messages.
	rules, err := ast.CompileModules(map[string]string{
		"testing.rego": module,
	})

	if err != nil {
		return nil, fmt.Errorf("failed to load policy rules: %v", err)
	}

	return &Enforcer{
		cache: map[interface{}]*rego.Rego{},
		rules: rules,
	}, nil
}

func (e *Enforcer) getRegoEngine(query string, unknowns ...string) *rego.Rego {
	key := fmt.Sprintf("%q: {%q}", query, unknowns)
	e.mu.Lock()
	defer e.mu.Unlock()

	query = "data." + query

	if cache, ok := e.cache[key]; ok {
		return cache
	}

	if len(unknowns) == 0 {
		e.cache[key] = rego.New(
			rego.Compiler(e.rules),
			rego.Query(query),
		)

		return e.cache[key]
	}

	data := make([]string, len(unknowns))
	for i, unknown := range unknowns {
		data[i] = "data." + unknown
	}

	e.cache[key] = rego.New(
		rego.Compiler(e.rules),
		rego.Query(query),
		rego.Unknowns(data),
	)

	return e.cache[key]
}

func (e *Enforcer) Allow(ctx context.Context, query string, input map[string]interface{}) (bool, error) {
	eng := e.getRegoEngine(query)

	e.mu.Lock()
	pq, err := eng.PrepareForEval(ctx)
	e.mu.Unlock()

	if err != nil {
		return false, fmt.Errorf("failed to prepare opa query: %w", err)
	}

	rs, err := pq.Eval(ctx, rego.EvalInput(input))
	if err != nil {
		return false, fmt.Errorf("failed to evaluate opa query: %w", err)
	}

	return rs.Allowed(), nil
}

func TestAllowed(t *testing.T) {
	t.Parallel()

	ctx := context.Background()

	p, err := NewEnforcer()
	if err != nil {
		t.Fatalf("Failed to initiate policy enforcer: %v", err)
	}

	testCases := []struct {
		name     string
		input    map[string]interface{}
		expected bool
	}{
		{"GET anonymous denied", map[string]interface{}{
			"path":   []interface{}{"user", 2},
			"method": "GET",
		},
			false},
		{"GET own user allowed", map[string]interface{}{
			"path":   []interface{}{"user", 2},
			"method": "GET",
			"user":   map[string]interface{}{"id": 2},
		},
			true},
		{"GET another user denied", map[string]interface{}{
			"path":   []interface{}{"user", 2},
			"method": "GET",
			"user":   map[string]interface{}{"id": 3},
		},
			false},
	}

	for _, tt := range testCases {
		tt := tt
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			allowed, err := p.Allow(ctx, "testing.allow", tt.input)
			if err != nil {
				t.Errorf("Unexpected error: %v, %v", err, tt.name)
			}

			if allowed != tt.expected {
				t.Errorf("Unexpected result. Expected %v, got %v. case_name: %v", tt.expected, allowed, tt.name)
			}
		})
	}
}

type PartialSQL struct {
	Fields []string
	Clause string
	Values []interface{}
}

type User struct {
	UserAttributes
	ID       int64    `json:"id"`
	Username string   `json:"username"`
	Password string   `json:"password,omitempty"`
	Role     []string `json:"role"`
	Email    string   `json:"email,omitempty"`
	GoogleID string   `json:"googleID,omitempty"`
}

// UserAttributes are a collection of non-primary fields stored in the config
// column of the user table.
type UserAttributes struct {
	DCI  string `json:"dci,omitempty"`
	Name string `json:"name,omitempty"`
}

var (
	// ErrMissingUnknowns is the error returned in Partial is called without
	// a list of unknown variables to construct partial SQL from.
	ErrMissingUnknowns = fmt.Errorf("cannot create partial query without unknowns")
	// ErrOperatorNotSupported is the error returned if the partial rego rule
	// uses an operator that cannot be translated to SQL (yet).
	ErrOperatorNotSupported = fmt.Errorf("invalid expression: operator not supported")
)

func queriesToSQL(modules []*ast.Module) (PartialSQL, error) {
	var (
		fields  = []string{}
		clauses = []string{}
		values  = []interface{}{}
	)

	for _, module := range modules {
		for _, rule := range module.Rules {
			sub, err := expressionsToSQL(rule.Body)
			if err != nil {
				return PartialSQL{}, err
			}
			if sub.Clause != "" {
				fields = append(fields, sub.Fields...)
				clauses = append(clauses, sub.Clause)
				values = append(values, sub.Values...)
			}
		}
	}

	return PartialSQL{
		Fields: fields,
		Clause: strings.Join(clauses, " OR "),
		Values: values,
	}, nil
}

var exprToOperator = map[string]string{
	"eq":       "=",
	"equal":    "=",
	"lt":       "<",
	"gt":       ">",
	"lte":      "<=",
	"gte":      ">=",
	"neq":      "!=",
	"contains": "CONTAINS",
	"re_match": "REGEXP",
}

func expressionsToSQL(expressions []*ast.Expr) (PartialSQL, error) {
	var (
		clauses = []string{}
		values  = []interface{}{}
	)

	for _, expr := range expressions {
		if !expr.IsCall() {
			continue
		}

		tableColumn, value, err := termsToTableColumnAndValue(expr.Operands())
		if err != nil {
			return PartialSQL{}, err
		}

		operator, ok := exprToOperator[expr.Operator().String()]
		if !ok {
			return PartialSQL{}, fmt.Errorf("%w: %q", ErrOperatorNotSupported, expr.Operator().String())
		}

		clauses = append(clauses, fmt.Sprintf("%s %s ?", tableColumn, operator))
		values = append(values, value)
	}

	sql := PartialSQL{}
	switch len(clauses) {
	case 0:
		// unchanged
	case 1:
		sql.Clause = clauses[0]
		sql.Values = values
	default:
		sql.Clause = "(" + strings.Join(clauses, " AND ") + ")"
		sql.Values = values
	}

	return sql, nil
}

func termsToTableColumnAndValue(terms []*ast.Term) (tableColumn string, value interface{}, err error) {
	if len(terms) != 2 {
		err = fmt.Errorf("invalid expression: too many arguments")
		return
	}

	for _, term := range terms {
		if ast.IsConstant(term.Value) {
			if value, err = ast.JSON(term.Value); err != nil {
				err = fmt.Errorf("error converting term to JSON: %w", err)
				return
			}

			if n, ok := value.(json.Number); ok {
				if i, err := n.Int64(); err == nil {
					value = i
				} else if f, err := n.Float64(); err == nil {
					value = f
				} else {
					panic("Whoops")
				}
			}

			continue
		}

		tokens := []string{}
		for _, token := range strings.Split(term.String(), ".") {
			tokens = append(tokens, strings.Split(token, "[")[0])
		}

		l := len(tokens)
		tableColumn = tokens[l-2] + "." + tokens[l-1]
	}

	return
}

func (e *Enforcer) Partial(ctx context.Context, query string, unknowns []string, input map[string]interface{}) (bool, PartialSQL, error) {
	if len(unknowns) == 0 {
		return false, PartialSQL{}, ErrMissingUnknowns
	}

	eng := e.getRegoEngine(query, unknowns...)

	e.mu.Lock()
	ppq, err := eng.PrepareForPartial(ctx)
	e.mu.Unlock()

	if err != nil {
		return false, PartialSQL{}, fmt.Errorf("failed to prepare partial opa query: %w", err)
	}

	pq, err := ppq.Partial(ctx, rego.EvalInput(input))
	if err != nil {
		return false, PartialSQL{}, fmt.Errorf("failed to evaluate partial opa query: %w", err)
	}

	if len(pq.Support) == 0 {
		return false, PartialSQL{}, nil
	}

	allowed := false
	fields := []string{}
	for _, module := range pq.Support {
		for _, rule := range module.Rules {
			permit := false
			if err := ast.As(rule.Head.Value.Value, &permit); err == nil {
				allowed = allowed || permit
				continue
			}

			f := []string{}
			if err := ast.As(rule.Head.Value.Value, &fields); err == nil {
				fields = append(fields, f...)
				allowed = allowed || len(fields) > 0
				continue
			}
		}
	}

	sql, err := queriesToSQL(pq.Support)
	sql.Fields = fields

	return allowed, sql, err
}

func TestPartial(t *testing.T) {
	t.Parallel()

	ctx := context.Background()

	p, err := NewEnforcer()
	if err != nil {
		t.Fatalf("Failed to initiate policy enforcer: %v", err)
	}

	testCases := []struct {
		name     string
		input    map[string]interface{}
		expected bool
		clause   string
		values   []interface{}
		fields   []string
	}{
		{"GET anonymous denied", map[string]interface{}{
			"path":   []string{"api", "character", "1"},
			"method": "GET",
		},
			false,
			``,
			[]interface{}{},
			[]string{},
		},
		{"GET own character allowed", map[string]interface{}{
			"path":   []string{"api", "character", "1"},
			"method": "GET",
			"user": &User{
				ID:       2,
				Username: "alice",
				Role:     []string{"player"},
			},
		},
			true,
			`(character.user_id = ? AND character.id = ?) OR (members.user_id = ? AND character.id = ?)`,
			[]interface{}{int64(2), int64(1), int64(2), int64(1)},
			[]string{"user_id", "name", "level", "config"},
		},
		{"GET list of character allowed", map[string]interface{}{
			"path":   []string{"api", "character"},
			"method": "GET",
			"user": &User{
				ID:       2,
				Username: "alice",
				Role:     []string{"player"},
			},
		},
			true,
			`character.user_id = ? OR members.user_id = ?`,
			[]interface{}{int64(2), int64(2)},
			[]string{"user_id", "name", "level"},
		},
		{"GET some others character will not work", map[string]interface{}{
			"path":   []string{"api", "character", "1"},
			"method": "GET",
			"user": &User{
				ID:       6,
				Username: "trudy",
				Role:     []string{"player"},
			},
		},
			true,
			`(character.user_id = ? AND character.id = ?) OR (members.user_id = ? AND character.id = ?)`,
			[]interface{}{int64(6), int64(1), int64(6), int64(1)},
			[]string{"user_id", "name", "level", "config"},
		},
		{"GET party character possible", map[string]interface{}{
			"path":   []string{"api", "character", "1"},
			"method": "GET",
			"user": &User{
				ID:       3,
				Username: "bob",
				Role:     []string{"player"},
			},
		},
			true,
			`(character.user_id = ? AND character.id = ?) OR (members.user_id = ? AND character.id = ?)`,
			[]interface{}{int64(3), int64(1), int64(3), int64(1)},
			[]string{"user_id", "name", "level", "config"},
		},
		{"GET character as admin allowed", map[string]interface{}{
			"path":   []string{"api", "character", "1"},
			"method": "GET",
			"user": &User{
				ID:       1,
				Username: "admin",
				Role:     []string{"admin"},
			},
		},
			true,
			`character.id = ?`,
			[]interface{}{int64(1)},
			[]string{"user_id", "name", "level", "config"},
		},
		{"PATCH own character allowed", map[string]interface{}{
			"path":   []string{"api", "character", "1"},
			"method": "PATCH",
			"user": &User{
				ID:       2,
				Username: "alice",
				Role:     []string{"player"},
			},
		},
			true,
			`(character.user_id = ? AND character.id = ?)`,
			[]interface{}{int64(2), int64(1)},
			[]string{"name", "config"},
		},
		{"PATCH others character will not work", map[string]interface{}{
			"path":   []string{"api", "character", "2"},
			"method": "PATCH",
			"user": &User{
				ID:       2,
				Username: "alice",
				Role:     []string{"player"},
			},
		},
			true,
			`(character.user_id = ? AND character.id = ?)`,
			[]interface{}{int64(2), int64(2)},
			[]string{"name", "config"},
		},
		{"DELETE own character allowed", map[string]interface{}{
			"path":   []string{"api", "character", "1"},
			"method": "DELETE",
			"user": &User{
				ID:       2,
				Username: "alice",
				Role:     []string{"player"},
			},
		},
			true,
			`(character.user_id = ? AND character.id = ?)`,
			[]interface{}{int64(2), int64(1)},
			[]string{},
		},
		{"DELETE others character will not work", map[string]interface{}{
			"path":   []string{"api", "character", "2"},
			"method": "DELETE",
			"user": &User{
				ID:       2,
				Username: "alice",
				Role:     []string{"player"},
			},
		},
			true,
			`(character.user_id = ? AND character.id = ?)`,
			[]interface{}{int64(2), int64(2)},
			[]string{},
		},
	}

	for _, tt := range testCases {
		tt := tt
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			possible, sql, err := p.Partial(ctx,
				"authz.character.allow",
				[]string{"character"},
				tt.input,
			)
			if err != nil {
				t.Errorf("Unexpected error: %v", err)
			}

			if possible != tt.expected {
				t.Errorf("Unexpected result. Expected %v, got %v.",
					tt.expected, possible)
			}

			if sql.Clause != tt.clause {
				t.Errorf("Unexpected clause. Expected %q, got %q.",
					tt.clause, sql.Clause)
			}

			if !reflect.DeepEqual(sql.Values, tt.values) {
				t.Errorf("Unexpected values. Expected %q, got %q.",
					tt.values, sql.Values)
			}

			if !reflect.DeepEqual(sql.Fields, tt.fields) {
				t.Errorf("Unexpected fields. Expected %q, got %q.",
					tt.fields, sql.Fields)
			}
		})
	}
}

保存为opa_test.go, 运行以下命令

➜  utils git:(master) ✗ go test -v -run TestAllowed opa_test.go
=== RUN   TestAllowed
=== PAUSE TestAllowed
=== CONT  TestAllowed
=== RUN   TestAllowed/GET_anonymous_denied
=== PAUSE TestAllowed/GET_anonymous_denied
=== RUN   TestAllowed/GET_own_user_allowed
=== PAUSE TestAllowed/GET_own_user_allowed
=== RUN   TestAllowed/GET_another_user_denied
=== PAUSE TestAllowed/GET_another_user_denied
=== CONT  TestAllowed/GET_anonymous_denied
=== CONT  TestAllowed/GET_another_user_denied
=== CONT  TestAllowed/GET_own_user_allowed
--- PASS: TestAllowed (0.00s)
    --- PASS: TestAllowed/GET_anonymous_denied (0.00s)
    --- PASS: TestAllowed/GET_own_user_allowed (0.00s)
    --- PASS: TestAllowed/GET_another_user_denied (0.00s)
PASS
ok      command-line-arguments  0.388s

参考

在线测试地址:https://play.openpolicyagent.org/p/ZXkIlAEPCY

Logo

K8S/Kubernetes社区为您提供最前沿的新闻资讯和知识内容

更多推荐