How to check if users owns the object in Firebase bolt compiler - firebase

Supposing I have a simple schema with two collections users & posts. Each post object has a key, value pair (ownerId:userId) to find out which users owns the posts object.
users/{1,2,3...}
posts/{a,b,c...}/ownerId:userId
I am trying to write the rules where user can only read/write his user data and his posts.
For this the bolt rules for the user is quite straightforward:
isUser(uid) = auth != null && auth.uid == uid;
path /users/$uid {
read() = isUser($uid);
write() = isUser($uid);
}
My Question is how can I secure the posts collection to be only accessed by the user. Can I check the ownderId property of the posts collection in rules? If so how, If not they how can I structure my schema to do it?
EDIT
I am trying to secure the posts path like this:
path /posts/$pid {
read() = isUser(this.ownerId);
write() = isUser(this.ownerId);
}
Is this the correct way to do this?

We can do this by adding a owner Id property to the collection and then checking if the user is authenticated.
path /posts/$pid {
read() = isUser(this.ownerId);
write() = isUser(this.ownerId);
}

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

Cloud Firestore Custom Claims when null

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

How do you keep your custom claims in sync with roles stored in Firebase database

This question is lifted from a comment on another thread and I figued that it was a good question that yet didn't have any answer on SO.
How would i link my users in auth, to their specific profiles in the database? (profile contains, full name, usertype, etc.)
The context of the question is referring to the different strategies of storing user auth access logic in the database and/or as custom claims. And how do you maintain those two to be in sync (Is there any function in User Authentication for User Roles?).
Custom claims must be set with the admin SDK, which already hints that cloud functions can be utilized to set claims according to your logic.
If the user profile data contains the user type and roles, you can create a trigger for updates on the user object. If you store your roles as an array you can implement a function like this.
functions.firestore.document('users/{uid}').onUpdate((change, context) => {
const uid = context.params.uid;
const userBefore = change.before.data();
const userAfter = change.after.data();
const hasDifferentNumberOfRoles = userBefore.roles.length !== userAfter.roles.length;
const hasOtherRoles = userBefore.roles.some((role: string) => userAfter.roles.indexOf(role) === -1);
if (hasDifferentNumberOfRoles || hasOtherRoles) {
return admin.auth().setCustomUserClaims(uid, { appRoles: userAfter.roles});
}
return null;
});
This ensures that every time the user roles in data base change, your custom claims auth will also change.
Arrays are convenient for roles since it's easy to query Firestore to find users with certain roles using array-contains and it's also easy to check roles in your database rules as a list.
service cloud.firestore {
match /databases/{database}/documents {
match /protected-content/{documentId} {
allow read: if hasUserRole('admin');
allow write: if hasUserRole('super-admin');
}
function hasUserRole(role) {
return role in request.auth.token.appRoles;
}
}
}
A small side-note about storing your roles in a appRoles attribute on the custom claims, is that it makes in conveniently easy to remove all current roles if you change your structure or want to replace all roles for a user.

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

How to hide information in Firebase?

I have these requirements:
User will log in via email and multiple Oauth providers. I use function like User.findByEmail('1#1.com'). So I need to have permision to see the list of users before being authenticated.
User's email address, geolocation and age should be kept secretly from other users.
My first plan was:
users:{
$user-id:{
// public
".read" : true,
name,
created_at,
private: {
".read" : "auth.email === data.child('email').val()",
age,
email,
geolocation,
}
$piority = email
}
}
Then I realized no, it just doesn't work that way. So would anyone please tell me how to do it correctly? Thanks in advance.
P.S. Firebase really needs a filter/serialize method.
There are really several questions in this post. The first is how to store private data. One simple change you can make is to invert the public/private and user keys--they don't have to be nested in the same user record.
/users/$user_id/...public data...
/private/$user_id/...private data...
This makes securing the data quite a bit simpler.
Another question is regarding duplicate email detection. If we assume you are using simple login here, this is all pretty moot. You can check to see if an email address exists by simply trying to create the account. An error will be returned if the email is already registered.
If that doesn't solve it, you can still check this manually, without providing a list of users' emails. This is typically done with an index. When a new account is created, write something like this:
/email_index/$escaped_email/$userid ($userid is the value)
Now when you want to check if the email is available, you do a read like this:
var ref = new Firebase(URL);
function checkEmail(emailAddress, callback) {
ref.child('email_index/'+escapeEmail(emailAddress)).once('value', function(snap) {
callback(snap.val() !== null);
});
}
function escapeEmail(emailAddress) {
return (email || '').replace('.', ',');
}
checkEmail(EMAIL_ADDRESS, function(exists) {
console.log(EMAIL_ADDRESS + (exists? 'does' : 'does not') + ' exist!');
});
To prevent someone from listing the emails, you do something like this in your (amazingly flexible and really quite sophisticated, even for enterprise apps) security rules:
"email_index": {
// no .read rule here means that the data cannot be listed; I have to know the email address to check it
"$email_address": {
".read": true,
// it can only be claimed once and the value must be my user id
".write": "auth.uid === newData.val() && !data.exists()"
}
}

Resources