• トップ
  • ブログ一覧
  • Firestoreエミュレータ+Jestでセキュリティルールをテストする!
  • Firestoreエミュレータ+Jestでセキュリティルールをテストする!

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

    IT技術

    Firestore(ファイアストア)とは?

    「Firestore」は、Google製のスケーラブルなドキュメントデータベースです。

    他の Firebase サービス群と同様に、サーバやスケーリングの管理が不要なため、開発者はフロントエンドの開発に集中することができます。

    また、従来のデータベースは、サーバプログラムを通して「データの取得・更新」を行うのが一般的です。

    それに対して「Firestore」では、サーバを介さず、フロントエンドから直接、データを取得・更新するかのように扱う事ができるのです。

    セキュリティルールが肝

    Firestore では、「セキュリティルール(データの読み書きの許可・拒否ルール)」を記述したファイルを設置する事で、データへのアクセス制限を設定することができます。

    非常に有能かつ便利な Firestore ですが、セキュリティルールの設定には細心の注意を払う必要があります。

    Firestore の利点

    クライアントコードに直接、データの「取得・更新」処理を書くことが出来るのが、Firestore の大きな利点の1つです。

    ですが、同時に Firestore への接続情報を、クライアントコードに記述する事が必要となります。

    例えば、ウェブクライアントから Firestore を利用する場合、記述された接続情報は、知識のある訪問者であれば容易に確認することができます。

    データを悪用・破壊される恐れ

    セキュリティルールの設定に不備がある場合、取得した接続情報を使用して、データの「取得」や「改ざん・削除」が可能となります。

    なので、訪問者が悪意のあるユーザであった場合、データを悪用されたり、破壊される恐れがあります。

    データの悪用や破壊を防ぐためには、セキュリティルールの設定に細心の注意を払う必要があります。

    つまり、データへのアクセスを適切に制限する事が重要となります。

    セキュリティルールを作成する

    今回は、肝となるセキュリティルールを、「Firestoreエミュレータ」とFacebook製のテストフレームワーク「Jest」を使って、テストする方法を解説していきたいと思います。

    まずは、「Jest」と「Firestoreエミュレータ」をセットアップします。

    その後、実際にサンプルテストとセキュリティルールを作成していきたいと思います。

    Jest(ジェスト)のセットアップ

    必要な環境

    Node.jsv8.0.0 以上

    Jest環境の構築

    まず最初に、「Jest」を使うために必要なパッケージをインストールします。

    デモ用のプロジェクトディレクトリを作成して、プロジェクトルートで以下のコマンドを実行してください。

    1npm init -y
    2npm i -D jest @types/jest

    項目の追加・調整

    生成された package.json に、以下の項目を、追加・調整してください。

    1{
    2  // ...
    3  "scripts": {
    4    "test": "jest",
    5    "test-watch": "jest --watch"
    6  },
    7  // ...
    8  "jest": {
    9    "moduleFileExtensions": [
    10      "js",
    11      "rules"
    12    ]
    13  }
    14}

    scripts フィールドには、テストに使用するコマンドを追加しています。

    test コマンドは「Jest」によるテストの実施に使用。

    test-watch コマンドはファイル監視&自動再テストに使用します。

    jest フィールドでは、「Jest」の設定を行っています。

    moduleFileExtensions では、テスト対象となる拡張子を指定しています。

    また、rules 拡張子を追加しています。

    これは、test-watch コマンドを使用する際に、テスト対象であるセキュリティルールの設定ファイルを監視するためです。

    Jestの動作チェック

    プロジェクトルートに sample.test.js を作成し、以下のように記述してください。

    1describe("Jest動作チェック", () => {
    2    test("サンプルテスト", async () => {
    3        expect(3 + 7).toBe(10);
    4    });
    5});

    テストファイルを追加したら、実際にテストしてみましょう!

    テストの実行

    以下のコマンドを入力し、テストを実行してください。

    1npm run test sample.test.js

    環境や「Jest」のバージョンにもよりますが、おおよそ以下のように結果が表示されれば、テストは成功です!

    1 PASS  ./sample.test.js
    2  Jest動作チェック
    3サンプルテスト (2ms)
    4 
    5Test Suites: 1 passed, 1 total
    6Tests:       1 passed, 1 total
    7Snapshots:   0 total
    8Time:        1.318s, estimated 2s
    9Ran all test suites.

    Firestoreエミュレータのセットアップ

    Firebase CLIのインストール

    エミュレータをセットアップするには、Firebase CLI が必要となります。

    以下のコマンドを実行して Firebase CLI をインストールしてください。

    1npm install -g firebase-tools

    エミュレータをインストール

    以下のコマンドを実行して、エミュレータをインストールしてください。

    1firebase setup:emulators:firestore

    エミュレータの起動設定

    プロジェクトルートに firebase.json を追加して、以下のように記述してください。

    1{
    2    "firestore": {
    3        "rules": "firestore.rules"
    4    },
    5    "emulators": {
    6        "firestore": {
    7            "port": "58080"
    8        }
    9    }
    10}

    firestore.rules プロパティは、エミュレータ起動時に読み込む「セキュリティルールファイル」を指定しています。

    emulators.firestore.port プロパティは、起動するエミュレータのポートを指定しています。

    エミュレータは、デフォルトでは 8080 ポートで起動します。

    ですが、8080 ポートは、他のサービスのポートと衝突する事が多いので、今回はポートを 58080 に変更しています。

    エミュレータの起動

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

    1firebase emulators:start --only firestore

    以上で、Firestoreエミュレータのセットアップは完了です。

    次は、セットアップした「Firestoreエミュレータ」と「Jest」を使って、実際にセキュリティルールのテストをしていきます。

    Firestoreセキュリティルールをテスト

    ついに、セキュリティルールをテストする環境が整いました。

    では、いよいよ、セキュリティルールをテストしていきます!

    今回は、折角なので、テスト可能な環境を活かします。

    テスト駆動開発ライクなスタイルで、

    1. 1、テスト作成
    2. 2、エラー確認
    3. 3、コードの調整
    4. 4、テストパス

    という流れで解説を進めていきたいと思います。

    目標

    以下のアクセス環境を目標に、テストとセキュリティルールの実装を進めて行きます。

    1. 認証されているユーザは、 fruits コレクションのドキュメントを読み書きできる。
    2. 認証されていないユーザは、 fruits コレクションのドキュメントを読み書きでない。
    3. fruites 以外のコレクションは、認証の有無にかかわらず、全てのユーザが読み書きできない。

    必要なパッケージのインストール

    プロジェクトルートで、以下のコマンドを実行して、テストに必要なパッケージをインストールしてください。

    1npm i -D @firebase/testing

    テストコードの作成

    まずは、目標に沿ってテストを作成します。

    プロジェクトルートに、テストファイル firestore.rules.test.js を追加して、以下のように記述してください。

    1// エミュレータホストの指定。デフォルトポートでエミュレータを起動した場合は不要
    2process.env.FIRESTORE_EMULATOR_HOST = "localhost:58080";
    3 
    4const fs = require("fs");
    5const firebase = require("@firebase/testing");
    6 
    7// 認証なしFirestoreクライアントの取得
    8function getFirestore() {
    9    const app = firebase.initializeTestApp({
    10        projectId: "my-test-project"
    11    });
    12 
    13    return app.firestore();
    14}
    15 
    16// 認証付きFirestoreクライアントの取得
    17function getFirestoreWithAuth() {
    18    const app = firebase.initializeTestApp({
    19        projectId: "my-test-project",
    20        auth: {uid: "test_user", email: "test_user@example.com"}
    21    });
    22 
    23    return app.firestore();
    24}
    25 
    26describe("fruitsコレクションへの認証付きでのアクセスのみを許可", () => {
    27    beforeEach(async () => {
    28        // セキュリティルールの読み込み
    29        await firebase.loadFirestoreRules({
    30            projectId: "my-test-project",
    31            rules: fs.readFileSync("firestore.rules", "utf8")
    32        });
    33    });
    34 
    35    afterEach(async () => {
    36        // 使用したアプリの削除
    37        await Promise.all(firebase.apps().map(app => app.delete()))
    38    });
    39 
    40    describe('fruitsコレクションへの認証付きアクセスを許可', () => {
    41        test('認証なしでのデータ保存に失敗', async () => {
    42            const db = getFirestore();
    43            const doc = db.collection('fruits').doc('apple');
    44            await firebase.assertFails(doc.set({color: 'red'}))
    45        });
    46 
    47        test('認証ありでのデータ保存に成功', async () => {
    48            const db = getFirestoreWithAuth();
    49            const doc = db.collection('fruits').doc('orange');
    50            await firebase.assertSucceeds(doc.set({color: 'orange'}))
    51        });
    52 
    53        test('認証なしでの取得に失敗', async () => {
    54            const db = getFirestore();
    55            const doc = db.collection('fruits').doc('strawberry');
    56            await firebase.assertFails(doc.get())
    57        });
    58 
    59        test('認証ありでの取得に成功', async () => {
    60            const db = getFirestoreWithAuth();
    61            const doc = db.collection('fruits').doc('cherry');
    62            await firebase.assertSucceeds(doc.get())
    63        });
    64    });
    65 
    66    describe('fruits以外のコレクションへのアクセス禁止', () => {
    67        test('認証なしでのデータ保存に失敗', async () => {
    68            const db = getFirestore();
    69            const doc = db.collection('countries').doc('japan');
    70            await firebase.assertFails(doc.set({language: 'japanese'}))
    71        });
    72 
    73        test('認証ありでのデータ保存に失敗', async () => {
    74            const db = getFirestoreWithAuth();
    75            const doc = db.collection('vegetables').doc('tomato');
    76            await firebase.assertFails(doc.set({color: 'red'}))
    77        });
    78 
    79        test('認証なしでの取得に失敗', async () => {
    80            const db = getFirestore();
    81            const doc = db.collection('vehicles').doc('car');
    82            await firebase.assertFails(doc.get())
    83        });
    84 
    85        test('認証ありでの取得に失敗', async () => {
    86            const db = getFirestoreWithAuth();
    87            const doc = db.collection('prefectures').doc('tokyo');
    88            await firebase.assertFails(doc.get())
    89        });
    90    });
    91});

    テストコードで使用した変数やメソッドの解説

    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 を指定しない場合は認証なしのアプリが返されます。

    テストコードでは、テスト対象のセキュリティルールをロードした projectIdloadFirestoreRulesauth で指定した projectId )を指定して、Firebaseアプリを取得しています。

    async/await

    セキュリティルールのロードや、データの保存・取得は非同期処理であるため、await表記により、非同期処理の完了を待機しています。

    関連記事

    featureImg2019.05.23【JavaScript】 async/await で非同期処理をわかりやすく記述するasync/await という仕組み本記事では、 JavaScript の非同期処理を扱うための async/awai...

    テストの実行

    テスト駆動のスタイルにならって、とりあえずテストを実行し、エラーさせてみます。

    以下のコマンドを実行して、作成したテストを実行してください。

    1npm run test-watch firestore.rules.test.js

    ENOENT: no such file or directory, open '...firerules.rules' のようなエラーメッセージが表示されます。

    firestore.rules ファイルがないという事なので、エラーメッセージに従って、firestore.rules ファイルを追加します。

    アクセス拒否の設定

    まずは安全運転で、全てのアクセスを拒否する設定を追加します。

    firestore.rules ファイルを追加して、以下のように記述してください。

    1rules_version = '2';
    2service cloud.firestore {
    3  match /databases/{database}/documents {
    4    // 全てのアクセスをブロック
    5    match /{document=**} {
    6      allow read, write: if false;
    7    }
    8  }
    9}

    firestore.rules を追加すると、自動でテストが再実施され、以下のようなテスト結果が表示されます。

    1 FAIL  ./firestore.rules.test.js
    2  fruitsコレクションへの認証付きでのアクセスのみを許可
    3    fruitsコレクションへの認証付きアクセスを許可
    4認証なしでのデータ保存に失敗 (1079ms)
    5      × 認証ありでのデータ保存に成功 (248ms)
    6認証なしでの取得に失敗 (44ms)
    7      × 認証ありでの取得に成功 (85ms)
    8    fruits以外のコレクションへのアクセス禁止
    9認証なしでのデータ保存に失敗 (39ms)
    10認証ありでのデータ保存に失敗 (35ms)
    11認証なしでの取得に失敗 (31ms)
    12認証ありでの取得に失敗 (34ms)
    13  // ... エラー詳細部分は省略

    全てのアクセスをブロックしているため、fruits コレクションへの認証付きでのアクセスも拒否されています。

    アクセスを許可する設定

    fruits コレクションへの認証付きアクセスを許可する設定を追加します。

    以下のように、firestore.rules を調整してください。

    1rules_version = '2';
    2service cloud.firestore {
    3  match /databases/{database}/documents {
    4    // 全てのアクセスをブロック
    5    match /{document=**} {
    6      allow read, write: if false;
    7  }
    8 
    9    // fruitsコレクションについては、認証されたユーザのみ読み書きを許可
    10    match /fruits/{fruit} {
    11      allow read, write: if request.auth.uid != null;
    12    }
    13  }
    14}

    テスト結果

    再び、テストが自動で実施され、以下のようなテスト結果が表示されます。

    1 PASS  ./firestore.rules.test.js
    2  fruitsコレクションへの認証付きでのアクセスのみを許可
    3    fruitsコレクションへの認証付きアクセスを許可
    4認証なしでのデータ保存に失敗 (1087ms)
    5認証ありでのデータ保存に成功 (52ms)
    6認証なしでの取得に失敗 (55ms)
    7認証ありでの取得に成功 (43ms)
    8    fruits以外のコレクションへのアクセス禁止
    9認証なしでのデータ保存に失敗 (37ms)
    10認証ありでのデータ保存に失敗 (36ms)
    11認証なしでの取得に失敗 (31ms)
    12認証ありでの取得に失敗 (30ms)
    13  // ... エラー詳細部分は省略

    全てのテストが成功しました。

    これで「 fruits コレクションに認証付きのユーザだけが読み書きできるセキュリティルール」を作成することができました。

    まとめ

    以上、Firestoreエミュレータにも「Jest」を使って、セキュリティルールをテストしてみました。

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

    1. 「Firestore」はとても便利
    2. 便利さゆえのリスクもあり
    3. セキュリティルールが肝
    4. Firestoreエミュレータを使えば、セキュリティルールをローカルでテスト可能

    なお、作成したテストは、CI(継続的インティグレーション) に組み込む事で、継続的にアクセス制限の漏れをチェックすることができます。

    ぜひ、セキュリティルールテストの、CI への組み込みも検討してみてください。

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

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

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

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

    広告メディア事業部

    広告メディア事業部

    おすすめ記事