Atelier de programmation CEL-Go: expressions intégrées rapides, sécurisées et sécurisées

1. Introduction

Qu'est-ce que le langage CEL ?

Le CEL est un langage d'expression complet sans Turing conçu pour être rapide, portable et à exécuter en toute sécurité. Le langage CEL peut être utilisé seul ou intégré à un produit plus vaste.

Le langage CEL a été conçu comme un langage dans lequel l'exécution du code utilisateur est sécurisée. Bien qu'il soit dangereux d'appeler aveuglément eval() sur le code Python d'un utilisateur, vous pouvez exécuter en toute sécurité le code CEL d'un utilisateur. Comme la méthode CEL empêche tout comportement qui le rendrait moins performante, elle effectue des évaluations en toute sécurité de l'ordre de la nanoseconde à la microseconde. est idéal pour les applications critiques.

Le langage CEL évalue les expressions qui sont semblables aux fonctions à ligne unique ou aux expressions lambda. Bien que le langage CEL soit couramment utilisé pour les décisions booléennes, il peut également servir à construire des objets plus complexes tels que des messages JSON ou protobuf.

Le langage CEL est-il adapté à votre projet ?

Étant donné que CEL évalue une expression de l'AST en nanosecondes ou en microsecondes, le cas d'utilisation idéal de CEL est celui des applications ayant des chemins critiques pour les performances. La compilation du code CEL dans AST ne doit pas être effectuée dans les chemins critiques. les applications idéales sont celles dans lesquelles la configuration est exécutée souvent et modifiée relativement peu fréquemment.

Par exemple, l'exécution d'une stratégie de sécurité avec chaque requête HTTP adressée à un service constitue un cas d'utilisation idéal pour CEL, car la stratégie change rarement, et CEL a un impact négligeable sur le temps de réponse. Dans ce cas, CEL renvoie une valeur booléenne, si la requête doit être autorisée ou non, mais elle peut renvoyer un message plus complexe.

Sujets abordés dans cet atelier de programmation

La première étape de cet atelier de programmation explique ce qui motive l'utilisation du CEL et décrit ses concepts fondamentaux. Le reste est dédié au codage des exercices qui couvrent des cas d'utilisation courants. Pour en savoir plus sur le langage, la sémantique et les fonctionnalités, consultez la définition du langage CEL sur GitHub et la documentation CEL Go.

Cet atelier de programmation s'adresse aux développeurs qui souhaitent apprendre le CEL afin d'utiliser des services déjà compatibles. Cet atelier de programmation n'aborde pas l'intégration du CEL dans votre propre projet.

Points abordés

  • Concepts fondamentaux du CEL
  • Hello, World: utiliser CEL pour évaluer une chaîne
  • Créer des variables
  • Comprendre le court-circuitage du CEL dans les opérations logiques AND/OR
  • Utiliser CEL pour créer des fichiers JSON
  • Utiliser CEL pour créer des tampons de protocole
  • Création de macros
  • Ajuster vos expressions CEL

Prérequis

Prérequis

Cet atelier de programmation s'appuie sur vos connaissances de base concernant les tampons de protocole et Go Lang.

Si vous ne connaissez pas les tampons de protocole, le premier exercice vous donnera une idée du fonctionnement du CEL. Toutefois, comme les exemples plus avancés utilisent des tampons de protocole comme entrée dans CEL, ils sont peut-être plus difficiles à comprendre. Envisagez d'abord de suivre l'un de ces tutoriels. Notez que les tampons de protocole ne sont pas obligatoires pour utiliser CEL, mais ils sont très utilisés dans cet atelier de programmation.

Vous pouvez vérifier que Go est installé en exécutant la commande suivante:

go --help

2. Concepts clés

Applications

Le CEL est à usage général et a été utilisé pour diverses applications, du routage des RPC à la définition de règles de sécurité. Le CEL est extensible, indépendant des applications et optimisé pour les workflows de compilation unique et d'évaluation de plusieurs.

De nombreux services et applications évaluent les configurations déclaratives. Par exemple, le contrôle des accès basé sur les rôles (RBAC) est une configuration déclarative qui produit une décision d’accès en fonction d’un rôle et d’un ensemble d’utilisateurs. Si les configurations déclaratives représentent 80% des cas d'utilisation, le CEL est un outil utile pour compléter les 20% restants lorsque les utilisateurs ont besoin de plus de puissance d'expression.

Compilation

Une expression est compilée en fonction d'un environnement. L'étape de compilation produit un arbre de syntaxe abstraite (AST) sous forme de tampon de protocole. Les expressions compilées sont généralement stockées pour une utilisation ultérieure afin d'accélérer l'évaluation le plus rapidement possible. Une expression compilée unique peut être évaluée avec de nombreuses entrées différentes.

Expressions

Les utilisateurs définissent des expressions. les services et applications définissent l'environnement dans lequel il s'exécute. Une signature de fonction déclare les entrées et est écrite en dehors de l'expression CEL. La bibliothèque de fonctions disponibles pour la bibliothèque CEL est importée automatiquement.

Dans l'exemple suivant, l'expression utilise un objet de requête et la requête inclut un jeton de revendications. L'expression renvoie une valeur booléenne indiquant si le jeton de revendications est toujours valide.

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

Environnement

Les environnements sont définis par des services. Les services et les applications qui intègrent des données CEL déclarent l'environnement de l'expression. L'environnement est un ensemble de variables et de fonctions pouvant être utilisées dans des expressions.

Les déclarations basées sur le protocole proto sont utilisées par l'outil de vérification du type CEL pour garantir que toutes les références d'identifiants et de fonctions dans une expression sont déclarées et utilisées correctement.

Les trois phases de l'analyse d'une expression

Le traitement d'une expression comporte trois phases: l'analyse, la vérification et l'évaluation. Le modèle le plus courant pour CEL consiste à faire en sorte qu'un plan de contrôle analyse et vérifie les expressions au moment de la configuration, et stocke le message AST.

c71fc08068759f81.png

Au moment de l'exécution, le plan de données récupère et évalue l'AST de manière répétée. Le CEL est optimisé pour l'efficacité de l'exécution, mais il est déconseillé d'analyser et de vérifier les chemins de code critiques en termes de latence.

49ab7d8517143b66.png

Le CEL est analysé à partir d'une expression lisible par une arborescence de syntaxe abstraite à l'aide d'une grammaire lexer / analyseur ANTLR. La phase d'analyse émet une arborescence de syntaxe abstraite basée sur le protocole proto dans lequel chaque nœud Expr de l'AST contient un identifiant d'entier qui est utilisé pour indexer les métadonnées générées lors de l'analyse et de la vérification. Le fichier syntax.proto généré lors de l'analyse représente fidèlement la représentation abstraite de ce qui a été saisi sous la forme de chaîne de l'expression.

Une fois qu'une expression a été analysée, elle peut être vérifiée dans l'environnement pour s'assurer que tous les identifiants de variables et de fonctions dans l'expression ont été déclarés et sont utilisés correctement. Le vérificateur de type produit un checked.proto qui inclut des métadonnées de type, de variable et de résolution de fonction, ce qui peut considérablement améliorer l'efficacité de l'évaluation.

L'évaluateur CEL a besoin de trois éléments:

  • Liaisons de fonctions pour toutes les extensions personnalisées
  • Liaisons de variables
  • Un AST à évaluer

La fonction et les liaisons de variables doivent correspondre à ce qui a été utilisé pour compiler l'AST. Chacune de ces entrées peut être réutilisée dans plusieurs évaluations, comme un AST évalué dans de nombreux ensembles de liaisons de variables, les mêmes variables utilisées avec de nombreux AST ou les liaisons de fonction utilisées tout au long de la durée de vie d'un processus (cas courant).

3. Configurer

Le code de cet atelier de programmation se trouve dans le dossier codelab du dépôt cel-go. La solution est disponible dans le dossier codelab/solution du même dépôt.

Clonez et utilisez la commande cd pour accéder au dépôt:

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

Exécutez le code à l'aide de go run:

go run .

Vous devriez obtenir le résultat suivant :

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

Où se trouvent les packages CEL ?

Dans votre éditeur, ouvrez codelab/codelab.go. Vous devriez voir la fonction principale qui pilote l'exécution des exercices de cet atelier de programmation, suivie de trois blocs de fonctions d'assistance. Le premier ensemble d'outils d'aide facilite les phases d'évaluation CEL:

  • Fonction Compile: analyse et vérifie l'expression d'entrée par rapport à un environnement
  • Fonction Eval: évalue un programme compilé par rapport à une entrée.
  • Fonction Report: affiche le style du résultat de l'évaluation.

De plus, les assistants request et auth ont été fournis pour vous aider à créer les entrées pour les différents exercices.

Les exercices feront référence aux packs par leur nom court. Vous trouverez ci-dessous le mappage entre le package et l'emplacement source dans le dépôt google/cel-go si vous souhaitez en savoir plus:

Package

Emplacement de la source

Description

cel

cel-go/cel

Interfaces de premier niveau

ref

cel-go/common/types/ref

Interfaces de référence

Types

cel-go/common/types

Valeurs du type d'environnement d'exécution

4. Bonjour !

Comme pour tous les langages de programmation, nous allons commencer par créer et évaluer "Hello World!".

Configurer l'environnement

Dans votre éditeur, recherchez la déclaration de exercise1 et renseignez les éléments suivants pour configurer l'environnement:

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

Les applications CEL évaluent une expression par rapport à un environnement. env, err := cel.NewEnv() configure l'environnement standard.

Vous pouvez personnaliser l'environnement en fournissant les options cel.EnvOption à l'appel. Ces options permettent de désactiver les macros, de déclarer des variables et des fonctions personnalisées, etc.

L'environnement CEL standard accepte tous les types, opérateurs, fonctions et macros définis dans la spécification du langage.

Analyser et vérifier l'expression

Une fois l'environnement configuré, les expressions peuvent être analysées et vérifiées. Ajoutez le code suivant à votre fonction:

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

La valeur iss renvoyée par les appels Parse et Check est une liste de problèmes pouvant correspondre à des erreurs. Si la valeur de iss.Err() n'est pas nulle, cela signifie qu'il y a une erreur dans la syntaxe ou la sémantique et que le programme ne peut pas continuer. Une fois l'expression bien structurée, le résultat de ces appels est un cel.Ast exécutable.

Évaluer l'expression

Une fois l'expression analysée et vérifiée dans un cel.Ast, elle peut être convertie en un programme évalue dont les liaisons de fonction et les modes d'évaluation peuvent être personnalisés avec des options fonctionnelles. Notez qu'il est également possible de lire un cel.Ast à partir d'un proto en utilisant les fonctions cel.CheckedExprToAst ou cel.ParsedExprToAst.

Une fois qu'une cel.Program est planifiée, elle peut être évaluée par rapport à l'entrée en appelant Eval. Le résultat de Eval contient le résultat, les détails de l'évaluation et l'état de l'erreur.

Ajoutez un planning et appelez le 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()
}

Par souci de concision, nous omettez les cas d'erreur inclus ci-dessus dans les prochains exercices.

Exécuter le code

Dans la ligne de commande, réexécutez le code:

go run .

Vous devriez obtenir le résultat suivant, avec des espaces réservés pour les prochains exercices.

=== Exercise 1: Hello World ===

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

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

5. Utiliser des variables dans une fonction

La plupart des applications CEL déclarent des variables pouvant être référencées dans des expressions. Les déclarations de variables spécifient un nom et un type. Le type d'une variable peut être un type intégré CEL, un type connu de tampon de protocole ou n'importe quel type de message protobuf à condition que son descripteur soit également fourni à CEL.

Ajouter la fonction

Dans votre éditeur, recherchez la déclaration de exercise2 et ajoutez les lignes suivantes:

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

Réexécuter et comprendre l'erreur

Exécutez à nouveau le programme:

go run .

Vous devriez obtenir le résultat suivant :

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

Le vérificateur de type génère une erreur au niveau de l'objet de la requête, qui inclut de manière pratique l'extrait source dans lequel l'erreur se produit.

6. Déclarer les variables

Ajouter EnvOptions

Dans l'éditeur, corrigez l'erreur obtenue en fournissant une déclaration pour l'objet de requête sous la forme d'un message de type google.rpc.context.AttributeContext.Request, comme suit:

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

Réexécuter et comprendre l'erreur

Exécuter à nouveau le programme:

go run .

Vous devriez obtenir l'erreur suivante :

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

Pour utiliser des variables qui font référence à des messages protobuf, le vérificateur de type doit également connaître le descripteur de type.

Utilisez cel.Types pour déterminer le descripteur de la requête dans votre fonction:

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

Réexécution réussie.

Exécutez à nouveau le programme:

go run .

Le résultat suivant doit s'afficher :

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

Pour rappel, nous venons de déclarer une variable pour une erreur, de lui attribuer un descripteur de type, puis de la référencer dans l'évaluation de l'expression.

7. Logique AND/OR

L'une des caractéristiques les plus uniques de CEL est son utilisation d'opérateurs logiques commutatifs. Chaque côté d'une branche conditionnelle peut court-circuiter l'évaluation, même en cas d'erreurs ou d'entrées partielles.

En d'autres termes, le CEL trouve un ordre d'évaluation qui donne un résultat dans la mesure du possible, en ignorant les erreurs ou même les données manquantes qui pourraient se produire dans d'autres ordres d'évaluation. Les applications peuvent s'appuyer sur cette propriété pour minimiser le coût de l'évaluation, en différant la collecte d'entrées coûteuses lorsqu'un résultat peut être obtenu sans elles.

Nous allons ajouter un exemple AND/OR, puis l'essayer avec d'autres entrées pour comprendre comment CEL court-circuits l'évaluation.

Créer la fonction

Dans votre éditeur, ajoutez le contenu suivant à l'exercice 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"),
    ),
  )
}

Ensuite, incluez cette instruction OU qui renvoie la valeur "true" si l'utilisateur est membre du groupe admin ou possède un identifiant d'adresse e-mail particulier:

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

Enfin, ajoutez le cas eval qui évalue l'utilisateur avec un ensemble de revendications vide:

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

Exécuter le code avec un ensemble de revendications vide

Lorsque vous réexécutez le programme, vous devez obtenir le résultat suivant:

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

Mettre à jour le cas d'évaluation

Ensuite, mettez à jour le cas d'évaluation pour transmettre un autre compte principal avec l'ensemble de revendications vide:

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

Exécuter le code avec une heure

Réexécuter le programme

go run .

vous devriez obtenir l'erreur suivante:

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

Dans protobuf, nous savons à quels champs et types nous attendons. Dans les valeurs map et json, nous ne savons pas si une clé sera présente. Puisqu'il n'existe pas de valeur par défaut sûre pour une clé manquante, CEL renvoie par défaut une erreur.

8. Fonctions personnalisées

Même si CEL inclut de nombreuses fonctions intégrées, une fonction personnalisée peut parfois s'avérer utile. Par exemple, les fonctions personnalisées permettent d'améliorer l'expérience utilisateur dans des conditions courantes ou d'exposer l'état sensible au contexte.

Dans cet exercice, nous allons voir comment exposer une fonction pour regrouper des vérifications couramment utilisées.

Appeler une fonction personnalisée

Commencez par créer le code pour configurer un forçage appelé contains qui détermine si une clé existe dans un mappage et possède une valeur particulière. Laissez des espaces réservés pour la définition et la liaison de fonction:

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

Exécuter le code et comprendre l'erreur

Lorsque vous réexécutez le code, l'erreur suivante doit s'afficher:

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

Pour corriger l'erreur, nous devons ajouter la fonction contains à la liste des déclarations, qui déclare actuellement la variable de requête.

Déclarez un type paramétré en ajoutant les trois lignes suivantes. (Cela est aussi compliqué que n'importe quelle surcharge de fonctions pour 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()
}

Ajouter la fonction personnalisée

Nous allons ensuite ajouter une nouvelle fonction "contains" qui utilisera les types paramétrés:

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

Exécutez le programme pour comprendre l'erreur

Exécutez l'exercice. L'erreur suivante devrait s'afficher pour une fonction d'exécution manquante:

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

Fournissez l'implémentation de la fonction dans la déclaration NewEnv à l'aide de la fonction 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()
}

Le programme devrait maintenant s'exécuter correctement:

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

Que se passe-t-il lorsque la revendication existe ?

Pour obtenir plus de crédit, essayez de définir la revendication administrateur sur l'entrée pour vérifier que la surcharge "contains" renvoie également la valeur "true" lorsque la revendication existe. Vous devriez obtenir le résultat suivant :

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

Avant de continuer, il peut être utile d'inspecter la fonction mapContainsKeyValue elle-même:

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

Pour que l'extension soit la plus simple possible, la signature des fonctions personnalisées attend des arguments de type ref.Val. En contrepartie, la facilité d'extension oblige l'utilisateur à s'assurer que tous les types de valeurs sont gérés correctement. Lorsque les types ou le nombre d'arguments d'entrée ne correspondent pas à la déclaration de la fonction, une erreur no such overload doit être renvoyée.

cel.FunctionBinding() ajoute un dispositif de protection du type d'environnement d'exécution pour s'assurer que le contrat d'exécution correspond à la déclaration vérifiée dans l'environnement.

9. Créer un fichier JSON

Le CEL peut également produire des sorties non booléennes, telles que JSON. Ajoutez le code suivant à votre fonction:

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

Exécuter le code

Lorsque vous réexécutez le code, l'erreur suivante doit s'afficher:

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

Ajoutez une déclaration pour la variable now de type cel.TimestampType à cel.NewEnv(), puis exécutez à nouveau la commande suivante:

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

Réexécutez le code. Cela devrait aboutir:

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

Le programme s'exécute, mais la valeur de sortie out doit être explicitement convertie au format JSON. Dans ce cas, la représentation CEL interne est convertible au format JSON, car elle ne fait référence qu'aux types compatibles avec JSON ou pour lesquels il existe un mappage Proto vers JSON bien connu.

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

Une fois le type converti à l'aide de la fonction d'assistance valueToJSON dans le fichier codelab.go, vous devriez obtenir le résultat supplémentaire suivant:

------ 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. Créer des prototypes

Le CEL peut créer des messages protobuf pour tout type de message compilé dans l'application. Ajoutez la fonction pour créer un google.rpc.context.AttributeContext.Request à partir d'une jwt d'entrée.

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

Exécuter le code

Lorsque vous réexécutez le code, l'erreur suivante doit s'afficher:

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

Le conteneur est essentiellement l'équivalent d'un espace de noms ou d'un package, mais il peut même être aussi précis qu'un nom de message protobuf. Les conteneurs CEL utilisent les mêmes règles de résolution d'espace de noms que Protobuf et C++ pour déterminer où un nom de variable, de fonction ou de type donné est déclaré.

Compte tenu du google.rpc.context.AttributeContext du conteneur, le vérificateur de type et l'évaluateur essaient les noms d'identifiants suivants pour toutes les variables, tous les types et toutes les fonctions:

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

Pour les noms absolus, faites précéder la variable, le type ou la fonction de référence d'un point. Dans cet exemple, l'expression .<id> ne recherchera que l'identifiant <id> de premier niveau sans vérifier au préalable dans le conteneur.

Essayez de spécifier l'option cel.Container("google.rpc.context.AttributeContext") pour l'environnement CEL, puis exécutez à nouveau la commande:

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

 ...
}

Vous devez obtenir le résultat suivant :

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

... et bien d'autres erreurs...

Ensuite, déclarez les variables jwt et now pour que le programme s'exécute comme prévu:

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

Pour obtenir plus de crédit, vous devez également désencapsuler le type en appelant out.Value() sur le message pour voir comment le résultat change.

11. Macros

Les macros permettent de manipuler le programme CEL au moment de l'analyse. Les macros font correspondre une signature d'appel et manipulent l'appel d'entrée et ses arguments afin de générer une nouvelle sous-expression AST.

Les macros permettent d'implémenter dans l'AST une logique complexe qui ne peut pas être écrite directement en CEL. Par exemple, la macro has active les tests de présence sur le terrain. Les macros de compréhension telles qu'existants et toutes remplacent un appel de fonction par une itération limitée sur une liste d'entrée ou une carte. Aucun de ces concepts n'est possible au niveau syntaxique, mais cela est possible grâce au développement des macros.

Ajoutez et exécutez l'exercice suivant:

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

Les erreurs suivantes doivent s'afficher:

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

... et bien d'autres ...

Ces erreurs sont dues au fait que les macros ne sont pas encore activées. Pour activer les macros, supprimez cel.ClearMacros(), puis exécutez à nouveau:

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

Voici les macros actuellement acceptées:

Macro

Signature

Description

tous

r.all(var, cond)

Testez si cond évalue "true" pour toutes les variables de la plage r.

existe

r.exists(var, cond)

Testez si cond évalue "true" pour n'importe quelle variable dans la plage r.

exists_one

r.exists_one(var, cond)

Testez si cond évalue "true" pour une seule variable dans la plage r.

filtre

r.filter(var, cond)

Pour les listes, créez une liste dans laquelle chaque élément var dans la plage r remplit la condition cond. Pour les mappages, créez une liste dans laquelle chaque variable de clé de la plage r remplit la condition cond.

carte

r.map(var, expr)

Créez une liste dans laquelle chaque variable de la plage r est transformée par expr.

r.map(var, cond; expr)

Identique à un mappage à deux arguments, mais avec un filtre cond conditionnel avant que la valeur ne soit transformée.

comporte

has(a.b)

Test de présence pour b sur la valeur a : pour les cartes, JSON teste la définition. Pour protos, teste une valeur primitive autre que celle par défaut, ou un champ de message défini.

Lorsque l'argument "range r" est de type map, var est la clé de mappage. Pour les valeurs de type list, var est la valeur de l'élément de liste. Les macros all, exists, exists_one, filter et map effectuent une réécriture AST qui effectue une itération pour chaque itération limitée par la taille de l'entrée.

Les compréhensions limitées garantissent que les programmes CEL ne seront pas complets de Turing, mais qu'ils évaluent en temps superlinéaire par rapport à l'entrée. Utilisez ces macros avec parcimonie ou pas du tout. Une utilisation intensive de la compréhension est généralement un bon indicateur qu'une fonction personnalisée offrirait une meilleure expérience utilisateur et de meilleures performances.

12. Réglage

Il existe quelques fonctionnalités qui sont exclusives à CEL-Go pour le moment, mais qui indiquent des projets futurs pour d'autres implémentations CEL. L'exercice suivant présente différents plans de programme pour le même 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

Lorsque l'option d'optimisation est activée, CEL consacre du temps supplémentaire à créer des littéraux de liste et de mappage à l'avance, et à optimiser certains appels, tels que l'opérateur "in", pour en faire un vrai test d'appartenance à un ensemble défini:

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

Lorsque le même programme est évalué à de nombreuses reprises sur différentes données, l'optimisation est alors un bon choix. Cependant, lorsque le programme est évalué une seule fois, l'optimisation ne fait qu'ajouter des frais généraux.

Évaluation exhaustive

L'évaluation exhaustive peut être utile pour déboguer le comportement d'évaluation des expressions, car elle fournit un aperçu de la valeur observée à chaque étape de l'évaluation de l'expression.

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

Vous devriez voir une liste des états d'évaluation de l'expression pour chaque ID d'expression:

------ 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 d'expression 2 correspond au résultat de l'opérateur "in" dans le premier. branche, et l'ID d'expression 11 correspond à l'opérateur == dans le second. En cours d'évaluation normale, l'expression aurait été court-circuitée après le calcul de 2. Si y n'avait pas été uint, alors l'état aurait indiqué deux raisons pour lesquelles l'expression aurait échoué, et pas une seule.

13. Sujets abordés

Si vous avez besoin d'un moteur d'expressions, envisagez d'utiliser le langage CEL. CEL est idéal pour les projets qui doivent exécuter une configuration utilisateur pour laquelle les performances sont essentielles.

Dans les exercices précédents, nous espérons que vous avez appris à transmettre vos données dans CEL et à extraire le résultat ou la décision.

Nous espérons que vous avez une idée du type d'opérations que vous pouvez effectuer, qu'il s'agisse d'une décision booléenne ou de la génération de messages JSON et Protobuffer.

Nous espérons que vous savez comment utiliser les expressions et ce qu'elles font. Nous connaissons les moyens courants d'étendre cette fonctionnalité.