
テスト駆動で学ぶ Firestore セキュリティルール 【データ検証編 / 後編】
2021.12.20
テスト駆動で商品データのルールを実装する【後編】
この記事では、前編・後編の二回に分けて、Firestore 上の商品データコレクションのセキュリティルール実装を例に、ルールによるデータ検証について解説しています。
前編では、「実装目標の設定」から「データ追加ルールの実装」までを解説しました。
後編では、「データ更新・取得ルールのテスト駆動による実装」を進めつつ、「データベースにすでにあるデータの利用」などについて解説していきます。
簡単な流れ
以下のような流れで解説を進めていきます。
- Firestore 上のデータ更新ルールを実装・解説
- Firestore 上のデータ取得ルールを実装・解説
- 全体テストで要件の最終チェック
- まとめ
前編の記事はこちら
Firestore 上の商品データ更新ルールの実装
テストの作成
前編で説明した、データ追加ルールの実装と同様、テスト駆動で「商品データ更新ルール」の実装を進めていきます。
テストを作成する前に、商品データ更新の要件を再確認しましょう。
- データ更新をリクエストしたユーザが商品管理者と一致しない場合は追加不可
- 商品管理者の変更不可
- ロックされたデータの更新不可
- 商品名の変更不可
上記の要件に沿って、テストを作成します。
tests/data.update.test.ts ファイルを追加して、以下のようにコードを記述してください。
テストのコード
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 | process.env.FIRESTORE_EMULATOR_HOST = "localhost:58080"; import {FirestoreTestSupporter} from "firestore-test-supporter"; import * as firebase from "@firebase/testing"; import path from "path"; import TestData from "./TestData"; describe("データ更新テスト", () => { const supporter = new FirestoreTestSupporter("my-test-project", path.join(__dirname, "firestore.rules")); const collectionPath = TestData.getCollectionPath(); const item_id = TestData.getItemId(); const initialData = TestData.getInitialData(); const validUpdateData = TestData.getValidUpdateData(); const admin_user = TestData.getAdminUser(); beforeEach(async () => { await supporter.loadRules(); // 初期データを追加 const db = supporter.getFirestoreWithAuth(admin_user); const doc = db.collection(collectionPath).doc(item_id); await doc.set(initialData) }); afterEach(async () => { await supporter.cleanup(); }); test('要件にあったデータの更新に成功', async () => { const db = supporter.getFirestoreWithAuth(admin_user); const doc = db.collection(collectionPath).doc(item_id); await firebase.assertSucceeds(doc.update(validUpdateData)) }); test('データ更新をリクエストしたユーザが商品管理者と一致しない場合は追加不可', async () => { const db = supporter.getFirestoreWithAuth("other_user"); const doc = db.collection(collectionPath).doc(item_id); await firebase.assertFails(doc.update(validUpdateData)) }); test('商品管理者の変更不可', async () => { const other_user = "other_user"; const db = supporter.getFirestoreWithAuth(other_user); const doc = db.collection(collectionPath).doc(item_id); // 商品管理者を変更したデータを作成 const bad_data = {...validUpdateData, admin_user: other_user}; await firebase.assertFails(doc.update(bad_data)) }); test('ロックされたデータの更新不可', async () => { const db = supporter.getFirestoreWithAuth(admin_user); const doc = db.collection(collectionPath).doc(item_id); // データをロック状態に設定 const locked_data = {...validUpdateData, locked: true}; await firebase.assertSucceeds(doc.update(locked_data)); // ロックされたデータの更新に失敗 const update_data = {...locked_data, price: 3000}; await firebase.assertFails(doc.update(update_data)); }); test('商品名の変更不可', async () => { const db = supporter.getFirestoreWithAuth(admin_user); const doc = db.collection(collectionPath).doc(item_id); const bad_data = {...validUpdateData, title: "吾輩は犬ではない!"}; await firebase.assertFails(doc.update(bad_data)) }); }); |
テストの概要
初期データの準備
beforeEach(...) 内で、初期データの準備を行っています。
「商品管理者の変更不可」テスト
const bad_data = {...validUpdateData, admin_user: other_user}; の部分で、更新データに元の商品管理者とは異なるユーザを設定しています。
「ロックされたデータの更新不可」テスト
以下の部分で、更新失敗をチェックする前に、ロックされたデータを用意しています。
1 2 | const locked_data = {...validUpdateData, locked: true}; await firebase.assertSucceeds(doc.update(locked_data)); |
Firestore セキュリティルールの実装
商品データ更新用のテストが完成したので、ルールの実装を進めていきます。
テストをスタート
以下のコマンドを実行し、テストをスタートさせてください。
1 | npm run test-watch tests/data.update.test.ts |
テスト結果
以下のような、テスト結果が表示されます。
1 2 3 4 5 6 7 | FAIL .../tests/data.update.test.ts データ更新テスト × 要件にあったデータの更新に成功 (2366ms) √ データ更新をリクエストしたユーザが商品管理者と一致しない場合は追加不可 (81ms) √ 商品管理者の変更不可 (81ms) × ロックされたデータの更新不可 (71ms) √ 商品名の変更不可 (66ms) |
商品データ追加をテストしたときと同じく、上から順にテストを通していきます。
全ての update アクセスを許可するセキュリティルールを設定
「要件にあったデータの更新に成功」テストを通すため、とりあえず全ての update アクセスを許可してみます。
以下のように Firestore セキュリティルールを調整してください。
1 2 3 4 5 6 7 8 9 10 | rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { match /items/{item_id}{ allow create: if request.auth.uid == request.resource.data.admin_user; allow update: if true; } } } |
テスト結果
以下のような、テスト結果が表示されます。
1 2 3 4 5 6 7 | FAIL .../tests/data.update.test.ts データ更新テスト √ 要件にあったデータの更新に成功 (2171ms) × データ更新をリクエストしたユーザが商品管理者と一致しない場合は追加不可 (97ms) × 商品管理者の変更不可 (94ms) × ロックされたデータの更新不可 (82ms) × 商品名の変更不可 (73ms) |
「要件にあったデータの更新に成功」テストが成功しました。
ユーザ情報を許可条件に使用するセキュリティルールを設定
次は、「データ更新をリクエストしたユーザが商品管理者と一致しない場合は追加不可」テストを通したいと思います。
商品データ追加の要件と同じく、 create に対するルールにならって、以下のようにルールを調整します。
1 2 3 4 5 6 7 8 9 10 | rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { match /items/{item_id}{ allow create: if request.auth.uid == request.resource.data.admin_user; allow update: if request.auth.uid == request.resource.data.admin_user; } } } |
テスト結果
テスト結果は、こんな感じになりました。
1 2 3 4 5 6 7 | FAIL tests/data.update.test.ts データ更新テスト √ 要件にあったデータの更新に成功 (2126ms) √ データ更新をリクエストしたユーザが商品管理者と一致しない場合は追加不可 (85ms) × 商品管理者の変更不可 (78ms) × ロックされたデータの更新不可 (69ms) × 商品名の変更不可 (66ms) |
データベース情報を許可条件に使用するセキュリティルールを設定
次に、「商品管理者の変更不可」テストを通すため、以下のようにルールを調整してください。
1 2 3 4 5 6 7 8 9 10 11 | rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { match /items/{item_id}{ allow create: if request.auth.uid == request.resource.data.admin_user; allow update: if request.auth.uid == request.resource.data.admin_user && request.resource.data.admin_user == resource.data.admin_user; } } } |
resource.data フィールドを使って、データベースに保存されているデータを参照することができます。
request.resource.data.admin_user == resource.data.admin_user の条件では、リクエストデータと既存データの admin_user が一致していることをチェックし、商品管理者に変更がないことを確認しています。
テスト結果
テスト結果は、以下の通りとなります。
1 2 3 4 5 6 7 | FAIL tests/data.update.test.ts データ更新テスト √ 要件にあったデータの更新に成功 (2144ms) √ データ更新をリクエストしたユーザが商品管理者と一致しない場合は追加不可 (88ms) √ 商品管理者の変更不可 (73ms) × ロックされたデータの更新不可 (74ms) × 商品名の変更不可 (69ms) |
「商品管理者の変更不可」テストが通りました。
残りの要件をセキュリティルールに設定
次に、残りのテストを通すため、以下のように Firestore セキュリティルールを調整してください。
1 2 3 4 5 6 7 8 9 10 11 12 13 | rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { match /items/{item_id}{ allow create: if request.auth.uid == request.resource.data.admin_user; allow update: if request.auth.uid == request.resource.data.admin_user && request.resource.data.admin_user == resource.data.admin_user && request.resource.data.title == resource.data.title && resource.data.locked == false; } } } |
「商品管理者の変更不可」の場合と同様、 request.resource.data.title == resource.data.title の条件により「商品名の変更不可」としています。
また、 resource.data.locked == false の条件により、「ロックされたデータの更新不可」としています。
テスト結果
テスト結果は、以下の通りとなります。
1 2 3 4 5 6 7 | PASS tests/data.update.test.ts データ更新テスト √ 要件にあったデータの更新に成功 (2137ms) √ データ更新をリクエストしたユーザが商品管理者と一致しない場合は追加不可 (83ms) √ 商品管理者の変更不可 (82ms) √ ロックされたデータの更新不可 (89ms) √ 商品名の変更不可 (80ms) |
テストが全て通りましたので、以上で商品データ更新用のセキュリティルールの実装は完了です。
次のテストとの競合を避けるために、商品データ更新用のテストを終了してください。
Firestore 上の商品データ取得ルールの実装
テストの作成
最後に、商品データ取得用の Firestore セキュリティルールをテスト駆動で実装していきます。
テストを作成する前に、データ取得の要件を再確認しましょう。
- 認証されていないユーザはデータ取得不可
- 売り切れ商品のデータ取得不可
上記の要件に沿って、テストを作成します。
tests/data.read.test.ts ファイルを追加し、以下のようにコードを記述してください。
テストのコード
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 | process.env.FIRESTORE_EMULATOR_HOST = "localhost:58080"; import {FirestoreTestSupporter} from "firestore-test-supporter"; import * as firebase from "@firebase/testing"; import path from "path"; import TestData from "./TestData"; describe("データ取得テスト", () => { const supporter = new FirestoreTestSupporter("my-test-project", path.join(__dirname, "firestore.rules")); const collectionPath = TestData.getCollectionPath(); const item_id = TestData.getItemId(); const initialData = TestData.getInitialData(); const validUpdateData = TestData.getValidUpdateData(); const admin_user = TestData.getAdminUser(); beforeEach(async () => { await supporter.loadRules(); // 初期データを追加 const db = supporter.getFirestoreWithAuth(admin_user); const doc = db.collection(collectionPath).doc(item_id); await doc.set(initialData) }); afterEach(async () => { await supporter.cleanup(); }); test('要件にあったデータの取得に成功', async () => { // 任意のユーザで認証 const db = supporter.getFirestoreWithAuth("any_user"); const doc = db.collection(collectionPath).doc(item_id); await firebase.assertSucceeds(doc.get()) }); test('認証されていないユーザはデータ取得不可', async () => { const db = supporter.getFirestore(); const doc = db.collection(collectionPath).doc(item_id); await firebase.assertFails(doc.get()) }); test('売り切れ商品のデータ取得不可', async () => { const db = supporter.getFirestoreWithAuth(admin_user); const doc = db.collection(collectionPath).doc(item_id); // データを売り切れに設定 const sold_out_data = {...validUpdateData, sold_out: true}; await firebase.assertSucceeds(doc.set(sold_out_data)); await firebase.assertFails(doc.get()) }) }); |
テストの概要
「要件にあったデータの取得に成功」テスト
任意の認証されたユーザがデータを取得できるので、任意ユーザ any_user で認証されたクライアントを使用しています。
「売り切れ商品のデータ取得不可」テスト
以下のコードで、売り切れ商品のデータを用意しています。
1 2 | const sold_out_data = {...validUpdateData, sold_out: true}; await firebase.assertSucceeds(doc.set(sold_out_data)); |
Firestore セキュリティルールの実装
テストに沿って、Firestore セキュリティルールの実装を進めていきます。
テストをスタート
以下のコマンドを実行し、テストをスタートしてください。
1 | npm run test-watch tests/data.get.test.ts |
全ての read アクセスを許可するセキュリティルールを設定
商品データの追加・更新テストの場合と同様、この時点では、「要件にあったデータの取得に成功」テストは「失敗」します。
まずは、これまでと同様、全ての read アクセスを許可してみます。
以下のようにルールを調整してください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { match /items/{item_id}{ allow create: if request.auth.uid == request.resource.data.admin_user; allow update: if request.auth.uid == request.resource.data.admin_user && request.resource.data.admin_user == resource.data.admin_user && request.resource.data.title == resource.data.title && resource.data.locked == false; allow read: if true; } } } |
テスト結果
テスト結果は、以下の通りとなります。
1 2 3 4 5 | FAIL tests/data.read.test.ts データ取得テスト √ 要件にあったデータの取得に成功 (2178ms) × 認証されていないユーザはデータ取得不可 (95ms) × 売り切れ商品のデータ取得不可 (83ms) |
「要件にあったデータの取得に成功」したので、残りの取得不可ルールを追加していきます。
商品データ取得の要件をセキュリティルールに設定
以下のように Firestore セキュリティルールを調整してください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { match /items/{item_id}{ allow create: if request.auth.uid == request.resource.data.admin_user; allow update: if request.auth.uid == request.resource.data.admin_user && request.resource.data.admin_user == resource.data.admin_user && request.resource.data.title == resource.data.title && resource.data.locked == false; allow read: if request.auth.uid != null && resource.data.sold_out == false; } } } |
ここで、 request.auth.uid != null により、認証されているユーザだけがデータ取得可能としています。
また、 resource.data.sold_out == false の条件により、売り切れていない商品のみ取得可能としています。
テスト結果
テスト結果は、以下の通りとなります。
1 2 3 4 5 | PASS tests/data.read.test.ts データ取得テスト √ 要件にあったデータの取得に成功 (2145ms) √ 認証されていないユーザはデータ取得不可 (160ms) √ 売り切れ商品のデータ取得不可 (86ms) |
テストが全て成功したので、以上で商品データ取得用のセキュリティルールの実装は完了です。
テストを終了してください。
全ての要件の再チェック
最後に、各項目でのルールの変更により、前に設定したルールが要件から外れていないかを確認します。
全てのテストを実行
以下のコマンドを実行し、全てのテストを実行してください。
1 | npm run test-watch |
テスト結果
テスト結果は、このような感じになりました!
1 2 3 4 5 6 | PASS tests/data.add.test.ts PASS tests/data.read.test.ts PASS tests/data.update.test.ts Test Suites: 3 passed, 3 total Tests: 10 passed, 10 total |
全てのテストに成功したので、これで前編冒頭に設定した、要件に沿ったルールの実装が完了しました。
Firestore セキュリティルールによるデータ検証のまとめ
以上、Firestore セキュリティルールでのデータの検証を、テスト駆動で解説してみました。
この記事では、解説のわかりやすさのため、個別のテストファイルについてテストを実施してきました。
ですが、実際の開発では、最初から全テストを実行することをおすすめします。
なぜなら、実装と並行して常にリグレッションテストが実行されるからです。
既存のテストの失敗に早目に気づくことができ、原因の特定・修正も比較的簡単で済みます。
最後に、今回の内容を簡単にまとめてみたいと思います。
- request.auth フィールドィールドでリクエストユーザの情報を参照できる
- request.resource.data フィールドでリクエストデータを参照できる
- resource.data で既存データを参照できる
- 更新不可ルールは request.resource.data.some_data == resource.data.some_data で実装
- request.auth.uid != null で認証なしユーザをブロック
こちらの記事もオススメ!
データ検証編はこちら
「データ検証編」では、リクエストデータ、データベース上のデータの参照などについて解説
データ比較編はこちら
「データ比較編」では、データの比較や型チェックなどについて解説
カスタム関数編はこちら
「カスタム関数編」では、「比較編」で作成したルールをもとに、カスタム関数の使い方を解説
書いた人はこんな人

- 「好きを仕事にするエンジニア集団」の(株)ライトコードです!
ライトコードは、福岡、東京、大阪の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が出来るまでとソフトウェアの未来