diff --git a/ast/annotations.go b/ast/annotations.go index 9663b0cc67..7d09379fd5 100644 --- a/ast/annotations.go +++ b/ast/annotations.go @@ -417,7 +417,7 @@ func (a *Annotations) Copy(node Node) *Annotations { return &cpy } -// toObject constructs an AST Object from a. +// toObject constructs an AST Object from the annotation. func (a *Annotations) toObject() (*Object, *Error) { obj := NewObject() @@ -556,7 +556,11 @@ func attachAnnotationsNodes(mod *Module) Errors { if a.Scope == "" { switch a.node.(type) { case *Rule: - a.Scope = annotationScopeRule + if a.Entrypoint { + a.Scope = annotationScopeDocument + } else { + a.Scope = annotationScopeRule + } case *Package: a.Scope = annotationScopePackage case *Import: @@ -596,8 +600,9 @@ func validateAnnotationScopeAttachment(a *Annotations) *Error { } func validateAnnotationEntrypointAttachment(a *Annotations) *Error { - if a.Entrypoint && !(a.Scope == annotationScopeRule || a.Scope == annotationScopePackage) { - return NewError(ParseErr, a.Loc(), "annotation entrypoint applied to non-rule or package scope '%v'", a.Scope) + if a.Entrypoint && !(a.Scope == annotationScopeDocument || a.Scope == annotationScopePackage) { + return NewError( + ParseErr, a.Loc(), "annotation entrypoint applied to non-document or package scope '%v'", a.Scope) } return nil } diff --git a/ast/annotations_test.go b/ast/annotations_test.go index 05563d5197..57b317de0e 100644 --- a/ast/annotations_test.go +++ b/ast/annotations_test.go @@ -10,6 +10,88 @@ import ( "testing" ) +func TestEntrypointAnnotationScopeRequirements(t *testing.T) { + tests := []struct { + note string + module string + expectError bool + expectScope string + }{ + { + note: "package scope explicit", + module: `# METADATA +# entrypoint: true +# scope: package +package foo`, + expectError: false, + expectScope: "package", + }, + { + note: "package scope implied", + module: `# METADATA +# entrypoint: true +package foo`, + expectError: false, + expectScope: "package", + }, + { + note: "subpackages scope explicit", + module: `# METADATA +# entrypoint: true +# scope: subpackages +package foo`, + expectError: true, + }, + { + note: "document scope explicit", + module: `package foo +# METADATA +# entrypoint: true +# scope: document +foo := true`, + expectError: false, + expectScope: "document", + }, + { + note: "document scope implied", + module: `package foo +# METADATA +# entrypoint: true +foo := true`, + expectError: false, + expectScope: "document", + }, + { + note: "rule scope explicit", + module: `package foo +# METADATA +# entrypoint: true +# scope: rule +foo := true`, + expectError: true, + }, + } + + for _, tc := range tests { + t.Run(tc.note, func(t *testing.T) { + module, err := ParseModuleWithOpts("test.rego", tc.module, ParserOptions{ProcessAnnotation: true}) + if err != nil { + if !tc.expectError { + t.Errorf("unexpected error: %v", err) + } + return + } + if tc.expectError { + t.Fatalf("expected error") + } + if tc.expectScope != module.Annotations[0].Scope { + t.Fatalf("expected scope %q, got %q", tc.expectScope, module.Annotations[0].Scope) + } + }) + } + +} + // Test of example code in docs/content/annotations.md func ExampleAnnotationSet_Flatten() { modules := [][]string{ diff --git a/cmd/build_test.go b/cmd/build_test.go index 3dc1bcfbb6..0b8178acde 100644 --- a/cmd/build_test.go +++ b/cmd/build_test.go @@ -653,7 +653,7 @@ p2 := 2 "entrypoint":"test/p2", "module":"/policy.wasm", "annotations":[{ - "scope":"rule", + "scope":"document", "title":"P2", "entrypoint":true }] @@ -742,7 +742,7 @@ bar := "baz" "entrypoint":"test/foo/bar", "module":"/policy.wasm", "annotations":[{ - "scope":"rule", + "scope":"document", "title":"BAR", "entrypoint":true }] @@ -750,7 +750,7 @@ bar := "baz" "entrypoint":"test/p2", "module":"/policy.wasm", "annotations":[{ - "scope":"rule", + "scope":"document", "title":"P2", "entrypoint":true }] @@ -767,10 +767,10 @@ package test # METADATA # title: P doc # scope: document +# entrypoint: true # METADATA # title: P -# entrypoint: true p := 1 `, }, @@ -784,11 +784,11 @@ p := 1 "module":"/policy.wasm", "annotations":[{ "scope":"document", - "title":"P doc" + "title":"P doc", + "entrypoint":true },{ "scope":"rule", - "title":"P", - "entrypoint":true + "title":"P" }] }] } diff --git a/compile/compile.go b/compile/compile.go index a0ac6fc4be..142cb87716 100644 --- a/compile/compile.go +++ b/compile/compile.go @@ -255,20 +255,20 @@ func (c *Compiler) WithRegoVersion(v ast.RegoVersion) *Compiler { return c } -func addEntrypointsFromAnnotations(c *Compiler, ar []*ast.AnnotationsRef) error { - for _, ref := range ar { +func addEntrypointsFromAnnotations(c *Compiler, arefs []*ast.AnnotationsRef) error { + for _, aref := range arefs { var entrypoint ast.Ref - scope := ref.Annotations.Scope + scope := aref.Annotations.Scope - if ref.Annotations.Entrypoint { + if aref.Annotations.Entrypoint { // Build up the entrypoint path from either package path or rule. switch scope { case "package": - if p := ref.GetPackage(); p != nil { + if p := aref.GetPackage(); p != nil { entrypoint = p.Path } - case "rule": - if r := ref.GetRule(); r != nil { + case "document": + if r := aref.GetRule(); r != nil { entrypoint = r.Ref().GroundPrefix() } default: diff --git a/compile/compile_test.go b/compile/compile_test.go index b3db2646b8..a5d1201e71 100644 --- a/compile/compile_test.go +++ b/compile/compile_test.go @@ -2106,7 +2106,7 @@ q = true`, Annotations: []*ast.Annotations{ { Title: "My P rule", - Scope: "rule", + Scope: "document", Entrypoint: true, }, }, @@ -2366,7 +2366,7 @@ func TestCompilerRegoEntrypointAnnotations(t *testing.T) { wantEntrypoints map[string]struct{} }{ { - note: "rule annotation", + note: "implied document scope annotation", entrypoints: []string{}, modules: map[string]string{ "test.rego": ` diff --git a/docs/content/policy-language.md b/docs/content/policy-language.md index dd4d25dc2e..efea244be4 100644 --- a/docs/content/policy-language.md +++ b/docs/content/policy-language.md @@ -2687,8 +2687,12 @@ Since the `document` scope annotation applies to all rules with the same name in and the `package` and `subpackages` scope annotations apply to all packages with a matching path, metadata blocks with these scopes are applied over all files with applicable package- and rule paths. As there is no ordering across files in the same package, the `document`, `package`, and `subpackages` scope annotations -can only be specified **once** per path. -The `document` scope annotation can be applied to any rule in the set (i.e., ordering does not matter.) +can only be specified **once** per path. The `document` scope annotation can be applied to any rule in the set (i.e., +ordering does not matter.) + +An `entrypoint` annotation implies a `scope` of either `package` or `document`. When `entrypoint` is set to `true` on a +rule, the `scope` is automatically set to `document` if not explicitly provided. Setting the `scope` to `rule` will +result in an error, as an entrypoint always applies to the whole document. #### Example @@ -2708,6 +2712,17 @@ allow if { allow if { x == 2 } + +# METADATA +# entrypoint: true +# description: | +# `scope` annotation automatically set to `document` +# as that is required for entrypoints +message := "welcome!" if allow +``` + +```live:rego/metadata/scope:package:read_only + ``` ### Title @@ -2890,7 +2905,8 @@ allow if { ### Entrypoint The `entrypoint` annotation is a boolean used to mark rules and packages that should be used as entrypoints for a policy. -This value is false by default, and can only be used at `rule` or `package` scope. +This value is false by default, and can only be used at `document` or `package` scope. When used on a rule with no +explicit `scope` set, the presence of an `entrypoint` annotation will automatically set the scope to `document`. The `build` and `eval` CLI commands will automatically pick up annotated entrypoints; you do not have to specify them with [`--entrypoint`](../cli/#options-1).