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

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

    IT技術

    テスト駆動で書籍コレクションのルールを実装する

    この記事では、前編・後編の二回に分けて、Firestore セキュリティルール中でのデータ比較について解説していきます。

    書店のネットショッピングサービスを想定し、Firestore 上の書籍コレクションのルール実装を例に、解説を進めていきます。

    前回のデータ検証編では、主にリクエストデータやデータベース上のデータの参照について解説しました。

    今回の記事では、より具体的に、データの数値比較やデータ型のチェックについて解説していきます。

    データ検証編の記事はこちら

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

    test-dirven-firestore-security-rules-data-verification-2nd2020.04.28テスト駆動で学ぶ Firestore セキュリティルール 【データ検証編 / 後編】テスト駆動で商品データのルールを実装する【後編】この記事では、前編・後編の二回に分けて、Firestore 上の商品デ...

    簡単な流れ

    「データ比較編」では、以下のような流れで解説を進めていきます。

    データ比較編:前編

    1. 書籍コレクションの要件を設定
    2. テスト環境の準備
    3. テストデータの作成
    4. Firestore 上のデータ追加ルールの実装と解説

    データ比較編:後編

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

    今回は、前編パートの、「実装目標(要件)の設定からデータ追加ルールの実装まで」を解説していきます。

    書籍コレクションの実装目標

    書籍コレクション books を想定し、以下の要件を目標に実装を進めていきます。

    書籍データ追加・更新の要件

    1. データ追加・更新をリクエストしたユーザが商品管理者でない場合は追加・更新不可
    2. データのサイズが「9」でない場合は追加・更新不可
    3. タイトルが string 型でない場合は追加・更新不可
    4. 書籍詳細が string 型でない場合は追加・更新不可
    5. 出版日が timestamp 型でない場合は追加・更新不可
    6. 価格が int 型で「0」以上でない場合は追加・更新不可
    7. 在庫が int 型で「0」以上でない場合は追加・更新不可
    8. 状態が new, used のいずれかでない場合は追加・更新不可

    商品管理者に対する要件

    ここで、商品管理者がデータ更新することを想定し、商品管理者に対する要件を以下のように補足します。

    ▼データ追加では、「リクエストデータの商品管理者にリクエストユーザが存在する」ものとします。

    ▼データ更新では、「データベース上のデータの商品管理者にリクエストユーザが存在する」ことを要件とします。

    データの取得要件

    1. 下書きデータの取得不可
    2. 在庫が「5」以下の書籍は取得不可

    在庫が「5」以下の場合は、店頭販売のみを行うと仮定し、取得不可としています。

    前準備 ~ テスト環境 ~

    初めに、テスト環境を準備します。

    テスト環境のセットアップ

    以下のコマンドを実行して、テスト環境をセットアップしてください。

    1git clone "https://github.com/rightcode/firestore-security-rules-test_data-compare.git" sandbox
    2cd sandbox
    3git checkout refs/tags/test-environment

    なお、今回の記事のコードは、以下のリポジトリにまとめてあります。

    【GitHub】
    rightcode/firestore-security-rules-test_data-compare

    npm パッケージのインストール

    以下のコマンドを実行して、必要となる npm パッケージをインストールしてください。

    1npm install

    Firebase CLI ツールのインストール

    まだ、Firebase CLI ツールをインストールしていない場合は、以下のコマンドを実行してインストールします。

    1npm install -g firebase-tools

    Firestore エミュレータを起動

    そして、コマンドを実行して、Firestore エミュレータを起動してください。

    1firebase setup:emulators:firestore
    2firebase emulators:start --only firestore

    以上で、テスト環境の準備は完了です!

    テスト環境のセットアップの解説

    なお、テスト環境のセットアップについて、より詳細な内容は、以下の記事をお読みください!

    featureImg2020.02.12Firestoreエミュレータ+Jestでセキュリティルールをテストする!Firestore(ファイアストア)とは?「Firestore」は、Google製のスケーラブルなドキュメントデータベ...

    コードをチェックアウトする

    今回の記事で解説するコードを確認する場合は、以下のコマンドより「master ブランチ」をチェックアウトしてください。

    1git checkout master

    「master ブランチ」に実装済みのコードを設置してあります。

    ※注意※
    コードの調整などにより、この記事の内容とは、記述が一部異なる場合があります。

    前準備 ~ テストデータ ~

    テストを作成する前に、まず、テストデータ用のコードを用意します。

    tests/data.ts ファイルを作成して、以下のように記述してください。

    テストデータのコード

    1// コレクションパス
    2export const collectionPath = "books";
    3
    4// 商品管理者
    5export const adminUser = "tanaka";
    6
    7// 商品ID
    8export const itemId = "XXXXXXXXXX";
    9
    10// 発売日
    11const releaseDate = new Date('2000-01-01 00:00:00');
    12
    13// 初期データ
    14export const initialData = {
    15    id: itemId,
    16    title: "銀河鉄道の朝",
    17    description: "人間か機械か、勝つのはどっちだ!",
    18    releaseDate: releaseDate,
    19    price: 2500,
    20    stock: 6,
    21    condition: "new",
    22    adminUsers: [adminUser, "yamada"],
    23    draft: false
    24};
    25
    26// 更新用データ
    27export const validUpdateData = {
    28    id: itemId,
    29    title: "北風と太平洋",
    30    description: "北風と太平洋、勝つのはどっちだ!",
    31    releaseDate: releaseDate,
    32    price: 30000,
    33    stock: 6,
    34    condition: "used",
    35    adminUsers: [adminUser, "yamada"],
    36    draft: false
    37};

    テストデータの概要

    adminUser

    テストで対象となる、書籍データの商品管理者の1人として定義。

    データの追加・更新が可能なユーザとして使用します。

    initialData

    初期データ。

    データ追加テストでのデータ追加と、更新・取得テスト前の初期データの準備に使用します。

    validUpdateData

    更新テスト用データ。

    要件に沿ったデータで、データの更新に使用します。

    Firestore 上のデータ追加ルールの実装

    データ追加の要件

    データ追加ルールのテストを作成する前に、まずは、データ追加の要件を再確認します。

    1. データ追加をリクエストしたユーザが商品管理者でない場合は追加不可
    2. データのサイズが「9」でない場合は追加不可
    3. タイトルが string 型でない場合は追加不可
    4. 書籍詳細が string 型でない場合は追加不可
    5. 出版日が timestamp 型でない場合は追加不可
    6. 価格が int 型で「0」以上でない場合は追加不可
    7. 在庫が int 型で「0」以上でない場合は追加不可
    8. 状態が new, used のいずれかでない場合は追加不可

    ここで、一番上の商品管理者に関する要件については、「リクエストデータの商品管理者にリクエストユーザが存在する」ことが要件となります。

    テストの作成

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

    tests/data.add.test.ts ファイルを追加して、以下のようにコードを記述してください。

    テストのコード

    1// Firestoreエミュレータのホストとポートを指定
    2process.env.FIRESTORE_EMULATOR_HOST = "localhost:58080";
    3
    4import {FirestoreTestSupporter} from "firestore-test-supporter";
    5
    6import * as path from "path";
    7import * as firebase from "@firebase/testing";
    8
    9// テストデータの読み込み
    10import {collectionPath, adminUser, itemId, initialData} from "./data";
    11
    12describe("書籍データの追加テスト", () => {
    13    const supporter = new FirestoreTestSupporter("my-test-project", path.join(__dirname, "firestore.rules"));
    14
    15    beforeEach(async () => {
    16        // セキュリティルールの読み込み
    17        await supporter.loadRules();
    18    });
    19
    20    afterEach(async () => {
    21        // データのクリーンアップ
    22        await supporter.cleanup()
    23    });
    24
    25    test('要件にあったデータの追加に成功', async () => {
    26        const db = supporter.getFirestoreWithAuth(adminUser);
    27        const doc = db.collection(collectionPath).doc(itemId);
    28        await firebase.assertSucceeds(doc.set(initialData))
    29    });
    30
    31    test('データ追加をリクエストしたユーザが商品管理者でない場合は追加不可', async () => {
    32        // 非商品管理者で認証されたクライアントを取得
    33        const db = supporter.getFirestoreWithAuth("non_admin_user");
    34        
    35        const doc = db.collection(collectionPath).doc(itemId);
    36        await firebase.assertFails(doc.set(initialData))
    37    });
    38
    39    test('データのサイズが9でない場合は追加不可', async () => {
    40        const db = supporter.getFirestoreWithAuth(adminUser);
    41        const doc = db.collection(collectionPath).doc(itemId);
    42
    43        // データサイズが追加対象外であるデータを作成
    44        const badData = {...initialData, author: "Hosoda"};
    45
    46        await firebase.assertFails(doc.set(badData))
    47    });
    48
    49    test('タイトルがstring型でない場合は追加不可', async () => {
    50        const db = supporter.getFirestoreWithAuth(adminUser);
    51        const doc = db.collection(collectionPath).doc(itemId);
    52
    53        // タイトルの型が追加対象外であるデータを作成
    54        const badData = {...initialData, title: 5};
    55
    56        await firebase.assertFails(doc.set(badData))
    57    });
    58
    59    test('書籍詳細がstring型でない場合は追加不可', async () => {
    60        const db = supporter.getFirestoreWithAuth(adminUser);
    61        const doc = db.collection(collectionPath).doc(itemId);
    62
    63        // 書籍詳細の型が追加対象外であるデータを作成
    64        const badData = {...initialData, description: true};
    65
    66        await firebase.assertFails(doc.set(badData))
    67    });
    68
    69    test('出版日がtimestamp型でない場合は追加不可', async () => {
    70        const db = supporter.getFirestoreWithAuth(adminUser);
    71        const doc = db.collection(collectionPath).doc(itemId);
    72
    73        // 出版日の型が追加対象外であるデータを作成
    74        const badData = {...initialData, releaseDate: "yesterday"};
    75
    76        await firebase.assertFails(doc.set(badData))
    77    });
    78
    79    test('価格がint型で0以上でない場合は追加不可', async () => {
    80        const db = supporter.getFirestoreWithAuth(adminUser);
    81        const doc = db.collection(collectionPath).doc(itemId);
    82
    83        // 価格の型が追加対象外であるデータを作成
    84        const badData1 = {...initialData, price: "二千"};
    85
    86        await firebase.assertFails(doc.set(badData1));
    87
    88        // 価格の値が追加対象外であるデータを作成
    89        const badData2 = {...initialData, price: -1};
    90
    91        await firebase.assertFails(doc.set(badData2))
    92    });
    93
    94    test('在庫がint型で0以上でない場合は追加不可', async () => {
    95        const db = supporter.getFirestoreWithAuth(adminUser);
    96        const doc = db.collection(collectionPath).doc(itemId);
    97
    98        // 在庫数の型が追加対象外であるデータを作成
    99        const badData1 = {...initialData, stock: "4"};
    100
    101        await firebase.assertFails(doc.set(badData1));
    102
    103        // 在庫数の値が追加対象外であるデータを作成
    104        const badData2 = {...initialData, stock: -1};
    105
    106        await firebase.assertFails(doc.set(badData2))
    107    });
    108
    109    test('状態がnew, usedのいずれかでない場合は追加不可', async () => {
    110        const db = supporter.getFirestoreWithAuth(adminUser);
    111        const doc = db.collection(collectionPath).doc(itemId);
    112
    113        // 状態の値が追加対象外であるデータを作成
    114        const badData = {...initialData, condition: "old"};
    115
    116        await firebase.assertFails(doc.set(badData))
    117    });
    118});

    テストの概要

    「データ追加をリクエストしたユーザが商品管理者でない場合は追加不可」となるテスト

    商品管理者に含まれないユーザとして、non_admin_user で認証したクライアントを取得しています。

    「データのサイズが「9」でない場合は追加不可」となるテスト

    リクエストデータのサイズを変更するため、初期データに author フィールドを追加しています。

    型のチェックテスト

    各テストのデータ追加リクエストの前に、初期データを調整して、不適切な型の値を設定しています。

    ルールの実装

    データ追加ルール用のテストが完成したので、このテストに沿ってセキュリティルールを実装していきます。

    テスト駆動のスタイルに従い、まずはテストを実行してみて、テストが失敗することを確認します

    全ての create アクセスを許可するセキュリティルールを設定

    今回は、少しだけルールの実装を進めて、create アクセスを全て許可するところからスタートします。

    books コレクションの create アクセスを全て許可し、「要件にあったデータの追加に成功」するテストだけを通します。

    その上で、失敗した追加不可テストを上から順に通しながらルールを実装していきます

    tests/firestore.rules を追加して、以下のように記述してください。

    1rules_version = '2';
    2service cloud.firestore {
    3  match /databases/{database}/documents {
    4    match /books/{id}{
    5      allow create: if true;
    6    }
    7  }
    8}

    テストをスタート

    ファイルを追加したら、以下のコマンドを実行して、テストをスタートしてください。

    1npm run test-watch tests/data.add.test.ts

    テスト結果

    テスト結果は、以下のようになります。(テスト結果の冒頭のみを抜粋)

    1 FAIL  tests/data.add.test.ts
    2  商品追加テスト
    3要件にあったデータの追加に成功 (2089ms)
    4    × データ追加をリクエストしたユーザが商品管理者でない場合は追加不可 (55ms)
    5    × データのサイズが9でない場合は追加不可 (39ms)
    6    × タイトルがstring型でない場合は追加不可 (39ms)
    7    × 書籍詳細がstring型でない場合は追加不可 (43ms)
    8    × 出版日がtimestamp型でない場合は追加不可 (38ms)
    9    × 価格がint型で0以上でない場合は追加不可 (33ms)
    10    × 在庫がint型で0以上でない場合は追加不可 (36ms)
    11    × 状態がnew, usedのいずれかでない場合は追加不可 (46ms)

    「要件にあったデータの追加に成功」するテストだけが通りました

    前述の通り、失敗した追加不可テストを上から順に通して行きます

    リスト型データ中の指定値の有無を条件に使用する

    「データ追加をリクエストしたユーザが、商品管理者でない場合は追加不可」となるよう、以下のルールを設定してください。

    1rules_version = '2';
    2service cloud.firestore {
    3  match /databases/{database}/documents {
    4    match /books/{id}{
    5      allow create: if request.auth.uid in request.resource.data.adminUsers;
    6    }
    7  }
    8}

    ここで、request.auth.uid in request.resource.data.adminUsers の条件により、リクエストデータの商品管理者リストの中に、リクエストユーザが含まれるかをチェックしています。

    リスト型のデータに、in 演算子を使用することで、そのリストに指定の値が存在するかをチェックできます。

    テスト結果

    ルールの変更により、テストが再度実行され、以下のような結果が表示されます!

    1 FAIL  tests/data.add.test.ts (5.147s)
    2  商品追加テスト
    3要件にあったデータの追加に成功 (2909ms)
    4データ追加をリクエストしたユーザが商品管理者でない場合は追加不可 (93ms)
    5    × データのサイズが9でない場合は追加不可 (63ms)
    6    × タイトルがstring型でない場合は追加不可 (61ms)
    7    × 書籍詳細がstring型でない場合は追加不可 (62ms)
    8    × 出版日がtimestamp型でない場合は追加不可 (45ms)
    9    × 価格がint型で0以上でない場合は追加不可 (43ms)
    10    × 在庫がint型で0以上でない場合は追加不可 (44ms)
    11    × 状態がnew, usedのいずれかでない場合は追加不可 (49ms)

    データの要素数の比較結果を条件に使用する

    次に、「データのサイズが「9」でない場合は追加不可」となるテストを通すため、以下のようにルールを追加してください。

    1rules_version = '2';
    2service cloud.firestore {
    3  match /databases/{database}/documents {
    4    match /books/{id}{
    5      allow create: if request.auth.uid in request.resource.data.adminUsers
    6                        && request.resource.data.size() == 9;
    7    }
    8  }
    9}

    request.resource.data.size() == 9 の条件により、リクエストデータの要素の数が「9」である場合のみ、追加アクセスを許可しています。

    マップ型やリスト型のデータには、size() メソッドが用意されており、データの要素数を参照することができます。

    テスト結果

    1  商品追加テスト
    2要件にあったデータの追加に成功 (2108ms)
    3データ追加をリクエストしたユーザが商品管理者でない場合は追加不可 (49ms)
    4データのサイズが9でない場合は追加不可 (44ms)
    5    × タイトルがstring型でない場合は追加不可 (56ms)
    6    × 書籍詳細がstring型でない場合は追加不可 (42ms)
    7    × 出版日がtimestamp型でない場合は追加不可 (43ms)
    8    × 価格がint型で0以上でない場合は追加不可 (40ms)
    9    × 在庫がint型で0以上でない場合は追加不可 (37ms)
    10    × 状態がnew, usedのいずれかでない場合は追加不可 (38ms)

    「データのサイズが9でない場合は追加不可」となるテストが通りました。

    データ型や数値の比較結果を条件に使用する

    最後に、「残っているテスト」と「型と値のチェックテスト」をまとめて通してしまいます。

    以下のように、ルールを調整しましょう!

    1rules_version = '2';
    2service cloud.firestore {
    3  match /databases/{database}/documents {
    4    match /books/{id}{
    5      allow create: if request.auth.uid in request.resource.data.adminUsers
    6                        && request.resource.data.size() == 9
    7                        && request.resource.data.title is string
    8                        && request.resource.data.description is string
    9                        && request.resource.data.releaseDate is timestamp
    10                        && request.resource.data.price is int
    11                        && request.resource.data.price >= 0
    12                        && request.resource.data.stock is int
    13                        && request.resource.data.stock >= 0
    14                        && request.resource.data.condition in ["new","used"];
    15    }
    16  }
    17}

    ここで、is 演算子により、各フィールドの型を指定しています。

    例えば、releaseDate フィールドは、request.resource.data.releaseDate is timestamp の条件により、タイムスタンプ型のみが許可されています。

    int 型などの数値型は、>=< などの演算子を使って、数値の比較ができます

    例えば、request.resource.data.price >= 0 の条件によって、price フィールドは、「0」以上でない場合はデータを追加できません。

    先ほど説明した in 演算子を使って、request.resource.data.condition in ["new","used"] の条件により、condition フィールドの値は "new" か "used" に制限しています。

    テスト結果

    1 PASS  tests/data.add.test.ts
    2  商品追加テスト
    3要件にあったデータの追加に成功 (2111ms)
    4データ追加をリクエストしたユーザが商品管理者でない場合は追加不可 (49ms)
    5データのサイズが9でない場合は追加不可 (39ms)
    6タイトルがstring型でない場合は追加不可 (50ms)
    7書籍詳細がstring型でない場合は追加不可 (41ms)
    8出版日がtimestamp型でない場合は追加不可 (38ms)
    9価格がint型で0以上でない場合は追加不可 (46ms)
    10在庫がint型で0以上でない場合は追加不可 (53ms)
    11    √ 状態がnew, usedのいずれかでない場合は追加不可 (41ms)

    全てのテストが通りました!

    以上で、書籍データ追加用のルールは、実装完了です!

    最後に、次のテストとの競合を避けるため、テストを終了してください。

    後編へつづく!

    今回の記事では、「実装目標(要件)の設定」から「データ追加ルールの実装」までを、テスト駆動のスタイルで解説してみました。

    後編では、前編の内容を復習しつつ、前編と同様に、テスト駆動スタイルで Firestore 上のデータ更新と取得のルールを実装していきます。

    後編はこちら

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

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

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

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

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

    広告メディア事業部

    広告メディア事業部

    おすすめ記事