Firebase Firestone rules with collection group ressource data - firebase

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).

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

Can a user read a collection of users in firestore from frontend?

I am saving the below Data in the user's collection in firebase
{
"uid":"randomid",
"name":"name",
"number":"1234"
}
when I try to check if the user exists the below code works ok
const result = await firestore().collection('users').where('uid', '==', userid).get()
so can an authenticated user read the whole users' collections?
const result = await firestore().collection('users').get()
What security rules I can write to prevent users from reading a collection but only reading their info based on uid?
In security rules you can split the read access to get and list. So if you want the give access to each user to get only his own data you need to use the following rule (I assume each user document in the collection is the uid of this user):
match /users/{user} {
function isUserOwner() {
return request.auth.uid == user
}
allow get: if isUserOwner();
allow list: if false;
}
First you need to set the uid field to the UID of the user who created the document.
To get the current user id See documentation
const uid = user.uid;
To add the currently logged in User id as a field visit stack overflow example link for javascript
After adding UID you can use request.auth and resource.data variables to restrict read and write access for each document to the respective users. Consider a database that contains a collection of story documents. Have a look at below example
{
title: "A Great Story",
content: "Once upon a time...",
author: "some_auth_id",
published: false
}
You can use below security rule to restrict read and write access for each story to its author:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /users/{storyid} {
// Only the authenticated user who authored the document can read or write
allow read, write: if request.auth != null && request.auth.uid == resource.data.author;
}
}
}
Note that the below query will fail for the above rule even if the current user actually is the author of every story document. The reason for this behavior is that when Cloud Firestore applies your security rules, it evaluates the query against its potential result set, not against the actual properties of documents in your database
// This query will fail
db.collection("stories").get()
The appropriate query for the above rule is
// This query will work
var user = firebase.auth().currentUser;
db.collection("stories").where("author", "==", user.uid).get()
For additional information on the above rules and query see official documentation

Firebase fetches role-based documents

If I have a collection in firebase with several documents inside and each document has a field called for example "role", is it possible to show a user only the documents that belong to him when the whole collection is fetched?
Yes. You can do this.
Let's assume the user has the role 'student', you can fetch all documents .where('role', isEqualTo: 'student').
NB: role could also be the userId (and you can fetch all documents where role is userId).
You can use the following firebase rules to prevent unauthorised
access.
match /document/{docs=**} {
allow write, read: if isAllowed();
}
function isAllowed() {
// request.resource.data is the data being fetched.
return request.resource.data.role == 'student';
}
function userRole() {
// returns the user role. Use this if the user role is stored in a document.
return get(/databases/$(database)/documents/users/$(userId)).data.role;
}
NB: your query must contain .where('role', isEqualTo: 'student') if not it will fail with insufficient permissions.

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.

Firebase rules acting very strange

Hello to everyone reading this.
I am coding a flutter app for an hospital, that has this db structure.
I am having an issue fetching sessions data, exactly the following document.
Using the following method to get the lastSession a therapist made, using his therapistUID as the filtering field.
Future<Session> getLastSession() async {
Query query;
query = Firestore.instance
.collection("sessions")
.where("therapistUID",
isEqualTo: this.uid)
.orderBy("date", descending: true)
.limit(1); //this.uid = auth uid of current therapist.
try {
QuerySnapshot querySnapshot = await query.getDocuments(); //exception thrown here
if (querySnapshot.documents.isEmpty) {
throw Exception("Empty query");
} else {
lastSession = Session.fromDocument(querySnapshot.documents[0]);
return lastSession;
}
} catch (e) {
throw Exception("cannot get data from database");
}}
with the following rules
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /patients/{document=**} {
allow read,write,list: if checkPatientAccess(resource.data);
}
match /therapists/{document=**} {
allow read,write,list: if checkOwnership();
}
match /sessions/{document=**} {
allow read, write,list: if checkPatientAccess(get(/databases/$(database)/documents/patients/$(resource.data.patientUID)).data);
}
match /devices/{document=**} {
allow read, write,list: if false;
}
match /clinics/{document=**} {
allow read, write,list: if false;
}}}
function checkOwnership(){
return resource.id == request.auth.uid;
}
function checkPatientAccess(patient){
return request.auth.uid in patient.therapistUIDs;
}
Code is throwing this exception
Does anyone know why is it rejecting the query? keep in mind query is only one document, and as well there is only one document in the database that could fit those filters. Using testlab with same parameters works.
Firebase security rules do not on their own filter data, as that would not scale. This becomes clear when we look at:
match /sessions/{document=**} {
allow read, write,list: if checkPatientAccess(get(/databases/$(database)/documents/patients/$(resource.data.patientUID)).data);
}
function checkPatientAccess(patient){
return request.auth.uid in patient.therapistUIDs;
}
In order to secure your read operation, these rules would have to load each document and check the therapistUIDs value in there. This would be an O(n) operation, while Firestore is guaranteed to return results on O(1). For this reason, such security rules don't work.
Your rules do work for reading a single document, but not for the list operation.
If you can come with with a query that returns the data that you want, you may be able to secure that query. But since Firestore doesn't support any type of join in queries, you'd need to replicate the data you want to filter on from the patient document into each session document in order to make this work.
As discussed in the comments: Since your query ensures all documents have the same patientUID, the get() call in your rules is guaranteed to always get the same document, and thus the rules engine can guarantee that it will never return an authorized document for the query.
Pretty nifty actually.
It doesn't matter how many documents you request - Firestore security rules will not act as a filter on those documents. Please read and understand this documentation. It won't let you conditionally check something for each document to determine if it can be read. Your rules are trying to express that something must exist in a matching patient document for each session read, but that's not allowed. It simply will not scale the way that Firestore requires, and would be extremely costly for queries with large result sets.

Resources