Firebase rules variable not matching as string - firebase

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.

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

Find document ID if the query know two of his fields, firestore rules

I'm trying my best to set up a functionality in my application that will allow people who know a group's login and password to join it.
Each group has a document in the "Groups" collection, and each user has a document in the "Users" collection.
To keep the id and the password information, I have another collection named "AuthGroups", containing as many documents as there are groups, with two fields: "login" and "password". Each auth document has the same ID as the corresponding document the Groups collection.
So, here is my strategy:
When the user valid the login and password, a first query is sent to the database, to find a document with theses credentials in the "AuthGroups" collection.
If a document is found, its ID is used to do another query in the "Groups" collection to retrieve the group's data.
Queries could look like this:
var ID = await firestore.collection('AuthGroups')
.where('login', isEqualTo: login)
.where('password', isEqualTo: password)
.get()
.then((value) {
return value.docs.first.id;
});
var groupName = await firestore.collection('Groups')
.doc(id)
.get()
.then((value) {
return value.get('name');
});
Now, let's speak about firestore rules to make it secure...
To prevent someone malicious from seeing all documents in my "AuthGroup" collection. I told myself that my rules need to only allow queries containing both "login" and "password" fields. But I don't know how to do it right, and if it's even possible...
Same thing for the documents in the "Groups" collection: users can only get a document if they know its ID.
A solution could be to name my documents in my "AuthGroup" collection like "login + password", and store the group's ID in it. And in my rules, allow only list requests like that:
service cloud.firestore {
match /databases/{database}/documents {
match /AuthGroup/{organization} {
allow list: if request.auth != null;
}
}
}
I told myself that my rules need to only allow queries containing both "login" and "password" fields. But I don't know how to do it right, and if it's even possible.
It's not possible. You can't check for specific query parameters in security rules.
A solution could be to name my documents in my "AuthGroup" collection like "login + password", and store the group's ID in it. And in my rules, allow only list requests like that
Yes, that is a possible solution. Or you can hash a concatenation of login and password strings so you don't expose them in the document id or exceed the max length of that ID.

Firebase Firestone rules with collection group ressource data

I want to delete all students in my Firestore database, to do this I used collection group but I had a problem with rules: I can't achieve to authorize read, delete & update permissions.
Code
Here is the dart code in Flutter to retrieve all students in any nested collections AND delete them:
FirebaseFirestore.instance
.collectionGroup('students')
.where('studentId', isEqualTo: studentId)
.get()
.then((querySnapshot) async {
for (var snapshot in querySnapshot.docs) {
await snapshot.reference.delete();
}
}
});
Rules
The rules I used but doesn't work because It seems resource.data.classId can't be accessed...
function isClassBelongToUser(classId) {
return classId in get(/databases/$(database)/documents/users/$(request.auth.uid)).data.classIds
}
match /{path=**}/students/{id} {
allow read, delete, update: if isSignedIn() && isClassBelongToUser(resource.data.classId); // TODO: resource.data.classId seems to not work
}
My database
classes / CLASS_ID / (students: collection, name: string, ...)
users / USER_ID / (classIds: array, firstName: string, ...)
Security rules don't filter data, but instead merely ensure that the operation you perform is authorized. See the documentation on rules are not filters.
Since your isClassBelongToUser check requires that the user exists in the classIds of a specific document, your query must ensure this condition is satisfied too. Since Firestore can only filter on values in the documents it returns, such a condition is unfortunately not possible.
You will have to adapt your data model to allow the use-case, for example by replicating the necessary information into the students document(s).

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

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)

Firestore security rules with reference fields

I am a bit stuck here as there is no way to debug those rules. I'd appreciate help with below rules.
I want to access:
/modules/module-id/sessions/session-id/parts/
The comparison with null in the first part of hasCompletedPrerequisiteSession() works well, the second part doesn't!
The path /modules/moduleId/sessions/sessionId/prerequisite points to a reference field.
service cloud.firestore {
match /databases/{database}/documents {
function hasCompletedPrerequisiteSession(moduleId,sessionId) {
// this part works well
return getPrerequisiteSession(moduleId,sessionId) == null ||
// !!! this part does not work !!!
hasCompleted(getPrerequisiteSession(moduleId,sessionId).id);
}
function getPrerequisiteSession(moduleId,sessionId) {
return get(/databases/$(database)/documents/modules/$(moduleId)/sessions/$(sessionId)).data.prerequisite;
}
function hasCompleted(sessionId) {
return exists(/databases/$(database)/documents/progress/$(request.auth.uid)/sessions/$(sessionId));
}
match /modules/{moduleId}/sessions/{sessionId}/parts/{partId} {
allow read: if hasCompletedPrerequisiteSession(moduleId,sessionId);
}
}
}
(If I store the session ID as a string instead of a reference to the session, it works fine.)
Edit
Questions
Reference field in security rules. Assuming modules/moduleId/owner points to a field of the type reference. What is the proper way to get the id of the referenced document?get(../modules/moduleId).data.owner.data.id or get(../modules/moduleId).data.owner or something else?
From Firebase support:
It seems that in your use case, you want to get the document name (sessionId) from the value of your reference field (prerequisite), unfortunately, this is not currently supported by Firestore security rules. I would suggest that you store only the sessionId as String on your prerequisite field, or you can also add String field for the sessionId. Keep in mind that the exists() and get() functions only allow you to check if a document exists, or retrieve the document at the given path.
It might be that around getPrerequisiteSession, after using get to pull the object by ref path, you had to use .data first before referencing the id field. Of course, id field needs to be stored as an object field.
For example, in my case I needed to allow user to add a message into a chat only if they're the owner of that chat room. There are 2 "tables" - chats and chat_messages, and chat_messages relate to a specific chat through chatId field. chats objects have ownerId field.
The rule I've used goes like this:
match /chat_messages/{itemId} {
function isOwner() {
return get(/databases/$(database)/documents/chats/$(request.resource.data.chatId)).data.ownerId == request.auth.uid;
}
allow read: if true;
allow create: if isOwner();
}

Resources