Ćwiczenie z programowania w CEL-Go: szybkie, bezpieczne i umieszczone wyrażenia

1. Wprowadzenie

Co to jest CEL?

CEL to język wyrażeń niezwiązany z objazdem wycieczki, zaprojektowany z myślą o szybkości, przenośności i bezpiecznej obsłudze. Kodu CEL można używać niezależnie lub można go umieścić w większej usłudze.

Język CEL został zaprojektowany jako język, w którym można bezpiecznie wykonywać kod użytkownika. Chociaż nieświadome wywołanie metody eval() w kodzie Pythona użytkownika jest niebezpieczne, możesz bezpiecznie wykonać jego kod CEL. A ponieważ CEL zapobiega działaniom, które mogłyby obniżyć jego wydajność, przeprowadza bezpieczną ocenę w kolejności od nanosekund do mikrosekund. Jest to więc idealne rozwiązanie w przypadku aplikacji, które mają newralgiczny poziom wydajności.

CEL ocenia wyrażenia, które są podobne do funkcji jednowierszowych lub wyrażeń lambda. Choć CEL jest powszechnie używany do podejmowania decyzji logicznych, można go też używać do tworzenia bardziej złożonych obiektów, takich jak komunikaty JSON lub protobuf.

Czy język CEL jest odpowiedni dla Twojego projektu?

Ponieważ CEL ocenia wyrażenie z AST w nanosekundach do mikrosekund, w przypadku CEL idealne zastosowanie to aplikacje ze ścieżkami o znaczeniu krytycznym dla wydajności. Kompilacji kodu CEL w AST nie należy przeprowadzać na ścieżkach krytycznych. idealne aplikacje to takie, w których konfiguracja jest wykonywana często i stosunkowo rzadko.

Na przykład wdrożenie zasady zabezpieczeń z każdym żądaniem HTTP do usługi jest idealnym przypadkiem użycia języka CEL, ponieważ zasada zabezpieczeń rzadko się zmienia, a CEL ma znikomy wpływ na czas odpowiedzi. W tym przypadku CEL zwraca wartość logiczną, jeśli żądanie powinno być dozwolone lub nie, ale może zwrócić bardziej złożony komunikat.

Co jest uwzględnione w tym ćwiczeniu z programowania?

W pierwszym kroku tego ćwiczenia w Codelabs omawiamy motywację do korzystania z języka CEL i jego podstawowe pojęcia. Pozostała część jest poświęcona kodowaniu ćwiczeń, które obejmują typowe przypadki użycia. Bardziej szczegółowe informacje na temat języka, semantyki i funkcji znajdziesz w definicji języka CEL w serwisie GitHub i w Dokumentach CEL Go.

To ćwiczenie w Codelabs jest przeznaczone dla programistów, którzy chcą nauczyć się języka CEL, aby korzystać z usług, które już obsługują ten język. To ćwiczenie w Codelabs nie obejmuje sposobu integracji języka CEL z własnym projektem.

Czego się nauczysz

  • Podstawowe pojęcia z języka CEL
  • Witaj świecie: używanie języka CEL do oceny ciągu znaków
  • Tworzenie zmiennych
  • Zniekształcenia w CEL w operacjach logicznych I/LUB
  • Jak używać języka CEL do tworzenia kodu JSON
  • Jak używać języka CEL do tworzenia protobuforów
  • Tworzenie makr
  • Sposoby dostrajania wyrażeń CEL

Czego potrzebujesz

Wymagania wstępne

Ćwiczenie w Codelabs opiera się na podstawowej wiedzy na temat buforów protokołów i Go Lang.

Jeśli nie wiesz nic o buforach protokołów, pierwsze ćwiczenie pozwoli Ci zobaczyć, jak działa CEL. Jednak ponieważ w bardziej zaawansowanych przykładach dane wejściowe są buforami protokołów w języku CEL, mogą być trudniejsze do zrozumienia. Możesz najpierw zapoznać się z jednym z tych samouczków. Pamiętaj, że bufory protokołów nie są wymagane do używania języka CEL, ale są one szeroko stosowane w tym ćwiczeniu z programowania.

Aby sprawdzić, czy to rozwiązanie zostało zainstalowane, uruchom polecenie:

go --help

2. Kluczowych pojęć

Aplikacje

Język CEL jest ogólny i stosował wiele różnych zastosowań, od kierowania wywołań RPC po definiowanie zasad bezpieczeństwa. Język CEL jest rozszerzalny, niezależny od aplikacji i zoptymalizowany pod kątem jednorazowych kompilacji, oceniających wiele przepływów pracy.

Wiele usług i aplikacji ocenia konfiguracje deklaratywne. Na przykład kontrola dostępu oparta na rolach (RBAC) to konfiguracja deklaratywna, która generuje decyzję o dostępie dla danej roli i zbioru użytkowników. Jeśli konfiguracje deklaratywne stanowią 80% przypadków użycia, CEL jest przydatnym narzędziem do zaokrąglania pozostałych 20%, gdy użytkownicy potrzebują bardziej wyrazistej mocy.

Kompilacja

Wyrażenie jest kompilowane w odniesieniu do środowiska. Podczas tego etapu kompilacji tworzone jest abstrakcyjne drzewo składni (AST) w formie protokołu protobuf. Skompilowane wyrażenia są zwykle przechowywane do wykorzystania w przyszłości, aby ocena przebiegała jak najszybciej. Jedno skompilowane wyrażenie może być oceniane z użyciem wielu różnych danych wejściowych.

Wyrażenia

Użytkownicy definiują wyrażenia; usługi i aplikacje definiują środowisko, w którym działają. Podpis funkcji deklaruje dane wejściowe i jest zapisywany poza wyrażeniem CEL. Biblioteka funkcji dostępnych dla języka CEL jest automatycznie importowana.

W poniższym przykładzie wyrażenie przyjmuje obiekt żądania, a żądanie zawiera token deklaracji. Wyrażenie zwraca wartość logiczną wskazującą, czy token deklaracji jest nadal prawidłowy.

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

Środowisko

Środowiska są definiowane przez usługi. Usługi i aplikacje osadzone w języku CEL deklarują środowisko wyrażeń. Środowisko to zbiór zmiennych i funkcji, których można używać w wyrażeniach.

Deklaracje oparte na proto są używane przez narzędzie do sprawdzania typów CEL do sprawdzania, czy wszystkie identyfikatory i odwołania do funkcji w wyrażeniu są deklarowane i używane prawidłowo.

3 fazy analizy wyrażenia

Przetwarzanie wyrażenia składa się z 3 etapów: analizy, sprawdzenia i oceny. Najpopularniejszym wzorcem CEL jest analiza i sprawdzanie wyrażeń przez platformę sterującą podczas konfiguracji oraz przechowywanie interfejsu AST.

c71fc08068759f81.png

W czasie działania obszar danych pobiera i ocenia AST wielokrotnie. Język CEL jest zoptymalizowany pod kątem wydajności środowiska wykonawczego, ale analiza i sprawdzanie nie powinny być wykonywane na ścieżkach kodu o znaczeniu krytycznym dla opóźnień.

49ab7d8517143b66.png

Kod CEL jest przekształcany z wyrażenia zrozumiałego dla człowieka na abstrakcyjne drzewo składniowe przy użyciu gramatyki leksera lub parsera ANTLR. Faza analizy generuje abstrakcyjne drzewo składniowe oparte na proto, w którym każdy węzeł Expr w AST zawiera identyfikator całkowity używany do indeksowania metadanych wygenerowanych podczas analizowania i sprawdzania. Plik syntax.proto utworzony podczas analizy wiernie odzwierciedla abstrakcyjną reprezentację tego, co zostało wpisane w formie ciągu znaków wyrażenia.

Po przeanalizowaniu wyrażenia można je sprawdzić w środowisku, aby upewnić się, że wszystkie zmienne i identyfikatory funkcji w wyrażeniu zostały zadeklarowane i są prawidłowo używane. Narzędzie do sprawdzania typów tworzy plik checked.proto zawierający metadane typu, zmiennej i rozdzielczości funkcji, które mogą znacznie zwiększyć wydajność oceny.

Tester CEL potrzebuje 3 elementów:

  • Powiązania funkcji dla dowolnych rozszerzeń niestandardowych
  • Powiązania zmiennych
  • AST do oceny

Powiązania funkcji i zmiennych powinny być zgodne z tym, co zostało użyte do skompilowania AST. Każde z tych danych wejściowych można wykorzystać ponownie w wielu ocenach, np. gdy AST jest oceniany w wielu zbiorach zmiennych zmiennych, te same zmienne używane w przypadku wielu AST lub powiązania funkcji używane przez cały okres użytkowania procesu (typowy przypadek).

3. Skonfiguruj

Kod do tego ćwiczenia w programie znajduje się w folderze codelab repozytorium cel-go. Rozwiązanie jest dostępne w folderze codelab/solution tego samego repozytorium.

Sklonuj i zapisz do repozytorium:

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

Uruchom kod za pomocą polecenia go run:

go run .

Powinny się wyświetlić te dane wyjściowe:

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

Gdzie są pakiety CEL?

W edytorze otwórz codelab/codelab.go. Powinna być widoczna główna funkcja, która napędza wykonanie ćwiczeń z tego ćwiczenia z programowania, oraz 3 bloki funkcji pomocniczych. Pierwszy zestaw elementów pomocniczych wspomaga fazy oceny CEL:

  • Funkcja Compile: analizowanie, sprawdzanie i wyrażenie wejściowe w odniesieniu do środowiska
  • Funkcja Eval: ocenia skompilowany program na podstawie danych wejściowych
  • Funkcja Report: ładnie wyświetla wynik oceny

Przy tworzeniu danych wejściowych do różnych ćwiczeń możesz też korzystać z pomocy request i auth.

W ćwiczeniach będą one odnosiły się do pakietów według ich krótkiej nazwy. Mapowanie z pakietu na lokalizację źródłową w repozytorium google/cel-go znajdziesz poniżej, jeśli chcesz poznać więcej szczegółów:

Pakiet

Lokalizacja źródłowa

Opis

cel

cel-go/cel

Interfejsy najwyższego poziomu

odsyłacz

cel-go/common/types/ref

Interfejsy referencyjne

typy

cel-go/common/types

Wartości typu środowiska wykonawczego

4. Witaj, świecie!

Tradycyjnie w przypadku wszystkich języków programowania zaczynamy od utworzenia i oceny tekstu „Hello World”.

Konfigurowanie środowiska

W edytorze znajdź deklarację exercise1 i wypełnij te pola, by skonfigurować środowisko:

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

Aplikacje CEL oceniają wyrażenie względem środowiska. env, err := cel.NewEnv() konfiguruje środowisko standardowe.

Środowisko można dostosować, podając opcje cel.EnvOption w rozmowie. Mogą one wyłączać makra, deklarować zmienne i funkcje niestandardowe.

Standardowe środowisko CEL obsługuje wszystkie typy, operatory, funkcje i makra zdefiniowane w specyfikacji języka.

Analiza i sprawdzanie wyrażenia

Po skonfigurowaniu środowiska można przeanalizować i sprawdzić wyrażenia. Dodaj do funkcji ten kod:

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

Wartość iss zwrócona przez wywołania Parse i Check to lista problemów, które mogą być błędami. Jeśli iss.Err() ma wartość inną niż nil, występuje błąd składni lub semantyki i program nie może kontynuować. Jeśli wyrażenie jest poprawne, wynikiem tych wywołań jest plik wykonywalny cel.Ast.

Oblicz wartość wyrażenia

Po przeanalizowaniu i zarejestrowaniu wyrażenia w elemencie cel.Ast można je przekonwertować na możliwy do oceny program, którego powiązania funkcji i tryby oceny można dostosować za pomocą opcji funkcjonalnych. Pamiętaj, że cel.Ast z protokołu można też odczytać przy użyciu funkcji cel.CheckedExprToAst lub cel.ParsedExprToAst.

Po zaplanowaniu obiektu cel.Program można go ocenić pod kątem danych wejściowych, wywołując funkcję Eval. Wynik funkcji Eval będzie zawierał wynik, szczegóły oceny i stan błędu.

Dodaj planowanie i zadzwoń do 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()
}

Dla zwięzłości pominiemy podane wyżej przypadki błędów w kolejnych ćwiczeniach.

Wykonaj kod

W wierszu poleceń uruchom ponownie kod:

go run .

Powinny wyświetlić się następujące wyniki oraz obiekty zastępcze dla przyszłych ćwiczeń.

=== Exercise 1: Hello World ===

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

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

5. Używanie zmiennych w funkcji

Większość aplikacji CEL deklaruje zmienne, do których można się odwoływać w wyrażeniach. Deklaracje zmiennych określają nazwę i typ. Typem zmiennej może być wbudowany typ CEL, dobrze znany typ bufora protokołu lub dowolny typ komunikatu protokołu protobuf, o ile jego deskryptor jest również dostarczany do CEL.

Dodaj funkcję

W edytorze znajdź deklarację exercise2 i dodaj:

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

Ponowne uruchomienie błędu i jego zrozumienie

Ponownie uruchom program:

go run .

Powinny się wyświetlić te dane wyjściowe:

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

Moduł sprawdzania typu generuje błąd dla obiektu żądania, który w prosty sposób zawiera fragment kodu źródłowego, w którym występuje błąd.

6. Deklarowanie zmiennych

Dodaj EnvOptions

Naprawmy błąd w edytorze, dodając deklarację obiektu żądania w postaci komunikatu typu 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()
}

Ponowne uruchomienie błędu i jego zrozumienie

Ponowne uruchamianie programu:

go run .

Powinien pojawić się następujący błąd:

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

Aby używać zmiennych odwołujących się do komunikatów buforów protokołu, mechanizm sprawdzania typów musi też znać deskryptor typu.

Użyj funkcji cel.Types, aby określić deskryptor żądania w Twojej funkcji:

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

Udało się ponownie uruchomić

Uruchom program ponownie:

go run .

Strona powinna wyglądać tak:

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

Aby to sprawdzić, zadeklarowaliśmy zmienną dla błędu, przypisaliśmy do niej deskryptor typu, a następnie odwołaliśmy się do tej zmiennej w ocenie wyrażenia.

7. Logiczne ORAZ/LUB

Jedną z wyjątkowych cech języka CEL jest użycie przemienników operatorów logicznych. Każda strona gałęzi warunkowej może spowodować zwarcie oceny, nawet w przypadku błędów lub częściowych danych wejściowych.

Innymi słowy, CEL znajduje kolejność oceny, która w miarę możliwości daje wynik. Ignoruje błędy, a nawet brakujące dane, które mogą wystąpić w innych kolejnościach oceny. Dzięki tej właściwości aplikacje mogą zminimalizować koszty oceny i opóźnić gromadzenie kosztownych danych wejściowych, jeśli nie uda się ich osiągnąć bez nich.

Dodamy przykład operatorów I/LUB, a następnie wypróbujemy go z innymi danymi wejściowymi, aby zrozumieć, w jaki sposób znikają zwarcia CEL.

Tworzenie funkcji

W edytorze dodaj do ćwiczenia 3 tę zawartość:

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

Następnie dołącz to wyrażenie LUB, które zwróci wartość „prawda”, jeśli użytkownik jest członkiem grupy admin lub ma określony identyfikator e-mail:

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

Na koniec dodaj przypadek eval oceniający użytkownika z pustym zestawem deklaracji:

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

Uruchom kod z pustym zestawem deklaracji

Ponowne uruchomienie programu powinno się wyświetlić następujące nowe dane wyjściowe:

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

Zaktualizuj wielkość liter

Następnie zmień wielkość liter w celu oceny, aby przekazać inny podmiot zabezpieczeń z pustym zestawem deklaracji:

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

Uruchamianie kodu z czasem

Uruchamiam program ponownie,

go run .

powinien zostać wyświetlony następujący błąd:

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

W protobufie wiemy, jakich pól i typów należy się spodziewać. W wartościach map i JSON nie wiemy, czy klucz jest dostępny. Ponieważ nie ma bezpiecznej wartości domyślnej dla brakującego klucza, w CEL domyślnie jest wybrany błąd.

8. Funkcje niestandardowe

Chociaż CEL oferuje wiele wbudowanych funkcji, w niektórych przypadkach przydatna jest funkcja niestandardowa. Funkcje niestandardowe mogą na przykład służyć do poprawy wygody użytkowników w przypadku typowych warunków lub do ujawnienia stanu zależnego od kontekstu

W tym ćwiczeniu sprawdzimy, jak udostępnić funkcję do pakowania najczęściej używanych kontroli.

Wywołanie funkcji niestandardowej

Najpierw utwórz kod konfiguracji zastąpienia o nazwie contains, które określa, czy klucz istnieje na mapie i ma określoną wartość. Pozostaw obiekty zastępcze definicji funkcji i powiązania funkcji:

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

Uruchom kod i zrozumiej, na czym polega błąd

Jeśli uruchomisz kod ponownie, powinien wyświetlić się następujący błąd:

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

Aby naprawić ten błąd, musimy dodać funkcję contains do listy deklaracji, które obecnie deklarują zmienną żądania.

Zadeklaruj typ z parametrami, dodając te 3 wiersze. Jest to tak skomplikowane, jak w przypadku języka CEL każde przeciążenie funkcji jest możliwe.

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

Dodawanie funkcji niestandardowej

Następnie dodamy nową funkcję „zawiera”, która będzie korzystać z typów z parametrami:

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

Uruchom program, aby przeanalizować błąd

Uruchom ćwiczenie. Powinien pojawić się następujący błąd dotyczący brakującej funkcji w czasie działania:

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

Podaj implementację funkcji w deklaracji NewEnv za pomocą funkcji 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()
}

Program powinien teraz działać bez problemów:

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

Co się stanie, kiedy zostanie zgłoszone roszczenie?

Aby uzyskać dodatkowe środki, spróbuj ustawić deklarację administratora dla danych wejściowych, aby sprawdzić, czy przeciążenie zawiera zwraca wartość „prawda”, jeśli istnieje roszczenie. Powinny się wyświetlić te dane wyjściowe:

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

Zanim przejdziesz dalej, sprawdź samą funkcję 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])
}

Aby ułatwić rozszerzenie, podpis funkcji niestandardowych oczekuje argumentów typu ref.Val. Wadą jest jednak to, że łatwość rozszerzenia przekłada się na obciążenie realizatora, co pozwala zapewnić prawidłową obsługę wszystkich typów wartości. Gdy typy lub liczba argumentów wejściowych nie są zgodne z deklaracją funkcji, powinien zostać zwrócony błąd no such overload.

cel.FunctionBinding() dodaje zabezpieczenie środowiska wykonawczego, aby mieć pewność, że umowa na środowisko wykonawcze jest zgodna z deklaracją ze zweryfikowanym typem w środowisku.

9. Kompiluję kod JSON

CEL może też generować dane wyjściowe inne niż logiczne, takie jak JSON. Dodaj do funkcji ten kod:

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

Uruchamianie kodu

Jeśli uruchomisz kod ponownie, powinien wyświetlić się następujący błąd:

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

Dodaj do pola cel.NewEnv() deklarację zmiennej now typu cel.TimestampType i wykonaj ponownie te czynności:

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

Uruchom kod ponownie – powinno się udać:

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

Program działa, ale wartość wyjściową out musi zostać jawnie przekonwertowana na format JSON. Wewnętrzna reprezentacja CEL w tym przypadku jest formatem konwertowalnym JSON, ponieważ odnosi się tylko do typów obsługiwanych przez JSON lub dla których jest dobrze znany mapowanie protokołu na format 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()
}

Gdy typ zostanie przekonwertowany przy użyciu funkcji pomocniczej valueToJSON w pliku codelab.go, powinny wyświetlić się te dodatkowe dane wyjściowe:

------ 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. Tworzenie Protos

W języku CEL można tworzyć komunikaty protokołu dla dowolnego typu wiadomości skompilowanego w aplikacji. Dodaj funkcję, aby utworzyć google.rpc.context.AttributeContext.Request na podstawie danych wejściowych 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()
}

Uruchamianie kodu

Jeśli uruchomisz kod ponownie, powinien wyświetlić się następujący błąd:

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

Kontener jest w zasadzie odpowiednikiem przestrzeni nazw lub pakietu, ale może być nawet tak szczegółowy jak nazwa wiadomości protobuf. Kontenery CEL używają tych samych reguł rozpoznawania przestrzeni nazw co Protobuf i C++ do określania, gdzie jest zadeklarowana dana nazwa zmiennej, funkcji lub typu.

Mając kontener google.rpc.context.AttributeContext, sprawdzanie typu i sprawdzający będzie wypróbować te nazwy identyfikatorów dla wszystkich zmiennych, typów i funkcji:

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

W przypadku nazw bezwzględnych poprzedź zmienną, typ lub odwołanie do funkcji kropką na początku. W tym przykładzie wyrażenie .<id> będzie wyszukiwać tylko identyfikator <id> najwyższego poziomu bez wcześniejszego sprawdzenia w kontenerze.

Określ opcję cel.Container("google.rpc.context.AttributeContext") dla środowiska CEL i uruchom ponownie:

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

 ...
}

Powinien pojawić się następujący wynik:

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

... i wiele innych błędów...

Następnie zadeklaruj zmienne jwt i now. Program powinien działać zgodnie z oczekiwaniami:

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

Aby uzyskać dodatkowe środki, wyodrębnij ten typ, wywołując w wiadomości funkcję out.Value(), aby zobaczyć, jak zmienią się wyniki.

11. Makra

Makra można używać do manipulowania programem CEL podczas analizy w czasie. Makra pasują do podpisu połączenia i modyfikują wywołanie wejściowe oraz jego argumenty w celu utworzenia nowego wyrażenia AST.

Makra mogą być używane do wdrażania w AST złożonej logiki, której nie można zapisać bezpośrednio w języku CEL. Na przykład makro has umożliwia testowanie obecności w polu. Makra interpretacji, takie jak „istnieje”, i zastępują wywołanie funkcji ograniczoną iteracją na liście danych wejściowych lub mapie. Żadne z tych koncepcji nie jest możliwe na poziomie składniowym, ale możliwe są rozwinięcie w postaci makro.

Dodaj i uruchom następne ćwiczenie:

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

Powinny się wyświetlić te błędy:

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

... i wiele innych ...

Te błędy występują, ponieważ makra nie są jeszcze włączone. Aby włączyć makra, usuń element cel.ClearMacros() i uruchom ponownie:

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

Obecnie obsługiwane są te makra:

Makro

Podpis

Opis

wszystkie

r.all(var, cond)

Przetestuj, czy warunek ma wartość prawda dla wszystkich zmiennych w zakresie r.

istnieje

r.exists(var, cond)

Przetestuj, czy cond zwraca wartość prawda dla dowolnej zmiennej w zakresie r.

exists_one

r.exists_one(var; cond)

Przetestuj, czy warunek ma wartość prawda dla tylko jednej zmiennej w zakresie r.

filtr

r.filter(var; cond)

W przypadku list utwórz nową listę, na której każda zmienna w zakresie r spełnia warunek. W przypadku map utwórz nową listę, na której każda zmienna kluczowa w zakresie r spełnia warunek.

mapa

r.map(var; expr)

Utwórz nową listę, w której każda zmienna w zakresie r jest przekształcona przez wyrażenie.

r.map(var, cond, expr)

Taka sama jak mapa z 2 argumentami, ale z filtrem warunkowym przed przekształceniem wartości.

zawiera

has(a.b)

Test obecności dla wartości b dla wartości a : w przypadku map definicja testów JSON. W przypadku protokołu testuje wartość podstawowa inną niż domyślna albo pole komunikatu lub ustawione pole wiadomości.

Gdy argument zakresu r jest typu map, klucz var jest kluczem mapy, a w przypadku wartości typu list wartość var – wartością elementu listy. Makra all, exists, exists_one, filter i map wykonują przepisywanie AST, które wykonuje każdą iterację ograniczoną do rozmiaru danych wejściowych.

Ograniczone interpretacje sprawiają, że programy CEL nie są w pełni ukończone przez Turinga, ale oceniają dane w czasie superliniowym względem danych wejściowych. Używaj tych makr z umiarem lub nie używaj ich wcale. Intensywne używanie wyjaśnień to zwykle dobry wskaźnik, że funkcja niestandardowa zapewni użytkownikom większą wygodę i lepszą wydajność.

12. Dostrajanie

Jest kilka funkcji, które są obecnie dostępne wyłącznie w CEL-Go, ale na pewno zapowiadają przyszłe wdrożenia tego języka. To ćwiczenie przedstawia różne plany programu dla tego samego 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

Gdy flaga optymalizacji jest włączona, CEL poświęca dodatkowy czas na tworzenie literałów i list z wyprzedzeniem oraz na optymalizację niektórych wywołań, takich jak operator in, tak aby pełnił funkcję prawdziwego testu członkostwa zestawu:

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

W przypadku wielokrotnego sprawdzania tego samego programu z uwzględnieniem różnych danych wejściowych, optymalizacja jest dobrym rozwiązaniem. Jeśli jednak program jest oceniany tylko raz, optymalizacja niesie ze sobą dodatkowe koszty.

Ocena wyczerpująca

Ocena wyczerpująca może być przydatna przy debugowaniu zachowania oceny wyrażenia, ponieważ zapewnia wgląd w obserwowaną wartość na każdym etapie oceny wyrażenia.

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

Powinna być widoczna lista stanu oceny wyrażenia dla każdego identyfikatora wyrażenia:

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

Identyfikator wyrażenia 2 odpowiada wynikowi operatora in w pierwszym z nich. gałąź, a wyrażenie identyfikator 11 odpowiada operatorowi == w sekundzie. Przy normalnej ocenie wyrażenie spowodowałoby zwarcie po obliczeniu wartości 2. Gdyby y nie było uint, stan przedstawiłby 2 przyczyny błędu wyrażenia, a nie tylko jedną.

13. Co zostało uwzględnione?

Jeśli potrzebujesz silnika wyrażeń, rozważ użycie języka CEL. Język CEL jest idealny dla projektów, w których wymagana jest konfiguracja użytkownika, gdy wydajność ma kluczowe znaczenie.

Mamy nadzieję, że z poprzednich ćwiczeń nauczysz się przekazywać dane do języka CEL i odzyskać wyniki lub decyzję.

Mamy nadzieję, że wiesz już, jakiego rodzaju operacje możesz wykonać – od decyzji logicznej po generowanie wiadomości JSON i Protobuffer.

Mamy nadzieję, że umiesz już zrozumieć, jak pracować z wyrażeniami i co one oznaczają. Rozumiemy też popularne sposoby jej wydłużania.