mirror of https://github.com/dexidp/dex.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
233 lines
6.5 KiB
233 lines
6.5 KiB
|
4 days ago
|
package cel
|
||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"fmt"
|
||
|
|
"reflect"
|
||
|
|
|
||
|
|
"github.com/google/cel-go/cel"
|
||
|
|
"github.com/google/cel-go/checker"
|
||
|
|
"github.com/google/cel-go/common/types/ref"
|
||
|
|
"github.com/google/cel-go/ext"
|
||
|
|
|
||
|
|
"github.com/dexidp/dex/pkg/cel/library"
|
||
|
|
)
|
||
|
|
|
||
|
|
// EnvironmentVersion represents the version of the CEL environment.
|
||
|
|
// New variables, functions, or libraries are introduced in new versions.
|
||
|
|
type EnvironmentVersion uint32
|
||
|
|
|
||
|
|
const (
|
||
|
|
// EnvironmentV1 is the initial CEL environment.
|
||
|
|
EnvironmentV1 EnvironmentVersion = 1
|
||
|
|
)
|
||
|
|
|
||
|
|
// CompilationResult holds a compiled CEL program ready for evaluation.
|
||
|
|
type CompilationResult struct {
|
||
|
|
Program cel.Program
|
||
|
|
OutputType *cel.Type
|
||
|
|
Expression string
|
||
|
|
|
||
|
|
ast *cel.Ast
|
||
|
|
}
|
||
|
|
|
||
|
|
// CompilerOption configures a Compiler.
|
||
|
|
type CompilerOption func(*compilerConfig)
|
||
|
|
|
||
|
|
type compilerConfig struct {
|
||
|
|
costBudget uint64
|
||
|
|
version EnvironmentVersion
|
||
|
|
}
|
||
|
|
|
||
|
|
func defaultCompilerConfig() *compilerConfig {
|
||
|
|
return &compilerConfig{
|
||
|
|
costBudget: DefaultCostBudget,
|
||
|
|
version: EnvironmentV1,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// WithCostBudget sets a custom cost budget for expression evaluation.
|
||
|
|
func WithCostBudget(budget uint64) CompilerOption {
|
||
|
|
return func(cfg *compilerConfig) {
|
||
|
|
cfg.costBudget = budget
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// WithVersion sets the target environment version for the compiler.
|
||
|
|
// Defaults to the latest version. Specifying an older version ensures
|
||
|
|
// that only functions/types available at that version are used.
|
||
|
|
func WithVersion(v EnvironmentVersion) CompilerOption {
|
||
|
|
return func(cfg *compilerConfig) {
|
||
|
|
cfg.version = v
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Compiler compiles CEL expressions against a specific environment.
|
||
|
|
type Compiler struct {
|
||
|
|
env *cel.Env
|
||
|
|
cfg *compilerConfig
|
||
|
|
}
|
||
|
|
|
||
|
|
// NewCompiler creates a new CEL compiler with the specified variable
|
||
|
|
// declarations and options.
|
||
|
|
//
|
||
|
|
// All custom Dex libraries are automatically included.
|
||
|
|
// The environment is configured with cost limits and safe defaults.
|
||
|
|
func NewCompiler(variables []VariableDeclaration, opts ...CompilerOption) (*Compiler, error) {
|
||
|
|
cfg := defaultCompilerConfig()
|
||
|
|
for _, opt := range opts {
|
||
|
|
opt(cfg)
|
||
|
|
}
|
||
|
|
|
||
|
|
envOpts := make([]cel.EnvOption, 0, 8+len(variables))
|
||
|
|
envOpts = append(envOpts,
|
||
|
|
cel.DefaultUTCTimeZone(true),
|
||
|
|
|
||
|
|
// Standard extension libraries (same set as Kubernetes)
|
||
|
|
ext.Strings(),
|
||
|
|
ext.Encoders(),
|
||
|
|
ext.Lists(),
|
||
|
|
ext.Sets(),
|
||
|
|
ext.Math(),
|
||
|
|
|
||
|
|
// Native Go types for typed variable access.
|
||
|
|
// This gives compile-time field checking: identity.emial → error at config load.
|
||
|
|
ext.NativeTypes(
|
||
|
|
ext.ParseStructTags(true),
|
||
|
|
reflect.TypeOf(IdentityVal{}),
|
||
|
|
reflect.TypeOf(RequestVal{}),
|
||
|
|
),
|
||
|
|
|
||
|
|
// Custom Dex libraries
|
||
|
|
cel.Lib(&library.Email{}),
|
||
|
|
cel.Lib(&library.Groups{}),
|
||
|
|
|
||
|
|
// Presence tests like has(field) and 'key' in map are O(1) hash
|
||
|
|
// lookups on map(string, dyn) variables, so they should not count
|
||
|
|
// toward the cost budget. Without this, expressions with multiple
|
||
|
|
// 'in' checks (e.g. "'admin' in identity.groups") would accumulate
|
||
|
|
// inflated cost estimates. This matches Kubernetes CEL behavior
|
||
|
|
// where presence tests are free for CRD validation rules.
|
||
|
|
cel.CostEstimatorOptions(
|
||
|
|
checker.PresenceTestHasCost(false),
|
||
|
|
),
|
||
|
|
)
|
||
|
|
|
||
|
|
for _, v := range variables {
|
||
|
|
envOpts = append(envOpts, cel.Variable(v.Name, v.Type))
|
||
|
|
}
|
||
|
|
|
||
|
|
env, err := cel.NewEnv(envOpts...)
|
||
|
|
if err != nil {
|
||
|
|
return nil, fmt.Errorf("failed to create CEL environment: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
return &Compiler{env: env, cfg: cfg}, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// CompileBool compiles a CEL expression that must evaluate to bool.
|
||
|
|
func (c *Compiler) CompileBool(expression string) (*CompilationResult, error) {
|
||
|
|
return c.compile(expression, cel.BoolType)
|
||
|
|
}
|
||
|
|
|
||
|
|
// CompileString compiles a CEL expression that must evaluate to string.
|
||
|
|
func (c *Compiler) CompileString(expression string) (*CompilationResult, error) {
|
||
|
|
return c.compile(expression, cel.StringType)
|
||
|
|
}
|
||
|
|
|
||
|
|
// CompileStringList compiles a CEL expression that must evaluate to list(string).
|
||
|
|
func (c *Compiler) CompileStringList(expression string) (*CompilationResult, error) {
|
||
|
|
return c.compile(expression, cel.ListType(cel.StringType))
|
||
|
|
}
|
||
|
|
|
||
|
|
// Compile compiles a CEL expression with any output type.
|
||
|
|
func (c *Compiler) Compile(expression string) (*CompilationResult, error) {
|
||
|
|
return c.compile(expression, nil)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (c *Compiler) compile(expression string, expectedType *cel.Type) (*CompilationResult, error) {
|
||
|
|
if len(expression) > MaxExpressionLength {
|
||
|
|
return nil, fmt.Errorf("expression exceeds maximum length of %d characters", MaxExpressionLength)
|
||
|
|
}
|
||
|
|
|
||
|
|
ast, issues := c.env.Compile(expression)
|
||
|
|
if issues != nil && issues.Err() != nil {
|
||
|
|
return nil, fmt.Errorf("CEL compilation failed: %w", issues.Err())
|
||
|
|
}
|
||
|
|
|
||
|
|
if expectedType != nil && !ast.OutputType().IsEquivalentType(expectedType) {
|
||
|
|
return nil, fmt.Errorf(
|
||
|
|
"expected expression output type %s, got %s",
|
||
|
|
expectedType, ast.OutputType(),
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Estimate cost at compile time and reject expressions that are too expensive.
|
||
|
|
costEst, err := c.env.EstimateCost(ast, &defaultCostEstimator{})
|
||
|
|
if err != nil {
|
||
|
|
return nil, fmt.Errorf("CEL cost estimation failed: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
if costEst.Max > c.cfg.costBudget {
|
||
|
|
return nil, fmt.Errorf(
|
||
|
|
"CEL expression estimated cost %d exceeds budget %d",
|
||
|
|
costEst.Max, c.cfg.costBudget,
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
prog, err := c.env.Program(ast,
|
||
|
|
cel.EvalOptions(cel.OptOptimize),
|
||
|
|
cel.CostLimit(c.cfg.costBudget),
|
||
|
|
)
|
||
|
|
if err != nil {
|
||
|
|
return nil, fmt.Errorf("CEL program creation failed: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
return &CompilationResult{
|
||
|
|
Program: prog,
|
||
|
|
OutputType: ast.OutputType(),
|
||
|
|
Expression: expression,
|
||
|
|
ast: ast,
|
||
|
|
}, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// Eval evaluates a compiled program against the given variables.
|
||
|
|
func Eval(ctx context.Context, result *CompilationResult, variables map[string]any) (ref.Val, error) {
|
||
|
|
out, _, err := result.Program.ContextEval(ctx, variables)
|
||
|
|
if err != nil {
|
||
|
|
return nil, fmt.Errorf("CEL evaluation failed: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
return out, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// EvalBool is a convenience function that evaluates and asserts bool output.
|
||
|
|
func EvalBool(ctx context.Context, result *CompilationResult, variables map[string]any) (bool, error) {
|
||
|
|
out, err := Eval(ctx, result, variables)
|
||
|
|
if err != nil {
|
||
|
|
return false, err
|
||
|
|
}
|
||
|
|
|
||
|
|
v, ok := out.Value().(bool)
|
||
|
|
if !ok {
|
||
|
|
return false, fmt.Errorf("expected bool result, got %T", out.Value())
|
||
|
|
}
|
||
|
|
|
||
|
|
return v, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// EvalString is a convenience function that evaluates and asserts string output.
|
||
|
|
func EvalString(ctx context.Context, result *CompilationResult, variables map[string]any) (string, error) {
|
||
|
|
out, err := Eval(ctx, result, variables)
|
||
|
|
if err != nil {
|
||
|
|
return "", err
|
||
|
|
}
|
||
|
|
|
||
|
|
v, ok := out.Value().(string)
|
||
|
|
if !ok {
|
||
|
|
return "", fmt.Errorf("expected string result, got %T", out.Value())
|
||
|
|
}
|
||
|
|
|
||
|
|
return v, nil
|
||
|
|
}
|