firestore rulesの書き方


firestoreとは

Googleが提供するNoSQL型のデータベースです。
Cloud functionsを経由してデータベースへのCRUD処理を行うこともできますが、クライアント側から直接アクセスできるというのが特徴です。
公式ではfirestoreからの処理を推奨しています。cloud functions経由だとコールドスタートとかの影響もあり、10倍くらい処理スピードも違うので、firestoreのCRUD処理は、高速で処理を実現することできるクライアント側から実装します。

firestore rulesとは

外部からの攻撃を防ぎ、firestoreへのアクセスコントロールをするためのものです。
主に、認証やデータのバリデーションを実行するための役割を持ちます。

firestoreの利用を開始すると、testモードを選択することができ、特に意識しなくても公式手順に沿って進めれば、エラーなく、データベースへのCRUD処理を行うことができてしまいます。
しかし、これでは、認証すらしていない状況であっても、データベースへのアクセスができてしまうことになります。

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read, write: if
          request.time < timestamp.date(2022, 4, 1);
    }
  }
}

これらを意図した状態にするために適切なrulesを書いていきます。

実際の書き方

実際に書き始めるにあたって、以下がとても参考になりました。firebaseやfirestore rulesが初めてな方はまず読んでみていただけると良さそうです。
この3つで、しっかりとキャッチアップをすることができました。

アプリを構築する上で、collectionが複数ある場合、似たような記述をいくつか記載する必要が出てきます。
その場合、関数化して書いていくことで記述を楽にできます。

1.認証されているuserか

これは認証がされているユーザーのみがアクセスできるという大前提部分の記述です。
広く一般公開しているコレクションであれば問題ないですが、アプリの利用者のみにアクセスを限定したい場合に記述が必要となります。

    function isAnyAuthenticated(){
      return request.auth != null;
    }

2.ドキュメントのuserIdとリクエストしてきた認証情報の中にあるuidが一致しているか

firebase authでアカウントによるログイン機能を実装している場合、すべてのユーザーがuidを持ちます。
そのuidとドキュメントのuserIdが一致する場合に、その特定のドキュメントのアクセス権を渡したい場合に記述が必要となります。

    function isUserAuthenticated(userId){
      return isAnyAuthenticated() && request.auth.uid == userId
    }

3.ドキュメントの更新してほしくないフィールドを更新されていないか

ドキュメントで更新されるとまずいフィールドがいくつか存在するケースがあります。
例えば、groupIdなどアクセスをコントロールするためにキーになるようなフィールドです。
このような値を変えられてしまうと、本来のgroupに所属する人に限定して、公開したかったドキュメントが、そのgroupに所属する人以外にもアクセスされてしまうリスクが発生します。
そういったことが発生しないよう更新してほしくない場合に記述が必要となります。

    function isNotUpdating(fieldName){
      return !(fieldName in request.resource.data) || request.resource.data[fieldName] == resource.data[fieldName]
    }

4.リクエストしてきたdataは事前に書き込みを許可されているフィールドか

上記3のrulesで更新してほしくないフィールドは制御できますが、一方で単純にドキュメントに対して、createやupdateの権限を渡すと、本来アプリには必要のないフィールドまで登録されたりするリスクがあります。
アプリの実装者が意図したフィールドのみの書き込み処理を許可したい場合に、記述が必要となります。

    function writingToAllowedFields(allowedFields){
      return allowedFields.hasAll(request.resource.data.keys())
    }

存在しているdataはリクエストしてきたuserが所属するgroupのものであるか

存在しているドキュメントがuser単位ではなくuserが所属するgroupでアクセスコントロールをしたい&userが複数のgroupに所属する場合、
(例:Aさんはグループ1に所属しているので、自分が作成したわけじゃないけど、グループ1に所属するuserが作成したドキュメントにアクセスできる)
(例:Aさんはグループ1にアプリ上切り替えたので、自分が作成したわけじゃないけど、グループ1に所属するuserが作成したドキュメントにアクセスできる)

あらかじめ、userとgroupの結びつきを表現するドキュメントを用意し、制御することができます。

ちなみに、カスタムクレームを使うと、あらかじめ用意されている認証情報に加えて、任意の値を設定することができます。カスタムクレームで設定した値はfirestore rulesでも利用することができるようです。
他にもgroupとuserの関係をサブコレクションで表現する方法もあるかと思いますので、このあたりは状況にあわせて実装方法を選択されるとよいかと思います。

    function resourceGroupAuthenticated(){
      return resourceData().groupId in get(/databases/$(database)/documents/users/$(request.auth.uid)).data.groups
        && resourceData().groupId == get(/databases/$(database)/documents/users/$(request.auth.uid)).data.selectGroupId
    }

他にも今回は割愛しましたが、firestoreのフィールドの型であったり、文字数や桁数を制御することもできるので、特定のフィールドに対する細かなバリデーションもしっかり設定することができます。

最後に

自分1人では太刀打ちできませんでしたが、先人たちの知見のおかげで、ある程度、理解できるようになりました。
この場をおかりして、感謝申し上げます。

自分もいつか、誰かの役に立てるように、日々精進していきたいです。