cloud firestore のsecurity rule でハマったところ

よくあるサーバー側のプログラムやWAFで行っているデータストアのバリデーションやアクセス制御は、cloud firestoreの場合はsecurity ruleを使って行うことになる。
そのsecurity ruleの編集をする中でいろいろハマったところがあったので、覚書。

## const, let, varなどの変数は使えない

```
service cloud.firestore {
  match /databases/{database}/documents {
    match /users/{user} {
      allow read: if request.auth.uid == resource.data.author_id;
      allow write: if get(/databases/$(database)/documents/users/$(request.auth.uid)).data.admin == true && get(/databases/$(database)/documents/users/$(request.auth.uid)).data.canWrite == true;
    }
  }
}
```

このように、`get(/databases/$(database)/documents/users/$(request.auth.uid))` で取得できるデータを複数使いたい場合（この場合はadminというフィールドとcanWriteというフィールド）、getで取得したデータを変数に格納しておいて使い回せるようにしたい。わざわざ2回同じ情報を取得しにいくのは無駄だし、コードも冗長だ。

が、security ruleでは変数を使うことができない。そのため、上記のようにわざわざ2回取得することになる。

```
function hasValidAuthority(){
	const user = get(/databases/$(database)/documents/users/$(request.auth.uid))
	user.data.admin == true && user.data.canWrite == true
}
service cloud.firestore {
  match /databases/{database}/documents {
    // Make sure the uid of the requesting user matches the 'author_id' field
    // of the document
    match /users/{user} {
      allow read: if request.auth.uid == resource.data.author_id;
      allow write: if hasValidAuthority();
    }
  }
}
```

このようにfunctionの中で使うのもダメ。とにかくsecurity ruleの中では変数が使えない。
公式ドキュメントにも以下のような注意がある

> 関数には 1 つの return ステートメントのみを含めることができます。追加ロジックを含めることはできません。たとえば、中間変数を作成したり、ループを実行したり、外部サービスを呼び出したりすることはできません。


## ==, != を使って明示的に条件判断をしないといけない。
`function` が使えるので security ruleはJavaScriptで書けるのかと思いきや、そうではない。あくまでもsecurity ruleはsecurity ruleなので、JavaScriptではない。

例えば以下のようなrule。getで取得できるadminフィールドの値はbool値が入ってるとする。


```
service cloud.firestore {
  match /databases/{database}/documents {
    // Make sure the uid of the requesting user matches the 'author_id' field
    // of the document
    match /users/{user} {
      allow write: if get(/databases/$(database)/documents/users/$(request.auth.uid)).data.admin;
    }
  }
}
```

`get(...).data.admin` で返ってくる値は `true` か `false` だし、この条件分岐でもいけるのでは…？と思ってしまうが、これはダメ。getで取得した情報の型情報などないのだから、当然といえば当然だし、JavaScriptではないので、boolean以外の値を条件式が勝手にいい感じに評価してくれるわけではない。
なので、以下のように条件を書く必要がある。

```
service cloud.firestore {
  match /databases/{database}/documents {
    // Make sure the uid of the requesting user matches the 'author_id' field
    // of the document
    match /users/{user} {
      allow write: if get(/databases/$(database)/documents/users/$(request.auth.uid)).data.admin == true;
    }
  }
}
```

## 時間によって条件評価したい

`request` という変数をrule内で使うことができる。その名の通り、firestoreへのリクエスト時に使われた情報が入っている。この中に `time` というフィールドがある。これはfirestoreへのリクエストが行われた時間が入っている。なので、この値を使えば時間を元にしたアクセス制限を行うことができる。
例えば最終ログイン時刻をfirestoreに入れていて、その最終ログイン時刻の30分以内であればreadができるというruleは以下のように書ける。

```
service cloud.firestore {
  match /databases/{database}/documents {
    // Make sure the uid of the requesting user matches the 'author_id' field
    // of the document
    match /users/{user} {
      allow write: if get(/databases/$(database)/documents/users/$(request.auth.uid)).data.lastLoginTime + duration.value(30, 'm') > request.time;
    }
  }
}
```

`duration.value(30, 'm')` で30分の部分を表している。 `lastLoginTime` はfirestoreのtimestamp型の値とする。timestamp型の値と `request.time` はそのまま比較することができる。 unix timestampの値をintegerとして `lastLoginTime` に格納していた場合などは値の変換が必要となる。
（未検証だけど、`request.time.toMillis()` とであれば比較できると思う）
security ruleで扱える値にもデータ型が存在しているので、以下のドキュメントを参照。
https://firebase.google.com/docs/reference/rules/rules

参考: https://firebase.google.com/docs/reference/rules/rules.timestamp_