k8s--访问管理opa
介绍使用下载按照系统或指定的版本,下载对应的命令行执行文件, 如下(我的是mac):curl -L -o opa https://openpolicyagent.org/downloads/v0.37.2/opa_darwin_amd64chmod +x opaln -s /usr/opa /usr/local/bin模版input/input1.json{"action": {"operatio
介绍
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(
®o.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(
®o.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 := ®o.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
更多推荐
所有评论(0)