From b0c64a031c3ee714396a14c8c8af09df2c405550 Mon Sep 17 00:00:00 2001 From: "maksim.nabokikh" Date: Fri, 13 Mar 2026 18:08:00 +0100 Subject: [PATCH] Removed most of the code from DEP Signed-off-by: maksim.nabokikh --- .../cel-expressions-2026-02-28.md | 453 ++++-------------- 1 file changed, 85 insertions(+), 368 deletions(-) diff --git a/docs/enhancements/cel-expressions-2026-02-28.md b/docs/enhancements/cel-expressions-2026-02-28.md index 7415ae19..2fd9dd03 100644 --- a/docs/enhancements/cel-expressions-2026-02-28.md +++ b/docs/enhancements/cel-expressions-2026-02-28.md @@ -238,19 +238,18 @@ reusable CEL environment with Kubernetes-grade guarantees. pkg/ cel/ cel.go # Core Environment, compilation, evaluation - cel_test.go # Tests - library.go # Custom Dex CEL function library - library_test.go # Library tests types.go # CEL type declarations (Identity, Request, etc.) cost.go # Cost estimation and budgeting - cost_test.go # Cost estimation tests doc.go # Package documentation + library/ + email.go # Email-related CEL functions + groups.go # Group-related CEL functions ``` #### Dependencies ``` -github.com/google/cel-go v0.24+ +github.com/google/cel-go v0.27.0 ``` The `cel-go` library is the canonical Go implementation maintained by Google, used by Kubernetes @@ -259,19 +258,9 @@ compatibility guarantees. #### Core API Design -```go -package cel - -import ( - "context" - "fmt" - - "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" -) +**Public types:** +```go // CompilationResult holds a compiled CEL program ready for evaluation. type CompilationResult struct { Program cel.Program @@ -279,194 +268,52 @@ type CompilationResult struct { Expression string } -// Compiler compiles and caches CEL expressions against a specific environment. -type Compiler struct { - env *cel.Env -} +// Compiler compiles CEL expressions against a specific environment. +type Compiler struct { /* ... */ } // CompilerOption configures a Compiler. type CompilerOption func(*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 := []cel.EnvOption{ - cel.DefaultUTCTimeZone(true), - - // Standard extension libraries (same set as Kubernetes) - ext.Strings(), - ext.Encoders(), - ext.Lists(), - ext.Sets(), - ext.Math(), - - // Custom Dex library - cel.Lib(&dexLib{}), - - // Cost limit - cel.CostEstimatorOptions(checker.CostEstimatorOptions{ - SizeEstimateOptions: []checker.SizeEstimateOption{ - checker.PresenceTestHasCost(false), - }, - }), - } - - // Register declared variables - 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}, 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) -} +**Compilation pipeline:** -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(), - ) - } - - prog, err := c.env.Program(ast, - cel.EvalOptions(cel.OptOptimize), - cel.CostLimit(cfg.costBudget), - ) - if err != nil { - return nil, fmt.Errorf("CEL program creation failed: %w", err) - } - - return &CompilationResult{ - Program: prog, - OutputType: ast.OutputType(), - Expression: expression, - }, nil -} +Each `Compile*` call performs these steps sequentially: +1. Reject expressions exceeding `MaxExpressionLength` (10,240 chars). +2. Compile and type-check the expression via `cel-go`. +3. Validate output type matches the expected type (for typed variants). +4. Estimate cost using `defaultCostEstimator` with size hints — reject if estimated max cost + exceeds the cost budget. +5. Create an optimized `cel.Program` with runtime cost limit. -// 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 -} -``` +Presence tests (`has(field)`, `'key' in map`) have zero cost, matching Kubernetes CEL behavior. #### Variable Declarations -```go -package cel +Variables are declared via `VariableDeclaration{Name, Type}` and registered with `NewCompiler`. +All variables are typed as `map(string, dyn)` to allow flexible field access in expressions. +Helper constructors provide pre-defined variable sets: -// VariableDeclaration declares a named variable and its CEL type -// that will be available in expressions. -type VariableDeclaration struct { - Name string - Type *cel.Type -} +**`IdentityVariables()`** — the `identity` variable (from `connector.Identity`): -// IdentityVariables provides the 'identity' variable with user claims. -// -// identity.user_id — string -// identity.username — string -// identity.email — string -// identity.email_verified — bool -// identity.groups — list(string) -// identity.extra — map(string, dyn) -func IdentityVariables() []VariableDeclaration { - return []VariableDeclaration{ - {Name: "identity", Type: cel.MapType(cel.StringType, cel.DynType)}, - } -} +| Field | CEL Type | Source | +|-------|----------|--------| +| `identity.user_id` | `string` | `connector.Identity.UserID` | +| `identity.username` | `string` | `connector.Identity.Username` | +| `identity.preferred_username` | `string` | `connector.Identity.PreferredUsername` | +| `identity.email` | `string` | `connector.Identity.Email` | +| `identity.email_verified` | `bool` | `connector.Identity.EmailVerified` | +| `identity.groups` | `list(string)` | `connector.Identity.Groups` | -// RequestVariables provides the 'request' variable with request context. -// -// request.client_id — string -// request.connector_id — string -// request.scopes — list(string) -// request.redirect_uri — string -func RequestVariables() []VariableDeclaration { - return []VariableDeclaration{ - {Name: "request", Type: cel.MapType(cel.StringType, cel.DynType)}, - } -} +**`RequestVariables()`** — the `request` variable (from `RequestContext`): -// ClaimsVariable provides a 'claims' map for raw upstream claims. -// -// claims — map(string, dyn) -func ClaimsVariable() []VariableDeclaration { - return []VariableDeclaration{ - {Name: "claims", Type: cel.MapType(cel.StringType, cel.DynType)}, - } -} -``` +| Field | CEL Type | +|-------|----------| +| `request.client_id` | `string` | +| `request.connector_id` | `string` | +| `request.scopes` | `list(string)` | +| `request.redirect_uri` | `string` | + +**`ClaimsVariable()`** — the `claims` variable for raw upstream claims as `map(string, dyn)`. #### Compatibility Guarantees @@ -488,21 +335,12 @@ Following the Kubernetes CEL compatibility model ) // 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 - } - } + func WithVersion(v EnvironmentVersion) CompilerOption ``` - This is directly modeled on how Kubernetes versions CEL environments in - `k8s.io/apiserver/pkg/cel/environment` — each Kubernetes version introduces a new - environment version that may include new CEL libraries, while older expressions compiled - against an older version remain valid. + This is directly modeled on `k8s.io/apiserver/pkg/cel/environment`. -2. **Library stability** — Custom functions added via `library.go` follow these rules: +2. **Library stability** — Custom functions in the `pkg/cel/library` subpackage follow these rules: - Functions MUST NOT be removed once released. - Function signatures MUST NOT change once released. - New functions MUST be added under a new `EnvironmentVersion`. @@ -526,39 +364,29 @@ Following the Kubernetes CEL compatibility model #### Cost Estimation and Budgets -Like Kubernetes, Dex CEL expressions must be bounded to prevent denial-of-service: +Like Kubernetes, Dex CEL expressions must be bounded to prevent denial-of-service. -```go -package cel +**Constants:** -// DefaultCostBudget is the default cost budget for a single expression evaluation. -// Aligned with Kubernetes defaults: enough for typical identity operations -// but prevents runaway expressions. -const DefaultCostBudget = 10_000_000 +| Constant | Value | Description | +|----------|-------|-------------| +| `DefaultCostBudget` | `10_000_000` | Max cost units per evaluation (aligned with Kubernetes) | +| `MaxExpressionLength` | `10_240` | Max expression string length in characters | +| `DefaultStringMaxLength` | `256` | Estimated max string size for cost estimation | +| `DefaultListMaxLength` | `100` | Estimated max list size for cost estimation | -// MaxExpressionLength is the maximum length of a CEL expression string. -const MaxExpressionLength = 10_240 +**How it works:** -// EstimateCost returns the estimated cost range for a compiled expression. -// This is computed statically at compile time without evaluating the expression. -func EstimateCost(result *CompilationResult) (min, max uint64) { - // Uses cel-go's built-in cost estimator (checker.CostEstimator). - // ... -} +A `defaultCostEstimator` (implementing `checker.CostEstimator`) provides size hints for known +variables (`identity`, `request`, `claims`) so the `cel-go` cost estimator doesn't assume +unbounded sizes. It also provides call cost estimates for custom Dex functions +(`dex.emailDomain`, `dex.emailLocalPart`, `dex.groupMatches`, `dex.groupFilter`). -// WithCostBudget sets a custom cost budget for expression evaluation. -func WithCostBudget(budget uint64) CompilerOption { - return func(cfg *compilerConfig) { - cfg.costBudget = budget - } -} -``` - -Kubernetes uses `checker.CostEstimator` at admission time to reject CRDs with validation rules -that exceed cost limits. Dex will similarly validate expressions at config load time: -- Reject expressions that exceed `MaxExpressionLength`. -- Estimate cost at compile time and warn if estimated max cost exceeds `DefaultCostBudget`. -- Enforce runtime cost budget during evaluation and abort expressions that exceed the budget. +Expressions are validated at three levels: +1. **Length check** — reject expressions exceeding `MaxExpressionLength`. +2. **Compile-time cost estimation** — reject expressions whose estimated max cost exceeds + the cost budget. +3. **Runtime cost limit** — abort evaluation if actual cost exceeds the budget. #### Extension Libraries @@ -572,153 +400,42 @@ The `pkg/cel` environment includes these cel-go standard extensions (same set as | `ext.Sets()` | Set operations on lists | `sets.contains(a, b)`, `sets.intersects(a, b)`, `sets.equivalent(a, b)` | | `ext.Math()` | Math functions | `math.greatest(a, b)`, `math.least(a, b)` | -Plus a custom `dex` library with identity-specific helpers: +Plus custom Dex libraries in the `pkg/cel/library` subpackage, each implementing the +`cel.Library` interface: -```go -package cel - -// dexLib is the custom Dex CEL function library. -// All functions here are subject to the compatibility guarantees above. -type dexLib struct{} - -// CompileOptions returns the CEL environment options for the Dex library. -func (dexLib) CompileOptions() []cel.EnvOption { - return []cel.EnvOption{ - cel.Function("dex.emailDomain", - cel.Overload("dex_email_domain_string", - []*cel.Type{cel.StringType}, - cel.StringType, - cel.UnaryBinding(emailDomainImpl), - ), - ), - cel.Function("dex.emailLocalPart", - cel.Overload("dex_email_local_part_string", - []*cel.Type{cel.StringType}, - cel.StringType, - cel.UnaryBinding(emailLocalPartImpl), - ), - ), - cel.Function("dex.groupMatches", - cel.Overload("dex_group_matches_list_string", - []*cel.Type{cel.ListType(cel.StringType), cel.StringType}, - cel.ListType(cel.StringType), - cel.BinaryBinding(groupMatchesImpl), - ), - ), - } -} +**`library.Email`** — email-related helpers: -// ProgramOptions returns the CEL program options for the Dex library. -func (dexLib) ProgramOptions() []cel.ProgramOption { - return nil -} +| Function | Signature | Description | +|----------|-----------|-------------| +| `dex.emailDomain` | `(string) -> string` | Returns the domain portion of an email address. `dex.emailDomain("user@example.com") == "example.com"` | +| `dex.emailLocalPart` | `(string) -> string` | Returns the local part of an email address. `dex.emailLocalPart("user@example.com") == "user"` | -// Functions provided by dexLib (V1): -// -// dex.emailDomain(email: string) -> string -// Returns the domain portion of an email address. -// Example: dex.emailDomain("user@example.com") == "example.com" -// -// dex.emailLocalPart(email: string) -> string -// Returns the local part of an email address. -// Example: dex.emailLocalPart("user@example.com") == "user" -// -// dex.groupMatches(groups: list(string), pattern: string) -> list(string) -// Returns groups matching a glob pattern. -// Example: dex.groupMatches(identity.groups, "team:*") -``` +**`library.Groups`** — group-related helpers: -#### Activation Data Mapping - -Internal Go types are mapped to CEL variables before evaluation: - -```go -package cel - -import "github.com/dexidp/dex/connector" - -// IdentityFromConnector converts a connector.Identity to a CEL-compatible map. -func IdentityFromConnector(id connector.Identity) map[string]any { - return map[string]any{ - "user_id": id.UserID, - "username": id.Username, - "preferred_username": id.PreferredUsername, - "email": id.Email, - "email_verified": id.EmailVerified, - "groups": id.Groups, - } -} - -// RequestContext represents the authentication/token request context -// available as the 'request' variable in CEL expressions. -type RequestContext struct { - ClientID string - ConnectorID string - Scopes []string - RedirectURI string -} - -// RequestFromContext converts a RequestContext to a CEL-compatible map. -func RequestFromContext(rc RequestContext) map[string]any { - return map[string]any{ - "client_id": rc.ClientID, - "connector_id": rc.ConnectorID, - "scopes": rc.Scopes, - "redirect_uri": rc.RedirectURI, - } -} -``` +| Function | Signature | Description | +|----------|-----------|-------------| +| `dex.groupMatches` | `(list(string), string) -> list(string)` | Returns groups matching a glob pattern. `dex.groupMatches(identity.groups, "team:*")` | +| `dex.groupFilter` | `(list(string), list(string)) -> list(string)` | Returns only groups present in the allowed list. `dex.groupFilter(identity.groups, ["admin", "ops"])` | #### Example: Compile and Evaluate ```go -package main - -import ( - "context" - "fmt" +// 1. Create a compiler with identity and request variables +compiler, _ := cel.NewCompiler( + append(cel.IdentityVariables(), cel.RequestVariables()...), +) - "github.com/dexidp/dex/connector" - dexcel "github.com/dexidp/dex/pkg/cel" +// 2. Compile a policy expression (type-checked, cost-estimated) +prog, _ := compiler.CompileBool( + `identity.email.endsWith('@example.com') && 'admin' in identity.groups`, ) -func main() { - // Create a compiler with identity and request variables - compiler, err := dexcel.NewCompiler( - append(dexcel.IdentityVariables(), dexcel.RequestVariables()...), - ) - if err != nil { - panic(err) - } - - // Compile a policy expression - prog, err := compiler.CompileBool( - `identity.email.endsWith('@example.com') && 'admin' in identity.groups`, - ) - if err != nil { - panic(err) - } - - // Evaluate against real data - result, err := dexcel.EvalBool(context.Background(), prog, map[string]any{ - "identity": dexcel.IdentityFromConnector(connector.Identity{ - UserID: "123", - Username: "john", - Email: "john@example.com", - Groups: []string{"admin", "dev"}, - }), - "request": dexcel.RequestFromContext(dexcel.RequestContext{ - ClientID: "my-app", - ConnectorID: "okta", - Scopes: []string{"openid", "email"}, - }), - }) - if err != nil { - panic(err) - } - - fmt.Println(result) // true -} +// 3. Evaluate against real data +result, _ := cel.EvalBool(ctx, prog, map[string]any{ + "identity": cel.IdentityFromConnector(connectorIdentity), + "request": cel.RequestFromContext(cel.RequestContext{...}), +}) +// result == true ``` ### Phase 2: Authentication Policies