Firestore: Query a collection for all documents user has access to - firebase

Running into a situation that I'm unclear has a clean solution.
In Firestore, I have a collection in which user are only allowed to access certain documents. Users can be assigned to one or more accounts, and Accounts can have one or more user. The general models and rules work as expected:
USER: {
id : abc123,
accounts : [ xyz789, ... ]
}
ACCOUNT: {
id : xyz789,
users : [ abc123, ... ]
}
service cloud.firestore {
match /databases/{database}/documents {
match /accounts/{accountID} {
allow read, write: if accountID in get(/databases/$(database)/documents/users/$(request.auth.uid)).data.accounts;
}
}
}
From what I can tell with the Firebase Rule Simulator, the above rule is working correctly (I can read/update the accounts that list my userID, but not the ones that don't).
The issue is that if I want to get those same accounts via a Query operator, I get an error. The error does go away when I relax the ruleset, but that's not ideal.
firestore.collection('accounts').where('users', 'array-contains', userID)
ERROR: Missing or insufficient permissions
Given that the ruleset and the query seem to refer to the same records, is there a way to get them to work in conjunction or am I forced to relax the rules in order to get this to work?

I had a similar problem before and I found that Firebase doesn't check the fetched data to validate the rules, but it compare the query code with the rules, and depending on that it throws the exception
So what I found is that the if condition should have a where filter in the code
This if condition is missing a where
allow read, write: if accountID in ...
To make your code work, it would need to add a where filter that refers to accountID
firestore().collection('accounts')
.where(firestore.FieldPath.documentId(), 'in', accounts) //accounts: ['xyz789']
.where('users', 'array-contains', userID)

Related

Firebase firestore security rule for collectionGroup query

I am trying to query and filter a collectionGroup from the client doing this:
const document = doc(db, 'forums/foo');
const posts = await getDocs(
query(
collectionGroup(db, 'posts'),
orderBy(documentId()),
startAt(document.path),
endAt(document.path + '\uf8ff')
)
);
My auth custom user claims looks like this:
{ forumIds: ['foo'] }
The documentation tells me to add the following security rule:
match /{path=**}/posts/{post} {
allow read: if request.auth != null;
}
But this is a security breach as it means that anyone can read all of the posts collections. I only want the user to read the posts in its forums. Is there no better way to secure a collectionGroup query?
(1) I have tried:
match /{path=**}/posts/{post} {
allow read: if path[1] in request.auth.token.forumIds;
}
but I get this error: Variable is not bound in path template. for 'list' # L49.
(2) I have also tried:
match /{path=**}/posts/{post} {
allow read: if resource.__name__[4] in request.auth.token.forumIds;
}
but I get this error: Property __name__ is undefined on object. for 'list' # L49.
I have also tried debugging the two previous security rules with debug and both of them return true.
Based on your stated requirements, you don't want a collection group query at all. A collection group query intends to fetch all of the documents in all of the named collections. You can only filter the results based on the contents of the document like you would any other query.
Since you have a list of forums that the user should be able to read, you should just query them each individually and combine the results in the app. Security rules are not going to be able to filter them out for you because security rules are not filters.
See also:
https://medium.com/firebase-developers/what-does-it-mean-that-firestore-security-rules-are-not-filters-68ec14f3d003
https://firebase.google.com/docs/firestore/security/rules-query#rules_are_not_filters

Firebase rules variable not matching as string

so I am trying to match the user email with the collection name like below in my Firestore rules:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /users/{userEmail} {
allow read: if request.auth.token.email.matches(userEmail);
}
}
}
I am aware its not good practice to set collection ID's as emails, but please assume it to be any string here. The above does not work. however, if I replace request.auth.token.email.matches(userEmail) with request.auth.token.email.matches("myemail#gmail.com") it works fine.
Above I have a single document in my users collection with id = myemail#gmail.com, so why is it not matching when I use the userEmail variable but will match if I use "myemail#gmail.com" string?
Additional Info:
Request to /getAccountInfo you can see myemail#gmail.com as email
App code
I used Vuexfire for firestore binding.
store/index.js
bindUsers: firestoreAction(({bindFirestoreRef}) => {
return bindFirestoreRef("users", db.collection("users")
.where('email', '==', 'myemail#gmail.com');
}),
App.vue
async mounted() {
if (firebase.auth.currentUser) {
// Bind Vuexfire after if/when user exists to capture Firestore changes
await this.$store.dispatch("bindUsers");
}
}
Your query is filtering on a document property called email (not its ID):
return bindFirestoreRef("users", db.collection("users")
.where('email', '==', 'myemail#gmail.com');
This has nothing to do with the email token in the user's Firebase Auth account. You haven't shown that you have an email property in the document at all - all you have is a document with an ID that contains an email address.
Your query ultimately needs to match the rule that limits the query. This means that you need some way of explicitly filtering on the client in a way that matches the constraints of the rule. This means you're going to have to use a get() type query for the specific document with an ID, not a collection query that requires filtering with a where clause.
I could be wrong, but it looks like you are writing your rule more like a filter than as a security rule.
#DougStevenson will know much better than me, but if you hard-code a string value then Firestore can determine explicitly if that rule will succeed or fail. But if you use a variable, then I believe that Firestore determines whether the rule will return true or false in general - not specific runtime cases. In this case, the rule should return false since there will be rows that fail the test.
It almost looks like you are trying to use your rule to filter out rows. Firestore Rules don't work that way.
As Doug suggests, you should show us some client-side code you are using for accessing that collection so we can determine if the code is falling into the "rule trying to be a filter" trap.

Firestore Security rules match array [duplicate]

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.

Security Rules for Admin Users and Creators

I can't understand exactly how it work to create my rules for Firestore.
So far what I've tried with the help from the doc,
I created a document called users with a user that have my UID.
Then I created a field named admin
I've set it to true.
Now my rule look like this :
service cloud.firestore {
match /databases/{database}/documents {
match /trainings/{$tId} {
allow read, write: if get(/databases/$(database)/documents/users/$(request.auth.uid)).data.admin == true
}
}
}
I fetch my data in javascript
this._db
.collection('/trainings')
.get()
.then(itemsColl => itemsColl)
That don't work. My next step will be to add a created_by field with the UID on each of my training to get the creator uid stored and add a rule so they can read/write them training stuff.
Someone can help me getting that first rule working and tell me if my next step look in the right way ?
Thanks!

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