CEL-Java: Common Expression Language for Java

repository·main·Indexed 42 days ago

https://github.com/cel-expr/cel-java

A Java implementation of the Common Expression Language (CEL), a non-Turing complete language for fast, safe, and portable expression evaluation. It provides a sandboxed scripting environment featuring a CelCompiler for parsing and type-checking, and a CelRuntime for evaluation. Supports custom function implementation, Protocol Buffer message integration, and commutative logical operators for robust error handling.

Tokens
20.6K
Snippets
55
Records
66
Agent score
55%

What's inside CEL-Java

  1. Overview of CEL Java Verifier

    main

    The CEL Java Verifier is a formal verification framework for CEL Java. It uses the Z3 SMT solver to translate CEL Abstract Syntax Trees (ASTs) into mathematical formulas. This allows you to statically prove semantic properties of expressions and policies without executing them.

    Common Use Cases

    • Compliance Auditing: Prove that critical resources are protected (e.g., ensuring access is only allowed for specific roles).
    • Optimization & Safe Refactoring: Verify that an optimized version of an expression behaves identically to the original for all possible inputs.
    • Dead Code Detection: Identify unreachable branches in an expression that are mathematically unsatisfiable.
  2. Understand the difference between Parsing and Checking

    main
    In CEL, Checking is an optional but strongly recommended step for expression validation. While you can simply Parse an expression and rely on runtime bindings and error handling during evaluation, Checking provides a more robust validation layer to ensure the expression is semantically correct before execution.
  3. Configure the CEL environment

    main

    You can configure the entire CEL stack at once using CelFactory.standardCelBuilder(), or compose it by configuring CelCompilerFactory and CelRuntimeFactory separately. This is useful when you want to declare specific types, macros, and variables for compilation while keeping the runtime separate.

    // Simplest form
    Cel cel = CelFactory.standardCelBuilder().build();
    
    // Detailed configuration for a compiler
    CelCompiler cel = CelCompilerFactory.standardCelCompilerBuilder()
        .setStandardMacros(CelStandardMacro.HAS)
        .setContainer("google.rpc.context.AttributeContext")
        .addMessageTypes(AttributeContext.getDescriptor())
        .addVar("resource",
            StructTypeReference.create("google.rpc.context.AttributeContext.Resource"))
        .addVar("group", SimpleType.STRING)
        .build();
  4. Key Features of CEL Java Verifier

    main

    The verifier provides several advanced formal verification capabilities:

    • Satisfiability & Validity Proving: Check if an expression can evaluate to true (isSatisfiable) or if it is guaranteed to always be true (isAlwaysTrue).
    • Logical Equivalence: Prove that two different ASTs or Policies are semantically identical.
    • Bounded Model Checking (BMC): Verifies list and map comprehensions (all, exists, map, filter) by unrolling them up to a configurable limit.
    • Counterexample & Witness Generation: Generates human-readable counterexamples when validity or equivalence checks fail. For satisfiability checks, it produces a 'witness' (concrete variable assignments) that makes the expression true.
    • Custom Invariants Verification: Allows you to define safety invariants and prove they are never violated across all possible input states.
    • Partial Evaluation (Unknowns) Support: Allows defining variables that can evaluate to Unknown, mirroring CEL's runtime partial evaluation behavior.
  5. How to navigate a compiled AST using CelNavigableAst

    main

    CEL provides a fluent API to traverse a compiled AST via CelNavigableAst. This is essential for writing custom validators that need to inspect specific parts of an expression.

    Key Navigation Methods

    • .getRoot(): Starts navigation from the root of the AST.
    • .allNodes(): Returns a stream of all nodes in the tree.
    • .descendants(): Returns a stream of all descendant nodes of a specific node.
    • .filter(Predicate): Filters nodes based on criteria (e.g., node.getKind().equals(Kind.STRUCT)).
    • .map(Function): Transforms nodes into other types (e.g., mapping a node to its underlying CelStruct).
    • .anyMatch(Predicate): Checks if any descendant matches a condition (useful for finding specific function calls within a macro).

    Pro-tips

    • Kind Checking: Use node.getKind() to identify the type of node (e.g., Kind.STRUCT, Kind.COMPREHENSION).
    • Convenience Methods: Use (kind)OrDefault methods like callOrDefault() to consolidate checking for an expression kind and retrieving the underlying expression in one step.
    • Human-Readable AST: Call .toString() on a CelExpr object to get a human-readable representation of the AST for debugging.
  6. How CEL's commutative logical operators work

    main

    CEL features unique commutative logical operators (|| and &&) designed for robustness. The engine attempts to find an evaluation order that yields a result even if some branches contain errors (like division by zero) or missing data.

    Short-circuiting and Error Handling

    • Success via Short-circuiting: If a logical operation can determine its result without evaluating an erroneous branch, it will return the logical result instead of propagating the error. For example, true || (1 / 0 > 2) returns true because the first operand satisfies the OR condition.
    • Error Propagation: An exception (specifically a CelEvaluationException wrapped in an IllegalArgumentException) is only surfaced if all branches of the logical operator resolve to an error. For example, false || (1 / 0 > 2) will throw an error because the second branch cannot be resolved.

    This behavior is particularly useful for systems like IAM (Identity and Access Management), where it allows the engine to minimize the cost of evaluation by deferring or skipping expensive or failing inputs if a result is already reachable.

    ```java
    // Example of short-circuiting an error
    // Returns true, does NOT throw division by zero error
    Object result = exercise3.compileAndEvaluate("true || (1 / 0 > 2)");
    
    // Example of error propagation
    // Throws IllegalArgumentException (caused by CelEvaluationException: / by zero)
    Object errorResult = exercise3.compileAndEvaluate("false || (1 / 0 > 2)");
    ```埋
  7. How CEL container namespace resolution works

    main

    A CEL container acts as a namespace or package for resolving identifiers (variables, types, and functions). When the compiler or evaluator looks up an identifier within a container (e.g., google.rpc.context.AttributeContext), it searches using the following hierarchy:

    1. google.rpc.context.AttributeContext.<id>
    2. google.rpc.context.<id>
    3. google.rpc.<id>
    4. google.<id>
    5. <id> (top-level)

    To perform an absolute lookup that bypasses this hierarchy and only searches the top-level, prefix the identifier with a leading dot (e.g., .<id>).

  8. How CEL-Java compilation and evaluation environments work

    main

    CEL applications evaluate expressions against an Environment. The standard environment supports all types, operators, functions, and macros defined in the CEL specification.

    Environments are customized using CelOptions and are constructed via factories:

    1. CelCompiler: Used to parse and type-check expressions. It is immutable and thread-safe, making it suitable for static initialization and caching.
    2. CelRuntime: Takes a compiled AST and produces an evaluable instance. Like the compiler, it is immutable and can be cached.

    Customization (such as disabling macros or declaring custom variables) is handled through fluent APIs on the builders provided by CelCompilerFactory and CelRuntimeFactory.

    // Setup an environment for expression compilation and evaluation.
    
    // Breaking behavior changes and optional features are controlled by CelOptions
    private static final CelOptions CEL_OPTIONS = CelOptions.current().build();
    
    // CelCompiler is used to parse and type-check expressions.
    private static final CelCompiler CEL_COMPILER = 
            CelCompilerFactory.standardCelCompilerBuilder()
                 .setOptions(CEL_OPTIONS)
                 .build();
    
    // CelRuntime takes in a compiled expression and produces an evaluable instance.
    private static final CelRuntime CEL_RUNTIME = 
            CelRuntimeFactory.plannerRuntimeBuilder()
                 .setOptions(CEL_OPTIONS)
                 .build();
  9. Use Two-Variable Comprehensions in CEL

    main

    The TwoVarComprehensions extension introduces support for comprehensions that use two variables (typically an index/key and a value). This allows for more complex transformations and predicates on lists and maps.

    all

    Tests if all elements satisfy a predicate. Short-circuits on false.

    • <list>.all(indexVar, valueVar, <predicate>) -> bool
    • <map>.all(keyVar, valueVar, <predicate>) -> bool

    exists

    Tests if any element satisfies a predicate. Short-circuits on true.

    • <list>.exists(indexVar, valueVar, <predicate>) -> bool
    • <map>.exists(keyVar, valueVar, <predicate>) -> bool

    exists_one

    Tests if exactly one element satisfies a predicate. Does not short-circuit.

    • <list>.existsOne(indexVar, valueVar, <predicate>)
    • <map>.existsOne(keyVar, valueVar, <predicate>)

    transformList

    Converts a list or map into a new list. Supports optional filtering.

    • <list>.transformList(indexVar, valueVar, <transform>)
    • <list>.transformList(indexVar, valueVar, <filter>, <transform>)
    • <map>.transformList(keyVar, valueVar, <transform>)
    • <map>.transformList(keyVar, valueVar, <filter>, <transform>)

    transformMap

    Converts a list or map into a new map. The key remains fixed from the source, while the value is transformed. Supports optional filtering.

    • <list>.transformMap(indexVar, valueVar, <transform>)
    • <list>.transformMap(indexVar, valueVar, <filter>, <transform>)
    • <map>.transformMap(keyVar, valueVar, <transform>)
    • <map>.transformMap(keyVar, valueVar, <filter>, <transform>)

    transformMapEntry

    Converts a list or map into a new map where the transform expression must be a map literal (e.g., {key: value}). If the transform produces duplicate keys, it will error.

    • <list>.transformMapEntry(indexVar, valueVar, <transform>)
    • <list>.transformMapEntry(indexVar, valueVar, <filter>, <transform>)
    • <map>.transformMapEntry(keyVar, valueVar, <transform>)
    • <map>.transformMapEntry(keyVar, valueVar, <filter>, <transform>)
    // all example
    [1, 2, 3].all(i, j, i < j) // true
    
    // transformList example
    [1, 2, 3].transformList(i, v, (i * v) + v) // [1, 4, 9]
    
    // transformMapEntry example
    {'greeting': 'hello'}.transformMapEntry(k, v, {v: k}) // {'hello': 'greeting'}
  10. Understand CEL Partial State semantics

    main

    CEL supports robust evaluation against partial or dynamic data (like JSON) using commutative logical operators && and ||. If an error or unknown value is encountered on one side of a logical operator, CEL may evaluate the other side to determine the result, aligning with SQL semantics.

    Truth Table for Error/Unknown values (<x>):

    ExpressionResult
    false && ?false
    true && falsefalse
    <x> && falsefalse
    true && truetrue
    true && <x><x>
    <x> && true<x>
    <x> && <y><x, y>
    true || ?true
    false || truetrue
    <x> || truetrue
    false || falsefalse
    false || <x><x>
    <x> || false<x>
    <x> || <y><x, y>
  11. How macros work in CEL

    main

    Macros are used to manipulate the CEL program at parse time. They match a specific call signature and transform the input call and its arguments into a new subexpression AST. This allows for complex logic that cannot be expressed through standard CEL syntax, such as field presence testing or bounded iteration.

    Comprehension macros like exists, all, filter, and map perform an AST rewrite that implements a for-each iteration bounded by the size of the input. This ensures CEL programs remain non-Turing-complete. However, because these evaluate in super-linear time, they should be used sparingly. If you find yourself using heavy comprehensions, consider implementing a custom function instead for better performance and user experience.

    // Example of a complex expression using macros
    String expression = "jwt.extra_claims.exists(c, c.startsWith('group')) && ...";
  12. How `cel.bind` works

    main

    The cel.bind function is a linguistic construct used to bind a simple identifier to an initialization expression. This identifier can then be used in a subsequent result expression. Bindings can be nested to create local scopes.

    Syntax: cel.bind(<varName>, <initExpr>, <resultExpr>)

    Important Note: Local bindings are not guaranteed to be evaluated before use.

    Examples:

    // Nested bindings
    cel.bind(a, 'hello', 
        cel.bind(b, 'world', a + b + b + a)) // "helloworldworldhello"
    
    // Using bindings to avoid repeated allocations in comprehensions
    cel.bind(valid_values, [a, b, c], 
        [d, e, f].exists(elem, elem in valid_values))
    
    // Binding a sum for conditional checks
    cel.bind(sum, x + y + z, sum < 10 || sum > 20)
    cel.bind(a, 'hello', 
        cel.bind(b, 'world', a + b + b + a)) // "helloworldworldhello"