CEL-Go-Codelab: Schnelle, sichere, eingebettete Ausdrücke

1. Einführung

Was ist CEL?

CEL ist eine Ausdruckssprache ohne Turing-Vollständigkeit, die schnell, portabel und sicher auszuführen ist. CEL kann eigenständig oder in ein größeres Produkt eingebettet verwendet werden.

CEL wurde als Sprache entwickelt, in der es sicher ist, Nutzercode auszuführen. Es ist gefährlich, eval() blind für den Python-Code eines Nutzers aufzurufen. Sie können den CEL-Code eines Nutzers jedoch sicher ausführen. Da CEL Verhalten verhindert, das die Leistung beeinträchtigen würde, wird es sicher im Bereich von Nanosekunden bis Mikrosekunden ausgewertet. Es ist also ideal für leistungsrelevante Anwendungen.

CEL wertet Ausdrücke aus, die mit einzeiligen Funktionen oder Lambda-Ausdrücken vergleichbar sind. CEL wird zwar häufig für boolesche Entscheidungen verwendet, kann aber auch zum Erstellen komplexerer Objekte wie JSON- oder Protobuf-Nachrichten verwendet werden.

Eignet sich CEL für Ihr Projekt?

Da CEL einen Ausdruck aus dem AST in Nanosekunden bis Mikrosekunden auswertet, sind die idealen Anwendungsfälle für CEL Anwendungen mit leistungskritischen Pfaden. Die Kompilierung von CEL-Code in den 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.

Die Ausführung einer Sicherheitsrichtlinie bei jeder HTTP-Anfrage an einen Dienst ist beispielsweise ein idealer Anwendungsfall für CEL, da sich die Sicherheitsrichtlinie selten ändert und CEL nur einen geringen Einfluss auf die Reaktionszeit hat. In diesem Fall gibt CEL einen booleschen Wert zurück, der angibt, ob die Anfrage zulässig ist oder nicht. Es könnte aber auch eine komplexere Meldung zurückgegeben werden.

Inhalt dieses Codelabs

Im ersten Schritt dieses Codelabs wird die Motivation für die Verwendung von CEL und den Kernkonzepten erläutert. Der Rest ist Übungen gewidmet, die häufige Anwendungsfälle abdecken. Eine ausführlichere Beschreibung der Sprache, Semantik und Funktionen finden Sie in der CEL-Sprachdefinition auf GitHub und in der CEL Go-Dokumentation.

Dieses Codelab richtet sich an Entwickler, die CEL lernen möchten, um Dienste zu verwenden, die CEL bereits unterstützen. In diesem Codelab wird nicht beschrieben, wie Sie CEL in Ihr eigenes Projekt einbinden.

Lerninhalte

  • Wichtige Konzepte aus CEL
  • „Hello, World“: Einen String mit CEL auswerten
  • Variablen erstellen
  • Short-Circuiting in CEL bei logischen AND/OR-Operationen
  • JSON mit CEL erstellen
  • Protobufs mit CEL erstellen
  • Makros erstellen
  • Möglichkeiten zum Optimieren von CEL-Ausdrücken

Voraussetzungen

Voraussetzungen

Dieses Codelab baut auf einem grundlegenden Verständnis von Protocol Buffers und Go auf.

Wenn Sie mit Protocol Buffers nicht vertraut sind, erhalten Sie in der ersten Übung einen Eindruck davon, wie CEL funktioniert. Da in den komplexeren Beispielen Protocol Buffers als Eingabe für CEL verwendet werden, sind sie möglicherweise schwieriger zu verstehen. Sehen Sie sich zuerst eine dieser Anleitungen an. Hinweis: Protocol Buffers sind für die Verwendung von CEL nicht erforderlich, werden aber in diesem Codelab häufig verwendet.

Sie können testen, ob Go installiert ist, indem Sie Folgendes ausführen:

go --help

2. Wichtige Konzepte

Anwendungen

CEL ist universell einsetzbar und wird für verschiedene Anwendungen verwendet, vom Routing von RPCs bis zur Definition von Sicherheitsrichtlinien. CEL ist erweiterbar, anwendungsunabhängig und für Workflows optimiert, bei denen einmal kompiliert und dann mehrfach ausgewertet wird.

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 Rolle und einer Gruppe von Nutzern eine Zugriffsentscheidung trifft. Wenn deklarative Konfigurationen die 80% der Anwendungsfälle abdecken, ist CEL ein nützliches Tool, um die verbleibenden 20% abzudecken, wenn Nutzer mehr Ausdruckskraft benötigen.

Compilation

Ein Ausdruck wird für eine Umgebung kompiliert. Im Kompilierungsschritt wird ein abstrakter Syntaxbaum (Abstract Syntax Tree, AST) im Protobuf-Format erstellt. 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, Dienste und Anwendungen definieren die Umgebung, in der sie ausgeführt werden. In einer Funktionssignatur werden die Eingaben deklariert. Sie wird außerhalb des CEL-Ausdrucks geschrieben. Die für CEL verfügbare Funktionsbibliothek wird automatisch importiert.

Im folgenden Beispiel wird ein Anfrageobjekt verwendet, das ein Anspruchstoken enthält. Der Ausdruck gibt einen booleschen Wert zurück, der angibt, ob das Anspruchstoken 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, in die CEL eingebettet ist, deklarieren die Ausdrucksumgebung. Die Umgebung ist die Sammlung von Variablen und Funktionen, die in Ausdrücken verwendet werden können.

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

Drei Phasen des Parsens eines Ausdrucks

Die Verarbeitung eines Ausdrucks umfasst drei Phasen: Parsen, Prüfen und Auswerten. Das gängigste Muster für CEL besteht darin, dass eine Steuerungsebene Ausdrücke zur Konfigurationszeit parst und prüft und den AST speichert.

c71fc08068759f81.png

Zur Laufzeit ruft die Datenebene das AST wiederholt ab und wertet es aus. CEL ist für Laufzeiteffizienz optimiert, das Parsen und Prüfen sollte jedoch nicht in latenzkritischen Codepfaden erfolgen.

49ab7d8517143b66.png

CEL wird aus einem für Menschen lesbaren Ausdruck in einen abstrakten Syntaxbaum geparst. Dazu wird eine ANTLR-Lexer-/Parsergrammatik verwendet. In der Parsing-Phase wird ein prototypbasierter abstrakter Syntaxbaum 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 Überprüfens generiert werden. Die während des Parsens erstellte Datei syntax.proto stellt die abstrakte Darstellung der Eingabe in der Stringform des Ausdrucks genau dar.

Nachdem ein Ausdruck geparst wurde, kann er mit der Umgebung abgeglichen werden, um sicherzustellen, dass alle Variablen- und Funktionsbezeichner im Ausdruck deklariert sind und korrekt verwendet werden. Der Type-Checker generiert eine checked.proto, die Metadaten zur Auflösung von Typen, Variablen und Funktionen enthält. Dadurch kann die Effizienz der Auswertung erheblich gesteigert werden.

Für die CEL-Auswertung sind drei Dinge erforderlich:

  • 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. So kann beispielsweise ein AST für viele Gruppen von Variablenbindungen ausgewertet werden oder dieselben Variablen können für viele ASTs verwendet werden. Auch die Funktionsbindungen können während der gesamten Lebensdauer eines Prozesses verwendet werden (ein häufiger Fall).

3. Erstelle ein

Der Code für dieses Codelab befindet sich im codelab-Ordner des cel-go-Repositorys. Die Lösung ist im codelab/solution-Ordner desselben Repositorys verfügbar.

Klonen Sie das Repository und wechseln Sie in das Repository:

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 finde ich 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 erste Gruppe von Helfern unterstützt bei den Phasen der CEL-Bewertung:

  • Compile-Funktion: Analysiert und prüft einen Eingabeausdruck anhand einer Umgebung.
  • Eval-Funktion: Wertet ein kompiliertes Programm anhand einer Eingabe aus.
  • Report-Funktion: Gibt das Bewertungsergebnis in einem lesbaren Format aus.

Außerdem wurden die Hilfsprogramme request und auth bereitgestellt, um die Eingabeerstellung für die verschiedenen Übungen zu erleichtern.

In den Übungen wird auf Pakete mit ihrem kurzen Paketnamen verwiesen. Die Zuordnung von Paket zu Quellspeicherort im google/cel-go-Repository finden Sie unten:

Paket

Quellort

Beschreibung

cel

cel-go/cel

Schnittstellen der obersten Ebene

Ref

cel-go/common/types/ref

Referenzschnittstellen

Typen

cel-go/common/types

Werte für den Laufzeittyp

4. Hallo Welt!

Wie bei allen Programmiersprachen beginnen wir mit dem Erstellen und Auswerten von „Hallo Welt!“.

Umgebung konfigurieren

Suchen Sie im 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
}

Bei CEL-Anwendungen wird ein Ausdruck für eine Umgebung ausgewertet. Mit env, err := cel.NewEnv() wird die Standardumgebung konfiguriert.

Die Umgebung kann angepasst werden, indem die Optionen cel.EnvOption für den Aufruf angegeben werden. Mit diesen Optionen können Makros deaktiviert, benutzerdefinierte Variablen und Funktionen deklariert usw. werden.

Die Standard-CEL-Umgebung unterstützt alle Typen, Operatoren, Funktionen und Makros, die in der Sprachspezifikation definiert sind.

Ausdruck parsen und prüfen

Nachdem die Umgebung konfiguriert wurde, können Ausdrücke geparst und geprüft werden. Fügen Sie Ihrer 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 von den Aufrufen Parse und Check zurückgegebene Wert iss ist eine Liste von Problemen, die Fehler sein könnten. Wenn iss.Err() nicht nil ist, liegt ein Fehler in der Syntax oder Semantik vor und das Programm kann nicht fortgesetzt werden. Wenn der Ausdruck wohlgeformt ist, ist das Ergebnis dieser Aufrufe eine ausführbare cel.Ast.

Ausdruck berechnen

Sobald der Ausdruck geparst und in ein cel.Ast eingecheckt wurde, kann er in ein auswertbares Programm umgewandelt werden, dessen Funktionsbindungen und Auswertungsmodi mit funktionalen Optionen angepasst werden können. Hinweis: Es ist auch möglich, ein cel.Ast aus einem Proto zu lesen, indem Sie entweder die Funktion cel.CheckedExprToAst oder cel.ParsedExprToAst verwenden.

Sobald ein cel.Program geplant ist, kann es anhand der Eingabe ausgewertet werden, indem Eval aufgerufen wird. Das Ergebnis von Eval enthält das Ergebnis, die Bewertungsdetails und den Fehlerstatus.

Fügen Sie die Planung und den Anruf von eval 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)
    }
    // 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()
}

Aus Gründen der Übersichtlichkeit werden wir die oben genannten Fehlerfälle in zukünftigen Übungen weglassen.

Code ausführen

Führen Sie den Code in der Befehlszeile noch einmal aus:

go run .

Sie sollten die folgende Ausgabe sehen, zusammen mit Platzhaltern für die zukünftigen Übungen.

=== Exercise 1: Hello World ===

------ input ------
(interpreter.emptyActivation)

------ result ------
value: Hello, World! (types.String)

5. Variablen in einer Funktion verwenden

In den meisten CEL-Anwendungen werden Variablen deklariert, auf die in Ausdrücken verwiesen werden kann. Variablendeklarationen geben einen Namen und einen Typ an. Der Typ einer Variablen kann entweder ein integrierter CEL-Typ, ein bekannter Protokollpuffertyp oder ein beliebiger Protokollpuffernachrichtentyp sein, sofern sein Deskriptor auch für CEL bereitgestellt wird.

Funktion hinzufügen

Suchen Sie im 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()
}

Fehler nachvollziehen und noch einmal ausführen

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'
 | ^

Der Type-Checker gibt einen Fehler für das Anfrageobjekt aus, der praktischerweise 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:

// 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()
}

Fehler nachvollziehen und noch einmal ausführen

Programm noch einmal ausführen:

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'
 | .......^

Wenn Sie Variablen verwenden möchten, die auf Protobuf-Nachrichten verweisen, muss der Type-Checker auch den Typdeskriptor 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()
}

Der Lauf wurde erfolgreich wiederholt.

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)

Wir haben gerade eine Variable für einen Fehler deklariert, ihr einen Typdeskriptor zugewiesen und dann in der Auswertung des Ausdrucks auf die Variable verwiesen.

7. Logisches UND/ODER

Eine der einzigartigen Funktionen von CEL ist die Verwendung kommutativer logischer Operatoren. Auf beiden Seiten eines bedingten Zweigs kann die Auswertung abgekürzt werden, auch wenn Fehler oder unvollständige Eingaben vorliegen.

Mit anderen Worten: CEL findet eine Auswertungsreihenfolge, die nach Möglichkeit ein Ergebnis liefert, und ignoriert Fehler oder sogar fehlende Daten, die in anderen Auswertungsreihenfolgen auftreten können. Anwendungen können sich auf diese Eigenschaft verlassen, um die Kosten für die Auswertung zu minimieren. Das Erfassen teurer Eingaben wird aufgeschoben, wenn ein Ergebnis ohne sie erreicht werden kann.

Wir fügen ein AND/OR-Beispiel hinzu und probieren es dann mit verschiedenen Eingaben aus, um zu sehen, wie die CEL-Auswertung abgekürzt wird.

Funktion erstellen

Fügen Sie in Ihrem Editor die folgenden Inhalte zu Übung 3 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 OR-Anweisung ein, die „true“ zurückgibt, wenn der Nutzer entweder Mitglied der Gruppe admin ist oder eine bestimmte E‑Mail-Kennung 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 schließlich den eval-Fall hinzu, in dem der Nutzer mit einem leeren Anspruchssatz ausgewertet 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("user:me@acme.co", emptyClaims), time.Now()))
}

Code mit dem leeren Anspruchssatz ausführen

Wenn Sie das Programm noch einmal ausführen, sollte die folgende neue Ausgabe angezeigt werden:

=== 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)

Bewertungsanfrage aktualisieren

Aktualisieren Sie als Nächstes den Testlauf, um ein anderes Hauptkonto mit dem leeren Anspruchssatz zu übergeben:

// 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 einer Zeitangabe ausführen

Das Programm wird noch einmal ausgeführt.

go run .

Es 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 wissen wir nicht, ob ein Schlüssel vorhanden ist. Da es keinen sicheren Standardwert für einen fehlenden Schlüssel gibt, wird in CEL standardmäßig ein Fehler ausgegeben.

8. Benutzerdefinierte Funktionen

CEL enthält zwar viele integrierte Funktionen, aber manchmal ist eine benutzerdefinierte Funktion nützlich. Benutzerdefinierte Funktionen können beispielsweise verwendet werden, um die Nutzerfreundlichkeit bei häufigen Bedingungen zu verbessern oder kontextbezogene Statusinformationen bereitzustellen.

In dieser Übung sehen wir uns an, wie Sie eine Funktion bereitstellen, um häufig verwendete Prüfungen zu bündeln.

Benutzerdefinierte Funktion aufrufen

Erstellen Sie zuerst den Code zum Einrichten einer Überschreibung mit dem Namen contains, mit der festgelegt wird, ob ein Schlüssel in einer Karte vorhanden ist und einen bestimmten Wert hat. Lassen Sie Platzhalter für die Funktionsdefinition und die Funktionsbindung:

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

Wenn Sie den Code noch einmal ausführen, sollte der folgende Fehler angezeigt werden:

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 der Liste der Deklarationen hinzufügen, in der derzeit die Anforderungsvariable deklariert wird.

Deklarieren Sie einen parametrisierten Typ, indem Sie die folgenden drei Zeilen hinzufügen. (So kompliziert wie jede Funktionsüberladung für CEL)

// 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ühren Sie die Übung aus. Es sollte der folgende Fehler zur fehlenden Laufzeitfunktion angezeigt werden:

------ result ------
error: no such overload: contains

Stellen Sie die Funktionsimplementierung für die NewEnv-Deklaration mit 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 jetzt 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 besteht?

Als zusätzliche Aufgabe können Sie die Admin-Anforderung für die Eingabe festlegen, um zu prüfen, ob die Überladung „contains“ auch dann „true“ zurückgibt, wenn die Anforderung 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 Funktion mapContainsKeyValue 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 so einfach wie möglich zu gestalten, werden für die Signatur benutzerdefinierter Funktionen Argumente vom Typ ref.Val erwartet. Der Vorteil der einfachen Erweiterung ist, dass der Implementierer dafür sorgen muss, dass alle Werttypen richtig verarbeitet werden. Wenn die Typen oder die Anzahl der Eingabeargumente nicht mit der Funktionsdeklaration übereinstimmen, sollte ein no such overload-Fehler zurückgegeben werden.

Mit cel.FunctionBinding() wird ein Laufzeittyp-Guard hinzugefügt, um sicherzustellen, dass der Laufzeitvertrag mit der typgeprüften Deklaration in der Umgebung übereinstimmt.

9. JSON erstellen

CEL kann auch nicht-boolesche Ausgaben wie JSON erzeugen. Fügen Sie Ihrer 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, sollte der folgende Fehler angezeigt werden:

ERROR: <input>:5:11: undeclared reference to 'now' (in container '')
 |    'iat': now,
 | ..........^
... and more ...

Fügen Sie eine Deklaration für die Variable now vom Typ cel.TimestampType zu cel.NewEnv() hinzu und führen Sie den Code 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 jetzt funktionieren:

=== 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 in JSON konvertierbar, da sie sich nur auf Typen bezieht, die von JSON unterstützt werden oder für die eine bekannte Proto-zu-JSON-Zuordnung vorhanden ist.

// 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, sollte die folgende zusätzliche Ausgabe angezeigt werden:

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

Mit CEL können Protobuf-Nachrichten für jeden Nachrichtentyp erstellt werden, der in die Anwendung kompiliert wurde. Fügen Sie die Funktion hinzu, um ein google.rpc.context.AttributeContext.Request aus einer Eingabe jwt zu erstellen.

// 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, sollte der folgende Fehler angezeigt werden:

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-Nachrichtennamen sein. Für CEL-Container gelten dieselben Regeln für die Namespace-Auflösung wie für Protobuf und C++, um zu ermitteln, wo eine bestimmte Variable, Funktion oder ein bestimmter Typname deklariert wird.

Angesichts des Containers google.rpc.context.AttributeContext versuchen der Type-Checker und der Evaluator die folgenden Bezeichnernamen 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 stellen Sie der Variablen-, Typ- oder Funktionsreferenz einen Punkt voran. Im Beispiel wird mit dem Ausdruck .<id> nur nach der <id>-Kennung der obersten Ebene gesucht, ohne zuerst im Container zu suchen.

Versuchen Sie, die Option cel.Container("google.rpc.context.AttributeContext") für die CEL-Umgebung anzugeben und den Befehl noch einmal auszuführen:

// 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. Das Programm sollte dann wie erwartet ausgeführt werden:

=== 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}

Als zusätzliche Aufgabe 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

Mit Makros kann das CEL-Programm zur Parse-Zeit bearbeitet werden. Makros entsprechen einer Aufrufsignatur und bearbeiten den Eingabeaufruf und seine Argumente, um einen neuen AST-Unterausdruck zu erzeugen.

Mit Makros lässt sich komplexe Logik im AST implementieren, die nicht direkt in CEL geschrieben werden kann. Mit dem Makro has lässt sich beispielsweise prüfen, ob ein Feld vorhanden ist. Die Makros für das Verständnis wie „exists“ und „all“ ersetzen einen Funktionsaufruf durch eine begrenzte Iteration über eine Eingabeliste oder ‑zuordnung. Keines der beiden Konzepte ist auf syntaktischer Ebene möglich, aber durch Makroerweiterungen.

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()
}

Es sollten die folgenden Fehler angezeigt werden:

ERROR: <input>:1:25: undeclared reference to 'c' (in container '')
 | jwt.extra_claims.exists(c, c.startsWith('group'))
 | ........................^

… und vieles mehr.

Diese Fehler treten auf, weil Makros noch nicht aktiviert sind. Wenn Sie Makros aktivieren möchten, entfernen Sie cel.ClearMacros() und führen Sie den Befehl noch einmal aus:

=== 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)

Folgende Makros werden derzeit unterstützt:

Macro

Unterschrift

Beschreibung

Alle

r.all(var, cond)

Testet, ob „cond“ für alle „var“ im Bereich „r“ als „true“ ausgewertet wird.

vorhanden

r.exists(var, cond)

Prüft, ob „cond“ für eine beliebige Variable im Bereich „r“ als „true“ ausgewertet wird.

exists_one

r.exists_one(var, cond)

Testet, ob „cond“ für nur eine Variable im Bereich „r“ als „true“ ausgewertet wird.

filtern

r.filter(var, cond)

Erstellt für Listen eine neue Liste, in der jedes Element „var“ im Bereich „r“ die Bedingung „cond“ erfüllt. Erstellt für Karten eine neue Liste, in der jede Schlüsselvariablen im Bereich „r“ die Bedingung „cond“ erfüllt.

Karte

r.map(var, expr)

Erstellt eine neue Liste, in der jede Variable im Bereich „r“ durch „expr“ transformiert wird.

r.map(var, cond, expr)

Wie bei der Map-Funktion mit zwei Argumenten, aber mit einem bedingten „cond“-Filter, bevor der Wert transformiert wird.

enthält

has(a.b)

Anwesenheitstest für „b“ für Wert „a“: Für Karten, JSON-Testdefinition. Für Protos wird ein nicht standardmäßiger einfacher Wert oder ein oder ein festgelegtes Nachrichtenfeld getestet.

Wenn das Argument „range“ (Bereich) vom Typ map ist, ist var der Kartenschlüssel. Bei Werten vom Typ list ist var der Wert des Listenelements. Die Makros all, exists, exists_one, filter und map führen eine AST-Umschreibung durch, die eine For-Each-Iteration ausführt, die durch die Größe der Eingabe begrenzt wird.

Die eingeschränkten List Comprehensions sorgen dafür, dass CEL-Programme nicht Turing-vollständig sind, aber sie werden in superlinearer Zeit in Bezug auf die Eingabe ausgewertet. Verwenden Sie diese Makros nur sparsam oder gar nicht. Eine intensive Nutzung von Comprehensions ist in der Regel ein guter Hinweis darauf, dass eine benutzerdefinierte Funktion eine bessere Nutzerfreundlichkeit und Leistung bieten würde.

12. Abstimmung

Es gibt einige Funktionen, die derzeit nur in CEL-Go verfügbar sind, aber auf zukünftige Pläne für andere CEL-Implementierungen hinweisen. In der folgenden Übung werden verschiedene Programmpläne für denselben AST vorgestellt:

// 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 Optimierungsflag aktiviert ist, verwendet CEL mehr Zeit, um Listen- und Map-Literale im Voraus zu erstellen und bestimmte Aufrufe wie den „in“-Operator zu optimieren, damit sie einen echten Test auf Mitgliedschaft in einer Menge darstellen:

    // 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 viele Male mit unterschiedlichen Eingaben ausgewertet wird, ist die Optimierung eine gute Wahl. Wenn das Programm jedoch nur einmal ausgewertet werden soll, führt die Optimierung nur zu zusätzlichem Aufwand.

Exhaustive Eval

Die umfassende Auswertung kann nützlich sein, um das Verhalten bei der Auswertung von Ausdrücken zu debuggen, da sie 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 mit dem Status der Auswertung des Ausdrucks 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)

Die Ausdrucks-ID 2 entspricht dem Ergebnis des IN-Operators im ersten Zweig und die Ausdrucks-ID 11 dem ==-Operator im zweiten Zweig. Bei der normalen Auswertung wäre der Ausdruck nach der Berechnung von „2“ abgekürzt worden. Wenn „y“ nicht „uint“ gewesen wäre, hätte der Status zwei Gründe für das Fehlschlagen des Ausdrucks angezeigt und nicht nur einen.

13. Was wurde behandelt?

Wenn Sie eine Ausdrucks-Engine benötigen, sollten Sie CEL verwenden. CEL ist ideal für Projekte, in denen die Nutzerkonfiguration ausgeführt werden muss und die Leistung entscheidend ist.

In den vorherigen Übungen haben Sie hoffentlich gelernt, wie Sie Ihre Daten in CEL übergeben und die Ausgabe oder Entscheidung zurückerhalten.

Wir hoffen, dass Sie eine Vorstellung davon haben, welche Art von Vorgängen Sie ausführen können, von einer booleschen Entscheidung bis hin zum Generieren von JSON- und Protobuffer-Nachrichten.

Wir hoffen, dass Sie jetzt eine Vorstellung davon haben, wie Sie mit den Ausdrücken arbeiten und was sie bewirken. Wir kennen gängige Möglichkeiten, die Lebensdauer zu verlängern.