Skip to content

Commit

Permalink
ast: add 'features' to capabilities, use it to check for ref heads (#…
Browse files Browse the repository at this point in the history
…5208)

Signed-off-by: Stephan Renatus <stephan.renatus@gmail.com>
  • Loading branch information
srenatus authored Oct 14, 2022
1 parent e67eb80 commit 2f264b1
Show file tree
Hide file tree
Showing 4 changed files with 89 additions and 0 deletions.
15 changes: 15 additions & 0 deletions ast/capabilities.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,24 @@ import (
"github.com/open-policy-agent/opa/util"
)

// In the compiler, we used this to check that we're OK working with ref heads.
// If this isn't present, we'll fail. This is to ensure that older versions of
// OPA can work with policies that we're compiling -- if they don't know ref
// heads, they wouldn't be able to parse them.
const FeatureRefHeadStringPrefixes = "rule_head_ref_string_prefixes"

// Capabilities defines a structure containing data that describes the capabilities
// or features supported by a particular version of OPA.
type Capabilities struct {
Builtins []*Builtin `json:"builtins"`
FutureKeywords []string `json:"future_keywords"`
WasmABIVersions []WasmABIVersion `json:"wasm_abi_versions"`

// Features is a bit of a mixed bag for checking that an older version of OPA
// is able to do what needs to be done.
// TODO(sr): find better words ^^
Features []string `json:"features"`

// allow_net is an array of hostnames or IP addresses, that an OPA instance is
// allowed to connect to.
// If omitted, ANY host can be connected to. If empty, NO host can be connected to.
Expand Down Expand Up @@ -60,6 +71,10 @@ func CapabilitiesForThisVersion() *Capabilities {
}
sort.Strings(f.FutureKeywords)

f.Features = []string{
FeatureRefHeadStringPrefixes,
}

return f
}

Expand Down
13 changes: 13 additions & 0 deletions ast/compile.go
Original file line number Diff line number Diff line change
Expand Up @@ -1635,6 +1635,19 @@ func (c *Compiler) rewriteRuleHeadRefs() {
rule.Head.Reference = ref
}

cannotSpeakRefs := true
for _, f := range c.capabilities.Features {
if f == FeatureRefHeadStringPrefixes {
cannotSpeakRefs = false
break
}
}

if cannotSpeakRefs && rule.Head.Name == "" {
c.err(NewError(CompileErr, rule.Loc(), "rule heads with refs are not supported: %v", rule.Head.Reference))
return true
}

for i := 1; i < len(ref); i++ {
// NOTE(sr): In the first iteration, non-string values in the refs are forbidden
// except for the last position, e.g.
Expand Down
58 changes: 58 additions & 0 deletions ast/compile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3418,6 +3418,64 @@ x.y.w contains bar[i] if true
}
}

func TestCompilerRefHeadsNeedCapability(t *testing.T) {
popts := ParserOptions{AllFutureKeywords: true, unreleasedKeywords: true}
for _, tc := range []struct {
note string
mod *Module
err string
}{
{
note: "one-dot ref, single-value rule, short+compat",
mod: MustParseModule(`package t
p[1] = 2`),
},
{
note: "one-dot ref, single-value rule, compat",
mod: MustParseModuleWithOpts(`package t
p[3] = 4 if true`, popts),
},
{
note: "multi-value non-ref head",
mod: MustParseModuleWithOpts(`package t
p contains 1 if true`, popts),
},
{ // NOTE(sr): this was previously forbidden because we need the `if` for disambiguation
note: "one-dot ref head",
mod: MustParseModuleWithOpts(`package t
p[1] if true`, popts),
err: "rule heads with refs are not supported: p[1]",
},
{
note: "single-value ref rule",
mod: MustParseModuleWithOpts(`package t
a.b.c[x] if x := input`, popts),
err: "rule heads with refs are not supported: a.b.c[x]",
},
{
note: "multi-value ref rule",
mod: MustParseModuleWithOpts(`package t
a.b.c contains x if x := input`, popts),
err: "rule heads with refs are not supported: a.b.c",
},
} {
t.Run(tc.note, func(t *testing.T) {
caps, err := LoadCapabilitiesVersion("v0.44.0")
if err != nil {
t.Fatal(err)
}
c := NewCompiler().WithCapabilities(caps)
c.Modules["test"] = tc.mod
compileStages(c, c.rewriteRefsInHead)
if tc.err != "" {
assertErrorWithMessage(t, c.Errors, tc.err)
} else {
assertNotFailed(t, c)
}
})
}
}

func TestCompilerRewriteRegoMetadataCalls(t *testing.T) {
tests := []struct {
note string
Expand Down
3 changes: 3 additions & 0 deletions capabilities.json
Original file line number Diff line number Diff line change
Expand Up @@ -4302,5 +4302,8 @@
"version": 1,
"minor_version": 2
}
],
"features": [
"rule_head_ref_string_prefixes"
]
}

0 comments on commit 2f264b1

Please sign in to comment.