
Firestoreエミュレータ+Jestでセキュリティルールをテストする!
2021.12.20
Firestore(ファイアストア)とは?
「Firestore」は、Google製のスケーラブルなドキュメントデータベースです。
他の Firebase サービス群と同様に、サーバやスケーリングの管理が不要なため、開発者はフロントエンドの開発に集中することができます。
また、従来のデータベースは、サーバプログラムを通して「データの取得・更新」を行うのが一般的です。
それに対して「Firestore」では、サーバを介さず、フロントエンドから直接、データを取得・更新するかのように扱う事ができるのです。
セキュリティルールが肝
Firestore では、「セキュリティルール(データの読み書きの許可・拒否ルール)」を記述したファイルを設置する事で、データへのアクセス制限を設定することができます。
非常に有能かつ便利な Firestore ですが、セキュリティルールの設定には細心の注意を払う必要があります。
Firestore の利点
クライアントコードに直接、データの「取得・更新」処理を書くことが出来るのが、Firestore の大きな利点の1つです。
ですが、同時に Firestore への接続情報を、クライアントコードに記述する事が必要となります。
例えば、ウェブクライアントから Firestore を利用する場合、記述された接続情報は、知識のある訪問者であれば容易に確認することができます。
データを悪用・破壊される恐れ
セキュリティルールの設定に不備がある場合、取得した接続情報を使用して、データの「取得」や「改ざん・削除」が可能となります。
なので、訪問者が悪意のあるユーザであった場合、データを悪用されたり、破壊される恐れがあります。
データの悪用や破壊を防ぐためには、セキュリティルールの設定に細心の注意を払う必要があります。
つまり、データへのアクセスを適切に制限する事が重要となります。
セキュリティルールを作成する
今回は、肝となるセキュリティルールを、「Firestoreエミュレータ」とFacebook製のテストフレームワーク「Jest」を使って、テストする方法を解説していきたいと思います。
まずは、「Jest」と「Firestoreエミュレータ」をセットアップします。
その後、実際にサンプルテストとセキュリティルールを作成していきたいと思います。
Jest(ジェスト)のセットアップ
必要な環境
Node.js | v8.0.0 以上 |
Jest環境の構築
まず最初に、「Jest」を使うために必要なパッケージをインストールします。
デモ用のプロジェクトディレクトリを作成して、プロジェクトルートで以下のコマンドを実行してください。
1 2 | npm init -y npm i -D jest @types/jest |
項目の追加・調整
生成された package.json に、以下の項目を、追加・調整してください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | { // ... "scripts": { "test": "jest", "test-watch": "jest --watch" }, // ... "jest": { "moduleFileExtensions": [ "js", "rules" ] } } |
scripts フィールドには、テストに使用するコマンドを追加しています。
test コマンドは「Jest」によるテストの実施に使用。
test-watch コマンドはファイル監視&自動再テストに使用します。
jest フィールドでは、「Jest」の設定を行っています。
moduleFileExtensions では、テスト対象となる拡張子を指定しています。
また、 rules 拡張子を追加しています。
これは、 test-watch コマンドを使用する際に、テスト対象であるセキュリティルールの設定ファイルを監視するためです。
Jestの動作チェック
プロジェクトルートに sample.test.js を作成し、以下のように記述してください。
1 2 3 4 5 | describe("Jest動作チェック", () => { test("サンプルテスト", async () => { expect(3 + 7).toBe(10); }); }); |
テストファイルを追加したら、実際にテストしてみましょう!
テストの実行
以下のコマンドを入力し、テストを実行してください。
1 | npm run test sample.test.js |
環境や「Jest」のバージョンにもよりますが、おおよそ以下のように結果が表示されれば、テストは成功です!
1 2 3 4 5 6 7 8 9 | PASS ./sample.test.js Jest動作チェック √ サンプルテスト (2ms) Test Suites: 1 passed, 1 total Tests: 1 passed, 1 total Snapshots: 0 total Time: 1.318s, estimated 2s Ran all test suites. |
Firestoreエミュレータのセットアップ
Firebase CLIのインストール
エミュレータをセットアップするには、Firebase CLI が必要となります。
以下のコマンドを実行して Firebase CLI をインストールしてください。
1 | npm install -g firebase-tools |
エミュレータをインストール
以下のコマンドを実行して、エミュレータをインストールしてください。
1 | firebase setup:emulators:firestore |
エミュレータの起動設定
プロジェクトルートに firebase.json を追加して、以下のように記述してください。
1 2 3 4 5 6 7 8 9 10 | { "firestore": { "rules": "firestore.rules" }, "emulators": { "firestore": { "port": "58080" } } } |
firestore.rules プロパティは、エミュレータ起動時に読み込む「セキュリティルールファイル」を指定しています。
emulators.firestore.port プロパティは、起動するエミュレータのポートを指定しています。
エミュレータは、デフォルトでは 8080 ポートで起動します。
ですが、 8080 ポートは、他のサービスのポートと衝突する事が多いので、今回はポートを 58080 に変更しています。
エミュレータの起動
以下のコマンドを実行して、エミュレータを起動してください。
1 | firebase emulators:start --only firestore |
以上で、Firestoreエミュレータのセットアップは完了です。
次は、セットアップした「Firestoreエミュレータ」と「Jest」を使って、実際にセキュリティルールのテストをしていきます。
Firestoreセキュリティルールをテスト
ついに、セキュリティルールをテストする環境が整いました。
では、いよいよ、セキュリティルールをテストしていきます!
今回は、折角なので、テスト可能な環境を活かします。
テスト駆動開発ライクなスタイルで、
- 1、テスト作成
- 2、エラー確認
- 3、コードの調整
- 4、テストパス
という流れで解説を進めていきたいと思います。
目標
以下のアクセス環境を目標に、テストとセキュリティルールの実装を進めて行きます。
- 認証されているユーザは、 fruits コレクションのドキュメントを読み書きできる。
- 認証されていないユーザは、 fruits コレクションのドキュメントを読み書きでない。
- fruites 以外のコレクションは、認証の有無にかかわらず、全てのユーザが読み書きできない。
必要なパッケージのインストール
プロジェクトルートで、以下のコマンドを実行して、テストに必要なパッケージをインストールしてください。
1 | npm i -D @firebase/testing |
テストコードの作成
まずは、目標に沿ってテストを作成します。
プロジェクトルートに、テストファイル firestore.rules.test.js を追加して、以下のように記述してください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 | // エミュレータホストの指定。デフォルトポートでエミュレータを起動した場合は不要 process.env.FIRESTORE_EMULATOR_HOST = "localhost:58080"; const fs = require("fs"); const firebase = require("@firebase/testing"); // 認証なしFirestoreクライアントの取得 function getFirestore() { const app = firebase.initializeTestApp({ projectId: "my-test-project" }); return app.firestore(); } // 認証付きFirestoreクライアントの取得 function getFirestoreWithAuth() { const app = firebase.initializeTestApp({ projectId: "my-test-project", auth: {uid: "test_user", email: "test_user@example.com"} }); return app.firestore(); } describe("fruitsコレクションへの認証付きでのアクセスのみを許可", () => { beforeEach(async () => { // セキュリティルールの読み込み await firebase.loadFirestoreRules({ projectId: "my-test-project", rules: fs.readFileSync("firestore.rules", "utf8") }); }); afterEach(async () => { // 使用したアプリの削除 await Promise.all(firebase.apps().map(app => app.delete())) }); describe('fruitsコレクションへの認証付きアクセスを許可', () => { test('認証なしでのデータ保存に失敗', async () => { const db = getFirestore(); const doc = db.collection('fruits').doc('apple'); await firebase.assertFails(doc.set({color: 'red'})) }); test('認証ありでのデータ保存に成功', async () => { const db = getFirestoreWithAuth(); const doc = db.collection('fruits').doc('orange'); await firebase.assertSucceeds(doc.set({color: 'orange'})) }); test('認証なしでの取得に失敗', async () => { const db = getFirestore(); const doc = db.collection('fruits').doc('strawberry'); await firebase.assertFails(doc.get()) }); test('認証ありでの取得に成功', async () => { const db = getFirestoreWithAuth(); const doc = db.collection('fruits').doc('cherry'); await firebase.assertSucceeds(doc.get()) }); }); describe('fruits以外のコレクションへのアクセス禁止', () => { test('認証なしでのデータ保存に失敗', async () => { const db = getFirestore(); const doc = db.collection('countries').doc('japan'); await firebase.assertFails(doc.set({language: 'japanese'})) }); test('認証ありでのデータ保存に失敗', async () => { const db = getFirestoreWithAuth(); const doc = db.collection('vegetables').doc('tomato'); await firebase.assertFails(doc.set({color: 'red'})) }); test('認証なしでの取得に失敗', async () => { const db = getFirestore(); const doc = db.collection('vehicles').doc('car'); await firebase.assertFails(doc.get()) }); test('認証ありでの取得に失敗', async () => { const db = getFirestoreWithAuth(); const doc = db.collection('prefectures').doc('tokyo'); await firebase.assertFails(doc.get()) }); }); }); |
テストコードで使用した変数やメソッドの解説
process.env.FIRESTORE_EMULATOR_HOST
アクセス先のFirestoreエミュレータのホストは FIRESTORE_EMULATOR_HOST で指定します。
今回は、Firestoreエミュレータの起動ポートを 58080 に変更しているので、localhost:58080 をホストに指定しています。
デフォルトポートの 8080 で起動している場合は変更は必要ありません。
firebase.initializeTestApp({projectId: <projectId>, auth: <auth>})
エミュレータにセキュリティルールをロードするメソッドです。
テストコードではテスト対象のセキュリティルールの読み込みを行っています。
firebase.loadFirestoreRules({ projectId: <projectId>, rules: <rules> })
初期化されたFirebaseアプリを返します。
auth を指定すると、指定ユーザとして認証されたアプリが返されます。
auth を指定しない場合は認証なしのアプリが返されます。
テストコードでは、テスト対象のセキュリティルールをロードした projectId ( loadFirestoreRulesauth で指定した projectId )を指定して、Firebaseアプリを取得しています。
async/await
セキュリティルールのロードや、データの保存・取得は非同期処理であるため、await表記により、非同期処理の完了を待機しています。
関連記事
テストの実行
テスト駆動のスタイルにならって、とりあえずテストを実行し、エラーさせてみます。
以下のコマンドを実行して、作成したテストを実行してください。
1 | npm run test-watch firestore.rules.test.js |
ENOENT: no such file or directory, open '...firerules.rules' のようなエラーメッセージが表示されます。
firestore.rules ファイルがないという事なので、エラーメッセージに従って、 firestore.rules ファイルを追加します。
アクセス拒否の設定
まずは安全運転で、全てのアクセスを拒否する設定を追加します。
firestore.rules ファイルを追加して、以下のように記述してください。
1 2 3 4 5 6 7 8 9 | rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { // 全てのアクセスをブロック match /{document=**} { allow read, write: if false; } } } |
firestore.rules を追加すると、自動でテストが再実施され、以下のようなテスト結果が表示されます。
1 2 3 4 5 6 7 8 9 10 11 12 13 | FAIL ./firestore.rules.test.js fruitsコレクションへの認証付きでのアクセスのみを許可 fruitsコレクションへの認証付きアクセスを許可 √ 認証なしでのデータ保存に失敗 (1079ms) × 認証ありでのデータ保存に成功 (248ms) √ 認証なしでの取得に失敗 (44ms) × 認証ありでの取得に成功 (85ms) fruits以外のコレクションへのアクセス禁止 √ 認証なしでのデータ保存に失敗 (39ms) √ 認証ありでのデータ保存に失敗 (35ms) √ 認証なしでの取得に失敗 (31ms) √ 認証ありでの取得に失敗 (34ms) // ... エラー詳細部分は省略 |
全てのアクセスをブロックしているため、 fruits コレクションへの認証付きでのアクセスも拒否されています。
アクセスを許可する設定
fruits コレクションへの認証付きアクセスを許可する設定を追加します。
以下のように、 firestore.rules を調整してください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { // 全てのアクセスをブロック match /{document=**} { allow read, write: if false; } // fruitsコレクションについては、認証されたユーザのみ読み書きを許可 match /fruits/{fruit} { allow read, write: if request.auth.uid != null; } } } |
テスト結果
再び、テストが自動で実施され、以下のようなテスト結果が表示されます。
1 2 3 4 5 6 7 8 9 10 11 12 13 | PASS ./firestore.rules.test.js fruitsコレクションへの認証付きでのアクセスのみを許可 fruitsコレクションへの認証付きアクセスを許可 √ 認証なしでのデータ保存に失敗 (1087ms) √ 認証ありでのデータ保存に成功 (52ms) √ 認証なしでの取得に失敗 (55ms) √ 認証ありでの取得に成功 (43ms) fruits以外のコレクションへのアクセス禁止 √ 認証なしでのデータ保存に失敗 (37ms) √ 認証ありでのデータ保存に失敗 (36ms) √ 認証なしでの取得に失敗 (31ms) √ 認証ありでの取得に失敗 (30ms) // ... エラー詳細部分は省略 |
全てのテストが成功しました。
これで「 fruits コレクションに認証付きのユーザだけが読み書きできるセキュリティルール」を作成することができました。
まとめ
以上、Firestoreエミュレータにも「Jest」を使って、セキュリティルールをテストしてみました。
簡単に、今回の内容をまとめてみたいと思います。
- 「Firestore」はとても便利
- 便利さゆえのリスクもあり
- セキュリティルールが肝
- Firestoreエミュレータを使えば、セキュリティルールをローカルでテスト可能
なお、作成したテストは、CI(継続的インティグレーション) に組み込む事で、継続的にアクセス制限の漏れをチェックすることができます。
ぜひ、セキュリティルールテストの、CI への組み込みも検討してみてください。
こちらの記事もオススメ!
書いた人はこんな人

- 「好きを仕事にするエンジニア集団」の(株)ライトコードです!
ライトコードは、福岡、東京、大阪の3拠点で事業展開するIT企業です。
現在は、国内を代表する大手IT企業を取引先にもち、ITシステムの受託事業が中心。
いずれも直取引で、月間PV数1億を超えるWebサービスのシステム開発・運営、インフラの構築・運用に携わっています。
システム開発依頼・お見積もり大歓迎!
また、現在「WEBエンジニア」「モバイルエンジニア」「営業」「WEBデザイナー」「WEBディレクター」を積極採用中です!
インターンや新卒採用も行っております。
以下よりご応募をお待ちしております!
https://rightcode.co.jp/recruit
ITエンタメ10月 13, 2023Netflixの成功はレコメンドエンジン?
ライトコードの日常8月 30, 2023退職者の最終出社日に密着してみた!
ITエンタメ8月 3, 2023世界初の量産型ポータブルコンピュータを開発したのに倒産!?アダム・オズボーン
ITエンタメ7月 14, 2023【クリス・ワンストラス】GitHubが出来るまでとソフトウェアの未来