TL;DR
サーバからFirestoreにアクセスするとき、ユーザのID Tokenを渡すことで、Firestoreのセキュリティルールにできる。
やりたいこと
奇妙な構成だが、仕事の事情でこういう風にしたい。
問題は、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を使いつつ、セキュリティルールを有効にする方法
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。
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する。
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)
}
- 難点
- Firestoreのクエリを組み立てるのが面倒くさい
- このコードだと単に件数指定しているだけだが、取得範囲指定、Where、OrderByなどを指定するのがとても面倒くさい
- Firestoreのレスポンスを使いやすい形に変換するが面倒くさい
- 補足
- client.RunQueryというgRPC APIを使っているが、Firebase SDKでコレクションを取得するときにも使われている
- SDKは色々と楽させてくれるだけで、最終的には生のgRPC APIを使っている
参考にした資料
-
GCPのgRPC APIの使い方
- GCPのgRPC APIは使い方が謎すぎて困ったが、このDocを読んで使い方が分かった
-
Firestore REST APIの概要
- ID TokenをFirestoreに送れば良いことは、このDocから分かった