• トップ
  • ブログ一覧
  • 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・コンピューターの歴史をまとめていきたいと思います!弊社ブログにある記事のみで構成しているため、まだ「未完成状態」...

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

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

    採用情報へ

    メディアチーム
    メディアチーム
    Show more...

    おすすめ記事

    エンジニア大募集中!

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

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

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

    background