1. はじめに
CEL とは
CEL は、高速でポータブルかつ安全に実行できるように設計された、Turing 以外の完全な式言語です。CEL は単独で使用することも、より大きなプロダクトに埋め込むこともできます。
CEL は、ユーザーコードを安全に実行できる言語として設計されています。ユーザーの Python コードでやみくもに eval()
を呼び出すことは危険ですが、ユーザーの CEL コードを安全に実行できます。また、CEL はパフォーマンスを低下させる動作を防ぐので、ナノ秒からマイクロ秒のオーダーで安全に評価できます。パフォーマンス重視のアプリケーションに最適です
CEL では式が評価されます。これは、単一行関数やラムダ式に似ています。CEL はブール値の決定によく使用されますが、JSON や protobuf メッセージなどのより複雑なオブジェクトの構築にも使用できます。
CEL がプロジェクトに適しているか
CEL は AST からの式をナノ秒単位からマイクロ秒単位で評価するため、CEL の理想的なユースケースは、パフォーマンスが重視されるパスを持つアプリケーションです。CEL コードの AST へのコンパイルはクリティカル パスで行わないでください。構成の実行頻度が高く、変更の頻度が比較的低いアプリケーションが理想的です。
たとえば、サービスへの HTTP リクエストごとにセキュリティ ポリシーを実行するのが CEL の理想的なユースケースです。セキュリティ ポリシーはめったに変更されず、CEL によるレスポンス時間への影響もわずかであるためです。この場合、リクエストを許可するかどうかに応じて CEL はブール値を返しますが、より複雑なメッセージを返すこともできます。
この Codelab の内容
この Codelab の最初のステップでは、CEL を使用する目的と、そのコアコンセプトについて説明します。残りは、一般的なユースケースを取り上げたコーディング演習に特化しています。言語、セマンティクス、機能の詳細については、GitHub の CEL 言語定義と CEL Go ドキュメントをご覧ください。
この Codelab は、すでに CEL をサポートしているサービスを使用するために CEL を学習したいと考えているデベロッパーを対象としています。この Codelab では、CEL を独自のプロジェクトに統合する方法については説明しません。
学習内容
- CEL の基本コンセプト
- Hello, World: CEL を使用した String の評価
- 変数の作成
- 論理 AND/OR 演算における CEL の短絡を理解する
- CEL を使用して JSON を作成する方法
- CEL を使用して Protobuffers をビルドする方法
- マクロの作成
- CEL 式を調整する方法
必要なもの
前提条件
この Codelab は、プロトコル バッファと Go Lang に関する基礎知識に基づいています。
プロトコル バッファに詳しくない方は、最初の演習で CEL の仕組みを理解できます。ただし、より高度な例では CEL への入力としてプロトコル バッファを使用しているため、わかりにくい可能性があります。まず、こちらのチュートリアルのいずれかを実践することを検討してください。CEL を使用するためにプロトコル バッファは必要ありませんが、この Codelab ではプロトコル バッファを幅広く使用します。
次のコマンドを実行して、go がインストールされていることをテストできます。
go --help
2. 主なコンセプト
アプリケーション
CEL は汎用的で、RPC のルーティングからセキュリティ ポリシーの定義まで、さまざまなアプリケーションで使用されています。CEL は拡張可能でアプリケーションに依存しないため、1 回のコンパイルと複数回の評価のワークフロー向けに最適化されています。
多くのサービスやアプリケーションは、宣言型の構成を評価します。たとえば、ロールベース アクセス制御(RBAC)は、特定のロールと一連のユーザーに基づいてアクセスを決定する宣言型構成です。宣言型の構成が 80% のユースケースである場合、CEL は、より表現力を強化する必要があるときに残りの 20% を丸めるのに役立つツールです。
コンパイル
式は環境に対してコンパイルされます。このコンパイル ステップで、protobuf 形式で抽象構文ツリー(AST)が生成されます。コンパイルされた式は通常、可能な限り高速に評価を行うために、後で使用するために保存されます。コンパイルされた単一の式は、さまざまな入力で評価できます。
式
ユーザーが式を定義します。実行される環境を定義します。関数シグネチャは入力を宣言し、CEL 式の外部に記述します。CEL で使用できる関数のライブラリは自動インポートされます。
次の例では、式がリクエスト オブジェクトを受け取り、リクエストにクレーム トークンが含まれています。この式は、クレーム トークンがまだ有効かどうかを示すブール値を返します。
// Check whether a JSON Web Token has expired by inspecting the 'exp' claim.
//
// Args:
// claims - authentication claims.
// now - timestamp indicating the current system time.
// Returns: true if the token has expired.
//
timestamp(claims["exp"]) < now
環境
環境はサービスによって定義されます。CEL を埋め込むサービスとアプリケーションは式環境を宣言します。環境とは、式で使用できる変数と関数のコレクションです。
proto ベースの宣言は、式内のすべての識別子と関数参照が正しく宣言され、使用されていることを保証するために、CEL 型チェッカーによって使用されます。
式の解析の 3 つのフェーズ
式の処理には、解析、チェック、評価の 3 つのフェーズがあります。CEL の最も一般的なパターンは、コントロール プレーンが構成時に式を解析してチェックし、AST を保存することです。
実行時に、データプレーンは AST を繰り返し取得して評価します。CEL はランタイムの効率化のために最適化されていますが、レイテンシが重要なコードパスでは解析とチェックを行うべきではありません。
CEL は、ANTLR レキサー / パーサー文法を使用して、人が読める式から抽象構文ツリーに解析されます。解析フェーズは、proto ベースの抽象構文ツリーを出力します。このツリーでは、AST の各 Expr ノードに整数 ID が含まれています。この整数 ID は、解析およびチェック中に生成されるメタデータへのインデックス付けに使用されます。解析中に生成される syntax.proto は、式の文字列形式で入力された内容の抽象表現を忠実に表現します。
式が解析された後、環境と照合して、式内のすべての変数 ID と関数 ID が宣言され、正しく使用されているかどうか確認します。型チェッカーは、評価の効率を大幅に向上させる型、変数、関数解決メタデータを含む checked.proto を生成します。
CEL エバリュエータには次の 3 つが必要です。
- カスタム拡張機能の関数バインディング
- 変数のバインディング
- 評価する AST
関数と変数のバインディングは、AST のコンパイルに使用されたものと一致する必要があります。これらの入力は、複数の変数バインディングのセットで評価される AST、多くの AST で使用される同じ変数、プロセスの存続期間全体で使用される関数バインディング(一般的なケース)など、複数の評価で再利用できます。
3. 設定
この Codelab のコードは、cel-go
リポジトリの codelab
フォルダにあります。この解答は、同じリポジトリの codelab/solution
フォルダから入手できます。
クローンを作成して、リポジトリに移動します。
git clone https://github.com/google/cel-go.git
cd cel-go/codelab
go run
を使用してコードを実行します。
go run .
次の出力が表示されます。
=== Exercise 1: Hello World ===
=== Exercise 2: Variables ===
=== Exercise 3: Logical AND/OR ===
=== Exercise 4: Customization ===
=== Exercise 5: Building JSON ===
=== Exercise 6: Building Protos ===
=== Exercise 7: Macros ===
=== Exercise 8: Tuning ===
CEL パッケージはどこにありますか?
エディタで codelab/codelab.go
を開きます。この Codelab の演習を実行する main 関数と、それに続く 3 つのヘルパー関数ブロックが表示されます。最初のヘルパーセットは、CEL 評価のフェーズを支援します。
Compile
関数: 式を解析してチェックし、環境に対して入力を行います。Eval
関数: コンパイル済みのプログラムを入力に照らして評価します。Report
関数: 評価結果をプリティ プリントします。
さらに、さまざまな演習の入力構築を支援する request
ヘルパーと auth
ヘルパーも用意されています。
演習では、短いパッケージ名でパッケージを参照します。google/cel-go
リポジトリ内のパッケージからソースの場所へのマッピングは、以下のようになります。
パッケージ | ソースの場所 | 説明 |
cel | トップレベル インターフェース | |
ref | リファレンス インターフェース | |
タイプ | ランタイム型の値 |
4. Hello World!
あらゆるプログラミング言語の慣例では、まず「Hello World!」を作成して評価します。
環境を構成する
エディタで exercise1
の宣言を探し、以下を入力して環境を設定します。
// exercise1 evaluates a simple literal expression: "Hello, World!"
//
// Compile, eval, profit!
func exercise1() {
fmt.Println("=== Exercise 1: Hello World ===\n")
// Create the standard environment.
env, err := cel.NewEnv()
if err != nil {
glog.Exitf("env error: %v", err)
}
// Will add the parse and check steps here
}
CEL アプリケーションは、環境に対して式を評価します。env, err := cel.NewEnv()
はスタンダード環境を構成します。
環境をカスタマイズするには、呼び出しに cel.EnvOption
オプションを指定します。これらのオプションを使用すると、マクロを無効にしたり、カスタム変数や関数の宣言を行ったりできます。
標準の CEL 環境では、言語仕様で定義されているすべての型、演算子、関数、マクロがサポートされています。
式を解析してチェックする
環境が構成されたら、式を解析してチェックできます。関数に以下を追加します。
// exercise1 evaluates a simple literal expression: "Hello, World!"
//
// Compile, eval, profit!
func exercise1() {
fmt.Println("=== Exercise 1: Hello World ===\n")
// Create the standard environment.
env, err := cel.NewEnv()
if err != nil {
glog.Exitf("env error: %v", err)
}
// Check that the expression compiles and returns a String.
ast, iss := env.Parse(`"Hello, World!"`)
// Report syntactic errors, if present.
if iss.Err() != nil {
glog.Exit(iss.Err())
}
// Type-check the expression for correctness.
checked, iss := env.Check(ast)
// Report semantic errors, if present.
if iss.Err() != nil {
glog.Exit(iss.Err())
}
// Check the output type is a string.
if !reflect.DeepEqual(checked.OutputType(), cel.StringType) {
glog.Exitf(
"Got %v, wanted %v output type",
checked.OutputType(), cel.StringType,
)
}
// Will add the planning step here
}
Parse
と Check
の呼び出しによって返される iss
値は、エラーの可能性がある問題のリストです。iss.Err()
が nil 以外の場合、構文またはセマンティクスにエラーがあり、プログラムは続行できません。式が正しい形式の場合、これらの呼び出しの結果は実行可能な cel.Ast
になります。
式を評価する
式が解析されて cel.Ast
にチェックインされると、評価可能なプログラムに変換できます。このプログラムでは、関数バインディングと評価モードを関数オプションを使用してカスタマイズできます。なお、cel.CheckedExprToAst 関数または cel.ParsedExprToAst 関数を使用して、proto から cel.Ast
を読み取ることもできます。
cel.Program
のプランニングが完了したら、Eval
を呼び出すことで、入力に対して評価できます。Eval
の結果には、結果、評価の詳細、エラー ステータスが含まれます。
計画を追加して、eval
を呼び出します。
// exercise1 evaluates a simple literal expression: "Hello, World!"
//
// Compile, eval, profit!
func exercise1() {
fmt.Println("=== Exercise 1: Hello World ===\n")
// Create the standard environment.
env, err := cel.NewEnv()
if err != nil {
glog.Exitf("env error: %v", err)
}
// Check that the expression compiles and returns a String.
ast, iss := env.Parse(`"Hello, World!"`)
// Report syntactic errors, if present.
if iss.Err() != nil {
glog.Exit(iss.Err())
}
// Type-check the expression for correctness.
checked, iss := env.Check(ast)
// Report semantic errors, if present.
if iss.Err() != nil {
glog.Exit(iss.Err())
}
// Check the output type is a string.
if !reflect.DeepEqual(checked.OutputType(), cel.StringType) {
glog.Exitf(
"Got %v, wanted %v output type",
checked.OutputType(), cel.StringType)
}
// Plan the program.
program, err := env.Program(checked)
if err != nil {
glog.Exitf("program error: %v", err)
}
// Evaluate the program without any additional arguments.
eval(program, cel.NoVars())
fmt.Println()
}
簡略化のために、上記のエラーケースは今後の演習からは省略します。
コードを実行する
コマンドラインでコードを再実行します。
go run .
次の出力と、今後の演習のプレースホルダが表示されます。
=== Exercise 1: Hello World ===
------ input ------
(interpreter.emptyActivation)
------ result ------
value: Hello, World! (types.String)
5. 関数で変数を使用する
ほとんどの CEL アプリケーションでは、式内で参照できる変数を宣言します。変数の宣言では、名前と型を指定します。変数の型は、CEL 組み込み型、プロトコル バッファのよく知られた型、または任意の protobuf メッセージ型のいずれかで、記述子も CEL に提供されている必要があります。
関数を追加する
エディタで exercise2
の宣言を見つけて、以下を追加します。
// exercise2 shows how to declare and use variables in expressions.
//
// Given a request of type google.rpc.context.AttributeContext.Request
// determine whether a specific auth claim is set.
func exercise2() {
fmt.Println("=== Exercise 2: Variables ===\n")
env, err := cel.NewEnv(
// Add cel.EnvOptions values here.
)
if err != nil {
glog.Exit(err)
}
ast := compile(env, `request.auth.claims.group == 'admin'`, cel.BoolType)
program, _ := env.Program(ast)
// Evaluate a request object that sets the proper group claim.
claims := map[string]string{"group": "admin"}
eval(program, request(auth("user:me@acme.co", claims), time.Now()))
fmt.Println()
}
再実行してエラーの内容を理解する
プログラムを再実行します。
go run .
次の出力が表示されます。
ERROR: <input>:1:1: undeclared reference to 'request' (in container '')
| request.auth.claims.group == 'admin'
| ^
型チェッカーはリクエスト オブジェクトに対してエラーを生成します。このオブジェクトには、エラーが発生するソース スニペットを簡単に含めることができます。
6. 変数を宣言する
EnvOptions
を追加
エディタで、次のようにリクエスト オブジェクトの宣言を google.rpc.context.AttributeContext.Request
型のメッセージとして指定して、結果のエラーを修正しましょう。
// exercise2 shows how to declare and use variables in expressions.
//
// Given a `request` of type `google.rpc.context.AttributeContext.Request`
// determine whether a specific auth claim is set.
func exercise2() {
fmt.Println("=== Exercise 2: Variables ===\n")
env, err := cel.NewEnv(
cel.Variable("request",
cel.ObjectType("google.rpc.context.AttributeContext.Request"),
),
)
if err != nil {
glog.Exit(err)
}
ast := compile(env, `request.auth.claims.group == 'admin'`, cel.BoolType)
program, _ := env.Program(ast)
// Evaluate a request object that sets the proper group claim.
claims := map[string]string{"group": "admin"}
eval(program, request(auth("user:me@acme.co", claims), time.Now()))
fmt.Println()
}
再実行してエラーの内容を理解する
プログラムを再度実行します。
go run .
次のようなエラーが表示されます。
ERROR: <input>:1:8: [internal] unexpected failed resolution of 'google.rpc.context.AttributeContext.Request'
| request.auth.claims.group == 'admin'
| .......^
protobuf メッセージを参照する変数を使用するには、型チェッカーが型記述子も認識している必要があります。
cel.Types
を使用して、関数内のリクエストの記述子を決定します。
// exercise2 shows how to declare and use variables in expressions.
//
// Given a `request` of type `google.rpc.context.AttributeContext.Request`
// determine whether a specific auth claim is set.
func exercise2() {
fmt.Println("=== Exercise 2: Variables ===\n")
env, err := cel.NewEnv(
cel.Types(&rpcpb.AttributeContext_Request{}),
cel.Variable("request",
cel.ObjectType("google.rpc.context.AttributeContext.Request"),
),
)
if err != nil {
glog.Exit(err)
}
ast := compile(env, `request.auth.claims.group == 'admin'`, cel.BoolType)
program, _ := env.Program(ast)
// Evaluate a request object that sets the proper group claim.
claims := map[string]string{"group": "admin"}
eval(program, request(auth("user:me@acme.co", claims), time.Now()))
fmt.Println()
}
正常に再実行します。
プログラムを再度実行します。
go run .
次の結果が表示されます。
=== Exercise 2: Variables ===
request.auth.claims.group == 'admin'
------ input ------
request = time: <
seconds: 1569255569
>
auth: <
principal: "user:me@acme.co"
claims: <
fields: <
key: "group"
value: <
string_value: "admin"
>
>
>
>
------ result ------
value: true (types.Bool)
おさらいすると、エラーの変数を宣言し、型記述子を割り当て、式の評価でその変数を参照しただけです。
7. 論理 AND/OR
CEL のよりユニークな機能の一つは、交換論理演算子の使用です。エラーや部分的な入力があっても、条件分岐のどちらの側も評価を短絡させる可能性があります。
つまり、CEL は可能な限り結果を返す評価順序を見つけます。他の評価順序で発生する可能性のあるエラーやデータの欠落は無視します。アプリケーションは、このプロパティを利用して評価のコストを最小限に抑え、高コストな入力値なしで結果に到達できる場合は、その収集を先延ばしにできます。
AND/OR の例を追加して、別の入力で試して、CEL が短絡評価を行う方法を理解します。
関数の作成
エディタで、次の内容を演習 3 に追加します。
// exercise3 demonstrates how CEL's commutative logical operators work.
//
// Construct an expression which checks if the `request.auth.claims.group`
// value is equal to admin or the `request.auth.principal` is
// `user:me@acme.co`. Issue two requests, one that specifies the proper
// user, and one that specifies an unexpected user.
func exercise3() {
fmt.Println("=== Exercise 3: Logical AND/OR ===\n")
env, _ := cel.NewEnv(
cel.Types(&rpcpb.AttributeContext_Request{}),
cel.Variable("request",
cel.ObjectType("google.rpc.context.AttributeContext.Request"),
),
)
}
次に、ユーザーが admin
グループのメンバーであるか、特定のメール ID を持っている場合に true を返す次の OR ステートメントを追加します。
// exercise3 demonstrates how CEL's commutative logical operators work.
//
// Construct an expression which checks if the `request.auth.claims.group`
// value is equal to admin or the `request.auth.principal` is
// `user:me@acme.co`. Issue two requests, one that specifies the proper
// user, and one that specifies an unexpected user.
func exercise3() {
fmt.Println("=== Exercise 3: Logical AND/OR ===\n")
env, _ := cel.NewEnv(
cel.Types(&rpcpb.AttributeContext_Request{}),
cel.Variable("request",
cel.ObjectType("google.rpc.context.AttributeContext.Request"),
),
)
ast := compile(env,
`request.auth.claims.group == 'admin'
|| request.auth.principal == 'user:me@acme.co'`,
cel.BoolType)
program, _ := env.Program(ast)
}
最後に、空のクレームセットでユーザーを評価する eval
ケースを追加します。
// exercise3 demonstrates how CEL's commutative logical operators work.
//
// Construct an expression which checks whether the request.auth.claims.group
// value is equal to admin or the request.auth.principal is
// user:me@acme.co. Issue two requests, one that specifies the proper user,
// and one that specifies an unexpected user.
func exercise3() {
fmt.Println("=== Exercise 3: Logical AND/OR ===\n")
env, _ := cel.NewEnv(
cel.Types(&rpcpb.AttributeContext_Request{}),
cel.Variable("request",
cel.ObjectType("google.rpc.context.AttributeContext.Request"),
),
)
ast := compile(env,
`request.auth.claims.group == 'admin'
|| request.auth.principal == 'user:me@acme.co'`,
cel.BoolType)
program, _ := env.Program(ast)
emptyClaims := map[string]string{}
eval(program, request(auth("user:me@acme.co", emptyClaims), time.Now()))
}
空のクレームセットを使用してコードを実行する
プログラムを再実行すると、次の新しい出力が表示されます。
=== Exercise 3: Logical AND/OR ===
request.auth.claims.group == 'admin'
|| request.auth.principal == 'user:me@acme.co'
------ input ------
request = time: <
seconds: 1569302377
>
auth: <
principal: "user:me@acme.co"
claims: <
>
>
------ result ------
value: true (types.Bool)
評価ケースを更新する
次に、評価ケースを更新して、空のクレームセットを持つ別のプリンシパルを渡します。
// exercise3 demonstrates how CEL's commutative logical operators work.
//
// Construct an expression which checks whether the request.auth.claims.group
// value is equal to admin or the request.auth.principal is
// user:me@acme.co. Issue two requests, one that specifies the proper user,
// and one that specifies an unexpected user.
func exercise3() {
fmt.Println("=== Exercise 3: Logical AND/OR ===\n")
env, _ := cel.NewEnv(
cel.Types(&rpcpb.AttributeContext_Request{}),
cel.Variable("request",
cel.ObjectType("google.rpc.context.AttributeContext.Request"),
),
)
ast := compile(env,
`request.auth.claims.group == 'admin'
|| request.auth.principal == 'user:me@acme.co'`,
cel.BoolType)
program, _ := env.Program(ast)
emptyClaims := map[string]string{}
eval(program, request(auth("other:me@acme.co", emptyClaims), time.Now()))
}
時間を指定してコードを実行する
プログラムを再実行すると
go run .
次のエラーが表示されます。
=== Exercise 3: Logical AND/OR ===
request.auth.claims.group == 'admin'
|| request.auth.principal == 'user:me@acme.co'
------ input ------
request = time: <
seconds: 1588989833
>
auth: <
principal: "user:me@acme.co"
claims: <
>
>
------ result ------
value: true (types.Bool)
------ input ------
request = time: <
seconds: 1588989833
>
auth: <
principal: "other:me@acme.co"
claims: <
>
>
------ result ------
error: no such key: group
protobuf では、期待されるフィールドと型がわかっています。Map 値と JSON 値では、キーが存在するかどうかがわからない。キーが欠落している場合の安全なデフォルト値がないため、CEL はデフォルトでエラーになります。
8. カスタム関数
CEL には多くの組み込み関数がありますが、カスタム関数は便利な場合もあります。たとえば、カスタム関数を使用して、一般的な条件に対するユーザー エクスペリエンスを改善したり、コンテキスト センシティブな状態を公開したりできます。
この演習では、関数を公開して、よく使用されるチェックをパッケージ化する方法を説明します。
カスタム関数を呼び出す
まず、キーがマップに存在し、特定の値を持つかどうかを判別する contains
というオーバーライドを設定するコードを作成します。プレースホルダは、関数定義と関数バインディング用に残します。
// exercise4 demonstrates how to extend CEL with custom functions.
// Declare a `contains` member function on map types that returns a boolean
// indicating whether the map contains the key-value pair.
func exercise4() {
fmt.Println("=== Exercise 4: Customization ===\n")
// Determine whether an optional claim is set to the proper value. The
// custom map.contains(key, value) function is used as an alternative to:
// key in map && map[key] == value
env, _ := cel.NewEnv(
cel.Types(&rpcpb.AttributeContext_Request{}),
// Declare the request.
cel.Variable("request",
cel.ObjectType("google.rpc.context.AttributeContext.Request"),
),
// Add the custom function declaration and binding here with cel.Function()
)
ast := compile(env,
`request.auth.claims.contains('group', 'admin')`,
cel.BoolType)
// Construct the program plan and provide the 'contains' function impl.
// Output: false
program, _ := env.Program(ast)
emptyClaims := map[string]string{}
eval(
program,
request(auth("user:me@acme.co", emptyClaims), time.Now()),
)
fmt.Println()
}
コードを実行してエラーを把握する
コードを再実行すると、次のエラーが表示されます。
ERROR: <input>:1:29: found no matching overload for 'contains' applied to 'map(string, dyn).(string, string)'
| request.auth.claims.contains('group', 'admin')
| ............................^
このエラーを修正するには、現在 request 変数を宣言している宣言のリストに contains
関数を追加する必要があります。
次の 3 行を追加して、パラメータ化された型を宣言します。(これは、CEL の関数のオーバーロードと同様に複雑です)。
// exercise4 demonstrates how to extend CEL with custom functions.
// Declare a `contains` member function on map types that returns a boolean
// indicating whether the map contains the key-value pair.
func exercise4() {
fmt.Println("=== Exercise 4: Customization ===\n")
// Determine whether an optional claim is set to the proper value. The custom
// map.contains(key, value) function is used as an alternative to:
// key in map && map[key] == value
// Useful components of the type-signature for 'contains'.
typeParamA := cel.TypeParamType("A")
typeParamB := cel.TypeParamType("B")
mapAB := cel.MapType(typeParamA, typeParamB)
env, _ := cel.NewEnv(
cel.Types(&rpcpb.AttributeContext_Request{}),
// Declare the request.
cel.Variable("request",
cel.ObjectType("google.rpc.context.AttributeContext.Request"),
),
// Add the custom function declaration and binding here with cel.Function()
)
ast := compile(env,
`request.auth.claims.contains('group', 'admin')`,
cel.BoolType)
// Construct the program plan.
// Output: false
program, _ := env.Program(ast)
emptyClaims := map[string]string{}
eval(program, request(auth("user:me@acme.co", emptyClaims), time.Now()))
fmt.Println()
}
カスタム関数を追加する
次に、パラメータ化された型を使用する新しい contains 関数を追加します。
// exercise4 demonstrates how to extend CEL with custom functions.
// Declare a `contains` member function on map types that returns a boolean
// indicating whether the map contains the key-value pair.
func exercise4() {
fmt.Println("=== Exercise 4: Customization ===\n")
// Determine whether an optional claim is set to the proper value. The custom
// map.contains(key, value) function is used as an alternative to:
// key in map && map[key] == value
// Useful components of the type-signature for 'contains'.
typeParamA := cel.TypeParamType("A")
typeParamB := cel.TypeParamType("B")
mapAB := cel.MapType(typeParamA, typeParamB)
// Env declaration.
env, _ := cel.NewEnv(
cel.Types(&rpcpb.AttributeContext_Request{}),
// Declare the request.
cel.Variable("request",
cel.ObjectType("google.rpc.context.AttributeContext.Request"),
),
// Declare the custom contains function and its implementation.
cel.Function("contains",
cel.MemberOverload(
"map_contains_key_value",
[]*cel.Type{mapAB, typeParamA, typeParamB},
cel.BoolType,
// Provide the implementation using cel.FunctionBinding()
),
),
)
ast := compile(env,
`request.auth.claims.contains('group', 'admin')`,
cel.BoolType)
// Construct the program plan and provide the 'contains' function impl.
// Output: false
program, _ := env.Program(ast)
emptyClaims := map[string]string{}
eval(program, request(auth("user:me@acme.co", emptyClaims), time.Now()))
fmt.Println()
}
プログラムを実行してエラーを確認する
エクササイズを実行します。ランタイム関数がない旨、次のエラーが表示されるはずです。
------ result ------
error: no such overload: contains
cel.FunctionBinding()
関数を使用して、関数の実装を NewEnv
宣言に指定します。
// exercise4 demonstrates how to extend CEL with custom functions.
//
// Declare a contains member function on map types that returns a boolean
// indicating whether the map contains the key-value pair.
func exercise4() {
fmt.Println("=== Exercise 4: Customization ===\n")
// Determine whether an optional claim is set to the proper value. The custom
// map.contains(key, value) function is used as an alternative to:
// key in map && map[key] == value
// Useful components of the type-signature for 'contains'.
typeParamA := cel.TypeParamType("A")
typeParamB := cel.TypeParamType("B")
mapAB := cel.MapType(typeParamA, typeParamB)
// Env declaration.
env, _ := cel.NewEnv(
cel.Types(&rpcpb.AttributeContext_Request{}),
// Declare the request.
cel.Variable("request",
cel.ObjectType("google.rpc.context.AttributeContext.Request"),
),
// Declare the custom contains function and its implementation.
cel.Function("contains",
cel.MemberOverload(
"map_contains_key_value",
[]*cel.Type{mapAB, typeParamA, typeParamB},
cel.BoolType,
cel.FunctionBinding(mapContainsKeyValue)),
),
)
ast := compile(env,
`request.auth.claims.contains('group', 'admin')`,
cel.BoolType)
// Construct the program plan.
// Output: false
program, err := env.Program(ast)
if err != nil {
glog.Exit(err)
}
eval(program, request(auth("user:me@acme.co", emptyClaims), time.Now()))
claims := map[string]string{"group": "admin"}
eval(program, request(auth("user:me@acme.co", claims), time.Now()))
fmt.Println()
}
プログラムは正常に実行されるはずです。
=== Exercise 4: Custom Functions ===
request.auth.claims.contains('group', 'admin')
------ input ------
request = time: <
seconds: 1569302377
>
auth: <
principal: "user:me@acme.co"
claims: <
>
>
------ result ------
value: false (types.Bool)
申し立てが存在する場合
さらに評価するために、入力に管理者クレームを設定して、クレームが存在する場合に、含むオーバーロードも true を返すことを確認してください。次の出力が表示されます。
=== Exercise 4: Customization ===
request.auth.claims.contains('group','admin')
------ input ------
request = time: <
seconds: 1588991010
>
auth: <
principal: "user:me@acme.co"
claims: <
>
>
------ result ------
value: false (types.Bool)
------ input ------
request = time: <
seconds: 1588991010
>
auth: <
principal: "user:me@acme.co"
claims: <
fields: <
key: "group"
value: <
string_value: "admin"
>
>
>
>
------ result ------
value: true (types.Bool)
次に進む前に、mapContainsKeyValue
関数自体を調べてみることをおすすめします。
// mapContainsKeyValue implements the custom function:
// map.contains(key, value) -> bool.
func mapContainsKeyValue(args ...ref.Val) ref.Val {
// The declaration of the function ensures that only arguments which match
// the mapContainsKey signature will be provided to the function.
m := args[0].(traits.Mapper)
// CEL has many interfaces for dealing with different type abstractions.
// The traits.Mapper interface unifies field presence testing on proto
// messages and maps.
key := args[1]
v, found := m.Find(key)
// If not found and the value was non-nil, the value is an error per the
// `Find` contract. Propagate it accordingly. Such an error might occur with
// a map whose key-type is listed as 'dyn'.
if !found {
if v != nil {
return types.ValOrErr(v, "unsupported key type")
}
// Return CEL False if the key was not found.
return types.False
}
// Otherwise whether the value at the key equals the value provided.
return v.Equal(args[2])
}
拡張を簡単にするため、カスタム関数のシグネチャには ref.Val
型の引数が必要です。この場合のトレードオフは、拡張の容易さが実装者の負担を増やし、すべての値の型が適切に処理されるようにすることです。入力引数の型または数が関数宣言と一致しない場合は、no such overload
エラーが返されます。
cel.FunctionBinding()
はランタイム型ガードを追加して、ランタイム コントラクトが環境内で型チェックされた宣言と一致するようにします。
9. JSON のビルド
CEL では、JSON などの非ブール値出力を生成することもできます。関数に以下を追加します。
// exercise5 covers how to build complex objects as CEL literals.
//
// Given the input now, construct a JWT with an expiry of 5 minutes.
func exercise5() {
fmt.Println("=== Exercise 5: Building JSON ===\n")
env, _ := cel.NewEnv(
// Declare the 'now' variable as a Timestamp.
// cel.Variable("now", cel.TimestampType),
)
// Note the quoted keys in the CEL map literal. For proto messages the
// field names are unquoted as they represent well-defined identifiers.
ast := compile(env, `
{'sub': 'serviceAccount:delegate@acme.co',
'aud': 'my-project',
'iss': 'auth.acme.com:12350',
'iat': now,
'nbf': now,
'exp': now + duration('300s'),
'extra_claims': {
'group': 'admin'
}}`,
cel.MapType(cel.StringType, cel.DynType))
program, _ := env.Program(ast)
out, _, _ := eval(
program,
map[string]interface{}{
"now": &tpb.Timestamp{Seconds: time.Now().Unix()},
},
)
fmt.Printf("------ type conversion ------\n%v\n", out)
fmt.Println()
}
コードの実行
コードを再実行すると、次のエラーが表示されます。
ERROR: <input>:5:11: undeclared reference to 'now' (in container '')
| 'iat': now,
| ..........^
... and more ...
cel.TimestampType
型の now
変数の宣言を cel.NewEnv()
に追加して、再度実行します。
// exercise5 covers how to build complex objects as CEL literals.
//
// Given the input now, construct a JWT with an expiry of 5 minutes.
func exercise5() {
fmt.Println("=== Exercise 5: Building JSON ===\n")
env, _ := cel.NewEnv(
cel.Variable("now", cel.TimestampType),
)
// Note the quoted keys in the CEL map literal. For proto messages the
// field names are unquoted as they represent well-defined identifiers.
ast := compile(env, `
{'sub': 'serviceAccount:delegate@acme.co',
'aud': 'my-project',
'iss': 'auth.acme.com:12350',
'iat': now,
'nbf': now,
'exp': now + duration('300s'),
'extra_claims': {
'group': 'admin'
}}`,
cel.MapType(cel.StringType, cel.DynType))
// Hint:
// Convert `out` to JSON using the valueToJSON() helper method.
// The valueToJSON() function calls ConvertToNative(&structpb.Value{})
// to adapt the CEL value to a protobuf JSON representation and then
// uses the jsonpb.Marshaler utilities on the output to render the JSON
// string.
program, _ := env.Program(ast)
out, _, _ := eval(
program,
map[string]interface{}{
"now": time.Now(),
},
)
fmt.Printf("------ type conversion ------\n%v\n", out)
fmt.Println()
}
コードを再実行すると、成功するはずです。
=== Exercise 5: Building JSON ===
{'aud': 'my-project',
'exp': now + duration('300s'),
'extra_claims': {
'group': 'admin'
},
'iat': now,
'iss': 'auth.acme.com:12350',
'nbf': now,
'sub': 'serviceAccount:delegate@acme.co'
}
------ input ------
now = seconds: 1569302377
------ result ------
value: &{0xc0002eaf00 map[aud:my-project exp:{0xc0000973c0} extra_claims:0xc000097400 iat:{0xc000097300} iss:auth.acme.com:12350 nbf:{0xc000097300} sub:serviceAccount:delegate@acme.co] {0x8c8f60 0xc00040a6c0 21}} (*types.baseMap)
------ type conversion ------
&{0xc000313510 map[aud:my-project exp:{0xc000442510} extra_claims:0xc0004acdc0 iat:{0xc000442450} iss:auth.acme.com:12350 nbf:{0xc000442450} sub:serviceAccount:delegate@acme.co] {0x9d0ce0 0xc0004424b0 21}}
プログラムは実行されますが、出力値 out
を明示的に JSON に変換する必要があります。この場合の内部 CEL 表現は JSON コンバーチブルです。これは、JSON でサポートできる型か、よく知られている Proto から JSON へのマッピングがある型のみを参照するためです。
// exercise5 covers how to build complex objects as CEL literals.
//
// Given the input now, construct a JWT with an expiry of 5 minutes.
func exercise5() {
fmt.Println("=== Exercise 5: Building JSON ===\n")
...
fmt.Printf("------ type conversion ------\n%v\n", valueToJSON(out))
fmt.Println()
}
codelab.go
ファイル内で valueToJSON
ヘルパー関数を使用して型が変換されると、次の追加の出力が表示されます。
------ type conversion ------
{
"aud": "my-project",
"exp": "2019-10-13T05:54:29Z",
"extra_claims": {
"group": "admin"
},
"iat": "2019-10-13T05:49:29Z",
"iss": "auth.acme.com:12350",
"nbf": "2019-10-13T05:49:29Z",
"sub": "serviceAccount:delegate@acme.co"
}
10. プロトコルのビルド
CEL では、アプリケーションにコンパイルされたあらゆるメッセージ タイプの protobuf メッセージをビルドできます。入力 jwt
から google.rpc.context.AttributeContext.Request
をビルドする関数を追加します。
// exercise6 describes how to build proto message types within CEL.
//
// Given an input jwt and time now construct a
// google.rpc.context.AttributeContext.Request with the time and auth
// fields populated according to the go/api-attributes specification.
func exercise6() {
fmt.Println("=== Exercise 6: Building Protos ===\n")
// Construct an environment and indicate that the container for all references
// within the expression is `google.rpc.context.AttributeContext`.
requestType := &rpcpb.AttributeContext_Request{}
env, _ := cel.NewEnv(
// Add cel.Container() option for 'google.rpc.context.AttributeContext'
cel.Types(requestType),
// Add cel.Variable() option for 'jwt' as a map(string, Dyn) type
// and for 'now' as a timestamp.
)
// Compile the Request message construction expression and validate that
// the resulting expression type matches the fully qualified message name.
//
// Note: the field names within the proto message types are not quoted as they
// are well-defined names composed of valid identifier characters. Also, note
// that when building nested proto objects, the message name needs to prefix
// the object construction.
ast := compile(env, `
Request{
auth: Auth{
principal: jwt.iss + '/' + jwt.sub,
audiences: [jwt.aud],
presenter: 'azp' in jwt ? jwt.azp : "",
claims: jwt
},
time: now
}`,
cel.ObjectType("google.rpc.context.AttributeContext.Request"),
)
program, _ := env.Program(ast)
// Construct the message. The result is a ref.Val that returns a dynamic
// proto message.
out, _, _ := eval(
program,
map[string]interface{}{
"jwt": map[string]interface{}{
"sub": "serviceAccount:delegate@acme.co",
"aud": "my-project",
"iss": "auth.acme.com:12350",
"extra_claims": map[string]string{
"group": "admin",
},
},
"now": time.Now(),
},
)
// Hint: Unwrap the CEL value to a proto. Make sure to use the
// `ConvertToNative(reflect.TypeOf(requestType))` to convert the dynamic proto
// message to the concrete proto message type expected.
fmt.Printf("------ type unwrap ------\n%v\n", out)
fmt.Println()
}
コードの実行
コードを再実行すると、次のエラーが表示されます。
ERROR: <input>:2:10: undeclared reference to 'Request' (in container '')
| Request{
| .........^
コンテナは基本的に名前空間やパッケージに相当しますが、protobuf メッセージ名のように細かく指定することもできます。CEL コンテナでは、Protobuf および C++ と同じ名前空間解決ルールを使用して、特定の変数、関数、型名の宣言場所を決定します。
コンテナが google.rpc.context.AttributeContext
の場合、型チェッカーとエバリュエータは、すべての変数、型、関数について次の識別子名を試します。
google.rpc.context.AttributeContext.<id>
google.rpc.context.<id>
google.rpc.<id>
google.<id>
<id>
絶対名の場合は、変数、型、関数参照の先頭にドットを付けます。この例では、式 .<id>
はコンテナ内で最初にチェックせずに、トップレベルの <id>
識別子のみを検索します。
cel.Container("google.rpc.context.AttributeContext")
オプションを CEL 環境に指定して、再度実行してみてください。
// exercise6 describes how to build proto message types within CEL.
//
// Given an input jwt and time now construct a
// google.rpc.context.AttributeContext.Request with the time and auth
// fields populated according to the go/api-attributes specification.
func exercise6() {
fmt.Println("=== Exercise 6: Building Protos ===\n")
// Construct an environment and indicate that the container for all references
// within the expression is `google.rpc.context.AttributeContext`.
requestType := &rpcpb.AttributeContext_Request{}
env, _ := cel.NewEnv(
// Add cel.Container() option for 'google.rpc.context.AttributeContext'
cel.Container("google.rpc.context.AttributeContext.Request"),
cel.Types(requestType),
// Later, add cel.Variable() options for 'jwt' as a map(string, Dyn) type
// and for 'now' as a timestamp.
// cel.Variable("now", cel.TimestampType),
// cel.Variable("jwt", cel.MapType(cel.StringType, cel.DynType)),
)
...
}
出力は次のようになります。
ERROR: <input>:4:16: undeclared reference to 'jwt' (in container 'google.rpc.context.AttributeContext')
| principal: jwt.iss + '/' + jwt.sub,
| ...............^
その他多数のエラー
次に、jwt
変数と now
変数を宣言します。プログラムは想定どおりに実行されます。
=== Exercise 6: Building Protos ===
Request{
auth: Auth{
principal: jwt.iss + '/' + jwt.sub,
audiences: [jwt.aud],
presenter: 'azp' in jwt ? jwt.azp : "",
claims: jwt
},
time: now
}
------ input ------
jwt = {
"aud": "my-project",
"extra_claims": {
"group": "admin"
},
"iss": "auth.acme.com:12350",
"sub": "serviceAccount:delegate@acme.co"
}
now = seconds: 1588993027
------ result ------
value: &{0xc000485270 0xc000274180 {0xa6ee80 0xc000274180 22} 0xc0004be140 0xc0002bf700 false} (*types.protoObj)
----- type unwrap ----
&{0xc000485270 0xc000274180 {0xa6ee80 0xc000274180 22} 0xc0004be140 0xc0002bf700 false}
さらに、メッセージに対して out.Value()
を呼び出して型をラップ解除し、結果がどのように変化するかを確認する必要もあります。
11. マクロ
マクロを使用して、解析時に CEL プログラムを操作できます。マクロは呼び出しシグネチャを照合し、新しいサブ式 AST を生成するために入力呼び出しとその引数を操作します。
マクロを使用すると、CEL では直接記述できない複雑なロジックを AST で実装できます。たとえば、has
マクロを使用すると、フィールド プレゼンス テストが可能になります。存在するなどの理解マクロはすべて、入力リストまたはマップに対する関数呼び出しを制限付きの反復処理に置き換えます。どちらの概念も構文レベルでは不可能ですが、マクロ展開によって可能です。
次の演習を追加して実行します。
// exercise7 introduces macros for dealing with repeated fields and maps.
//
// Determine whether the jwt.extra_claims has at least one key that starts
// with the group prefix, and ensure that all group-like keys have list
// values containing only strings that end with '@acme.co`.
func exercise7() {
fmt.Println("=== Exercise 7: Macros ===\n")
env, _ := cel.NewEnv(
cel.ClearMacros(),
cel.Variable("jwt", cel.MapType(cel.StringType, cel.DynType)),
)
ast := compile(env,
`jwt.extra_claims.exists(c, c.startsWith('group'))
&& jwt.extra_claims
.filter(c, c.startsWith('group'))
.all(c, jwt.extra_claims[c]
.all(g, g.endsWith('@acme.co')))`,
cel.BoolType)
program, _ := env.Program(ast)
// Evaluate a complex-ish JWT with two groups that satisfy the criteria.
// Output: true.
eval(program,
map[string]interface{}{
"jwt": map[string]interface{}{
"sub": "serviceAccount:delegate@acme.co",
"aud": "my-project",
"iss": "auth.acme.com:12350",
"extra_claims": map[string][]string{
"group1": {"admin@acme.co", "analyst@acme.co"},
"labels": {"metadata", "prod", "pii"},
"groupN": {"forever@acme.co"},
},
},
})
fmt.Println()
}
次のエラーが表示されます。
ERROR: <input>:1:25: undeclared reference to 'c' (in container '')
| jwt.extra_claims.exists(c, c.startsWith('group'))
| ........................^
その他多数 ...
これらのエラーは、マクロがまだ有効になっていないために発生します。マクロを有効にするには、cel.ClearMacros()
を削除してから再度実行します。
=== Exercise 7: Macros ===
jwt.extra_claims.exists(c, c.startsWith('group'))
&& jwt.extra_claims
.filter(c, c.startsWith('group'))
.all(c, jwt.extra_claims[c]
.all(g, g.endsWith('@acme.co')))
------ input ------
jwt = {
"aud": "my-project",
"extra_claims": {
"group1": [
"admin@acme.co",
"analyst@acme.co"
],
"groupN": [
"forever@acme.co"
],
"labels": [
"metadata",
"prod",
"pii"
]
},
"iss": "auth.acme.com:12350",
"sub": "serviceAccount:delegate@acme.co"
}
------ result ------
value: true (types.Bool)
現在サポートされているマクロは次のとおりです。
マクロ | 署名 | 説明 |
すべて | r.all(var, cond) | 範囲 r のすべて の変数に対して cond が true と評価されるかどうかをテストします。 |
存在しています | r.exists(var, cond) | 範囲 r のいずれかの 変数に対して cond が true と評価されるかどうかをテストします。 |
exists_one | r.exists_one(var, cond) | 範囲 r の変数のうち 1 つのみ に対して cond が true と評価するかどうかをテストします。 |
フィルタ | r.filter(var, cond) | リストの場合は、範囲 r の各要素が条件 cond を満たす新しいリストを作成します。マップの場合は、範囲 r の各キー変数が条件 cond を満たす新しいリストを作成します。 |
地図 | r.map(var, expr) です | 範囲 r の各変数が expr で変換される新しいリストを作成します。 |
r.map(var, cond, expr) です | 引数が 2 つのマップと同じですが、値が変換される前に条件付き cond フィルタがあります。 | |
has | has(a.b) | 値 a での b のプレゼンス テスト : マップの場合の JSON テスト定義。proto の場合、デフォルト以外のプリミティブ値または設定済みのメッセージ フィールドをテストします。 |
範囲 r の引数が map
型の場合、var
はマップキーになり、list
型の値の場合、var
はリスト要素の値になります。all
、exists
、exists_one
、filter
、map
の各マクロは、入力サイズによって制限される反復処理を実行する AST 書き換えを行います。
有限の理解により、CEL プログラムはチューリング完全ではなく、入力に関しては超線形時間で評価されます。これらのマクロは慎重に、またはまったく使用しない。一般的に、理解度を多用すると、カスタム関数がユーザー エクスペリエンスと性能の向上をもたらすよい判断材料になります。
12. 調整
現時点では CEL-Go 専用の機能がいくつかありますが、これらは他の CEL 実装の今後の計画を示しています。次の演習では、同じ AST のさまざまなプログラム プランをご紹介します。
// exercise8 covers features of CEL-Go which can be used to improve
// performance and debug evaluation behavior.
//
// Turn on the optimization, exhaustive eval, and state tracking
// ProgramOption flags to see the impact on evaluation behavior.
func exercise8() {
fmt.Println("=== Exercise 8: Tuning ===\n")
// Declare the x and 'y' variables as input into the expression.
env, _ := cel.NewEnv(
cel.Variable("x", cel.IntType),
cel.Variable("y", cel.UintType),
)
ast := compile(env,
`x in [1, 2, 3, 4, 5] && type(y) == uint`,
cel.BoolType)
// Try the different cel.EvalOptions flags when evaluating this AST for
// the following use cases:
// - cel.OptOptimize: optimize the expression performance.
// - cel.OptExhaustiveEval: turn off short-circuiting.
// - cel.OptTrackState: track state and compute a residual using the
// interpreter.PruneAst function.
program, _ := env.Program(ast)
eval(program, cel.NoVars())
fmt.Println()
}
オプティマイズ
最適化フラグをオンにすると、CEL は事前にリストを作成してリテラルをマッピングし、in 演算子などの特定の呼び出しが true セットのメンバーシップ テストになるように最適化するために追加の時間を費やします。
// Turn on optimization.
trueVars := map[string]interface{}{"x": int64(4), "y": uint64(2)}
program, _ := env.Program(ast, cel.EvalOptions(cel.OptOptimize))
// Try benchmarking with the optimization flag on and off.
eval(program, trueVars)
同じプログラムが異なる入力に対して何度も評価される場合は、最適化が良い選択です。ただし、プログラムが 1 回だけ評価される場合は、最適化によってオーバーヘッドが増加するだけです。
包括的な評価
包括的な評価は、式の評価の各ステップで観測された値に関する分析情報を提供するため、式の評価動作のデバッグに役立ちます。
// 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)
各式 ID の式の評価状態のリストが表示されます。
------ 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)
式 id 2 は、最初の in 演算子の結果に対応します。式 id 11 は、2 番目の == 演算子に対応します。通常の評価では、2 が計算された後に式が短絡していた可能性があります。y が uint ではなかった場合、状態は式が失敗した理由を 1 つだけではなく 2 つ示します。
13. 学習した内容
式エンジンが必要な場合は、CEL の使用を検討してください。CEL は、パフォーマンスが重要なユーザー構成を実行する必要があるプロジェクトに最適です。
これまでの演習で、データを CEL に渡して出力や判断内容を取得する方法に慣れてきたことと思います。
ブール型の決定から JSON や Protobuffer メッセージの生成まで、どのような操作が可能か、理解していただけたかと思います。
式の操作方法や処理内容を理解していただけていれば幸いです。Google は、その拡張の一般的な方法を理解しています。