Cloud Firestore のセキュリティルールを Jest でテスト

はじめに

プライベートサブネットに DB を配置してネットワーク的にアクセス制御を行うインフラ構成で育ってきた身としては、セキュリティルールが用意されているとは言え、Cloud Firestore のクライアントから直接アクセスできる自由度に不安を感じてしまうわけです。

そんな不安感を解消するためにセキュリティルールを Jest でテストしてみたので、内容をまとめておきます。

今回対応する内容

  • ユーザー情報を格納する users コレクションを用意。
  • 認証済みユーザーのみ、自ユーザー ID を ID とするドキュメントにアクセス可能となるように権限設定

前提

Firebase CLI によって Cloud Firestore は初期化済み、かつ、Firebase Local Emulator Suite もセットアップ済みとします。

Firebase Local Emulator Suite はドキュメント記載の通りにセットアップできると思いますが、Java もインストールする必要があるので、そこはお忘れなく。手元の MacBook Pro では HomebrewOpenJDK をインストールし、環境変数 $JAVA_HOME を設定して対応しました。

手順

1. 専用ディレクトリを用意

デフォルトでは Cloud Firestore 専用ディレクトリは存在しませんが、関連範囲を局所化するために専用のディレクトリを作成して、そこで作業することにします。

mkdir firestore
mv firestore.indexes.json firestore/
mv firestore.rules firestore/

関連ファイルを移動したので、firebase.json も併せて修正します。

 {
   "firestore": {
-    "rules": "firestore.rules",
-    "indexes": "firestore.indexes.json"
+    "rules": "firestore/firestore.rules",
+    "indexes": "firestore/firestore.indexes.json"
   },
   "hosting": {
     "public": "public",

以下のような最低限の package.json も配置します。

{
  "name": "firestore",
  "scripts": {
    "serve": "firebase emulators:start --only firestore",
    "deploy": "firebase deploy --only firestore:rules",
    "test": "jest"
  },
  "private": true
}

2. ts-jest をセットアップ

TypeScript でサクッと Jest を使いたいので、ts-jest を使います。

yarn add --dev jest typescript ts-jest
yarn ts-jest config:init

上記2番目のコマンドにより、 下記 jest.config.js が生成されます。

module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'node',
};

3. テスト記述

まずはテストに必要なライブラリをインストールします。

yarn add @firebase/rules-unit-testing --dev

あとは下記のようなテストを記述します。initializeTestApp() メソッド引数で認証状態をシミュレートできるのは素晴らしいですね。auth: undefined は未認証状態です。

import * as fs from 'fs';
import * as firebaseTest from '@firebase/rules-unit-testing';

describe('Firestore セキュリティルール', () => {
  const projectId = 'my-test-project';
  const uid1 = 'test-uid1';
  const uid2 = 'test-uid2';

  beforeAll(
    async () => {
      await firebaseTest.loadFirestoreRules({
        projectId,
        rules: fs.readFileSync('./firestore.rules', 'utf8'),
      });
    },
  );

  test('users 認証アクセス', async () => {
    const user1TestApp = firebaseTest.initializeTestApp({
      projectId,
      auth: { uid: uid1 },
    });
    // 自ユーザーアクセス
    const user1Reference = user1TestApp.firestore().collection('users').doc(uid1);
    await firebaseTest.assertSucceeds(user1Reference.set({}));
    await firebaseTest.assertSucceeds(user1Reference.get());
    // 他ユーザーアクセス
    const user2Reference = user1TestApp.firestore().collection('users').doc(uid2);
    await firebaseTest.assertFails(user2Reference.set({}));
    await firebaseTest.assertFails(user2Reference.get());
  });

  test('users 未認証アクセス', async () => {
    const noAuthTestApp = firebaseTest.initializeTestApp({
      projectId,
      auth: undefined,
    });
    const noAuthUser1Reference = noAuthTestApp.firestore().collection('users').doc(uid1);
    await firebaseTest.assertFails(noAuthUser1Reference.set({}));
    await firebaseTest.assertFails(noAuthUser1Reference.get());
  });

  afterAll(async () => {
    await Promise.all(firebaseTest.apps().map((app) => app.delete()));
  });
});

4. テスト実行

yarn serve でエミュレーターを立ち上げた上で、 yarn test でテスト実行になります。

5. ルールを修正

テストをパスするように firestore.rules を下記の通りに修正します。

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /users/{userId} {
      allow read, write: if request.auth != null && request.auth.uid == userId;
    }
  }
}

おわりに

セキュリティルールもテストを書けば安心ですね。

ちなみに、今回はテスト実行時に Jest did not exit one second after the test run has completed. という警告が出てしまいました。色々調査したのですが、解決策が見つからず・・・。テスト自体は実行できているので、後日詳細を調査しようと思います。