Firebase authentication rules using array elements - javascript

I'm using Firebase with react native. In my app I allow users to share content with: read, copy or modify permissions, with either specific users or All.
Since Firebase does not provide an OR function for queries, I'm using an array to store the user:permission combinations, so that I can use the array-contains-any capability to query like this
.where("access", "array-contains-any", [user_id, `${user_id}:R`, `${user_id}:C`, `${user_id}:M`, "All_R", "All_C", "All_M"])
This allows me to query all content that is either owned by user_id OR shared to user_id OR shared to "All" with either R, C OR M permissions.
Now I want to do something similar for the authentication rules, (so that only users with "M" permission can modify etc.) by comparing the authenticated user ID with the user IDs in the array. In order to do that it would require a capability similar to "array-contains-any" and string concatenation for authentication rules.
Is that possible or do I need to do something different?

I ran a few tests, and think I got a subset of the types you have working. I'll walk through the steps below, so that you can build from it at the end.
First step is to make this query work:
ref.where("access", "array-contains", user_id)
This is fairly easy to secure with:
allow list: if request.auth != null && request.auth.uid in resource.data.access;
So anyone can read the document if their UID is mentioned in the access array.
The next step is to allow this query:
ref.where("access", "array-contains-any", [user_id, user_id+":R"]);
When we run this against the earlier rules, it is not allowed. That is because we're requesting a value that the rules are not aware of.
To make it work, change the rules to:
allow list: if request.auth != null && (
request.auth.uid in resource.data.access ||
request.auth.uid+":R" in resource.data.access
);
So now the conditions in the code and in the rules match again, and the read is allowed.
If we add another condition for the all-access rules:
ref.where("access", "array-contains-any", [user_id, user_id+":R", "All_R"]);
Then once again with the above rules our permission is denied, because we have a value in the query that the rules don't know.
To fix it, we check for that value in our rules too:
allow list: if request.auth != null && (
request.auth.uid in resource.data.access ||
request.auth.uid+":R" in resource.data.access ||
"All:R" in resource.data.access
);
And with those rules, the query succeeds again.
At this point you can add your other conditions in here, and it should work.

Related

How to access my data field in Firebase Security Rules

Security Rules
I am trying to grant permission based at a document field:
match /users/{user}/{documents=**} {
allow read, write: if resource.data.uid == request.auth.uid
}
Firebase query
Here is how my query looks:
query(collection(db, "users", match.params.uid, "promotors"));
Error message
But I keep geting this message:
FirebaseError: Missing or insufficient permissions.
Your query is not in any way checking the data in a field in the documents, so it will never meet this part of your rules: resource.data.uid.
Instead what you seem to have is a case where the document ID matches the UID of the user, which you can check with:
match /users/{user}/{documents=**} {
allow read, write: if user == request.auth.uid
}
Also see the documentation on content owner only access.

Why do I get an insufficient permissions error while trying to delete an array element from firestore, yet I can delete the entire document

I have a collection in my firestore named "reports"
one of the fields in the reports map is an array of objects. These objects reference a photo in cloud storage. They look like this (url points to cloud storage):
{
id: uid value
url: some-url
}
my firebase rules are set up like this for the reports document:
match /databases/{database}/documents {
match /reports/{report} {
allow read: if request.auth != null && request.auth.uid == resource.data.userID;
allow create: if request.auth != null && request.resource.data.userID == request.auth.uid;
allow delete, update: if request.auth != null &&
request.auth.uid == resource.data.userID;
}
}
for some reason, I can delete the entire document if I want to proving that I have delete permission.....but when I attempt to delete an item from the photos array like this:
const reportRef = db.collection('reports')
reportRef.doc(activeReport.id).update({
photos: firebase.firestore.FieldValue.arrayRemove(photoToDelete)
})
I end up with an error stating:
Unhandled promise rejection: FirebaseError: Missing or insufficient permissions
Why? Haven't I given permission to update this document properly?
Go in Database ->
Rules ->
For development:
Change allow read, write: if false; to true;
Note: It's a quick solution for development purposes only because it will turn off all the security. So, it's not recommended for production.
For production:
If authenticated from firebase: Change allow read, write: if false; to request.auth != null;
In this case it was a bad parameter that was passed in. I needed to pass activeReport not activeReport.id.
This is a misleading error message. I would delete this question but stackoverflow doesn't want me to because an answer has been posted here. Silly.

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.

Can a user update the script in a console and access other users' data in my firebase code?

For context, I display my Firebase app's configuration in the app.js file (is this safe?) and in order to give a user their data I list the following code:
db.collection('whispers').where("sender", "array-contains",
userID).orderBy('time').onSnapshot(snapshot => {
let changes = snapshot.docChanges();
changes.forEach(change => {
renderSay(change.doc);
});
});
Could a user remove the 'where' condition and access all data in this collection? I only use anonymous sign in so nothing special required in accessing their own data besides using the same device and not clearing data.
I doubt that could be possible by reverse-engineering but the actual security lies in the security rules.
You can change them from the Firebase Console
Here is a video from Firebase and here is the documentation
service cloud.firestore {
match /databases/{database}/documents {
// Match any document in the 'users' collection
match /users/{userID} {
allow read: if request.auth.uid === userID;
allow write: if request.auth.uid === userID;
}
}
}
Using the above security rules will allow users to read and write their data ONLY.
So even if they reverse-engineer your code, it will harm their data only and other data will be safe.
For more details please check the links above and you will get familiar with wildcard [{userID} in this case]
PS: Only you or anyone with access to your Firebase Console can change the rules
EDIT: This Medium Article has many types of rules explained. Please have a read there too :D

firestore rules matching parent data, only work in simulator

I'm trying to make a simple direct message part. I have a document with some meta informations and a subcollection with the single messages.
When I try to get the messages the rules are valid in the simulator, but in js I get everytime: Uncaught (in promise) FirebaseError: Missing or insufficient permissions.
The mongodb structure:
chatRooms
roomId
metaInformation (senderId/Name, recieverId/Name, timestamp, etc.)
messages
messageId
name
text
timestamp
The rules:
match /chatRooms/{chatRoom} {
allow create: if request.auth.uid != null;
allow update, delete: if resource.data.uid == request.auth.uid;
function isChatPartner() {
return parentDoc().receiverId == request.auth.uid || parentDoc().senderId == request.auth.uid;
}
function parentDoc() {
return get(/databases/$(database)/documents/chatRooms/$(chatroom)).data;
}
match /messages/{message} {
allow read: if isChatPartner();
}
}
The js-request:
db.collection("chatRoom").doc(_roomId).collection("messages").get().then(msg => {
console.log(msg);
})
Has anyone an idea what maybe could be wrong?
Your database and rules don't match. In the rule, the top level collection is called "chatRooms", but in your code, it's called "chatRoom". The collection names need to match exactly.
There's another problem. Your code is trying to get all of the documents in the subcollection, but the rules don't allow that. The rules are checking certain fields for access. This won't work in security rules, since rules are not filters (be sure to read and understand those docs). The query must only request documents that will definitely pass the rules - the rules will not check each document and exclude the ones that don't match.

Categories