1. Einführung
Was ist CEL?
CEL ist eine Nicht-Turing-Sprache für vollständige Ausdrücke, die schnell, portabel und sicher ausgeführt werden kann. CEL kann allein verwendet oder in ein größeres Produkt eingebettet werden.
CEL wurde als Sprache entwickelt, in der Nutzercode sicher ausgeführt werden kann. Es ist zwar gefährlich, eval()
blind im Python-Code eines Nutzers aufzurufen, Sie können den CEL-Code eines Nutzers aber sicher ausführen. Da CEL ein Verhalten verhindert, das die Leistung beeinträchtigen würde, wird es sicher in der Größenordnung von Nanosekunden bis Mikrosekunden ausgewertet. und sind 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, kann aber auch zum Erstellen komplexerer Objekte wie JSON- oder protobuf-Nachrichten verwendet werden.
Ist CEL das Richtige für Ihr Projekt?
Da CEL einen Ausdruck vom AST in Nanosekunden bis Mikrosekunden auswertet, ist CEL ideal für Anwendungen mit leistungskritischen Pfaden. Die Kompilierung des CEL-Codes in der AST sollte nicht in kritischen Pfaden erfolgen. Ideale Anwendungen sind solche, in denen die Konfiguration häufig ausgeführt und relativ selten geändert wird.
Beispielsweise ist das Ausführen einer Sicherheitsrichtlinie bei jeder HTTP-Anfrage an einen Dienst ein idealer Anwendungsfall für CEL, da sich die Sicherheitsrichtlinie nur selten ändert und CEL nur eine geringe Auswirkung auf die Antwortzeit hat. In diesem Fall gibt CEL einen booleschen Wert zurück, je nachdem, ob die Anfrage zulässig ist oder nicht. Es könnte jedoch eine komplexere Nachricht zurückgegeben werden.
Worum geht es in diesem Codelab?
Im ersten Schritt dieses Codelabs werden die Motivation zur Verwendung von CEL und die zugehörigen Kernkonzepte erläutert. Der Rest widmet sich dem Codieren von Übungen, die häufige Anwendungsfälle behandeln. Detaillierte Informationen zur Sprache, Semantik und Funktionen finden Sie in der CEL-Sprachdefinition auf GitHub und in den CEL-Go-Dokumenten.
Dieses Codelab richtet sich an Entwickler, die CEL erlernen möchten, um Dienste zu nutzen, die CEL bereits unterstützen. In diesem Codelab wird nicht beschrieben, wie Sie CEL in Ihr eigenes Projekt einbinden.
Aufgaben in diesem Lab
- Kernkonzepte von CEL
- Hello World: String mit CEL auswerten
- Variablen erstellen
- Informationen zum Kurzschluss der CEL in logischen UND/ODER-Operationen
- Mit CEL JSON erstellen
- Mit CEL Protobuffer erstellen
- Makros erstellen
- Möglichkeiten zum Optimieren von CEL-Ausdrücken
Voraussetzungen
Voraussetzungen
Dieses Codelab baut auf einem grundlegenden Verständnis von Protokollzwischenspeichern und Go Lang auf.
Wenn Sie mit Protokollzwischenspeichern nicht vertraut sind, vermittelt Ihnen die erste Übung einen Eindruck davon, wie CEL funktioniert. Da die fortgeschritteneren Beispiele jedoch Protokollzwischenspeicher als Eingabe für CEL verwenden, sind sie möglicherweise schwerer zu verstehen. Arbeiten Sie zuerst eine dieser Anleitungen durch. Protokollzwischenspeicher sind für die Verwendung von CEL nicht erforderlich, werden in diesem Codelab aber intensiv eingesetzt.
Sie können testen, ob Go installiert ist, indem Sie folgenden Befehl ausführen:
go --help
2. Wichtige Konzepte
Anwendungen
CEL ist allgemein einsetzbar und wurde für verschiedene Anwendungen verwendet, vom Routing-RPCs bis zum Definieren von Sicherheitsrichtlinien. CEL ist erweiterbar, anwendungsunabhängig und für Workflows mit einmaliger Kompilierung und vielen Auswertungen optimiert.
Viele Dienste und Anwendungen werten deklarative Konfigurationen aus. Die rollenbasierte Zugriffssteuerung (Role-Based Access Control, RBAC) ist beispielsweise eine deklarative Konfiguration, die anhand einer Rolle und einer Gruppe von Nutzern eine Zugriffsentscheidung trifft. Wenn deklarative Konfigurationen zu 80% verwendet werden, ist CEL ein nützliches Tool, um die verbleibenden 20% zu runden, wenn Nutzer mehr Ausdrucksstärke benötigen.
Compilation
Ein Ausdruck wird für eine Umgebung kompiliert. Beim Kompilierungsschritt wird ein abstrakter Syntaxbaum (AST) in der protobuf-Form erzeugt. Kompilierte Ausdrücke werden in der Regel für die 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.
Ausdrücke
Nutzer definieren Ausdrücke. und Anwendungen definieren die Umgebung, in der sie ausgeführt werden. Eine Funktionssignatur deklariert die Eingaben und wird außerhalb des CEL-Ausdrucks geschrieben. Die für CEL verfügbare Bibliothek mit Funktionen wird automatisch importiert.
Im folgenden Beispiel verwendet der Ausdruck ein Anfrageobjekt und die Anfrage enthält ein Anforderungstoken. Der Ausdruck gibt einen booleschen Wert zurück, der angibt, ob das Anforderungstoken noch gültig ist.
// 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
Umgebung
Umgebungen werden durch Dienste definiert. Dienste und Anwendungen, die CEL einbetten, deklarieren die Ausdrucksumgebung. Die Umgebung ist eine Sammlung von Variablen und Funktionen, die in Ausdrücken verwendet werden können.
Die proto-basierten Deklarationen werden von der CEL-Typprüfung verwendet, um sicherzustellen, dass alle Kennungs- und Funktionsverweise innerhalb eines Ausdrucks korrekt deklariert und verwendet werden.
Drei Phasen des Parsens eines Ausdrucks
Die Verarbeitung eines Ausdrucks besteht aus drei Phasen: parsen, überprüfen und auswerten. Das gängigste Muster für CEL besteht darin, dass eine Steuerungsebene Ausdrücke zum Konfigurationszeitpunkt parst und überprüft und die AST speichert.
Zur Laufzeit ruft die Datenebene den AST wiederholt ab und wertet ihn aus. CEL ist für die Laufzeiteffizienz optimiert. In latenzkritischen Codepfaden sollte das Parsen und die Prüfung jedoch nicht durchgeführt werden.
CEL wird von einem für Menschen lesbaren Ausdruck mithilfe einer ANTLR-Lexer-/Parser-Grammatik in einen abstrakten Syntaxbaum geparst. Die Parsing-Phase gibt einen proto-basierten abstrakten Syntaxbaum aus, in dem jeder Expr-Knoten in der AST eine Ganzzahl-ID enthält, die zur Indexierung in Metadaten verwendet wird, die während des Parsens und der Prüfung generiert werden. Die beim Parsen erstellte syntax.proto stellt die abstrakte Darstellung dessen dar, was in die Stringform des Ausdrucks eingegeben wurde.
Nachdem ein Ausdruck geparst wurde, kann er mit der Umgebung abgeglichen werden, um sicherzustellen, dass alle Variablen- und Funktions-IDs im Ausdruck deklariert wurden und korrekt verwendet werden. Die Typprüfung erzeugt eine checked.proto-Datei, die Metadaten zu Typ, Variablen und Funktionsauflösung enthält, die die Auswertungseffizienz erheblich verbessern können.
Der CEL-Evaluierende benötigt drei Dinge:
- Funktionsbindungen für benutzerdefinierte Erweiterungen
- Variablenbindungen
- Auszuwertende AST
Die Funktions- und Variablenbindungen sollten mit dem übereinstimmen, was zum Kompilieren der AST verwendet wurde. Jede dieser Eingaben kann für mehrere Auswertungen wiederverwendet werden, z. B. eine AST, die über viele Gruppen von Variablenbindungen ausgewertet wird, oder dieselben Variablen, die für viele ASTs verwendet werden, oder die Funktionsbindungen, die über die Lebensdauer eines Prozesses hinweg verwendet werden (häufiger Fall).
3. Erstelle ein
Der Code für dieses Codelab befindet sich im codelab
-Ordner des Repositorys cel-go
. Die Lösung ist im Ordner codelab/solution
desselben Repositorys verfügbar.
Klonen Sie das Repository und fügen Sie "cd" in das Repository ein:
git clone https://github.com/google/cel-go.git
cd cel-go/codelab
Führen Sie den Code mit go run
aus:
go run .
Es sollte folgende Ausgabe angezeigt werden:
=== Exercise 1: Hello World ===
=== Exercise 2: Variables ===
=== Exercise 3: Logical AND/OR ===
=== Exercise 4: Customization ===
=== Exercise 5: Building JSON ===
=== Exercise 6: Building Protos ===
=== Exercise 7: Macros ===
=== Exercise 8: Tuning ===
Wo sind die CEL-Pakete?
Öffnen Sie codelab/codelab.go
in Ihrem Editor. Sie sollten die Hauptfunktion sehen, die die Ausführung der Übungen in diesem Codelab steuert, gefolgt von drei Blöcken mit Hilfsfunktionen. Die ersten Helfer unterstützen die Phasen der CEL-Bewertung:
Compile
-Funktion: parst und prüft und gibt den Ausdruck in einer Umgebung einEval
-Funktion: Wertet ein kompiliertes Programm anhand einer Eingabe ausReport
-Funktion: gibt das Bewertungsergebnis korrekt aus
Außerdem wurden die Hilfsprogramme request
und auth
bereitgestellt, die Sie bei der Eingabe von Eingaben für die verschiedenen Übungen unterstützen.
In den Übungen werden Pakete mit ihrem kurzen Paketnamen bezeichnet. Die Zuordnung vom Paket zum Quellspeicherort innerhalb des Repositorys google/cel-go
finden Sie unten, wenn Sie ins Detail gehen möchten:
Paket | Quellort | Beschreibung |
cel | Oberflächen der obersten Ebene | |
Ref | Referenzschnittstellen | |
Typen | Laufzeittypwerte |
4. Hallo Welt!
In der Tradition aller Programmiersprachen beginnen wir mit der Erstellung und Bewertung von „Hello World!“.
Umgebung konfigurieren
Suchen Sie in Ihrem Editor nach der Deklaration von exercise1
und füllen Sie Folgendes aus, um die Umgebung einzurichten:
// exercise1 evaluates a simple literal expression: "Hello, World!"
//
// Compile, eval, profit!
func exercise1() {
fmt.Println("=== Exercise 1: Hello World ===\n")
// Create the standard environment.
env, err := cel.NewEnv()
if err != nil {
glog.Exitf("env error: %v", err)
}
// Will add the parse and check steps here
}
CEL-Anwendungen werten einen Ausdruck in Bezug auf eine Umgebung aus. env, err := cel.NewEnv()
konfiguriert die Standardumgebung.
Sie können die Umgebung anpassen, indem Sie die cel.EnvOption
-Optionen für den Aufruf angeben. Mit diesen Optionen können unter anderem Makros deaktiviert und benutzerdefinierte Variablen und Funktionen deklariert werden.
Die CEL-Standardumgebung unterstützt alle Typen, Operatoren, Funktionen und Makros, die in der Sprachspezifikation definiert werden.
Ausdruck parsen und prüfen
Sobald die Umgebung konfiguriert ist, können Ausdrücke geparst und geprüft werden. Fügen Sie der Funktion Folgendes hinzu:
// exercise1 evaluates a simple literal expression: "Hello, World!"
//
// Compile, eval, profit!
func exercise1() {
fmt.Println("=== Exercise 1: Hello World ===\n")
// Create the standard environment.
env, err := cel.NewEnv()
if err != nil {
glog.Exitf("env error: %v", err)
}
// Check that the expression compiles and returns a String.
ast, iss := env.Parse(`"Hello, World!"`)
// Report syntactic errors, if present.
if iss.Err() != nil {
glog.Exit(iss.Err())
}
// Type-check the expression for correctness.
checked, iss := env.Check(ast)
// Report semantic errors, if present.
if iss.Err() != nil {
glog.Exit(iss.Err())
}
// Check the output type is a string.
if !reflect.DeepEqual(checked.OutputType(), cel.StringType) {
glog.Exitf(
"Got %v, wanted %v output type",
checked.OutputType(), cel.StringType,
)
}
// Will add the planning step here
}
Der Wert iss
, der von den Aufrufen Parse
und Check
zurückgegeben wird, ist eine Liste von Problemen, die Fehler sein können. Wenn iss.Err()
nicht null ist, liegt ein Fehler in der Syntax oder Semantik vor und das Programm kann nicht fortfahren. Wenn der Ausdruck wohlgeformt ist, ist das Ergebnis dieser Aufrufe eine ausführbare cel.Ast
.
Den Ausdruck berechnen
Nachdem der Ausdruck geparst und in eine cel.Ast
eingecheckt wurde, kann er in ein auswertbares Programm konvertiert werden, dessen Funktionsbindungen und Auswertungsmodi mit funktionalen Optionen angepasst werden können. Es ist auch möglich, ein cel.Ast
-Objekt mit der Funktion cel.CheckedExprToAst oder cel.ParsedExprToAst aus einem Proto zu lesen.
Sobald eine cel.Program
geplant ist, kann sie anhand von Eingaben ausgewertet werden, indem Eval
aufgerufen wird. Das Ergebnis von Eval
enthält das Ergebnis, die Bewertungsdetails und den Fehlerstatus.
Termin hinzufügen und eval
anrufen:
// exercise1 evaluates a simple literal expression: "Hello, World!"
//
// Compile, eval, profit!
func exercise1() {
fmt.Println("=== Exercise 1: Hello World ===\n")
// Create the standard environment.
env, err := cel.NewEnv()
if err != nil {
glog.Exitf("env error: %v", err)
}
// Check that the expression compiles and returns a String.
ast, iss := env.Parse(`"Hello, World!"`)
// Report syntactic errors, if present.
if iss.Err() != nil {
glog.Exit(iss.Err())
}
// Type-check the expression for correctness.
checked, iss := env.Check(ast)
// Report semantic errors, if present.
if iss.Err() != nil {
glog.Exit(iss.Err())
}
// Check the output type is a string.
if !reflect.DeepEqual(checked.OutputType(), cel.StringType) {
glog.Exitf(
"Got %v, wanted %v output type",
checked.OutputType(), cel.StringType)
}
// Plan the program.
program, err := env.Program(checked)
if err != nil {
glog.Exitf("program error: %v", err)
}
// Evaluate the program without any additional arguments.
eval(program, cel.NoVars())
fmt.Println()
}
Der Einfachheit halber werden wir die oben aufgeführten Fehlerfälle bei zukünftigen Übungen weglassen.
Code ausführen
Führen Sie in der Befehlszeile den Code noch einmal aus:
go run .
Sie sollten die folgende Ausgabe zusammen mit Platzhaltern für die nächsten Übungen sehen.
=== Exercise 1: Hello World ===
------ input ------
(interpreter.emptyActivation)
------ result ------
value: Hello, World! (types.String)
5. Variablen in einer Funktion verwenden
Die meisten CEL-Anwendungen deklarieren Variablen, auf die in Ausdrücken verwiesen werden kann. In Deklarationen von Variablen werden ein Name und ein Typ angegeben. Der Typ einer Variablen kann entweder ein CEL-integrierter Typ, ein bekannter Protokollpuffertyp oder ein beliebiger protobuf-Nachrichtentyp sein, solange ihr Deskriptor auch für CEL angegeben wird.
Funktion hinzufügen
Suchen Sie in Ihrem Editor nach der Deklaration von exercise2
und fügen Sie Folgendes hinzu:
// exercise2 shows how to declare and use variables in expressions.
//
// Given a request of type google.rpc.context.AttributeContext.Request
// determine whether a specific auth claim is set.
func exercise2() {
fmt.Println("=== Exercise 2: Variables ===\n")
env, err := cel.NewEnv(
// Add cel.EnvOptions values here.
)
if err != nil {
glog.Exit(err)
}
ast := compile(env, `request.auth.claims.group == 'admin'`, cel.BoolType)
program, _ := env.Program(ast)
// Evaluate a request object that sets the proper group claim.
claims := map[string]string{"group": "admin"}
eval(program, request(auth("user:me@acme.co", claims), time.Now()))
fmt.Println()
}
Führen Sie den Fehler noch einmal aus und analysieren Sie den Fehler.
Führen Sie das Programm noch einmal aus:
go run .
Es sollte folgende Ausgabe angezeigt werden:
ERROR: <input>:1:1: undeclared reference to 'request' (in container '')
| request.auth.claims.group == 'admin'
| ^
Die Typprüfung erzeugt einen Fehler für das Anfrageobjekt, der praktisch das Quell-Snippet enthält, in dem der Fehler auftritt.
6. Variablen deklarieren
EnvOptions
hinzufügen
Beheben wir den resultierenden Fehler im Editor, indem wir eine Deklaration für das Anfrageobjekt als Nachricht vom Typ google.rpc.context.AttributeContext.Request
angeben. Beispiel:
// exercise2 shows how to declare and use variables in expressions.
//
// Given a `request` of type `google.rpc.context.AttributeContext.Request`
// determine whether a specific auth claim is set.
func exercise2() {
fmt.Println("=== Exercise 2: Variables ===\n")
env, err := cel.NewEnv(
cel.Variable("request",
cel.ObjectType("google.rpc.context.AttributeContext.Request"),
),
)
if err != nil {
glog.Exit(err)
}
ast := compile(env, `request.auth.claims.group == 'admin'`, cel.BoolType)
program, _ := env.Program(ast)
// Evaluate a request object that sets the proper group claim.
claims := map[string]string{"group": "admin"}
eval(program, request(auth("user:me@acme.co", claims), time.Now()))
fmt.Println()
}
Führen Sie den Fehler noch einmal aus und analysieren Sie den Fehler.
Erneutes Ausführen des Programms:
go run .
Es sollte folgender Fehler angezeigt werden:
ERROR: <input>:1:8: [internal] unexpected failed resolution of 'google.rpc.context.AttributeContext.Request'
| request.auth.claims.group == 'admin'
| .......^
Um Variablen verwenden zu können, die auf protobuf-Nachrichten verweisen, muss der Typprüfer auch die Typbeschreibung kennen.
Verwenden Sie cel.Types
, um den Deskriptor für die Anfrage in Ihrer Funktion zu bestimmen:
// exercise2 shows how to declare and use variables in expressions.
//
// Given a `request` of type `google.rpc.context.AttributeContext.Request`
// determine whether a specific auth claim is set.
func exercise2() {
fmt.Println("=== Exercise 2: Variables ===\n")
env, err := cel.NewEnv(
cel.Types(&rpcpb.AttributeContext_Request{}),
cel.Variable("request",
cel.ObjectType("google.rpc.context.AttributeContext.Request"),
),
)
if err != nil {
glog.Exit(err)
}
ast := compile(env, `request.auth.claims.group == 'admin'`, cel.BoolType)
program, _ := env.Program(ast)
// Evaluate a request object that sets the proper group claim.
claims := map[string]string{"group": "admin"}
eval(program, request(auth("user:me@acme.co", claims), time.Now()))
fmt.Println()
}
Wiederholung erfolgreich!
Führen Sie das Programm noch einmal aus:
go run .
Sie sollten Folgendes sehen:
=== Exercise 2: Variables ===
request.auth.claims.group == 'admin'
------ input ------
request = time: <
seconds: 1569255569
>
auth: <
principal: "user:me@acme.co"
claims: <
fields: <
key: "group"
value: <
string_value: "admin"
>
>
>
>
------ result ------
value: true (types.Bool)
Zur Überprüfung haben wir gerade eine Variable für einen Fehler deklariert, ihr einen Typdeskriptor zugewiesen und dann in der Ausdrucksauswertung auf die Variable verwiesen.
7. Logisches UND/ODER
Zu den besonderen Funktionen von CEL gehört die Verwendung kommutativer logischer Operatoren. Jede Seite eines bedingten Zweigs kann die Bewertung kurzschließen, auch wenn Fehler oder Teileingaben vorliegen.
Mit anderen Worten, CEL findet eine Bewertungsreihenfolge, die nach Möglichkeit ein Ergebnis liefert, wobei Fehler oder sogar fehlende Daten, die in anderen Bewertungsreihenfolgen auftreten könnten, ignoriert werden. Anwendungen können sich auf dieses Attribut verlassen, um die Bewertungskosten zu minimieren und die Erfassung teurer Eingaben zu verschieben, wenn ein Ergebnis ohne sie erreicht werden kann.
Wir fügen ein UND/ODER-Beispiel hinzu und probieren es dann mit einer anderen Eingabe aus, um zu verstehen, wie die CEL-Kurzschlussauswertung funktioniert.
Funktion erstellen
Fügen Sie in Ihrem Editor zu Übung 3 folgenden Inhalt hinzu:
// exercise3 demonstrates how CEL's commutative logical operators work.
//
// Construct an expression which checks if the `request.auth.claims.group`
// value is equal to admin or the `request.auth.principal` is
// `user:me@acme.co`. Issue two requests, one that specifies the proper
// user, and one that specifies an unexpected user.
func exercise3() {
fmt.Println("=== Exercise 3: Logical AND/OR ===\n")
env, _ := cel.NewEnv(
cel.Types(&rpcpb.AttributeContext_Request{}),
cel.Variable("request",
cel.ObjectType("google.rpc.context.AttributeContext.Request"),
),
)
}
Fügen Sie als Nächstes diese ODER-Anweisung ein, die „true“ zurückgibt, wenn der Nutzer Mitglied der Gruppe admin
ist oder eine bestimmte E-Mail-ID hat:
// exercise3 demonstrates how CEL's commutative logical operators work.
//
// Construct an expression which checks if the `request.auth.claims.group`
// value is equal to admin or the `request.auth.principal` is
// `user:me@acme.co`. Issue two requests, one that specifies the proper
// user, and one that specifies an unexpected user.
func exercise3() {
fmt.Println("=== Exercise 3: Logical AND/OR ===\n")
env, _ := cel.NewEnv(
cel.Types(&rpcpb.AttributeContext_Request{}),
cel.Variable("request",
cel.ObjectType("google.rpc.context.AttributeContext.Request"),
),
)
ast := compile(env,
`request.auth.claims.group == 'admin'
|| request.auth.principal == 'user:me@acme.co'`,
cel.BoolType)
program, _ := env.Program(ast)
}
Fügen Sie abschließend den eval
-Fall hinzu, der den Nutzer mit einem leeren Anspruchssatz bewertet:
// exercise3 demonstrates how CEL's commutative logical operators work.
//
// Construct an expression which checks whether the request.auth.claims.group
// value is equal to admin or the request.auth.principal is
// user:me@acme.co. Issue two requests, one that specifies the proper user,
// and one that specifies an unexpected user.
func exercise3() {
fmt.Println("=== Exercise 3: Logical AND/OR ===\n")
env, _ := cel.NewEnv(
cel.Types(&rpcpb.AttributeContext_Request{}),
cel.Variable("request",
cel.ObjectType("google.rpc.context.AttributeContext.Request"),
),
)
ast := compile(env,
`request.auth.claims.group == 'admin'
|| request.auth.principal == 'user:me@acme.co'`,
cel.BoolType)
program, _ := env.Program(ast)
emptyClaims := map[string]string{}
eval(program, request(auth("user:me@acme.co", emptyClaims), time.Now()))
}
Code mit leerem Anspruchssatz ausführen
Wenn Sie das Programm noch einmal ausführen, sollten Sie die folgende neue Ausgabe sehen:
=== Exercise 3: Logical AND/OR ===
request.auth.claims.group == 'admin'
|| request.auth.principal == 'user:me@acme.co'
------ input ------
request = time: <
seconds: 1569302377
>
auth: <
principal: "user:me@acme.co"
claims: <
>
>
------ result ------
value: true (types.Bool)
Bewertungsfall aktualisieren
Aktualisieren Sie als Nächstes den Bewertungsfall, damit ein anderes Hauptkonto mit dem leeren Anspruchssatz übergeben wird:
// exercise3 demonstrates how CEL's commutative logical operators work.
//
// Construct an expression which checks whether the request.auth.claims.group
// value is equal to admin or the request.auth.principal is
// user:me@acme.co. Issue two requests, one that specifies the proper user,
// and one that specifies an unexpected user.
func exercise3() {
fmt.Println("=== Exercise 3: Logical AND/OR ===\n")
env, _ := cel.NewEnv(
cel.Types(&rpcpb.AttributeContext_Request{}),
cel.Variable("request",
cel.ObjectType("google.rpc.context.AttributeContext.Request"),
),
)
ast := compile(env,
`request.auth.claims.group == 'admin'
|| request.auth.principal == 'user:me@acme.co'`,
cel.BoolType)
program, _ := env.Program(ast)
emptyClaims := map[string]string{}
eval(program, request(auth("other:me@acme.co", emptyClaims), time.Now()))
}
Code mit Uhrzeit ausführen
Führen Sie das Programm noch einmal aus,
go run .
sollte folgender Fehler angezeigt werden:
=== Exercise 3: Logical AND/OR ===
request.auth.claims.group == 'admin'
|| request.auth.principal == 'user:me@acme.co'
------ input ------
request = time: <
seconds: 1588989833
>
auth: <
principal: "user:me@acme.co"
claims: <
>
>
------ result ------
value: true (types.Bool)
------ input ------
request = time: <
seconds: 1588989833
>
auth: <
principal: "other:me@acme.co"
claims: <
>
>
------ result ------
error: no such key: group
In protobuf wissen wir, welche Felder und Typen zu erwarten sind. Bei Map- und JSON-Werten ist nicht bekannt, ob ein Schlüssel vorhanden ist. Da es für einen fehlenden Schlüssel keinen sicheren Standardwert gibt, wird in CEL standardmäßig „error“ verwendet.
8. Benutzerdefinierte Funktionen
CEL enthält zwar viele integrierte Funktionen, aber in bestimmten Fällen ist eine benutzerdefinierte Funktion nützlich. Beispielsweise können benutzerdefinierte Funktionen verwendet werden, um die Nutzererfahrung bei gängigen Bedingungen zu verbessern oder kontextsensitiven Status anzuzeigen.
In dieser Übung erfahren Sie, wie Sie eine Funktion freigeben, um häufig verwendete Prüfungen zu bündeln.
Benutzerdefinierte Funktion aufrufen
Erstellen Sie zuerst den Code zum Einrichten einer Überschreibung namens contains
, die bestimmt, ob ein Schlüssel in einer Zuordnung vorhanden ist und einen bestimmten Wert hat. Lassen Sie Platzhalter für die Funktionsdefinition und Funktionsbindung unverändert:
// exercise4 demonstrates how to extend CEL with custom functions.
// Declare a `contains` member function on map types that returns a boolean
// indicating whether the map contains the key-value pair.
func exercise4() {
fmt.Println("=== Exercise 4: Customization ===\n")
// Determine whether an optional claim is set to the proper value. The
// custom map.contains(key, value) function is used as an alternative to:
// key in map && map[key] == value
env, _ := cel.NewEnv(
cel.Types(&rpcpb.AttributeContext_Request{}),
// Declare the request.
cel.Variable("request",
cel.ObjectType("google.rpc.context.AttributeContext.Request"),
),
// Add the custom function declaration and binding here with cel.Function()
)
ast := compile(env,
`request.auth.claims.contains('group', 'admin')`,
cel.BoolType)
// Construct the program plan and provide the 'contains' function impl.
// Output: false
program, _ := env.Program(ast)
emptyClaims := map[string]string{}
eval(
program,
request(auth("user:me@acme.co", emptyClaims), time.Now()),
)
fmt.Println()
}
Code ausführen und den Fehler verstehen
Wenn Sie den Code noch einmal ausführen, sollten Sie den folgenden Fehler sehen:
ERROR: <input>:1:29: found no matching overload for 'contains' applied to 'map(string, dyn).(string, string)'
| request.auth.claims.contains('group', 'admin')
| ............................^
Um den Fehler zu beheben, müssen wir die Funktion contains
zur Liste der Deklarationen hinzufügen, in denen derzeit die Variable „request“ deklariert ist.
Geben Sie einen parametrisierten Typ an, indem Sie die folgenden drei Zeilen hinzufügen. (Das ist so kompliziert, wie jede Funktionsüberlastung für CEL sein wird.)
// exercise4 demonstrates how to extend CEL with custom functions.
// Declare a `contains` member function on map types that returns a boolean
// indicating whether the map contains the key-value pair.
func exercise4() {
fmt.Println("=== Exercise 4: Customization ===\n")
// Determine whether an optional claim is set to the proper value. The custom
// map.contains(key, value) function is used as an alternative to:
// key in map && map[key] == value
// Useful components of the type-signature for 'contains'.
typeParamA := cel.TypeParamType("A")
typeParamB := cel.TypeParamType("B")
mapAB := cel.MapType(typeParamA, typeParamB)
env, _ := cel.NewEnv(
cel.Types(&rpcpb.AttributeContext_Request{}),
// Declare the request.
cel.Variable("request",
cel.ObjectType("google.rpc.context.AttributeContext.Request"),
),
// Add the custom function declaration and binding here with cel.Function()
)
ast := compile(env,
`request.auth.claims.contains('group', 'admin')`,
cel.BoolType)
// Construct the program plan.
// Output: false
program, _ := env.Program(ast)
emptyClaims := map[string]string{}
eval(program, request(auth("user:me@acme.co", emptyClaims), time.Now()))
fmt.Println()
}
Benutzerdefinierte Funktion hinzufügen
Als Nächstes fügen wir eine neue „contains“-Funktion hinzu, die die parametrisierten Typen verwendet:
// exercise4 demonstrates how to extend CEL with custom functions.
// Declare a `contains` member function on map types that returns a boolean
// indicating whether the map contains the key-value pair.
func exercise4() {
fmt.Println("=== Exercise 4: Customization ===\n")
// Determine whether an optional claim is set to the proper value. The custom
// map.contains(key, value) function is used as an alternative to:
// key in map && map[key] == value
// Useful components of the type-signature for 'contains'.
typeParamA := cel.TypeParamType("A")
typeParamB := cel.TypeParamType("B")
mapAB := cel.MapType(typeParamA, typeParamB)
// Env declaration.
env, _ := cel.NewEnv(
cel.Types(&rpcpb.AttributeContext_Request{}),
// Declare the request.
cel.Variable("request",
cel.ObjectType("google.rpc.context.AttributeContext.Request"),
),
// Declare the custom contains function and its implementation.
cel.Function("contains",
cel.MemberOverload(
"map_contains_key_value",
[]*cel.Type{mapAB, typeParamA, typeParamB},
cel.BoolType,
// Provide the implementation using cel.FunctionBinding()
),
),
)
ast := compile(env,
`request.auth.claims.contains('group', 'admin')`,
cel.BoolType)
// Construct the program plan and provide the 'contains' function impl.
// Output: false
program, _ := env.Program(ast)
emptyClaims := map[string]string{}
eval(program, request(auth("user:me@acme.co", emptyClaims), time.Now()))
fmt.Println()
}
Programm ausführen, um den Fehler zu verstehen
Führe das Training aus. Sie sollten den folgenden Fehler zu der fehlenden Laufzeitfunktion sehen:
------ result ------
error: no such overload: contains
Stellen Sie die Funktionsimplementierung in der Deklaration NewEnv
mithilfe der Funktion cel.FunctionBinding()
bereit:
// exercise4 demonstrates how to extend CEL with custom functions.
//
// Declare a contains member function on map types that returns a boolean
// indicating whether the map contains the key-value pair.
func exercise4() {
fmt.Println("=== Exercise 4: Customization ===\n")
// Determine whether an optional claim is set to the proper value. The custom
// map.contains(key, value) function is used as an alternative to:
// key in map && map[key] == value
// Useful components of the type-signature for 'contains'.
typeParamA := cel.TypeParamType("A")
typeParamB := cel.TypeParamType("B")
mapAB := cel.MapType(typeParamA, typeParamB)
// Env declaration.
env, _ := cel.NewEnv(
cel.Types(&rpcpb.AttributeContext_Request{}),
// Declare the request.
cel.Variable("request",
cel.ObjectType("google.rpc.context.AttributeContext.Request"),
),
// Declare the custom contains function and its implementation.
cel.Function("contains",
cel.MemberOverload(
"map_contains_key_value",
[]*cel.Type{mapAB, typeParamA, typeParamB},
cel.BoolType,
cel.FunctionBinding(mapContainsKeyValue)),
),
)
ast := compile(env,
`request.auth.claims.contains('group', 'admin')`,
cel.BoolType)
// Construct the program plan.
// Output: false
program, err := env.Program(ast)
if err != nil {
glog.Exit(err)
}
eval(program, request(auth("user:me@acme.co", emptyClaims), time.Now()))
claims := map[string]string{"group": "admin"}
eval(program, request(auth("user:me@acme.co", claims), time.Now()))
fmt.Println()
}
Das Programm sollte nun erfolgreich ausgeführt werden:
=== Exercise 4: Custom Functions ===
request.auth.claims.contains('group', 'admin')
------ input ------
request = time: <
seconds: 1569302377
>
auth: <
principal: "user:me@acme.co"
claims: <
>
>
------ result ------
value: false (types.Bool)
Was passiert, wenn der Anspruch erhoben wurde?
Um zusätzliches Guthaben zu erhalten, kannst du den Administrator-Anspruch auf die Eingabe festlegen, um zu prüfen, ob die Überlastung „Enthält“ auch „true“ zurückgibt, wenn der Anspruch vorhanden ist. Es sollte folgende Ausgabe angezeigt werden:
=== Exercise 4: Customization ===
request.auth.claims.contains('group','admin')
------ input ------
request = time: <
seconds: 1588991010
>
auth: <
principal: "user:me@acme.co"
claims: <
>
>
------ result ------
value: false (types.Bool)
------ input ------
request = time: <
seconds: 1588991010
>
auth: <
principal: "user:me@acme.co"
claims: <
fields: <
key: "group"
value: <
string_value: "admin"
>
>
>
>
------ result ------
value: true (types.Bool)
Bevor Sie fortfahren, sollten Sie sich die mapContainsKeyValue
-Funktion ansehen:
// mapContainsKeyValue implements the custom function:
// map.contains(key, value) -> bool.
func mapContainsKeyValue(args ...ref.Val) ref.Val {
// The declaration of the function ensures that only arguments which match
// the mapContainsKey signature will be provided to the function.
m := args[0].(traits.Mapper)
// CEL has many interfaces for dealing with different type abstractions.
// The traits.Mapper interface unifies field presence testing on proto
// messages and maps.
key := args[1]
v, found := m.Find(key)
// If not found and the value was non-nil, the value is an error per the
// `Find` contract. Propagate it accordingly. Such an error might occur with
// a map whose key-type is listed as 'dyn'.
if !found {
if v != nil {
return types.ValOrErr(v, "unsupported key type")
}
// Return CEL False if the key was not found.
return types.False
}
// Otherwise whether the value at the key equals the value provided.
return v.Equal(args[2])
}
Um die Erweiterung möglichst einfach zu ermöglichen, erwartet die Signatur für benutzerdefinierte Funktionen die Argumente des Typs ref.Val
. Der Nachteil ist hier, dass die einfache Erweiterung eine Belastung für den Implementierer darstellt, um sicherzustellen, dass alle Werttypen ordnungsgemäß verarbeitet werden. Wenn die Eingabeargumenttypen oder die Anzahl nicht mit der Funktionsdeklaration übereinstimmen, sollte der Fehler no such overload
zurückgegeben werden.
cel.FunctionBinding()
fügt einen Laufzeittyp-Guard hinzu, um sicherzustellen, dass der Laufzeitvertrag mit der typgeprüften Deklaration in der Umgebung übereinstimmt.
9. JSON wird erstellt
CEL kann auch nicht boolesche Ausgaben wie JSON erzeugen. Fügen Sie der Funktion Folgendes hinzu:
// exercise5 covers how to build complex objects as CEL literals.
//
// Given the input now, construct a JWT with an expiry of 5 minutes.
func exercise5() {
fmt.Println("=== Exercise 5: Building JSON ===\n")
env, _ := cel.NewEnv(
// Declare the 'now' variable as a Timestamp.
// cel.Variable("now", cel.TimestampType),
)
// Note the quoted keys in the CEL map literal. For proto messages the
// field names are unquoted as they represent well-defined identifiers.
ast := compile(env, `
{'sub': 'serviceAccount:delegate@acme.co',
'aud': 'my-project',
'iss': 'auth.acme.com:12350',
'iat': now,
'nbf': now,
'exp': now + duration('300s'),
'extra_claims': {
'group': 'admin'
}}`,
cel.MapType(cel.StringType, cel.DynType))
program, _ := env.Program(ast)
out, _, _ := eval(
program,
map[string]interface{}{
"now": &tpb.Timestamp{Seconds: time.Now().Unix()},
},
)
fmt.Printf("------ type conversion ------\n%v\n", out)
fmt.Println()
}
Code ausführen
Wenn Sie den Code noch einmal ausführen, sollten Sie den folgenden Fehler sehen:
ERROR: <input>:5:11: undeclared reference to 'now' (in container '')
| 'iat': now,
| ..........^
... and more ...
Fügen Sie cel.NewEnv()
eine Deklaration für die Variable now
vom Typ cel.TimestampType
hinzu und führen Sie sie noch einmal aus:
// exercise5 covers how to build complex objects as CEL literals.
//
// Given the input now, construct a JWT with an expiry of 5 minutes.
func exercise5() {
fmt.Println("=== Exercise 5: Building JSON ===\n")
env, _ := cel.NewEnv(
cel.Variable("now", cel.TimestampType),
)
// Note the quoted keys in the CEL map literal. For proto messages the
// field names are unquoted as they represent well-defined identifiers.
ast := compile(env, `
{'sub': 'serviceAccount:delegate@acme.co',
'aud': 'my-project',
'iss': 'auth.acme.com:12350',
'iat': now,
'nbf': now,
'exp': now + duration('300s'),
'extra_claims': {
'group': 'admin'
}}`,
cel.MapType(cel.StringType, cel.DynType))
// Hint:
// Convert `out` to JSON using the valueToJSON() helper method.
// The valueToJSON() function calls ConvertToNative(&structpb.Value{})
// to adapt the CEL value to a protobuf JSON representation and then
// uses the jsonpb.Marshaler utilities on the output to render the JSON
// string.
program, _ := env.Program(ast)
out, _, _ := eval(
program,
map[string]interface{}{
"now": time.Now(),
},
)
fmt.Printf("------ type conversion ------\n%v\n", out)
fmt.Println()
}
Führen Sie den Code noch einmal aus. Er sollte erfolgreich sein:
=== Exercise 5: Building JSON ===
{'aud': 'my-project',
'exp': now + duration('300s'),
'extra_claims': {
'group': 'admin'
},
'iat': now,
'iss': 'auth.acme.com:12350',
'nbf': now,
'sub': 'serviceAccount:delegate@acme.co'
}
------ input ------
now = seconds: 1569302377
------ result ------
value: &{0xc0002eaf00 map[aud:my-project exp:{0xc0000973c0} extra_claims:0xc000097400 iat:{0xc000097300} iss:auth.acme.com:12350 nbf:{0xc000097300} sub:serviceAccount:delegate@acme.co] {0x8c8f60 0xc00040a6c0 21}} (*types.baseMap)
------ type conversion ------
&{0xc000313510 map[aud:my-project exp:{0xc000442510} extra_claims:0xc0004acdc0 iat:{0xc000442450} iss:auth.acme.com:12350 nbf:{0xc000442450} sub:serviceAccount:delegate@acme.co] {0x9d0ce0 0xc0004424b0 21}}
Das Programm wird ausgeführt, aber der Ausgabewert out
muss explizit in JSON konvertiert werden. Die interne CEL-Darstellung ist in diesem Fall JSON-konvertierbar, da sie sich nur auf Typen bezieht, die von JSON unterstützt werden oder für die es eine bekannte Proto-zu-JSON-Zuordnung gibt.
// exercise5 covers how to build complex objects as CEL literals.
//
// Given the input now, construct a JWT with an expiry of 5 minutes.
func exercise5() {
fmt.Println("=== Exercise 5: Building JSON ===\n")
...
fmt.Printf("------ type conversion ------\n%v\n", valueToJSON(out))
fmt.Println()
}
Nachdem der Typ mit der Hilfsfunktion valueToJSON
in der Datei codelab.go
konvertiert wurde, sollten Sie die folgende zusätzliche Ausgabe sehen:
------ type conversion ------
{
"aud": "my-project",
"exp": "2019-10-13T05:54:29Z",
"extra_claims": {
"group": "admin"
},
"iat": "2019-10-13T05:49:29Z",
"iss": "auth.acme.com:12350",
"nbf": "2019-10-13T05:49:29Z",
"sub": "serviceAccount:delegate@acme.co"
}
10. Protos erstellen
CEL kann protobuf-Nachrichten für jeden in der Anwendung kompilierten Nachrichtentyp erstellen. Funktion zum Erstellen einer google.rpc.context.AttributeContext.Request
aus einer Eingabe-jwt
hinzufügen
// exercise6 describes how to build proto message types within CEL.
//
// Given an input jwt and time now construct a
// google.rpc.context.AttributeContext.Request with the time and auth
// fields populated according to the go/api-attributes specification.
func exercise6() {
fmt.Println("=== Exercise 6: Building Protos ===\n")
// Construct an environment and indicate that the container for all references
// within the expression is `google.rpc.context.AttributeContext`.
requestType := &rpcpb.AttributeContext_Request{}
env, _ := cel.NewEnv(
// Add cel.Container() option for 'google.rpc.context.AttributeContext'
cel.Types(requestType),
// Add cel.Variable() option for 'jwt' as a map(string, Dyn) type
// and for 'now' as a timestamp.
)
// Compile the Request message construction expression and validate that
// the resulting expression type matches the fully qualified message name.
//
// Note: the field names within the proto message types are not quoted as they
// are well-defined names composed of valid identifier characters. Also, note
// that when building nested proto objects, the message name needs to prefix
// the object construction.
ast := compile(env, `
Request{
auth: Auth{
principal: jwt.iss + '/' + jwt.sub,
audiences: [jwt.aud],
presenter: 'azp' in jwt ? jwt.azp : "",
claims: jwt
},
time: now
}`,
cel.ObjectType("google.rpc.context.AttributeContext.Request"),
)
program, _ := env.Program(ast)
// Construct the message. The result is a ref.Val that returns a dynamic
// proto message.
out, _, _ := eval(
program,
map[string]interface{}{
"jwt": map[string]interface{}{
"sub": "serviceAccount:delegate@acme.co",
"aud": "my-project",
"iss": "auth.acme.com:12350",
"extra_claims": map[string]string{
"group": "admin",
},
},
"now": time.Now(),
},
)
// Hint: Unwrap the CEL value to a proto. Make sure to use the
// `ConvertToNative(reflect.TypeOf(requestType))` to convert the dynamic proto
// message to the concrete proto message type expected.
fmt.Printf("------ type unwrap ------\n%v\n", out)
fmt.Println()
}
Code ausführen
Wenn Sie den Code noch einmal ausführen, sollten Sie den folgenden Fehler sehen:
ERROR: <input>:2:10: undeclared reference to 'Request' (in container '')
| Request{
| .........^
Der Container entspricht im Grunde einem Namespace oder Paket, kann aber auch so detailliert wie ein protobuf-Nachrichtenname sein. CEL-Container verwenden dieselben Namespace-Auflösungsregeln wie Protobuf und C++, um zu bestimmen, wo eine bestimmte Variable, eine Funktion oder ein Typname deklariert wird.
Bei Verwendung des Containers google.rpc.context.AttributeContext
versuchen die Typprüfung und der Evaluator die folgenden Kennungsnamen für alle Variablen, Typen und Funktionen:
google.rpc.context.AttributeContext.<id>
google.rpc.context.<id>
google.rpc.<id>
google.<id>
<id>
Bei absoluten Namen müssen Sie der Variablen, dem Typ oder der Funktionsreferenz einen führenden Punkt voranstellen. In diesem Beispiel sucht der Ausdruck .<id>
nur nach der Kennung <id>
der obersten Ebene, ohne zuvor den Container zu prüfen.
Versuchen Sie, die Option cel.Container("google.rpc.context.AttributeContext")
für die CEL-Umgebung anzugeben, und führen Sie sie noch einmal aus:
// exercise6 describes how to build proto message types within CEL.
//
// Given an input jwt and time now construct a
// google.rpc.context.AttributeContext.Request with the time and auth
// fields populated according to the go/api-attributes specification.
func exercise6() {
fmt.Println("=== Exercise 6: Building Protos ===\n")
// Construct an environment and indicate that the container for all references
// within the expression is `google.rpc.context.AttributeContext`.
requestType := &rpcpb.AttributeContext_Request{}
env, _ := cel.NewEnv(
// Add cel.Container() option for 'google.rpc.context.AttributeContext'
cel.Container("google.rpc.context.AttributeContext.Request"),
cel.Types(requestType),
// Later, add cel.Variable() options for 'jwt' as a map(string, Dyn) type
// and for 'now' as a timestamp.
// cel.Variable("now", cel.TimestampType),
// cel.Variable("jwt", cel.MapType(cel.StringType, cel.DynType)),
)
...
}
Sie sollten die folgende Ausgabe erhalten:
ERROR: <input>:4:16: undeclared reference to 'jwt' (in container 'google.rpc.context.AttributeContext')
| principal: jwt.iss + '/' + jwt.sub,
| ...............^
... und viele weitere Fehler...
Deklarieren Sie als Nächstes die Variablen jwt
und now
, damit das Programm wie erwartet ausgeführt werden sollte:
=== Exercise 6: Building Protos ===
Request{
auth: Auth{
principal: jwt.iss + '/' + jwt.sub,
audiences: [jwt.aud],
presenter: 'azp' in jwt ? jwt.azp : "",
claims: jwt
},
time: now
}
------ input ------
jwt = {
"aud": "my-project",
"extra_claims": {
"group": "admin"
},
"iss": "auth.acme.com:12350",
"sub": "serviceAccount:delegate@acme.co"
}
now = seconds: 1588993027
------ result ------
value: &{0xc000485270 0xc000274180 {0xa6ee80 0xc000274180 22} 0xc0004be140 0xc0002bf700 false} (*types.protoObj)
----- type unwrap ----
&{0xc000485270 0xc000274180 {0xa6ee80 0xc000274180 22} 0xc0004be140 0xc0002bf700 false}
Um zusätzliches Guthaben zu erhalten, sollten Sie den Typ auch entpacken, indem Sie out.Value()
für die Nachricht aufrufen, um zu sehen, wie sich das Ergebnis ändert.
11. Makros
Makros können verwendet werden, um das CEL-Programm beim Parsen zu bearbeiten. Makros stimmen mit einer Aufrufsignatur überein und bearbeiten den Eingabeaufruf und dessen Argumente, um eine neue Unterausdruck-AST zu erzeugen.
Makros können verwendet werden, um komplexe Logik in der AST zu implementieren, die nicht direkt in CEL geschrieben werden kann. Das has
-Makro ermöglicht beispielsweise Tests der Feldpräsenz. Die Verständnismakros, wie etwa die vorhandenen und alle, ersetzen einen Funktionsaufruf mit begrenzter Iteration über eine Eingabeliste oder Map. Keines der Konzepte ist auf syntaktischer Ebene möglich, durch Makroerweiterungen jedoch.
Fügen Sie die nächste Übung hinzu und führen Sie sie aus:
// exercise7 introduces macros for dealing with repeated fields and maps.
//
// Determine whether the jwt.extra_claims has at least one key that starts
// with the group prefix, and ensure that all group-like keys have list
// values containing only strings that end with '@acme.co`.
func exercise7() {
fmt.Println("=== Exercise 7: Macros ===\n")
env, _ := cel.NewEnv(
cel.ClearMacros(),
cel.Variable("jwt", cel.MapType(cel.StringType, cel.DynType)),
)
ast := compile(env,
`jwt.extra_claims.exists(c, c.startsWith('group'))
&& jwt.extra_claims
.filter(c, c.startsWith('group'))
.all(c, jwt.extra_claims[c]
.all(g, g.endsWith('@acme.co')))`,
cel.BoolType)
program, _ := env.Program(ast)
// Evaluate a complex-ish JWT with two groups that satisfy the criteria.
// Output: true.
eval(program,
map[string]interface{}{
"jwt": map[string]interface{}{
"sub": "serviceAccount:delegate@acme.co",
"aud": "my-project",
"iss": "auth.acme.com:12350",
"extra_claims": map[string][]string{
"group1": {"admin@acme.co", "analyst@acme.co"},
"labels": {"metadata", "prod", "pii"},
"groupN": {"forever@acme.co"},
},
},
})
fmt.Println()
}
Sie sollten die folgenden Fehler sehen:
ERROR: <input>:1:25: undeclared reference to 'c' (in container '')
| jwt.extra_claims.exists(c, c.startsWith('group'))
| ........................^
... und viele weitere ...
Diese Fehler treten auf, weil Makros noch nicht aktiviert sind. Entfernen Sie cel.ClearMacros()
und führen Sie den Befehl noch einmal aus, um Makros zu aktivieren:
=== Exercise 7: Macros ===
jwt.extra_claims.exists(c, c.startsWith('group'))
&& jwt.extra_claims
.filter(c, c.startsWith('group'))
.all(c, jwt.extra_claims[c]
.all(g, g.endsWith('@acme.co')))
------ input ------
jwt = {
"aud": "my-project",
"extra_claims": {
"group1": [
"admin@acme.co",
"analyst@acme.co"
],
"groupN": [
"forever@acme.co"
],
"labels": [
"metadata",
"prod",
"pii"
]
},
"iss": "auth.acme.com:12350",
"sub": "serviceAccount:delegate@acme.co"
}
------ result ------
value: true (types.Bool)
Derzeit werden die folgenden Makros unterstützt:
Macro | Unterschrift | Beschreibung |
Alle | r.all(var; cond) | Testen Sie, ob cond für alle Variablen im Bereich r „wahr“ ist. |
vorhanden | r.exists(var, cond) | Testen Sie, ob cond für eine beliebige Variable im Bereich r „wahr“ ist. |
exists_one | r.exists_one(var, cond) | Testen Sie, ob cond für nur eine Variable im Bereich r „wahr“ ist. |
Filter | r.filter(var; cond) | Erstellen Sie für Listen eine neue Liste, in der jedes Element var im Bereich r die Bedingung erfüllt. Erstellen Sie für Zuordnungen eine neue Liste, in der jede Schlüsselvariable im Bereich r die Bedingung "cond" erfüllt. |
Karte | r.map(var; expr) | Erstellen Sie eine neue Liste, in der jede Variable im Bereich r durch expr transformiert wird. |
r.map(var; cond; expr) | Entspricht der Zuordnung mit zwei Argumenten, aber mit einem bedingten cond-Filter vor der Transformation des Werts. | |
enthält | has(a.b) | Anwesenheitstest für „b“ bei Wert a : Definition von JSON für Karten. Testet bei Proto-Dateien einen nicht standardmäßigen primitiven Wert oder ein Nachrichtenfeld oder ein festgelegtes Nachrichtenfeld. |
Wenn das Bereichs-r-Argument ein map
-Typ ist, ist var
der Kartenschlüssel und für Werte vom Typ list
der Wert var
der Listenelementwert. Die Makros all
, exists
, exists_one
, filter
und map
führen eine AST-Umschreibung durch, die für jede Iteration, die durch die Größe der Eingabe begrenzt ist, durchgeführt wird.
Das begrenzte Verständnis stellt sicher, dass CEL-Programme nicht Turing-vollständig sind, aber sie werden in Bezug auf die Eingabe in superlinearer Zeit ausgewertet. Verwenden Sie diese Makros sparsam oder gar nicht. Ein intensives Verständnis, das in der Regel ein guter Indikator dafür ist, dass eine benutzerdefinierte Funktion eine bessere User Experience und bessere Leistung bieten würde.
12. Abstimmung
Es gibt einige Funktionen, die derzeit nur für CEL-Go verfügbar sind, aber ein Hinweis auf zukünftige Pläne für andere CEL-Implementierungen sind. Die folgende Übung zeigt unterschiedliche Programmpläne für dieselbe AST:
// exercise8 covers features of CEL-Go which can be used to improve
// performance and debug evaluation behavior.
//
// Turn on the optimization, exhaustive eval, and state tracking
// ProgramOption flags to see the impact on evaluation behavior.
func exercise8() {
fmt.Println("=== Exercise 8: Tuning ===\n")
// Declare the x and 'y' variables as input into the expression.
env, _ := cel.NewEnv(
cel.Variable("x", cel.IntType),
cel.Variable("y", cel.UintType),
)
ast := compile(env,
`x in [1, 2, 3, 4, 5] && type(y) == uint`,
cel.BoolType)
// Try the different cel.EvalOptions flags when evaluating this AST for
// the following use cases:
// - cel.OptOptimize: optimize the expression performance.
// - cel.OptExhaustiveEval: turn off short-circuiting.
// - cel.OptTrackState: track state and compute a residual using the
// interpreter.PruneAst function.
program, _ := env.Program(ast)
eval(program, cel.NoVars())
fmt.Println()
}
Optimize
Wenn das Optimierungs-Flag aktiviert ist, benötigt CEL zusätzliche Zeit, um Listen- und Mapliterale im Voraus zu erstellen und bestimmte Aufrufe, z. B. den in-Operator, für einen Mitgliedschaftstest mit echtem Satz zu optimieren:
// Turn on optimization.
trueVars := map[string]interface{}{"x": int64(4), "y": uint64(2)}
program, _ := env.Program(ast, cel.EvalOptions(cel.OptOptimize))
// Try benchmarking with the optimization flag on and off.
eval(program, trueVars)
Wenn dasselbe Programm mehrmals auf verschiedene Eingaben hin ausgewertet wird, ist die Optimierung eine gute Wahl. Wenn das Programm jedoch nur einmal evaluiert wird, erhöht die Optimierung lediglich den Aufwand.
Erschöpfende Auswertung
Die Exhaustive Eval-Phase kann beim Debuggen des Ausdrucksbewertungsverhaltens hilfreich sein, da sie einen Einblick in den beobachteten Wert in jedem Schritt der Ausdrucksauswertung bietet.
// Turn on exhaustive eval to see what the evaluation state looks like.
// The input is structure to show a false on the first branch, and true
// on the second.
falseVars := map[string]interface{}{"x": int64(6), "y": uint64(2)}
program, _ = env.Program(ast, cel.EvalOptions(cel.OptExhaustiveEval))
eval(program, falseVars)
Sie sollten eine Liste des Ausdrucksbewertungsstatus für jede Ausdrucks-ID sehen:
------ eval states ------
1: 6 (types.Int)
2: false (types.Bool)
3: &{0xc0000336b0 [1 2 3 4 5] {0x89f020 0xc000434760 151}} (*types.baseList)
4: 1 (types.Int)
5: 2 (types.Int)
6: 3 (types.Int)
7: 4 (types.Int)
8: 5 (types.Int)
9: uint (*types.Type)
10: 2 (types.Uint)
11: true (types.Bool)
12: uint (*types.Type)
13: false (types.Bool)
Ausdrucks-ID 2 entspricht dem Ergebnis des in-Operators im ersten. und die Ausdrucks-ID 11 dem Operator == in der zweiten Zeile entspricht. Bei normaler Auswertung hätte der Ausdruck einen Kurzschluss erhalten, nachdem 2 berechnet worden wäre. Wenn y nicht uint gewesen wäre, hätte der Status zwei Gründe für den Fehler des Ausdrucks gezeigt – nicht nur einen.
13. Was wurde abgedeckt?
Wenn Sie eine Ausdrucks-Engine benötigen, sollten Sie CEL verwenden. CEL ist ideal für Projekte, bei denen eine Nutzerkonfiguration ausgeführt werden muss, wenn die Leistung entscheidend ist.
In den vorherigen Übungen haben Sie sich damit vertraut gemacht, dass Sie Ihre Daten an CEL übergeben und die Ausgabe oder Entscheidung zurückerhalten.
Wir hoffen, dass du ein Gespür für die möglichen Operationen hast, von einer booleschen Entscheidung bis zum Generieren von JSON- und Protobuffer-Nachrichten.
Wir hoffen, dass du einen Eindruck davon hast, wie du mit den Ausdrücken umgehst und was sie bewirken. Und wir kennen gängige Möglichkeiten, diese zu erweitern.