diff --git a/cel/env.go b/cel/env.go index 3af2b36f..a8650c4e 100644 --- a/cel/env.go +++ b/cel/env.go @@ -44,6 +44,9 @@ type Ast struct { // NativeRep converts the AST to a Go-native representation. func (ast *Ast) NativeRep() *celast.AST { + if ast == nil { + return nil + } return ast.impl } @@ -55,16 +58,13 @@ func (ast *Ast) Expr() *exprpb.Expr { if ast == nil { return nil } - pbExpr, _ := celast.ExprToProto(ast.impl.Expr()) + pbExpr, _ := celast.ExprToProto(ast.NativeRep().Expr()) return pbExpr } // IsChecked returns whether the Ast value has been successfully type-checked. func (ast *Ast) IsChecked() bool { - if ast == nil { - return false - } - return ast.impl.IsChecked() + return ast.NativeRep().IsChecked() } // SourceInfo returns character offset and newline position information about expression elements. @@ -72,7 +72,7 @@ func (ast *Ast) SourceInfo() *exprpb.SourceInfo { if ast == nil { return nil } - pbInfo, _ := celast.SourceInfoToProto(ast.impl.SourceInfo()) + pbInfo, _ := celast.SourceInfoToProto(ast.NativeRep().SourceInfo()) return pbInfo } @@ -95,7 +95,7 @@ func (ast *Ast) OutputType() *Type { if ast == nil { return types.ErrorType } - return ast.impl.GetType(ast.impl.Expr().ID()) + return ast.NativeRep().GetType(ast.NativeRep().Expr().ID()) } // Source returns a view of the input used to create the Ast. This source may be complete or @@ -218,12 +218,12 @@ func (e *Env) Check(ast *Ast) (*Ast, *Issues) { if err != nil { errs := common.NewErrors(ast.Source()) errs.ReportError(common.NoLocation, err.Error()) - return nil, NewIssuesWithSourceInfo(errs, ast.impl.SourceInfo()) + return nil, NewIssuesWithSourceInfo(errs, ast.NativeRep().SourceInfo()) } - checked, errs := checker.Check(ast.impl, ast.Source(), chk) + checked, errs := checker.Check(ast.NativeRep(), ast.Source(), chk) if len(errs.GetErrors()) > 0 { - return nil, NewIssuesWithSourceInfo(errs, ast.impl.SourceInfo()) + return nil, NewIssuesWithSourceInfo(errs, ast.NativeRep().SourceInfo()) } // Manually create the Ast to ensure that the Ast source information (which may be more // detailed than the information provided by Check), is returned to the caller. @@ -244,7 +244,7 @@ func (e *Env) Check(ast *Ast) (*Ast, *Issues) { } } // Apply additional validators on the type-checked result. - iss := NewIssuesWithSourceInfo(errs, ast.impl.SourceInfo()) + iss := NewIssuesWithSourceInfo(errs, ast.NativeRep().SourceInfo()) for _, v := range e.validators { v.Validate(e, vConfig, checked, iss) } diff --git a/policy/BUILD.bazel b/policy/BUILD.bazel index e4131b9d..1cc2b3c0 100644 --- a/policy/BUILD.bazel +++ b/policy/BUILD.bazel @@ -37,6 +37,7 @@ go_library( "//common/decls:go_default_library", "//common/operators:go_default_library", "//common/types:go_default_library", + "//common/types/ref:go_default_library", "//ext:go_default_library", "@in_gopkg_yaml_v3//:go_default_library", ], diff --git a/policy/compiler.go b/policy/compiler.go index de2100ec..a8a67128 100644 --- a/policy/compiler.go +++ b/policy/compiler.go @@ -24,10 +24,12 @@ import ( "github.com/google/cel-go/common/ast" "github.com/google/cel-go/common/decls" "github.com/google/cel-go/common/types" + "github.com/google/cel-go/common/types/ref" ) // CompiledRule represents the variables and match blocks associated with a rule block. type CompiledRule struct { + exprID int64 id *ValueString variables []*CompiledVariable matches []*CompiledMatch @@ -35,7 +37,7 @@ type CompiledRule struct { // SourceID returns the source metadata identifier associated with the compiled rule. func (r *CompiledRule) SourceID() int64 { - return r.ID().ID + return r.exprID } // ID returns the expression id associated with the rule. @@ -63,9 +65,25 @@ func (r *CompiledRule) OutputType() *cel.Type { return cel.DynType } +// HasOptionalOutput returns whether the rule returns a concrete or optional value. +// The rule may return an optional value if all match expressions under the rule are conditional. +func (r *CompiledRule) HasOptionalOutput() bool { + optionalOutput := false + for _, m := range r.Matches() { + if m.NestedRule() != nil && m.NestedRule().HasOptionalOutput() { + return true + } + if m.ConditionIsLiteral(types.True) { + return false + } + optionalOutput = true + } + return optionalOutput +} + // CompiledVariable represents the variable name, expression, and associated type-check declaration. type CompiledVariable struct { - id int64 + exprID int64 name string expr *cel.Ast varDecl *decls.VariableDecl @@ -73,7 +91,7 @@ type CompiledVariable struct { // SourceID returns the source metadata identifier associated with the variable. func (v *CompiledVariable) SourceID() int64 { - return v.id + return v.exprID } // Name returns the variable name. @@ -94,17 +112,29 @@ func (v *CompiledVariable) Declaration() *decls.VariableDecl { // CompiledMatch represents a match block which has an optional condition (true, by default) as well // as an output or a nested rule (one or the other, but not both). type CompiledMatch struct { + exprID int64 cond *cel.Ast output *OutputValue nestedRule *CompiledRule } +// SourceID returns the source identifier associated with the compiled match. +func (m *CompiledMatch) SourceID() int64 { + return m.exprID +} + // Condition returns the compiled predicate expression which must evaluate to true before the output // or subrule is entered. func (m *CompiledMatch) Condition() *cel.Ast { return m.cond } +// ConditionIsLiteral indicates whether the condition for the match is a literal with a given value. +func (m *CompiledMatch) ConditionIsLiteral(val ref.Val) bool { + c := m.cond.NativeRep().Expr() + return c.Kind() == ast.LiteralKind && c.AsLiteral().Equal(val) == types.True +} + // Output returns the compiled output expression associated with the match block, if set. func (m *CompiledMatch) Output() *OutputValue { return m.output @@ -128,13 +158,13 @@ func (m *CompiledMatch) OutputType() *cel.Type { // OutputValue represents the output expression associated with a match block. type OutputValue struct { - id int64 - expr *cel.Ast + exprID int64 + expr *cel.Ast } -// ID returns the expression id associated with the output expression. -func (o *OutputValue) ID() int64 { - return o.id +// SourceID returns the expression id associated with the output expression. +func (o *OutputValue) SourceID() int64 { + return o.exprID } // Expr returns the compiled expression associated with the output. @@ -229,7 +259,7 @@ func (c *compiler) compileRule(r *Rule, ruleEnv *cel.Env, iss *cel.Issues) (*Com iss.ReportErrorAtID(v.Expression().ID, "invalid variable declaration") } compiledVar := &CompiledVariable{ - id: v.name.ID, + exprID: v.name.ID, name: v.name.Value, expr: varAST, varDecl: varDecl, @@ -261,10 +291,11 @@ func (c *compiler) compileRule(r *Rule, ruleEnv *cel.Env, iss *cel.Issues) (*Com outAST, outIss := ruleEnv.CompileSource(outSrc) iss = iss.Append(outIss) compiledMatches = append(compiledMatches, &CompiledMatch{ - cond: condAST, + exprID: m.exprID, + cond: condAST, output: &OutputValue{ - id: m.Output().ID, - expr: outAST, + exprID: m.Output().ID, + expr: outAST, }, }) continue @@ -273,6 +304,7 @@ func (c *compiler) compileRule(r *Rule, ruleEnv *cel.Env, iss *cel.Issues) (*Com nestedRule, ruleIss := c.compileRule(m.Rule(), ruleEnv, iss) iss = iss.Append(ruleIss) compiledMatches = append(compiledMatches, &CompiledMatch{ + exprID: m.exprID, cond: condAST, nestedRule: nestedRule, }) @@ -285,13 +317,20 @@ func (c *compiler) compileRule(r *Rule, ruleEnv *cel.Env, iss *cel.Issues) (*Com } } + // Validate that all branches in the rule are reachable rule := &CompiledRule{ + exprID: r.exprID, id: r.id, variables: compiledVars, matches: compiledMatches, } + + // Note: Consider supporting configurable policy validators that take the policy, rule, and issues // Validate type agreement between the different match outputs c.checkMatchOutputTypesAgree(rule, iss) + // Validate that all branches in the policy are reachable + c.checkUnreachableCode(rule, iss) + return rule, iss } @@ -309,13 +348,35 @@ func (c *compiler) checkMatchOutputTypesAgree(rule *CompiledRule, iss *cel.Issue if matchOutputType.TypeName() == "error" { continue } - if !outputType.IsAssignableType(matchOutputType) { - iss.ReportErrorAtID(m.Output().ID(), "incompatible output types: %s not assignable to %s", outputType, matchOutputType) + // Handle assignability as the output type is assignable to the match output or vice versa. + // During composition, this is roughly how the type-checker will handle the type agreement check. + if !(outputType.IsAssignableType(matchOutputType) || matchOutputType.IsAssignableType(outputType)) { + iss.ReportErrorAtID(m.Output().SourceID(), "incompatible output types: %s not assignable to %s", outputType, matchOutputType) return } } } +func (c *compiler) checkUnreachableCode(rule *CompiledRule, iss *cel.Issues) { + ruleHasOptional := rule.HasOptionalOutput() + compiledMatches := rule.Matches() + matchCount := len(compiledMatches) + for i := matchCount - 1; i >= 0; i-- { + m := compiledMatches[i] + triviallyTrue := m.ConditionIsLiteral(types.True) + + if triviallyTrue && !ruleHasOptional && i != matchCount-1 { + if m.Output() != nil { + iss.ReportErrorAtID(m.SourceID(), "match creates unreachable outputs") + } + if m.NestedRule() != nil { + iss.ReportErrorAtID(m.NestedRule().SourceID(), "rule creates unreachable outputs") + } + break + } + } +} + func (c *compiler) relSource(pstr ValueString) *RelativeSource { line := 0 col := 1 diff --git a/policy/compiler_test.go b/policy/compiler_test.go index ecad14f9..9323dce9 100644 --- a/policy/compiler_test.go +++ b/policy/compiler_test.go @@ -43,6 +43,49 @@ func TestCompileError(t *testing.T) { } } +func TestCompiledRuleHasOptionalOutput(t *testing.T) { + env, err := cel.NewEnv() + if err != nil { + t.Fatalf("cel.NewEnv() failed: %v", err) + } + tests := []struct { + rule *CompiledRule + optional bool + }{ + {rule: &CompiledRule{}, optional: false}, + { + rule: &CompiledRule{ + matches: []*CompiledMatch{{}}, + }, + optional: true, + }, + { + rule: &CompiledRule{ + matches: []*CompiledMatch{{}}, + }, + optional: true, + }, + { + rule: &CompiledRule{ + matches: []*CompiledMatch{{cond: mustCompileExpr(t, env, "true")}}, + }, + optional: false, + }, + { + rule: &CompiledRule{ + matches: []*CompiledMatch{{cond: mustCompileExpr(t, env, "1 < 0")}}, + }, + optional: true, + }, + } + for _, tst := range tests { + got := tst.rule.HasOptionalOutput() + if got != tst.optional { + t.Errorf("rule.HasOptionalOutput() got %v, wanted, %v", got, tst.optional) + } + } +} + func BenchmarkCompile(b *testing.B) { for _, tst := range policyTests { r := newRunner(b, tst.name, tst.expr, tst.parseOpts, tst.envOpts...) @@ -70,7 +113,17 @@ type runner struct { prg cel.Program } +func mustCompileExpr(t testing.TB, env *cel.Env, expr string) *cel.Ast { + t.Helper() + out, iss := env.Compile(expr) + if iss.Err() != nil { + t.Fatalf("env.Compile(%s) failed: %v", expr, iss.Err()) + } + return out +} + func compile(t testing.TB, name string, parseOpts []ParserOption, envOpts []cel.EnvOption, compilerOpts []CompilerOption) (*cel.Env, *cel.Ast, *cel.Issues) { + t.Helper() config := readPolicyConfig(t, fmt.Sprintf("testdata/%s/config.yaml", name)) srcFile := readPolicy(t, fmt.Sprintf("testdata/%s/policy.yaml", name)) parser, err := NewParser(parseOpts...) @@ -158,12 +211,8 @@ func (r *runner) run(t *testing.T) { } else if testOut.Equal(optOut.GetValue()) != types.True { t.Errorf("policy eval got %v, wanted %v", out, testOut) } - } else if boolOut, ok := out.(types.Bool); ok { - if testOut.Equal(boolOut) != types.True { - t.Errorf("policy eval got %v, wanted %v", boolOut, testOut) - } - } else { - t.Errorf("unexpected policy output type %v", out) + } else if testOut.Equal(out) != types.True { + t.Errorf("policy eval got %v, wanted %v", out, testOut) } }) } diff --git a/policy/composer.go b/policy/composer.go index 5b2f2c2d..022f6a7e 100644 --- a/policy/composer.go +++ b/policy/composer.go @@ -53,21 +53,28 @@ type ruleComposerImpl struct { func (opt *ruleComposerImpl) Optimize(ctx *cel.OptimizerContext, a *ast.AST) *ast.AST { // The input to optimize is a dummy expression which is completely replaced according // to the configuration of the rule composition graph. - ruleExpr, _ := opt.optimizeRule(ctx, opt.rule) + ruleExpr := opt.optimizeRule(ctx, opt.rule) return ctx.NewAST(ruleExpr) } -func (opt *ruleComposerImpl) optimizeRule(ctx *cel.OptimizerContext, r *CompiledRule) (ast.Expr, bool) { +func (opt *ruleComposerImpl) optimizeRule(ctx *cel.OptimizerContext, r *CompiledRule) ast.Expr { matchExpr := ctx.NewCall("optional.none") matches := r.Matches() + matchCount := len(matches) vars := r.Variables() - optionalResult := true + optionalResult := true // Build the rule subgraph. - for i := len(matches) - 1; i >= 0; i-- { + for i := matchCount - 1; i >= 0; i-- { m := matches[i] cond := ctx.CopyASTAndMetadata(m.Condition().NativeRep()) - triviallyTrue := cond.Kind() == ast.LiteralKind && cond.AsLiteral() == types.True + // If the condition is trivially true, not of the matches in the rule causes the result + // to become optional, and the rule is not the last match, then this will introduce + // unreachable outputs or rules. + triviallyTrue := m.ConditionIsLiteral(types.True) + + // If the output is non-nil, then determine whether the output should be wrapped + // into an optional value, a conditional, or both. if m.Output() != nil { out := ctx.CopyASTAndMetadata(m.Output().Expr().NativeRep()) if triviallyTrue { @@ -85,28 +92,33 @@ func (opt *ruleComposerImpl) optimizeRule(ctx *cel.OptimizerContext, r *Compiled matchExpr) continue } - nestedRule, nestedOptional := opt.optimizeRule(ctx, m.NestedRule()) - if optionalResult && !nestedOptional { + + // If the match has a nested rule, then compute the rule and whether it has + // an optional return value. + child := m.NestedRule() + nestedRule := opt.optimizeRule(ctx, child) + nestedHasOptional := child.HasOptionalOutput() + if optionalResult && !nestedHasOptional { nestedRule = ctx.NewCall("optional.of", nestedRule) } - if !optionalResult && nestedOptional { + if !optionalResult && nestedHasOptional { matchExpr = ctx.NewCall("optional.of", matchExpr) optionalResult = true } - if !optionalResult && !nestedOptional { - ctx.ReportErrorAtID(nestedRule.ID(), "subrule early terminates policy") - continue - } - if triviallyTrue { + // If either the nested rule or current condition output are optional then + // use optional.or() to specify the combination of the first and second results + // Note, the argument order is reversed due to the traversal of matches in + // reverse order. + if optionalResult && triviallyTrue { matchExpr = ctx.NewMemberCall("or", nestedRule, matchExpr) - } else { - matchExpr = ctx.NewCall( - operators.Conditional, - cond, - nestedRule, - matchExpr, - ) + continue } + matchExpr = ctx.NewCall( + operators.Conditional, + cond, + nestedRule, + matchExpr, + ) } // Bind variables in reverse order to declaration on top of rule-subgraph. @@ -121,5 +133,5 @@ func (opt *ruleComposerImpl) optimizeRule(ctx *cel.OptimizerContext, r *Compiled ctx.UpdateExpr(matchExpr, inlined) ctx.SetMacroCall(matchExpr.ID(), bindMacro) } - return matchExpr, optionalResult + return matchExpr } diff --git a/policy/helper_test.go b/policy/helper_test.go index f3c84b40..29924e66 100644 --- a/policy/helper_test.go +++ b/policy/helper_test.go @@ -33,6 +33,7 @@ var ( envOpts []cel.EnvOption parseOpts []ParserOption expr string + expr2 string }{ { name: "k8s", @@ -59,6 +60,31 @@ var ( optional.of((resource.origin in variables.permitted_regions) ? {"banned": false} : {"banned": true})))`, }, + { + 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}, + (resource.origin in variables.banned_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") + ? 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) + ? optional.of({"banned": "unconfigured_region"}) : optional.none()))`, + }, { name: "pb", expr: `(spec.single_int32 > 10) @@ -171,14 +197,22 @@ ERROR: testdata/errors/policy.yaml:38:16: incompatible output types: bool not as | .............^`, compilerOpts: []CompilerOption{MaxNestedExpressions(2)}, }, - { name: "limits", - err: `ERROR: testdata/limits/policy.yaml:30:14: rule exceeds nested expression limit + err: `ERROR: testdata/limits/policy.yaml:30:9: rule exceeds nested expression limit | id: "farewells" - | .............^`, + | ........^`, compilerOpts: []CompilerOption{MaxNestedExpressions(5)}, }, + { + name: "errors_unreachable", + err: `ERROR: testdata/errors_unreachable/policy.yaml:28:9: rule creates unreachable outputs + | match: + | ........^ +ERROR: testdata/errors_unreachable/policy.yaml:36:13: match creates unreachable outputs + | - output: | + | ............^`, + }, } ) diff --git a/policy/parser.go b/policy/parser.go index d3e9d156..5dc57e21 100644 --- a/policy/parser.go +++ b/policy/parser.go @@ -120,8 +120,9 @@ func (p *Policy) GetExplanationOutputPolicy() *Policy { } // NewRule creates a Rule instance. -func NewRule() *Rule { +func NewRule(exprID int64) *Rule { return &Rule{ + exprID: exprID, variables: []*Variable{}, matches: []*Match{}, } @@ -129,6 +130,7 @@ func NewRule() *Rule { // Rule declares a rule identifier, description, along with a set of variables and match statements. type Rule struct { + exprID int64 id *ValueString description *ValueString variables []*Variable @@ -204,12 +206,13 @@ func (r *Rule) getExplanationOutputRule() *Rule { } // NewVariable creates a variable instance. -func NewVariable() *Variable { - return &Variable{} +func NewVariable(exprID int64) *Variable { + return &Variable{exprID: exprID} } // Variable is a named expression which may be referenced in subsequent expressions. type Variable struct { + exprID int64 name ValueString expression ValueString } @@ -235,13 +238,14 @@ func (v *Variable) SetExpression(e ValueString) { } // NewMatch creates a match instance. -func NewMatch() *Match { - return &Match{} +func NewMatch(exprID int64) *Match { + return &Match{exprID: exprID} } // Match declares a condition (defaults to true) as well as an output or a rule. // Either the output or the rule field may be set, but not both. type Match struct { + exprID int64 condition ValueString output *ValueString explanation *ValueString @@ -493,22 +497,22 @@ func (p *parserImpl) NewPolicy(node *yaml.Node) (*Policy, int64) { // NewRule creates a new Rule instance with an ID associated with the YAML node. func (p *parserImpl) NewRule(node *yaml.Node) (*Rule, int64) { - r := NewRule() id := p.CollectMetadata(node) + r := NewRule(id) return r, id } // NewVariable creates a new Variable instance with an ID associated with the YAML node. func (p *parserImpl) NewVariable(node *yaml.Node) (*Variable, int64) { - v := NewVariable() id := p.CollectMetadata(node) + v := NewVariable(id) return v, id } // NewMatch creates a new Match instance with an ID associated with the YAML node. func (p *parserImpl) NewMatch(node *yaml.Node) (*Match, int64) { - m := NewMatch() id := p.CollectMetadata(node) + m := NewMatch(id) return m, id } diff --git a/policy/testdata/errors_unreachable/config.yaml b/policy/testdata/errors_unreachable/config.yaml new file mode 100644 index 00000000..21861546 --- /dev/null +++ b/policy/testdata/errors_unreachable/config.yaml @@ -0,0 +1,55 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: "errors_unreachable" +extensions: + - name: "lists" + - name: "sets" + - name: "strings" + version: "latest" +variables: + - name: "destination.ip" + type: + type_name: "string" + - name: "origin.ip" + type: + type_name: "string" + - name: "spec.restricted_destinations" + type: + type_name: "list" + params: + - type_name: "string" + - name: "spec.origin" + type: + type_name: "string" + - name: "request" + type: + type_name: "map" + params: + - type_name: "string" + - type_name: "dyn" + - name: "resource" + type: + type_name: "map" + params: + - type_name: "string" + - type_name: "dyn" +functions: + - name: "locationCode" + overloads: + - id: "locationCode_string" + args: + - type_name: "string" + return: + type_name: "string" diff --git a/policy/testdata/errors_unreachable/policy.yaml b/policy/testdata/errors_unreachable/policy.yaml new file mode 100644 index 00000000..556b8bd9 --- /dev/null +++ b/policy/testdata/errors_unreachable/policy.yaml @@ -0,0 +1,39 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: "errors_unreachable" +rule: + variables: + - name: want + expression: request.labels + - name: missing + expression: variables.want.filter(l, !(l in resource.labels)) + - name: invalid + expression: > + resource.labels.filter(l, + l in variables.want && variables.want[l] != resource.labels[l]) + match: + - rule: + match: + - output: "''" + - condition: variables.missing.size() > 0 + output: | + "missing one or more required labels: %s".format([variables.missing]) + - condition: variables.invalid.size() > 0 + rule: + match: + - output: | + "invalid values provided on one or more labels: %s".format([variables.invalid]) + - condition: "false" + output: "'unreachable'" diff --git a/policy/testdata/nested_rule2/config.yaml b/policy/testdata/nested_rule2/config.yaml new file mode 100644 index 00000000..9c3a3917 --- /dev/null +++ b/policy/testdata/nested_rule2/config.yaml @@ -0,0 +1,22 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: "nested_rule2" +variables: + - name: "resource" + type: + type_name: "map" + params: + - type_name: "string" + - type_name: "dyn" diff --git a/policy/testdata/nested_rule2/policy.yaml b/policy/testdata/nested_rule2/policy.yaml new file mode 100644 index 00000000..ef2c0b81 --- /dev/null +++ b/policy/testdata/nested_rule2/policy.yaml @@ -0,0 +1,40 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: nested_rule2 +rule: + variables: + - name: "permitted_regions" + expression: "['us', 'uk', 'es']" + match: + - condition: resource.?user.orValue("").startsWith("bad") + rule: + id: "banned regions" + description: > + determine whether the resource origin is in the banned + list. If the region is also in the permitted list, the + ban has no effect. + variables: + - name: "banned_regions" + expression: "{'us': false, 'ru': false, 'ir': false}" + match: + - condition: | + resource.origin in variables.banned_regions && + !(resource.origin in variables.permitted_regions) + output: "{'banned': 'restricted_region'}" + explanation: "'resource is in the banned region ' + resource.origin" + - output: "{'banned': 'bad_actor'}" + - condition: "!(resource.origin in variables.permitted_regions)" + output: "{'banned': 'unconfigured_region'}" + - output: "{}" diff --git a/policy/testdata/nested_rule2/tests.yaml b/policy/testdata/nested_rule2/tests.yaml new file mode 100644 index 00000000..cd93b3aa --- /dev/null +++ b/policy/testdata/nested_rule2/tests.yaml @@ -0,0 +1,48 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +description: Nested rule conformance tests +section: + - name: "banned" + tests: + - name: "restricted_origin" + input: + resource: + value: + user: "bad-user" + origin: "ir" + output: "{'banned': 'restricted_region'}" + - name: "by_default" + input: + resource: + value: + user: "bad-user" + origin: "de" + output: "{'banned': 'bad_actor'}" + - name: "unconfigured_region" + input: + resource: + value: + user: "good-user" + origin: "de" + output: "{'banned': 'unconfigured_region'}" + - name: "permitted" + tests: + - name: "valid_origin" + input: + resource: + value: + user: "good-user" + origin: "uk" + output: "{}" diff --git a/policy/testdata/nested_rule3/config.yaml b/policy/testdata/nested_rule3/config.yaml new file mode 100644 index 00000000..dc39114e --- /dev/null +++ b/policy/testdata/nested_rule3/config.yaml @@ -0,0 +1,22 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: "nested_rule3" +variables: + - name: "resource" + type: + type_name: "map" + params: + - type_name: "string" + - type_name: "dyn" diff --git a/policy/testdata/nested_rule3/policy.yaml b/policy/testdata/nested_rule3/policy.yaml new file mode 100644 index 00000000..54e33ba1 --- /dev/null +++ b/policy/testdata/nested_rule3/policy.yaml @@ -0,0 +1,39 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: nested_rule3 +rule: + variables: + - name: "permitted_regions" + expression: "['us', 'uk', 'es']" + match: + - condition: resource.?user.orValue("").startsWith("bad") + rule: + id: "banned regions" + description: > + determine whether the resource origin is in the banned + list. If the region is also in the permitted list, the + ban has no effect. + variables: + - name: "banned_regions" + expression: "{'us': false, 'ru': false, 'ir': false}" + match: + - condition: | + resource.origin in variables.banned_regions && + !(resource.origin in variables.permitted_regions) + output: "{'banned': 'restricted_region'}" + explanation: "'resource is in the banned region ' + resource.origin" + - output: "{'banned': 'bad_actor'}" + - condition: "!(resource.origin in variables.permitted_regions)" + output: "{'banned': 'unconfigured_region'}" diff --git a/policy/testdata/nested_rule3/tests.yaml b/policy/testdata/nested_rule3/tests.yaml new file mode 100644 index 00000000..8a25cfec --- /dev/null +++ b/policy/testdata/nested_rule3/tests.yaml @@ -0,0 +1,48 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +description: Nested rule conformance tests +section: + - name: "banned" + tests: + - name: "restricted_origin" + input: + resource: + value: + user: "bad-user" + origin: "ir" + output: "{'banned': 'restricted_region'}" + - name: "by_default" + input: + resource: + value: + user: "bad-user" + origin: "de" + output: "{'banned': 'bad_actor'}" + - name: "unconfigured_region" + input: + resource: + value: + user: "good-user" + origin: "de" + output: "{'banned': 'unconfigured_region'}" + - name: "permitted" + tests: + - name: "valid_origin" + input: + resource: + value: + user: "good-user" + origin: "uk" + output: "optional.none()"