Common Expression Language (CEL) ist eine Ausdruckssprache für allgemeine Zwecke, die schnell, portierbar 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, von der Weiterleitung von Remoteprozeduraufrufen (Remote Procedure Calls, RPCs) bis hin zur Definition von Sicherheitsrichtlinien. CEL ist erweiterbar, plattformunabhängig, formal überprüfbar und für Workflows optimiert, bei denen einmal kompiliert und mehrmals ausgewertet wird.
CEL wurde speziell für die sichere Ausführung von Nutzercode entwickelt. Es ist gefährlich, eval() blind auf den Python-Code eines Nutzers anzuwenden. Sie können den CEL-Code eines Nutzers jedoch sicher ausführen. Da CEL Verhaltensweisen verhindert, die die Leistung beeinträchtigen würden, erfolgt die Auswertung sicher in Nanosekunden oder Mikrosekunden. Aufgrund der Geschwindigkeit und Sicherheit von CEL ist es ideal für leistungskritische Anwendungen.
CEL wertet Ausdrücke aus, die Funktionen mit einer Zeile oder Lambda-Ausdrücken ähneln. CEL wird häufig für boolesche Entscheidungen verwendet, kann aber auch zum Erstellen komplexerer Objekte wie JSON- oder Protokollpuffer-Nachrichten verwendet werden.
Warum CEL?
Viele Dienste und Anwendungen werten deklarative Konfigurationen aus. Die rollenbasierte Zugriffssteuerung (Role-Based Access Control, RBAC) ist beispielsweise eine deklarative Konfiguration, die basierend auf einer Nutzerrolle und einer Gruppe von Nutzern eine Zugriffsentscheidung trifft. Deklarative Konfigurationen reichen in den meisten Fällen aus, aber manchmal benötigen Sie mehr Ausdruckskraft. Hier kommt CEL ins Spiel.
Ein Beispiel für die Erweiterung einer deklarativen Konfiguration mit CEL sind die Funktionen von Google Cloud Identity and Access Management (IAM). RBAC ist der häufigste Fall, aber IAM bietet CEL-Ausdrücke, mit denen Nutzer den Umfang der rollenbasierten Berechtigung weiter einschränken können. Dies erfolgt anhand der Proto-Nachrichteneigenschaften der Anfrage oder der Ressourcen, auf die zugegriffen wird. Wenn Sie solche Bedingungen über das Datenmodell beschreiben, entsteht eine komplizierte API-Oberfläche, mit der sich nur schwer arbeiten lässt. Stattdessen ist die Verwendung von CEL mit der attributbasierten Zugriffssteuerung (Attribute-Based Access Control, ABAC) eine ausdrucksstarke und leistungsstarke Erweiterung von RBAC.
Grundlegende Konzepte von CEL
In CEL wird ein Ausdruck für eine Umgebung kompiliert. Im Kompilierungsschritt wird ein abstrakter Syntaxbaum (Abstract Syntax Tree, AST) im Protokollpufferformat erstellt. Kompilierte Ausdrücke werden für die zukünftige 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 Funktionen mit einer Zeile oder Lambda-Ausdrücken. Die Funktionssignatur, die die Eingabe deklariert, wird außerhalb des CEL-Ausdrucks geschrieben und die Bibliothek der für CEL verfügbaren Funktionen wird automatisch importiert.
Der folgende CEL-Ausdruck verwendet beispielsweise ein Anfrageobjekt, das ein claims-Token enthält. Der Ausdruck gibt einen booleschen Wert zurück, der angibt, ob das claims-Token noch gültig ist.
Beispiel für einen CEL-Ausdruck zur Authentifizierung eines „claims“-Tokens
// 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 von Diensten 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.
Der folgende textproto-Code deklariert beispielsweise eine Umgebung, die die Variablen request und now mit der Nachricht CompileRequest aus einem CEL-Dienst enthält.
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 von der CEL-Typüberprüfung verwendet, um sicherzustellen, dass alle Bezeichner- und Funktionsreferenzen in einem Ausdruck deklariert und korrekt verwendet werden.
Phasen der Ausdrucksverarbeitung
CEL-Ausdrücke werden in drei Phasen verarbeitet:
- Parsen
- Prüfen
- Bewerten
Das häufigste Muster der 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

CEL wird mit einer
ANTLR Lexer- und Parser-Grammatik von einem für Menschen lesbaren Ausdruck in einen AST geparst. In der Parsing-Phase wird ein protobasierter
AST ausgegeben, wobei jeder Expr Knoten im AST eine Ganzzahl-ID enthält, die zum
Indexieren in Metadaten verwendet wird, die während des Parsens und Prüfens generiert werden. Die
syntax.proto Datei, die während des Parsens erstellt wird, stellt
die abstrakte Darstellung des Inhalts dar, der in der Stringform des
Ausdrucks eingegeben wurde.
Nachdem ein Ausdruck geparst wurde, wird er anhand der Umgebung typgeprüft, um sicherzustellen, dass alle Variablen- und Funktionsbezeichner im Ausdruck deklariert und korrekt verwendet werden. Die Typüberprüfung erstellt eine
checked.proto-Datei, die Metadaten zur Typ-, Variablen- und Funktions
auflösung enthält, die die Auswertungseffizienz erheblich verbessern können.
Nachdem ein Ausdruck geparst und geprüft wurde, wird der gespeicherte AST ausgewertet.
Für die CEL-Auswertung sind drei Dinge erforderlich:
- Funktionsbindungen für benutzerdefinierte Erweiterungen
- Variablenbindungen
- Ein AST zur Auswertung
Die Funktions- und Variablenbindungen müssen mit dem übereinstimmen, was zum Kompilieren des AST verwendet wurde. Alle diese Eingaben können für mehrere Auswertungen wiederverwendet werden. So kann beispielsweise ein AST für viele Gruppen von Variablenbindungen ausgewertet werden, dieselben Variablen können für viele ASTs verwendet werden oder die Funktionsbindungen können während der gesamten Lebensdauer eines Prozesses verwendet werden (ein häufiger Fall).
Formale Überprüfung
Neben der Laufzeitauswertung können CEL-Ausdrücke und -Richtlinien formal überprüft werden, um ihre Korrektheit für alle möglichen Eingaben mathematisch zu beweisen.
Das CEL Formal Verification Framework in CEL-Java basiert auf dem Z3-Theorembeweiser und übersetzt Ausdrücke und CEL Policies in SMT-Formeln (Satisfiability Modulo Theories), um Folgendes zu ermöglichen:
- Sicherheitsinvarianten beweisen:Mit den Spezifikationen
assumeundassertkönnen Sie überprüfen, ob kritische Richtlinien bei keiner Kombination von Eingaben umgangen werden können. - Logische Äquivalenz überprüfen:Mathematisch beweisen, dass umgestaltete oder KI-generierte Ausdrücke sich genauso verhalten wie die ursprüngliche Regel.
- Umfassende Gültigkeit erzwingen:Sicherstellen, dass Schutzmaßnahmen (z. B. Kubernetes-Validierungsrichtlinien für die Zulassung) für alle Eingaben gelten, oder konkrete Gegenbeispiele generieren, wenn sie verletzt werden.
- Falsch-Positive eliminieren:Mit der dreistufigen Taint-Tracking-Methode nicht zugeordnete benutzerdefinierte Funktionen isolieren, um sicherzustellen, dass gemeldete Verstöße immer reproduzierbare Fehler sind.
Eine Einführung und Beispiele aus der Praxis finden Sie im Google Open Source-Blog post: Securing the agentic era: Introducing formal verification for CEL.