Common Expression Language (CEL)

Common Expression Language (CEL) is a general-purpose expression language designed to be fast, portable, and safe to execute. You can use CEL on its own or embed it into a larger product. CEL is a great fit for a wide variety applications, from routing remote procedure calls (RPCs) to defining security policies. CEL is extensible, platform independent, and optimized for compile-once/evaluate-many workflows.

CEL was designed specifically to be safe for executing user code. While it's dangerous to blindly call eval() on a user's python code, you can safely execute a user's CEL code. And because CEL prevents behavior that would make it less performant, it evaluates safely in nanoseconds or microseconds. The speed and safety of CEL makes it ideal for performance-critical applications.

CEL evaluates expressions that are similar to single-line functions or lambda expressions. While CEL is commonly used for boolean decisions, you can also use it to construct more complex objects like JSON or protocol buffer messages.

Why CEL?

Many services and applications evaluate declarative configurations. For example, role-based access control (RBAC) is a declarative configuration that produces an access decision given a user role and a set of users. While declarative configs are sufficient for most cases, sometimes you need more expressive power. That's where CEL comes in.

As an example of extending a declarative config with CEL, consider the capabilities of Google Cloud Identity and Access Management (IAM). While RBAC is the common case, IAM offers CEL expressions to allow users to further constrain the scope of the role-based grant according to the proto message properties of the request or the resources being accessed. Describing such conditions through the data model would result in a complicated API surface that's difficult to work with. Instead, using CEL with attribute-based access control (ABAC) is an expressive and powerful extension to RBAC.

Core concepts of CEL

In CEL, an expression is compiled against an environment. The compilation step produces an abstract syntax tree (AST) in protocol buffer format. Compiled expressions are stored for future use to keep the evaluation as fast as possible. A single compiled expression can be evaluated with many different inputs.

Here's a closer look at some of these concepts.

Expressions

Expressions are written by users. Expressions are similar to single-line function bodies or lambda expressions. The function signature that declares the input is written outside of the CEL expression, and the library of functions available to CEL is auto-imported.

For example, the following CEL expression takes a request object, and the request includes a claims token. The expression returns a boolean value that indicates whether the claims token is still valid.

Example CEL expression to authenticate a claims token

// Check whether a JSON Web Token has expired by inspecting the 'exp' claim.
//
// Args:
//   claims - authentication claims.
//   now    - timestamp indicating the current system time.
// Returns: true if the token has expired.
//
timestamp(claims["exp"]) < now

While users define the CEL expression, services and applications define the environment where it runs.

Environments

Environments are defined by services. Services and applications that embed CEL declare the expression environment. The environment is the collection of variables and functions that can be used in CEL expressions.

For example, the following textproto code declares an environment containing the request and now variables using the CompileRequest message from a CEL service.

Example CEL environment declaration

# Format: $SOURCE_PATH/service.proto#CompileRequest
declarations {
  name: "request"
  ident {
    type { message_type: "google.rpc.context.AttributeContext.Request" }
  }
}
declarations {
  name: "now"
  ident {
    type { well_known: "TIMESTAMP" }
  }
}

The proto-based declarations are used by the CEL type-checker to ensure that all identifier and function references within an expression are declared and used correctly.

Phases of expression processing

CEL expressions are processed in three phases:

  1. Parse
  2. Check
  3. Evaluate

The most common pattern of CEL usage is to parse and check expressions at config time, store the AST, and then retrieve and evaluate the AST repeatedly at runtime.

Illustration of CEL processing phases

Expressions are parsed and checked on configuration paths, stored, and then
evaluated against one or more contexts on read paths.

CEL is parsed from a human-readable expression to an AST using an ANTLR lexer and parser grammar. The parse phase emits a proto-based AST where each Expr node in the AST contains an integer ID that's used to index into metadata generated during parsing and checking. The syntax.proto file that's produced during parsing represents the abstract representation of what was typed in the string form of the expression.

After an expression is parsed, it's then type-checked against the environment to ensure all variable and function identifiers in the expression have been declared and are being used correctly. The type-checker produces a checked.proto file that includes type, variable, and function resolution metadata that can drastically improve evaluation efficiency.

Finally, after an expression is parsed and checked, the stored AST is evaluated.

The CEL evaluator needs three things:

  • Function bindings for any custom extensions
  • Variable bindings
  • An AST to evaluate

The function and variable bindings should match what was used to compile the AST. Any of these inputs can be reused across multiple evaluations, such as an AST being evaluated across many sets of variable bindings, the same variables used against many ASTs, or the function bindings used across the lifetime of a process (a common case).