CEL-Go Codelab: быстрые, безопасные встроенные выражения

1. Введение

Что такое КЭЛ?

CEL — это полный язык выражений, не поддерживающий Тьюринга, разработанный для быстрого, переносимого и безопасного выполнения. CEL можно использовать отдельно или встроить в более крупный продукт.

CEL был разработан как язык, на котором безопасно выполнять пользовательский код. Хотя слепо вызывать eval() для пользовательского кода Python опасно, вы можете безопасно выполнить пользовательский CEL-код. А поскольку CEL предотвращает поведение, которое могло бы снизить его производительность, он безопасно оценивает порядок от наносекунд до микросекунд; он идеально подходит для приложений, критичных к производительности.

CEL оценивает выражения, которые похожи на однострочные функции или лямбда-выражения. Хотя CEL обычно используется для логических решений, его также можно использовать для создания более сложных объектов, таких как сообщения JSON или protobuf.

Подходит ли CEL для вашего проекта?

Поскольку CEL оценивает выражение из AST в течение от наносекунд до микросекунд, идеальным вариантом использования CEL являются приложения с критичными к производительности путями. Компиляция кода CEL в AST не должна выполняться на критических путях; идеальные приложения — это те, в которых конфигурация выполняется часто и изменяется относительно редко.

Например, выполнение политики безопасности при каждом HTTP-запросе к службе является идеальным вариантом использования CEL, поскольку политика безопасности меняется редко, а CEL окажет незначительное влияние на время ответа. В этом случае CEL возвращает логическое значение, следует ли разрешить запрос или нет, но он может вернуть более сложное сообщение.

Что рассматривается в этой Codelab?

На первом этапе этой лаборатории рассматривается мотивация использования CEL и его основных концепций. Остальное посвящено упражнениям по кодированию, которые охватывают общие случаи использования. Более подробную информацию о языке, семантике и функциях см. в документе «Определение языка CEL» на GitHub и в документации CEL Go .

Эта лаборатория кода предназначена для разработчиков, которые хотели бы изучить CEL, чтобы использовать сервисы, которые уже поддерживают CEL. В этой лаборатории не рассматривается, как интегрировать CEL в ваш собственный проект.

Что вы узнаете

  • Основные концепции CEL
  • Привет, мир: использование CEL для оценки строки
  • Создание переменных
  • Понимание короткого замыкания CEL в логических операциях И/ИЛИ
  • Как использовать CEL для создания JSON
  • Как использовать CEL для создания протобуферов
  • Создание макросов
  • Способы настройки выражений CEL

Что вам понадобится

Предварительные условия

Эта кодовая лаборатория основана на базовом понимании протокольных буферов и Go Lang .

Если вы не знакомы с протокольными буферами, первое упражнение даст вам представление о том, как работает CEL, но поскольку более сложные примеры используют протокольные буферы в качестве входных данных в CEL, их может быть труднее понять. Подумайте о том, чтобы сначала поработать с одним из этих руководств . Обратите внимание, что буферы протокола не обязательны для использования CEL, но они широко используются в этой лаборатории кода.

Вы можете проверить, что go установлен, выполнив:

go --help

2. Ключевые понятия

Приложения

CEL имеет общее назначение и используется для различных приложений, от маршрутизации RPC до определения политики безопасности. CEL является расширяемым, независимым от приложений и оптимизированным для рабочих процессов с однократной компиляцией и множественной оценкой.

Многие службы и приложения оценивают декларативные конфигурации. Например, управление доступом на основе ролей (RBAC) — это декларативная конфигурация, которая принимает решение о доступе с учетом роли и набора пользователей. Если декларативные конфигурации составляют 80% случаев использования, то CEL является полезным инструментом для округления оставшихся 20%, когда пользователям требуется больше выразительных возможностей.

Сборник

Выражение компилируется с учетом среды. На этапе компиляции создается абстрактное синтаксическое дерево (AST) в форме protobuf. Скомпилированные выражения обычно сохраняются для использования в будущем, чтобы обеспечить максимально быструю оценку. Одно скомпилированное выражение может быть оценено с использованием множества различных входных данных.

Выражения

Пользователи определяют выражения; службы и приложения определяют среду, в которой он работает. Сигнатура функции объявляет входные данные и записывается вне выражения CEL. Библиотека функций, доступных CEL, импортируется автоматически.

В следующем примере выражение принимает объект запроса, а запрос включает токен утверждений. Выражение возвращает логическое значение, указывающее, действителен ли токен утверждений.

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

Среда

Среды определяются службами . Службы и приложения, встраивающие CEL, объявляют среду выражения. Среда — это набор переменных и функций, которые можно использовать в выражениях.

Объявления на основе прототипов используются средством проверки типов CEL, чтобы гарантировать, что все ссылки на идентификаторы и функции внутри выражения объявлены и используются правильно.

Три этапа анализа выражения

Обработка выражения состоит из трех этапов: анализ, проверка и оценка. Наиболее распространенный шаблон для CEL — это когда плоскость управления анализирует и проверяет выражения во время настройки и сохраняет AST.

c71fc08068759f81.png

Во время выполнения плоскость данных неоднократно извлекает и оценивает AST. CEL оптимизирован для повышения эффективности во время выполнения, но синтаксический анализ и проверка не должны выполняться в путях кода, критических по задержке.

49ab7d8517143b66.png

CEL анализируется из удобочитаемого выражения в абстрактное синтаксическое дерево с использованием грамматики лексера/парсера ANTLR . На этапе анализа создается абстрактное синтаксическое дерево на основе прототипа, где каждый узел Expr в AST содержит целочисленный идентификатор, который используется для индексации метаданных, сгенерированных во время синтаксического анализа и проверки. Синтаксис.proto , созданный во время синтаксического анализа, точно представляет абстрактное представление того, что было введено в строковую форму выражения.

После анализа выражения его можно проверить на соответствие среде, чтобы убедиться, что все идентификаторы переменных и функций в выражении объявлены и используются правильно. Средство проверки типов создает файл Checked.proto , который включает в себя метаданные разрешения типа, переменной и функции, что может значительно повысить эффективность оценки.

Оценщику CEL нужны 3 вещи:

  • Привязки функций для любых пользовательских расширений
  • Привязки переменных
  • AST для оценки

Привязки функций и переменных должны соответствовать тому, что использовалось для компиляции AST. Любые из этих входных данных можно повторно использовать в нескольких вычислениях, например, AST оценивается по множеству наборов привязок переменных, или одни и те же переменные используются для многих AST, или привязки функций, используемые на протяжении всего жизненного цикла процесса (частый случай). ).

3. Настройка

Код для этой лаборатории находится в папке codelab репозитория cel-go . Решение доступно в папке codelab/solution того же репозитория.

Клонируйте и перейдите в репозиторий:

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

Запустите код, используя go run :

go run .

Вы должны увидеть следующий вывод:

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

Где находятся пакеты CEL?

В вашем редакторе откройте codelab/codelab.go . Вы должны увидеть основную функцию, которая управляет выполнением упражнений в этой лаборатории кода, за которой следуют три блока вспомогательных функций. Первая группа помощников помогает на этапах оценки CEL:

  • Функция Compile : анализирует, проверяет и вводит выражение в соответствии со средой.
  • Функция Eval : оценивает скомпилированную программу по входным данным.
  • Функция Report : красивая распечатка результата оценки.

Кроме того, были предоставлены помощники request и auth для помощи в построении входных данных для различных упражнений.

В упражнениях пакеты будут называться по их короткому имени. Сопоставление пакета с исходным местоположением в репозитории google/cel-go приведено ниже, если вы хотите углубиться в подробности:

Упаковка

Исходное местоположение

Описание

чел.

чел-гоу/чел

Интерфейсы верхнего уровня

ссылка

cel-go/common/types/ref

Эталонные интерфейсы

типы

cel-go/common/типы

Значения типов среды выполнения

4. Привет, мир!

По традиции всех языков программирования мы начнем с создания и оценки «Hello World!».

Настройка среды

В вашем редакторе найдите объявление exercise1 и заполните следующее, чтобы настроить среду:

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

Приложения CEL оценивают выражение по среде. env, err := cel.NewEnv() настраивает стандартную среду.

Среду можно настроить, предоставив параметры cel.EnvOption для вызова. Эти параметры позволяют отключать макросы, объявлять пользовательские переменные и функции и т. д.

Стандартная среда CEL поддерживает все типы, операторы, функции и макросы, определенные в спецификации языка .

Разобрать и проверить выражение

После настройки среды выражения можно анализировать и проверять. Добавьте в свою функцию следующее:

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

Значение iss возвращаемое вызовами Parse и Check , представляет собой список проблем, которые могут быть ошибками. Если iss.Err() не равен нулю, это означает ошибку в синтаксисе или семантике, и программа не может продолжить работу. Если выражение правильно сформировано, результатом этих вызовов является исполняемый cel.Ast .

Оцените выражение

После того как выражение было проанализировано и зарегистрировано в cel.Ast , его можно преобразовать в оцениваемую программу, привязки функций и режимы оценки которой можно настроить с помощью функциональных опций. Обратите внимание, что cel.Ast также можно прочитать из прототипа, используя функции cel.CheckedExprToAst или cel.ParsedExprToAst .

После планирования cel.Program ее можно сравнить с входными данными, вызвав Eval . Результат Eval будет содержать результат, сведения об оценке и статус ошибки.

Добавьте планирование и вызовите 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()
}

Для краткости мы опустим описанные выше случаи ошибок из будущих упражнений.

Выполнить код

В командной строке перезапустите код:

go run .

Вы должны увидеть следующий результат вместе с заполнителями для будущих упражнений.

=== Exercise 1: Hello World ===

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

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

5. Используйте переменные в функции

Большинство приложений CEL объявляют переменные, на которые можно ссылаться в выражениях. В объявлениях переменных указываются имя и тип. Тип переменной может быть встроенным типом CEL , общеизвестным типом буфера протокола или любым типом сообщения protobuf, при условии, что его дескриптор также предоставляется CEL.

Добавьте функцию

В вашем редакторе найдите объявление exercise2 и добавьте следующее:

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

Перезапустите и поймите ошибку

Перезапустите программу:

go run .

Вы должны увидеть следующий вывод:

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

Средство проверки типов выдает ошибку для объекта запроса, который для удобства включает фрагмент исходного кода, в котором произошла ошибка.

6. Объявите переменные

Добавить EnvOptions

Давайте исправим возникшую ошибку в редакторе, предоставив объявление объекта запроса в виде сообщения типа google.rpc.context.AttributeContext.Request следующим образом:

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

Перезапустите и поймите ошибку

Запускаем программу еще раз:

go run .

Вы должны увидеть следующую ошибку:

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

Чтобы использовать переменные, которые ссылаются на сообщения protobuf, средству проверки типов также необходимо знать дескриптор типа.

Используйте cel.Types чтобы определить дескриптор запроса в вашей функции:

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

Перезапуск успешно!

Запустите программу еще раз:

go run .

Вы должны увидеть следующее:

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

Для примера мы просто объявили переменную для ошибки, присвоили ей дескриптор типа, а затем сослались на эту переменную при вычислении выражения.

7. Логическое И/ИЛИ

Одной из наиболее уникальных особенностей CEL является использование коммутативных логических операторов. Любая сторона условного перехода может привести к короткому замыканию вычисления, даже в случае ошибок или неполного ввода.

Другими словами, CEL находит порядок оценки, который дает результат, когда это возможно, игнорируя ошибки или даже недостающие данные, которые могут возникнуть в других порядках оценки. Приложения могут полагаться на это свойство, чтобы минимизировать стоимость оценки, откладывая сбор дорогостоящих входных данных, когда результат может быть достигнут без них.

Мы добавим пример И/ИЛИ, а затем попробуем его с разными входными данными, чтобы понять, как CEL сокращает оценку.

Создайте функцию

Добавьте в редакторе следующий контент для упражнения 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"),
    ),
  )
}

Затем включите этот оператор OR, который вернет true, если пользователь является членом группы admin или имеет определенный идентификатор электронной почты:

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

И, наконец, добавьте вариант eval , который оценивает пользователя с пустым набором утверждений:

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

Запустите код с пустым набором утверждений.

Перезапустив программу, вы должны увидеть следующий новый вывод:

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

Обновить оценочный случай

Затем обновите оценочный вариант, чтобы передать другому участнику с пустым набором утверждений:

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

Запустите код с указанием времени

Повторный запуск программы,

go run .

вы должны увидеть следующую ошибку:

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

В protobuf мы знаем, какие поля и типы следует ожидать. В значениях карты и json мы не знаем, будет ли присутствовать ключ. Поскольку для отсутствующего ключа не существует безопасного значения по умолчанию, CEL по умолчанию выдает ошибку.

8. Пользовательские функции

Хотя CEL включает в себя множество встроенных функций, бывают случаи, когда полезна пользовательская функция. Например, пользовательские функции можно использовать для улучшения взаимодействия с пользователем в распространенных условиях или для предоставления контекстно-зависимого состояния.

В этом упражнении мы рассмотрим, как предоставить функцию для объединения часто используемых проверок.

Вызов пользовательской функции

Сначала создайте код для настройки переопределения с именем contains , которое определяет, существует ли ключ на карте и имеет ли он определенное значение. Оставьте заполнители для определения функции и привязки функции:

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

Запустите код и поймите ошибку

Перезапустив код, вы должны увидеть следующую ошибку:

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

Чтобы исправить ошибку, нам нужно добавить функцию contains в список объявлений, который в настоящее время объявляет переменную запроса.

Объявите параметризованный тип, добавив следующие 3 строки. (Это так же сложно, как и любая перегрузка функции для 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()
}

Добавьте пользовательскую функцию

Далее мы добавим новую функцию contains, которая будет использовать параметризованные типы:

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

Запустите программу, чтобы понять ошибку

Выполните упражнение. Вы должны увидеть следующую ошибку об отсутствующей функции времени выполнения:

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

Предоставьте реализацию функции для объявления NewEnv , используя функцию 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()
}

Теперь программа должна работать успешно:

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

Что происходит, когда претензия существует?

Чтобы получить дополнительную оценку, попробуйте установить утверждение администратора на входе, чтобы убедиться, что перегрузка содержит также возвращает true, если утверждение существует. Вы должны увидеть следующий вывод:

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

Прежде чем двигаться дальше, стоит проверить саму функцию 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])
}

Чтобы обеспечить максимальную простоту расширения, сигнатура пользовательских функций предполагает аргументы типа ref.Val . Компромисс здесь заключается в том, что простота расширения добавляет разработчику дополнительную нагрузку по обеспечению правильной обработки всех типов значений. Если типы или количество входных аргументов не соответствуют объявлению функции, должна быть возвращена ошибка « no such overload .

cel.FunctionBinding() добавляет защиту типа времени выполнения, чтобы гарантировать, что контракт времени выполнения соответствует объявлению с проверкой типа в среде.

9. Создание JSON

CEL также может выдавать нелогические выходные данные, например 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")
    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()
}

Запустите код

Перезапустив код, вы должны увидеть следующую ошибку:

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

Добавьте объявление переменной now типа cel.TimestampType в cel.NewEnv() и запустите снова:

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

Перезапустите код, и он должен завершиться успешно:

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

Программа запускается, но out значение необходимо явно преобразовать в JSON. Внутреннее представление CEL в этом случае является конвертируемым в JSON, поскольку оно относится только к типам, которые JSON может поддерживать или для которых существует известное сопоставление Proto с 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()
}

После преобразования типа с помощью вспомогательной функции valueToJSON в файле codelab.go вы должны увидеть следующий дополнительный вывод:

------ 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. Создание прототипов

CEL может создавать сообщения protobuf для любого типа сообщений, скомпилированных в приложение. Добавьте функцию для создания google.rpc.context.AttributeContext.Request из входного 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()
}

Запустите код

Перезапустив код, вы должны увидеть следующую ошибку:

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

Контейнер по сути является эквивалентом пространства имен или пакета, но может быть даже таким же детальным, как имя сообщения protobuf. Контейнеры CEL используют те же правила разрешения пространства имен, что и Protobuf и C++, для определения того, где объявлена ​​данная переменная, функция или имя типа.

Учитывая контейнер google.rpc.context.AttributeContext средство проверки типов и оценщик будут пробовать следующие имена идентификаторов для всех переменных, типов и функций:

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

Для абсолютных имен добавьте точку перед переменной, типом или ссылкой на функцию. В этом примере выражение .<id> будет искать только идентификатор <id> верхнего уровня без предварительной проверки внутри контейнера.

Попробуйте указать параметр cel.Container("google.rpc.context.AttributeContext") для среды CEL и запустите еще раз:

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

 ...
}

Вы должны получить следующий вывод:

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

... и еще много ошибок...

Затем объявите переменные jwt и now , и программа должна работать как положено:

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

Для дополнительной оценки вам также следует развернуть тип, вызвав out.Value() в сообщении, чтобы увидеть, как изменится результат.

11. Макросы

Макросы можно использовать для управления программой CEL во время анализа. Макросы сопоставляют сигнатуру вызова и манипулируют входным вызовом и его аргументами, чтобы создать новое подвыражение AST.

Макросы можно использовать для реализации сложной логики в AST, которую невозможно записать непосредственно в CEL. Например, макрос has включает тестирование присутствия в поле. Макросы понимания, такие как существуют, заменяют вызов функции ограниченной итерацией по входному списку или карте. Ни одна из концепций невозможна на синтаксическом уровне, но они возможны посредством макрорасширений.

Добавьте и выполните следующее упражнение:

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

Вы должны увидеть следующие ошибки:

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

... и многое другое...

Эти ошибки возникают из-за того, что макросы еще не включены. Чтобы включить макросы, удалите cel.ClearMacros() и запустите снова:

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

Вот поддерживаемые на данный момент макросы:

Макрос

Подпись

Описание

все

r.all(вар, условие)

Проверьте, верен ли cond для всех переменных в диапазоне r.

существует

r.exists(вар, условие)

Проверьте, верен ли cond для любой переменной в диапазоне r.

существует_один

r.exists_one(вар, условие)

Проверьте, является ли cond истинным только для одной переменной в диапазоне r.

фильтр

r.filter(вар, условие)

Для списков создайте новый список, в котором каждый элемент var в диапазоне r удовлетворяет условию cond. Для карт создайте новый список, в котором каждая ключевая переменная в диапазоне r удовлетворяет условию cond.

карта

r.map(вар, выражение)

Создайте новый список, в котором каждая переменная в диапазоне r преобразуется выражением.

r.map(вар, условие, выражение)

То же, что и карта с двумя аргументами, но с условным фильтром перед преобразованием значения.

имеет

имеет (аб)

Проверка присутствия b по значению a: для карт определение проверки JSON. Для прото проверяет примитивное значение, отличное от заданного по умолчанию, или установленное поле сообщения.

Если аргумент диапазона r является типом map , var будет ключом карты, а для значений типа list var будет значением элемента списка. Макросы all , exists , exists_one , filter и map выполняют перезапись AST, которая выполняет итерацию для каждой итерации, ограниченную размером входных данных.

Ограниченное понимание гарантирует, что программы CEL не будут полными по Тьюрингу, но они выполняют вычисления за сверхлинейное время относительно входных данных. Используйте эти макросы экономно или не используйте их вообще. Интенсивное использование понятий обычно является хорошим показателем того, что пользовательская функция обеспечит лучший пользовательский опыт и лучшую производительность.

12. Тюнинг

Есть несколько функций, которые на данный момент являются эксклюзивными для CEL-Go, но которые указывают на будущие планы по другим реализациям CEL. В следующем упражнении демонстрируются различные планы программы для одного и того же 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()
}

Оптимизировать

Когда флаг оптимизации включен, CEL будет тратить дополнительное время на предварительное построение списка и сопоставление литералов, а также оптимизацию определенных вызовов, таких как оператор in, для проверки членства в истинном наборе:

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

Когда одна и та же программа оценивается много раз с использованием разных входных данных, оптимизация является хорошим выбором. Однако если программа будет оцениваться только один раз, оптимизация лишь добавит накладные расходы.

Исчерпывающая оценка

Исчерпывающая оценка может быть полезна для отладки поведения оценки выражения, поскольку она дает представление о наблюдаемом значении на каждом этапе оценки выражения.

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

Вы должны увидеть список состояний оценки выражения для каждого идентификатора выражения:

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

Идентификатор выражения 2 соответствует результату оператора in в первом. ветвь, а идентификатор выражения 11 соответствует оператору == во второй. При нормальной оценке выражение было бы замкнуто после вычисления 2. Если бы y не было uint, то состояние показало бы две причины, по которым выражение не удалось бы, а не только одну.

13. Что было освещено?

Если вам нужен механизм выражений, рассмотрите возможность использования CEL. CEL идеально подходит для проектов, которым необходимо выполнить пользовательскую настройку, где производительность имеет решающее значение.

Мы надеемся, что в предыдущих упражнениях вы научились передавать данные в CEL и получать выходные данные или решения обратно.

Мы надеемся, что у вас есть представление о том, какие операции вы можете выполнять: от логического решения до генерации сообщений JSON и Protobuffer.

Мы надеемся, что у вас есть представление о том, как работать с выражениями и что они делают. И мы понимаем распространенные способы его расширения.