データの安全なクエリ

このページでは、セキュリティ ルールの構造化セキュリティ ルールの条件の記述のコンセプトを基に、Firestore のセキュリティ ルールとクエリがどのように関係しているかを説明します。作成可能なクエリにセキュリティ ルールがどのように影響するかを詳しく見ていき、セキュリティ ルールと同じ制約をクエリで使用する方法を説明します。また、limitorderBy などのクエリ プロパティに基づいてクエリを許可または拒否するセキュリティ ルールを記述する方法についても説明します。

ルールはフィルタではない

ドキュメントを取得するクエリを記述するとき、セキュリティ ルールはフィルタではないことにご注意ください。セキュリティ ルールは、クエリが完全に動作するか、まったく動作しないかを決定します。時間とリソースを節約するため、Firestore はすべてのドキュメントの実際のフィールドの値ではなく、結果セットの可能性に対してクエリを評価します。クライアントが読み取り権限を持たないドキュメントをクエリが返す可能性がある場合、リクエスト全体が失敗します。

クエリとセキュリティ ルール

以下の例で示すように、セキュリティ ルールの制約に合わせてクエリを記述する必要があります。

auth.uid に基づくドキュメントの保護とクエリ

次の例は、セキュリティ ルールで保護されたドキュメントを取得するクエリを記述する方法を示しています。story ドキュメントのコレクションを含むデータベースを考えてみましょう。

/stories/{storyid}

{
  title: "A Great Story",
  content: "Once upon a time...",
  author: "some_auth_id",
  published: false
}

各ドキュメントには title フィールドと content フィールドに加えて、アクセス制御に使用される author フィールドと published フィールドが格納されています。ここで示す例では、アプリが Firebase Authentication を使用して、ドキュメントを作成したユーザーの UID を author フィールドに設定していると想定しています。また、Firebase Authentication はセキュリティ ルールの request.auth 変数に値を設定します。

次のセキュリティ ルールでは、request.auth 変数と resource.data 変数を使用して、各 story に対する読み取りと書き込みのアクセス権を作成者のみに制限しています。

service cloud.firestore {
  match /databases/{database}/documents {
    match /stories/{storyid} {
      // Only the authenticated user who authored the document can read or write
      allow read, write: if request.auth != null && request.auth.uid == resource.data.author;
    }
  }
}

あるアプリに、ユーザーが作成した story ドキュメントのリストを表示するページを含めるとします。次のクエリを使用してこのページのデータを取得できそうに思えますが、セキュリティ ルールと同じ制約が含まれていないため、このクエリは失敗します。

無効: クエリの制約がセキュリティ ルールの制約と一致していない

// This query will fail
db.collection("stories").get()

現在のユーザーがすべての story ドキュメントの作成者であったとしても、このクエリは失敗します。この動作の理由は、Firestore がセキュリティ ルールを適用するときには、データベース内のドキュメントの実際のプロパティに対してではなく、結果セットの「可能性」に対してクエリを評価するためです。セキュリティ ルールに違反するドキュメントがクエリに含まれる可能性がある場合、クエリは失敗します。

これとは対照的に、次のクエリは成功します。これは、author フィールドに関してセキュリティ ルールと同じ制約が含まれているためです。

有効: クエリの制約がセキュリティ ルールの制約と一致している

var user = firebase.auth().currentUser;

db.collection("stories").where("author", "==", user.uid).get()

フィールドに基づいてドキュメントを保護し、クエリする

クエリとルールの関係の例をさらに示します。次のセキュリティ ルールでは、stories コレクションの読み取りアクセス対象を広げ、published フィールドが true に設定されている story ドキュメントをすべてのユーザーが読み取ることができるようにします。

service cloud.firestore {
  match /databases/{database}/documents {
    match /stories/{storyid} {
      // Anyone can read a published story; only story authors can read unpublished stories
      allow read: if resource.data.published == true || (request.auth != null && request.auth.uid == resource.data.author);
      // Only story authors can write
      allow write: if request.auth != null && request.auth.uid == resource.data.author;
    }
  }
}

公開ページのクエリには、セキュリティ ルールと同じ制約を含める必要があります。

db.collection("stories").where("published", "==", true).get()

クエリの制約 .where("published", "==", true) によって、すべての結果で resource.data.publishedtrue であることが保証されます。したがって、このクエリはセキュリティ ルールに合致し、データを読み取ることができます。

OR 件のクエリ

ルールセットに対して論理 OR クエリ(orin、または array-contains-any)を評価する場合、Firestore によって各比較値が個別に評価されます。各比較値はセキュリティ ルールの制約を満たしている必要があります。たとえば、次のルールの場合:

match /mydocuments/{doc} {
  allow read: if resource.data.x > 5;
}

無効: 可能性のあるすべてのドキュメントについて x > 5 になることがクエリによって保証されない

// These queries will fail
query(db.collection("mydocuments"),
      or(where("x", "==", 1),
         where("x", "==", 6)
      )
    )

query(db.collection("mydocuments"),
      where("x", "in", [1, 3, 6, 42, 99])
    )

有効: 可能性のあるすべてのドキュメントについて x > 5 になることがクエリによって保証される

query(db.collection("mydocuments"),
      or(where("x", "==", 6),
         where("x", "==", 42)
      )
    )

query(db.collection("mydocuments"),
      where("x", "in", [6, 42, 99, 105, 200])
    )

クエリの制約の評価

セキュリティ ルールは、クエリの制約に基づいてそのクエリを許可または拒否することもできます。request.query 変数には、クエリの limitoffsetorderBy の各プロパティが含まれています。たとえば、取得されるドキュメントの最大数を特定の範囲に制限していないクエリをセキュリティ ルールで拒否できます。

allow list: if request.query.limit <= 10;

次のルールセットは、クエリ内の制約を評価するセキュリティ ルールを記述する方法を示しています。この例では、前出の stories ルールセットを拡張し、次の変更を加えています。

  • このルールセットでは read ルールを get ルールと list ルールに分割しています。
  • get ルールでは、単一のドキュメントの取得を、公開ドキュメントか、そのユーザー自身が作成したドキュメントに制限しています。
  • list ルールでは、クエリ以外については get ルールと同じ制限が適用されます。また、このルールではクエリの制限がチェックされ、無制限のクエリまたは制限が 10 を超えるクエリは拒否されます。
  • このルールセットでは、コードの重複を避けるために、authorOrPublished() 関数を定義しています。
service cloud.firestore {

  match /databases/{database}/documents {

    match /stories/{storyid} {

      // Returns `true` if the requested story is 'published'
      // or the user authored the story
      function authorOrPublished() {
        return resource.data.published == true || request.auth.uid == resource.data.author;
      }

      // Deny any query not limited to 10 or fewer documents
      // Anyone can query published stories
      // Authors can query their unpublished stories
      allow list: if request.query.limit <= 10 &&
                     authorOrPublished();

      // Anyone can retrieve a published story
      // Only a story's author can retrieve an unpublished story
      allow get: if authorOrPublished();

      // Only a story's author can write to a story
      allow write: if request.auth.uid == resource.data.author;
    }

  }
}

コレクション グループのクエリとセキュリティ ルール

デフォルトでは、クエリは単一コレクションにスコープされ、コレクションからのみ結果を取得します。コレクション グループ クエリを使用すると、同一 ID を持つすべてのコレクションで構成されるコレクション グループから結果を取得できます。ここでは、セキュリティ ルールを使用してコレクション グループ クエリを保護する方法について説明します。

コレクション グループに基づくドキュメントの保護とクエリ

セキュリティ ルールでは、コレクション グループのルールを記述して、コレクション グループのクエリを明示的に許可する必要があります。

  1. rules_version = '2'; がルールセットの最初の行であることを確認します。コレクション グループ クエリには、セキュリティ ルール バージョン 2 の新しい再帰ワイルドカード {name=**} の動作が必要です。
  2. match /{path=**}/[COLLECTION_ID]/{doc} を使用して、コレクション グループのルールを記述します。

たとえば、posts サブコレクションを含む forum ドキュメントに編成されたフォーラムを考えてみましょう。

/forums/{forumid}/posts/{postid}

{
  author: "some_auth_id",
  authorname: "some_username",
  content: "I just read a great story.",
}

このアプリケーションでは、投稿はオーナーが編集でき、認証済みのユーザーが読み取りできます。

service cloud.firestore {
  match /databases/{database}/documents {
    match /forums/{forumid}/posts/{post} {
      // Only authenticated users can read
      allow read: if request.auth != null;
      // Only the post author can write
      allow write: if request.auth != null && request.auth.uid == resource.data.author;
    }
  }
}

認証済みのユーザーは、任意の単一のフォーラムの投稿を取得できます。

db.collection("forums/technology/posts").get()

では、現在のユーザーが、すべてのフォーラムにわたる自分の投稿を見ることができるようにする場合はどうでしょうか。コレクション グループ クエリを使用して、すべての posts コレクションから結果を取得できます。

var user = firebase.auth().currentUser;

db.collectionGroup("posts").where("author", "==", user.uid).get()

セキュリティ ルールでは、posts コレクション グループの読み取りまたは一覧のルールを記述して、このクエリを許可する必要があります。

rules_version = '2';
service cloud.firestore {

  match /databases/{database}/documents {
    // Authenticated users can query the posts collection group
    // Applies to collection queries, collection group queries, and
    // single document retrievals
    match /{path=**}/posts/{post} {
      allow read: if request.auth != null;
    }
    match /forums/{forumid}/posts/{postid} {
      // Only a post's author can write to a post
      allow write: if request.auth != null && request.auth.uid == resource.data.author;

    }
  }
}

ただし、これらのルールは、階層に関係なく、ID posts を持つすべてのコレクションに適用されます。たとえば、これらのルールは、次の posts コレクションすべてに適用されます。

  • /posts/{postid}
  • /forums/{forumid}/posts/{postid}
  • /forums/{forumid}/subforum/{subforumid}/posts/{postid}

フィールドに基づく安全なコレクション グループ クエリ

単一コレクションのクエリと同様に、コレクション グループ クエリも、セキュリティ ルールで設定されている制約を満たす必要があります。たとえば、上記の stories の例と同様、各フォーラム投稿に published フィールドを追加できます。

/forums/{forumid}/posts/{postid}

{
  author: "some_auth_id",
  authorname: "some_username",
  content: "I just read a great story.",
  published: false
}

これにより、published ステータスと投稿 author に基づく、posts コレクション グループのルールを記述できます。

rules_version = '2';
service cloud.firestore {

  match /databases/{database}/documents {

    // Returns `true` if the requested post is 'published'
    // or the user authored the post
    function authorOrPublished() {
      return resource.data.published == true || request.auth.uid == resource.data.author;
    }

    match /{path=**}/posts/{post} {

      // Anyone can query published posts
      // Authors can query their unpublished posts
      allow list: if authorOrPublished();

      // Anyone can retrieve a published post
      // Authors can retrieve an unpublished post
      allow get: if authorOrPublished();
    }

    match /forums/{forumid}/posts/{postid} {
      // Only a post's author can write to a post
      allow write: if request.auth.uid == resource.data.author;
    }
  }
}

こうしたルールにより、ウェブ、Apple、Android のクライアントは次のクエリを実行できます。

  • フォーラムで公開された投稿を誰でも取得できる。

    db.collection("forums/technology/posts").where('published', '==', true).get()
    
  • すべてのフォーラムで、誰でも投稿者が公開した投稿を取得できる。

    db.collectionGroup("posts").where("author", "==", "some_auth_id").where('published', '==', true).get()
    
  • 投稿者は、すべてのフォーラムで、公開済みおよび未公開の投稿をすべて取得できる。

    var user = firebase.auth().currentUser;
    
    db.collectionGroup("posts").where("author", "==", user.uid).get()
    

コレクション グループとドキュメント パスに基づいてドキュメントを保護し、クエリを実行する

場合によっては、ドキュメント パスに基づいてコレクション グループ クエリを制限することもできます。こうした制限を作成するには、フィールドに基づいてドキュメントの保護とクエリを行う場合と同じ手法を使用します。

いくつかの証券取引所と暗号通貨取引所の間で、各ユーザーの取引を追跡するアプリケーションを考えてみましょう。

/users/{userid}/exchange/{exchangeid}/transactions/{transaction}

{
  amount: 100,
  exchange: 'some_exchange_name',
  timestamp: April 1, 2019 at 12:00:00 PM UTC-7,
  user: "some_auth_id",
}

user フィールドに注目してください。transaction ドキュメントをどのユーザーが所有しているかはドキュメントのパスから確認できますが、次の 2 つのことを行うために、各 transaction ドキュメントにこの情報を複製します。

  • ドキュメント パスに特定の /users/{userid} が含まれるドキュメントのみを対象とするコレクション グループ クエリを記述します。次に例を示します。

    var user = firebase.auth().currentUser;
    // Return current user's last five transactions across all exchanges
    db.collectionGroup("transactions").where("user", "==", user).orderBy('timestamp').limit(5)
    
  • transactions コレクション グループのすべてのクエリに対してこの制限を適用し、別のユーザーの transaction ドキュメントを取得できないようにします。

この制限は、Google のセキュリティ ルールに適用され、user フィールドのデータ検証を含みます。

rules_version = '2';
service cloud.firestore {

  match /databases/{database}/documents {

    match /{path=**}/transactions/{transaction} {
      // Authenticated users can retrieve only their own transactions
      allow read: if resource.data.user == request.auth.uid;
    }

    match /users/{userid}/exchange/{exchangeid}/transactions/{transaction} {
      // Authenticated users can write to their own transactions subcollections
      // Writes must populate the user field with the correct auth id
      allow write: if userid == request.auth.uid && request.data.user == request.auth.uid
    }
  }
}

次のステップ