Firestoreでドキュメントの有効期限付けつつ、特定のフィールド値を持つドキュメントのみをクエリできるようにする

こんにちは、ピリカ開発チームの九鬼です。

Firestoreについて、ドキュメントに有効期限を付けつつ、フィールドが特定値を持つドキュメントのみ取得できるようにしたいことがあります。

セキュリティルールと複合クエリを使って実現できたので、その方法をまとめました。

[[TOC]]

注記

  • 処理対象のコレクションとしてdataをdataコレクションのフィールドとしてexpirationDate, someValueと定義しています。expirationDateはドキュメントの有効日時、someValueは何かしらの値としています。
  • Firebase Authenticationによるログインかどうかのルールについては記載を省略しています。

結論

以下のいずれかをセキュリティルールを設定することで実現できます。

パターン1: 特定のフィールド値について、型チェックを使う

match /data/{dataId} {
  allow list: if
    resource.data.expirationDate > request.time
    && resource.data.someValue is int;   // someValueの型に応じて、適切な型を指定する
}

expirationDate > リクエスト日時 かつ someValue == 整数値 でクエリする限り、ドキュメントを取得できます。これにより、リクエスト日時がexpirationDateより新しい場合は権限不足でデータのクエリをブロックできます。複合クエリでは複数フィールドで不等号を使えないため、someValueをユーザ側が知っているときのみクエリできる、という機構を作ることができます。

パターン2: 特定のフィールド値について、値を制限する

もしフィールド値について値域が決まっていれば、以下のルールが利用できます。

match /data/{dataId} {
  // someValueは2桁の整数とする
  allow list: if
    resource.data.expirationDate > request.time
    && resource.data.someValue >= -99
    && resource.data.someValue <= 99
}

もちろん、クエリ時に上記値域外の値でクエリすると権限エラーとなり、ドキュメントを取得できません。

注意事項

  1. セキュリティルールに記載する各フィールドについて、複合クエリのindexを作る必要があります。
  2. 有効期限のexpirationDateについてクエリするとき、右辺値はfirestoreへのリクエスト日時(request.time)より新しい必要があります。もしリクエスト日時よりも右辺値が古い場合、セキュリティ条件のresource.data.expirationDate > request.timeを満たせず権限エラーになります。

クエリ例

JavaScript(Firestore v9)においては、以下の通りクエリできます(importは省略しています)。

import { collection, getFirestore, where } from "firebase/firestore";

const db = getFirestore();
/*
  requestDateはfirestoreへの実際のリクエスト日時より新しい必要がある。
  暫定的に5秒加算している
*/
const bufferMilliSeconds = 5 * 1000;
const requestDate = new Date(Date.now() + bufferMilliSeconds);

const collection = collection(db, "data");
const querySpecificData = query(
  collection,
  where("expirationDate", ">", requestDate),
  where("someValue", "==", 123456789)
));
const querySnapshot = await getDocs(querySpecificData);

/* 以後、querySnapshotを使った処理 */

セキュリティルールで試行錯誤したこと

NG1: request.resource.dataとresource.dataで比較しようとした

以下の通り、セキュリティルールを設定しました。

match /data/{dataId} {
  allow list: if
    request.resource.data.someValue == resource.data.someValue;
}

リファレンスにもあるのですが、request.resource.dataは書き込みオペレーション実行後のドキュメントを指し、update, createでのみ有効です。readやdeleteでは定義されないので、使うことができません。

NG2: allow getとクエリが絡むルールを併用しようとした

以下の通り、セキュリティルールを設定しました。

match /data/{dataId} {
  allow get: if
    resource.data.expirationDate > request.time
}

セキュリティルールはドキュメントにアクセスする前に判定されます。ドキュメント取得前には対象ドキュメントでresource.data.expirationDate > request.timeかどうか判定できないため、権限エラーとなります。