Cloud Firestore Custom Claims when null - firebase

I am blocking users by adding to their token using cloud functions
exports.blockUser = functions.https.onCall(async(data, context) => {
const user = await admin.auth().getUserByEmail(data['email']);
if(context.auth.token.admin){
admin.auth().setCustomUserClaims(user.uid, {
block: true
});
console.log(data['email'] + " has been blocked");
return 1;
}else{
return 2;
}});
In my rules i have set to allow read if block == null, because users that are not blocked will not have that data on their token.
allow read: if request.auth.token.block == null;
However this does not work and permission is denied.
I have tried the other way round just to ensure that the token data exist
allow read: if request.auth.token.block == true;
This allowed only block users to be able to read data. and it work. which means there is no issue with the data on the token.
What can i do to allow users which dont have the "block" property on their token to be able to read data?

With security rules, a missing property is not the same as the property equating to null. You should instead check to see if the block property actually exists, and also check if its value should restrict access
allow read: if !("block" in request.auth.token) || equest.auth.token.block == false;
Reference the documentation for Map (request.auth.token is a Map).

Related

Why my rule in firebase database is not working?

I'm trying to add a rule that automatically merges two users if the user already exist with the same email and just keep one of them with new user newest data.
match /users/{userId} {
allow create: if request.resource.data.email != null;
allow update: if request.resource.data.email != null && request.auth.uid == userId;
function isDuplicateEmail() {
return get(/databases/$(database)/documents/users/$(request.resource.data.email)).exists;
}
function mergeUsers(userId) {
// Get the data of the new user
let newUser = get(/databases/$(database)/documents/users/$(userId)).data;
// Get the data of the existing user
let existingUser = get(/databases/$(database)/documents/users/$(newUser.email)).data;
// Merge the data from the two users
let mergedData = {...existingUser, ...newUser};
// Update the data of the existing user
return update(/databases/$(database)/documents/users/$(newUser.email), mergedData);
}
allow create: if !isDuplicateEmail()
allow create: if isDuplicateEmail() && mergeUsers(userId);
}
But I'm seeing an error in the rule editor: "Unexpected "}". Line 40:
let mergedData = {...existingUser, ...newUser};
What I'm missing?
Thanks.
The security rules expression language does not support the ... spread operator like JavaScript. In fact, it is not JavaScript at all - it just looks a bit like JS. You might want to read about its syntax in the documentation.
On top of that, there is no function called update. You can't modify data in security rules at all. You can only check to see if the incoming access should be allowed or denied. If you want to modify document data, you will have to write application or backend code for that.
The } is closing the match statement before the allow create statement that uses the mergeUsers() function. Try:
match /users/{userId} {
allow create: if request.resource.data.email != null;
allow update: if request.resource.data.email != null && request.auth.uid == userId;
function isDuplicateEmail() {
return get(/databases/$(database)/documents/users/$(request.resource.data.email)).exists;
}
function mergeUsers(userId) {
// Get the data of the new user
let newUser = get(/databases/$(database)/documents/users/$(userId)).data;
// Get the data of the existing user
let existingUser = get(/databases/$(database)/documents/users/$(newUser.email)).data;
// Merge the data from the two users
let mergedData = {...existingUser, ...newUser};
// Update the data of the existing user
return update(/databases/$(database)/documents/users/$(newUser.email), mergedData);
}
allow create: if !isDuplicateEmail()
allow create: if isDuplicateEmail() && mergeUsers(userId);
}
Also, if you going to use the update function, you also need to include a rule allowing the update to happen.

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

Firestore role based security roles not working properly

Hello I need to configure firestore security roles for my ionic app as shown in here. But when it applies to my firestore it always say permission denied.
If i use just WRITE:TRUE its working.
Here is my firestore db
and Here how i defined security roles according to myone
match /tournaments/{tournamentsID} {
allow read:if isLoggedIn();
allow write:if getUserData().roles.keys().hasAny(['subscriber']);
}
function getUserData(){
return get(/databases/$(database)/documents/users/$(request.auth.uid)).data
}
Now I edited it as
allow write:if get(/databases/$(database)/documents/users/$(request.auth.uid)).data.roles.subscriber == true
Then its working.
But if i try same thing using a function as following
function getRole(role) {
return get(/databases/$(database)/documents/users/$(request.auth.uid)).data.roles[role]
}
and calling like
allow write:if getRole('subscriber') == true;
Its not working

Firestore Rules: validate data does not have field

So I currently have two roles for all users: isAdmin and isReader.
An Admin is allowed to read and write data and an Reader is allowed to read data.
When someone creates an account he has no rights. Not even isReader. Only an Admin can change rules.
This is how I planned to do it:
Once someone creates an account I create an Document in the Users Collection like this:
uid: user.uid,
email: user.email,
role: {
isAdmin: false,
isReader: false,
}
On each login I update 'email' and uid but keep role untouched. To secure this behaviour I have these rules:
match /Users/{userId} {
allow read: if isOwner(userId) || isAdmin();
allow create: if request.resource.data.hasAll(['uid', 'email', 'role']) && request.resource.data.role.isAdmin == false && request.resource.data.role.isReader == false;
allow update: if resource.data.role == null || isAdmin();
}
function isAdmin() {
return getUserData().role.isAdmin == true;
}
I think I have 2 errors:
for some reason the data.hasAll(['uid', 'email', 'role']) does not work. When I remove this part the create rule works as planned.
resource.data.role == null does not work. I intend to check if the data contains any updates for role because I can't allow it is it doesn't come from an Admin. But for some reason it does not work.
Any Ideas what I'm doing wrong? Also is my strategy save or is there a way someone could "hack" himself Reader or Admin rights?
This looks like it may be a good use case for custom auth claims. You can set specific roles on a user within a secured environment, as shown in this codelab. Below is an example of setting a custom claim in your server. You can even use Cloud Functions for this. I recommend you check out the full code of the Codelab so you can see how to ensure not just anyone can request custom claims be added to their user.
admin.auth().setCustomUserClaims(uid, {Admin: true}).then(() => {
// The new custom claims will propagate to the user's ID token the
// next time a new one is issued.
});
Then you can check for those roles on the user in your security rules.
service cloud.firestore {
match /databases/{database}/documents {
match /Users/{userId} {
allow read: if request.auth.token.Owner == true || request.auth.token.Admin == true;
allow create: request.auth.uid == userId &&
request.resource.data.uid == request.auth.uid &&
request.resource.data.email != null;
allow update: request.auth.uid == userId || request.auth.token.Admin == true;
}
}
}
Notice all the rules about "role" have been removed because they're no longer needed. Let me know if you have questions about implementation because I'm trying to make some more content around this since it's such a common problem.
request.resource.data.hasAll(['uid', 'email', 'role']) does not work, because request.resource.data is a Map and not a List. You should use keys() to create a List from the Map and ensure certain keys exist.
In regards to your second issue, you should just check whether there is a write to roles: allow update: if !('roles' in request.writeFields) || isAdmin();. This will ensure that any updates to roles will fail unless a user is an Admin.
About your security question; I see a couple issues. The first is anyone can create unlimited users which also means that any Admin can create unlimited other Admin accounts. To stop this from happening, I would add another section to the allow create that restricts creation to the user:
allow create: if userId == request.resource.data.uid
&& request.auth.uid == request.resource.data.uid
&& request.resource.data.hasAll(['uid', 'email', 'role'])
&& request.resource.data.role.isAdmin == false
&& request.resource.data.role.isReader == false;`
The second is anyone can change their uid and try to impersonate someone else. Obviously this doesn't change the uid associated to their Auth Token, but depending on how you write the rest of your rules, the backend security, or even the frontend display, someone could use that flaw to exploit your code or another user (potentially an Admin). You can ensure no one changes their uid by checking whether it is in the writeFields (you will also need the previous security solution to also ensure they don't impersonate during creation).
allow update: if !('uid' in request.writeFields)
&& (!('roles' in request.writeFields) || isAdmin());
I hope this helps.

Resources