javascriptgoogle-cloud-firestorefirebase-security

Firestore Security Rules: Getting all docs with field-specific "where"-query


Goal/intention

The rule should grant access to a user document (which contains some personal information like email address) and also its subcollections for movie and tv watchlists, in case the user allows this for the requesting person by adding his/her email address to his shareWith field array.

However, with the request below using where I intent to get all user documents which grant access to a specific other user (i.e. sharing the watchlists). Currently, I solve this by having a separate collection called "conntections" which is readable for everybody and contains the uids of the users who want to share. And with that uid I currently can then access the user document itself with the watchlist. But it would be more effective if I could leave out this extra collection in case I could directly find out which user is sharing the watchlist with the current (logged in) user on the website.

enter image description here


As mentioned in the positive example of the Firestore Documentation I tried the query with the where condition, but it didn't work (premission error):

getDocs(query(collection(db, 'users'), where('shareWith', 'array-contains', email)))
  .then(q => q.forEach(doc => console.log(doc.id, " => ", doc.data())))
;

enter image description here

Why does it not work with the following rule?

Hint: The get() is needed because I also want to access subcollections of the user, i.e. the watchlists, see Getting read access for subcollections of specific document in case a field matches in the main document only

match /users/{userId}/{documents=**} {  
  allow read: if request.auth.token.email in get(/databases/$(database)/documents/users/$(userId)).data.shareWith;
  allow read, write: if request.auth.uid == userId;
}

I'm currently using the (working) query below as an in-between step to get the uid for accessing the document directly without the where condition:

  return getDocs(query(collection(db, 'connections'), where('shareWith', 'array-contains', api.account.id)))
    .then(qs => qs.docs.map(doc => doc.id))
  ;

For accessing the connections collection I can have a more open rule, because there is no personal data visible:

match /connections/{userId} {  
  allow read: if request.auth.uid != null;
  allow read, write: if request.auth.uid == userId;
}

The information in connections is redundant, but currently necessary: enter image description here

PS: What works with the rule above, is accessing a specific user document (and also subcollections) though (for example users/123), using the following request:

  const
    docRef = doc(db, 'users', uid)
  ;

  return getDoc(docRef)
    .then(docSnap => docSnap.data())
  ;

Solution

  • It seems to me that you're running into a known limitation of security rules. Essentially, you can't use the result of a single get() to control access to a full query from a client, but you can when the client is requesting a single document.

    Others have had the same problem.

    See: Firestore Security Rules get() call on list operation

    From what I can see, if you attempt to use get() to get data of document being evaluated in a list operation, the rule fails (though it works when you are fetching a single document by ID).

    A list operation here means a collection query as opposed to a single document fetch. read is the same as list plus get.

    Also: https://www.reddit.com/r/Firebase/comments/1l8oo8s/security_rules_for_lists/

    This works perfectly when I fetch a single document by ID.
    However, when I try to fetch a list of documents, even though each one meets the rule’s conditions, the read is denied.

    I suspect that the problem is due to the fact that a rule must satisfy potentially every document in the result set, and the system is not set to re-evaluate a get for every potentially document, as that might be potentially a very expensive operation. Indeed, the number of get() operations is limited in security rules for precisely this reason.

    In my opinion, you're better off using a separate collection as you are now instead of trying to overload the purpose of the security rules on the users collection.