テスト駆動で学ぶ Firestore セキュリティルール 【データ検証編 / 後編】
IT技術
テスト駆動で商品データのルールを実装する【後編】
この記事では、前編・後編の二回に分けて、Firestore 上の商品データコレクションのセキュリティルール実装を例に、ルールによるデータ検証について解説しています。
前編では、「実装目標の設定」から「データ追加ルールの実装」までを解説しました。
後編では、「データ更新・取得ルールのテスト駆動による実装」を進めつつ、「データベースにすでにあるデータの利用」などについて解説していきます。
簡単な流れ
以下のような流れで解説を進めていきます。
- Firestore 上のデータ更新ルールを実装・解説
- Firestore 上のデータ取得ルールを実装・解説
- 全体テストで要件の最終チェック
- まとめ
前編の記事はこちら
Firestore 上の商品データ更新ルールの実装
テストの作成
前編で説明した、データ追加ルールの実装と同様、テスト駆動で「商品データ更新ルール」の実装を進めていきます。
テストを作成する前に、商品データ更新の要件を再確認しましょう。
- データ更新をリクエストしたユーザが商品管理者と一致しない場合は追加不可
- 商品管理者の変更不可
- ロックされたデータの更新不可
- 商品名の変更不可
上記の要件に沿って、テストを作成します。
tests/data.update.test.ts ファイルを追加して、以下のようにコードを記述してください。
テストのコード
1process.env.FIRESTORE_EMULATOR_HOST = "localhost:58080";
2
3import {FirestoreTestSupporter} from "firestore-test-supporter";
4import * as firebase from "@firebase/testing";
5import path from "path";
6
7import TestData from "./TestData";
8
9describe("データ更新テスト", () => {
10 const supporter = new FirestoreTestSupporter("my-test-project", path.join(__dirname, "firestore.rules"));
11
12 const collectionPath = TestData.getCollectionPath();
13 const item_id = TestData.getItemId();
14 const initialData = TestData.getInitialData();
15 const validUpdateData = TestData.getValidUpdateData();
16 const admin_user = TestData.getAdminUser();
17
18 beforeEach(async () => {
19 await supporter.loadRules();
20
21 // 初期データを追加
22 const db = supporter.getFirestoreWithAuth(admin_user);
23 const doc = db.collection(collectionPath).doc(item_id);
24 await doc.set(initialData)
25 });
26
27 afterEach(async () => {
28 await supporter.cleanup();
29 });
30
31 test('要件にあったデータの更新に成功', async () => {
32 const db = supporter.getFirestoreWithAuth(admin_user);
33 const doc = db.collection(collectionPath).doc(item_id);
34 await firebase.assertSucceeds(doc.update(validUpdateData))
35 });
36
37 test('データ更新をリクエストしたユーザが商品管理者と一致しない場合は追加不可', async () => {
38 const db = supporter.getFirestoreWithAuth("other_user");
39 const doc = db.collection(collectionPath).doc(item_id);
40 await firebase.assertFails(doc.update(validUpdateData))
41 });
42
43 test('商品管理者の変更不可', async () => {
44 const other_user = "other_user";
45 const db = supporter.getFirestoreWithAuth(other_user);
46 const doc = db.collection(collectionPath).doc(item_id);
47
48 // 商品管理者を変更したデータを作成
49 const bad_data = {...validUpdateData, admin_user: other_user};
50 await firebase.assertFails(doc.update(bad_data))
51 });
52
53 test('ロックされたデータの更新不可', async () => {
54 const db = supporter.getFirestoreWithAuth(admin_user);
55 const doc = db.collection(collectionPath).doc(item_id);
56
57 // データをロック状態に設定
58 const locked_data = {...validUpdateData, locked: true};
59 await firebase.assertSucceeds(doc.update(locked_data));
60
61 // ロックされたデータの更新に失敗
62 const update_data = {...locked_data, price: 3000};
63 await firebase.assertFails(doc.update(update_data));
64 });
65
66 test('商品名の変更不可', async () => {
67 const db = supporter.getFirestoreWithAuth(admin_user);
68 const doc = db.collection(collectionPath).doc(item_id);
69
70 const bad_data = {...validUpdateData, title: "吾輩は犬ではない!"};
71 await firebase.assertFails(doc.update(bad_data))
72 });
73});
テストの概要
初期データの準備
beforeEach(...) 内で、初期データの準備を行っています。
「商品管理者の変更不可」テスト
const bad_data = {...validUpdateData, admin_user: other_user}; の部分で、更新データに元の商品管理者とは異なるユーザを設定しています。
「ロックされたデータの更新不可」テスト
以下の部分で、更新失敗をチェックする前に、ロックされたデータを用意しています。
1const locked_data = {...validUpdateData, locked: true};
2await firebase.assertSucceeds(doc.update(locked_data));
Firestore セキュリティルールの実装
商品データ更新用のテストが完成したので、ルールの実装を進めていきます。
テストをスタート
以下のコマンドを実行し、テストをスタートさせてください。
1npm run test-watch tests/data.update.test.ts
テスト結果
以下のような、テスト結果が表示されます。
1 FAIL .../tests/data.update.test.ts
2 データ更新テスト
3 × 要件にあったデータの更新に成功 (2366ms)
4 √ データ更新をリクエストしたユーザが商品管理者と一致しない場合は追加不可 (81ms)
5 √ 商品管理者の変更不可 (81ms)
6 × ロックされたデータの更新不可 (71ms)
7 √ 商品名の変更不可 (66ms)
商品データ追加をテストしたときと同じく、上から順にテストを通していきます。
全てのアクセスを許可するセキュリティルールを設定
「要件にあったデータの更新に成功」テストを通すため、とりあえず全ての update アクセスを許可してみます。
以下のように Firestore セキュリティルールを調整してください。
1rules_version = '2';
2service cloud.firestore {
3 match /databases/{database}/documents {
4 match /items/{item_id}{
5 allow create: if request.auth.uid == request.resource.data.admin_user;
6
7 allow update: if true;
8 }
9 }
10}
テスト結果
以下のような、テスト結果が表示されます。
1 FAIL .../tests/data.update.test.ts
2 データ更新テスト
3 √ 要件にあったデータの更新に成功 (2171ms)
4 × データ更新をリクエストしたユーザが商品管理者と一致しない場合は追加不可 (97ms)
5 × 商品管理者の変更不可 (94ms)
6 × ロックされたデータの更新不可 (82ms)
7 × 商品名の変更不可 (73ms)
「要件にあったデータの更新に成功」テストが成功しました。
ユーザ情報を許可条件に使用するセキュリティルールを設定
次は、「データ更新をリクエストしたユーザが商品管理者と一致しない場合は追加不可」テストを通したいと思います。
商品データ追加の要件と同じく、create に対するルールにならって、以下のようにルールを調整します。
1rules_version = '2';
2service cloud.firestore {
3 match /databases/{database}/documents {
4 match /items/{item_id}{
5 allow create: if request.auth.uid == request.resource.data.admin_user;
6
7 allow update: if request.auth.uid == request.resource.data.admin_user;
8 }
9 }
10}
テスト結果
テスト結果は、こんな感じになりました。
1 FAIL tests/data.update.test.ts
2 データ更新テスト
3 √ 要件にあったデータの更新に成功 (2126ms)
4 √ データ更新をリクエストしたユーザが商品管理者と一致しない場合は追加不可 (85ms)
5 × 商品管理者の変更不可 (78ms)
6 × ロックされたデータの更新不可 (69ms)
7 × 商品名の変更不可 (66ms)
データベース情報を許可条件に使用するセキュリティルールを設定
次に、「商品管理者の変更不可」テストを通すため、以下のようにルールを調整してください。
1rules_version = '2';
2service cloud.firestore {
3 match /databases/{database}/documents {
4 match /items/{item_id}{
5 allow create: if request.auth.uid == request.resource.data.admin_user;
6
7 allow update: if request.auth.uid == request.resource.data.admin_user
8 && request.resource.data.admin_user == resource.data.admin_user;
9 }
10 }
11}
resource.data フィールドを使って、データベースに保存されているデータを参照することができます。
request.resource.data.admin_user == resource.data.admin_user の条件では、リクエストデータと既存データの admin_user が一致していることをチェックし、商品管理者に変更がないことを確認しています。
テスト結果
テスト結果は、以下の通りとなります。
1 FAIL tests/data.update.test.ts
2 データ更新テスト
3 √ 要件にあったデータの更新に成功 (2144ms)
4 √ データ更新をリクエストしたユーザが商品管理者と一致しない場合は追加不可 (88ms)
5 √ 商品管理者の変更不可 (73ms)
6 × ロックされたデータの更新不可 (74ms)
7 × 商品名の変更不可 (69ms)
「商品管理者の変更不可」テストが通りました。
残りの要件をセキュリティルールに設定
次に、残りのテストを通すため、以下のように Firestore セキュリティルールを調整してください。
1rules_version = '2';
2service cloud.firestore {
3 match /databases/{database}/documents {
4 match /items/{item_id}{
5 allow create: if request.auth.uid == request.resource.data.admin_user;
6
7 allow update: if request.auth.uid == request.resource.data.admin_user
8 && request.resource.data.admin_user == resource.data.admin_user
9 && request.resource.data.title == resource.data.title
10 && resource.data.locked == false;
11 }
12 }
13}
「商品管理者の変更不可」の場合と同様、request.resource.data.title == resource.data.title の条件により「商品名の変更不可」としています。
また、resource.data.locked == false の条件により、「ロックされたデータの更新不可」としています。
テスト結果
テスト結果は、以下の通りとなります。
1 PASS tests/data.update.test.ts
2 データ更新テスト
3 √ 要件にあったデータの更新に成功 (2137ms)
4 √ データ更新をリクエストしたユーザが商品管理者と一致しない場合は追加不可 (83ms)
5 √ 商品管理者の変更不可 (82ms)
6 √ ロックされたデータの更新不可 (89ms)
7 √ 商品名の変更不可 (80ms)
テストが全て通りましたので、以上で商品データ更新用のセキュリティルールの実装は完了です。
次のテストとの競合を避けるために、商品データ更新用のテストを終了してください。
Firestore 上の商品データ取得ルールの実装
テストの作成
最後に、商品データ取得用の Firestore セキュリティルールをテスト駆動で実装していきます。
テストを作成する前に、データ取得の要件を再確認しましょう。
- 認証されていないユーザはデータ取得不可
- 売り切れ商品のデータ取得不可
上記の要件に沿って、テストを作成します。
tests/data.read.test.ts ファイルを追加し、以下のようにコードを記述してください。
テストのコード
1process.env.FIRESTORE_EMULATOR_HOST = "localhost:58080";
2
3import {FirestoreTestSupporter} from "firestore-test-supporter";
4import * as firebase from "@firebase/testing";
5import path from "path";
6
7import TestData from "./TestData";
8
9describe("データ取得テスト", () => {
10 const supporter = new FirestoreTestSupporter("my-test-project", path.join(__dirname, "firestore.rules"));
11
12 const collectionPath = TestData.getCollectionPath();
13 const item_id = TestData.getItemId();
14 const initialData = TestData.getInitialData();
15 const validUpdateData = TestData.getValidUpdateData();
16 const admin_user = TestData.getAdminUser();
17
18 beforeEach(async () => {
19 await supporter.loadRules();
20
21 // 初期データを追加
22 const db = supporter.getFirestoreWithAuth(admin_user);
23 const doc = db.collection(collectionPath).doc(item_id);
24 await doc.set(initialData)
25 });
26
27 afterEach(async () => {
28 await supporter.cleanup();
29 });
30
31 test('要件にあったデータの取得に成功', async () => {
32 // 任意のユーザで認証
33 const db = supporter.getFirestoreWithAuth("any_user");
34 const doc = db.collection(collectionPath).doc(item_id);
35 await firebase.assertSucceeds(doc.get())
36 });
37 test('認証されていないユーザはデータ取得不可', async () => {
38 const db = supporter.getFirestore();
39 const doc = db.collection(collectionPath).doc(item_id);
40 await firebase.assertFails(doc.get())
41 });
42 test('売り切れ商品のデータ取得不可', async () => {
43 const db = supporter.getFirestoreWithAuth(admin_user);
44 const doc = db.collection(collectionPath).doc(item_id);
45
46 // データを売り切れに設定
47 const sold_out_data = {...validUpdateData, sold_out: true};
48 await firebase.assertSucceeds(doc.set(sold_out_data));
49
50 await firebase.assertFails(doc.get())
51 })
52});
テストの概要
「要件にあったデータの取得に成功」テスト
任意の認証されたユーザがデータを取得できるので、任意ユーザ any_user で認証されたクライアントを使用しています。
「売り切れ商品のデータ取得不可」テスト
以下のコードで、売り切れ商品のデータを用意しています。
1const sold_out_data = {...validUpdateData, sold_out: true};
2await firebase.assertSucceeds(doc.set(sold_out_data));
Firestore セキュリティルールの実装
テストに沿って、Firestore セキュリティルールの実装を進めていきます。
テストをスタート
以下のコマンドを実行し、テストをスタートしてください。
1npm run test-watch tests/data.get.test.ts
全てのアクセスを許可するセキュリティルールを設定
商品データの追加・更新テストの場合と同様、この時点では、「要件にあったデータの取得に成功」テストは「失敗」します。
まずは、これまでと同様、全ての read アクセスを許可してみます。
以下のようにルールを調整してください。
1rules_version = '2';
2service cloud.firestore {
3 match /databases/{database}/documents {
4 match /items/{item_id}{
5 allow create: if request.auth.uid == request.resource.data.admin_user;
6
7 allow update: if request.auth.uid == request.resource.data.admin_user
8 && request.resource.data.admin_user == resource.data.admin_user
9 && request.resource.data.title == resource.data.title
10 && resource.data.locked == false;
11
12 allow read: if true;
13 }
14 }
15}
テスト結果
テスト結果は、以下の通りとなります。
1FAIL tests/data.read.test.ts
2 データ取得テスト
3 √ 要件にあったデータの取得に成功 (2178ms)
4 × 認証されていないユーザはデータ取得不可 (95ms)
5 × 売り切れ商品のデータ取得不可 (83ms)
「要件にあったデータの取得に成功」したので、残りの取得不可ルールを追加していきます。
商品データ取得の要件をセキュリティルールに設定
以下のように Firestore セキュリティルールを調整してください。
1rules_version = '2';
2service cloud.firestore {
3 match /databases/{database}/documents {
4 match /items/{item_id}{
5 allow create: if request.auth.uid == request.resource.data.admin_user;
6
7 allow update: if request.auth.uid == request.resource.data.admin_user
8 && request.resource.data.admin_user == resource.data.admin_user
9 && request.resource.data.title == resource.data.title
10 && resource.data.locked == false;
11
12 allow read: if request.auth.uid != null
13 && resource.data.sold_out == false;
14 }
15 }
16}
ここで、request.auth.uid != null により、認証されているユーザだけがデータ取得可能としています。
また、resource.data.sold_out == false の条件により、売り切れていない商品のみ取得可能としています。
テスト結果
テスト結果は、以下の通りとなります。
1PASS tests/data.read.test.ts
2 データ取得テスト
3 √ 要件にあったデータの取得に成功 (2145ms)
4 √ 認証されていないユーザはデータ取得不可 (160ms)
5 √ 売り切れ商品のデータ取得不可 (86ms)
テストが全て成功したので、以上で商品データ取得用のセキュリティルールの実装は完了です。
テストを終了してください。
全ての要件の再チェック
最後に、各項目でのルールの変更により、前に設定したルールが要件から外れていないかを確認します。
全てのテストを実行
以下のコマンドを実行し、全てのテストを実行してください。
1npm run test-watch
テスト結果
テスト結果は、このような感じになりました!
1 PASS tests/data.add.test.ts
2 PASS tests/data.read.test.ts
3 PASS tests/data.update.test.ts
4
5Test Suites: 3 passed, 3 total
6Tests: 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 で認証なしユーザをブロック
こちらの記事もオススメ!
2020.07.17ライトコード的「やってみた!」シリーズ「やってみた!」を集めました!(株)ライトコードが今まで作ってきた「やってみた!」記事を集めてみました!※作成日が新し...
2020.08.04エンジニアの働き方 特集社員としての働き方社員としてのエンジニアの働き方とは?ライトコードのエンジニアはどんな働き方をしてるのか、まとめたいと...
2020.07.27IT・コンピューターの歴史特集IT・コンピューターの歴史をまとめていきたいと思います!弊社ブログにある記事のみで構成しているため、まだ「未完成状態」...
データ検証編はこちら
「データ検証編」では、リクエストデータ、データベース上のデータの参照などについて解説
データ比較編はこちら
「データ比較編」では、データの比較や型チェックなどについて解説
カスタム関数編はこちら
「カスタム関数編」では、「比較編」で作成したルールをもとに、カスタム関数の使い方を解説
ライトコードでは、エンジニアを積極採用中!
ライトコードでは、エンジニアを積極採用しています!社長と一杯しながらお話しする機会もご用意しております。そのほかカジュアル面談等もございますので、くわしくは採用情報をご確認ください。
採用情報へ
「好きを仕事にするエンジニア集団」の(株)ライトコードです! ライトコードは、福岡、東京、大阪、名古屋の4拠点で事業展開するIT企業です。 現在は、国内を代表する大手IT企業を取引先にもち、ITシステムの受託事業が中心。 いずれも直取引で、月間PV数1億を超えるWebサービスのシステム開発・運営、インフラの構築・運用に携わっています。 システム開発依頼・お見積もり大歓迎! また、現在「WEBエンジニア」「モバイルエンジニア」「営業」「WEBデザイナー」を積極採用中です! インターンや新卒採用も行っております。 以下よりご応募をお待ちしております! https://rightcode.co.jp/recruit
おすすめ記事
immichを知ってほしい
2024.10.31