Common Expression Language (CEL)

Common Expression Language (CEL) ist eine Allzweck-Ausdruckssprache, die schnell, portabel und sicher ausgeführt werden kann. Sie können CEL eigenständig verwenden oder in ein größeres Produkt einbetten. CEL eignet sich für eine Vielzahl von Anwendungen, vom Routing von Remote-Prozeduraufrufen (RPCs) bis hin zur Definition von Sicherheitsrichtlinien. CEL ist erweiterbar, plattformunabhängig und für Workflows optimiert, bei denen einmal kompiliert und dann mehrfach ausgewertet wird.

CEL wurde speziell für die sichere Ausführung von Nutzercode entwickelt. Es ist gefährlich, eval() blind in Python-Code eines Nutzers aufzurufen. CEL-Code eines Nutzers kann jedoch sicher ausgeführt werden. Da CEL Verhalten verhindert, das die Leistung beeinträchtigen würde, wird es sicher in Nanosekunden oder Mikrosekunden ausgewertet. Die Geschwindigkeit und Sicherheit von CEL machen es ideal für leistungsintensive Anwendungen.

CEL wertet Ausdrücke aus, die Singleline-Funktionen oder Lambda-Ausdrücken ähneln. CEL wird zwar häufig für boolesche Entscheidungen verwendet, Sie können damit aber auch komplexere Objekte wie JSON- oder Protokollzwischenspeicher-Nachrichten erstellen.

Warum CEL?

Viele Dienste und Anwendungen werten deklarative Konfigurationen aus. Die rollenbasierte Zugriffssteuerung (Role-based Access Control, RBAC) ist beispielsweise eine deklarative Konfiguration, die auf Grundlage einer Nutzerrolle und einer Gruppe von Nutzern eine Zugriffsentscheidung trifft. Deklarative Konfigurationen reichen für die meisten Fälle aus, manchmal benötigen Sie jedoch mehr Ausdruckskraft. Hier kommt CEL ins Spiel.

Ein Beispiel für die Erweiterung einer deklarativen Konfiguration mit CEL sind die Funktionen von Identity and Access Management (IAM) von Google Cloud. RBAC ist zwar der häufigste Fall, aber IAM bietet CEL-Ausdrücke, mit denen Nutzer den Umfang der rollenbasierten Zuweisung entsprechend den Protobuf-Nachrichteneigenschaften der Anfrage oder der Ressourcen, auf die zugegriffen wird, weiter einschränken können. Wenn solche Bedingungen über das Datenmodell beschrieben würden, würde dies zu einer komplizierten API-Oberfläche führen, mit der sich nur schwer arbeiten lässt. Stattdessen ist die Verwendung von CEL mit attributbasierter Zugriffssteuerung (Attribute-Based Access Control, ABAC) eine ausdrucksstarke und leistungsstarke Erweiterung von RBAC.

Wichtige Konzepte von CEL

In CEL wird ein Ausdruck für eine Umgebung kompiliert. Beim Kompilieren wird ein abstrakter Syntaxbaum (Abstract Syntax Tree, AST) im Protobuf-Format erstellt. Kompilierte Ausdrücke werden zur späteren Verwendung gespeichert, um die Auswertung so schnell wie möglich zu gestalten. Ein einzelner kompilierter Ausdruck kann mit vielen verschiedenen Eingaben ausgewertet werden.

Sehen wir uns einige dieser Konzepte genauer an.

Ausdrücke

Ausdrücke werden von Nutzern geschrieben. Ausdrücke ähneln einzeiligen Funktionskörpern oder Lambda-Ausdrücken. Die Funktionssignatur, mit der die Eingabe deklariert wird, wird außerhalb des CEL-Ausdrucks geschrieben und die für CEL verfügbare Funktionsbibliothek wird automatisch importiert.

Der folgende CEL-Ausdruck verwendet beispielsweise ein Anfrageobjekt und die Anfrage enthält ein claims-Token. Der Ausdruck gibt einen booleschen Wert zurück, der angibt, ob das claims-Token noch gültig ist.

Beispiel für einen CEL-Ausdruck zum Authentifizieren eines Anspruchstokens

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

Nutzer definieren den CEL-Ausdruck, Dienste und Anwendungen definieren die Umgebung, in der er ausgeführt wird.

Umgebungen

Umgebungen werden durch Dienste definiert. Dienste und Anwendungen, in die CEL eingebettet ist, deklarieren die Ausdrucksumgebung. Die Umgebung ist die Sammlung von Variablen und Funktionen, die in CEL-Ausdrücken verwendet werden können.

Im folgenden textproto-Code wird beispielsweise eine Umgebung mit den Variablen request und now mithilfe der CompileRequest-Nachricht aus einem CEL-Dienst deklariert.

Beispiel für eine CEL-Umgebungsdeklaration

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

Die protobasierten Deklarationen werden vom CEL-Typprüfer verwendet, um sicherzustellen, dass alle Bezeichner- und Funktionsreferenzen in einem Ausdruck deklariert und korrekt verwendet werden.

Phasen der Verarbeitung von Ausdrücken

CEL-Ausdrücke werden in drei Phasen verarbeitet:

  1. Parsen
  2. Scheck
  3. Bewerten

Das gängigste Muster für die CEL-Verwendung besteht darin, Ausdrücke zur Konfigurationszeit zu parsen und zu prüfen, den AST zu speichern und ihn dann zur Laufzeit wiederholt abzurufen und auszuwerten.

Abbildung der CEL-Verarbeitungsphasen

Ausdrücke werden auf Konfigurationspfaden geparst und geprüft, gespeichert und dann auf Lesepfaden anhand von einem oder mehreren Kontexten ausgewertet.

CEL wird mithilfe einer ANTLR-Lexer- und Parsergrammatik aus einem für Menschen lesbaren Ausdruck in einen AST geparst. In der Parse-Phase wird ein protobasierter AST ausgegeben, in dem jeder Expr-Knoten im AST eine Ganzzahl-ID enthält, die zum Indexieren von Metadaten verwendet wird, die während des Parsens und Prüfens generiert werden. Die Datei syntax.proto, die während des Parsens erstellt wird, stellt die abstrakte Darstellung des Ausdrucks dar, der in Stringform eingegeben wurde.

Nachdem ein Ausdruck geparst wurde, wird er anhand der Umgebung typgeprüft, um sicherzustellen, dass alle Variablen- und Funktionsbezeichner im Ausdruck deklariert wurden und korrekt verwendet werden. Der Type-Checker generiert eine checked.proto-Datei mit Metadaten zur Auflösung von Typen, Variablen und Funktionen, die die Effizienz der Auswertung erheblich verbessern können.

Nachdem ein Ausdruck geparst und geprüft wurde, wird der gespeicherte AST ausgewertet.

Der CEL-Evaluator benötigt drei Dinge:

  • Funktionsbindungen für benutzerdefinierte Erweiterungen
  • Variablenbindungen
  • Ein auszuwertender AST

Die Funktions- und Variablenbindungen sollten mit den Bindungen übereinstimmen, die zum Kompilieren des AST verwendet wurden. Alle diese Eingaben können für mehrere Auswertungen wiederverwendet werden, z. B. ein AST, das für viele Gruppen von Variablenbindungen ausgewertet wird, dieselben Variablen, die für viele ASTs verwendet werden, oder die Funktionsbindungen, die während der gesamten Lebensdauer eines Prozesses verwendet werden (ein häufiger Fall).