この記事は Go5 Advent Calendar 2019 の2日目の記事です。

grpc-goではログやメトリクス取得、権限チェックなど、gRPCサーバの全てのメソッドで共通して行いたい処理を interceptor で行います。

本記事では、interceptor でメソッドごとの処理を記述する方法を紹介します。

gRPC の interceptor とは

gRPCが提供している、メソッドの前後に処理を行うための仕組みです。gRPCのRPCには Unary RPCとStream RPCの2種類があり、それぞれに対して interceptor が定義されています。

interceptor の定義は下記のようになっています。どちらも context、request情報、Server情報が共通で、StreamInterceptorにのみ追加で ServerStream が渡されます。

type UnaryServerInterceptor func(ctx context.Context, req interface{}, info *UnaryServerInfo, handler UnaryHandler) (resp interface{}, err error)

type StreamServerInterceptor func(srv interface{}, ss ServerStream, info *StreamServerInfo, handler StreamHandler) error

メソッドごとの処理を interceptor で実装する

メソッドごとの処理を interceptor で実装することを考えます。実装の一例として 権限チェック処理を考えます。単純化のため、UnaryServerInterceptor の実装のみを紹介します。

実装するサーバ

例で実装するのは下記protoファイルのサーバです。

service Server{
    rpc GetData (GetDataRequest) returns (GetDataResponse) {}
    rpc SetData (SetDataRequest) returns (google.protobuf.Empty) {}
    rpc ListData (google.protobuf.Empty) returns (ListDataResponse) {}
    rpc DeleteData (DeleteDataRequest) returns (google.protobuf.Empty) {}
}

// Request, Response の message は略

メソッドごとの設定を定義する

権限チェックでは、各メソッドにどのような権限を持つクライアントがアクセスしてよいか設定し、クライアントが条件を満たしているか検証します。

引数の ServerInfo にはリクエストされたメソッド名が含まれています。そのため、これをキーに下記のように設定を記述します。

const (
    DATA_GET    = "data.get"
    DATA_SET    = "data.set"
    DATA_DELETE = "data.delete"
)

APIPolicy = Policy(map[string]Permissions{
    "/proto.Server/GetData":    NewPermissions(DATA_GET),
    "/proto.Server/SetData":    NewPermissions(DATA_SET),
    "/proto.Server/ListData":   NewPermissions(DATA_GET),
    "/proto.Server/DeleteData": NewPermissions(DATA_DELETE),
})

func NewPermissions(scopes ...string) Permissions {
    return Permissions(scopes)
}

type Permissions []string

type Policy map[string]Permissions

クライアントはアクセストークンを持っており、アクセストークンの決められたパラメータにクライアントの権限が記されているものとします。

interceptor の実装

次に interceptorの実装です。上記のPolicyをもたせて Interceptor を定義します。

Go の関数はクロージャなので、下記のように関数を返すことで、関数は policy を 参照し続けることができます。

本記事では実装の詳細については省きますが、下記コードで GetScopes でctx内のアクセストークンからクライアントの権限を取得し、VerifyPermissionsでクライアントの保持している権限がメソッドに要求されている権限を満たしているかチェックしています。

func AuthInterceptor(policy Policy) grpc.UnaryServerInterceptor {
    return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
        scopes, err := GetScopes(ctx) // func(context.Context) ([]string, error)
        if err != nil {
            return nil, status.Error(codes.Unauthenticated, codes.Unauthenticated.String())
        }

        // check if required permission
        if !VerifyPermissions(scopes, policy[info.FullMethod]) {
            return nil, status.Error(codes.Unauthenticated, codes.Unauthenticated.String())
        }
        return handler(ctx, req) // Do RPC
    }
}

Interceptor を grpc.Serverに設定する

下記のように interceptorを設定したサーバを作成します。

grpcServer := grpc.NewServer(
		grpc.UnaryInterceptor(
			app.AuthInterceptor(app.APIPolicy),
		),
	)

以上でメソッドごとの設定をカスタマイズした interceptorを実装することができました。

まとめ

本記事では、interceptor でメソッドごとの処理を記述する方法を紹介しました。gRPCのinterceptorでは、呼び出されているメソッドの情報がメソッド名で渡されるので、メソッド名と設定のペアでメソッドの設定を記述しました。

本記事で紹介している実装は soichisumi/grpc-api-setting においています。動かして試してみたい方はこちらも参照ください。

Go5 Advent Calendar 2019 の3日目は nandehu0323 さんの記事です。お楽しみに!

参考リンク