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

アプリを構築する過程で Cloud Firestore データベースへのアクセスをロックダウンすることが必要な場合もありますが、リリースする前には、より詳細な Cloud Firestore Security Rules が必要になります。Cloud Firestore エミュレータを使用すると、アプリの一般的な機能と動作のプロトタイピングとテストに加えて、Cloud Firestore Security Rules の動作をチェックする単体テストを作成できます。

クイックスタート

簡単なルールを使った基本的なテストケースについては、クイックスタート サンプルをお試しください。

Cloud Firestore Security Rules について理解する

モバイル クライアント ライブラリやウェブ クライアント ライブラリを使用する場合は、サーバーレスの認証、認可、データ検証を行うために Firebase AuthenticationCloud Firestore Security Rules を実装します。

Cloud Firestore Security Rules には次の 2 つの部分があります。

  1. データベース内のドキュメントを識別する match ステートメント
  2. それらのドキュメントへのアクセスを制御する allow

Firebase Authentication はユーザーの認証情報を検証し、ユーザーベースとロールベースのアクセス システムの基盤を提供します。

Cloud Firestore のモバイル クライアント ライブラリまたはウェブ クライアント ライブラリから送られたすべてのデータベース リクエストは、データの読み取りまたは書き込みの前に、セキュリティ ルールと照合して評価されます。指定されたドキュメント パスのいずれかへのアクセスがルールによって拒否されると、リクエスト全体が失敗します。

Cloud Firestore Security Rules の詳細については、Cloud Firestore Security Rules を使ってみるをご覧ください。

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

Cloud Firestore エミュレータをインストールするには、Firebase CLI を使用して次のコマンドを実行します。

firebase setup:emulators:firestore

エミュレータを実行する

まず、作業ディレクトリで Firebase プロジェクトを初期化します。これは、Firebase CLI を使用するときの一般的な最初のステップです。

firebase init

次のコマンドを使用してエミュレータを起動します。エミュレータは、プロセスを終了するまで実行されます。

firebase emulators:start --only firestore

多くの場合、エミュレータを起動し、テストスイートを実行して、テストの実行後にエミュレータをシャットダウンします。これは emulators:exec コマンドで簡単に実施できます。

firebase emulators:exec --only firestore "./my-test-script.sh"

エミュレータが起動すると、デフォルト ポート(8080)での実行が試行されます。デフォルト ポート以外のエミュレータ ポートで実行するには、firebase.json ファイルの "emulators" セクションを変更します。

{
  // ...
  "emulators": {
    "firestore": {
      "port": "YOUR_PORT"
    }
  }
}

エミュレータを実行する前に

エミュレータを使用する前に、次の点に注意してください。

  • エミュレータは最初に firebase.json ファイルの firestore.rules フィールドで指定されたルールを読み込みます。Cloud Firestore Security Rules が含まれているローカル ファイルの名前をこのフラグに指定すると、そのセキュリティ ルールがすべてのプロジェクトに適用されます。以下で説明するとおり、ローカル ファイルパスを指定しない場合、または loadFirestoreRules メソッドを使用しない場合、エミュレータはすべてのプロジェクトをオープンルールが適用されるものとして扱います。
  • ほとんどの Firebase SDK はエミュレータで直接動作しますが、セキュリティ ルールで auth の擬似的再現をサポートしているのは @firebase/rules-unit-testing ライブラリのみです。したがって、このライブラリでは単体テストがはるかに簡単になります。また、このライブラリは以下にリストされているエミュレータ固有の機能(すべてのデータのクリアなど)もサポートしています。
  • エミュレータは、クライアント SDK から提供される本番環境の Firebase Auth トークンも受け入れ、それに応じてルールを評価します。そのため、統合テストと手動テストでアプリケーションをエミュレータに直接接続できます。

ローカル単体テストを実行する

v9 JavaScript SDK を使用してローカル単体テストを実行する

Firebase は、バージョン 9 の JavaScript SDK とバージョン 8 の SDK の両方を備えた、セキュリティ ルールの単体テスト ライブラリを配布しています。これらのライブラリの API は大きく異なります。v9 テスト ライブラリの使用をおすすめします。v9 テスト ライブラリを使用すると、エミュレータへの接続がより効率化され、必要な設定も少なくて済みます。これにより、本番環境リソースが誤って使用されることを回避できます。下位互換性のために、引き続き v8 テスト ライブラリをご利用いただけます

@firebase/rules-unit-testing モジュールを使用して、ローカルで動作するエミュレータを操作します。タイムアウトまたは ECONNREFUSED エラーが発生する場合は、エミュレータが実行されていることを確認してください。

async/await の表記法を使用できるようにするため、新しいバージョンの Node.js を使用することを強くおすすめします。テスト対象となる可能性のある動作のほとんどには非同期関数があります。また、テスト モジュールは Promise ベースのコードで動作するように設計されています。

v9 ルールの単体テスト用ライブラリはエミュレータを常に認識し、本番環境のリソースに接続することはありません。

ライブラリをインポートするには、v9 モジュラー インポート ステートメントを使用します。次に例を示します。

import {
  assertFails,
  assertSucceeds,
  initializeTestEnvironment
} from "@firebase/rules-unit-testing"

// Use `const { … } = require("@firebase/rules-unit-testing")` if imports are not supported
// Or we suggest `const testing = require("@firebase/rules-unit-testing")` if necessary.

インポートしたら単体テストを実装します。実装には次の作業が含まれます。

  • initializeTestEnvironment を呼び出して、RulesTestEnvironment を作成して構成します。
  • ルールを一時的にバイパスできる便利なメソッド RulesTestEnvironment.withSecurityRulesDisabled を使用して、ルールをトリガーせずにテストデータをセットアップします。
  • RulesTestEnvironment.cleanup()RulesTestEnvironment.clearFirestore() など、テストデータと環境をクリーンアップするための呼び出しを使用して、テストスイートとテストごとの前後のフックをセットアップします。
  • RulesTestEnvironment.authenticatedContextRulesTestEnvironment.unauthenticatedContext を使用して、認証状態を模倣するテストケースを実装します。

一般的なメソッドとユーティリティ関数

v9 SDK のエミュレータ固有のテストメソッドもご覧ください。

initializeTestEnvironment() => RulesTestEnvironment

この関数は、ルールの単体テストのテスト環境を初期化します。テストのセットアップを行うには、まずこの関数を呼び出します。これが正常に実行されるには、エミュレータが実行されている必要があります。

この関数は、TestEnvironmentConfig を定義するオプションのオブジェクトを受け入れます。このオブジェクトはプロジェクト ID とエミュレータ構成設定で構成されます。

let testEnv = await initializeTestEnvironment({
  projectId: "demo-project-1234",
  firestore: {
    rules: fs.readFileSync("firestore.rules", "utf8"),
  },
});

RulesTestEnvironment.authenticatedContext({ user_id: string, tokenOptions?: TokenOptions }) => RulesTestContext

このメソッドは RulesTestContext を作成します。これは、認証された Authentication ユーザーと同様に動作します。返されたコンテキストを介して作成されたリクエストには、疑似の Authentication トークンが添付されます。必要に応じて、Authentication トークン ペイロードのカスタムのクレームまたはオーバーライドを定義するオブジェクトを渡してください。

テストで返されたテスト コンテキスト オブジェクトを使用して、initializeTestEnvironment で構成されたものも含め、構成されたエミュレータ インスタンスにアクセスします。

// Assuming a Firestore app and the Firestore emulator for this example
import { setDoc } from "firebase/firestore";

const alice = testEnv.authenticatedContext("alice", {  });
// Use the Firestore instance associated with this context
await assertSucceeds(setDoc(alice.firestore(), '/users/alice'), { ... });

RulesTestEnvironment.unauthenticatedContext() => RulesTestContext

このメソッドは RulesTestContext を作成します。これは、Authentication を介してログインしていないクライアントのように動作します。返されたコンテキストを介して作成されたリクエストには、Firebase Auth トークンが添付されません。

テストで返されたテスト コンテキスト オブジェクトを使用して、initializeTestEnvironment で構成されたものも含め、構成されたエミュレータ インスタンスにアクセスします。

// Assuming a Cloud Storage app and the Storage emulator for this example
import { getStorage, ref, deleteObject } from "firebase/storage";

const alice = testEnv.unauthenticatedContext();

// Use the Cloud Storage instance associated with this context
const desertRef = ref(alice.storage(), 'images/desert.jpg');
await assertSucceeds(deleteObject(desertRef));

RulesTestEnvironment.withSecurityRulesDisabled()

セキュリティ ルールが無効になっているかのように動作するコンテキストで、テスト セットアップ関数を実行します。

このメソッドはコールバック関数を受け取ります。このコールバック関数は Security-Rules-bypassing コンテキストを受け取って Promise を返します。Promise が解決または拒否されると、コンテキストは破棄されます。

RulesTestEnvironment.cleanup()

このメソッドは、テスト環境で作成されたすべての RulesTestContexts を破棄し、それらの元となっているリソースをクリーンアップします。これにより、クリーンな終了が可能になります。

このメソッドでは、エミュレータの状態は一切変更されません。複数のテストの合間にデータをリセットするには、アプリケーション エミュレータ固有のデータ消去メソッドを使用してください。

assertSucceeds(pr: Promise<any>)) => Promise<any>

これはテスト ケースのユーティリティ関数です。

この関数は、エミュレータ オペレーションをラップする指定された Promise が、セキュリティ ルール違反なしで解決されることをアサートします。

await assertSucceeds(setDoc(alice.firestore(), '/users/alice'), { ... });

assertFails(pr: Promise<any>)) => Promise<any>

これはテスト ケースのユーティリティ関数です。

この関数は、エミュレータ オペレーションをラップする指定された Promise が、セキュリティ ルール違反により拒否されることをアサートします。

await assertFails(setDoc(alice.firestore(), '/users/bob'), { ... });

エミュレータ固有のメソッド

v9 SDK の一般的なテストメソッドとユーティリティ関数もご覧ください。

RulesTestEnvironment.clearFirestore() => Promise<void>

このメソッドは、Firestore エミュレータ用に構成された projectId に属する Firestore データベースのデータをクリアします。

RulesTestContext.firestore(settings?: Firestore.FirestoreSettings) => Firestore;

このメソッドは、このテストのコンテキストの Firestore インスタンスを取得します。返された Firebase JS Client SDK インスタンスは、クライアント SDK API(v9 モジュラーまたは v9 互換)で使用できます。

ルール評価を可視化する

Cloud Firestore エミュレータを使用すると、Emulator Suite UI でクライアント リクエストを可視化できます。たとえば、Firebase セキュリティ ルールの評価トレースを行うことができます。

[Firestore] > [Requests] タブを開くと、各リクエストの詳細な評価シーケンスが表示されます。

セキュリティ ルールの評価を示す Firestore エミュレータのリクエスト モニタリング

テストレポートを生成する

一連のテストを実行した後、それぞれのセキュリティ ルールの評価を示したテスト カバレッジ レポートにアクセスできます。

レポートを取得するには、エミュレータの実行中に公開されたエンドポイントに対してクエリを実行します。ブラウザでの表示に適したバージョンを参照するには、次の URL を使用します。

http://localhost:8080/emulator/v1/projects/<project_id>:ruleCoverage.html

これによりルールが式やサブ式に分割されます。それぞれの式の上にマウスカーソルを重ねて、評価回数や返された値などの詳細情報を確認できます。このデータの未加工の JSON バージョンを取得するには、クエリに次の URL を含めます。

http://localhost:8080/emulator/v1/projects/<project_id>:ruleCoverage

エミュレータと本番環境の違い

  1. Cloud Firestore プロジェクトを明示的に作成する必要はありません。エミュレータがアクセスされるインスタンスを自動的に作成します。
  2. Cloud Firestore エミュレータは、通常の Firebase Authentication フローでは動作しません。そこで、Firebase Test SDK の中には auth フィールドを受け取る initializeTestApp() メソッドが rules-unit-testing ライブラリに用意されています。このメソッドを使用して作成された Firebase ハンドルは、どのようなエンティティを指定しても正常に認証されたかのように動作します。null を渡すと、認証されていないユーザーとして動作します(たとえば auth != null ルールは失敗します)。

既知の問題のトラブルシューティング

Cloud Firestore エミュレータを使用する際には、次のような既知の問題が発生する可能性があります。発生する異常な動作をトラブルシューティングするには、以下のガイダンスに従ってください。これらの注記はセキュリティ ルールの単体テスト ライブラリを考慮して記述されていますが、一般的なアプローチはすべての Firebase SDK に適用されます。

テスト動作に一貫性がない

テスト自体に変更を加えていないのに、テストに合格したりしなかったりする場合は、テストの順序が正しいことを確認する必要があります。エミュレータとのやり取りのほとんどは非同期であるため、すべての非同期コードの順序が正しいことを再確認してください。順序を修正するには、Promise をチェーン化するか、await 表記を多数使用できます。

特に、以下の非同期オペレーションを確認してください。

  • initializeTestEnvironment などを使用したセキュリティ ルールの設定。
  • db.collection("users").doc("alice").get() などを使用したデータの読み書き。
  • assertSucceedsassertFails を含む動作可能なアサーション。

エミュレータを初めて読み込むときにのみテストに合格する

エミュレータはステートフルです。書き込まれたすべてのデータをメモリに保存するので、エミュレータがシャットダウンするたびにデータは失われます。同じプロジェクト ID に対して複数のテストを実行している場合は、各テストで後続のテストに影響するデータが生成される可能性があります。次のいずれかの方法を使用すれば、この動作を回避できます。

  • テストごとに一意のプロジェクト ID を使用する。これを行う場合は、各テストの一環として initializeTestEnvironment を呼び出す必要があるのでご注意ください。ルールが自動的に読み込まれるのはデフォルトのプロジェクト ID のみです。
  • 過去に書き込まれたデータを扱わないようにテストを再構成する(テストごとに異なるコレクションを使用するなど)。
  • テスト中に書き込まれたすべてのデータを削除する。

テストのセットアップが非常に複雑である

テストを設定する際に、Cloud Firestore Security Rules で実際には許可されない方法でデータを変更することが必要な場合もあります。ルールが原因でテストのセットアップが複雑になる場合は、セットアップのステップで RulesTestEnvironment.withSecurityRulesDisabled を使用すると、読み取りや書き込みによって PERMISSION_DENIED エラーがトリガーされなくなります。

その後、テストはそれぞれ RulesTestEnvironment.authenticatedContextunauthenticatedContext を使用して、認証されたユーザーまたは認証されていないユーザーとして操作を実行できます。これにより、Cloud Firestore Security Rules によってさまざまなケースが正しく許可 / 拒否されるかどうかを検証できます。