62

First, sorry for my terrible English, it is not my native language...

I am building a simple app in Firebase, using the Firestore database. In my app, users are members of small groups. They have access to other users' data. In order not to query too many documents (one per user, in a subcollection of the group's document), I have chosen to add the users' data in an array inside the group's document. Here is my group's document:

{
   "name":"fefefefe",
   "days":[false,false,false,false,true],
   "members":[
       {"email":"eee@ff.com","id":"aaaaaaaa","name":"Mavireck"}, 
       {"email":"eee2@ff.com","id":"bbbbbbbb","name":"Mavireck2"}, 
   ],
}

How can I check with the security rules if a user is in a group ? Should I use an object instead ? I'd really prefer not use a subcollection for users, because I would reach the free quota's limits too quickly...

Thank you for your time !

EDIT: Thanks for the answer. I will change it to an object : "Members": { uid1 : {}, uid2 : {} }

Mavireck
  • 621
  • 1
  • 5
  • 4

3 Answers3

81

In general, you need to write a rule like the following:

service cloud.firestore {
  match /databases/{database}/documents {
    match /collection/{documentId} {
      // works if `members` = [uid1, uid2, uid3]
      // no way to iterate over a collection and check members
      allow read: if request.auth.uid in resource.data.members;
      // you could also have `members` = {uid1: {}, uid2: {}}
      allow read: if resource.data.members[request.auth.uid] != null;
    }
  }
}

You could also use subcollections:

service cloud.firestore {
  match /databases/{database}/documents {
    // Allow a user to read a message if the user is in the room
    match /rooms/{roomId} {
      match /documents/{documentId} {
        allow read: if exists(/databases/$(database)/documents/documents/$(documentId)/users/$(request.auth.uid));
      }
      match /users/{userId} {
        // rules to allow users to operate on a document
      }
    }
  }
}
Mike McDonald
  • 15,609
  • 2
  • 46
  • 49
  • Thank you but I can't get it to work... I am quite sure my database is correct : i have an object "members" in the group's document, containing objects with more user infos. : `members = {uid1: {}, uid2: {}}`. However using `allow read: if resource.data.members[request.auth.uid] != null;` won't work. I also tried `resource.data.members[request.auth.uid].name != null;`, and `resource.data.members[(request.auth.uid)] != null;` – Mavireck Oct 24 '17 at 13:55
  • I did a simple test locally : `if(doc.data()['members'][(firebase.auth()currentUser.uid)] != null){console.log("not null)}`... It always log "not null" – Mavireck Oct 24 '17 at 14:16
  • I can't edit so here is my final comment : `allow read: if resource.data.membres[(request.auth.uid)] != null;` doesn't work while `allow read: if resource.data.membres != null;` does. – Mavireck Oct 24 '17 at 14:41
  • If you are only trying in the emulator provided, you should try it in the real application if you have one. The emulator is known to have problems with arrays. – Jack Jan 01 '19 at 08:05
  • Great answer. Works well. However it would be nice to tie it to some appropriate queries as well otherwise it won't work for collection queries. This works for the first one but I couldn't get a query working with the 2nd method. this.db.collection('collection', ref => ref .where('members', 'array-contains', uid) ).get(); – MadMac Dec 07 '19 at 03:50
  • Also keep in mind that the first option only works if you are accessing that particular resource. @calebeaires's answer above will work on any resource, but requires an extra read. – MadMac Dec 07 '19 at 04:06
47

I made it happen with this code

Allow some user to read/write some document of a collection if this same user is present into an array of another collection

service cloud.firestore {
  match /databases/{database}/documents {
    match /repositories/{accountId} {
      allow read, write: if request.auth.uid in get(/databases/$(database)/documents/accounts/$(accountId)).data.users
    }
  }
}
calebeaires
  • 1,972
  • 1
  • 22
  • 34
4

Just offering an alternative solution. In my case I store two separate fields. In your case it would be:

"membersSummary":[
  {"email":"eee@ff.com","id":"aaaaaaaa","name":"Mavireck"}, 
  {"email":"eee2@ff.com","id":"bbbbbbbb","name":"Mavireck2"}, 
],
"members": ["aaaaaaaa", "bbbbbbbb"]

I'm aware that this is not necessarily optimal but as we're using firebase I assume we're ok with using denormalised data in our documents.

I'd use the members field for collection queries and firestore rules (allow read: if request.auth.uid in resource.data.members; as per Mike's answer above), and the membersSummary for rendering the info in the UI or using the additional fields for other types of processing.

If you use uids as keys then if you wanted to query a collection and list all the documents for which that user is a member, and order them by name, then firebase would need a separate composite index for each uid, which unless you have a fixed set of users (highly unlikely) would basically result in your app breaking.

I really don't like the idea of extra document reads just for access control but if you prefer that approach to tracking two separate related fields then do that. There's no perfect solution - just offering another possibility with its own pros and cons.

Daniel Storey
  • 825
  • 4
  • 8