1. Introduction
Qu'est-ce que CEL ?
CEL est un langage d'expression non Turing-complet conçu pour être rapide, portable et sûr à exécuter. CEL peut être utilisé seul ou intégré à un produit plus vaste.
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 CEL empêche les comportements qui réduiraient les performances, il effectue des évaluations sécurisées en quelques nanosecondes ou microsecondes. Il est donc idéal pour les applications dont les performances sont critiques.
CEL évalue les expressions, qui sont semblables aux fonctions sur une seule ligne ou aux expressions lambda. Bien que le langage CEL soit couramment utilisé pour prendre des décisions booléennes, il peut également servir à construire des objets plus complexes, comme des messages JSON ou Protobuf.
Le CEL est-il adapté à votre projet ?
Étant donné que CEL évalue une expression à partir de l'AST en nanosecondes ou microsecondes, les cas d'utilisation idéaux pour CEL sont les applications avec des chemins critiques en termes de performances. La compilation du code CEL dans l'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 rarement.
Par exemple, l'exécution d'une règle de sécurité à chaque requête HTTP envoyée à un service est un cas d'utilisation idéal pour CEL, car la règle de sécurité change rarement et CEL aura un impact négligeable sur le temps de réponse. Dans ce cas, CEL renvoie une valeur booléenne indiquant si la requête doit être autorisée ou non, mais il pourrait renvoyer un message plus complexe.
Contenu de cet atelier de programmation
La première étape de cet atelier de programmation explique pourquoi utiliser CEL et ses concepts de base. Le reste est consacré aux exercices de programmation qui couvrent les 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 Go de CEL.
Cet atelier de programmation s'adresse aux développeurs qui souhaitent apprendre à utiliser CEL pour utiliser des services qui le prennent déjà en charge. Cet atelier de programmation n'explique pas comment intégrer CEL à votre propre projet.
Points abordés
- Concepts de base de CEL
- Hello, World : utiliser CEL pour évaluer une chaîne
- Créer des variables
- Comprendre le court-circuitage de CEL dans les opérations logiques AND/OR
- Utiliser CEL pour créer du code JSON
- Utiliser le CEL pour créer des Protobuffers
- Créer des macros
- Méthodes pour ajuster vos expressions CEL
Prérequis
Prérequis
Cet atelier de programmation s'appuie sur une compréhension de base des Protocol Buffers et du langage Go.
Si vous ne connaissez pas les Protocol Buffers, le premier exercice vous donnera une idée du fonctionnement de CEL. Toutefois, les exemples plus avancés utilisent les Protocol Buffers comme entrée dans CEL, ce qui peut les rendre plus difficiles à comprendre. Commencez par suivre l'un de ces tutoriels. Notez que les Protocol Buffers ne sont pas nécessaires pour utiliser CEL, mais qu'ils sont largement 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
CEL est un langage à usage général qui a été utilisé pour diverses applications, du routage des RPC à la définition de règles de sécurité. CEL est extensible, indépendant des applications et optimisé pour les workflows de compilation unique et d'évaluation multiple.
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, 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 par rapport à un environnement. L'étape de compilation produit un arbre syntaxique abstrait (AST) au format protobuf. Les expressions compilées sont généralement stockées pour une utilisation ultérieure afin de rendre l'évaluation aussi rapide que possible. Une même expression compilée peut être évaluée avec de nombreuses entrées différentes.
Expressions
Les utilisateurs définissent des expressions, tandis que les services et les applications définissent l'environnement dans lequel elles s'exécutent. 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 CEL est importée automatiquement.
Dans l'exemple suivant, l'expression prend 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 les services. Les services et applications qui intègrent CEL déclarent l'environnement d'expression. L'environnement est l'ensemble des variables et des fonctions qui peuvent être utilisées dans les expressions.
Les déclarations basées sur les protos sont utilisées par le vérificateur de type CEL pour s'assurer que toutes les références d'identifiants et de fonctions dans une expression sont déclarées et utilisées correctement.
Trois phases d'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 à ce qu'un plan de contrôle analyse et vérifie les expressions au moment de la configuration, puis stocke l'AST.

Lors de l'exécution, le plan de données récupère et évalue l'AST à plusieurs reprises. CEL est optimisé pour l'efficacité de l'exécution, mais l'analyse et la vérification ne doivent pas être effectuées dans les chemins de code critiques en termes de latence.

CEL est analysé à partir d'une expression lisible par l'homme dans un arbre de syntaxe abstraite à l'aide d'une grammaire d'analyseur / lexique ANTLR. La phase d'analyse émet un arbre de syntaxe abstraite basé sur un proto, où chaque nœud Expr de l'AST contient un ID entier utilisé pour indexer les métadonnées générées lors de l'analyse et de la vérification. Le fichier syntax.proto produit 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 par rapport à l'environnement pour s'assurer que tous les identifiants de variables et de fonctions de l'expression ont été déclarés et sont utilisés correctement. Le vérificateur de type produit un fichier checked.proto qui inclut des métadonnées de résolution de type, de variable et de fonction, ce qui peut améliorer considérablement l'efficacité de l'évaluation.
L'évaluateur CEL a besoin de trois éléments :
- Liaisons de fonctions pour les extensions personnalisées
- Liaisons de variables
- AST à évaluer
Les liaisons de fonctions et de variables doivent correspondre à celles utilisées pour compiler l'AST. Chacune de ces entrées peut être réutilisée dans plusieurs évaluations, par exemple un AST évalué sur plusieurs ensembles de liaisons de variables, les mêmes variables utilisées sur plusieurs AST ou les liaisons de fonctions utilisées pendant toute 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 accédez 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'assistants vous aide à évaluer le CEL :
- Fonction
Compile: analyse et vérifie une expression d'entrée par rapport à un environnement - Fonction
Eval: évalue un programme compilé par rapport à une entrée. - Fonction
Report: affiche le résultat de l'évaluation dans un format lisible
De plus, les assistants request et auth ont été fournis pour vous aider à créer des entrées pour les différents exercices.
Les exercices feront référence aux packages par leur nom court. Si vous souhaitez en savoir plus, vous trouverez ci-dessous le mappage du package à l'emplacement source dans le dépôt google/cel-go :
Package | Emplacement de la source | Description |
cel | Interfaces de premier niveau | |
ref | Interfaces de référence | |
Types | Valeurs du type d'exécution |
4. Bonjour !
Comme le veut la tradition de 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 informations suivantes 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.
L'environnement peut être personnalisé 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 est compatible avec tous les types, opérateurs, fonctions et macros définis dans les spécifications du langage.
Analyser et vérifier l'expression
Une fois l'environnement configuré, les expressions peuvent être analysées et vérifiées. Ajoutez les éléments suivants à 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 être des erreurs. Si iss.Err() n'est pas nul, cela signifie qu'il y a une erreur de syntaxe ou de sémantique, et que le programme ne peut pas continuer. Lorsque l'expression est bien formé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 évaluable 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 à l'aide des fonctions cel.CheckedExprToAst ou cel.ParsedExprToAst.
Une fois qu'un cel.Program est planifié, il peut être évalué par rapport à l'entrée en appelant Eval. Le résultat de Eval contiendra le résultat, les détails de l'évaluation et l'état de l'erreur.
Ajoutez la planification et appelez 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 omettrons les cas d'erreur inclus ci-dessus dans les prochains exercices.
Exécuter le code
Sur la ligne de commande, réexécutez le code :
go run .
Vous devriez voir le résultat suivant, ainsi que des espaces réservés pour les futurs 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 qui peuvent être référencées dans les 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 tout 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 éléments suivants :
// 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
Relancez 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 pour l'objet de requête, qui inclut de manière pratique l'extrait source où l'erreur se produit.
6. Déclarer les variables
Ajouter EnvOptions
Dans l'éditeur, corrigeons l'erreur en fournissant une déclaration pour l'objet de requête en tant que 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 .
L'erreur suivante doit vous être renvoyée :
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 récapituler, nous venons de déclarer une variable pour une erreur, de lui attribuer un descripteur de type, puis de référencer la variable dans l'évaluation de l'expression.
7. Opérateur logique AND/OR
L'une des caractéristiques les plus spécifiques 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ée partielle.
En d'autres termes, CEL trouve un ordre d'évaluation qui donne un résultat chaque fois que 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 différentes entrées pour comprendre comment CEL court-circuite 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 OR qui renverra la valeur "true" si l'utilisateur est membre du groupe admin ou s'il possède un identifiant d'adresse e-mail spécifique :
// 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 l'ensemble de revendications vide
Si vous réexécutez le programme, vous devriez obtenir le nouveau 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)
Modifier la demande d'évaluation
Ensuite, mettez à jour le cas de test 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 un délai
Réexécuter le programme
go run .
L'erreur suivante devrait s'afficher :
=== 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 attendre. Dans les valeurs de carte et JSON, nous ne savons pas si une clé sera présente. Comme il n'existe pas de valeur par défaut sécurisée pour une clé manquante, CEL renvoie une erreur par défaut.
8. Fonctions personnalisées
Bien que CEL inclue de nombreuses fonctions intégrées, il peut être utile d'utiliser une fonction personnalisée dans certains cas. Par exemple, les fonctions personnalisées peuvent être utilisées pour améliorer l'expérience utilisateur pour les conditions courantes ou pour exposer un état sensible au contexte.
Dans cet exercice, nous allons voir comment exposer une fonction pour regrouper les vérifications courantes.
Appeler une fonction personnalisée
Commencez par créer le code pour configurer un remplacement appelé contains qui détermine si une clé existe dans une carte et possède une valeur particulière. Laissez des espaces réservés pour la définition et la liaison de la 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
Si vous réexécutez le code, l'erreur suivante devrait 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. (Il s'agit de la surcharge de fonction la plus complexe 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
Ensuite, nous allons ajouter une 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écuter le programme pour comprendre l'erreur
Exécutez l'exercice. L'erreur suivante concernant la fonction d'exécution manquante devrait s'afficher :
------ result ------
error: no such overload: contains
Fournissez l'implémentation de la fonction à 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 lorsqu'une revendication existe ?
Pour obtenir des points supplémentaires, essayez de définir la revendication d'administrateur sur l'entrée afin de 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 est 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 faciliter au maximum l'extension, la signature des fonctions personnalisées attend les arguments de type ref.Val. Le compromis ici est que la facilité d'extension impose à l'implémenteur de s'assurer que tous les types de valeurs sont gérés correctement. Lorsqu'un nombre ou un type d'arguments d'entrée ne correspond pas à la déclaration de fonction, une erreur no such overload doit être renvoyée.
cel.FunctionBinding() ajoute une protection de type d'exécution pour s'assurer que le contrat d'exécution correspond à la déclaration vérifiée par le type dans l'environnement.
9. Créer un fichier JSON
CEL peut également générer des sorties non booléennes, telles que JSON. Ajoutez les éléments suivants à 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
Si vous réexécutez le code, l'erreur suivante devrait 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 :
// 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. Il devrait fonctionner :
=== 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 en JSON. Dans ce cas, la représentation CEL interne est convertible en JSON, car elle ne fait référence qu'à des 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 voir 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 protos
CEL peut créer des messages protobuf pour n'importe quel type de message compilé dans l'application. Ajoutez la fonction pour créer un google.rpc.context.AttributeContext.Request à partir d'un 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
Si vous réexécutez le code, l'erreur suivante devrait s'afficher :
ERROR: <input>:2:10: undeclared reference to 'Request' (in container '')
| Request{
| .........^
Le conteneur est l'équivalent d'un espace de noms ou d'un package, mais 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é.
Étant donné le conteneur google.rpc.context.AttributeContext, le vérificateur de type et l'évaluateur essaieront 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, ajoutez un point au début de la référence de variable, de type ou de fonction. Dans l'exemple, l'expression .<id> ne recherchera que l'identifiant <id> de premier niveau sans d'abord vérifier dans le conteneur.
Essayez de spécifier l'option cel.Container("google.rpc.context.AttributeContext") pour l'environnement CEL et 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. Le programme devrait s'exécuter 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 un crédit supplémentaire, vous devez également désenvelopper le type en appelant out.Value() sur le message pour voir comment le résultat change.
11. Macros
Les macros peuvent être utilisées pour manipuler le programme CEL lors de l'analyse. Les macros correspondent à une signature d'appel et manipulent l'appel d'entrée et ses arguments afin de produire un nouvel AST de sous-expression.
Les macros peuvent être utilisées pour implémenter une logique complexe dans l'AST qui ne peut pas être écrite directement en CEL. Par exemple, la macro has permet de tester la présence des champs. Les macros de compréhension, telles que "exists" et "all", remplacent un appel de fonction par une itération limitée sur une liste ou une carte d'entrée. Aucun de ces concepts n'est possible au niveau syntaxique, mais ils le sont grâce aux expansions de 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 devraient s'afficher :
ERROR: <input>:1:25: undeclared reference to 'c' (in container '')
| jwt.extra_claims.exists(c, c.startsWith('group'))
| ........................^
… et bien d'autres encore…
Ces erreurs se produisent, car les macros ne sont pas encore activées. Pour activer les macros, supprimez cel.ClearMacros() et exécutez à nouveau la commande :
=== 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 renvoie la valeur "true" pour toutes les var de la plage r. |
existe | r.exists(var, cond) | Testez si cond renvoie la valeur true pour n'importe quelle variable dans la plage r. |
exists_one | r.exists_one(var, cond) | Testez si cond renvoie la valeur true pour une seule var dans la plage r. |
filtre | r.filter(var, cond) | Pour les listes, créez une liste dans laquelle chaque élément var de la plage r satisfait la condition cond. Pour les cartes, créez une liste où chaque var clé de la plage r satisfait la condition cond. |
carte | r.map(var, expr) | Crée une liste dans laquelle chaque var de la plage r est transformée par expr. |
r.map(var, cond, expr) | Identique à la map à deux arguments, mais avec un filtre cond conditionnel avant la transformation de la valeur. | |
comporte | has(a.b) | Test de présence de b sur la valeur a : pour les mappages, définition des tests JSON. Pour les protos, teste une valeur primitive non définie par défaut ou un champ de message ou un ensemble de champs. |
Lorsque l'argument de plage r est de type map, var correspond à la clé de carte. Pour les valeurs de type list, var correspond à 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 for-each limitée par la taille de l'entrée.
Les compréhensions limitées garantissent que les programmes CEL ne seront pas Turing-complets, mais elles sont évaluées dans un temps super-linéaire par rapport à l'entrée. Utilisez ces macros avec parcimonie, voire pas du tout. Une utilisation intensive des compréhensions 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
Certaines fonctionnalités sont actuellement exclusives à CEL-Go, mais elles sont représentatives des futurs plans 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'indicateur d'optimisation est activé, CEL passe plus de temps à créer des littéraux de liste et de carte à l'avance, et à optimiser certains appels tels que l'opérateur in pour qu'il s'agisse d'un véritable test d'appartenance à un ensemble :
// 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)
L'optimisation est un bon choix lorsque le même programme est évalué plusieurs fois par rapport à différentes entrées. Toutefois, lorsque le programme ne doit être évalué qu'une seule fois, l'optimisation ne fait qu'ajouter de la surcharge.
Évaluation exhaustive
L'évaluation exhaustive peut être utile pour déboguer le comportement d'évaluation des expressions, car elle fournit des informations sur la valeur observée à chaque étape de l'évaluation des expressions.
// 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)
Une liste de l'état d'évaluation des expressions pour chaque ID d'expression devrait s'afficher :
------ 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 la première branche, et l'ID d'expression 11 correspond à l'opérateur "==" dans la seconde. Lors d'une évaluation normale, l'expression aurait été court-circuitée après le calcul de 2. Si y n'avait pas été uint, 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'expression, envisagez d'utiliser CEL. CEL est idéal pour les projets qui doivent exécuter la configuration utilisateur lorsque les performances sont essentielles.
Nous espérons que les exercices précédents vous ont permis de vous familiariser avec la transmission de vos données à CEL et la récupération du résultat ou de 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 avez une idée de la façon de travailler avec les expressions et de ce qu'elles font. Nous comprenons les méthodes courantes pour l'étendre.