• トップ
  • ブログ一覧
  • テスト駆動で学ぶ Firestore セキュリティルール 【データ検証編 / 後編】
  • テスト駆動で学ぶ Firestore セキュリティルール 【データ検証編 / 後編】

    広告メディア事業部広告メディア事業部
    2020.04.28

    IT技術

    テスト駆動で商品データのルールを実装する【後編】

    この記事では、前編・後編の二回に分けて、Firestore 上の商品データコレクションのセキュリティルール実装を例に、ルールによるデータ検証について解説しています。

    前編では、「実装目標の設定」から「データ追加ルールの実装」までを解説しました。

    後編では、「データ更新・取得ルールのテスト駆動による実装」を進めつつ、「データベースにすでにあるデータの利用」などについて解説していきます。

    簡単な流れ

    以下のような流れで解説を進めていきます。

    1. Firestore 上のデータ更新ルールを実装・解説
    2. Firestore 上のデータ取得ルールを実装・解説
    3. 全体テストで要件の最終チェック
    4. まとめ

    前編の記事はこちら

    featureImg2020.04.22テスト駆動で学ぶ Firestore セキュリティルール 【データ検証編 / 前編】テスト駆動で商品データのルールを実装するこの記事では、セキュリティルールによるデータ検証について、商品データのルールを...

    Firestore 上の商品データ更新ルールの実装

    テストの作成

    前編で説明した、データ追加ルールの実装と同様、テスト駆動で「商品データ更新ルール」の実装を進めていきます。

    テストを作成する前に、商品データ更新の要件を再確認しましょう。

    1. データ更新をリクエストしたユーザが商品管理者と一致しない場合は追加不可
    2. 商品管理者の変更不可
    3. ロックされたデータの更新不可
    4. 商品名の変更不可

    上記の要件に沿って、テストを作成します。

    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 セキュリティルールをテスト駆動で実装していきます。

    テストを作成する前に、データ取得の要件を再確認しましょう。

    1. 認証されていないユーザはデータ取得不可
    2. 売り切れ商品のデータ取得不可

    上記の要件に沿って、テストを作成します。

    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 セキュリティルールでのデータの検証を、テスト駆動で解説してみました。

    この記事では、解説のわかりやすさのため、個別のテストファイルについてテストを実施してきました。

    ですが、実際の開発では、最初から全テストを実行することをおすすめします

    なぜなら、実装と並行して常にリグレッションテストが実行されるからです。

    既存のテストの失敗に早目に気づくことができ、原因の特定・修正も比較的簡単で済みます。

    最後に、今回の内容を簡単にまとめてみたいと思います。

    1. request.auth フィールドィールドでリクエストユーザの情報を参照できる
    2. request.resource.data フィールドでリクエストデータを参照できる
    3. resource.data で既存データを参照できる
    4. 更新不可ルールは request.resource.data.some_data == resource.data.some_data で実装
    5. request.auth.uid != null で認証なしユーザをブロック

    こちらの記事もオススメ!

    featureImg2020.07.17ライトコード的「やってみた!」シリーズ「やってみた!」を集めました!(株)ライトコードが今まで作ってきた「やってみた!」記事を集めてみました!※作成日が新し...

    featureImg2020.08.04エンジニアの働き方 特集社員としての働き方社員としてのエンジニアの働き方とは?ライトコードのエンジニアはどんな働き方をしてるのか、まとめたいと...

    featureImg2020.07.27IT・コンピューターの歴史特集IT・コンピューターの歴史をまとめていきたいと思います!弊社ブログにある記事のみで構成しているため、まだ「未完成状態」...

    データ検証編はこちら

    「データ検証編」では、リクエストデータ、データベース上のデータの参照などについて解説

    featureImg2020.04.22テスト駆動で学ぶ Firestore セキュリティルール 【データ検証編 / 前編】テスト駆動で商品データのルールを実装するこの記事では、セキュリティルールによるデータ検証について、商品データのルールを...

    データ比較編はこちら

    「データ比較編」では、データの比較や型チェックなどについて解説

    featureImg2020.05.07テスト駆動で学ぶ Firestore セキュリティルール 【データ比較編 / 前編】テスト駆動で書籍コレクションのルールを実装するこの記事では、前編・後編の二回に分けて、Firestore セキュリティ...

    カスタム関数編はこちら

    「カスタム関数編」では、「比較編」で作成したルールをもとに、カスタム関数の使い方を解説

    featureImg2020.06.04テスト駆動で学ぶ Firestore セキュリティルール【カスタム関数編:第1回】テスト駆動で成績データコレクションのルールを実装する【第1回】この記事では、全4回に分けて、Firestore セキュ...

    ライトコードでは、エンジニアを積極採用中!

    ライトコードでは、エンジニアを積極採用しています!社長と一杯しながらお話しする機会もご用意しております。そのほかカジュアル面談等もございますので、くわしくは採用情報をご確認ください。

    採用情報へ

    広告メディア事業部

    広告メディア事業部

    おすすめ記事

    エンジニア大募集中!

    ライトコードでは、エンジニアを積極採用中です。

    特に、WEBエンジニアとモバイルエンジニアは是非ご応募お待ちしております!

    また、フリーランスエンジニア様も大募集中です。

    background