Firestoreのセキュリティルールとは

#はじめに
Firestoreのセキュリティルールについて備忘録としてまとめます。

#セキュリティルールとは
このルールを設定することによって`保管したデータのセキュリティを確保`でき、`悪意のあるユーザーからデータを守ること`ができます。

単純なルールだけでなく、アプリの構造と動作に応じて複雑なルールも自分で設定できます。
[Cloud Firestore セキュリティ ルールを使ってみる | Firebase](https://firebase.google.com/docs/firestore/security/get-started?hl=ja)

このセキュリティルールの設定方法は

・Firebaseコンソール

・[Firebase CLI](https://firebase.google.com/docs/cli?hl=ja)

の2種類から設定できます。

要するに`ブラウザから直接ルールを編集`してルールを決めるか、`自分のPCでファイルから設定を記述`してルールを決めるかですね。

因みに自分は、Firebaseコンソールから設定しています。

#セキュリティルールの基本的な構成
デフォルトでは以下のようになっています。

```
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read, write: if
          request.time < timestamp.date(2021, 3, 18);
    }
  }
}
```
順にコードの意味を理解していきましょう。

##rules_version = '2';
`バージョン`を指定しており、`バージョン2`が最新となっています。

バージョンの指定がないと自動的に`バージョン1`になります。

##service cloud.firestore
`ルールを適用するプロダクト(サービス)`を指定しています。

他のプロダクトとの競合を防ぐための設定なので、`1つ`しか設定できません。

Firebase CLIを使用してFirestoreとFirebase Storageの両方のルールを定義する場合は
`それぞれ別のファイルでルールを保守`しなくてはいけません。

##match /databases/{database}/documents
`ルールを適用するプロジェクト内でのFirestoreデータベース`を指定しています。

現在、Firestoreでは`1つのプロジェクトに1つのデータベース`と決まっていて`(default)`という名前が付いています。
[Cloud Firestore セキュリティ ルールを構造化する | Firebase](https://firebase.google.com/docs/firestore/security/rules-structure?hl=ja)

##match /{document=**}
先ほども登場しましたが、matchステートメントを使用することで`パス(場所)`を指定することが出来ます。

指定先は`ドキュメントのみ`しか指定できず、`コレクションは指定できません`。

matchステートメントの次に記述されている括弧の部分は`ワイルドカード表記`と呼ばれます。

```
// ワイルドカード表記
{document=**}
```
この表記方法を用いれば`任意のドキュメントを指定`することができます。

例えば、foodコレクションがあって、その中のドキュメントを指定するとします。

```
// foodコレクションの任意のドキュメントを指定する
// {}の中は自由に記述可能で、変数として使うこともできる
match /food/{meet} {
   allow read: if resource.data.name == meet;
}

※resource.data.name "ドキュメントに格納されている全てのフィールドと値のマップを参照できる"

// 特定のドキュメントも指定できる
// しかし、fruitsドキュメント以外の読み書きは拒否される
match /food/fruits {
}
```
このようになります。

前後しますが、何度も登場している{document=**}は`再帰ワイルドカード`と呼ばれ、普通のワイルドカードとは少し違います。

```
// 再帰ワイルドカード
{document=**}

// ワイルドカード
{meet}
```

これは例えば、`各階層のサブコレクション全てにルールを適用したい場合`などに使用すると非常に便利です。

例えば、foodコレクションより下の階層全てにルールを適用したい場合だと下記のようになります。

```
// foodコレクション以下の全てのドキュメントに対してルールを適用する
match /food/{document=**} {
}

// 普通のワイルドカードだとfoodコレクション内のドキュメントしか指定できず、それより下の階層は指定できない
match /food/{document} {
}
```
以上を踏まえてデフォルトで記述されている下記のコードの意味を考えると、`プロジェクト内全てのドキュメントに対してルールが適用される`という意味合いになります。

```
// プロジェクト内すべてのドキュメントに対してルールを適用する
match /{document=**} {
}
```

##allow read, write: if
この部分が実際にセキュリティルールを指定する所で`読み書きの許可条件`を決めます。

分かりやすくするとこんな感じ↓

```
allow [アクセスタイプ(メソッド)]: if [条件];
```
allowステートメントの後に記述されている`read`や`write`なんかは`アクセスタイプ(メソッド)`として機能しており、`ユーザーに対して読み書きのアクセス権を付与`します。

大きく分けると`write`と`read`の2つなのですが、ここから更に`細分化`することが出来ます。

```
read "あらゆるタイプの読み取りリクエスト"
 - get "単一のドキュメントを対象とした読み取りリクエスト"
 - list "クエリとコレクションを対象とした読み取りリクエスト"

write "あらゆるタイプの書き込みリクエスト"
 - create "新しいドキュメントの書き込み"
 - update "既存のドキュメントへの書き込み"
 - delete "データの削除"
```
これらのアクセスタイプ(メソッド)を、必要に応じて加えていく感じですね。

そして、ifの後には様々な`条件`を書き足すことができます。

デフォルトだと、改行されていて分かりにくいですが`request.time < timestamp.date(2021, 3, 18);`が条件ですね(後ほど説明)。

条件のよくある例として、`Bool値`を記述して読み書きしてもいいかを決めます。

```
// 読み書きを許可する
allow read, write: if true;

// 読み書きを拒否する
allow read, write: if false;
```

デフォルトではBool値が省略されており、その場合は無条件で`true`になります。

```
// 無条件でtrueが返される
allow read, write: if
```

##request.time < timestamp.date(2021, 3, 18);
先ほどのifの後に続く`条件`です。

意味としては、`2021/3/18の日付より過去なら読み書き可能`という感じになっています。



#Firebase Authenticationとの連携
Firestoreと`Firebase Authentication`を連携して、より強固なセキュリティルールを設定することが出来ます。
[セキュリティ ルールと Firebase Authentication | Firebase](https://firebase.google.com/docs/rules/rules-and-auth?hl=ja)

※Firebase Authenticationがアプリにて実装されていることが大前提です。

##ユーザーを識別する
Firestoreにはユーザーを識別するために使用できる`変数`がいくつか用意されています。

例えばログインプロバイダに`メール/パスワード`を用いている場合は、

```
uid "リクエストしているユーザーに割り当てられたユーザーID"
auth.token.email "アカウントに関連付けられているメールアドレス"
```
などが使用できます。

他にも色々、変数が存在しますので[公式ドキュメント](https://firebase.google.com/docs/rules/rules-and-auth?hl=ja)を読んでみて下さい。

##ルールでユーザー情報を使用する
早速、セキュリティルールにユーザー情報を使用してみましょう。



下記では、usersコレクションの任意のドキュメントを指定しており、

```
service cloud.firestore {
  match /databases/{database}/documents {
    match /users/{userId} {
      // アカウントが存在し、auth.uid変数とリクエスト送信者のユーザーIDが一致していれば読み書きを許可する
      allow read, write: if request.auth != null && request.auth.uid == userId;
    }
  }
}
```
という構成になっています。

##requst.auth != null
`request.auth`は、`Firebase Authenticationから取得した認証情報`を表しており、ここでは`アカウントが存在していれば`という意味になります。

##request.auth.uid == userId
この部分は、`アカウントのuidとFirestoreに保存してあるユーザーIDを照合`して一致していれば読み書きを許可します。

```
match /users/{userId} {
  allow read, write: if request.auth.uid == userId;
}
```

#関数
セキュリティルールには`関数`を定義することができ、`一連の条件をまとめる`ことが出来ます。

```
service cloud.firestore {
  match /databases/{database}/documents {

    // アカウントが存在していればtrueを返す関数
    function isAuthentication() {
      return request.auth.uid != null;
    }

    // このように複数の場所から条件を指定できる
    match /cities/city {
      allow read: if isAuthentication();
    }
    match /users/{user} {
      allow read, write: if isAuthentication();
    }
  }
}
```

関数に`引数`を割り当てることも可能です。

```
function isMe(userEmail) {
    return request.auth.token.email == userEmail;
}

match /users/{userEmail} {
    allow write, read: if isMe(userEmail);
}
```

#おわりに
とにかく[公式ドキュメント](https://firebase.google.com/docs/firestore/security/rules-structure?hl=ja)を読もう。

次は、実際にFirestoreとFirebase Authenticationを連携してユーザーによって保存されているデータを分ける方法をまとめようと思います。

#参考記事
[Cloud Firestore セキュリティ ルールを構造化する](https://firebase.google.com/docs/firestore/security/rules-structure?hl=ja)
[Firestoreセキュリティルールを読み解く](https://qiita.com/hiroki-harada/items/99d13efe432eb89e9391)
[Firestore セキュリティルール ～入門編～](https://rightcode.co.jp/blog/information-technology/firestore-security-rules-introduction)