Codelab su CEL-Go: espressioni rapide, sicure e incorporate

1. Introduzione

Che cos'è CEL?

CEL è un linguaggio di espressione completo non Turing progettato per essere veloce, portabile e sicuro da eseguire. La tecnologia CEL può essere utilizzata da sola o incorporata in un prodotto più grande.

Il linguaggio CEL è stato progettato come linguaggio in cui è sicuro eseguire il codice utente. Anche se è pericoloso chiamare ciecamente eval() sul codice Python di un utente, puoi eseguire in sicurezza il codice CEL di un utente. Inoltre, poiché la tecnologia CEL impedisce un comportamento che lo renderebbe meno performante, esegue una valutazione sicura nell'ordine dei nanosecondi in microsecondi; ed è ideale per applicazioni critiche per le prestazioni.

CEL valuta le espressioni, che sono simili alle funzioni a linea singola o alle espressioni lambda. Sebbene il linguaggio CEL sia comunemente utilizzato per le decisioni booleane, può essere utilizzato anche per costruire oggetti più complessi come messaggi JSON o protobuf.

La tecnologia CEL è adatta al tuo progetto?

Poiché CEL valuta un'espressione dell'AST in nanosecondi o microsecondi, il caso d'uso ideale per CEL sono applicazioni con percorsi critici per le prestazioni. La compilazione del codice CEL nell'AST non deve avvenire in percorsi critici; le applicazioni ideali sono quelle in cui la configurazione viene eseguita spesso e modificata relativamente raramente.

Ad esempio, l'esecuzione di un criterio di sicurezza con ciascuna richiesta HTTP a un servizio è un caso d'uso ideale per CEL, perché il criterio di sicurezza cambia raramente e CEL avrà un impatto trascurabile sul tempo di risposta. In questo caso, CEL restituisce un valore booleano, se la richiesta deve essere consentita o meno, ma potrebbe restituire un messaggio più complesso.

Argomenti trattati in questo codelab

Il primo passaggio di questo codelab illustra la motivazione per l'utilizzo di CEL e i suoi concetti principali. Il resto è dedicato agli allenamenti di programmazione che coprono casi d'uso comuni. Per uno sguardo più approfondito su linguaggio, semantica e funzionalità, consulta la pagina CEL Language Definition su GitHub e la documentazione CEL Go.

Questo codelab è rivolto agli sviluppatori che vorrebbero imparare il linguaggio CEL per usare servizi che supportano già questa tecnologia. Questo codelab non illustra come integrare CEL nel tuo progetto.

Cosa imparerai a fare

  • Concetti principali di CEL
  • Hello, World: utilizzo della tecnologia CEL per valutare una stringa
  • Creazione delle variabili
  • Comprensione del cortocircuito delle soluzioni CEL nelle operazioni logiche AND/OR
  • Come utilizzare CEL per creare JSON
  • Come utilizzare CEL per creare protobuffer
  • Creazione di macro
  • Modi per ottimizzare le espressioni CEL

Che cosa ti serve

Prerequisiti

Questo codelab si basa su una conoscenza di base di Buffer di protocollo e Go Lang.

Se non hai familiarità con i buffer di protocollo, il primo esercizio ti darà un'idea di come funziona CEL, ma poiché gli esempi più avanzati utilizzano i buffer di protocollo come input in CEL, potrebbero essere più difficili da comprendere. Innanzitutto, valuta la possibilità di seguire uno di questi tutorial. Tieni presente che i buffer di protocollo non sono necessari per usare CEL, ma sono ampiamente utilizzati in questo codelab.

Puoi verificare che l'app sia installata eseguendo:

go --help

2. Concetti fondamentali

Applicazioni

La tecnologia CEL è per uso generico ed è stata utilizzata per diverse applicazioni, dal routing delle RPC alla definizione dei criteri di sicurezza. CEL è estensibile, indipendente dall'applicazione e ottimizzato per i flussi di lavoro compile-una volta, "valuta molti".

Molti servizi e applicazioni valutano le configurazioni dichiarative. Ad esempio, il controllo degli accessi basato su ruoli (RBAC) è una configurazione dichiarativa che produce una decisione di accesso dato un ruolo e un insieme di utenti. Se le configurazioni dichiarative sono il caso d'uso dell'80%, CEL è uno strumento utile per arrotondare il restante 20% quando gli utenti hanno bisogno di una potenza più espressiva.

Compilation

Viene compilata un'espressione in base a un ambiente. Il passaggio di compilazione produce un Abstract Syntax Tree (AST) in formato protobuf. In genere, le espressioni compilate vengono archiviate per uso futuro, in modo da garantire la velocità della valutazione. Una singola espressione compilata può essere valutata con molti input diversi.

Espressioni

Gli utenti definiscono espressioni; e applicazioni definiscono l'ambiente di esecuzione. Una firma di funzione dichiara gli input ed è scritta al di fuori dell'espressione CEL. La libreria di funzioni disponibili per CEL viene importata automaticamente.

Nell'esempio seguente, l'espressione prende un oggetto della richiesta e la richiesta include un token delle attestazioni. L'espressione restituisce un valore booleano che indica se il token delle attestazioni è ancora valido.

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

Ambiente

Gli ambienti sono definiti dai servizi. I servizi e le applicazioni che incorporano CEL dichiarano l'ambiente di espressione. L'ambiente è l'insieme di variabili e funzioni che possono essere utilizzate nelle espressioni.

Le dichiarazioni basate su proto vengono utilizzate dal controllo tipi CEL per garantire che tutti gli identificatori e i riferimenti alle funzioni all'interno di un'espressione vengano dichiarati e utilizzati correttamente.

Tre fasi di analisi di un'espressione

L'elaborazione di un'espressione prevede tre fasi: analisi, verifica e valutazione. Il pattern più comune per CEL consiste nell'analizzare e verificare le espressioni in fase di configurazione da un piano di controllo e archiviare l'AST.

c71fc08068759f81.png

In fase di runtime, il piano dati recupera e valuta l'AST ripetutamente. La tecnologia CEL è ottimizzata per l'efficienza del runtime, ma l'analisi e il controllo non devono essere eseguiti nei percorsi di codice critici per la latenza.

49ab7d8517143b66.png

Il linguaggio CEL viene analizzato da un'espressione leggibile a un albero di sintassi astratta utilizzando una grammatica lexer / analizzatore sintattico ANTLR. La fase di analisi emette un albero di sintassi astratta basato su un protocollo in cui ogni nodo Expr in AST contiene un ID intero che viene utilizzato per indicizzare i metadati generati durante l'analisi e il controllo. Il valore syntax.proto generato durante l'analisi rappresenta fedelmente la rappresentazione astratta di ciò che è stato digitato sotto forma di stringa dell'espressione.

Una volta analizzata un'espressione, è possibile confrontarla con l'ambiente per garantire che tutti gli identificatori di variabili e funzioni nell'espressione siano stati dichiarati e utilizzati correttamente. Il controllo dei tipi produce un valore checked.proto che include metadati di risoluzione di tipi, variabili e funzioni che possono migliorare drasticamente l'efficienza della valutazione.

Il valutatore CEL ha bisogno di tre elementi:

  • Associazioni di funzioni per eventuali estensioni personalizzate
  • Associazioni di variabili
  • Un AST da valutare

Le associazioni di funzioni e variabili devono corrispondere a quanto utilizzato per la compilazione dell'AST. Ognuno di questi input può essere riutilizzato in più valutazioni, ad esempio un AST che viene valutato in molti insiemi di associazioni di variabili, le stesse variabili utilizzate per molti AST o le associazioni di funzioni utilizzate per tutta la durata di un processo (un caso comune).

3. Configura

Il codice di questo codelab si trova nella cartella codelab del repository cel-go. La soluzione è disponibile nella cartella codelab/solution dello stesso repository.

Clona e invia un cd nel repository:

git clone https://github.com/google/cel-go.git 
cd cel-go/codelab

Esegui il codice utilizzando go run:

go run .

Dovresti vedere l'output seguente:

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

Dove si trovano i pacchetti CEL?

Nell'editor, apri codelab/codelab.go. Dovresti vedere la funzione principale che guida l'esecuzione degli esercizi in questo codelab, seguita da tre blocchi di funzioni helper. Il primo gruppo di assistenti fornisce assistenza nelle fasi della valutazione CEL:

  • Funzione Compile: analizza e controlla ed esegue l'espressione di input in base a un ambiente
  • Funzione Eval: valuta un programma compilato in base a un input
  • Funzione Report: stampa semplicemente il risultato della valutazione

Inoltre, gli assistenti request e auth sono stati forniti per aiutarti nella creazione di input per i vari esercizi.

Gli esercizi faranno riferimento ai pacchetti mediante il loro nome breve. Di seguito è riportata la mappatura del pacchetto alla posizione di origine all'interno del repository google/cel-go se vuoi approfondire i dettagli:

Pacchetto

Posizione di origine

Descrizione

cel

cel-go/cel

Interfacce di primo livello

riferimento

cel-go/common/types/ref

Interfacce di riferimento

tipi

cel-go/common/types

Valori del tipo di runtime

4. Un saluto da Google!

Nella tradizione di tutti i linguaggi di programmazione, inizieremo creando e valutando "Hello World!".

configura l'ambiente

Nell'editor, trova la dichiarazione di exercise1 e compila quanto segue per configurare l'ambiente:

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

Le applicazioni CEL valutano un'espressione rispetto a un ambiente. env, err := cel.NewEnv() configura l'ambiente standard.

L'ambiente può essere personalizzato fornendo le opzioni cel.EnvOption per la chiamata. Queste opzioni consentono di disattivare le macro, dichiarare funzioni e variabili personalizzate e così via.

L'ambiente CEL standard supporta tutti i tipi, gli operatori, le funzioni e le macro definiti nella specifica della lingua.

Analizza e controlla l'espressione

Una volta configurato l'ambiente, è possibile analizzare e controllare le espressioni. Aggiungi il seguente codice alla funzione:

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

Il valore iss restituito dalle chiamate Parse e Check è un elenco di problemi che potrebbero essere errori. Se il valore del campo iss.Err() non è null, significa che esiste un errore di sintassi o di semantica e il programma non può procedere oltre. Se l'espressione ha un formato corretto, il risultato di queste chiamate è un eseguibile cel.Ast.

Valuta l'espressione

Una volta analizzata e archiviata in un cel.Ast, l'espressione può essere convertita in un programma valutabile le cui associazioni di funzioni e modalità di valutazione possono essere personalizzate con opzioni funzionali. Tieni presente che è anche possibile leggere un cel.Ast da un protocollo utilizzando le funzioni cel.CheckedExprToAst o cel.ParsedExprToAst.

Una volta pianificato, un cel.Program può essere valutato in base all'input chiamando il numero Eval. Il risultato di Eval conterrà il risultato, i dettagli della valutazione e lo stato di errore.

Aggiungi la pianificazione e chiama il numero eval:

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

Per brevità, i casi di errore inclusi sopra verranno omessi nei prossimi esercizi.

Esegui il codice

Nella riga di comando, esegui nuovamente il codice:

go run .

Dovrebbe essere visualizzato il seguente output, insieme ai segnaposto per gli esercizi futuri.

=== Exercise 1: Hello World ===

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

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

5. Utilizzare le variabili in una funzione

La maggior parte delle applicazioni CEL dichiara le variabili a cui fare riferimento all'interno delle espressioni. Le dichiarazioni delle variabili specificano un nome e un tipo. Il tipo di variabile può essere un tipo CEL integrato, un tipo noto del buffer di protocollo o qualsiasi tipo di messaggio protobuf, purché il relativo descrittore sia fornito anche a CEL.

Aggiungi la funzione

Nell'editor, trova la dichiarazione di exercise2 e aggiungi quanto segue:

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

Esegui di nuovo il test e comprendi l'errore

Esegui di nuovo il programma:

go run .

Dovresti vedere l'output seguente:

ERROR: <input>:1:1: undeclared reference to 'request' (in container '')
 | request.auth.claims.group == 'admin'
 | ^

Il controllo del tipo produce un errore per l'oggetto di richiesta che include convenientemente lo snippet di origine in cui si verifica l'errore.

6. Dichiara le variabili

Aggiungi EnvOptions

Nell'editor, correggiamo l'errore risultante fornendo una dichiarazione per l'oggetto di richiesta sotto forma di messaggio di tipo google.rpc.context.AttributeContext.Request, in questo modo:

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

Esegui di nuovo il test e comprendi l'errore

Nuova esecuzione del programma:

go run .

Dovresti visualizzare il seguente errore:

ERROR: <input>:1:8: [internal] unexpected failed resolution of 'google.rpc.context.AttributeContext.Request'
 | request.auth.claims.group == 'admin'
 | .......^

Per utilizzare variabili che fanno riferimento a messaggi protobuf, il controllore deve conoscere anche il descrittore del tipo.

Utilizza cel.Types per determinare il descrittore per la richiesta nella tua funzione:

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

Esegui nuovamente correttamente.

Esegui di nuovo il programma:

go run .

Dovresti visualizzare quanto segue:

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

Per esaminare una variabile, abbiamo appena dichiarato un errore, le abbiamo assegnato un descrittore del tipo e abbiamo fatto riferimento alla variabile nella valutazione dell'espressione.

7. AND/OR logico

Una delle caratteristiche più uniche di CEL è l'uso di operatori logici commutativi. Entrambi i lati di un ramo condizionale possono cortocircuitare la valutazione, anche in presenza di errori o di input parziali.

In altre parole, la tecnologia CEL trova un ordine di valutazione che dà un risultato quando possibile, ignorando gli errori o persino i dati mancanti che potrebbero verificarsi in altri ordini di valutazione. Le applicazioni possono fare affidamento su questa proprietà per ridurre al minimo il costo della valutazione, rinviando la raccolta di input costosi quando è possibile raggiungere un risultato senza questi elementi.

Aggiungeremo un esempio AND/OR e quindi lo proveremo con input diversi per capire come la valutazione CEL cortocircuiti.

crea la funzione

Nell'editor, aggiungi i seguenti contenuti all'esercizio 3:

// 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"),
    ),
  )
}

Quindi, includi questa istruzione OR che restituirà true se l'utente è membro del gruppo admin o dispone di un particolare identificatore email:

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

Infine, aggiungi la richiesta eval che valuta l'utente con un insieme di attestazioni vuoto:

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

Esegui il codice con il set di attestazioni vuoto

Eseguendo di nuovo il programma, dovresti vedere il nuovo output seguente:

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

Aggiorna la richiesta di valutazione

Quindi, aggiorna il caso di valutazione in modo che venga passato un'entità diversa con il set di attestazioni vuoto:

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

Esegui il codice con un timestamp

Eseguendo di nuovo il programma,

go run .

dovresti visualizzare il seguente errore:

=== 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, sappiamo quali campi e tipi ci aspettiamo. Nei valori Map e JSON, non sappiamo se sarà presente una chiave. Poiché non esiste un valore predefinito sicuro per una chiave mancante, CEL restituisce l'errore per impostazione predefinita.

8. Funzioni personalizzate

Sebbene CEL includa molte funzioni integrate, in alcuni casi una funzione personalizzata è utile. Ad esempio, le funzioni personalizzate possono essere utilizzate per migliorare l'esperienza utente in caso di condizioni comuni o esporre lo stato sensibile al contesto

In questo esercizio esploreremo come esporre una funzione per raggruppare i controlli di uso comune.

Richiama una funzione personalizzata

Innanzitutto, crea il codice per configurare un override denominato contains che determina se una chiave esiste in una mappa e ha un determinato valore. Lascia i segnaposto per la definizione e l'associazione della funzione:

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

Esegui il codice e comprendi l'errore

Eseguendo di nuovo il codice, dovresti visualizzare il seguente errore:

ERROR: <input>:1:29: found no matching overload for 'contains' applied to 'map(string, dyn).(string, string)'
 | request.auth.claims.contains('group', 'admin')
 | ............................^

Per correggere l'errore, dobbiamo aggiungere la funzione contains all'elenco delle dichiarazioni in cui attualmente dichiara la variabile di richiesta.

Dichiara un tipo con parametri aggiungendo le tre righe seguenti. (Questo è complicato come qualsiasi sovraccarico di funzioni per 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()
}

Aggiungi la funzione personalizzata

Successivamente, aggiungeremo una nuova funzione contiene che utilizzerà i tipi con parametri:

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

Esegui il programma per comprendere l'errore

Esegui l'allenamento. Dovresti visualizzare il seguente errore relativo alla funzione di runtime mancante:

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

Fornisci l'implementazione della funzione alla dichiarazione NewEnv utilizzando la funzione cel.FunctionBinding():

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

Il programma dovrebbe essere eseguito correttamente:

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

Cosa succede quando esiste la rivendicazione?

Per un credito aggiuntivo, prova a impostare la dichiarazione dell'amministratore sull'input per verificare che anche il sovraccarico contenente la stringa restituisca un valore true quando la rivendicazione esiste. Dovresti vedere l'output seguente:

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

Prima di andare avanti, è consigliabile controllare la funzione mapContainsKeyValue stessa:

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

Per offrire la massima facilità di estensione, la firma per le funzioni personalizzate prevede argomenti di tipo ref.Val. Lo svantaggio in questo caso è che la facilità di estensione aggiunge un carico sull'implementatore per garantire che tutti i tipi di valore vengano gestiti correttamente. Quando i tipi di argomento di input o il conteggio non corrispondono alla dichiarazione di funzione, deve essere restituito un errore no such overload.

cel.FunctionBinding() aggiunge una protezione dei tipi di runtime per garantire che il contratto di runtime corrisponda alla dichiarazione con controllo del tipo nell'ambiente.

9. Creazione di JSON

CEL può anche produrre output non booleani, come JSON. Aggiungi il seguente codice alla funzione:

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

Esegui il codice

Eseguendo di nuovo il codice, dovresti visualizzare il seguente errore:

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

Aggiungi una dichiarazione per la variabile now di tipo cel.TimestampType a cel.NewEnv() ed esegui di nuovo:

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

Esegui di nuovo il codice. L'operazione dovrebbe riuscire:

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

Il programma viene eseguito, ma il valore di output out deve essere convertito esplicitamente in JSON. La rappresentazione CEL interna in questo caso è convertibile in formato JSON poiché si riferisce solo ai tipi che JSON può supportare o per i quali esiste una nota mappatura da protocollo a JSON.

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

Dopo aver convertito il tipo utilizzando la funzione helper valueToJSON all'interno del file codelab.go, dovresti vedere il seguente output aggiuntivo:

------ 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. Creazione di proto

CEL può creare messaggi protobuf per qualsiasi tipo di messaggio compilato nell'applicazione. Aggiungi la funzione per creare un google.rpc.context.AttributeContext.Request da un input jwt

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

Esegui il codice

Eseguendo di nuovo il codice, dovresti visualizzare il seguente errore:

ERROR: <input>:2:10: undeclared reference to 'Request' (in container '')
 |   Request{
 | .........^

Il container è sostanzialmente l'equivalente di uno spazio dei nomi o di un pacchetto, ma può anche essere granulare come il nome di un messaggio protobuf. I container CEL utilizzano le stesse regole di risoluzione dello spazio dei nomi di Protobuf e C++ per determinare dove viene dichiarato un determinato nome di variabile, funzione o tipo.

Dati il container google.rpc.context.AttributeContext, il controllo tipi e il valutatore provano i seguenti nomi di identificatori per tutte le variabili, tutti i tipi e tutte le funzioni:

  • google.rpc.context.AttributeContext.<id>
  • google.rpc.context.<id>
  • google.rpc.<id>
  • google.<id>
  • <id>

Per i nomi assoluti, aggiungi un punto iniziale al riferimento alla variabile, al tipo o alla funzione. Nell'esempio, l'espressione .<id> cercherà solo l'identificatore <id> di primo livello senza prima eseguire il controllo all'interno del contenitore.

Prova a specificare l'opzione cel.Container("google.rpc.context.AttributeContext") per l'ambiente CEL ed esegui di nuovo:

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

 ...
}

Dovrebbe essere visualizzato il seguente output:

ERROR: <input>:4:16: undeclared reference to 'jwt' (in container 'google.rpc.context.AttributeContext')
 |     principal: jwt.iss + '/' + jwt.sub,
 | ...............^

... e molti altri errori...

Quindi, dichiara le variabili jwt e now e il programma dovrebbe essere eseguito come previsto:

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

Per aggiungere credito, ti conviene anche annullare il wrapping del tipo chiamando out.Value() sul messaggio per vedere come cambia il risultato.

11. Macro

Le macro possono essere utilizzate per manipolare il programma CEL al momento dell'analisi. Le macro associano la firma di una chiamata e manipolano la chiamata di input e i suoi argomenti per produrre una nuova sottoespressione AST.

Le macro possono essere utilizzate per implementare in AST logica complessa che non può essere scritta direttamente in CEL. Ad esempio, la macro has abilita i test della presenza sul campo. Le macro di comprensione come Esiste e sostituiscono tutte una chiamata di funzione con iterazione limitata su un elenco o una mappa di input. Nessuno dei due concetti è possibile a livello sintattico, ma sono possibili attraverso le macro espansioni.

Aggiungi ed esegui l'esercizio successivo:

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

Dovresti vedere i seguenti errori:

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

... e tanti altri ...

Questi errori si verificano perché le macro non sono ancora abilitate. Per attivare le macro, rimuovi cel.ClearMacros() ed esegui di nuovo:

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

Di seguito sono riportate le macro attualmente supportate:

Macro

Firma

Descrizione

tutti

r.all(var, cond)

Verifica se cond restituisce true per all var nell'intervallo r.

esiste

r.exists(var, cond)

Verifica se cond restituisce true per qualsiasi var nell'intervallo r.

exists_one

r.esiste_uno(var, cond)

Verifica se cond restituisce true per una sola var nell'intervallo r.

filtro

r.filter(var, cond)

Per gli elenchi, crea un nuovo elenco in cui ogni variabile elemento nell'intervallo r soddisfa la condizione cond. Per le mappe, crea un nuovo elenco in cui ogni variabile chiave nell'intervallo r soddisfi la condizione cond.

mappa

r.map(var, expr)

Crea un nuovo elenco in cui ogni variabile nell'intervallo r viene trasformata da expr.

r.map(var, cond, expr)

Come la mappa a due argomenti, ma con un filtro condizionale prima che il valore venga trasformato.

contiene

has(a.b)

Test di presenza per b sul valore a : per le mappe, definizione dei test json. Per i proto, testa il valore primitivo non predefinito o un campo messaggio impostato.

Quando l'argomento intervallo r è di tipo map, var sarà la chiave mappa e per i valori di tipo list var sarà il valore dell'elemento dell'elenco. Le macro all, exists, exists_one, filter e map eseguono una riscrittura AST che esegue un'iterazione per ogni iterazione limitata dalla dimensione dell'input.

Le comprensioni limitate assicurano che i programmi CEL non siano completi di Turing, ma effettuano una valutazione in tempo superlineare rispetto all'input. Utilizza queste macro con parsimonia o non utilizzarle affatto. Un uso intensivo delle informazioni di solito è un buon indicatore del fatto che una funzione personalizzata potrebbe offrire una migliore esperienza utente e un miglior rendimento.

12. Ottimizzazione

Al momento esistono alcune funzionalità esclusive di CEL-Go, ma indicative dei piani futuri per altre implementazioni di CEL. L'esercizio seguente illustra i diversi piani di programma per lo stesso 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

Quando il flag di ottimizzazione è attivato, CEL impiegherà del tempo aggiuntivo per creare in anticipo l'elenco e la mappatura dei valori letterali e ottimizzare determinate chiamate come l'operatore "in" per un test di appartenenza impostato su true:

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

Quando lo stesso programma viene valutato più volte sulla base di input diversi, l'ottimizzazione è una buona scelta. Tuttavia, quando il programma verrà valutato una sola volta, l'ottimizzazione aggiungerà semplicemente l'overhead.

Valutazione esaustiva

Exhaustive Eval può essere utile per il debug del comportamento di valutazione delle espressioni, in quanto fornisce informazioni sul valore osservato in ogni fase della valutazione dell'espressione.

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

Dovresti vedere un elenco dello stato di valutazione dell'espressione per ogni ID espressione:

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

L'ID espressione 2 corrisponde al risultato dell'operatore in nel primo. ramo e l'ID espressione 11 corrisponde all'operatore == nel secondo. In una valutazione normale, l'espressione sarebbe stata in cortocircuito dopo il calcolo di 2. Se y non fosse stato uint, lo stato avrebbe mostrato due motivi per cui l'espressione non sarebbe riuscita e non soltanto uno.

13. Che cosa era coperto?

Se hai bisogno di un motore di espressione, valuta la possibilità di utilizzare CEL. La tecnologia CEL è ideale per i progetti che devono eseguire la configurazione utente in cui le prestazioni sono cruciali.

Negli esercizi precedenti, ci auguriamo che tu ti sia abituato a trasferire i tuoi dati in CEL e a ottenere nuovamente i risultati o le decisioni.

Ci auguriamo che tu abbia un'idea del tipo di operazioni che puoi eseguire, da una decisione booleana alla generazione di messaggi JSON e Protobuffer.

Ci auguriamo che tu abbia idea di come utilizzare le espressioni e di cosa fanno. Inoltre, comprendiamo i metodi più comuni per estenderla.