Skip to content

Commit c9164bc

Browse files
Variable and nested rule limits (#989)
* Variable and nested rule limits * Update the config version to be a string and address feedback
1 parent 5cbef66 commit c9164bc

File tree

10 files changed

+260
-30
lines changed

10 files changed

+260
-30
lines changed

policy/compiler.go

Lines changed: 62 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ type CompiledRule struct {
3333
matches []*CompiledMatch
3434
}
3535

36+
// SourceID returns the source metadata identifier associated with the compiled rule.
37+
func (r *CompiledRule) SourceID() int64 {
38+
return r.ID().ID
39+
}
40+
3641
// ID returns the expression id associated with the rule.
3742
func (r *CompiledRule) ID() *ValueString {
3843
return r.id
@@ -50,11 +55,17 @@ func (r *CompiledRule) Matches() []*CompiledMatch {
5055

5156
// CompiledVariable represents the variable name, expression, and associated type-check declaration.
5257
type CompiledVariable struct {
58+
id int64
5359
name string
5460
expr *cel.Ast
5561
varDecl *decls.VariableDecl
5662
}
5763

64+
// SourceID returns the source metadata identifier associated with the variable.
65+
func (v *CompiledVariable) SourceID() int64 {
66+
return v.id
67+
}
68+
5869
// Name returns the variable name.
5970
func (v *CompiledVariable) Name() string {
6071
return v.name
@@ -110,12 +121,28 @@ func (o *OutputValue) Expr() *cel.Ast {
110121
return o.expr
111122
}
112123

124+
// CompilerOption specifies a functional option to be applied to new RuleComposer instances.
125+
type CompilerOption func(*compiler) error
126+
127+
// MaxNestedExpressions limits the number of variable and nested rule expressions during compilation.
128+
//
129+
// Defaults to 100 if not set.
130+
func MaxNestedExpressions(limit int) CompilerOption {
131+
return func(c *compiler) error {
132+
if limit <= 0 {
133+
return fmt.Errorf("nested expression limit must be non-negative, non-zero value: %d", limit)
134+
}
135+
c.maxNestedExpressions = limit
136+
return nil
137+
}
138+
}
139+
113140
// Compile combines the policy compilation and composition steps into a single call.
114141
//
115142
// This generates a single CEL AST from a collection of policy expressions associated with a
116143
// CEL environment.
117-
func Compile(env *cel.Env, p *Policy) (*cel.Ast, *cel.Issues) {
118-
rule, iss := CompileRule(env, p)
144+
func Compile(env *cel.Env, p *Policy, opts ...CompilerOption) (*cel.Ast, *cel.Issues) {
145+
rule, iss := CompileRule(env, p, opts...)
119146
if iss.Err() != nil {
120147
return nil, iss
121148
}
@@ -126,14 +153,21 @@ func Compile(env *cel.Env, p *Policy) (*cel.Ast, *cel.Issues) {
126153
// CompileRule creates a compiled rules from the policy which contains a set of compiled variables and
127154
// match statements. The compiled rule defines an expression graph, which can be composed into a single
128155
// expression via the RuleComposer.Compose method.
129-
func CompileRule(env *cel.Env, p *Policy) (*CompiledRule, *cel.Issues) {
156+
func CompileRule(env *cel.Env, p *Policy, opts ...CompilerOption) (*CompiledRule, *cel.Issues) {
130157
c := &compiler{
131-
env: env,
132-
info: p.SourceInfo(),
133-
src: p.Source(),
158+
env: env,
159+
info: p.SourceInfo(),
160+
src: p.Source(),
161+
maxNestedExpressions: defaultMaxNestedExpressions,
134162
}
135163
errs := common.NewErrors(c.src)
136164
iss := cel.NewIssuesWithSourceInfo(errs, c.info)
165+
for _, o := range opts {
166+
if err := o(c); err != nil {
167+
iss.ReportErrorAtID(p.Name().ID, "error configuring compiler option: %s", err)
168+
return nil, iss
169+
}
170+
}
137171
rule, ruleIss := c.compileRule(p.Rule(), c.env, iss)
138172
iss = iss.Append(ruleIss)
139173
return rule, iss
@@ -143,6 +177,9 @@ type compiler struct {
143177
env *cel.Env
144178
info *ast.SourceInfo
145179
src *Source
180+
181+
maxNestedExpressions int
182+
nestedCount int
146183
}
147184

148185
func (c *compiler) compileRule(r *Rule, ruleEnv *cel.Env, iss *cel.Issues) (*CompiledRule, *cel.Issues) {
@@ -170,12 +207,22 @@ func (c *compiler) compileRule(r *Rule, ruleEnv *cel.Env, iss *cel.Issues) (*Com
170207
if err != nil {
171208
iss.ReportErrorAtID(v.Expression().ID, "invalid variable declaration")
172209
}
173-
compiledVars[i] = &CompiledVariable{
210+
compiledVar := &CompiledVariable{
211+
id: v.name.ID,
174212
name: v.name.Value,
175213
expr: varAST,
176214
varDecl: varDecl,
177215
}
216+
compiledVars[i] = compiledVar
217+
218+
// Increment the nesting count post-compile.
219+
c.nestedCount++
220+
if c.nestedCount == c.maxNestedExpressions+1 {
221+
iss.ReportErrorAtID(compiledVar.SourceID(), "variable exceeds nested expression limit")
222+
}
178223
}
224+
225+
// Compile the set of match conditions under the rule.
179226
compiledMatches := []*CompiledMatch{}
180227
for _, m := range r.Matches() {
181228
condSrc := c.relSource(m.Condition())
@@ -208,6 +255,12 @@ func (c *compiler) compileRule(r *Rule, ruleEnv *cel.Env, iss *cel.Issues) (*Com
208255
cond: condAST,
209256
nestedRule: nestedRule,
210257
})
258+
259+
// Increment the nesting count post-compile.
260+
c.nestedCount++
261+
if c.nestedCount == c.maxNestedExpressions+1 {
262+
iss.ReportErrorAtID(nestedRule.SourceID(), "rule exceeds nested expression limit")
263+
}
211264
}
212265
}
213266
return &CompiledRule{
@@ -232,4 +285,6 @@ func (c *compiler) relSource(pstr ValueString) *RelativeSource {
232285
const (
233286
// Consider making the variables namespace configurable.
234287
variablePrefix = "variables"
288+
289+
defaultMaxNestedExpressions = 100
235290
)

policy/compiler_test.go

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ func TestCompile(t *testing.T) {
3333

3434
func TestCompileError(t *testing.T) {
3535
for _, tst := range policyErrorTests {
36-
_, _, iss := compile(t, tst.name, []ParserOption{}, []cel.EnvOption{})
36+
_, _, iss := compile(t, tst.name, []ParserOption{}, []cel.EnvOption{}, tst.compilerOpts)
3737
if iss.Err() == nil {
3838
t.Fatalf("compile(%s) did not error, wanted %s", tst.name, tst.err)
3939
}
@@ -61,15 +61,16 @@ func newRunner(t testing.TB, name, expr string, parseOpts []ParserOption, opts .
6161
}
6262

6363
type runner struct {
64-
name string
65-
envOpts []cel.EnvOption
66-
parseOpts []ParserOption
67-
env *cel.Env
68-
expr string
69-
prg cel.Program
64+
name string
65+
envOpts []cel.EnvOption
66+
parseOpts []ParserOption
67+
compilerOpts []CompilerOption
68+
env *cel.Env
69+
expr string
70+
prg cel.Program
7071
}
7172

72-
func compile(t testing.TB, name string, parseOpts []ParserOption, envOpts []cel.EnvOption) (*cel.Env, *cel.Ast, *cel.Issues) {
73+
func compile(t testing.TB, name string, parseOpts []ParserOption, envOpts []cel.EnvOption, compilerOpts []CompilerOption) (*cel.Env, *cel.Ast, *cel.Issues) {
7374
config := readPolicyConfig(t, fmt.Sprintf("testdata/%s/config.yaml", name))
7475
srcFile := readPolicy(t, fmt.Sprintf("testdata/%s/policy.yaml", name))
7576
parser, err := NewParser(parseOpts...)
@@ -84,6 +85,7 @@ func compile(t testing.TB, name string, parseOpts []ParserOption, envOpts []cel.
8485
t.Errorf("policy name is %v, wanted %q", policy.name, name)
8586
}
8687
env, err := cel.NewEnv(
88+
cel.DefaultUTCTimeZone(true),
8789
cel.OptionalTypes(),
8890
cel.EnableMacroCallTracking(),
8991
cel.ExtendedValidations())
@@ -104,12 +106,12 @@ func compile(t testing.TB, name string, parseOpts []ParserOption, envOpts []cel.
104106
if err != nil {
105107
t.Fatalf("env.Extend() with config options %v, failed: %v", config, err)
106108
}
107-
ast, iss := Compile(env, policy)
109+
ast, iss := Compile(env, policy, compilerOpts...)
108110
return env, ast, iss
109111
}
110112

111113
func (r *runner) setup(t testing.TB) {
112-
env, ast, iss := compile(t, r.name, r.parseOpts, r.envOpts)
114+
env, ast, iss := compile(t, r.name, r.parseOpts, r.envOpts, r.compilerOpts)
113115
if iss.Err() != nil {
114116
t.Fatalf("Compile() failed: %v", iss.Err())
115117
}

policy/composer.go

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,10 @@ import (
2424
// NewRuleComposer creates a rule composer which stitches together rules within a policy into
2525
// a single CEL expression.
2626
func NewRuleComposer(env *cel.Env, p *Policy) *RuleComposer {
27-
return &RuleComposer{env: env, p: p}
27+
return &RuleComposer{
28+
env: env,
29+
p: p,
30+
}
2831
}
2932

3033
// RuleComposer optimizes a set of expressions into a single expression.
@@ -41,22 +44,26 @@ func (c *RuleComposer) Compose(r *CompiledRule) (*cel.Ast, *cel.Issues) {
4144
}
4245

4346
type ruleComposerImpl struct {
44-
rule *CompiledRule
47+
rule *CompiledRule
48+
maxNestedExpressionLimit int
4549
}
4650

4751
// Optimize implements an AST optimizer for CEL which composes an expression graph into a single
4852
// expression value.
4953
func (opt *ruleComposerImpl) Optimize(ctx *cel.OptimizerContext, a *ast.AST) *ast.AST {
5054
// The input to optimize is a dummy expression which is completely replaced according
5155
// to the configuration of the rule composition graph.
52-
ruleExpr, _ := optimizeRule(ctx, opt.rule)
56+
ruleExpr, _ := opt.optimizeRule(ctx, opt.rule)
5357
return ctx.NewAST(ruleExpr)
5458
}
5559

56-
func optimizeRule(ctx *cel.OptimizerContext, r *CompiledRule) (ast.Expr, bool) {
60+
func (opt *ruleComposerImpl) optimizeRule(ctx *cel.OptimizerContext, r *CompiledRule) (ast.Expr, bool) {
5761
matchExpr := ctx.NewCall("optional.none")
5862
matches := r.Matches()
63+
vars := r.Variables()
5964
optionalResult := true
65+
66+
// Build the rule subgraph.
6067
for i := len(matches) - 1; i >= 0; i-- {
6168
m := matches[i]
6269
cond := ctx.CopyASTAndMetadata(m.Condition().NativeRep())
@@ -78,7 +85,7 @@ func optimizeRule(ctx *cel.OptimizerContext, r *CompiledRule) (ast.Expr, bool) {
7885
matchExpr)
7986
continue
8087
}
81-
nestedRule, nestedOptional := optimizeRule(ctx, m.NestedRule())
88+
nestedRule, nestedOptional := opt.optimizeRule(ctx, m.NestedRule())
8289
if optionalResult && !nestedOptional {
8390
nestedRule = ctx.NewCall("optional.of", nestedRule)
8491
}
@@ -90,10 +97,19 @@ func optimizeRule(ctx *cel.OptimizerContext, r *CompiledRule) (ast.Expr, bool) {
9097
ctx.ReportErrorAtID(nestedRule.ID(), "subrule early terminates policy")
9198
continue
9299
}
93-
matchExpr = ctx.NewMemberCall("or", nestedRule, matchExpr)
100+
if triviallyTrue {
101+
matchExpr = ctx.NewMemberCall("or", nestedRule, matchExpr)
102+
} else {
103+
matchExpr = ctx.NewCall(
104+
operators.Conditional,
105+
cond,
106+
nestedRule,
107+
matchExpr,
108+
)
109+
}
94110
}
95111

96-
vars := r.Variables()
112+
// Bind variables in reverse order to declaration on top of rule-subgraph.
97113
for i := len(vars) - 1; i >= 0; i-- {
98114
v := vars[i]
99115
varAST := ctx.CopyASTAndMetadata(v.Expr().NativeRep())

policy/config.go

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ package policy
1616

1717
import (
1818
"fmt"
19+
"math"
20+
"strconv"
1921

2022
"github.com/google/cel-go/cel"
2123
"github.com/google/cel-go/ext"
@@ -74,7 +76,7 @@ type ExtensionResolver interface {
7476
// ExtensionConfig represents a YAML serializable definition of a versioned extension library reference.
7577
type ExtensionConfig struct {
7678
Name string `yaml:"name"`
77-
Version int `yaml:"version"`
79+
Version string `yaml:"version"`
7880
ExtensionResolver
7981
}
8082

@@ -87,7 +89,18 @@ func (ec *ExtensionConfig) AsEnvOption(baseEnv *cel.Env) (cel.EnvOption, error)
8789
if !found {
8890
return nil, fmt.Errorf("unrecognized extension: %s", ec.Name)
8991
}
90-
return fac(uint32(ec.Version)), nil
92+
// If the version is 'latest', set the version value to the max uint.
93+
if ec.Version == "latest" {
94+
return fac(math.MaxUint32), nil
95+
}
96+
if ec.Version == "" {
97+
return fac(0), nil
98+
}
99+
ver, err := strconv.ParseUint(ec.Version, 10, 32)
100+
if err != nil {
101+
return nil, fmt.Errorf("error parsing uint version: %w", err)
102+
}
103+
return fac(uint32(ver)), nil
91104
}
92105

93106
// VariableDecl represents a YAML serializable CEL variable declaration.

policy/conformance.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ type TestCase struct {
3636
Output string `yaml:"output"`
3737
}
3838

39+
// TestInput represents an input literal value or expression.
3940
type TestInput struct {
4041
Value any `yaml:"value"`
4142
Expr string `yaml:"expr"`

policy/helper_test.go

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,12 +112,33 @@ var (
112112
return types.String("ir")
113113
}
114114
}))),
115-
}},
115+
},
116+
},
117+
{
118+
name: "limits",
119+
expr: `
120+
cel.bind(variables.greeting, "hello",
121+
cel.bind(variables.farewell, "goodbye",
122+
cel.bind(variables.person, "me",
123+
cel.bind(variables.message_fmt, "%s, %s",
124+
(now.getHours() >= 20)
125+
? cel.bind(variables.message, variables.message_fmt.format([variables.farewell, variables.person]),
126+
(now.getHours() < 21)
127+
? optional.of(variables.message + "!")
128+
: ((now.getHours() < 22)
129+
? optional.of(variables.message + "!!")
130+
: ((now.getHours() < 24)
131+
? optional.of(variables.message + "!!!")
132+
: optional.none())))
133+
: optional.of(variables.message_fmt.format([variables.greeting, variables.person]))
134+
))))`,
135+
},
116136
}
117137

118138
policyErrorTests = []struct {
119-
name string
120-
err string
139+
name string
140+
err string
141+
compilerOpts []CompilerOption
121142
}{
122143
{
123144
name: "errors",
@@ -140,6 +161,21 @@ ERROR: testdata/errors/policy.yaml:34:67: undeclared reference to 'format' (in c
140161
| "invalid values provided on one or more labels: %s".format([variables.invalid])
141162
| ..................................................................^`,
142163
},
164+
{
165+
name: "limits",
166+
err: `ERROR: testdata/limits/policy.yaml:22:14: variable exceeds nested expression limit
167+
| - name: "person"
168+
| .............^`,
169+
compilerOpts: []CompilerOption{MaxNestedExpressions(2)},
170+
},
171+
172+
{
173+
name: "limits",
174+
err: `ERROR: testdata/limits/policy.yaml:30:14: rule exceeds nested expression limit
175+
| id: "farewells"
176+
| .............^`,
177+
compilerOpts: []CompilerOption{MaxNestedExpressions(5)},
178+
},
143179
}
144180
)
145181

policy/testdata/errors/config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
name: "labels"
15+
name: "errors"
1616
extensions:
1717
- name: "lists"
1818
- name: "sets"

0 commit comments

Comments
 (0)