Firestore Rules Occasional Permission Denied - firebase

So I'm making a journaling app with Flutter that has journals and journal entries.
Here are my firestore rules that have been working for the most part:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /users/{userId} {
allow read, update, delete: if request.auth != null && request.auth.uid == userId;
allow create: if request.auth != null;
}
match /journals/{journal} {
allow read, update, delete: if resource.data.users.hasAny([request.auth.uid]);
allow create: if request.auth != null;
}
match /journalEntries/{journalEntry} {
allow read, update, delete: if request.auth.uid != null && get(/databases/$(database)/documents/journals/$(resource.data.journal)).data.users.hasAny([request.auth.uid]);
allow create: if request.auth != null;
}
}
}
The issue I run into happens about half the time when adding journals:
W/Firestore(31194): (24.0.1) [Firestore]: Listen for Query(target=Query(journalEntries where journal == GivKPhcnen8OmCDOcXs3 order by -dateCreated, -__name__);limitType=LIMIT_TO_FIRST) failed: Status{code=PERMISSION_DENIED, description=Missing or insufficient permissions., cause=null}
E/flutter (31194): [ERROR:flutter/lib/ui/ui_dart_state.cc(209)] Unhandled Exception: [cloud_firestore/permission-denied] The caller does not have permission to execute the specified operation.
To clarify, I add a journal document, and then immediately query all the journal entries for that journal, which there are none initially.
Everything works fine if I remove the "get(/databases/$(database)/documents/journals/$(resource.data.journal)).data.users.hasAny([request.auth.uid])" part in the rules but I need that for security. The only thing I can think of is that there is some race condition happening where firestore rules don't think the journal document has been created yet when I start querying the journal entries for the new journal.
Any ideas of what is going on?

Any ideas of what is going on?
Writing to the Firestore database is asynchronous. What's probably going on is that you try to read before the write operation is complete and therefore the security rule does not find the newly created journal document.
We don't see your code, but do you wait that the asynchronous write operation is complete before fetching the journalEntries collection?
In addition, remember that Firestore security rules are not filters. It means that your query to the journalEntries collection must filter the documents that have the user uid in the users array.

Related

What is the proper way to handle "Permission Denied" when a Firestore document does not exist

I have written rules for Firestore, however, when a document does not exist it throws a "Missing or Insufficient Permissions error". For example
firebase.firestore().collection('shipments').doc(order_doc_id)
.onSnapshot(fsResponse => {
...
)}
throws the "Missing or Insufficient Permissions error" when order_doc_id does not exist.
Here is my security rule for the 'shipments' collection:
allow read: if request.auth != null && (resource.data.book_data.seller_id == request.auth.uid || resource.data.buyer_id == request.auth.uid)
Ideally I would like for it to resolve successfully with fsResponse.exists === false and then show the user a "not-found" screen, but this is not the case and the error says nothing about the existence of the document.
What is the proper way to handle this case?
This answer builds on #RenaudTarnec's answer.
The reason your current rules fail, is because they throw an error when used on a non-existent document. Any rule that throws an error is treated as if the rule blocked access.
Your current read rule:
allow read: if request.auth != null
&& (resource.data.book_data.seller_id == request.auth.uid
|| resource.data.buyer_id == request.auth.uid);
For a document that exists, the rule allows read access if the accessing user is the seller or buyer. But when the document doesn't exist, resource will be null - this leads to syntax error where you try to read the property data on a null object - blocking the read. You can see this behaviour in action when testing your rules in the Rules Playground on the Firebase Console.
To allow reads of a non-existant document, you would need to add a null-check for resource. To prevent abuse, you will probably still want to require a user to be logged in.
allow read: if request.auth != null
&& (resource == null
|| resource.data.book_data.seller_id == request.auth.uid
|| resource.data.buyer_id == request.auth.uid);
Based on your comments, your problem is as follows:
The security rule for the shipments collection is
allow read: if request.auth != null && (resource.data.book_data.seller_id == request.auth.uid || resource.data.buyer_id == request.auth.uid)
therefore resource.data.book_data.seller_id == request.auth.uid cannot be true if the doc does not exist
and therefore the rule throws the "Missing or Insufficient Permissions error" when I set a listener on the non-existing document.
HOWEVER,
I would still like to know if it doesn't exist instead of getting
permission denied error
The following security rule, using !exists to check if the document DOES NOT exist, should do the trick:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /shipments/{shipmentId} {
allow read: if (
(!exists(/databases/$(database)/documents/shipments/shipmentId))
|| (request.auth != null && (resource.data.book_data.seller_id == request.auth.uid || resource.data.buyer_id == request.auth.uid))
);
}
}
}

Firestore security rule should not allow deletion, but it allows

Firebase security rules do not seem to be working.
I want it to be forbidden to delete other's person objects.
I configured rule for delete:
if resource == null || request.auth.uid == resource.data.owner
I have test that verifies the request fails if user tries to delete other's object. However, the request succeeds.
These are my all security rules:
service cloud.firestore {
match /databases/{database}/documents {
match /{collection}/{document} {
allow create:
if request.auth.uid == request.resource.data.owner
&& (collection != "Person"
|| request.resource.data.owner == request.resource.data.id)
allow update:
if request.auth.uid == resource.data.owner
&& request.auth.uid == request.resource.data.owner
&& (collection != "Person"
|| request.resource.data.owner == request.resource.data.id)
allow get, delete:
if resource == null
|| request.auth.uid == resource.data.owner
allow list:
if collection == "XfCard"
}
}
}
What can be missing?
The resource.data.owner is more a path than a variable that you can use to compare to another. Considering that, it's usually needed that you set this as a specific path and not just perform the direct, usual comparison.
As seen in this other question from the Community here, it's usually necessary to compare the owner with the path of the owner of document, something, more or less like the below code:
if resource.data.owner == /databases/$(database)/documents/users/$(uid_owner)
This way, you should be able to use it and confirm the identity of the owner, in relation to the document.
In addition to checking the above Community post and giving a try using the above code - not tested, but I believe it's a good starting point - , I would recommend you to take a look at the below official documentation, that you can find more information on security rules for group queries.
Collection group queries and security rules
Let me know if the information helped you!
After debugging I found out that the request succeeds, but the object is NOT deleted.
My HTTP request just does not return error message.

where cluase not working after security rule in google cloud

this is my security rule for firebase, it throwing permission for query :
app.firestore().collection('users').where('email', '==' ,'test#gmail.com').get().then(snap => console.log(snap.docs[0].data())).catch(err => console.log("err",err));
the above query is throwing permission error!!
service cloud.firestore {
match /databases/{database}/documents {
// Make sure the uid of the requesting user matches name of the user
// document. The wildcard expression {userId} makes the userId variable
// available in rules.
match /users/{userId} {
allow read, update, delete: if request.auth.uid == userId;
allow create: if request.auth.uid != null;
}
}
}
why ?
Your security rule is saying that a user can only read the document in users with an id that's the same as their authenticated UID. But your query is trying to read any document where the email field has some value. Since that query could possibly try to read documents other than the one that matches the UID, the query will fail every time. With the rules you have now, the client is only allowed to get() their own specific document, with no other queries allowed.
You have to remember that security rules are not filters.
Example:
my uid: 123123
doc1:
email: test#gmail.com, id: 123123
The securtiy rule will pass for doc1.
doc2: email: test#gmail.com, id: 321321
The security rule will fail for doc2.
That is because your security rule is saying that if ALL documents have the uid in then it should pass, but because it is not, it fails.
To fix this, you would need to provide some further information on how your db is setup and what you are querying. Ie, you are finding all emails which match a criteria, or something else.

Firebase Firestore Security Rules Variable not used

Writing rules for Firestore it seems that custom variables are not working.
Did anyone know why or have seen similar behaviour?
Using the below I got access denied although the uid is in the array of admin.
service cloud.firestore {
match /databases/{database}/documents {
match /conferences/{confid} {
allow read,write: if request.auth.uid in get(/databases/$(database)/documents/conferences/$(confid)).data.admin;
}
}
}
Simulator is giving the below error:
Function [get] called with path to nonexistent resource: /databases/%28default%29/documents/conferences/%7Bconfid%7D
Also testing this on a real devices I got access denied.
If however I use the ID of the document like below it works and access is granted.
service cloud.firestore {
match /databases/{database}/documents {
match /conferences/{confid} {
allow read,write: if request.auth.uid in get(/databases/$(database)/documents/conferences/ySWLb8NSTj9sur6n2CbS).data.admin;
}
}
}
Obviously I can't hardcode this for each and every ID.
UPDATE
Apart from logging the case with support I have done some further testing.
On the below the simulator is now granting access.
service cloud.firestore {
match /databases/{database}/documents {
match /conferences/{confID}{
allow read, write: if request.auth.uid in get(/databases/$(database)/documents/conferences/$(confID)/permissions/permission).data.users;
}
}
}
For reference I use the below to query from my web-application:
db.collection("conferences")
.get()
.then(query => {
console.log("SUCCESS!!!")
query.forEach(function(doc) {
// doc.data() is never undefined for query doc snapshots
console.log(doc.id, " => ", doc.data());
});
}).catch((e) => {
console.log(e)
})
This is the log from the browser:
FirebaseError: Missing or insufficient permissions.
at new FirestoreError (webpack-internal:///./node_modules/#firebase/firestore/dist/index.cjs.js:352:28)
at JsonProtoSerializer.fromRpcStatus (webpack-internal:///./node_modules/#firebase/firestore/dist/index.cjs.js:5649:16)
at JsonProtoSerializer.fromWatchChange (webpack-internal:///./node_modules/#firebase/firestore/dist/index.cjs.js:6146:44)
at PersistentListenStream.onMessage (webpack-internal:///./node_modules/#firebase/firestore/dist/index.cjs.js:14350:43)
at eval (webpack-internal:///./node_modules/#firebase/firestore/dist/index.cjs.js:14279:30)
at eval (webpack-internal:///./node_modules/#firebase/firestore/dist/index.cjs.js:14319:28)
at eval (webpack-internal:///./node_modules/#firebase/firestore/dist/index.cjs.js:7411:20)
I am using the latest Firebase package 5.8.3.
If I change the above rule to something simple like below it got access as long as I am logged in with a user:
service cloud.firestore {
match /databases/{database}/documents {
match /conferences/{confID}{
allow read, write: if request.auth.uid != null
}
}
}
This even confuses me more. Is this because the rule is more complex and it takes too long to get this verified and gives back access denied?
Update-2
Quickly tested this in a mobile app via Flutter. Same result. Access denied with this ruleset.
service cloud.firestore {
match /databases/{database}/documents {
match /conferences/{confID}{
allow read, write: if request.auth.uid in get(/databases/$(database)/documents/conferences/$(confID)/permissions/permission).data.users;
}
}
}
I think my problem was the query's don't match security rules. If you would only access a single specific document it would work but if you query multiple documents in a collection you got blocked by the security rules.
I had two options. Restructure my data so that a single document will hold all the data I need or redesign security rules to match query's.
In the end I have attached to each document an indentifier like the UID to make sure query's match the security rules.
One solution would be to put the users with permissions into an array in the conference document instead,
so request.resource.data.permissions
So, instead of this:
get(/databases/$(database)/documents/conferences/$(confID)/permissions/permission).data.users
use this:
request.resource.data.permissions
This wouldn't solve the get() problem, but it would eliminate the need for a get() call, which could save you 15% or more on your quota.

Firestore Security Rule denying read/write on firebase-storage

Trying to add security rules to storage. When I add a security rule to make sure only authenticated users should be allowed with read/write, simulator is working. But when I try to add another constraint on the size of a file, I'm encountering an error.
The following is the Security Rule:
service firebase.storage {
match /b/{bucket}/o {
match /{allPaths=**} {
allow read, write: if request.auth != null && request.resource.size < 5 * 1024 * 1024;
}
}
}
I have files under files\
I get the following error in simulator:
Simulated read denied
Error details:
Error: simulator.rules line [4], column [29]. Property resource is undefined on object.
Issue happens if I try to simulate write as well.
EDIT1: IMPORTANT
OK! I found this question and tried experimenting a bit on that line and got the simulator allowing read/write! I made the following change:
allow read, write: if request.auth != null && (request != null
|| request.resource.size < 5 * 1024 * 1024);
That's basically I added a null check. So, at the moment, I'm not clear what's going on here!
I was able to work around this with a rule like
match /users/{uid}/{document=**} {
allow read, create, update: if
request.auth != null &&
request.auth.uid == uid &&
(!("resource" in request) || request.resource.data.uid == request.auth.uid);
}
In this case I wanted to ensure that the "uid" property in the update matches the user's uid. However, if in the simulator you don't do "Build Document" first then request.resource is undefined (I think this is a bug; it should be defined but null IMO).
I believe this really only applies in the simulator, and is not a realistic scenario since create/update requests will always contain a document, even an empty one. But I think I'll keep it in my rule just in case.

Resources