Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Simple Policy Compiler #924

Merged
merged 10 commits into from
May 20, 2024
Prev Previous commit
Next Next commit
Updates based on review feedback
TristonianJones committed May 17, 2024
commit 14bd121a43111af0a1a236168ce37bdb520c0795
18 changes: 14 additions & 4 deletions policy/compiler.go
Original file line number Diff line number Diff line change
@@ -64,8 +64,8 @@ func Compile(env *cel.Env, p *Policy) (*cel.Ast, *cel.Issues) {
}
ruleRoot, _ := env.Compile("true")
opt := cel.NewStaticOptimizer(&ruleComposer{rule: rule})
ruleExprAST, iss := opt.Optimize(env, ruleRoot)
return ruleExprAST, iss.Append(iss)
ruleExprAST, optIss := opt.Optimize(env, ruleRoot)
return ruleExprAST, iss.Append(optIss)
}

func (c *compiler) compileRule(r *Rule, ruleEnv *cel.Env, iss *cel.Issues) (*compiledRule, *cel.Issues) {
@@ -75,7 +75,7 @@ func (c *compiler) compileRule(r *Rule, ruleEnv *cel.Env, iss *cel.Issues) (*com
exprSrc := c.relSource(v.Expression())
varAST, exprIss := ruleEnv.CompileSource(exprSrc)
if exprIss.Err() == nil {
ruleEnv, err = ruleEnv.Extend(cel.Variable(fmt.Sprintf("variables.%s", v.Name().Value), varAST.OutputType()))
ruleEnv, err = ruleEnv.Extend(cel.Variable(fmt.Sprintf("%s.%s", variablePrefix, v.Name().Value), varAST.OutputType()))
if err != nil {
iss.ReportErrorAtID(v.Expression().ID, "invalid variable declaration")
}
@@ -91,6 +91,9 @@ func (c *compiler) compileRule(r *Rule, ruleEnv *cel.Env, iss *cel.Issues) (*com
condSrc := c.relSource(m.Condition())
condAST, condIss := ruleEnv.CompileSource(condSrc)
iss = iss.Append(condIss)
// This case cannot happen when the Policy object is parsed from yaml, but could happen
// with a non-YAML generation of the Policy object.
// TODO: Test this case once there's an alternative method of constructing Policy objects
if m.HasOutput() && m.HasRule() {
iss.ReportErrorAtID(m.Condition().ID, "either output or rule may be set but not both")
TristonianJones marked this conversation as resolved.
Show resolved Hide resolved
continue
@@ -139,6 +142,8 @@ type ruleComposer struct {
// Optimize implements an AST optimizer for CEL which composes an expression graph into a single
// expression value.
func (opt *ruleComposer) 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, _ := optimizeRule(ctx, opt.rule)
ctx.UpdateExpr(a.Expr(), ruleExpr)
TristonianJones marked this conversation as resolved.
Show resolved Hide resolved
return ctx.NewAST(ruleExpr)
@@ -191,9 +196,14 @@ func optimizeRule(ctx *cel.OptimizerContext, r *compiledRule) (ast.Expr, bool) {
// Build up the bindings in reverse order, starting from root, all the way up to the outermost
// binding:
// currExpr = cel.bind(outerVar, outerExpr, currExpr)
inlined, bindMacro := ctx.NewBindMacro(matchExpr.ID(), fmt.Sprintf("variables.%s", v.name), varAST, matchExpr)
inlined, bindMacro := ctx.NewBindMacro(matchExpr.ID(), fmt.Sprintf("%s.%s", variablePrefix, v.name), varAST, matchExpr)
ctx.SetMacroCall(inlined.ID(), bindMacro)
matchExpr = inlined
}
return matchExpr, optionalResult
}

const (
// Consider making the variables namespace configurable.
variablePrefix = "variables"
)
25 changes: 17 additions & 8 deletions policy/parser.go
Original file line number Diff line number Diff line change
@@ -262,7 +262,7 @@ func (defaultTagVisitor) MatchTag(ctx ParserContext, id int64, fieldName string,
}

func (defaultTagVisitor) VariableTag(ctx ParserContext, id int64, fieldName string, node *yaml.Node, v *Variable) {
ctx.ReportErrorAtID(id, "unsupported match tag: %s", fieldName)
ctx.ReportErrorAtID(id, "unsupported variable tag: %s", fieldName)
}

// Parser parses policy files into a canonical Policy representation.
@@ -310,6 +310,7 @@ func (p *parserImpl) parseYaml(src *Source) *Policy {
p.iss.ReportErrorAtID(0, err.Error())
return nil
}
// Entry point always has a single Content node
return p.parsePolicy(p, docNode.Content[0])
TristonianJones marked this conversation as resolved.
Show resolved Hide resolved
}

@@ -426,7 +427,7 @@ func (p *parserImpl) CollectMetadata(node *yaml.Node) int64 {
func (p *parserImpl) parsePolicy(ctx ParserContext, node *yaml.Node) *Policy {
ctx.CollectMetadata(node)
policy, id := ctx.NewPolicy(node)
if p.assertYamlType(id, node, yamlMap) == nil {
if p.assertYamlType(id, node, yamlMap) == nil || !p.checkMapValid(ctx, id, node) {
return policy
}
for i := 0; i < len(node.Content); i += 2 {
TristonianJones marked this conversation as resolved.
Show resolved Hide resolved
@@ -448,7 +449,7 @@ func (p *parserImpl) parsePolicy(ctx ParserContext, node *yaml.Node) *Policy {

func (p *parserImpl) parseRule(ctx ParserContext, node *yaml.Node) *Rule {
r, id := ctx.NewRule(node)
if p.assertYamlType(id, node, yamlMap) == nil {
if p.assertYamlType(id, node, yamlMap) == nil || !p.checkMapValid(ctx, id, node) {
return r
}
for i := 0; i < len(node.Content); i += 2 {
@@ -458,7 +459,7 @@ func (p *parserImpl) parseRule(ctx ParserContext, node *yaml.Node) *Rule {
val := node.Content[i+1]
if val.Style == yaml.FoldedStyle || val.Style == yaml.LiteralStyle {
val.Line++
val.Column = key.Column + 2
val.Column = key.Column + 1
}
switch fieldName {
case "id":
@@ -488,7 +489,7 @@ func (p *parserImpl) parseVariables(ctx ParserContext, r *Rule, node *yaml.Node)

func (p *parserImpl) parseVariable(ctx ParserContext, node *yaml.Node) *Variable {
v, id := ctx.NewVariable(node)
if p.assertYamlType(id, node, yamlMap) == nil {
if p.assertYamlType(id, node, yamlMap) == nil || !p.checkMapValid(ctx, id, node) {
return v
}
for i := 0; i < len(node.Content); i += 2 {
@@ -498,7 +499,7 @@ func (p *parserImpl) parseVariable(ctx ParserContext, node *yaml.Node) *Variable
val := node.Content[i+1]
if val.Style == yaml.FoldedStyle || val.Style == yaml.LiteralStyle {
val.Line++
val.Column = key.Column + 2
val.Column = key.Column + 1
}
switch fieldName {
case "name":
@@ -524,7 +525,7 @@ func (p *parserImpl) parseMatches(ctx ParserContext, r *Rule, node *yaml.Node) {

func (p *parserImpl) parseMatch(ctx ParserContext, node *yaml.Node) *Match {
m, id := ctx.NewMatch(node)
if p.assertYamlType(id, node, yamlMap) == nil {
if p.assertYamlType(id, node, yamlMap) == nil || !p.checkMapValid(ctx, id, node) {
return m
}
m.SetCondition(ValueString{ID: ctx.NextID(), Value: "true"})
@@ -535,7 +536,7 @@ func (p *parserImpl) parseMatch(ctx ParserContext, node *yaml.Node) *Match {
val := node.Content[i+1]
if val.Style == yaml.FoldedStyle || val.Style == yaml.LiteralStyle {
val.Line++
val.Column = key.Column + 2
val.Column = key.Column + 1
}
switch fieldName {
case "condition":
@@ -576,6 +577,14 @@ func (p *parserImpl) ReportErrorAtID(id int64, format string, args ...interface{
p.iss.ReportErrorAtID(id, format, args...)
}

func (p *parserImpl) checkMapValid(ctx ParserContext, id int64, node *yaml.Node) bool {
valid := len(node.Content)%2 == 0
if !valid {
ctx.ReportErrorAtID(id, "mismatched key-value pairs in map")
}
return valid
}

type yamlNodeType int

const (
2 changes: 1 addition & 1 deletion policy/parser_test.go
Original file line number Diff line number Diff line change
@@ -72,7 +72,7 @@ rule:
variables:
- name: "true"
alt_name: "bool_true"`,
err: `ERROR: <input>:5:7: unsupported match tag: alt_name
err: `ERROR: <input>:5:7: unsupported variable tag: alt_name
| alt_name: "bool_true"
| ......^`,
},