Skip to content

Commit

Permalink
Support for typename import aliases
Browse files Browse the repository at this point in the history
  • Loading branch information
TristonianJones committed Aug 2, 2024
1 parent d62ef29 commit b2bfca8
Show file tree
Hide file tree
Showing 10 changed files with 222 additions and 85 deletions.
12 changes: 12 additions & 0 deletions common/containers/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package containers
import (
"fmt"
"strings"
"unicode"

"github.com/google/cel-go/common/ast"
)
Expand Down Expand Up @@ -212,6 +213,13 @@ type ContainerOption func(*Container) (*Container, error)
func Abbrevs(qualifiedNames ...string) ContainerOption {
return func(c *Container) (*Container, error) {
for _, qn := range qualifiedNames {
qn = strings.TrimSpace(qn)
for _, r := range []rune(qn) {
if !isIdentifierChar(r) {
return nil, fmt.Errorf(
"invalid qualified name: %s, wanted name of the form 'qualified.name'", qn)
}
}
ind := strings.LastIndex(qn, ".")
if ind <= 0 || ind >= len(qn)-1 {
return nil, fmt.Errorf(
Expand Down Expand Up @@ -278,6 +286,10 @@ func aliasAs(kind, qualifiedName, alias string) ContainerOption {
}
}

func isIdentifierChar(r rune) bool {
return r <= unicode.MaxASCII && (r == '.' || r == '_' || unicode.IsLetter(r) || unicode.IsNumber(r))
}

// Name sets the fully-qualified name of the Container.
func Name(name string) ContainerOption {
return func(c *Container) (*Container, error) {
Expand Down
122 changes: 74 additions & 48 deletions common/containers/container_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package containers

import (
"fmt"
"reflect"
"testing"

Expand Down Expand Up @@ -104,54 +105,79 @@ func TestContainers_Abbrevs(t *testing.T) {
}

func TestContainers_Aliasing_Errors(t *testing.T) {
_, err := NewContainer(Abbrevs("my.alias.R", "yer.other.R"))
wantErr := "abbreviation collides with existing reference: " +
"name=yer.other.R, abbreviation=R, existing=my.alias.R"
if err == nil || err.Error() != wantErr {
t.Errorf("got error %v, expected %s.", err, wantErr)
}

_, err = NewContainer(Name("a.b.c.M.N"), Abbrevs("my.alias.a", "yer.other.b"))
wantErr = "abbreviation collides with container name: name=my.alias.a, " +
"abbreviation=a, container=a.b.c.M.N"
if err == nil || err.Error() != wantErr {
t.Errorf("got error %v, expected %s.", err, wantErr)
}

_, err = NewContainer(Abbrevs(".bad"))
wantErr = "invalid qualified name: .bad, wanted name of the form 'qualified.name'"
if err == nil || err.Error() != wantErr {
t.Errorf("got error %v, expected %s.", err, wantErr)
}

_, err = NewContainer(Abbrevs("bad.alias."))
wantErr = "invalid qualified name: bad.alias., wanted name of the form 'qualified.name'"
if err == nil || err.Error() != wantErr {
t.Errorf("got error %v, expected %s.", err, wantErr)
}

_, err = NewContainer(Alias("a", "b"))
wantErr = "alias must refer to a valid qualified name: a"
if err == nil || err.Error() != wantErr {
t.Errorf("got error %v, expected %s.", err, wantErr)
}

_, err = NewContainer(Alias("my.alias", "b.c"))
wantErr = "alias must be non-empty and simple (not qualified): alias=b.c"
if err == nil || err.Error() != wantErr {
t.Errorf("got error %v, expected %s.", err, wantErr)
}

_, err = NewContainer(Alias(".my.qual.name", "a"))
wantErr = "qualified name must not begin with a leading '.': .my.qual.name"
if err == nil || err.Error() != wantErr {
t.Errorf("got error %v, expected %s.", err, wantErr)
}

_, err = NewContainer(Alias(".my.qual.name", "a"))
wantErr = "qualified name must not begin with a leading '.': .my.qual.name"
if err == nil || err.Error() != wantErr {
t.Errorf("got error %v, expected %s.", err, wantErr)
type aliasDef struct {
name string
alias string
}
tests := []struct {
container string
abbrevs []string
aliases []aliasDef
err string
}{
{
abbrevs: []string{"my.alias.R", "yer.other.R"},
err: "abbreviation collides with existing reference: " +
"name=yer.other.R, abbreviation=R, existing=my.alias.R",
},
{
container: "a.b.c.M.N",
abbrevs: []string{"my.alias.a", "yer.other.b"},
err: "abbreviation collides with container name: name=my.alias.a, " +
"abbreviation=a, container=a.b.c.M.N",
},
{
abbrevs: []string{".bad"},
err: "invalid qualified name: .bad, wanted name of the form 'qualified.name'",
},
{
abbrevs: []string{"bad.alias."},
err: "invalid qualified name: bad.alias., wanted name of the form 'qualified.name'",
},
{
abbrevs: []string{" bad_alias1"},
err: "invalid qualified name: bad_alias1, wanted name of the form 'qualified.name'",
},
{
abbrevs: []string{" bad.alias! "},
err: "invalid qualified name: bad.alias!, wanted name of the form 'qualified.name'",
},
{
aliases: []aliasDef{{name: "a", alias: "b"}},
err: "alias must refer to a valid qualified name: a",
},
{
aliases: []aliasDef{{name: "my.alias", alias: "b.c"}},
err: "alias must be non-empty and simple (not qualified): alias=b.c",
},
{
aliases: []aliasDef{{name: ".my.qual.name", alias: "a'"}},
err: "qualified name must not begin with a leading '.': .my.qual.name",
},
}
for i, tst := range tests {
tc := tst
t.Run(fmt.Sprintf("[%d]", i), func(t *testing.T) {
opts := []ContainerOption{}
if tc.container != "" {
opts = append(opts, Name(tc.container))
}
if len(tc.abbrevs) != 0 {
opts = append(opts, Abbrevs(tc.abbrevs...))
}
if len(tc.aliases) != 0 {
for _, a := range tc.aliases {
opts = append(opts, Alias(a.name, a.alias))
}
}
_, err := NewContainer(opts...)
if err == nil {
t.Fatalf("NewContainer() succeeded, wanted err %s", tc.err)
}
if err.Error() != tc.err {
t.Errorf("NewContainer() got error %s, wanted error %s", err.Error(), tc.err)
}
})
}
}

Expand Down
15 changes: 11 additions & 4 deletions policy/compiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"github.com/google/cel-go/cel"
"github.com/google/cel-go/common"
"github.com/google/cel-go/common/ast"
"github.com/google/cel-go/common/containers"
"github.com/google/cel-go/common/decls"
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref"
Expand Down Expand Up @@ -228,14 +229,20 @@ func CompileRule(env *cel.Env, p *Policy, opts ...CompilerOption) (*CompiledRule

importCount := len(p.Imports())
if importCount > 0 {
importNames := make([]string, importCount)
for i, imp := range p.Imports() {
importNames := make([]string, 0, importCount)
for _, imp := range p.Imports() {
typeName := imp.Name().Value
importNames[i] = typeName
_, err := containers.NewContainer(containers.Abbrevs(typeName))
if err != nil {
iss.ReportErrorAtID(imp.Name().ID, "error configuring import: %s", err)
} else {
importNames = append(importNames, typeName)
}
}
env, err := c.env.Extend(cel.Abbrevs(importNames...))
if err != nil {
iss.ReportErrorAtID(p.Imports()[0].Name().ID, "error configuring imports: %s", err)
// validation happens earlier in the sequence, so this should be unreachable.
iss.ReportErrorAtID(p.Imports()[0].SourceID(), "error configuring imports: %s", err)
} else {
c.env = env
}
Expand Down
14 changes: 14 additions & 0 deletions policy/compiler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,20 @@ func TestCompiledRuleHasOptionalOutput(t *testing.T) {
}
}

func TestMaxNestedExpressions_Error(t *testing.T) {
policyName := "required_labels"
wantError := `ERROR: testdata/required_labels/policy.yaml:15:8: error configuring compiler option: nested expression limit must be non-negative, non-zero value: -1
| name: "required_labels"
| .......^`
_, _, iss := compile(t, policyName, []ParserOption{}, []cel.EnvOption{}, []CompilerOption{MaxNestedExpressions(-1)})
if iss.Err() == nil {
t.Fatalf("compile(%s) did not error, wanted %s", policyName, wantError)
}
if iss.Err().Error() != wantError {
t.Errorf("compile(%s) got error %s, wanted %s", policyName, iss.Err().Error(), wantError)
}
}

func BenchmarkCompile(b *testing.B) {
for _, tst := range policyTests {
r := newRunner(b, tst.name, tst.expr, tst.parseOpts, tst.envOpts...)
Expand Down
43 changes: 23 additions & 20 deletions policy/helper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,26 +63,26 @@ var (
{
name: "nested_rule2",
expr: `
cel.bind(variables.permitted_regions, ["us", "uk", "es"],
resource.?user.orValue("").startsWith("bad")
? cel.bind(variables.banned_regions, {"us": false, "ru": false, "ir": false},
cel.bind(variables.permitted_regions, ["us", "uk", "es"],
resource.?user.orValue("").startsWith("bad")
? cel.bind(variables.banned_regions, {"us": false, "ru": false, "ir": false},
(resource.origin in variables.banned_regions &&
!(resource.origin in variables.permitted_regions))
? {"banned": "restricted_region"} : {"banned": "bad_actor"})
: (!(resource.origin in variables.permitted_regions)
!(resource.origin in variables.permitted_regions))
? {"banned": "restricted_region"} : {"banned": "bad_actor"})
: (!(resource.origin in variables.permitted_regions)
? {"banned": "unconfigured_region"} : {}))`,
},
{
name: "nested_rule3",
expr: `
cel.bind(variables.permitted_regions, ["us", "uk", "es"],
resource.?user.orValue("").startsWith("bad")
cel.bind(variables.permitted_regions, ["us", "uk", "es"],
resource.?user.orValue("").startsWith("bad")
? optional.of(
cel.bind(variables.banned_regions, {"us": false, "ru": false, "ir": false},
(resource.origin in variables.banned_regions &&
!(resource.origin in variables.permitted_regions))
? {"banned": "restricted_region"} : {"banned": "bad_actor"}))
: (!(resource.origin in variables.permitted_regions)
!(resource.origin in variables.permitted_regions))
? {"banned": "restricted_region"} : {"banned": "bad_actor"}))
: (!(resource.origin in variables.permitted_regions)
? optional.of({"banned": "unconfigured_region"}) : optional.none()))`,
},
{
Expand Down Expand Up @@ -172,31 +172,34 @@ var (
}{
{
name: "errors",
err: `ERROR: testdata/errors/policy.yaml:17:12: error configuring imports: invalid qualified name: bad import, wanted name of the form 'qualified.name'
err: `ERROR: testdata/errors/policy.yaml:19:1: error configuring import: invalid qualified name: punc.Import!, wanted name of the form 'qualified.name'
| punc.Import!
| ^
ERROR: testdata/errors/policy.yaml:20:12: error configuring import: invalid qualified name: bad import, wanted name of the form 'qualified.name'
| - name: "bad import"
| ...........^
ERROR: testdata/errors/policy.yaml:21:19: undeclared reference to 'spec' (in container '')
ERROR: testdata/errors/policy.yaml:24:19: undeclared reference to 'spec' (in container '')
| expression: spec.labels
| ..................^
ERROR: testdata/errors/policy.yaml:22:7: invalid variable declaration: overlapping identifier for name 'variables.want'
ERROR: testdata/errors/policy.yaml:25:7: invalid variable declaration: overlapping identifier for name 'variables.want'
| - name: want
| ......^
ERROR: testdata/errors/policy.yaml:25:50: Syntax error: mismatched input 'resource' expecting ')'
ERROR: testdata/errors/policy.yaml:28:50: Syntax error: mismatched input 'resource' expecting ')'
| expression: variables.want.filter(l, !(lin resource.labels))
| .................................................^
ERROR: testdata/errors/policy.yaml:25:66: Syntax error: extraneous input ')' expecting <EOF>
ERROR: testdata/errors/policy.yaml:28:66: Syntax error: extraneous input ')' expecting <EOF>
| expression: variables.want.filter(l, !(lin resource.labels))
| .................................................................^
ERROR: testdata/errors/policy.yaml:27:27: Syntax error: mismatched input '2' expecting {'}', ','}
ERROR: testdata/errors/policy.yaml:30:27: Syntax error: mismatched input '2' expecting {'}', ','}
| expression: "{1:305 2:569}"
| ..........................^
ERROR: testdata/errors/policy.yaml:35:75: Syntax error: extraneous input ']' expecting ')'
ERROR: testdata/errors/policy.yaml:38:75: Syntax error: extraneous input ']' expecting ')'
| "missing one or more required labels: %s".format(variables.missing])
| ..........................................................................^
ERROR: testdata/errors/policy.yaml:38:67: undeclared reference to 'format' (in container '')
ERROR: testdata/errors/policy.yaml:41:67: undeclared reference to 'format' (in container '')
| "invalid values provided on one or more labels: %s".format([variables.invalid])
| ..................................................................^
ERROR: testdata/errors/policy.yaml:38:16: incompatible output types: bool not assignable to string
ERROR: testdata/errors/policy.yaml:45:16: incompatible output types: bool not assignable to string
| output: "'false'"
| ...............^`,
},
Expand Down
26 changes: 19 additions & 7 deletions policy/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ func (p *Policy) SourceInfo() *ast.SourceInfo {
return p.info
}

// Imports returns the list of imports associated with the policy.
func (p *Policy) Imports() []*Import {
return p.imports
}
Expand Down Expand Up @@ -94,8 +95,9 @@ func (p *Policy) MetadataKeys() []string {
return keys
}

func (p *Policy) AddImport(name ValueString) {
p.imports = append(p.imports, &Import{name: name})
// AddImport adds an import to the policy.
func (p *Policy) AddImport(i *Import) {
p.imports = append(p.imports, i)
}

// SetName configures the policy name.
Expand Down Expand Up @@ -129,18 +131,28 @@ func (p *Policy) GetExplanationOutputPolicy() *Policy {
return &ep
}

func NewImport() *Import {
return &Import{}
// NewImport creates a new typename import node
func NewImport(exprID int64) *Import {
return &Import{exprID: exprID}
}

// Import represents an imported type name which is aliased within CEL expressions.
type Import struct {
name ValueString
exprID int64
name ValueString
}

// SourceID returns the source identifier associated with the import.
func (i *Import) SourceID() int64 {
return i.exprID
}

// Name returns the fully qualified type name.
func (i *Import) Name() ValueString {
return i.name
}

// SetName updates the fully qualified type name for the import.
func (i *Import) SetName(name ValueString) {
i.name = name
}
Expand Down Expand Up @@ -627,13 +639,13 @@ func (p *parserImpl) parseImports(ctx ParserContext, policy *Policy, node *yaml.
return
}
for _, val := range node.Content {
policy.AddImport(p.parseImport(ctx, policy, val).Name())
policy.AddImport(p.parseImport(ctx, policy, val))
}
}

func (p *parserImpl) parseImport(ctx ParserContext, _ *Policy, node *yaml.Node) *Import {
id := ctx.CollectMetadata(node)
imp := NewImport()
imp := NewImport(id)
if p.assertYamlType(id, node, yamlMap) == nil || !p.checkMapValid(ctx, id, node) {
return imp
}
Expand Down
Loading

0 comments on commit b2bfca8

Please sign in to comment.