diff --git a/sdk/opa.go b/sdk/opa.go index 69175a5a4d..545478d88a 100644 --- a/sdk/opa.go +++ b/sdk/opa.go @@ -27,6 +27,7 @@ import ( "github.com/open-policy-agent/opa/server" "github.com/open-policy-agent/opa/storage" "github.com/open-policy-agent/opa/storage/inmem" + "github.com/open-policy-agent/opa/topdown" "github.com/open-policy-agent/opa/topdown/builtins" "github.com/open-policy-agent/opa/topdown/cache" "github.com/open-policy-agent/opa/topdown/print" @@ -254,6 +255,7 @@ func (opa *OPA) Decision(ctx context.Context, options DecisionOptions) (*Decisio input: *record.Input, m: record.Metrics, strictBuiltinErrors: options.StrictBuiltinErrors, + tracer: options.Tracer, }) if record.Error == nil { record.Results = &result.Result @@ -269,11 +271,12 @@ func (opa *OPA) Decision(ctx context.Context, options DecisionOptions) (*Decisio // DecisionOptions contains parameters for query evaluation. type DecisionOptions struct { - Now time.Time // specifies wallclock time used for time.now_ns(), decision log timestamp, etc. - Path string // specifies name of policy decision to evaluate (e.g., example/allow) - Input interface{} // specifies value of the input document to evaluate policy with - NDBCache interface{} // specifies the non-deterministic builtins cache to use for evaluation. - StrictBuiltinErrors bool // treat built-in function errors as fatal + Now time.Time // specifies wallclock time used for time.now_ns(), decision log timestamp, etc. + Path string // specifies name of policy decision to evaluate (e.g., example/allow) + Input interface{} // specifies value of the input document to evaluate policy with + NDBCache interface{} // specifies the non-deterministic builtins cache to use for evaluation. + StrictBuiltinErrors bool // treat built-in function errors as fatal + Tracer topdown.QueryTracer // specifies the tracer to use for evaluation, optional } // DecisionResult contains the output of query evaluation. @@ -364,6 +367,7 @@ func (opa *OPA) Partial(ctx context.Context, options PartialOptions) (*PartialRe input: *record.Input, m: record.Metrics, strictBuiltinErrors: options.StrictBuiltinErrors, + tracer: options.Tracer, }) if record.Error == nil { result.Result, record.Error = options.Mapper.MapResults(pq) @@ -398,12 +402,13 @@ type PartialQueryMapper interface { // PartialOptions contains parameters for partial query evaluation. type PartialOptions struct { - Now time.Time // specifies wallclock time used for time.now_ns(), decision log timestamp, etc. - Input interface{} // specifies value of the input document to evaluate policy with - Query string // specifies the query to be partially evaluated - Unknowns []string // specifies the unknown elements of the policy - Mapper PartialQueryMapper // specifies the mapper to use when processing results - StrictBuiltinErrors bool // treat built-in function errors as fatal + Now time.Time // specifies wallclock time used for time.now_ns(), decision log timestamp, etc. + Input interface{} // specifies value of the input document to evaluate policy with + Query string // specifies the query to be partially evaluated + Unknowns []string // specifies the unknown elements of the policy + Mapper PartialQueryMapper // specifies the mapper to use when processing results + StrictBuiltinErrors bool // treat built-in function errors as fatal + Tracer topdown.QueryTracer // specifies the tracer to use for evaluation, optional } type PartialResult struct { @@ -454,6 +459,7 @@ type evalArgs struct { ndbcache builtins.NDBCache m metrics.Metrics strictBuiltinErrors bool + tracer topdown.QueryTracer } func evaluate(ctx context.Context, args evalArgs) (interface{}, ast.Value, map[string]server.BundleInfo, error) { @@ -501,6 +507,7 @@ func evaluate(ctx context.Context, args evalArgs) (interface{}, ast.Value, map[s rego.EvalMetrics(args.m), rego.EvalInterQueryBuiltinCache(args.interQueryCache), rego.EvalNDBuiltinCache(args.ndbcache), + rego.EvalQueryTracer(args.tracer), ) if err != nil { return nil, inputAST, bundles, err @@ -523,6 +530,7 @@ type partialEvalArgs struct { input interface{} m metrics.Metrics strictBuiltinErrors bool + tracer topdown.QueryTracer } func partial(ctx context.Context, args partialEvalArgs) (*rego.PartialQueries, ast.Value, map[string]server.BundleInfo, error) { @@ -548,6 +556,7 @@ func partial(ctx context.Context, args partialEvalArgs) (*rego.PartialQueries, a rego.Unknowns(args.unknowns), rego.PrintHook(args.printHook), rego.StrictBuiltinErrors(args.strictBuiltinErrors), + rego.QueryTracer(args.tracer), ) pq, err := re.Partial(ctx) diff --git a/sdk/opa_test.go b/sdk/opa_test.go index e7d1cba18e..4106d57fbb 100644 --- a/sdk/opa_test.go +++ b/sdk/opa_test.go @@ -19,6 +19,7 @@ import ( "github.com/open-policy-agent/opa/rego" "github.com/open-policy-agent/opa/topdown" "github.com/open-policy-agent/opa/topdown/builtins" + "github.com/open-policy-agent/opa/topdown/lineage" "github.com/fortytw2/leaktest" loggingtest "github.com/open-policy-agent/opa/logging/test" @@ -105,7 +106,11 @@ func TestSDKConfigurableID(t *testing.T) { server := sdktest.MustNewServer( sdktest.MockBundle("/bundles/bundle.tar.gz", map[string]string{ - "main.rego": "package system\nmain = time.now_ns()", + "main.rego": ` +package system + +main = time.now_ns() +`, }), ) @@ -160,14 +165,14 @@ func TestDecision(t *testing.T) { server := sdktest.MustNewServer( sdktest.MockBundle("/bundles/bundle.tar.gz", map[string]string{ "main.rego": ` - package system +package system - main = true +main = true - str = "foo" +str = "foo" - loopback = input - `, +loopback = input +`, }), ) @@ -223,7 +228,7 @@ func TestDecisionWithStrictBuiltinErrors(t *testing.T) { server := sdktest.MustNewServer( sdktest.MockBundle("/bundles/bundle.tar.gz", map[string]string{ "main.rego": ` - package example +package example erroring_function(number) = output { output := number / 0 @@ -231,7 +236,8 @@ erroring_function(number) = output { allow { erroring_function(1) -}`, +} +`, }), ) @@ -272,29 +278,103 @@ allow { t.Fatalf("expected eval_builtin_error but got %v", actual) } - expectedMessage := "div: divide by zero" - if actual.Message != expectedMessage { - t.Fatalf("expected %v but got %v", expectedMessage, actual.Message) + if exp, act := "div: divide by zero", actual.Message; exp != act { + t.Fatalf("expected %v but got %v", exp, act) } } -func TestPartial(t *testing.T) { +func TestDecisionWithTrace(t *testing.T) { ctx := context.Background() server := sdktest.MustNewServer( sdktest.MockBundle("/bundles/bundle.tar.gz", map[string]string{ "main.rego": ` - package test - allow { - data.junk.x = input.y - } +package system + +main { + trace("foobar") + true +} `, }), ) defer server.Stop() + config := fmt.Sprintf(`{ + "services": { + "test": { + "url": %q + } + }, + "bundles": { + "test": { + "resource": "/bundles/bundle.tar.gz" + } + } + }`, server.URL()) + + opa, err := sdk.New(ctx, sdk.Options{ + Config: strings.NewReader(config), + }) + if err != nil { + t.Fatal(err) + } + + defer opa.Stop(ctx) + + tracer := topdown.NewBufferTracer() + + if result, err := opa.Decision(ctx, sdk.DecisionOptions{ + Path: "/system/main", + Tracer: tracer, + }); err != nil { + t.Fatal(err) + } else if decision, ok := result.Result.(bool); !ok || !decision { + t.Fatal("expected true but got:", decision, ok) + } + + events := lineage.Notes(*(tracer)) + if exp, act := 3, len(events); exp != act { + t.Fatalf("expected %d events, got %d", exp, act) + } + + if exp, act := "Enter", string(events[0].Op); exp != act { + t.Errorf("expected %s event, got %s", exp, act) + } + if exp, act := "data.system.main", string(events[0].Location.Text); exp != act { + t.Errorf("expected location %q got %q", exp, act) + } + if exp, act := "Enter", string(events[1].Op); exp != act { + t.Errorf("expected %s event, got %v", exp, act) + } + if exp, act := "Note", string(events[2].Op); exp != act { + t.Errorf("expected %s event, got %v", exp, act) + } + if exp, act := "foobar", events[2].Message; exp != act { + t.Errorf("unexpected message, wanted %q, got %q", exp, act) + } +} + +func TestPartial(t *testing.T) { + + ctx := context.Background() + + server := sdktest.MustNewServer( + sdktest.MockBundle("/bundles/bundle.tar.gz", map[string]string{ + "main.rego": ` +package test + +allow { + data.junk.x = input.y +} +`, + }), + ) + + defer server.Stop() + config := fmt.Sprintf(`{ "services": { "test": { @@ -363,7 +443,7 @@ func TestPartialWithStrictBuiltinErrors(t *testing.T) { server := sdktest.MustNewServer( sdktest.MockBundle("/bundles/bundle.tar.gz", map[string]string{ "main.rego": ` - package example +package example erroring_function(number) = output { output := number / 0 @@ -371,7 +451,8 @@ erroring_function(number) = output { allow { erroring_function(1) -}`, +} +`, }), ) @@ -421,11 +502,90 @@ allow { t.Fatalf("expected eval_builtin_error but got %v", actual) } - expectedMessage := "div: divide by zero" - if actual.Message != expectedMessage { - t.Fatalf("expected %v but got %v", expectedMessage, actual.Message) + if exp, act := "div: divide by zero", actual.Message; exp != act { + t.Fatalf("expected %v but got %v", exp, act) } +} + +func TestPartialWithTrace(t *testing.T) { + + ctx := context.Background() + server := sdktest.MustNewServer( + sdktest.MockBundle("/bundles/bundle.tar.gz", map[string]string{ + "main.rego": ` +package system + +main { + trace("foobar") +} +`, + }), + ) + + defer server.Stop() + + config := fmt.Sprintf(`{ + "services": { + "test": { + "url": %q + } + }, + "bundles": { + "test": { + "resource": "/bundles/bundle.tar.gz" + } + }, + "decision_logs": { + "console": true + } + }`, server.URL()) + + testLogger := loggingtest.New() + opa, err := sdk.New(ctx, sdk.Options{ + Config: strings.NewReader(config), + ConsoleLogger: testLogger, + }) + if err != nil { + t.Fatal(err) + } + + defer opa.Stop(ctx) + + tracer := topdown.NewBufferTracer() + _, err = opa.Partial(ctx, sdk.PartialOptions{ + Input: map[string]interface{}{}, + Query: "data.system.main", + Unknowns: []string{}, + Mapper: &sdk.RawMapper{}, + Now: time.Unix(0, 1619868194450288000).UTC(), + Tracer: tracer, + }) + if err != nil { + t.Fatalf("unexpected error %v", err) + } + + events := lineage.Notes(*(tracer)) + + if exp, act := 3, len(events); exp != act { + t.Fatalf("expected %d events, got %d", exp, act) + } + + if exp, act := "Enter", string(events[0].Op); exp != act { + t.Errorf("expected %s event, got %s", exp, act) + } + if exp, act := "data.system.main", string(events[0].Location.Text); exp != act { + t.Errorf("expected location %q got %q", exp, act) + } + if exp, act := "Enter", string(events[1].Op); exp != act { + t.Errorf("expected %s event, got %v", exp, act) + } + if exp, act := "Note", string(events[2].Op); exp != act { + t.Errorf("expected %s event, got %v", exp, act) + } + if exp, act := "foobar", events[2].Message; exp != act { + t.Errorf("unexpected message, wanted %q, got %q", exp, act) + } } func TestUndefinedError(t *testing.T) { @@ -479,7 +639,11 @@ func TestDecisionLogging(t *testing.T) { server := sdktest.MustNewServer( sdktest.MockBundle("/bundles/bundle.tar.gz", map[string]string{ - "main.rego": "package system\nmain = time.now_ns()", + "main.rego": ` +package system + +main = time.now_ns() +`, }), ) @@ -541,7 +705,11 @@ func TestDecisionLoggingWithNDBCache(t *testing.T) { server := sdktest.MustNewServer( sdktest.MockBundle("/bundles/bundle.tar.gz", map[string]string{ - "main.rego": "package system\nmain = time.now_ns()", + "main.rego": ` +package system + +main = time.now_ns() +`, }), ) @@ -611,7 +779,11 @@ func TestQueryCaching(t *testing.T) { server := sdktest.MustNewServer( sdktest.MockBundle("/bundles/bundle.tar.gz", map[string]string{ - "main.rego": "package system\nmain = 7", + "main.rego": ` +package system + +main = 7 +`, }), ) @@ -676,16 +848,16 @@ func TestDiscovery(t *testing.T) { server := sdktest.MustNewServer( sdktest.MockBundle("/bundles/discovery.tar.gz", map[string]string{ "bundles.rego": ` - package bundles +package bundles - test := {"resource": "/bundles/bundle.tar.gz"} +test := {"resource": "/bundles/bundle.tar.gz"} `, }), sdktest.MockBundle("/bundles/bundle.tar.gz", map[string]string{ "main.rego": ` - package system +package system - main = 7 +main = 7 `, }), ) @@ -732,7 +904,11 @@ func TestAsync(t *testing.T) { server := sdktest.MustNewServer( sdktest.Ready(callerReadyCh), sdktest.MockBundle("/bundles/bundle.tar.gz", map[string]string{ - "main.rego": "package system\nmain = 7", + "main.rego": ` +package system + +main = 7 +`, }), ) @@ -844,7 +1020,11 @@ func TestConfigAsYAML(t *testing.T) { server := sdktest.MustNewServer( sdktest.MockBundle("/bundles/bundle.tar.gz", map[string]string{ - "main.rego": "package system\nmain = 7", + "main.rego": ` +package system + +main = 7 +`, }), ) defer server.Stop() @@ -870,10 +1050,18 @@ func TestConfigure(t *testing.T) { server := sdktest.MustNewServer( sdktest.MockBundle("/bundles/bundle1.tar.gz", map[string]string{ - "main.rego": "package system\nmain = 7", + "main.rego": ` +package system + +main = 7 +`, }), sdktest.MockBundle("/bundles/bundle2.tar.gz", map[string]string{ - "main.rego": "package system\nmain = 8", + "main.rego": ` +package system + +main = 8 +`, }), ) defer server.Stop() @@ -958,8 +1146,9 @@ func TestOpaVersion(t *testing.T) { server := sdktest.MustNewServer( sdktest.MockBundle("/bundles/bundle.tar.gz", map[string]string{ "main.rego": ` - package system - opa_version := opa.runtime().version +package system + +opa_version := opa.runtime().version `, }), ) @@ -1003,14 +1192,15 @@ func TestOpaRuntimeConfig(t *testing.T) { server := sdktest.MustNewServer( sdktest.MockBundle("/bundles/bundle.tar.gz", map[string]string{ "main.rego": ` - package system - rt := opa.runtime() - - result := { - "service_url": rt.config.services.test.url, - "bundle_resource": rt.config.bundles.test.resource, - "test_label": rt.config.labels.test - } +package system + +rt := opa.runtime() + +result := { + "service_url": rt.config.services.test.url, + "bundle_resource": rt.config.bundles.test.resource, + "test_label": rt.config.labels.test +} `, }), ) @@ -1063,9 +1253,11 @@ func TestPrintStatements(t *testing.T) { ctx := context.Background() s := sdktest.MustNewServer(sdktest.MockBundle("/bundles/b.tar.gz", map[string]string{ - "x.rego": `package foo + "x.rego": ` +package foo - p { print("XXX") }`, +p { print("XXX") } +`, })) defer s.Stop()