How to protect email addresses but make them searchable with Firestore - firebase

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

Related

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.

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

How to set firebase security rules in order to allow only update of certain fields in document?

I would like to know how I can achieve that my user
that has the following fields: uid, friends, notifications, name, username.
Currently my security rules look like this for the user folder
function signedIn() {
return request.auth.uid != null;
}
match /users/{user} {
allow read, update, write: if signedIn();
}
So how can I make a rule for update: "condition",
so that only friends and notifications are updateable,
but not username, uid or name. Any ideas ?
You're looking for map diffs.
For example:
request.resource.data.diff(resource.data).affectedKeys()
.difference("username", "uid", "name"].toSet()).size() === 0;
Or shorter:
!request.resource.data.diff(resource.data).affectedKeys()
.hasAny("username", "uid", "name"];
Also see:
The documentation on map diff operations
The documentation o controlling field access.

Firebase Firestore security rules: Using queries in security rules?

I am developing a web app on Firebase and Firestore, and here is what I am trying to do:
Users can upload their own posts, and set its visibility to public or private.
Any signed in user can see public posts, but only the users who are subscribing to the writer can view private posts. I am trying to write security rules for this.
Here is the database structure:
db.collection('posts').doc({postid})
//When a user writes a post, a new document is created. Includes the boolean 'public' field and the 'uid' field, which stores the writer's uid.
db.collection('subscription').doc({viewer_user_id})
//Once the logged-in user subscribes to another user, a document is created under the user's UID. The doc includes array of the UID of the users the viewer is subscribing.
Here are the descriptions of the relevant fields under each of the docs in 'posts' and 'subscription' collections:
posts/{postid}: {
'uid': Writer's uid (String)
'public': Boolean value to reflect visibility. If false, it is private.
}
subscription/{viewer's uid}: {
'subscription': Array of uids of the users the viewer is subscribing.
}
So, for the private documents, the basic idea is to look at the viewer's document in the subscription collection, get the data, and check whether the uid of the writer is included there. This will require some Javascript codes, but I don't know what the syntax would be in Firestore security rules, or is possible or not there to begin with.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
function publicOrSubscribed() {
return request.auth.uid!=null&&(resource.data.public==true|| /*What could be the syntax here??*/)
}
match /posts/{postid} {
allow read: if publicOrSubscribed();
allow create: if request.auth.uid!=null;
allow update, delete: if request.auth.uid==resource.data.userid;
}
}
}
Any suggestions? If it is not possible in security rules, what might be the workaround? Thanks in advance!

How to get documents which is readable by user on Firestore Cloud depends on rules

I have a basic rule on my Firestore Cloud database. I want to give user cafes that user is in but i am always getting the error "Missing or insufficient permissions.". What is the thing that I miss?
service cloud.firestore {
match /databases/{db}/documents {
match /cafes{
allow read: if request.auth.uid != null;
match /{cafe}{
allow read: if get(/databases/$(db)/documents/cafes/$(cafe)/participants/$(request.auth.uid)) != null;
}
}
}
}
here is the code that i use to reach to documents
export class MyApp {
private rootPage:any;
// rootPage:any = HomePage;
constructor(platform: Platform, statusBar: StatusBar, splashScreen: SplashScreen, afAuth: AngularFireAuth, afs: AngularFirestore) {
platform.ready().then(() => {
statusBar.styleDefault();
splashScreen.hide();
afAuth.auth.onIdTokenChanged(user=>{
if(user){
console.log(user.uid)
afs.collection('cafes').valueChanges().subscribe(console.log)
} else {
console.log('logged out')
}
})
});
}
}
One of the key sentences from the docs on security rules is
Every database request from a Cloud Firestore mobile/web client
library is evaluated against your security rules before reading or
writing any data.
and this should answer your question: You cannot query documents by their visibility for a specific user because this would require Firestore to read and evaluate every document.
If you read this rule
match /{cafe}{
allow read: if get(/databases/$(db)/documents/cafes/$(cafe)/participants/$(request.auth.uid)) != null;
}
as "Allow every user to view a cafe document if it contains his user id in the participants list" it becomes clear that Firestore needs to read the document to answer the question.
What you can try is allow read on all cafe documents and then query for those "the user is in" with the implication that users can read the other cafes as well.
You should take a look at this official guide for a concept on role based access on Firestore documents.
Update
I want to be more precise on the statement "You cannot query documents by their visibility for a specific user because this would require Firestore to read and evaluate every document."
You cannot query for all documents and expect to get those visible to the requesting user (like you do in afs.collection('cafes'), i. e. security rules are not filters.
What you can do is qualify (add a where clause) your query to only include the documents visible to the user. Firestore will match the qualified query against your security rules and if both match, fetch the desired results.

Resources