Firestore Security Rules for Query with Array Contains

2,840

Solution 1

If I had to guess, I'd say that groupIds isn't actually a List type object, which means that the field from the document is also not an array. If it's a string, this code won't work, since strings don't have a method called size() in the rules language.

If you aren't 100% certain what the type of field is going to be, you will need to check the type in the rule and determine what to do with it. You can use the is operator to check the type. For example, groupIds is list will be boolean true if you're actually working with one.

In your rules, you can use the debug() function to dump the value of some expression to the log. It will return the same value. So, you can say debug(groupIds) != null to both print the value and check it for null.

Solution 2

As per this blog post, if you can maintain an index of member IDs for a given post (based on group assignments), then you can secure post read access storing member IDs in an array data type and matching against the member IDs with the "array-contains" clause in your ruleset. It looks like this in your Firebase rules:

service cloud.firestore {
  match /databases/{database}/documents {
    match /posts/{postId} {
     allow read: if request.auth.uid in resource.data.members
     allow write: if request.auth.uid == resource.data.owner
    }
  }
}

Solution 3

It appears Firestore does not currently support security rules for this scenario at the moment (thanks for your help tracking this down Doug Stevenson). I have come up with a mechanism to work around the limitation and wanted to share in case someone else is dealing with this issue. It requires an extra query but keeps me from having to create a Web API using the Admin SDK just to get around the security rules.

Posts are stored as follows (simplified):

/posts/{postId}
- userId
- timestamp
- groupIds[]
- message
- photo

Now I am adding an additional post references collection which just stores pointer information:

/postRefs/{postId}
- userId
- timestamp
- groupIds[]

The posts collection will have security rules which does all the validation to ensure the user is in at least one of the groups in which the post is tagged. Firestore is able to handle this properly for simple get requests, just not list requests at the moment.

Since the postRefs collection stores only ID's, and not sensitive information which may be in the post, its security rules can be relaxed such that I only verify a user is logged in. So, the user will perform post queries on the postRefs collection to retrieve a list of ordered postId's to be lazily loaded from the posts collection.

Clients add/delete posts to/from the normal posts collection and then there is a Cloud Function which copies the ID information over to the postRefs collection.

Share:
2,840
TNC
Author by

TNC

Updated on December 12, 2022

Comments

  • TNC
    TNC over 1 year

    I have a Flutter app in which users can make posts and tag the post as belonging to a group. Posts are stored in a global collection and each has a Post.groupId field:

    /posts/{postId}
    

    Based on my Firestore security rules and queries, users are only allow to read posts if they are in the group for which the post is tagged (i.e the posts's groupId field). Approved group users are stored in:

    /groups/{groupId}/users/{userId}
    

    I could query the posts from a particular user's group like:

    _firestore.collection('posts').where('groupId', isEqualTo: 'groupA')...
    

    This above was all working properly.

    I am attempting to make an improvement in which a post can be tagged in multiple groups instead of just one, so I am replacing the single Post.groupId field with a Post.groupIds array. A user should be able to read a post if he/she is a member of ANY of the groups from Post.groupIds. I attempt to read all posts tagged with a particular group with the following query from my Flutter app:

    _firestore.collection('posts').where('groupIds', arrayContains: 'groupA')...
    

    I keep receiving the following exception Missing or insufficient permissions with these security rules:

    match /posts/{postId} {
        allow read: if canActiveUserReadAnyGroupId(resource.data.groupIds);
    }
    
    function isSignedIn() {
        return request.auth != null;
    }
    
    function getActiveUserId() {
        return request.auth.uid;
    }
    
    function isActiveUserGroupMember(groupId) {
        return isSignedIn() &&
                exists(/databases/$(database)/documents/groups/$(groupId)/users/$(getActiveUserId()));
    }
    
    function canActiveUserReadAnyGroupId(groupIds) {
        return groupIds != null && (
                (groupIds.size() >= 1 && isActiveUserGroupMember(groupIds[0])) ||
                (groupIds.size() >= 2 && isActiveUserGroupMember(groupIds[1])) ||
                (groupIds.size() >= 3 && isActiveUserGroupMember(groupIds[2])) ||
                (groupIds.size() >= 4 && isActiveUserGroupMember(groupIds[3])) ||
                (groupIds.size() >= 5 && isActiveUserGroupMember(groupIds[4]))
                );
    }
    

    With these security rules I can read a single post but I cannot make the above query. Is it possible to have security rules which allow me to make this query?

    UPDATE 1

    Added isSignedIn() and getActiveUserId() security rules functions for completeness.

    UPDATE 2

    Here is the error I am receiving when I attempt to execute this query with the Firestore Emulator locally:

         FirebaseError: 
    Function not found error: Name: [size]. for 'list' @ L215
    

    Line 215 corresponds to the allow read line within this rule:

    match /posts/{postId} {
        allow read: if canActiveUserReadAnyGroupId(resource.data.groupIds);
    }
    
  • TNC
    TNC almost 5 years
    I did verify groupIds is an array in the database document. However, I added the debug(groupIds) != null call to the rule and it prints the following. constraint_value { simple_constraints { comparator: LIST_CONTAINS value { string_value: "groupA" } } }
  • Doug Stevenson
    Doug Stevenson almost 5 years
    Hmm, actually, the way that rules work, it can't provide a value for each document evaluated individually (it's not going to read each possible document - that won't scale). Instead, it's going to provide a description of what the query looks like so that the rules can check the validity of the query more generally.
  • TNC
    TNC almost 5 years
    Gotcha, then is there a way to extract the string_value out of the constraint so I verify user access to that group? I tried the following but it gives an error: groupIds.constraint_value.simple_constraints.value.string_va‌​lue. Here is the error I now get: Type error. Received: [constraint] Expected: [map,path]. for 'list' @ L219
  • Doug Stevenson
    Doug Stevenson almost 5 years
    The value behaves kind of a like a list, but not totally. What you can do here is check if something exists in the constraints given by the client. You should be able to say "groupA" in resource.data.groupId to check what the client sent. That's about as much as I know right now. I'm told resource.data.groupId[0] will not.
  • TNC
    TNC almost 5 years
    Unfortunately I cannot hardcode the group like "groupA" in resource.data.groupIds" in the security rules as the groups are user-generated. I also tried resource.data.groupIds[0] but got the following error: Function not found error: Name: [[]]. for 'list' @ L219
  • Doug Stevenson
    Doug Stevenson almost 5 years
    I understand. I think what you're trying to do is currently not possible with security rules. At least not for queries. Individual document gets should be OK, as you can access the entire list from the document being fetched.
  • TNC
    TNC almost 5 years
    Thanks for your help. I added a workaround I'm planning on implementing.