Common Expression Language (CEL)

Common Expression Language (CEL) ist eine allgemeine Ausdruckssprache, die schnell, portabel und sicher ausgeführt werden kann. Sie können CEL allein verwenden oder in ein größeres Produkt einbetten. CEL eignet sich hervorragend für eine Vielzahl von Anwendungen, vom Routing von Remoteprozeduraufrufen (Remote Procedure Calls, RPCs) bis zum Definieren von Sicherheitsrichtlinien. CEL ist erweiterbar, plattformunabhängig und für Workflows optimiert, die einmalig kompiliert bzw. viele ausgewertet werden sollen.

CEL wurde speziell für die sichere Ausführung von Nutzercode entwickelt. Es ist zwar gefährlich, eval() für den Python-Code eines Nutzers blind aufzurufen, aber Sie können den CEL-Code eines Nutzers bedenkenlos ausführen. Da CEL Verhaltensweisen verhindert, die zu einer geringeren Leistung führen würden, wird es sicher in Nanosekunden oder Mikrosekunden bewertet. Aufgrund der Geschwindigkeit und Sicherheit ist CEL ideal für leistungskritische Anwendungen.

CEL wertet Ausdrücke aus, die einzeiligen Funktionen oder Lambda-Ausdrücken ähneln. CEL wird zwar häufig für boolesche Entscheidungen verwendet, Sie können es aber auch verwenden, um komplexere Objekte wie JSON- oder Protokollpuffernachrichten zu 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 eine Zugriffsentscheidung auf Basis einer Nutzerrolle und einer Gruppe von Nutzern erzeugt. Deklarative Konfigurationen sind in den meisten Fällen ausreichend, aber manchmal benötigen Sie auch Ausdrucksstärke. Hier kommt CEL ins Spiel.

Sehen Sie sich als Beispiel für die Erweiterung einer deklarativen Konfiguration mit CEL die Funktionen von Identity and Access Management (IAM) von Google Cloud an. Während RBAC der häufigste Fall ist, bietet IAM CEL-Ausdrücke, mit denen Nutzer den Umfang der rollenbasierten Berechtigung entsprechend den Proto-Nachrichtenattributen der Anfrage oder der Ressourcen, auf die zugegriffen wird, weiter einschränken können. Das Beschreiben solcher Bedingungen über das Datenmodell würde 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 leistungsstarke und leistungsstarke Erweiterung von RBAC.

Kernkonzepte von CEL

In CEL wird ein Ausdruck anhand einer Umgebung kompiliert. Der Kompilierungsschritt erzeugt einen abstrakten Syntaxbaum (AST) im Protokollpufferformat. Kompilierte Ausdrücke werden für eine zukünftige Verwendung gespeichert, um die Auswertung so schnell wie möglich zu halten. 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 sind ähnlich wie einzeilige Funktionskörper oder Lambda-Ausdrücke. Die Funktionssignatur, die die Eingabe deklariert, wird außerhalb des CEL-Ausdrucks geschrieben. Die Bibliothek der Funktionen, die für CEL zur Verfügung steht, 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.

CEL-Beispielausdruck zur Authentifizierung 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

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

Umgebungen

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

Mit dem folgenden textproto-Code wird beispielsweise eine Umgebung deklariert, die die Variablen request und now enthält. Dazu wird die Nachricht CompileRequest von einem CEL-Dienst verwendet.

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 proto-basierten Deklarationen werden von der CEL-Typprüfung verwendet, um sicherzustellen, dass alle Kennzeichnungs- und Funktionsverweise innerhalb eines Ausdrucks korrekt deklariert und verwendet werden.

Phasen der Ausdrucksverarbeitung

CEL-Ausdrücke werden in drei Phasen verarbeitet:

  1. Parsen
  2. Häkchen
  3. Bewerten

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

Illustration der CEL-Verarbeitungsphasen

Ausdrücke werden geparst und in Konfigurationspfaden geprüft, gespeichert und dann anhand eines oder mehrerer Kontexte in Lesepfaden ausgewertet.

CEL wird mithilfe eines ANTLR-Lexers und Parsers von einem menschenlesbaren Ausdruck zu einem AST geparst. Die Analysephase gibt einen proto-basierten AST aus, wobei jeder Expr-Knoten im AST eine Ganzzahl-ID enthält, die zur Indexierung in Metadaten verwendet wird, die während des Parsens und der Überprüfung generiert werden. Die syntax.proto-Datei, die während des Parsings erstellt wird, stellt die abstrakte Darstellung dessen, was in der Stringform des Ausdrucks eingegeben wurde, dar.

Nachdem ein Ausdruck geparst wurde, wird er auf die Umgebung geprüft, um sicherzustellen, dass alle Variablen- und Funktionskennungen im Ausdruck deklariert wurden und korrekt verwendet werden. Die Typprüfung erstellt eine checked.proto-Datei mit Metadaten zur Typ-, Variablen- und Funktionsauflösung, die die Auswertungseffizienz erheblich verbessern können.

Nachdem ein Ausdruck geparst und überprüft wurde, wird schließlich der gespeicherte AST ausgewertet.

Das CEL-Evaluierende benötigt drei Dinge:

  • Funktionsbindungen für alle benutzerdefinierten Erweiterungen
  • Variablenbindungen
  • Ein zu bewertender AST

Die Funktions- und Variablenbindungen sollten mit dem übereinstimmen, was zum Kompilieren des AST verwendet wurde. Jede dieser Eingaben kann für mehrere Bewertungen wiederverwendet werden, z. B. ein AST, der über viele Gruppen von Variablenbindungen ausgewertet wird, dieselben Variablen für viele ASTs oder die Funktionsbindungen, die während der gesamten Lebensdauer eines Prozesses verwendet werden (ein häufiger Fall).