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

1. Wprowadzenie

Co to jest CEL?

CEL to język wyrażeń niekompletny w sensie Turinga, który został zaprojektowany z myślą o szybkości, przenośności i bezpieczeństwie wykonywania. CEL może być używany samodzielnie lub wbudowany w większy produkt.

Język CEL został zaprojektowany tak, aby bezpiecznie wykonywać w nim kod użytkownika. Chociaż bezkrytyczne wywoływanie funkcji eval() w kodzie Pythona użytkownika jest niebezpieczne, możesz bezpiecznie wykonywać kod CEL użytkownika. CEL zapobiega działaniom, które mogłyby obniżyć wydajność, dlatego bezpiecznie ocenia wyrażenia w czasie od nanosekund do mikrosekund. Jest to idealne rozwiązanie w przypadku aplikacji, w których wydajność ma kluczowe znaczenie.

CEL ocenia wyrażenia, które są podobne do funkcji jednoliniowych lub wyrażeń lambda. Wyrażenia CEL są zwykle używane do podejmowania decyzji logicznych, ale można ich też używać do tworzenia bardziej złożonych obiektów, takich jak wiadomości JSON lub protobuf.

Czy CEL jest odpowiedni dla Twojego projektu?

CEL ocenia wyrażenie z AST w nanosekundach lub mikrosekundach, dlatego idealnie nadaje się do aplikacji, w których wydajność ma kluczowe znaczenie. Kompilacja kodu CEL do AST nie powinna być wykonywana na ścieżkach krytycznych. Idealne zastosowania to te, w których konfiguracja jest często wykonywana i stosunkowo rzadko modyfikowana.

Na przykład wykonywanie zasad bezpieczeństwa przy każdym żądaniu HTTP do usługi to idealny przypadek użycia CEL, ponieważ zasady bezpieczeństwa rzadko się zmieniają, a CEL będzie miał znikomy wpływ na czas odpowiedzi. W tym przypadku CEL zwraca wartość logiczną, która określa, czy żądanie powinno zostać dozwolone, ale może też zwracać bardziej złożony komunikat.

Czego dowiesz się z tych ćwiczeń z programowania?

Pierwszy krok tego laboratorium kodu wyjaśnia, dlaczego warto używać CEL, i omawia podstawowe pojęcia. Pozostała część jest poświęcona ćwiczeniom z kodowania, które obejmują typowe przypadki użycia. Szczegółowe informacje o języku, semantyce i funkcjach znajdziesz w definicji języka CEL na GitHubie i dokumentacji CEL Go.

Te ćwiczenia z programowania są przeznaczone dla programistów, którzy chcą nauczyć się języka CEL, aby korzystać z usług, które już go obsługują. Ten moduł nie obejmuje integracji CEL z Twoim projektem.

Czego się nauczysz

  • Podstawowe pojęcia z CEL
  • Witaj świecie: używanie CEL do oceny ciągu znaków
  • Tworzenie zmiennych
  • Zrozumienie skracania obliczeń w CEL w przypadku operacji logicznych AND/OR
  • Jak używać CEL do tworzenia JSON
  • Jak używać CEL do tworzenia buforów protokołu
  • Tworzenie makr
  • Sposoby dostosowywania wyrażeń CEL

Czego potrzebujesz

Wymagania wstępne

Ten codelab jest przeznaczony dla osób, które znają podstawy buforów protokołujęzyka Go.

Jeśli nie znasz buforów protokołu, pierwsze ćwiczenie pozwoli Ci zrozumieć, jak działa CEL. Jednak bardziej zaawansowane przykłady używają buforów protokołu jako danych wejściowych dla CEL, więc mogą być trudniejsze do zrozumienia. Najpierw zapoznaj się z jednym z tych samouczków. Pamiętaj, że do korzystania z CEL nie są wymagane bufory protokołu, ale w tym module są one często używane.

Aby sprawdzić, czy środowisko Go jest zainstalowane, uruchom polecenie:

go --help

2. Kluczowych pojęć

Aplikacje

CEL jest językiem ogólnego przeznaczenia i był używany w różnych zastosowaniach, od routingu wywołań RPC po definiowanie zasad bezpieczeństwa. CEL jest rozszerzalny, niezależny od aplikacji i zoptymalizowany pod kątem przepływów pracy, w których kompilacja jest wykonywana raz, a ocena wiele razy.

Wiele usług i aplikacji ocenia deklaratywne konfiguracje. Na przykład kontrola dostępu oparta na rolach (RBAC) to deklaratywna konfiguracja, która na podstawie roli i zestawu użytkowników podejmuje decyzję o dostępie. Jeśli konfiguracje deklaratywne stanowią 80% przypadków użycia, CEL jest przydatnym narzędziem do uzupełnienia pozostałych 20% przypadków, w których użytkownicy potrzebują większej mocy wyrażania.

Kompilacja

Wyrażenie jest kompilowane w środowisku. W kroku kompilacji powstaje drzewo składni abstrakcyjnej (AST) w formacie protobuf. Skompilowane wyrażenia są zwykle przechowywane do wykorzystania w przyszłości, aby jak najbardziej przyspieszyć ocenę. Pojedyncze skompilowane wyrażenie można oceniać na podstawie wielu różnych danych wejściowych.

Wyrażenia

Użytkownicy definiują wyrażenia, a usługi i aplikacje określają środowisko, w którym są one uruchamiane. Sygnatura funkcji deklaruje dane wejściowe i jest zapisywana poza wyrażeniem CEL. Biblioteka funkcji dostępnych w CEL jest importowana automatycznie.

W tym przykładzie wyrażenie przyjmuje obiekt żądania, a żądanie zawiera token danych. Wyrażenie zwraca wartość logiczną wskazującą, czy token roszczeń jest nadal ważny.

// 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, które zawierają język CEL, deklarują środowisko wyrażeń. Środowisko to zbiór zmiennych i funkcji, których można używać w wyrażeniach.

Deklaracje oparte na protokole są używane przez moduł sprawdzania typów CEL, aby upewnić się, że wszystkie identyfikatory i odwołania do funkcji w wyrażeniu są zadeklarowane i używane prawidłowo.

3 fazy analizowania wyrażenia

Przetwarzanie wyrażenia obejmuje 3 fazy: analizowanie, sprawdzanie i obliczanie. Najczęstszym wzorcem w przypadku CEL jest parsowanie i sprawdzanie wyrażeń przez platformę sterującą w momencie konfiguracji oraz przechowywanie drzewa składni abstrakcyjnej.

c71fc08068759f81.png

W czasie działania obszar danych wielokrotnie pobiera i ocenia AST. CEL jest zoptymalizowany pod kątem wydajności w czasie działania, ale analizowanie i sprawdzanie nie powinno być wykonywane w przypadku ścieżek kodu o krytycznym znaczeniu dla opóźnienia.

49ab7d8517143b66.png

CEL jest analizowany z wyrażenia czytelnego dla człowieka do drzewa składni abstrakcyjnej za pomocą gramatyki analizatora leksykalnego i składniowego ANTLR. Faza analizy generuje drzewo składni abstrakcyjnej oparte na protokole, w którym każdy węzeł Expr w drzewie składni abstrakcyjnej zawiera identyfikator liczbowy używany do indeksowania metadanych generowanych podczas analizowania i sprawdzania. Plik syntax.proto wygenerowany podczas analizowania składni wiernie odzwierciedla abstrakcyjną reprezentację tego, co zostało wpisane w postaci ciągu znaków wyrażenia.

Po przeanalizowaniu wyrażenia można je sprawdzić w środowisku, aby upewnić się, że wszystkie identyfikatory zmiennych i funkcji w wyrażeniu zostały zadeklarowane i są używane prawidłowo. Sprawdzanie typów generuje plik checked.proto, który zawiera metadane dotyczące typów, zmiennych i funkcji, co może znacznie zwiększyć wydajność oceny.

Oceniający CEL potrzebuje 3 rzeczy:

  • Wiązania funkcji dla wszystkich rozszerzeń niestandardowych
  • Powiązania zmiennych
  • Drzewo składniowe do oceny

Powiązania funkcji i zmiennych powinny być zgodne z powiązaniami używanymi do kompilowania AST. Każde z tych danych wejściowych może być ponownie używane w wielu ocenach, np. drzewo składniowe może być oceniane w wielu zestawach powiązań zmiennych, te same zmienne mogą być używane w wielu drzewach składniowych, a powiązania funkcji mogą być używane przez cały czas trwania procesu (jest to częsty przypadek).

3. Skonfiguruj

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

Sklonuj repozytorium i przejdź do niego:

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

Uruchom kod za pomocą 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 się wyświetlić funkcja główna, która steruje wykonywaniem ćwiczeń w tym laboratorium, a za nią 3 bloki funkcji pomocniczych. Pierwszy zestaw funkcji pomocniczych ułatwia ocenę wyrażeń CEL:

  • Compile: analizuje i sprawdza wyrażenie wejściowe w środowisku.
  • Funkcja Eval: ocenia skompilowany program na podstawie danych wejściowych.
  • Funkcja Report: wyświetla wynik oceny w czytelnej formie

Dodatkowo udostępniliśmy funkcje pomocnicze requestauth, które ułatwiają tworzenie danych wejściowych do różnych ćwiczeń.

W ćwiczeniach pakiety będą określane za pomocą krótkich nazw. Jeśli chcesz poznać szczegóły, poniżej znajdziesz mapowanie pakietu na lokalizację źródłową w repozytorium google/cel-go:

Pakiet

Lokalizacja źródłowa

Opis

cel

cel-go/cel

Interfejsy najwyższego poziomu

ref

cel-go/common/types/ref

Interfejsy referencyjne

typy

cel-go/common/types

Wartości typu środowiska wykonawczego

4. Witaj świecie!

Zgodnie z tradycją wszystkich języków programowania zaczniemy od utworzenia i oceny programu „Hello World!”.

Konfigurowanie środowiska

W edytorze znajdź deklarację exercise1 i skonfiguruj środowisko, wpisując te wartości:

// 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 w odniesieniu do środowiska. env, err := cel.NewEnv() konfiguruje środowisko standardowe.

Środowisko można dostosować, przekazując do wywołania opcje cel.EnvOption. Te opcje umożliwiają wyłączanie makr, deklarowanie zmiennych i funkcji niestandardowych itp.

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

Analizowanie i sprawdzanie wyrażenia

Po skonfigurowaniu środowiska można analizować i sprawdzać wyrażenia. Dodaj do funkcji te informacje:

// 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 zwracana przez wywołania ParseCheck to lista problemów, które mogą być błędami. Jeśli wartość iss.Err() jest różna od zera, oznacza to, że wystąpił błąd składniowy lub semantyczny i program nie może kontynuować działania. Jeśli wyrażenie jest dobrze sformułowane, wynikiem tych wywołań jest wykonywalny obiekt cel.Ast.

Obliczanie wartości wyrażenia

Gdy wyrażenie zostanie przeanalizowane i sprawdzone w cel.Ast, można je przekształcić w program, który można ocenić, a jego powiązania funkcji i tryby oceny można dostosować za pomocą opcji funkcyjnych. Pamiętaj, że cel.Ast można też odczytać z protokołu za pomocą funkcji cel.CheckedExprToAst lub cel.ParsedExprToAst.

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

Dodaj planowanie i połączenie 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()
}

Aby nie powtarzać tych samych informacji, w przyszłych ćwiczeniach pominiemy przypadki błędów wymienione powyżej.

Uruchom kod

W wierszu poleceń uruchom ponownie kod:

go run .

Powinny się wyświetlić te dane wyjściowe wraz z wartościami zastępczymi 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. Typ zmiennej może być wbudowanym typem CEL, dobrze znanym typem bufora protokołu lub dowolnym typem wiadomości protobuf, o ile jego deskryptor jest też przekazywany do CEL.

Dodawanie funkcji

W edytorze znajdź deklarację exercise2 i dodaj ten kod:

// 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 i zrozumienie błędu

Ponowne uruchomienie programu:

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'
 | ^

Sprawdzanie typów generuje błąd dla obiektu żądania, który zawiera fragment kodu źródłowego, w którym występuje błąd.

6. Deklarowanie zmiennych

Dodaj EnvOptions

W edytorze naprawmy powstały błąd, podając deklarację obiektu żądania jako komunikat 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 i zrozumienie błędu

Ponowne uruchomienie programu:

go run .

Powinien wyświetlić się taki 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 wiadomości protobuf, narzędzie do sprawdzania typów musi też znać deskryptor typu.

Użyj cel.Types, aby określić deskryptor żądania w 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()
}

Ponowne uruchomienie zakończone.

Ponownie uruchom program:

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)

Podsumowując, zadeklarowaliśmy zmienną dla błędu, przypisaliśmy do niej deskryptor typu, a następnie odwołaliśmy się do niej w ocenie wyrażenia.

7. Operator logiczny LUB

Jedną z bardziej unikalnych funkcji języka CEL jest używanie przemiennych operatorów logicznych. Każda strona gałęzi warunkowej może przerwać ocenę, nawet w przypadku błędów lub częściowych danych wejściowych.

Innymi słowy, CEL znajduje kolejność obliczeń, która w miarę możliwości daje wynik, ignorując błędy lub nawet brakujące dane, które mogą wystąpić w innych kolejnościach obliczeń. Aplikacje mogą korzystać z tej właściwości, aby zminimalizować koszt oceny, odraczając zbieranie kosztownych danych wejściowych, gdy wynik można uzyskać bez nich.

Dodamy przykład z użyciem operatora AND/OR, a potem wypróbujemy go z różnymi danymi wejściowymi, aby sprawdzić, jak CEL skraca obliczenia.

Tworzenie funkcji

W edytorze dodaj do ćwiczenia 3 te wiersze:

// 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 dodaj to wyrażenie OR, które zwróci wartość „prawda”, jeśli użytkownik należy do 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 eval, który ocenia użytkownika z pustym zbiorem roszczeń:

// 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 zbiorem roszczeń.

Po ponownym uruchomieniu programu powinny się wyświetlić te 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)

Aktualizowanie zgłoszenia dotyczącego oceny

Następnie zaktualizuj przypadek oceny, aby przekazać innego podmiotu z pustym zbiorem roszczeń:

// 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 określonym czasem

ponowne uruchomienie programu,

go run .

Powinien wyświetlić się ten 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 przypadku protokołu buforów wiemy, jakich pól i typów się spodziewać. W przypadku wartości mapy i JSON nie wiemy, czy klucz będzie obecny. Ponieważ w przypadku brakującego klucza nie ma bezpiecznej wartości domyślnej, CEL domyślnie zwraca błąd.

8. Funkcje niestandardowe

CEL zawiera wiele wbudowanych funkcji, ale czasami przydatna jest funkcja niestandardowa. Funkcje niestandardowe mogą na przykład poprawiać komfort użytkowników w przypadku typowych warunków lub udostępniać stan zależny od kontekstu.

W tym ćwiczeniu dowiesz się, jak udostępnić funkcję, aby połączyć często używane sprawdzenia.

Wywoływanie funkcji niestandardowej

Najpierw utwórz kod, który skonfiguruje zastąpienie o nazwie contains. Określa ono, czy klucz istnieje w mapie i czy ma określoną wartość. Pozostaw symbole zastępcze dla 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 zrozum błąd

Po ponownym uruchomieniu kodu powinien pojawić się ten 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óra obecnie deklaruje zmienną żądania.

Zadeklaruj typ sparametryzowany, dodając te 3 wiersze. (To jest tak skomplikowane, jak przeciążenie funkcji w przypadku składni 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()
}

Dodawanie funkcji niestandardowej

Następnie dodamy nową funkcję contains, która będzie używać typów sparametryzowanych:

// 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 zrozumieć błąd

Uruchom ćwiczenie. Powinien wyświetlić się ten błąd dotyczący brakującej funkcji środowiska wykonawczego:

------ 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ć prawidłowo:

=== 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, gdy roszczenie już istnieje?

Aby uzyskać dodatkowe punkty, spróbuj ustawić roszczenie administratora w danych wejściowych, aby sprawdzić, czy przeciążenie zawiera również zwraca wartość „true”, gdy roszczenie istnieje. 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, warto przyjrzeć się samej funkcji 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 zapewnić jak największą łatwość rozszerzania, sygnatura funkcji niestandardowych oczekuje argumentów typu ref.Val. W tym przypadku łatwość rozszerzania wiąże się z koniecznością zapewnienia przez osobę wdrażającą prawidłowej obsługi wszystkich typów wartości. Jeśli typy lub liczba argumentów wejściowych nie pasują do deklaracji funkcji, powinien zostać zwrócony błąd no such overload.

Symbol cel.FunctionBinding() dodaje ochronę typu w czasie działania, aby zapewnić, że kontrakt w czasie działania jest zgodny z deklaracją sprawdzoną pod kątem typu w środowisku.

9. Tworzenie kodu JSON

CEL może też generować dane wyjściowe inne niż logiczne, np. JSON. Dodaj do funkcji te informacje:

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

Uruchom kod

Po ponownym uruchomieniu kodu powinien pojawić się ten błąd:

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

Dodaj deklarację zmiennej now typu cel.TimestampType do cel.NewEnv() i uruchom ponownie:

// 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 ponownie kod. Powinien zadziałać:

=== 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 trzeba jawnie przekonwertować na JSON. Wewnętrzna reprezentacja CEL w tym przypadku jest konwertowalna na JSON, ponieważ odnosi się tylko do typów, które JSON może obsługiwać lub dla których istnieje dobrze znane mapowanie Proto na 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()
}

Po przekonwertowaniu typu za pomocą funkcji pomocniczej valueToJSON w pliku codelab.go powinny się wyświetlić 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 prototypów

CEL może tworzyć wiadomości protobuf dla każdego typu wiadomości skompilowanego w aplikacji. Dodaj funkcję, która tworzy google.rpc.context.AttributeContext.Request z 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()
}

Uruchom kod

Po ponownym uruchomieniu kodu powinien pojawić się ten 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ł rozwiązywania przestrzeni nazw co Protobuf i C++, aby określić, gdzie zadeklarowana jest dana zmienna, funkcja lub nazwa typu.

W przypadku kontenera google.rpc.context.AttributeContext moduł sprawdzania typów i moduł obliczania będą próbować używać tych nazw 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 dodaj kropkę przed odwołaniem do zmiennej, typu lub funkcji. W tym przykładzie wyrażenie .<id> będzie wyszukiwać tylko identyfikator najwyższego poziomu <id> bez sprawdzania go w kontenerze.

Spróbuj określić opcję cel.Container("google.rpc.context.AttributeContext") dla środowiska CEL i uruchomić 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 jwtnow, a 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 punkty, rozpakuj typ, wywołując out.Value() w wiadomości, aby zobaczyć, jak zmienia się wynik.

11. Makra

Makr można używać do manipulowania programem CEL w czasie analizowania. Makra dopasowują sygnaturę wywołania i manipulują wywołaniem wejściowym oraz jego argumentami, aby utworzyć nowe podwyrażenie AST.

Makr można używać do implementowania w abstrakcyjnym drzewie składni złożonej logiki, której nie można zapisać bezpośrednio w CEL. Na przykład makro has umożliwia testowanie obecności pola. Makra comprehension, takie jak exists i all, zastępują wywołanie funkcji ograniczoną iteracją po liście lub mapie wejściowej. Żadna z tych koncepcji nie jest możliwa na poziomie składni, ale można je zrealizować za pomocą rozwinięć makr.

Dodaj i uruchom kolejne ć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ń 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)

Sprawdza, czy warunek cond jest spełniony dla wszystkich zmiennych var w zakresie r.

istnieje

r.exists(var, cond)

Sprawdza, czy warunek cond przyjmuje wartość true (prawda) dla dowolnej zmiennej var w zakresie r.

exists_one

r.exists_one(var, cond)

Sprawdza, czy warunek cond jest spełniony tylko w przypadku jednej zmiennej w zakresie r.

filtr

r.filter(var, cond)

W przypadku list utwórz nową listę, na której każdy element var w zakresie r spełnia warunek cond. W przypadku map utwórz nową listę, na której każdy klucz var w zakresie r spełnia warunek cond.

mapa

r.map(var, expr)

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

r.map(var, cond, expr)

Podobnie jak funkcja map z 2 argumentami, ale z filtrem warunkowym cond przed przekształceniem wartości.

zawiera

has(a.b)

Test obecności b na wartości a : w przypadku map definicja testów JSON. W przypadku protokołów testuje niedomyślną wartość pierwotną lub pole wiadomości.

Jeśli argument zakresu r jest typu map, var będzie kluczem mapy, a w przypadku wartości typu list var będzie wartością elementu listy. Makra all, exists, exists_one, filtermap wykonują przekształcenie AST, które przeprowadza iterację „for-each” ograniczoną rozmiarem danych wejściowych.

Ograniczone listy składane zapewniają, że programy CEL nie będą kompletne w sensie Turinga, ale są wykonywane w czasie superliniowym w stosunku do danych wejściowych. Używaj tych makr oszczędnie lub wcale. Intensywne korzystanie z wyrażeń listowych jest zwykle dobrym wskaźnikiem, że funkcja niestandardowa zapewni lepsze wrażenia użytkownika i większą wydajność.

12. Dostrajanie

Obecnie istnieje kilka funkcji dostępnych wyłącznie w CEL-Go, ale wskazują one na przyszłe plany dotyczące innych implementacji CEL. W poniższym ćwiczeniu przedstawiono różne plany programów 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 więcej czasu na wcześniejsze utworzenie listy i mapowanie literałów oraz optymalizuje niektóre wywołania, np. operatora in, aby był prawdziwym testem przynależności do zbioru:

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

Optymalizacja jest dobrym rozwiązaniem, gdy ten sam program jest wielokrotnie oceniany na podstawie różnych danych wejściowych. Jeśli jednak program ma być oceniany tylko raz, optymalizacja będzie tylko dodatkowym obciążeniem.

Szczegółowa ocena

Szczegółowa ocena może być przydatna do debugowania zachowania oceny wyrażeń, 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 się wyświetlić lista stanów oceny wyrażeń 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 pierwszej gałęzi, a identyfikator wyrażenia 11 odpowiada operatorowi == w drugiej gałęzi. W normalnej kolejności obliczania wyrażeń po obliczeniu wartości 2 nastąpiłoby przerwanie obliczeń. Gdyby zmienna y nie była typu uint, stan zawierałby 2 przyczyny niepowodzenia wyrażenia, a nie tylko 1.

13. Co zostało omówione?

Jeśli potrzebujesz silnika wyrażeń, rozważ użycie CEL. CEL to idealne rozwiązanie w przypadku projektów, w których trzeba wykonywać konfigurację użytkownika, a wydajność ma kluczowe znaczenie.

Mamy nadzieję, że w poprzednich ćwiczeniach udało Ci się opanować przekazywanie danych do CEL i otrzymywanie wyniku lub decyzji.

Mamy nadzieję, że masz już pojęcie o rodzajach operacji, które możesz wykonywać – od decyzji logicznych po generowanie wiadomości JSON i Protobuffer.

Mamy nadzieję, że rozumiesz już, jak działają wyrażenia i do czego służą. Znamy też typowe sposoby jego przedłużenia.