4

I'm attempting to setup security rules that allow access to a collection, based on the value of a document field in a subcollection.

This works as expected when retrieving an individual document by id, which is a get operation. However, when querying main_collection (a list operation), this fails with a "permission denied" error. Since there is only a single document in the collection, this is not a case where I don't have permission to some of the documents being queried, such as on this question.

My database structure looks like the following. It contains the collection being listed (main_collection), which has a single document (some_doc), which has a single subcollection (sub_collection), which has a single document (another_doc).

/main_collection/some_doc/sub_collection/another_doc

another_doc has one string field someFieldValue.

For this example, my query is of the entire collection, which is the single document. In my actual application it only queries the documents it expects to have access to, but the end result here is the same because I cannot filter against a document's subcollection from the client library.

firestore.collection('main_collection').get()

These are my security rules.

service cloud.firestore {
  match /databases/{database}/documents {
    match /main_collection/{mainColDoc} {
      // This operation works
      allow get: if subCollectionDocumentHasField('someFieldValue');
      // This operation fails with permission denied
      allow list: if subCollectionDocumentHasField('someFieldValue');

      // This checks for the existence of a field on the subcollection's document
      function subCollectionDocumentHasField(fieldName) {
        return get(/databases/$(database)/documents/main_collection/$(mainColDoc)/sub_collection/another_doc).data.keys().hasAny([fieldName]);
        //return get(/databases/$(database)/documents/main_collection/some_doc/sub_collection/another_doc).data.keys().hasAny([fieldName]);
      }
    }
  }
}

The subCollectionDocumentHasField function checks for the existence of the someFieldValue field on the document another_doc. In this function, if I replace the $(mainColDoc) variable with the hard-coded document id some_doc, the list operation is successful. Since the $(database) path variable can be used in this context, I would expect that others could be as well.

Is this a bug or expected behavior?

cokeman19
  • 2,405
  • 1
  • 25
  • 40

3 Answers3

2

This is actually the expected behavior, you can't use Firebase's rules to filter the results of your query.


A typical scenario would be to have collection of messages, where each message refers to its creator.

You can't simply add a rule where reading is only allowed on messages for which creator is the authenticated user, to filter automatically the messages of the current authenticated user.

The only way to go is to query with filter on the client side (or through a Cloud function).


The documentation is very clear about this :

When writing queries to retrieve documents, keep in mind that security rules are not filters—queries are all or nothing. To save you time and resources, Cloud Firestore evaluates a query against its potential result set instead of the actual field values for all of your documents. If a query could potentially return documents that the client does not have permission to read, the entire request fails.

From Firebase's documentation

José
  • 471
  • 5
  • 11
  • Thanks for looking. While the functionality you reference may be a factor, it doesn't address *why* replacing the `$(mainColDoc)` variable with the hard-coded document id allows the `list` to succeed; that is, the security rule works as I'd expect, allowing/denying access to the document based on the subcollection's document's field's existence. Since I'm using the same query (now in the question) in both cases, if it were an issue with what you reference, I'd expect the query to fail regardless, because the "potential result set" "potentially include documents that violate…security rules". – cokeman19 Feb 18 '19 at 04:48
  • With this rule condition `get(/databases/.../.../.../some_doc/.../another_doc).data.keys().hasAny([fieldName])`, where `some_doc` is hard-coded. The condition is only checked **once per query** as **it's not a per-document condition**. It evaluates `another_doc` which appears to be in the collection, but it could be any other document in your database. – José Feb 18 '19 at 12:58
  • With this other rule condition `get(/databases/.../.../.../$(mainColDoc)/.../another_doc).data.keys().hasAny([fieldName])`. Rule is much more dynamic as it should be tested against each nested documents for every `$(mainColDoc)`possible values. **Per-document condition** doesn't work for queries, as it consumes too much resources. That's why you can't filter queries with rules. – José Feb 18 '19 at 13:01
  • On its face, your per-query-evaluation explanation makes sense. Do you have an official reference for this? I ask because another SO answer from the Firestore team, https://stackoverflow.com/a/49440901, suggests using a function, `parentDoc()`, to query a parent document's data on `read`, which would include `list`. That `parentDoc()` function *is* using a variable in its path. Admittedly, this is in the opposite direction from what I'm trying (i.e. a child document querying a parent document), but in the context of `parentDoc()`, it would still need to query per-document to work. – cokeman19 Feb 18 '19 at 15:45
  • @cokeman19, this is the understanding I have from the above linked ([documentation](https://firebase.google.com/docs/firestore/security/rules-query#queries_and_security_rules)) and from my personal experience with Firestore. I think of firestore rules as lazy, as they won't iterate over each item of a list to check the rule (even if the list is empty). From the other thread you pointed out, rule is `allow read, write: if get(/databases/../../$(performanceId)).data.owner = request.auth.uid;`. `list` will work as there is only one `$(performanceId)` to check when querying (nested) scenes. – José Feb 18 '19 at 16:16
  • True, so that would fit your once-per-query argument. Everything you've outlined is consistent with what I'm experiencing. I've opened a ticket with Firestore support in hopes of finding a definitive, canonical answer. Since I'm still looking for an official explanation, I'm going to leave this question as unanswered, but the bounty is yours. Thanks again for your time and research. – cokeman19 Feb 19 '19 at 03:15
  • You're welcome and I'm glad I could help. If you get a final official answer, feel free to share it with us ! Cheers. – José Feb 19 '19 at 07:44
1

I opened a ticket with Google and confirmed effectively what @José inferred from usage, which is that a security rule "is only checked once per query".

For clarification, while a security rule on a list operation will typically not query the contents of a document (to avoid potenitally-poor performance), there is at least one condition when it will query the contents of a document. This is when the security rule is guaranteed to return only one document. When this guarantee is met, the single document's contents will be queried because high performance can be maintained; the same as on a get operation.

So, in the linked example in my question where the list operation's rule is referencing a parent document, this guarantee is met and the parent document's contents will get queried.

Also, in my example where the list operation's rule is referencing a hard-coded document id, this guarantee is met and the hard-coded document's contents will get queried.

For the sake of stating it explicitly, for a list operation, in any case where Firestore cannot guarantee that its rule will only query a single document, access will be automatically denied, by design.

cokeman19
  • 2,405
  • 1
  • 25
  • 40
0

To reiterate what the other answers say, but stated in a slightly different way: The query must be consistent with the security rules, before any query documents are looked at, or it will fail with permission denied.

For example, if all of the documents in a sub-collection happen to match the security rule (e.g., your create and list rules both require the owner field is "X"), the query still must match the security rules (e.g., the query must also filter on owner is "X") or it will fail with a permission denied error, independent of the actual content of the sub-collection.

P.T.
  • 24,557
  • 7
  • 64
  • 95