Firestore rules: fetch message thread by threadId in secure way - javascript

Assumptions
I'm using Firestore and I can't set the correct rule for it.
Suppose there's a chat app.
And there are threads and messages collections.
threads attributes
createdAt
userAId
userBId
messages attributes
threadId
senderId
receiverId
I set the security rule as followed since I don't want my users to see messages of other users(omitted the irrelevant part):
match /messages/{messageId} {
allow read: if isAuthenticated() &&
(request.auth.uid == resource.data.senderId || request.auth.uid == resource.data.receiverId);
}
Suppose we have only one thread and only one message of that thread saved in firestore.
Problem
When I execute the following query from my web app with authenticated user, it says insufficient permission of firestore even though the query is requesting the thread where the user is sender or receiver as required in rules.
const querySnapshot = await firebase.firestore()
.collection('messages')
.where('threadId', '==', someThreadId)
.get()
However, when I execute the following query that fetchs the message by documentId, it returns the message successfully.
const documentQuerySnapshot = await firebase.firestore()
.collection('messages')
.doc(specificMessageId)
.get()
Workaround
I'm forced to write 2 queries and I don't want since it's not effective.
const qs1 = await firebase.firestore()
.collection('messages')
.where('senderId', '==', someUserId)
.where('threadId', '==', someThreadId)
.get()
const qs2 = await firebase.firestore()
.collection('messages')
.where('receiverId', '==', someUserId)
.where('threadId', '==', someThreadId)
.get()
messages = ${merge 2 messages list above}
Quesetion
How can I fetch all messages of a thread by threadId while setting a correct security rules so that messages wouldn't be read by other users?

The key thing to realize is that Firebase's server-side security rules don't filter the data. They merely ensure that any operation your code tries to perform is authorizes. So to securely get filtered data, your code and security rules need to work together. For full details on this, see the documentation on securely querying data.
Since your rules only allow reading messages that have the correct value for senderId or receiverId, your query needs to filter for one of those fields too. Just ensuring that the threadId belongs to an allowed user isn't enough. That would require that Firestore read all documents to ensure they meet your security rules, which it can never do at scale.
So your "workaround" is actually the correct solution for this. Alternatively, you might want to consider storing each thread in a separate collection, so that you can secure access on the collection level and bypass the conditions.

Related

Firebase v9 - Get a Referentiated Document Content

I don't understand why I'm not finding this documentation anywhere.
But I have a collection called users in my Firebase Firestore project. Inside users there are three collections: companies, policies and stores.
In the collection policies I have one store field and one company field that are references to one of that user's store or company.
Ok so as far as now is fine. But now, I'm performing the next query:
const subcollectionSnapshot = await getDocs(collection(db, 'users', 'S3casIyXxdddEAaa1YJL6UjBXLy2', 'policies'));
And the response is the next one:
But now... how can I get the company and store nested documents? How can I "populate" them in order to see their information and access their fields?
Thanks.
It looks like the company and store fields are of type DocumentReference.
In that case you can get a DocumentSnapshot of each of them by calling getDoc() with the DocumentReference:
subcollectionSnapshot.docs.forEach((policyDoc) => {
const companyRef = policyDoc.data()["company"];
const companyDoc = await getDoc(companyRef);
const storeRef = policyDoc.data()["store"];
const storeDoc = await getDoc(storeRef);
...
})
If you have multiple policy documents, you will need to do this for each of them. There is no concept of a server-side join in Firestore (nor in most other NoSQL databases).

Firestore rules to allow query on the document ID and another field

I am having troubles writing Firestore rules to securely allow fetching a document by its ID and an additional field. Let's consider that I want to fetch the user with the ID USER_ID only if his deletionDate is null.
I wrote the following security rule:
/users/{userId} {
allow read: if resource.data.deletionDate == null;
}
My query with the JS client:
await db.collection('users')
.where(firebase.firestore.FieldPath.documentId(), '==', 'USER_ID')
.where('deletionDate', '==', null)
.limit(1)
.get();
It appears that this is not handled as a query on a collection. But rather like a document fetched by its ID. The Firestore simulator throws the following exception for non existing docs:
Variable read error. Variable: [resource]. for 'list'
Changing the security rule to resource == null || resource.data.deletionDate == null does not help. The thing is, I would prefer to receive an empty result if the document cannot be fetched because of absent / with deletionDate. I don't want mute the permission exception received by the clients. So I can monitor potential misconfigurations.
I noticed that the following query does exactly what I want:
await db.collection('users')
.where(firebase.firestore.FieldPath.documentId(), '!=', 'FAKE_ID')
.where(firebase.firestore.FieldPath.documentId(), '==', 'USER_ID')
.where('deletionDate', '==', null)
.limit(1)
.get();
But this looks too hacky to be used in production.
Thanks for your help!

Permissions with where clause on data field

I want the capacity for a user to read and delete records on Firebase created by someone they invited to create records.
Created records have a field 'resource.data.ownerUid', with ownerId being the uid for the user that invited others to create records.
If I try something like:
allow read, delete: if request.auth.uid == resource.data.ownerUid;
But I get permission refused at the 'get' step of:
firestore.collection('screeningResponses')
.where('screeningId', '==', screeningId)
.get()
results in
FirebaseError: Missing or insufficient permissions.
At that point I was planning on iterating through the QuerySnapshot and deleting the records, but can't get that far. Any suggestions?
Firebase security rules don't filter data on their own. Instead they merely check whether the read operation is allowed according to the rules.
For this reason your query will have to replicate what the rules require. Since your rules require that the user is the owner of the data they read, the query must do the same:
firestore.collection('screeningResponses')
.where('ownerUid', '==', firebase.auth().currentUser.uid) // 👈 this is new
.where('screeningId', '==', screeningId)
.get()

Firestore Security Rules - Same Rule Works for Write But Not for Read

I am developing a Firebase project where I am using firestore.
I am querying Firestore to get Chat Messages from the messages collection. I want only messages beloning to a certain conversation:
const messages = await db
.collection('messages')
.where('room', '==', room)
.onSnapshot(snap => {//stuff})
So fa so good. It works. Things go wrong when I set up security rules.
If I do somthing simple, such as:
allow read: if request.auth != null;
everything is fine. But if I want to allow access only to users whose uid is included in the 'partiesIDs' message object property, things go wrong:
allow read: if
request.auth.uid == resource.data.pertiesIDs[0] ||
request.auth.uid == resource.data.parties[1];
The strangest thing of all is that I have in place a very similar rule for update, which works as expected:
allow update: if
(request.resource.data.diff(resource.data).affectedKeys()
.hasOnly(['read', 'notified'])) &&
(request.auth.token.name == resource.data.parties[0] ||
request.auth.token.name == resource.data.parties[1]);
The query for the second rule (which works) looks like this:
const update = await db
.collection('messages')
.doc(docid)
.update({read: true, notified: true})
I m stuck! Can anybody shed some light into this mistery?
The problem is that Firestore security rules are not filters. I strongly suggest reading that documentation.
When you write rules to place conditions on the reading of documents within a collection, the client is obliged to make a query that matches exactly the conditions of the rules. When you make a requirement that some data must exist in some field, then your query must match that by filtering for only documents that would satisfy the contents of the fields required by the rules. The rules will not extract only the matching documents to return them. You can think of the client query as demanding the full set of documents that match the given filters, and the rules as rejecting that demand because the conditions are not satisfied.
However, you have a bit of a problem here, because queries don't have a way of specifying array indexes. It's not possible to make a query that requires index 0 of an array field must contain a certain value.
I suggest rethinking your document data and rules, and structure them in such a way that the client app can exactly match their requirements.
Solved!
#Doug Stevenson thanks for having pointed me to the right direction.
The rules I have in place are the following:
allow read: if request.auth.token.name == resource.data.receiver || request.auth.token.name == resource.data.sender;
allow create: if request.auth != null;
allow update: if (request.resource.data.diff(resource.data).affectedKeys()
.hasOnly(['read'])) && (request.auth.token.name == resource.data.parties[0] || request.auth.token.name == resource.data.parties[1]);}
allow delete: if false;
With these rules I am able to do the following:
First Load: load chats messages of a given room through a cloud function (it has admin priviledges, so no security rules issues there)
Get Incoming Messages: add an onsnapshot realtime listener to get all messages where receiver is the logged in user and property room is equal to the open room
await db.collection('messages').where("receiver","==",user.displayName).where("room", "==", room).orderBy('timestamp').onSnapshot(snapshot => { //Do Stuff })
Updated Read Status: when a message is displayed, the receiver updates the read property of the mssage in firestore:
const update = await db.collection("messages").doc(id).update({read: true});
Render Read Status in the UI in real time: add a second onsnapshot realtime listener to get all messages where sender is the logged in user and room is equal to the open room
const readUpdate = await db.collection('messages')
.where("sender", "==", user.displayName)
.where("room", "==", room)
.orderBy('timestamp')
.onSnapshot(snapshot => { //Do Stuff })
Send Message: user sends new messages through firestore. On the sender side, new messages are rendered locally through JS (Not through a real time listener).
On the receiving side, new messages are delivered and displayed throught the listener described in 2)
const deliverMSG = await db.collection('messages').doc(newID).set(newMsgObj);
//Render Message
renderMSG({id: newID, data: newMsgObj});
Perhaps this is not the most elegant way to handle this, but it works pretty well and is definitelly secure.
If you have any extra tip, I ll be very happy to read it.

Get Firestore document where value not in array

I know firestore doesn't allow inequality statements in .where() queries and I should instead chain > and < queries, but I don't know how this will work in my case.
In my react native app, I want to select some users who have not been already added by the user. After getting an array of all the users the current user has already added as so:
var doc = await firebase
.firestore()
.collection(`users/${currentUser.uid}/user_data`)
.doc("friends")
.get();
var friends = doc.data()
I then want to choose some users who have not been added by the current user as so:
var docs = await firebase
.firestore()
.collection("users")
.limit(10)
.where("username", "not in", friends)
.get();
How would I do this? Thanks
This kind of query is not possible with Firestore. Firestore can only query for things that exists in the indexes that it creates for each field. These indexes can't be queried efficiently for things that don't exist.
See also: Firestore: how to perform a query with inequality / not equals

Categories