Codelab de CEL-Go: Expresiones incorporadas rápidas y seguras

1. Introducción

¿Qué es el CEL?

CEL es un lenguaje de expresión no Turing-complete diseñado para ser rápido, portátil y seguro de ejecutar. CEL se puede usar por sí solo o incorporado en un producto más grande.

CEL se diseñó como un lenguaje en el que es seguro ejecutar código del usuario. Si bien es peligroso llamar a eval() a ciegas en el código de Python de un usuario, puedes ejecutar de forma segura el código de CEL de un usuario. Además, como CEL evita el comportamiento que podría reducir su rendimiento, se evalúa de forma segura en el orden de nanosegundos a microsegundos, por lo que es ideal para aplicaciones en las que el rendimiento es fundamental.

CEL evalúa expresiones, que son similares a funciones de una sola línea o expresiones lambda. Si bien CEL se usa comúnmente para tomar decisiones booleanas, también se puede usar para construir objetos más complejos, como mensajes JSON o Protobuf.

¿CEL es adecuado para tu proyecto?

Dado que CEL evalúa una expresión del AST en nanosegundos a microsegundos, los casos de uso ideales para CEL son las aplicaciones con rutas críticas para el rendimiento. La compilación del código de CEL en el AST no debe realizarse en rutas críticas. Las aplicaciones ideales son aquellas en las que la configuración se ejecuta con frecuencia y se modifica con relativa poca frecuencia.

Por ejemplo, ejecutar una política de seguridad con cada solicitud HTTP a un servicio es un caso de uso ideal para CEL, ya que la política de seguridad cambia con poca frecuencia y CEL tendrá un impacto insignificante en el tiempo de respuesta. En este caso, CEL devuelve un valor booleano que indica si se debe permitir la solicitud, pero podría devolver un mensaje más complejo.

¿Qué se aborda en este codelab?

En el primer paso de este codelab, se explica la motivación para usar CEL y sus conceptos básicos. El resto se dedica a ejercicios de programación que abarcan casos de uso comunes. Para obtener una visión más detallada del lenguaje, la semántica y las funciones, consulta la definición del lenguaje de CEL en GitHub y la documentación de CEL en Go.

Este codelab está dirigido a desarrolladores que deseen aprender sobre CEL para usar servicios que ya admiten este lenguaje. En este codelab, no se explica cómo integrar CEL en tu propio proyecto.

Qué aprenderás

  • Conceptos básicos de CEL
  • Hola mundo: Cómo usar CEL para evaluar una cadena
  • Cómo crear variables
  • Cómo comprender el cortocircuito de CEL en las operaciones lógicas AND/OR
  • Cómo usar CEL para compilar JSON
  • Cómo usar CEL para compilar Protobuf
  • Cómo crear macros
  • Formas de ajustar tus expresiones CEL

Requisitos

Requisitos previos

Este codelab se basa en un conocimiento básico de Protocol Buffers y Go Lang.

Si no conoces Protocol Buffers, el primer ejercicio te dará una idea de cómo funciona CEL, pero, como los ejemplos más avanzados usan Protocol Buffers como entrada en CEL, es posible que sean más difíciles de comprender. Primero, considera leer uno de estos instructivos. Ten en cuenta que no es necesario usar Protocol Buffers para usar CEL, pero se usan ampliamente en este codelab.

Para probar si Go está instalado, ejecuta el siguiente comando:

go --help

2. Conceptos clave

Aplicaciones

CEL es de uso general y se ha utilizado para diversas aplicaciones, desde el enrutamiento de RPC hasta la definición de políticas de seguridad. CEL es extensible, independiente de la aplicación y está optimizado para flujos de trabajo de compilación única y evaluación múltiple.

Muchos servicios y aplicaciones evalúan las configuraciones declarativas. Por ejemplo, el control de acceso basado en roles (RBAC) es una configuración declarativa que produce una decisión de acceso dado un rol y un conjunto de usuarios. Si las configuraciones declarativas son el 80% de los casos de uso, CEL es una herramienta útil para completar el 20% restante cuando los usuarios necesitan más poder expresivo.

Compilación

Una expresión se compila en función de un entorno. El paso de compilación produce un árbol de sintaxis abstracta (AST) en formato protobuf. Por lo general, las expresiones compiladas se almacenan para su uso futuro y así mantener la evaluación lo más rápida posible. Una sola expresión compilada se puede evaluar con muchas entradas diferentes.

Expresiones

Los usuarios definen expresiones, y los servicios y las aplicaciones definen el entorno en el que se ejecutan. La firma de una función declara las entradas y se escribe fuera de la expresión CEL. La biblioteca de funciones disponibles para CEL se importa automáticamente.

En el siguiente ejemplo, la expresión toma un objeto de solicitud, y la solicitud incluye un token de reclamos. La expresión devuelve un valor booleano que indica si el token de reclamos sigue siendo 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

Entorno

Los entornos se definen por los servicios. Los servicios y las aplicaciones que incorporan CEL declaran el entorno de la expresión. El entorno es la colección de variables y funciones que se pueden usar en las expresiones.

El verificador de tipos de CEL usa las declaraciones basadas en .proto para garantizar que todas las referencias a identificadores y funciones dentro de una expresión se declaren y usen correctamente.

Tres fases del análisis de una expresión

El procesamiento de una expresión consta de tres fases: análisis, verificación y evaluación. El patrón más común para CEL es que un plano de control analice y verifique las expresiones en el momento de la configuración, y almacene el AST.

c71fc08068759f81.png

En el tiempo de ejecución, el plano de datos recupera y evalúa el AST de forma repetida. CEL está optimizado para la eficiencia del tiempo de ejecución, pero el análisis y la verificación no se deben realizar en rutas de código críticas para la latencia.

49ab7d8517143b66.png

CEL se analiza a partir de una expresión legible para el ser humano en un árbol de sintaxis abstracta con una gramática de analizador / analizador léxico ANTLR. La fase de análisis emite un árbol de sintaxis abstracta basado en proto en el que cada nodo Expr del AST contiene un ID de número entero que se usa para indexar los metadatos generados durante el análisis y la verificación. El archivo syntax.proto que se produce durante el análisis representa fielmente la representación abstracta de lo que se escribió en la forma de cadena de la expresión.

Una vez que se analiza una expresión, se puede verificar en el entorno para garantizar que todos los identificadores de variables y funciones de la expresión se hayan declarado y se estén usando correctamente. El verificador de tipos produce un archivo checked.proto que incluye metadatos de resolución de tipos, variables y funciones que pueden mejorar drásticamente la eficiencia de la evaluación.

El evaluador de CEL necesita 3 elementos:

  • Vinculaciones de funciones para cualquier extensión personalizada
  • Vinculaciones de variables
  • Un AST para evaluar

Las vinculaciones de funciones y variables deben coincidir con lo que se usó para compilar el AST. Cualquiera de estas entradas se puede volver a usar en varias evaluaciones, como un AST que se evalúa en muchos conjuntos de vinculaciones de variables, o las mismas variables que se usan en muchos AST, o las vinculaciones de funciones que se usan durante el ciclo de vida de un proceso (un caso común).

3. Configurar

El código de este codelab se encuentra en la carpeta codelab del repo cel-go. La solución está disponible en la carpeta codelab/solution del mismo repo.

Clona el repo y cambia al directorio:

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

Ejecuta el código con go run:

go run .

Deberías ver el siguiente resultado:

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

¿Dónde están los paquetes de CEL?

En tu editor, abre codelab/codelab.go. Deberías ver la función principal que impulsa la ejecución de los ejercicios en este codelab, seguida de tres bloques de funciones auxiliares. El primer conjunto de asistentes ayuda con las fases de evaluación de CEL:

  • Función Compile: Analiza y verifica una expresión de entrada en relación con un entorno
  • Función Eval: Evalúa un programa compilado en función de una entrada
  • Función Report: Imprime de forma legible el resultado de la evaluación

Además, se proporcionan los asistentes request y auth para ayudar con la construcción de entradas para los diversos ejercicios.

En los ejercicios, se hará referencia a los paquetes por su nombre corto. A continuación, se muestra la asignación del paquete a la ubicación de la fuente dentro del repo de google/cel-go si deseas profundizar en los detalles:

Paquete

Ubicación de origen

Descripción

cel

cel-go/cel

Interfaces de nivel superior

ref

cel-go/common/types/ref

Interfaces de referencia

máquinas activas

cel-go/common/types

Valores del tipo de entorno de ejecución

4. ¡Hola, mundo!

Como es tradición en todos los lenguajes de programación, comenzaremos por crear y evaluar "Hello World!".

Configura el entorno

En tu editor, busca la declaración de exercise1 y completa lo siguiente para configurar el entorno:

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

Las aplicaciones de CEL evalúan una expresión en función de un entorno. env, err := cel.NewEnv() configura el entorno estándar.

El entorno se puede personalizar proporcionando las opciones cel.EnvOption a la llamada. Estas opciones pueden inhabilitar macros, declarar variables y funciones personalizadas, etcétera.

El entorno estándar de CEL admite todos los tipos, operadores, funciones y macros definidos en la especificación del lenguaje.

Analiza y verifica la expresión

Una vez que se configura el entorno, se pueden analizar y verificar las expresiones. Agrega lo siguiente a tu función:

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

El valor de iss que devuelven las llamadas Parse y Check es una lista de problemas que podrían ser errores. Si iss.Err() no es nulo, hay un error en la sintaxis o la semántica, y el programa no puede continuar. Cuando la expresión está bien formada, el resultado de estas llamadas es un cel.Ast ejecutable.

Evalúa la expresión

Una vez que la expresión se analizó y se verificó en un cel.Ast, se puede convertir en un programa evaluable cuyos vínculos de funciones y modos de evaluación se pueden personalizar con opciones funcionales. Ten en cuenta que también es posible leer un cel.Ast desde un .proto con las funciones cel.CheckedExprToAst o cel.ParsedExprToAst.

Una vez que se planifica un cel.Program, se puede evaluar en función de la entrada llamando a Eval. El resultado de Eval contendrá el resultado, los detalles de la evaluación y el estado del error.

Agrega la planificación y llama a 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 mayor brevedad, omitiremos los casos de error incluidos anteriormente en los ejercicios futuros.

Ejecuta el código

En la línea de comandos, vuelve a ejecutar el código:

go run .

Deberías ver el siguiente resultado, junto con marcadores de posición para los ejercicios futuros.

=== Exercise 1: Hello World ===

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

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

5. Usa variables en una función

La mayoría de las aplicaciones de CEL declararán variables a las que se puede hacer referencia dentro de las expresiones. Las declaraciones de variables especifican un nombre y un tipo. El tipo de una variable puede ser un tipo integrado de CEL, un tipo conocido de búfer de protocolo o cualquier tipo de mensaje de Protobuf, siempre y cuando su descriptor también se proporcione a CEL.

Agrega la función

En el editor, busca la declaración de exercise2 y agrega lo siguiente:

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

Vuelve a ejecutar el código y comprende el error

Vuelve a ejecutar el programa:

go run .

Deberías ver el siguiente resultado:

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

El verificador de tipos produce un error para el objeto de solicitud, que incluye de forma conveniente el fragmento de código fuente en el que se produce el error.

6. Declara las variables

Agregar EnvOptions

En el editor, corregiremos el error resultante proporcionando una declaración para el objeto de solicitud como un mensaje de tipo google.rpc.context.AttributeContext.Request de la siguiente manera:

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

Vuelve a ejecutar el código y comprende el error

Vuelve a ejecutar el programa:

go run .

Deberías ver el siguiente error:

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

Para usar variables que hacen referencia a mensajes de protobuf, el verificador de tipos también debe conocer el descriptor de tipo.

Usa cel.Types para determinar el descriptor de la solicitud en tu función:

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

La nueva ejecución se realizó correctamente.

Vuelve a ejecutar el programa:

go run .

Deberías ver lo siguiente:

=== 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 repasar, acabamos de declarar una variable para un error, le asignamos un descriptor de tipo y, luego, hicimos referencia a la variable en la evaluación de la expresión.

7. AND/OR lógico

Una de las características más exclusivas de CEL es el uso de operadores lógicos conmutativos. Cualquiera de los lados de una rama condicional puede cortocircuitar la evaluación, incluso ante errores o entradas parciales.

En otras palabras, CEL encuentra un orden de evaluación que arroja un resultado siempre que sea posible, ignorando los errores o incluso los datos faltantes que podrían ocurrir en otros órdenes de evaluación. Las aplicaciones pueden confiar en esta propiedad para minimizar el costo de la evaluación, ya que posponen la recopilación de entradas costosas cuando se puede llegar a un resultado sin ellas.

Agregaremos un ejemplo de AND/OR y, luego, lo probaremos con diferentes entradas para comprender cómo CEL cortocircuita la evaluación.

Crea la función

En el editor, agrega el siguiente contenido al ejercicio 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"),
    ),
  )
}

A continuación, incluye esta instrucción OR que devolverá verdadero si el usuario es miembro del grupo admin o tiene un identificador de correo electrónico en particular:

// 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 último, agrega el caso eval que evalúa al usuario con un conjunto de reclamos vacío:

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

Ejecuta el código con el conjunto de reclamos vacío

Si vuelves a ejecutar el programa, deberías ver el siguiente resultado nuevo:

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

Actualiza el caso de evaluación

A continuación, actualiza el caso de evaluación para pasar un principal diferente con el conjunto de reclamos vacío:

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

Ejecuta el código con un tiempo

Si vuelves a ejecutar el programa,

go run .

Deberías ver el siguiente error:

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

En protobuf, sabemos qué campos y tipos esperar. En los valores de mapa y JSON, no sabemos si habrá una clave. Como no hay un valor predeterminado seguro para una clave faltante, CEL establece el error de forma predeterminada.

8. Funciones personalizadas

Si bien CEL incluye muchas funciones integradas, en ocasiones es útil usar una función personalizada. Por ejemplo, las funciones personalizadas se pueden usar para mejorar la experiencia del usuario en condiciones comunes o exponer estados sensibles al contexto.

En este ejercicio, exploraremos cómo exponer una función para agrupar las verificaciones de uso común.

Llama a una función personalizada

Primero, crea el código para configurar una anulación llamada contains que determine si existe una clave en un mapa y si tiene un valor en particular. Deja marcadores de posición para la definición y la vinculación de la función:

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

Ejecuta el código y comprende el error

Si vuelves a ejecutar el código, deberías ver el siguiente error:

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

Para corregir el error, debemos agregar la función contains a la lista de declaraciones que actualmente declara la variable de solicitud.

Declara un tipo con parámetros agregando las siguientes 3 líneas. (Esta es la sobrecarga de función más compleja que tendrá 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()
}

Agrega la función personalizada

A continuación, agregaremos una nueva función contains que usará los 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()
}

Ejecuta el programa para comprender el error

Ejecuta el ejercicio. Deberías ver el siguiente error sobre la función de tiempo de ejecución faltante:

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

Proporciona la implementación de la función a la declaración NewEnv con la función 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()
}

Ahora el programa debería ejecutarse correctamente:

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

¿Qué sucede cuando existe el reclamo?

Para obtener crédito adicional, intenta establecer el reclamo de administrador en la entrada para verificar que la sobrecarga de contains también devuelva verdadero cuando exista el reclamo. Deberías ver el siguiente resultado:

=== 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 la pena inspeccionar la función mapContainsKeyValue en sí:

// 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 proporcionar la mayor facilidad de extensión, la firma de las funciones personalizadas espera los argumentos del tipo ref.Val. La desventaja aquí es que la facilidad de extensión agrega una carga al implementador para garantizar que todos los tipos de valores se controlen correctamente. Cuando los tipos o la cantidad de argumentos de entrada no coinciden con la declaración de la función, se debe devolver un error de no such overload.

El cel.FunctionBinding() agrega una protección de tipo de tiempo de ejecución para garantizar que el contrato de tiempo de ejecución coincida con la declaración verificada por tipo en el entorno.

9. Cómo compilar JSON

CEL también puede producir resultados no booleanos, como JSON. Agrega lo siguiente a tu función:

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

Ejecuta el código

Si vuelves a ejecutar el código, deberías ver el siguiente error:

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

Agrega una declaración para la variable now del tipo cel.TimestampType a cel.NewEnv() y vuelve a ejecutarla:

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

Vuelve a ejecutar el código. Debería funcionar:

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

El programa se ejecuta, pero el valor de salida out debe convertirse de forma explícita a JSON. En este caso, la representación interna de CEL se puede convertir en JSON, ya que solo hace referencia a tipos que JSON puede admitir o para los que existe una asignación de Proto a JSON conocida.

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

Una vez que el tipo se haya convertido con la función de ayuda valueToJSON dentro del archivo codelab.go, deberías ver el siguiente resultado 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. Cómo compilar protos

CEL puede compilar mensajes de protobuf para cualquier tipo de mensaje compilado en la aplicación. Agrega la función para compilar un google.rpc.context.AttributeContext.Request a partir de un jwt de entrada

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

Ejecuta el código

Si vuelves a ejecutar el código, deberías ver el siguiente error:

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

Básicamente, el contenedor es el equivalente de un espacio de nombres o un paquete, pero puede ser tan específico como el nombre de un mensaje de protobuf. Los contenedores de CEL usan las mismas reglas de resolución de espacios de nombres que Protobuf y C++ para determinar dónde se declara un nombre de variable, función o tipo determinado.

Dado el contenedor google.rpc.context.AttributeContext, el verificador de tipos y el evaluador intentarán los siguientes nombres de identificadores para todas las variables, tipos y funciones:

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

En el caso de los nombres absolutos, antepón un punto a la referencia de la variable, el tipo o la función. En el ejemplo, la expresión .<id> solo buscará el identificador <id> de nivel superior sin verificar primero dentro del contenedor.

Intenta especificar la opción cel.Container("google.rpc.context.AttributeContext") para el entorno de CEL y vuelve a ejecutar el comando:

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

 ...
}

Deberías obtener el siguiente resultado:

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

… y muchos errores más…

A continuación, declara las variables jwt y now, y el programa debería ejecutarse según lo previsto:

=== Exercise 6: Building Protos ===

  Request{
   auth: Auth{
    principal: jwt.iss + '/' + jwt.sub,
    audiences: [jwt.aud],
    presenter: 'azp' in jwt ? jwt.azp : "",
    claims: jwt
   },
   time: now
  }

------ input ------
jwt = {
  "aud": "my-project",
  "extra_claims": {
    "group": "admin"
  },
  "iss": "auth.acme.com:12350",
  "sub": "serviceAccount:delegate@acme.co"
}
now = seconds: 1588993027

------ result ------
value: &{0xc000485270 0xc000274180 {0xa6ee80 0xc000274180 22} 0xc0004be140 0xc0002bf700 false} (*types.protoObj)

----- type unwrap ----
&{0xc000485270 0xc000274180 {0xa6ee80 0xc000274180 22} 0xc0004be140 0xc0002bf700 false}

Para obtener crédito adicional, también debes desenvolver el tipo llamando a out.Value() en el mensaje para ver cómo cambia el resultado.

11. Macros

Las macros se pueden usar para manipular el programa CEL en el momento del análisis. Las macros coinciden con una firma de llamada y manipulan la llamada de entrada y sus argumentos para producir un nuevo AST de subexpresión.

Las macros se pueden usar para implementar lógica compleja en el AST que no se puede escribir directamente en CEL. Por ejemplo, la macro has habilita la prueba de presencia de campos. Las macros de comprensión, como exists y all, reemplazan una llamada a una función por una iteración limitada sobre una lista o un mapa de entrada. Ninguno de los dos conceptos es posible a nivel sintáctico, pero sí lo son a través de expansiones de macros.

Agrega y ejecuta el siguiente ejercicio:

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

Deberías ver los siguientes errores:

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

… y muchos más …

Estos errores se producen porque las macros aún no están habilitadas. Para habilitar las macros, quita cel.ClearMacros() y vuelve a ejecutar el comando:

=== 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 son las macros que se admiten actualmente:

Macro

Firma

Descripción

todos

r.all(var, cond)

Prueba si cond se evalúa como verdadero para todos los var en el rango r.

exists

r.exists(var, cond)

Prueba si cond se evalúa como verdadero para cualquier var en el rango r.

exists_one

r.exists_one(var, cond)

Prueba si cond se evalúa como verdadero para solo una var en el rango r.

filtrar

r.filter(var, cond)

Para las listas, crea una nueva lista en la que cada variable de elemento en el rango r satisfaga la condición cond. En el caso de los mapas, crea una lista nueva en la que cada variable de clave en el rango r satisfaga la condición cond.

mapa

r.map(var, expr)

Crea una lista nueva en la que cada var del rango r se transforma con expr.

r.map(var, cond, expr)

Es igual que el mapa de dos argumentos, pero con un filtro condicional cond antes de que se transforme el valor.

tiene

has(a.b)

Prueba de presencia de b en el valor a : Para mapas, definición de pruebas json. Para los protos, prueba el valor primitivo no predeterminado o un campo de mensaje o un conjunto.

Cuando el argumento de rango r es de tipo map, el var será la clave del mapa, y para los valores de tipo list, el var será el valor del elemento de la lista. Las macros all, exists, exists_one, filter y map realizan una reescritura del AST que ejecuta una iteración for-each limitada por el tamaño de la entrada.

Las comprensiones acotadas garantizan que los programas de CEL no sean Turing-completos, pero se evalúan en tiempo superlineal con respecto a la entrada. Usa estas macros con moderación o no las uses en absoluto. El uso intensivo de las comprensiones suele ser un buen indicador de que una función personalizada proporcionaría una mejor experiencia del usuario y un mejor rendimiento.

12. Ajuste

Por el momento, hay algunas funciones exclusivas de CEL-Go, pero que son indicativas de los planes futuros para otras implementaciones de CEL. En el siguiente ejercicio, se muestran diferentes planes de programas para el mismo 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

Cuando se activa la marca de optimización, CEL dedica tiempo adicional a compilar literales de lista y mapa con anticipación, y optimiza ciertas llamadas, como el operador in, para que sea una prueba de membresía de conjunto verdadera:

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

Cuando el mismo programa se evalúa muchas veces con diferentes entradas, la optimización es una buena opción. Sin embargo, cuando el programa solo se evaluará una vez, la optimización solo agregará sobrecarga.

Evaluación exhaustiva

La evaluación exhaustiva puede ser útil para depurar el comportamiento de la evaluación de expresiones, ya que proporciona información sobre el valor observado en cada paso de la evaluación de expresiones.

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

Deberías ver una lista del estado de evaluación de la expresión para cada ID de expresión:

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

El ID de expresión 2 corresponde al resultado del operador in en la primera rama, y el ID de expresión 11 corresponde al operador == en la segunda. En una evaluación normal, la expresión se habría cortocircuitado después de calcular el valor 2. Si y no hubiera sido uint, el estado habría mostrado dos motivos por los que la expresión habría fallado y no solo uno.

13. ¿Qué se abordó?

Si necesitas un motor de expresiones, considera usar CEL. CEL es ideal para proyectos que necesitan ejecutar la configuración del usuario en los que el rendimiento es fundamental.

En los ejercicios anteriores, esperamos que te hayas familiarizado con el paso de datos a CEL y la obtención de la salida o la decisión.

Esperamos que tengas una idea del tipo de operaciones que puedes realizar, desde una decisión booleana hasta la generación de mensajes JSON y Protobuffer.

Esperamos que tengas una idea de cómo trabajar con las expresiones y qué hacen. Además, conocemos las formas habituales de extenderla.