How do you debug Firestore security rules? - firebase

I'm crying myself to sleep on this one.
My getAfter is returning an object that only has 1 field, as every other field type is incorrect. Which I have no idea how to check without any debugging tools (I can't see the data, so its all guess and check).
Here is a watered down version of my rules for users.
match /users/{userId} {
function isValidUser(user) {
return user.id is string &&
(user.address is string || user.address == null) &&
(user.dateOfBirth is number || user.dateOfBirth == null) &&
user.email is string &&
user.name is string &&
(user.phoneNumber is string || user.phoneNumber == null);
}
function isValidWrite(userId, user) {
return signedIn() &&
writeHasMatchingId(userId, user) &&
isValidUser(user);
}
allow read: if signedIn();
allow create: if signedInAndWriteHasMatchingId(userId) &&
userHasId(userId) &&
isValidUser(request.resource.data); // Tested
allow update: if isValidWrite(
userId,
getAfter(/databases/$(database)/documents/users/$(userId))
);
}
and this is the transaction I am trying to run.
const user1Ref = this.userCollection.doc(user1Id);
const user2Ref = this.userCollection.doc(user2Id);
const batchWrite = this.store.batch();
batchWrite.update(user1Ref, {
"details.friend": user2Id,
});
batchWrite.update(user2Ref, {
"details.wishlist": true,
});
batchWrite.commit();
If I comment out the isValidUser(user) line, the operation succeeds. If I leave any line uncommented out inside the function isValidUser(user) except user.id is string, it fails.
Why would the getAfter document only have the id field and no others when they are listed in the Firebase console? Is there a way to output or debug the value of getAfter so I can see what it even is?

I'm answering based on just one line of your question:
Is there a way to output or debug the value of getAfter so I can see what it even is?
There kind of is - at least in 2020.
When one runs something in the Rules Playground (Rules Simulator, see bottom left), the steps taken in the rule evaluation are shown like this:
This list sometimes gives indications that help figure out what the rules evaluator is doing. It's a bit tedious that one needs to 'click' the steps open, individually, instead of seeing true/false just by glancing. But it's better than nothing.
Note: I presume this feature is under development by Firebase. It sometimes seems to give wrong information - or I have failed to read it correctly. But it may help, and looks like a good place for providing such information to the developers. We really would like to see: with the current data, the built query document, and the rules, how does Firebase see it and why does the rule evaluate to true or false?

Another approach, not mentioned here yet and likely not available at the time the question was raised, is wrapping your rules with debug().
Why this is cool?
Allows to see the values suspected of not being right; I still use the same comment-out-narrow-down method that #ColdLogic nicely described in one of their comments
Why this is not enough?
There is no tagging about which value was output; just eg. int_value: 0. Debug would benefit from eg. printing the first 10 letters of the equation it's evaluating, in the output.
Security Rules rejection reasons are still awfully short, as false for 'update' # L44.
the line number always points to the main expression being evaluated. Never to a function called, or a subexpression with && that really causes the fail.
Firebase could fix this (not change the output syntax; just give a more detailed line number). That would eliminate the need to comment-out-and-narrow-down.
The output goes to firestore-debug.log (fairly hidden), so one needs to open yet another terminal and keep an eye on it.
Debugging Security Rules is unnecessarily difficult - and I'm afraid it means people don't use their full potential. We should change this.

Related

Why is this Cloud Firestore security rule suddenly failing?

For some time I was using the following rule for Cloud Firestore in my Flutter project:
"You can see chats if you're logged in, a member and the chat was not flagged".
match /chats/{chatId} {
allow read: if signedIn() && request.auth.uid in resource.data.members && resource.data.flagged == false;
}
It used to work and to me, it seems correct. But recently, it started to fail. When I use only
allow read: if signedIn();
It works just fine. Any idea what might be the problem? In the Firebase emulator, I can see it also fails, but there is no explanation. Obviously, members and flagged fields exist (when a chat document is available).
Could this have happened after updating a particular package, like cloud_firestore?
Any ideas?
I found out, finally, what was wrong. According to this page, "the result set should satisfy the rule's condition". However, I thought my query was exactly the same as my rule and I didn't know what was wrong.
It turns out, this is taken quite literally.
In my query I had:
where('flagged', isNotEqualTo: true) and my rule was flagged == false. I needed to change that to flagged != true. This is very confusing. It seems like a bug to me, and otherwise, I would be very curious to know the reason behind this.

Firebase Security rules comparison logic

I have a question about how cloud firestore security rules' logic works:
Lets say I have rule such as this:
allow read: if (auth.token.user === true && request.query.limit < 100 && uidInDocument()) || auth.token.admin === true && get(/databases/$(database)/documents/users/$(request.auth.uid)).data.name == "foo";
Now, my question here is if the first part of the or statement is true, does that mean that the second part doesn't execute? I'm thinking about this as I don't want to be incurring reads unless it is necessary to do so, in this case if the user is an admin
Thanks in advance.
Boolean expression in security rules evaluate left to right and short circuit just like pretty much all other programming languages. If you have a series of logical AND operations, and the first one is false, then the entire rule is finished, and access is rejected.
Please read the documentation for more information.

Firestore Rules break if the document path has a comma

So, I am building a multi tenant application which involves multitude of collections and user invitation. Since I don't know which user ID the signup is going to use and I want the companies to start making changes and updates to their user data I am pre setting the user ID and I use it in several paths as:
/companies/{company}/customers/{userId}
/users/{userId}
Now, this userId is pretty much the email with the dot replaced by a comma. The dot is a character not allowed in firebase but the comma is, and viceversa with emails so it makes sense and there is no problem there.
The issue is when I need to make some rules. For example I have this in the configuration since I want admins to access every company:
function cleanEmail(){
return request.auth.token.email.split('.').join(',') //Here I also tried %2C
}
function isSuperadmin() {
return exists(/databases/$(database)/documents/admins/$(cleanEmail()))
}
match /companies/{company} {
allow write: if isSuperadmin();
allow read: if isSuperadmin() || belongsToCompany(company)
}
The simulator was breaking but I could not understand why, so I even sent a bug report. Eventually I tried making the path hard coded and then I found the problem:
function isSuperadmin() {
return exists(/databases/$(database)/documents/admins/test,account#gmail,com) //Same issue with get()
}
I tried with exists() because I thought it could be a bug with get() but the issue remains. I believe this should be a bug since that is a valid Firestore path and I have seen some people already using this "clean email" strategy.
Funny thing is that this was not happening during first testing and I realized that this bug only happens if the comma is BEFORE the #. If you remove the one before that, and leave the second one, seems to work:
I might add a new step to the clean email that turns that into base64 and that might work tho. If someone has a solution great.
I do something similar that looks like this in outline:
/invitation
/fb-generated-doc-name
email: somecustomer#gmail.com
trip: db-ref-to-trip
/usertrips
/doc-name-is-UID
trip: db-ref-to-trip
/trips
/fb-generated-doc-name
{ ... describes the trip ... }
The outline for onCreate auth'd user is:
exports.authDidCreateUser = functions.auth.user().onCreate((authUser, context) => {
/*
find invitation where email == authUser.email
let docref = collection('usertrips').doc(authUser.uid)
docref.set({ trip: invitation.trip })
*/
});

Unable to base security rule condition on resource data in Firebase

I am attempting very simple thing and that is matching request.auth.uid to a field value in my transaction documents (like this resource.data.useruid) in Firebase security rule in order to get transactions of a particular logged in user. However, I don't get any documents while querying for them and get an error instead.
This is how the collection looks like - just one document there with useruid field.
The field's value is mapped to the users uid (screenshot taken in the Authentication -> Users tab.
And the rule looks like this
I should get the one document back but every time I query the documents with that user logged in (I am using angularfire2 for those purposes) I get Error: Missing or insufficient permissions.
If I modify the rule condition to return always true or if I only check for truthiness of request.auth.uid I get the query result alright. The funny thing though is that with resource.data involved - eg. checking for value of the amount field in the firebase rule - the condition is never met. I tried to write it like
allow read, write: if resource.data.amount == 3
and got the error again. Seems like I don't get the resource.data Map at all.
I feel like I am missing something obvious, although after reading the guides, it seems alright to me and I am already out of ideas. The debugging capabilities (or lack of) make the whole process very slow.
Could you please explain to me, why I don't get the resource.data Map in the firebase security rule or point me to a place where the problem might be?
You have most probably missed one specific point in the doc: your query fails "because it does not include the same constraints as your security rules". See https://firebase.google.com/docs/firestore/security/rules-query#secure_and_query_documents_based_on_authuid
The following, with your security rules works perfectly:
firebase.auth().signInWithEmailAndPassword("xxxx#xxxx.com", "xxxxx")
.then(function (info) {
db.collection("transactions").where("userid", "==", info.uid).get().then(function(querySnapshot) {
querySnapshot.forEach(function(doc) {
console.log(doc.id, " => ", doc.data());
});
});
});
If you remove the where clause, you get the exact error you are getting

Firebase security rule that limits writes to an increment (+1) of current value?

I'm building a very simple JavaScript-based Firebase app that increments a key's value by 1 whenever my webpage is loaded. I don't want any authentication friction, so want to use either an open database, or one restricted to anonymous authentications. So, for example's sake:
$(document).ready(function() {
// after config and initialize...
var fb = firebase.database();
var fbCount = fb.ref("count");
fbCount.transaction(function(current) {
return current + 1;
});
});
In either case, as I understand it, there is nothing to stop anyone who can access the page from copying my code (including my initialization config) and using it on their own server--with adjustments--to not only increment the value (fbCount above), but to change the code in any other way they like (e.g. letting them increment the value by 100, 1000, or changing it to something else entirely).
My further understanding is that the best way to deal with this potential is through security rules. So what I'm trying to figure out is whether there's a way, through the security rules, to limit any write to only an increment by 1 of the current value?
If not, is there another method I should be investigating?
You'll want to make use of predefined variables. Your validation rule will look something like this (at the location of the field you want to protect):
".validate": "newData.val() == data.val() + 1"

Resources