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

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.

Related

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

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.

How to protect email addresses but make them searchable with Firestore

I couldn't find anything on the topic, so here goes.
I'm creating an app with a Firebase Cloud Firestore database with users.
My goal is to "prevent people from stealing all email addresses but still make them searchable"
My user data is saved per user like so:
in /users/{userId}
{
email: 'user#gmail.com',
displayName: 'James Liverstone'
}
I can protect the user data with these rules:
match /users/{userId} {
allow write, read: if request.auth.uid == userId
&& request.auth.uid != null;
}
But what if I want to make it so someone can search for a friend in my app, by email or display name?
eg.
const searchVal = 'user#gmail.com' // search value from <input>
firebase.firestore().collection('users').where('email', '==', searchVal)
This is not possible because of the read rule. However, if I open up read to allow everyone, you could steal all email addresses of my users like so:
firebase.firestore().collection('users').get()
how can I prevent people from stealing all email addresses but still make them searchable?
So in short:
allow: firebase.firestore().collection('users').where('email', '==', searchVal)
prevent: firebase.firestore().collection('users').get()
It seems you can't enforce this with security rules, so your best best would be to write a Cloud Function (http or callable) that will perform the query safely and return the desired result to the client. This function would take the email address as an input argument and minimally output some boolean that indicates if the user exists.
There is a workaround without using Cloud Functions
One workaround for this using only firestore is to create an additional collection like so:
Every time a user is created, set an empty document with email address as the key:
const email = 'user#gmail.com' //get the email of the new user
firestore().doc(`searchUsers/${email}`).set({})
This way we have a collection called searchUsers with a bunch of empty documents with the email address as key.
Required security rules:
Prevent users from getting all these emails
with .collection('searchUsers').get()
Allow checking the existence for a single email address
with .doc('searchUsers/user#gmail.com').get()
Set the security rules like so:
match /searchUsers/{value} {
allow create: if request.auth != null
&& value == request.auth.token.email;
allow list: if false;
allow get;
}
These security rules explained:
allow create rule: "only allow users to create a doc with their own email address"
allow list rule: "Prevent users from getting all these emails"
with .collection('searchUsers').get()
allow get rule: "you can query for a single doc with the email as key to check existence"
with .doc('searchUsers/user#gmail.com').get()
In practice
You will have a search form <input> and target this to execute:
const searchVal = 'user#gmail.com' // search value from <input>
const docRef = await firestore().doc(`searchUsers/${searchVal}`).get()
const userExists = docRef.exists

Resources