Firestore で絶対にセキュアにしたいデータは Trigger で更新する

https://qiita.com/sgr-ksmt/items/a7bda48535033f6a4b68
こちらの記事を読んで、updateTimeをクライアントからpostされてきたデータを使っている時点でもっとセキュアにできると思ったのでこの記事を書いています。

### トリガーとは
詳細は、 [Cloud Firestore トリガー](https://firebase.google.com/docs/functions/firestore-events) を読んでもらうとして、要は **Firestore のドキュメントが更新(追加削除含)されたときに発火したイベントをハンドルする関数** です。
Cloud Functionsで下記のような関数を作成すると、 `posts` コレクションにドキュメントが追加されたイベントを購読できます。

```javascript
functions.firestore
  .document('posts/{postID}')
  .onCreate(async (snap, context) => {
    /* ... */
  });
```

`onCreate` `onUpdate` `onDelete` `onWrite` があります。それぞれが購読するイベントは[こちら](https://firebase.google.com/docs/functions/firestore-events#trigger_a_function)を参照してください。

### トリガー内での処理

ドキュメント追加時であれば、こんな感じ

```createPostTrigger.js
const functions = require('firebase-functions');
const admin = require('firebase-admin');

const Post = admin.firestore().collection('posts');

exports.createPostTrigger = functions.firestore.document('posts/{postId}').onCreate(async (snap, context) => {
  const newData = Object.assign(snap.data(), {
    createTime: snap.createTime,
    updateTime: snap.updateTime,
    // 他になにか入れたい場合はここに突っ込む
  });
  await Post.doc(context.params.postId).set(newData).catch((error) => {
    console.error(error.code, error.message);
  });
});
```

これで ドキュメントに createTime と updateTime に更新日時が入ります。

### update には注意

post にレコードが追加された際に、発火した onCreate イベントによってドキュメントを更新した場合、 onUpdate イベントが発火してしまうので、 対策を行わないと無限に onUpdate の処理が行われてしまうので注意です。

今回はたまたま updateTime をドキュメントに設定しているので、更新前後の updateTime を比較して、異なっていれば更新せずに終了すればよいです。

```updatePostTrigger.js
const functions = require('firebase-functions');
const admin = require('firebase-admin');

const Post = admin.firestore().collection('posts');

exports.updatePostTrigger = functions.firestore.document('posts/{postId}').onUpdate(async (snap, context) => {
  if (!snap.before.data().updateDate) return;

  const beforeUpdateTime = snap.before.data().updateTime.toDate();
  const afterUpdateTime = snap.after.data().updateTime.toDate();
  if (beforeUpdateTime.getTime() !== afterUpdateTime.getTime()) {
    return;
  }

  const newData = Object.assign(snap.after.data(), {
    updateTime: snap.after.updateTime,
  });
  await Post.doc(context.params.postId).set(newData).catch((error) => {
    console.error(error.code, error.message);
  });
});
```

`snap` には更新前後のデータが入っているので、もし更新前のデータに updateTime がないということは onCreate 内の更新処理により発火したイベントであるといえるので、早期リターンします。
次に、更新前後の `updateTime` を比較して、同じ時間ではない場合は、 updateTime　が更新されたといえます。updateTime が存在し、更新前後で値が異なるという処理は、 `updatePostTrigger` でしか行っていないので、この処理を続行する必要がないと言えるので、ここで早期リターンします。
ポイントとしては、　`Firestore.timestamp` 型同士の比較はうまく機能しません。 `toDate()` で `Date` 型のインスタンスを取得して、そのオブジェクト同士を比較してください。それ以降は `onCreate` 時と同じです。

### もちろん firestore.rules の設定は忘れずに！
`updateTime` 等がトリガーを経由せずとも登録できる状態ではこの対策をしたところでセキュアではないです。

### リアルタイム性が必要なアプリの場合、この対策ではだめかも
Firestoreの更新→トリガーの発火にはそれなりにロードタイムが必要になります。リアルタイム性が求められる場合はこの対策は使えないかもしれません。

他にもっといい方法があれば教えてください！それではー。


### 2020/4/2追記

Trigger 使うの辞めました。理由は、更新が漏れるデータが定期的に発生していたためですログを仕込んでもエラーコード `13` しか帰って来ない。

同じような問題が起こっているケースも見つけました。
https://github.com/firebase/firebase-functions/issues/536

Firebase (gRPC） の問題ということで、お手上げです。。。今のうちは、おとなしくAPI経由で処理するしかなさそうですね。