1. Introdução
O que é CEL?
A CEL é uma linguagem de expressão completa e não Turing desenvolvida para ser rápida, portátil e segura de executar. A CEL pode ser usada sozinha ou incorporada em um produto maior.
O CEL foi desenvolvido como uma linguagem na qual é seguro executar o código do usuário. Embora seja perigoso chamar eval()
cegamente no código Python de um usuário, é possível executar com segurança o código CEL de um usuário. E, como o CEL impede comportamentos que o tornariam menos eficientes, ele faz a avaliação com segurança na ordem de nanossegundos a microssegundos. e é ideal para aplicativos
com alto desempenho.
A CEL avalia expressões, que são semelhantes a funções de linha única ou expressões lambda. Embora a CEL seja comumente usada para decisões booleanas, ela também pode ser usada para construir objetos mais complexos, como mensagens JSON ou protobuf.
A CEL é adequada para seu projeto?
Como a CEL avalia uma expressão do AST em nanossegundos para microssegundos, o caso de uso ideal para CEL são aplicativos com caminhos críticos para o desempenho. A compilação do código CEL na AST não deve ser feita em caminhos críticos. Os aplicativos ideais são aqueles em que a configuração é executada com frequência e modificada com pouca frequência.
Por exemplo, executar uma política de segurança com cada solicitação HTTP para um serviço é um caso de uso ideal para a CEL, porque a política de segurança muda raramente, e a CEL terá um impacto insignificante no tempo de resposta. Nesse caso, a CEL retorna um booleano, se a solicitação deve ser permitida ou não, mas pode retornar uma mensagem mais complexa.
O que é abordado neste codelab?
A primeira etapa deste codelab mostra os motivos para usar a CEL e os conceitos principais dela. O restante é dedicado a Exercícios de codificação que abrangem casos de uso comuns. Para uma análise mais aprofundada sobre a linguagem, a semântica e os recursos, consulte Definição de linguagem CEL (link em inglês) no GitHub e os documentos sobre CEL Go (links em inglês).
Este codelab é destinado a desenvolvedores que querem aprender a usar CEL para usar serviços que já oferecem suporte a CEL. Este codelab não aborda como integrar a CEL ao seu próprio projeto.
O que você vai aprender
- Principais conceitos da CEL
- Hello World: como usar a CEL para avaliar uma string
- Como criar variáveis
- Noções básicas sobre o curto-circuito do CEL em operações lógicas de AND/OR
- Como usar a CEL para criar JSON
- Como usar CEL para criar Protobuffers.
- Criação de macros
- Maneiras de ajustar suas expressões de CEL
O que é necessário
Pré-requisitos
Este codelab se baseia em uma compreensão básica de buffers de protocolo e Go Lang.
Se você não conhece os buffers de protocolo, o primeiro exercício dará uma noção de como a CEL funciona. No entanto, como os exemplos mais avançados usam buffers de protocolo como entrada para a CEL, eles podem ser mais difíceis de entender. Recomendamos seguir um destes tutoriais primeiro. Observe que os buffers de protocolo não são necessários para usar CEL, mas são muito usados neste codelab.
Para testar se o Go foi instalado, execute:
go --help
2. Principais conceitos
Aplicativos
O CEL é de uso geral e tem sido usado para diversos aplicativos, desde o roteamento de RPCs até a definição de políticas de segurança. O CEL é extensível, independente de aplicativo e otimizado para fluxos de trabalho de compilação única e avaliação de muitos.
Muitos serviços e aplicativos avaliam configurações declarativas. Por exemplo, o controle de acesso baseado em função (RBAC) é uma configuração declarativa que produz uma decisão de acesso com base em uma função e um conjunto de usuários. Se as configurações declarativas forem o caso de uso de 80%, a CEL será uma ferramenta útil para arredondar os 20% restantes quando os usuários precisarem de poder mais expressivo.
Compilação
Uma expressão é compilada em um ambiente. A etapa de compilação produz uma árvore de sintaxe abstrata (AST, na sigla em inglês) em formato protobuf. Em geral, as expressões compiladas são armazenadas para uso futuro a fim de manter a avaliação o mais rápida possível. Uma única expressão compilada pode ser avaliada com muitas entradas diferentes.
Expressões
Os usuários definem expressões; serviços e aplicativos definem o ambiente em que ele é executado. Uma assinatura de função declara as entradas e é escrita fora da expressão CEL. A biblioteca de funções disponíveis para CEL é importada automaticamente.
No exemplo a seguir, a expressão usa um objeto de solicitação, e a solicitação inclui um token de declarações. A expressão retorna um booleano indicando se o token de declarações ainda é válido.
// Check whether a JSON Web Token has expired by inspecting the 'exp' claim.
//
// Args:
// claims - authentication claims.
// now - timestamp indicating the current system time.
// Returns: true if the token has expired.
//
timestamp(claims["exp"]) < now
Ambiente
Ambientes são definidos por serviços. Os serviços e aplicativos que incorporam a CEL declaram a expressão "ambiente". O ambiente é a coleção de variáveis e funções que podem ser usadas em expressões.
As declarações baseadas em proto são usadas pelo verificador de tipos CEL para garantir que todas as referências de identificador e função em uma expressão sejam declaradas e usadas corretamente.
Três fases da análise de uma expressão
Há três fases no processamento de uma expressão: analisar, verificar e avaliar. O padrão mais comum da CEL é que um plano de controle analise e verifique expressões no momento da configuração e armazene a AST.
No ambiente de execução, o plano de dados recupera e avalia o AST repetidamente. A CEL é otimizada para eficiência no tempo de execução, mas a análise e a verificação não devem ser feitas em caminhos de código críticos de latência.
A CEL é analisada a partir de uma expressão legível por humanos para uma árvore de sintaxe abstrata usando uma gramática léxica / analisador ANTLR. A fase de análise emite uma árvore de sintaxe abstrata baseada em proto, em que cada nó Expr no AST contém um ID de número inteiro que é usado para indexar metadados gerados durante a análise e a verificação. O syntax.proto produzido durante a análise representa fielmente a representação abstrata do que foi digitado na forma de string da expressão.
Depois que uma expressão é analisada, ela pode ser verificada no ambiente para garantir que todos os identificadores de variáveis e funções na expressão tenham sido declarados e estejam sendo usados corretamente. O verificador de tipos produz um checked.proto que inclui metadados de resolução de tipo, variável e função que podem melhorar muito a eficiência da avaliação.
O avaliador de CEL precisa de três coisas:
- Vinculações de funções para qualquer extensão personalizada
- Vinculações de variáveis
- Uma AST para avaliar
As vinculações de função e variável devem corresponder ao que foi usado para compilar o AST. Qualquer uma dessas entradas pode ser reutilizada em várias avaliações, como uma AST sendo avaliada em muitos conjuntos de vinculações de variáveis, as mesmas variáveis usadas em muitas ASTs ou as vinculações de função usadas durante a vida útil de um processo (um caso comum).
3. Configurar
O código deste codelab fica na pasta codelab
do repositório cel-go
. A solução está disponível na pasta codelab/solution
do mesmo repositório.
Clone e use cd no repositório:
git clone https://github.com/google/cel-go.git
cd cel-go/codelab
Execute o código usando go run
:
go run .
Você verá esta resposta:
=== 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 ===
Onde estão os pacotes de CEL?
No seu editor, abra codelab/codelab.go
. A função principal que orienta a execução dos exercícios deste codelab deve ser mostrada, seguida de três blocos de funções auxiliares. O primeiro conjunto de auxiliares auxilia nas fases da avaliação de CEL:
- Função
Compile
: analisa e verifica, além de expressar uma expressão de entrada, em um ambiente. - Função
Eval
: avalia um programa compilado em relação a uma entrada. - Função
Report
: faz a formatação do resultado da avaliação.
Além disso, os auxiliares request
e auth
ajudam na criação de entradas para os vários exercícios.
Os exercícios se referem aos pacotes pelo nome curto. O mapeamento do pacote para o local de origem no repositório google/cel-go
está abaixo, se você quiser se aprofundar nos detalhes:
Pacote | Local de origem | Descrição |
cel | Interfaces de nível superior | |
ref | Interfaces de referência | |
tipos | Valores do tipo de ambiente de execução |
4. Olá, mundo!
Seguindo a tradição de todas as linguagens de programação, vamos começar criando e avaliando "Hello World!".
Configurar o ambiente
No editor, encontre a declaração de exercise1
e preencha o seguinte para configurar o ambiente:
// exercise1 evaluates a simple literal expression: "Hello, World!"
//
// Compile, eval, profit!
func exercise1() {
fmt.Println("=== Exercise 1: Hello World ===\n")
// Create the standard environment.
env, err := cel.NewEnv()
if err != nil {
glog.Exitf("env error: %v", err)
}
// Will add the parse and check steps here
}
Os aplicativos CEL avaliam uma expressão em relação a um ambiente. env, err := cel.NewEnv()
configura o ambiente padrão.
O ambiente pode ser personalizado fornecendo as opções cel.EnvOption
para a chamada. Essas opções podem desativar macros, declarar variáveis e funções personalizadas etc.
O ambiente CEL padrão é compatível com todos os tipos, operadores, funções e macros definidos na especificação de linguagem.
Analisar e verificar a expressão
Depois que o ambiente for configurado, as expressões poderão ser analisadas e verificadas. Adicione o seguinte à sua função:
// 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
}
O valor iss
retornado pelas chamadas Parse
e Check
é uma lista de problemas que podem ser erros. Se o iss.Err()
não for nulo, haverá um erro de sintaxe ou semântica e o programa não poderá continuar. Quando a expressão é bem formada, o resultado dessas chamadas é um cel.Ast
executável.
Determine a expressão
Depois que a expressão for analisada e verificada em um cel.Ast
, ela poderá ser convertida em um programa valioso com vinculações de funções e modos de avaliação que podem ser personalizados com opções funcionais. Também é possível ler um cel.Ast
de um proto usando as funções cel.CheckedExprToAst ou cel.ParsedExprToAst.
Depois que uma cel.Program
é planejada, ela pode ser avaliada em relação à entrada chamando Eval
. O resultado do Eval
conterá o resultado, os detalhes da avaliação e o status do erro.
Adicione o planejamento e chame 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()
}
Para facilitar, vamos omitir de exercícios futuros os casos de erro incluídos acima.
Executar o código
Na linha de comando, execute o código novamente:
go run .
A saída a seguir vai aparecer com marcadores de posição para os exercícios futuros.
=== Exercise 1: Hello World ===
------ input ------
(interpreter.emptyActivation)
------ result ------
value: Hello, World! (types.String)
5. Usar variáveis em uma função
A maioria dos aplicativos CEL declara variáveis que podem ser referenciadas em expressões. As declarações de variáveis especificam um nome e um tipo. O tipo de uma variável pode ser um tipo integrado de CEL, um tipo conhecido de buffer de protocolo ou qualquer tipo de mensagem protobuf, desde que o descritor dela também seja fornecido à CEL.
Adicionar a função
No editor, encontre a declaração de exercise2
e adicione o seguinte:
// 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()
}
Execute novamente e entenda o erro
Execute o programa novamente:
go run .
Você verá esta resposta:
ERROR: <input>:1:1: undeclared reference to 'request' (in container '')
| request.auth.claims.group == 'admin'
| ^
O verificador de tipos produz um erro para o objeto da solicitação que inclui convenientemente o snippet de origem onde o erro ocorre.
6. Declarar as variáveis
Adicionar EnvOptions
No editor, vamos corrigir o erro resultante fornecendo uma declaração para o objeto da solicitação como uma mensagem do tipo google.rpc.context.AttributeContext.Request
, da seguinte maneira:
// 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()
}
Execute novamente e entenda o erro
Execute o programa novamente:
go run .
O seguinte erro aparecerá:
ERROR: <input>:1:8: [internal] unexpected failed resolution of 'google.rpc.context.AttributeContext.Request'
| request.auth.claims.group == 'admin'
| .......^
Para usar variáveis que se referem a mensagens protobuf, o verificador de tipos também precisa conhecer o descritor de tipo.
Use cel.Types
para determinar o descritor da solicitação na sua função:
// 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()
}
Execução novamente concluída.
Execute o programa novamente:
go run .
Você verá o seguinte:
=== 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)
Para revisar, apenas declaramos uma variável para um erro, atribuímos a ela um descritor de tipo e, em seguida, referenciamos a variável na avaliação da expressão.
7. AND/OR lógico
Um dos recursos mais exclusivos da CEL é o uso de operadores lógicos comutativos. Qualquer um dos lados de uma ramificação condicional pode causar um curto-circuito na avaliação, mesmo diante de erros ou entradas parciais.
Em outras palavras, a CEL encontra uma ordem de avaliação que fornece um resultado sempre que possível, ignorando erros ou até mesmo dados ausentes que podem ocorrer em outros pedidos de avaliação. Os aplicativos podem contar com essa propriedade para minimizar o custo da avaliação, adiando a coleta de entradas caras quando um resultado pode ser alcançado sem elas.
Vamos adicionar um exemplo AND/OR e depois tentar com uma entrada diferente para entender como a CEL curto-circuito a avaliação.
Criar a função
No editor, adicione o conteúdo abaixo ao exercício 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"),
),
)
}
Em seguida, inclua esta instrução OR que retornará "true" se o usuário for participante do grupo admin
ou tiver um identificador de e-mail específico:
// 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)
}
Por fim, adicione o caso eval
que avalia o usuário com um conjunto de declarações vazio:
// 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()))
}
Executar o código com o conjunto de declarações vazio
Ao executar o programa novamente, você verá a seguinte nova saída:
=== 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)
Atualizar o caso de avaliação
Em seguida, atualize o caso de avaliação para transmitir uma principal diferente com o conjunto de declarações vazio:
// 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()))
}
Execute o código com um horário
Execute o programa novamente,
go run .
Este erro será exibido:
=== 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
Em protobuf, sabemos quais campos e tipos esperar. Nos valores map e json, não sabemos se uma chave estará presente. Como não há um valor padrão seguro para uma chave ausente, a CEL assume o padrão de erro.
8. Funções personalizadas
Embora a CEL inclua muitas funções integradas, há ocasiões em que uma função personalizada é útil. Por exemplo, as funções personalizadas podem ser usadas para melhorar a experiência do usuário em condições comuns ou expor estados sensíveis ao contexto.
Neste exercício, vamos descobrir como expor uma função para agrupar as verificações mais usadas.
Chamar uma função personalizada
Primeiro, crie o código para configurar uma substituição com o nome contains
, que determina se uma chave existe em um mapa e tem um valor específico. Deixe marcadores de posição para a definição da função e a vinculação dela:
// 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()
}
Executar o código e entender o erro
Ao executar o código novamente, você verá o seguinte erro:
ERROR: <input>:1:29: found no matching overload for 'contains' applied to 'map(string, dyn).(string, string)'
| request.auth.claims.contains('group', 'admin')
| ............................^
Para corrigir o erro, precisamos adicionar a função contains
à lista de declarações que atualmente declara a variável de solicitação.
Declare um tipo parametrizado adicionando as três linhas a seguir. (Isso é tão complicado quanto qualquer sobrecarga de função será para 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()
}
Adicionar a função personalizada
Em seguida, adicionaremos uma nova função "contains" que usará os tipos parametrizados:
// 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()
}
Execute o programa para entender o erro
Execute o exercício. O seguinte erro sobre a função de ambiente de execução ausente vai aparecer:
------ result ------
error: no such overload: contains
Forneça a implementação da função à declaração NewEnv
usando a função 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()
}
O programa vai ser executado corretamente:
=== 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)
O que acontece quando a reivindicação existe?
Para crédito extra, tente definir a reivindicação do administrador na entrada para verificar se a sobrecarga "contém" também retorna "true" quando a reivindicação existe. Você verá esta resposta:
=== 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)
Antes de continuar, vale a pena inspecionar a própria função mapContainsKeyValue
:
// 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])
}
Para oferecer a maior facilidade de extensão, a assinatura de funções personalizadas espera os argumentos do tipo ref.Val
. A desvantagem aqui é que a facilidade da extensão sobrecarrega o implementador para garantir que todos os tipos de valor sejam tratados adequadamente. Quando os tipos de argumento de entrada ou a contagem não correspondem à declaração da função, um erro no such overload
é retornado.
O cel.FunctionBinding()
adiciona uma proteção de tipo no ambiente de execução para garantir que o contrato do ambiente de execução corresponda à declaração de tipo verificado no ambiente.
9. Criação de JSON
A CEL também pode produzir saídas não booleanas, como JSON. Adicione o seguinte à sua função:
// 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()
}
Executar o código
Ao executar o código novamente, você verá o seguinte erro:
ERROR: <input>:5:11: undeclared reference to 'now' (in container '')
| 'iat': now,
| ..........^
... and more ...
Adicione uma declaração para a variável now
do tipo cel.TimestampType
em cel.NewEnv()
e execute novamente:
// 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()
}
Execute o código novamente para que ele funcione:
=== 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}}
O programa é executado, mas o valor de saída out
precisa ser explicitamente convertido em JSON. Nesse caso, a representação interna de CEL é conversível em JSON, porque se refere apenas aos tipos compatíveis com o JSON ou para os quais há um conhecido mapeamento Proto para JSON.
// exercise5 covers how to build complex objects as CEL literals.
//
// Given the input now, construct a JWT with an expiry of 5 minutes.
func exercise5() {
fmt.Println("=== Exercise 5: Building JSON ===\n")
...
fmt.Printf("------ type conversion ------\n%v\n", valueToJSON(out))
fmt.Println()
}
Depois que o tipo for convertido usando a função auxiliar valueToJSON
no arquivo codelab.go
, você verá a seguinte saída adicional:
------ 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. Como criar Protos
A CEL pode criar mensagens protobuf para qualquer tipo de mensagem compilado no aplicativo. Adicione a função para criar um google.rpc.context.AttributeContext.Request
com base em uma entrada jwt
.
// exercise6 describes how to build proto message types within CEL.
//
// Given an input jwt and time now construct a
// google.rpc.context.AttributeContext.Request with the time and auth
// fields populated according to the go/api-attributes specification.
func exercise6() {
fmt.Println("=== Exercise 6: Building Protos ===\n")
// Construct an environment and indicate that the container for all references
// within the expression is `google.rpc.context.AttributeContext`.
requestType := &rpcpb.AttributeContext_Request{}
env, _ := cel.NewEnv(
// Add cel.Container() option for 'google.rpc.context.AttributeContext'
cel.Types(requestType),
// Add cel.Variable() option for 'jwt' as a map(string, Dyn) type
// and for 'now' as a timestamp.
)
// Compile the Request message construction expression and validate that
// the resulting expression type matches the fully qualified message name.
//
// Note: the field names within the proto message types are not quoted as they
// are well-defined names composed of valid identifier characters. Also, note
// that when building nested proto objects, the message name needs to prefix
// the object construction.
ast := compile(env, `
Request{
auth: Auth{
principal: jwt.iss + '/' + jwt.sub,
audiences: [jwt.aud],
presenter: 'azp' in jwt ? jwt.azp : "",
claims: jwt
},
time: now
}`,
cel.ObjectType("google.rpc.context.AttributeContext.Request"),
)
program, _ := env.Program(ast)
// Construct the message. The result is a ref.Val that returns a dynamic
// proto message.
out, _, _ := eval(
program,
map[string]interface{}{
"jwt": map[string]interface{}{
"sub": "serviceAccount:delegate@acme.co",
"aud": "my-project",
"iss": "auth.acme.com:12350",
"extra_claims": map[string]string{
"group": "admin",
},
},
"now": time.Now(),
},
)
// Hint: Unwrap the CEL value to a proto. Make sure to use the
// `ConvertToNative(reflect.TypeOf(requestType))` to convert the dynamic proto
// message to the concrete proto message type expected.
fmt.Printf("------ type unwrap ------\n%v\n", out)
fmt.Println()
}
Executar o código
Ao executar o código novamente, você verá o seguinte erro:
ERROR: <input>:2:10: undeclared reference to 'Request' (in container '')
| Request{
| .........^
O contêiner é basicamente o equivalente a um namespace ou pacote, mas pode até ser tão granular quanto um nome de mensagem protobuf. Os contêineres CEL usam as mesmas regras de resolução de namespace que Protobuf e C++ para determinar onde um determinado nome de variável, função ou tipo é declarado.
Considerando o contêiner google.rpc.context.AttributeContext
, o verificador de tipos e o avaliador vão testar os seguintes nomes de identificador para todas as variáveis, tipos e funções:
google.rpc.context.AttributeContext.<id>
google.rpc.context.<id>
google.rpc.<id>
google.<id>
<id>
Para nomes absolutos, insira um ponto no início da referência de variável, tipo ou função. No exemplo, a expressão .<id>
vai pesquisar apenas o identificador <id>
de nível superior, sem antes fazer a verificação no contêiner.
Tente especificar a opção cel.Container("google.rpc.context.AttributeContext")
para o ambiente CEL e execute novamente:
// 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)),
)
...
}
A seguinte saída será exibida:
ERROR: <input>:4:16: undeclared reference to 'jwt' (in container 'google.rpc.context.AttributeContext')
| principal: jwt.iss + '/' + jwt.sub,
| ...............^
... e muitos outros erros...
Em seguida, declare as variáveis jwt
e now
. O programa será executado conforme o esperado:
=== 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}
Para receber mais crédito, você também precisa desencapsular o tipo chamando out.Value()
na mensagem para conferir como o resultado muda.
11. Macros
Elas podem ser usadas para manipular o programa CEL no momento da análise. As macros correspondem a uma assinatura de chamada e manipulam a chamada de entrada e seus argumentos para produzir uma nova subexpressão AST.
As macros podem ser usadas para implementar uma lógica complexa na AST que não pode ser escrita diretamente em CEL. Por exemplo, a macro has
permite testes de presença de campo. As macros de compreensão, como existe e todas, substituem uma chamada de função por iteração limitada em uma lista de entrada ou mapa. Nenhum dos conceitos é possível em nível sintático, mas eles são possíveis por meio de expansões macro.
Adicione e execute o próximo exercício:
// 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()
}
Você vai encontrar os seguintes erros:
ERROR: <input>:1:25: undeclared reference to 'c' (in container '')
| jwt.extra_claims.exists(c, c.startsWith('group'))
| ........................^
E muito mais...
Esses erros ocorrem porque as macros ainda não estão ativadas. Para ativar as macros, remova cel.ClearMacros()
e execute novamente:
=== 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)
Estas são as macros compatíveis no momento:
Macro | Assinatura | Descrição |
todas | r.all(var, cond) | Teste se a condição avalia como verdadeira para todas as var no intervalo r. |
existe | r.exists(var, cond) | Teste se a condição avalia como verdadeira para qualquer var no intervalo r. |
exists_one | r.exists_one(var, cond) | Teste se a condição avalia como verdadeira para apenas uma var no intervalo r. |
filtro | r.filter(var, cond) | Para listas, crie uma nova lista em que cada elemento var no intervalo r satisfaça a condição de condição. Para mapas, crie uma nova lista em que cada var de chave no intervalo r satisfaça a condição de condição. |
mapa | r.map(var, expr) | Crie uma nova lista em que cada var no intervalo r é transformado por expr. |
r.map(var, cond, expr) | Igual ao mapa de dois argumentos, mas com um filtro de condição condicional antes da transformação do valor. | |
tem | tem(a.b) | Teste de presença para b no valor a : para mapas, JSON testa a definição. Para protos, testa um valor primitivo não padrão ou um campo de mensagem ou definido. |
Quando o argumento r do intervalo é do tipo map
, var
é a chave do mapa e, para valores do tipo list
, var
é o valor do elemento da lista. As macros all
, exists
, exists_one
, filter
e map
executam uma regravação AST que realiza uma iteração para cada, delimitada pelo tamanho da entrada.
As compreensões limitadas garantem que os programas CEL não sejam completos de Turing, mas sejam avaliados em tempo superlinear em relação à entrada. Use essas macros com moderação ou não use. O uso intenso de compreensão geralmente é um bom indicador de que uma função personalizada forneceria uma melhor experiência do usuário e melhor desempenho.
12. Ajuste
No momento, há alguns recursos exclusivos do CEL-Go, mas que indicam planos futuros para outras implementações da CEL. O exercício a seguir mostra diferentes planos de programa para a mesma AST:
// exercise8 covers features of CEL-Go which can be used to improve
// performance and debug evaluation behavior.
//
// Turn on the optimization, exhaustive eval, and state tracking
// ProgramOption flags to see the impact on evaluation behavior.
func exercise8() {
fmt.Println("=== Exercise 8: Tuning ===\n")
// Declare the x and 'y' variables as input into the expression.
env, _ := cel.NewEnv(
cel.Variable("x", cel.IntType),
cel.Variable("y", cel.UintType),
)
ast := compile(env,
`x in [1, 2, 3, 4, 5] && type(y) == uint`,
cel.BoolType)
// Try the different cel.EvalOptions flags when evaluating this AST for
// the following use cases:
// - cel.OptOptimize: optimize the expression performance.
// - cel.OptExhaustiveEval: turn off short-circuiting.
// - cel.OptTrackState: track state and compute a residual using the
// interpreter.PruneAst function.
program, _ := env.Program(ast)
eval(program, cel.NoVars())
fmt.Println()
}
Optimize
Quando o flag de otimização está ativado, o CEL gasta mais tempo para criar literais de lista e de mapa com antecedência e otimiza algumas chamadas, como o operador "in", para que seja um teste de associação "true":
// Turn on optimization.
trueVars := map[string]interface{}{"x": int64(4), "y": uint64(2)}
program, _ := env.Program(ast, cel.EvalOptions(cel.OptOptimize))
// Try benchmarking with the optimization flag on and off.
eval(program, trueVars)
Quando o mesmo programa é avaliado muitas vezes em relação a entradas diferentes, a otimização é uma boa escolha. No entanto, quando o programa só vai ser avaliado uma vez, a otimização vai apenas aumentar a sobrecarga.
Avaliação exaustiva
O Eval exaustivo pode ser útil para depurar o comportamento de avaliação da expressão, já que fornece informações sobre o valor observado em cada etapa da avaliação da expressão.
// 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)
Você verá uma lista do estado de avaliação da expressão para cada ID de expressão:
------ 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)
O ID da expressão 2 corresponde ao resultado do operador "in" no primeiro. ramificação, e a expressão id 11 corresponde ao operador == na segunda. Na avaliação normal, a expressão entraria em curto-circuito depois que 2 fosse calculado. Se y não tivesse sido uint, o estado teria mostrado duas razões pelas quais a expressão teria falhado, e não apenas uma.
13. O que foi abordado?
Se precisar de um mecanismo de expressão, considere usar a CEL. A CEL é ideal para projetos que precisam executar a configuração do usuário em que o desempenho é crítico.
Nos exercícios anteriores, esperamos que você tenha se familiarizado com a transmissão de dados para CEL e como receber novamente o resultado ou a decisão.
Esperamos que você tenha uma noção dos tipos de operações que você pode fazer, desde uma decisão booleana até a geração de mensagens JSON e Protobuffer.
Esperamos que você tenha uma noção de como trabalhar com as expressões e o que elas estão fazendo. E entendemos maneiras comuns de ampliar isso.