Carpe Diem

備忘録

CELで独自のオブジェクトをprotobufを使って変数定義する

概要

CELでは評価式で扱う変数をEnvironment内で定義しますが、既存のデータモデルを使いたい場合は

  • 同じ定義を都度作らないといけない
  • 変更があった際の追従漏れが発生する

といった手間が発生してしまいます。

しかしそのデータモデルがprotobufで定義されていれば再利用することが可能です。

今回はその方法を紹介します。

環境

  • go v1.22.1
  • cel-go v0.20.1

実装

外部protoの例

cel.Typesで型を読み込み、cel.ObjectTypeでその型を指定してcel.Variableで自分の変数として定義します。

import (
    ...

    "github.com/google/cel-go/cel"
    rpcpb "google.golang.org/genproto/googleapis/rpc/context/attribute_context"
    "google.golang.org/protobuf/types/known/structpb"
    tpb "google.golang.org/protobuf/types/known/timestamppb"
)

func main() {
    env, _ := cel.NewEnv(
        cel.Types(&rpcpb.AttributeContext_Request{}),
        cel.Variable("request",
            cel.ObjectType("google.rpc.context.AttributeContext.Request"),
        ),
    )
    ...

ポイント

ポイントは以下です。

すると次のように評価式においてprotobufの定義に基づくオブジェクトとして扱うことが可能です。

   ast, iss := env.Compile(
        `request.auth.claims.group == 'admin'
            || request.auth.principal == 'user:me@acme.co'`,
    )

独自protoの例

例えば次のようなprotoを自前で定義して、

syntax = "proto3";

package helloworld;

option go_package = "github.com/jun06t/cel-sample/external-proto/proto;helloworld";

message HelloRequest {
  string name = 1;
  int32 age = 2;
  bool man = 3;
}

message HelloReply { string message = 1; }

これを先ほどと同じようにEnvにセットし、評価式もmessageのフィールドを使うように与えます。

import (
        ...
        pb "github.com/jun06t/cel-sample/external-proto/proto"
        ...
)

main() 
        env, _ := cel.NewEnv(
                cel.Types(&pb.HelloRequest{}),
                cel.Variable("request",
                        cel.ObjectType("helloworld.HelloRequest"),
                ),
        )

        ast, iss := env.Compile(
                `request.name == 'Alice' && request.age > 20 && request.man == false`,
        )
        ...

入力値を与えて評価すると、

        input := map[string]any{
                "request": &pb.HelloRequest{
                        Name: "Alice",
                        Age:  21,
                        Man:  false,
                },
        }
        out, _, err := prog.Eval(input)
        if err != nil {
                log.Fatalf("Evaluation error: %v", err)
        }

        fmt.Println("Is permitted user?", out)

期待通りの結果になります。

$ go run main.go
Is permitted user? true

その他

サンプルコード

今回のサンプルコードはこちらです。

github.com

まとめ

既存のデータモデルを扱う場合にproto定義を利用することで二重管理の負債を避けることができます。

参考