通用表达式语言 (CEL)

通用表达式语言 (CEL) 是一种通用表达式语言,旨在实现快速、可移植且安全的执行。您可以单独使用 CEL,也可以将其嵌入到更大的产品中。CEL 非常适合多种应用,包括路由远程过程调用 (RPC) 和定义安全政策。CEL 可扩展、独立于平台,并针对一次编译/评估多工作流进行了优化。

CEL 经过专门设计,可以安全地执行用户代码。虽然对用户的 Python 代码盲调用 eval() 很危险,但您可以安全地执行用户的 CEL 代码。由于 CEL 会阻止会降低性能的行为,因此它可以在纳秒或微秒内安全地进行评估。CEL 的速度和安全性使其成为对性能至关重要的应用的理想选择。

CEL 对与单行函数或 lambda 表达式类似的表达式求值。虽然 CEL 通常用于布尔决策,但您也可以用它来构建更复杂的对象,如 JSON 或协议缓冲区消息。

为何选择 CEL?

许多服务和应用都会评估声明式配置。例如,基于角色的访问权限控制 (RBAC) 是一种声明式配置,可针对一个用户角色和一组用户生成访问权限决策。虽然声明式配置在大多数情况下已经足够,但有时您需要更具表现力。这就是 CEL 的用武之地。

作为使用 CEL 扩展声明性配置的示例,请考虑 Google Cloud Identity and Access Management (IAM) 的功能。虽然 RBAC 很常见,但 IAM 提供 CEL 表达式,以便用户根据请求的 proto 消息属性或访问的资源,进一步限制基于角色的授权的范围。通过数据模型描述此类条件会导致复杂的 API Surface,难以使用。相反,将 CEL 与基于特性的访问权限控制 (ABAC) 搭配使用是对 RBAC 的表达力且强大的扩展。

CEL 的核心概念

在 CEL 中,表达式是针对环境编译的。编译步骤会生成协议缓冲区格式的抽象语法树 (AST)。系统会存储已编译的表达式,以供将来使用,以尽可能加快评估速度。一个编译后的表达式可以使用许多不同的输入进行求值。

下面详细介绍了其中一些概念。

表达式

表达式由用户编写。表达式类似于单行函数正文或 lambda 表达式。声明输入的函数签名在 CEL 表达式之外编写,并且可供 CEL 使用的函数库会自动导入。

例如,以下 CEL 表达式接受一个请求对象,并且该请求包含一个 claims 令牌。该表达式会返回一个布尔值,指示 claims 令牌是否仍然有效。

用于对声明令牌进行身份验证的 CEL 表达式示例

// 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

用户定义 CEL 表达式,而服务和应用则定义其运行的环境。

环境

环境由服务定义。嵌入 CEL 的服务和应用会声明表达式环境。环境是可在 CEL 表达式中使用的变量和函数的集合。

例如,以下 textproto 代码使用 CEL 服务中的 CompileRequest 消息声明包含 requestnow 变量的环境。

CEL 环境声明示例

# 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" }
  }
}

CEL 类型检查工具使用基于 proto 的声明,以确保正确声明和使用表达式中的所有标识符和函数引用。

表达式处理的阶段

CEL 表达式分三个阶段处理:

  1. Parse
  2. 检查
  3. 评估

最常见的 CEL 使用模式是在配置时解析和检查表达式,存储 AST,然后在运行时重复检索和评估 AST。

CEL 处理阶段图示

系统会在配置路径上解析和检查表达式,存储表达式,然后根据读取路径上的一个或多个上下文对表达式求值。

系统使用 ANTLR 词法分析器和解析器语法将 CEL 从人类可读的表达式解析为 AST。解析阶段会发出基于 proto 的 AST,其中 AST 中的每个 Expr 节点都包含一个整数 ID,该 ID 用于编入解析和检查期间生成的元数据的索引。解析期间生成的 syntax.proto 文件表示以表达式的字符串形式输入的内容的抽象表示。

解析表达式后,系统会对照环境对其进行类型检查,以确保表达式中的所有变量和函数标识符均已声明并得到正确使用。类型检查工具会生成一个 checked.proto 文件,其中包含类型、变量和函数解析元数据,可以大幅提高评估效率。

最后,在解析和检查某个表达式后,系统会对存储的 AST 进行求值。

CEL 评估器需要三项:

  • 任何自定义扩展程序的函数绑定
  • 变量绑定
  • 要评估的 AST

函数和变量绑定应与用于编译 AST 的绑定一致。其中任何输入都可以在多个评估中重复使用,例如在多组变量绑定中评估某个 AST、针对多个 AST 使用的相同变量,或者在整个进程的整个生命周期内使用的函数绑定(常见情况)。