GoサーバからFirestoreにアクセスするときに、セキュリティルールを有効にする

## TL;DR

サーバからFirestoreにアクセスするとき、ユーザのID Tokenを渡すことで、Firestoreのセキュリティルールにできる。

## やりたいこと

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/196890/4409237e-7a21-bf28-dd1e-6dc907275087.png)

奇妙な構成だが、仕事の事情でこういう風にしたい。

問題は、APIサーバからFirestoreにアクセスするとき、Firestoreのセキュリティルールが適用されないこと。APIサーバではFirebaseのadmin SDKを使うが、通常の使い方だと、サービスアカウントとしてFirestoreにアクセスすることになるため。

Firestoreのセキュリティルールに相当する認証や認可処理を自分で実装しないといけない。実装自体はそれほど手間でないのだが、Firestore側で認証/認可をするのに比べて、処理時間が結構かかる。

だから、APIサーバからFirestoreにアクセスするときに、Firestoreのセキュリティルールを適用させたい。

## 対処方針

Web FrontはFirebaseでユーザ認証しているので、ユーザのID Tokenは取得できる。このID TokenをWeb Front → APIサーバ →　Firestoreの流れで送ることで、ユーザとしてFirestoreにアクセスすることになり、Firestoreのセキュリティルールが適用されるようになる。

## Firebase SDKを使いつつ、セキュリティルールを有効にする方法

```go
ctx := context.Background()

// 重要
// WithoutAuthenticationとWithGRPCDialOptionを指定する。
// WithoutAuthenticationがないとID Tokenが無視される。WithoutAuthenticationをつけるなら、WithTransportCredentialsもないとエラーになる。
app, _ := firebase.NewApp(ctx,
    &firebase.Config{ProjectID: "your-project-id"},
    option.WithoutAuthentication(),
    option.WithGRPCDialOption(grpc.WithTransportCredentials(credentials.NewClientTLSFromCert(nil, ""))),
)
client, _ := app.Firestore(ctx)

h := client.Collection("your-collection-id").
    Limit(10)

// 重要
// metadataにFirebaseのID Tokenを積む
ctx = metadata.AppendToOutgoingContext(ctx, "Authorization", "Bearer "+idToken)
it := h.Documents(ctx)

docs, _ := it.GetAll()

for _, doc := range docs {
    data := doc.Data()
    fmt.Println(data)
}
```

気になる点は、WithoutAuthenticationを指定している点。このオプションのDocを見ると、テストか公開されているデータへのアクセス時に指定すべき、と書かれている。この使い方が「公開されているデータへのアクセス時」に該当するのかがわからない(インターネットからアクセスできるので、公開されているとみなして良いと思っている)。

## Firebase SDKを使わないで、セキュリティルールを有効にする方法

Firebase SDKではなく、生のFirestoreのgRPC APIを使う。実装例を示す。

まず、生のFirestoreのgRPC APIを使うためのモジュールをimport。

```go
import (
	"context"
	"fmt"
	firestore "google.golang.org/genproto/googleapis/firestore/v1beta1"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials"
	"google.golang.org/grpc/metadata"
	"google.golang.org/protobuf/types/known/wrapperspb"
	"io"
	"log"
)
```

生のgRPC APIを使ってFirestoreからデータをReadする。

```go
address := "firestore.googleapis.com:443"
conn, _ := grpc.Dial(address, grpc.WithTransportCredentials(credentials.NewClientTLSFromCert(nil, "")))
defer conn.Close()

client := firestore.NewFirestoreClient(conn)

ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
// 重要
// gRPCリクエストのmetadataに、ID Tokenを積む
ctx = metadata.AppendToOutgoingContext(ctx, "Authorization", "Bearer "+idToken)

stream, _ := client.RunQuery(ctx, &firestore.RunQueryRequest{
	Parent: "projects/your-project-id/databases/(default)/documents",
	QueryType: &firestore.RunQueryRequest_StructuredQuery{
		StructuredQuery: &firestore.StructuredQuery{
			From: []*firestore.StructuredQuery_CollectionSelector{
				{
					CollectionId:   "your-collection-id",
					AllDescendants: false,
				},
			},
			Limit: &wrapperspb.Int32Value{Value: 10},
		},
	},
})
defer cancel()

var docs []*firestore.Document
for {
	res, err := stream.Recv()
	if err == io.EOF {
		break
	}

	if err != nil {
		log.Fatalln(err)
	}

	if res.Document != nil {
		docs = append(docs, res.Document)
	}
}

for _, doc := range docs {
	fmt.Println(doc)
}
```

- 難点
  1. Firestoreのクエリを組み立てるのが面倒くさい
    - このコードだと単に件数指定しているだけだが、取得範囲指定、Where、OrderByなどを指定するのがとても面倒くさい
  1. Firestoreのレスポンスを使いやすい形に変換するが面倒くさい
- 補足
  - client.RunQueryというgRPC APIを使っているが、Firebase SDKでコレクションを取得するときにも使われている
  - SDKは色々と楽させてくれるだけで、最終的には生のgRPC APIを使っている


## 参考にした資料

- [GCPのgRPC APIの使い方](https://github.com/GoogleCloudPlatform/grpc-gcp-go/blob/f4aa234b2c20ac7b01544479022ade295204ed18/doc/gRPC-client-user-guide.md)
  - GCPのgRPC APIは使い方が謎すぎて困ったが、このDocを読んで使い方が分かった
- [Firestore REST APIの概要](https://firebase.google.com/docs/firestore/use-rest-api?hl=ja)
  - ID TokenをFirestoreに送れば良いことは、このDocから分かった