Firestore Security rules match array [duplicate] - firebase

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 : {} }

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
}
}
}
}

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
}
}
}

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.

Related

Firestore security rule to cover parent documents and nested collection based on array field

I've got a top-level collection of documents, each of which has an array field containing the email addresses of the users that are permitted to view the document in question (there's a reason in this case that it's the email addresses, not the UIDs).
Additionally, I have a custom claim set up on some users that mark them as admin who should have read/write access to everything.
The following security rule works well for the top level:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /accounts/{account} {
function allowRead() {
return resource.data.users.hasAny([request.auth.token.email]);
}
allow read: if allowRead();
allow read, write: if request.auth.token.admin == true;
}
}
}
I want the rule to check the 'users' array field when requesting documents in a collectionGroup query across nested collections called 'projects' and block access if a nested document is requested for which there is no match to the 'users' array field in the parent.
I get that the allowRead() funtion won't work as is, even if I add a recursive wildcard after match /accounts/{account} like /accounts/{account}/{document=**} since the definition of resource.data will be different at that level and won't return true for the allowRead() function.
I know that Firestore rules don't really support an operation like 'check foo in parent' when querying, so I was hoping for some advice on the best way to approach this requirement.
The collectionGroup query to the nested collections is
this.firestore.collectionGroup('projects', (ref) => ref.where('accountId','in',accountIds)).get()
in which 'accountIds' is a given array of the ids of the parent account, which is stored in each document in the projects collection for ease of querying with collectionGroup.
I don't really want to move the nested collection to top level because then I'd have to store the users array in each document as well as the reference to the account to which it belongs, but perhaps that's the best way to do this?
Or am I thinking about this whole thing wrong? I'm fairly new to Firebase.
I've solved this with the following:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /accounts/{account} {
function allowRead() {
return resource.data.users.hasAny([request.auth.token.email]);
}
allow read,write: if request.auth.token.admin == true;
allow read: if allowRead();
}
match /{path=**}/projects/{project} {
allow read,write: if request.auth.token.admin == true;
allow read: if get(/databases/$(database)/documents/accounts/$(resource.data.accountId)).data.users.hasAny([request.auth.token.email]);
}
}
}
The only issue is that as far as I can tell, this is going to double the amount of billable queries because there's an extra 'get' made any time the nested collection is queried.
Any thoughts from the community on better ways to do this would be welcome!

Firestore data with multiple variables in document name

I'm creating an app where users have their own calendars, and those calendars have events.
I've created a firestore document called 'days', my intention was to have the structure of
- days
- {user1}-{date}
- events: [{array of events during the day}]
- {user2-{date}
...
In the rules, only the owner should be able to view their event data.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /users/{userId}/{documents=**} {
allow read, write: if isOwner(userId)
}
match /day/{userId}-{date}/{documents=**} {
allow read, write: if isOwner(userId)
}
}
function isOwner(userId) {
return request.auth.uid == userId;
}
}
I've tried multiple structures of creating a regex to get {userId}-{date} but haven't got anything that is working.
Is this possible with Firebase?
Am I doing this structurally wrong? Should I be using sub-collections?
Firestore security rules don't allow any type of regex on the path of a document. A rule can only match 0 or more entire path segments using a wildcard. You will need to find a different way to structure your data.
The names of collections are not themselves meant to contain multiple items of data. In fact, it's better if they don't contain any data at all (excepting things like UID, whose specific values need to be used in security rules). I suggest simply moving the date to a field in the document for the day collection, or using a subcollection to group things under a parent document.

Firestore Match Rules for looking up data in a document that is in another collection

I am having an issue with Firestore rules when the permission is stored in another document in another collection. I haven't been able to find any examples of this, but I have read that it can be done.
I want to do it this way to avoid having to do a lot of writes when a student shares his homework list with many other students. Yes, I know this counts as another read.
I have 3 collections, users, permissions, and homework along with some sample data.
users
{
id: fK3ddutEpD2qQqRMXNW,
name: "Steven Smith"
},
{
id: YI2Fx656kkkk25,
name: "Becky Kinsley"
},
{
id: CAFDSDFR244bb,
name: "Tonya Benz"
}
permissions
{
id: fK3ddutEpD2qQqRMXNW,
followers: [YI2Fx656kkkk25,CAFDSDFR244bb]
}
homework
{
id: adfsajkfsk4444,
owner: fK3ddutEpD2qQqRMXNW,
name: "Math Homework",
isDone: false
}
The start of my firestore rules:
service cloud.firestore {
//lock down the entire firestore then open rules up.
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if false;
}
match /homework/{ } {
allow get: if isSignedIn()
}
// helper functions
function isSignedIn() {
return request.auth != null;
}
function isUser(userId) {
return request.auth.uid == userId;
}
function isOwner(userId) {
return request.auth.uid == resource.data.uid;
}
}
}
Use case:
Steven Smith Shared his homework list with Tonya Benz.
Tonya Benz is logged into the app to view her friend Steven's homework. The app runs this query to get the homework list of Steven Smith.
var homeworkRef = db.collection("homework");
var query = homeworkRef.where("owner", "==", "fK3ddutEpD2qQqRMXNW");
Question:
What is the proper Firestore match rule that takes the "owner" field from the homework collection to look up it up as the id in the permissions collection when the user Tonya Benz is signed in so this query can run.
With your current query and database structure, you won't be able to achieve your goal using security rules.
Firstly, it sounds like you're expecting to be able to filter the results of the query based on the contents of another document. Security rules can't act as query filters. All the documents matched by the query must be granted read access by security rules, or the entire query is denied. You will need to come up with a query that is specific about which documents should be allowed access. Unfortunately, there is no single query that can do this with your current structure, because that would require a sort of "join" between permissions and homework. But Firestore (like all NoSQL databases), do not support joins.
You will need to model your data in such a way that is compatible with rules. You have one option that I can think of.
You could store the list users who should have read have access to a particular document in homework, within that same document, represented as a list field. The query could specify a filter based on the user's uid presence in that list field. And the rule could specify that read access only be granted to users whose IDs are present in that list.
{
id: adfsajkfsk4444,
owner: fK3ddutEpD2qQqRMXNW,
name: "Math Homework",
isDone: false,
readers: [ 'list', 'of', 'userids' ] // filter against this list field
}
The bottom line here is that you'll need to satisfy these two requirements:
Your query needs to be specific about exactly which documents that it expects to be readable. You can't use a rule to filter the results.
Your rule needs a way to determine, using nothing more complicated than the fields of the document itself, or a get() on other known documents, what the access should be for the current uid.

Recursive wildcards in Firestore security rules not working as expected

I have a data structure like this (Collections and Documents rather than JSON of course but you get the idea):
{
users: {
user1:{
name: Alice,
groups: {
groupA:{subbed:true},
groupB:{subbed:true}
}
},
user2:{
name: Bob,
groups: {
groupC:{subbed:true},
groupD:{subbed:true}
}
}
}
}
Basically this is registered users IDs and the group IDs that each user is subscribed to. I wanted to write a security rule allowing access to a users profile and sub-collections only if they are the current auth user and, based on my reading of the docs, I thought that a wildcard would achieve this...
match /users/{user=**}{
allow read,write: if user == request.auth.uid;
}
With this in place I can read the user document fine but I get a permissions error when I try and read the groups sub-collection. I can only make it work by matching the sub-collection explicitly...
match /appUsers/{user}{
allow read,write: if user == request.auth.uid;
match /groups/{group}{
allow read,write: if user == request.auth.uid;
}
}
...so my question is, what is the difference between the two examples and what am I misunderstanding about the recursive wildcards? I thought that the {user=**} part of the first example should grant access to the user document and all its sub-collections, sub-sub-collections etc etc ad infinitum (for the authorised user) and should remove the need to write rules specifically for data stored lower down as I have had to do in the second example.
I've only been messing around with Firestore for a short time so this could be a real dumb question :)
Thanks all
The firebase docs are a bit confusing when it comes to using the recursive while card. What I found in testing was that I needed to set two rules to give a user permission to write to the users document and all sub collections (and their sub documents) which is the most logical setup for managing user data.
You must set two rules.
Give user permission to the /users/{userId} document
Give user permission to all sub collections and their sub documents that begin at the /users/{userId} path.
service cloud.firestore {
match /databases/{database}/documents {
match /users/{userId} {
allow read, write: if request.auth.uid == userId;
}
match /users/{userId}/{document=**} {
allow read, write: if request.auth.uid == userId;
}
}
}
Rules
Sorry about including the images. I couldn't get SO to format them correctly.
I think the problem is that, while you are indeed using the subcollections wildcard =**, you are then allowing permissions only if user == request.auth.uid, so this is what happens (pseudocode):
(when accessing users/aHt3vGtyggD5fgGHJ)
user = 'aHt3vGtyggD5fgGHJ'
user == request.auth.uid? Yes
allow access
(when accessing users/aHt3vGtyggD5fgGHJ/groups/h1s5GDS53)
user = 'aHt3vGtyggD5fgGHJ/groups/h1s5GDS53'
user == request.auth.uid? No
deny access
You have two options: either you do as you've done and explicitly match the subcollection, or use this:
function checkAuthorization(usr) {
return usr.split('/')[0] == request.auth.uid;
}
match /users/{user=**}{
allow read,write: if checkAuthorization(user);
}
(the function must be inside your match /databases/{database}/documents, like your rule)
Let me know if this works :)
Security rules now has version 2.
match/cities/{city}/{document=**} matches documents in any
subcollections as well as documents in the cities collection.
You must opt-in to version 2 by adding rules_version = '2'; at the top
of your security rules.
Recursive wildcards (version 2).
This is what works for me:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Matches any document in the cities collection as well as any document
// in a subcollection.
match /cities/{city}/{document=**} {
allow read, write: if <condition>;
}
}
}

Firestore security rules : searching for a user's id in array in a document

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 : {} }
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
}
}
}
}
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
}
}
}
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.

Resources