OpenID Connect (OIDC) identity and OAuth 2.0 provider with pluggable connectors
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

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
}