Cloud Firestoreのセキュリティルールを正しく実装する

概要

セキュリティルールを実装する際に気をつけること、実装の流れについて、実際のユースケースを踏まえて記載します。
基本的な文法についてはここでは触れず、実装の全体感を掴めるようになるのが目的です。

セキュリティールール実装の前に理解すべきこと

料金体系を無視した設計になっていたり、拡張性のない設計になっていると、設計を変更するたびにセキュリティールールを再設定する必要があり手戻りになるため、事前に以下を確認することをお勧めします。

  1. 設計のベストソリューションについて (集約クエリ・分散カウンタ)
  2. 料金体系について

セキュリティルールを実装する

想定するアプリケーション

MediumやQiitaのような、記事投稿アプリをベースに考えてきます。
機能はシンプルで投稿した記事を他のユーザーが閲覧できるようなものです。
Mediumに合わせて、記事を以下ではストーリーと記載することにします。

投稿ユーザー

  1. ストーリーの作成・更新・削除ができ、他のユーザーに公開できる。
  2. ストーリーのstatusを、”published”, “draft”で設定できる。
  3. 独自のユーザー詳細ページをもち、ストーリー一覧を表示できる。
  4. ストーリーごとのview数を確認できる。

一般ユーザー

  1. statusがpublishedになっている記事を閲覧できる。
  2. ストーリーの更新・削除権限はない。
  3. ストーリーページを開いた時に、view_countのみ更新することができる。
  4. ユーザーごとのストーリー一覧を閲覧できる。

機能を元に設計した Firestoreが以下になります。


Created with Sketch. user story Cloud Funcitonでコピー functions.firestore.document(‘user/{userId}/story/{storyId}’) story (サブコレクション) uid: 54wnYhCCQLXM7Jomx2 title: 公開中のストーリーです。 view_count: { ‘total’: 0, } status: ‘published’ story (サブコレクション) uid: 54wnYhCCQLXM7Jomx2 title: 公開中のストーリーです。 view_count: { ‘total’: 0, } status: ‘published’ title: 非公開のストーリーです。 view_count: { ‘total’: 0, } status: ‘draft’ title: 公開中のストーリーです。 view_count: { ‘total’: 0, } status: ‘published’ title: 公開中のストーリーです。 view_count: { ‘total’: 0, } status: ‘published’ title: 非公開のストーリーです。 view_count: { ‘total’: 0, } status: ‘draft’

このような冗長構成をとっているのは、理由があります。

この機能を満たそうと思う時、以下の2つの実装が考えられます。

① storyモデルに外部キーを持たせ、storyモデルから該当レコードを持ってくる。
② userモデルのサブコレクションにstoryモデルを持たせ、該当レコードを取得する。ルートのstoryモデルにCloudFunctionでコピーする。(上の図はこれに該当)

今回のアプリケーションのユースケースを考えると、ストーリーの作成・更新より、閲覧が支配的だと言えます。
Firestoreの料金体系は、レコードへのアクセス数をベースとしたものなので、今回は②の実装方式をとります。
設計については詳しく別記事で解説したいと思います。

セキュリティルールの実装

上記の設計を元に作成したセキュリティルールが以下になります。

まず、3 ~ 8行目の、
storyモデル(/story/{story})へのセキュリティルールを見ていきましょう。
一般ユーザーが記事を一覧を取得する時にここを参照するので、read権限はtrueなのは理解しやすいと思います。
ただ、他の権限がfalseとなっているのはなぜでしょうか。

設計でもお話ししたように今回の設計では、userモデルのサブコレクションのstoryモデル (/user/{user}/story/{story})にレコードができると、Cloud Functionによって、storyモデル(/story/{story})にコピーされます。

この際のタスクが特権権限で実行されるため、セキュリティルールの対象外となり、そのためstoryモデルへのセキュリティルールはread以外がfalseとなっています。(Firebase Admin SDK) 

次に、9行目以降の、
userモデル(/user/{user})へのセキュリティルールを見ていきましょう。
今回はuserモデルは他ユーザーから閲覧可能とするので、read権限はtrueです。
update / deleteについては、同一ユーザーのみ許可しています。
もしも、他のユーザーから隠蔽したいデータ(個人情報や、外部サービスのkeyなど)がある場合についての設計は、別記事にて記載したいと思います。

23行目以降の、
userモデルのサブコレクションであるstoryモデルを見ていきましょう。
ここに作成されたレコードが、ルートのstoryモデル(/story/{story})にコピーされるのでした。
ストーリーごとに閲覧数のカラム(view_count)をもち、これは全ユーザーから更新可能にしたいですが、他のカラムは投稿ユーザー以外から隠蔽したい時、どのようにすればいいでしょうか。
そのためのソリューションが、10行目のonlyViewCountChanged()です。
ここで、変更前後のレコードを比較し、view_count以外のカラムの内容が書き換わっていないこと、カラムが追加・削除されていないことを保証しています。

セキュリティルールをどこまで詳細に設定するか

QiitaやMediumのようなCGMライクなアプリケーションを想定してセキュリティルールを設計しました。
実際に設計してみて感じた方もいたと思うのですが、セキュリティルールはどこまで詳細にするべきなのでしょうか。
私の考えですが、セキュリティルールの設計よりも、Firestoreそのものの設計がセキュリティに大きな影響を及ぼすと思っています。
そのため、正しいFirestoreの設計ができている前提で、セキュリティルールは以下のチェックリストを満たす程度に詳細化できていればいいと思います。

  • アプリケーションの機密情報は、admin権限でのみアクセス可能としている。
  • ユーザーの機密情報は、そのユーザーにしかアクセスできない。
  • ログインユーザーのみにアクセスできるデータが、ルールで保護されている。

5