2

I have a Collection of Player Documents in firestore. I want to mark some of those Documents as private, so that they can't be queried against. A JSON dump of my data looks like this:

[
  {
    "id": "H0ycPIqXB5pX5VmdYlmY",
    "name": "Tim",
  },
  {
    "id": "VICMGdutgIN7PUjG571h",
    "name": "Zoe",
  },
  {
    "id": "query-blocker",
    "name": "Don't look here",
    "private": true
  },
  {
    "id": "zYkhO5f7gYPe2VgqQQXe",
    "name": "Bob"
  }
]

Now apply this security rule, intended to protect any document with a field labelled private:

match /players/{document=**} {
    allow read: if !('private' in resource.data);
}

Results:

  • A query to read a single document that contains a field private, correctly returns a permission denied error.
  • A query to read all the documents in the collection successfully returns all documents in the collection, including all of the ones marked private.

It seems like the query for all documents should also fail (I understand that security rules are not filters). Is there something I am misunderstanding here?

Here is a working example of the issue using the emulator: https://github.com/Keltin42/firebase-it-rulestest

Here is a simplified example you can run from the command line:

'use strict';

const firebase = require('firebase');
require('firebase/firestore');

firebase.initializeApp({
    apiKey: 'your api key here',
    projectId: 'your project id here'
});
const db = firebase.firestore();

async function doTest() {
    const playersCollection = db.collection('players');
    await playersCollection.add({ name: 'Sue' });
    await playersCollection.add({ name: 'Bob' });
    await playersCollection.doc('good').set({ name: 'Fred' });
    await playersCollection.doc('query-blocker').set({ name: 'Tim', private: true });

    // Read a good document.
    await playersCollection.doc('good').get().then(doc => {
        console.log('The good document: ', JSON.stringify(doc.data()));
    });

    // Read all the documents
    await playersCollection.get().then(querySnapshot => {
        console.log('All documents: ');
        querySnapshot.forEach(doc => {
            console.log('\t', doc.id, ' => ', doc.data());
        });
    });

    // Read the query-block document
    await playersCollection.doc('query-blocker').get().then(doc => {
        console.log('The query-blocker document: ', JSON.stringify(doc.data()));
    }).catch(error => {
        console.error('Error retrieving query-blocker document: ', error);
    });
}

doTest();

with the security rules:

service cloud.firestore {
  match /databases/{database}/documents {
    match /players/{document=**} {
      allow write;
      allow read: if !('private' in resource.data);
    }
  }
}
Tim Philip
  • 121
  • 1
  • 6
  • Please show the minimal code that reproduces the error in your question. – Frank van Puffelen Nov 21 '19 at 22:38
  • I used the firestore emulator to create a test case. You can find it here: https://github.com/Keltin42/firebase-it-rulestest Does that help? – Tim Philip Nov 22 '19 at 18:18
  • Thanks for providing a full repo. I was looking for something much simpler though. Please edit the minimal information that is needed in your question. This should not require more than a few lines of code, instead of the 100+ lines in the test.js in the repo. – Frank van Puffelen Nov 22 '19 at 18:26
  • Appending a simple working example to the end of the question. – Tim Philip Nov 22 '19 at 20:43
  • Are you asking why `firebase.firestore().collection('players').get()` works when you have `allow read: if !('private' in resource.data);` on the documents in the `players` collection? – Frank van Puffelen Nov 22 '19 at 22:17
  • Yes. The security rule should be blocking it. – Tim Philip Nov 22 '19 at 22:48
  • You might want to trim your question to the minimum, as it's quite confusing (to me at least). Meanwhile, let's see if we can get @DougStevenson to have a look. – Frank van Puffelen Nov 22 '19 at 23:50
  • @FrankvanPuffelen - have you had a chance to take a look at this? I tried to simplify down the original question, but it requires a combination of data, code and security rules to demonstrate the issue. – Tim Philip Dec 03 '19 at 21:57
  • Your query doesnt' seem to filter against the `private` field, which is required. Keep in mind that security rules do not filter the data on their own. They merely ensure that you're not trying to read more data than you have access to. So whatever you want to disallow, you'll have to put both in the rules and in your query. That is probably a blocking problem here, since you [can't do an inequality check in queries](https://stackoverflow.com/questions/47251919/firestore-how-to-perform-a-query-with-inequality-not-equals), which is what you're doing in your rules. – Frank van Puffelen Dec 03 '19 at 22:14
  • Don't use a glob like `{document=**}`, just use `{docId}` or similar. There's no reason for a glob here and that may help. Might also true `resource.data.private != true` to see if that works instead. – Kato Sep 11 '20 at 14:44
  • Also recommend reading [this blog post](https://medium.com/firebase-developers/what-does-it-mean-that-firestore-security-rules-are-not-filters-68ec14f3d003) so you fully understand how rules work with queries. – Kato Sep 11 '20 at 14:55

1 Answers1

0

You've created a highly complex scenario here that is hidden behind your proposed solution (see what is the XY problem). A reasonable answer based on what we can discern from your use case is to simplify your data and the required query. Also recognize that security rules are not filters and that you can't do a get() on the collection and expect that to filter the results on your behalf.

Start by setting private: false for all existing records which don't have a value (you can't query for absence of things or missing values).

Then set up your rules like this:

match /players/{playerId} {
   allow read: if resource.data.private == false;
}

And your queries like this:

playersCollection.where("private", "==", false).get()

When performing queries, the query must match the rule for it to succeed (the data isn't actually examined as this can't scale). So your goal is to match those two.

Generally speaking, avoid the use of globs (e.g. document=**) as they introduce potential security issues (implicit allow is usually discouraged in security; explicit allow is preferred).

Kato
  • 40,352
  • 6
  • 119
  • 149