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.
Related
This question already has answers here:
security rules to allow update of specific fields
(2 answers)
Firestore Rules to restrict write access to a specific field in a document
(4 answers)
Closed 2 years ago.
I have a Firebase document (actually, may documents need the same thing) where, due to security reasons I only allow updates from backend code (aka, Firebase functions). I still want connected clients to be able to read this data and in some cases update the data but only limited fields.
My current solution is below, basically verify that the value in the document isn't being changes in the request. Yes, this works but I feel this is problemmatic because it's easy to forget to update the rules when adding a new field and since I haven't found any really good tools to edit rules it's fairly tedious.
This is an example of what I'm doing now:
allow update:
if request.auth != null &&
request.auth.uid == resource.data.firebaseUID &&
resource.data.pendingDelete == false &&
// Some values are protected
resource.data.creditBalance == request.resource.data.creditBalance &&
resource.data.creditLimit == request.resource.data.creditLimit &&
resource.data.firebaseUID == request.resource.data.firebaseUID &&
resource.data.mostRecentTermsOfService == request.resource.data.mostRecentTermsOfService &&
resource.data.mostRecentTermsOfServiceAccepted == request.resource.data.mostRecentTermsOfServiceAccepted &&
resource.data.mostRecentTermsOfServiceAcceptedOn == request.resource.data.mostRecentTermsOfServiceAcceptedOn &&
resource.data.promotionCreditBalance == request.resource.data.promotionCreditBalance &&
resource.data.promotionCreditsExpiration == request.resource.data.promotionCreditsExpiration &&
resource.data.registeredOn == request.resource.data.registeredOn &&
resource.data.testRecord == request.resource.data.testRecord &&
resource.data.userOwnedCompanyID == request.resource.data.userOwnedCompanyID;
This gets especially ugly when I mix in user roles. It would be great if there was some way to say only X value can be changed, is that even possible?
I had thought about using a many of these:
!("creditLimit" in request.resource.data)
But that appears to not work (PERMISSION_DENIED) but it isn't much better as it appears I would still need to individually list all values I don't want clients to update
I've looked around a few articles but most appear to be relatively limited and most appear to expect that clients can change almost any value, for example:
Basic Security Rules and
Data validation Rules
If in a batch I update documents A and B and the rule for A does a getAfter(B) and the rule for B does a getAfter(A), am I charged with 2 reads for these or not? As they are part of the batch anyway.
Example rules:
match /collA/{docAid} {
allow update: if getAfter(/databases/$(database)/documents/collA/${docAid}/collB/{request.resource.data.lastdocBidupdated}).data.timestamp == request.time
&& ...
}
match /collA/{docAid}/collB/{docBid} {
allow update: if getAfter(/databases/$(database)/documents/collA/${docAid}).data.timestamp == request.time
&& getAfter(/databases/$(database)/documents/collA/${docAid}).data.lastdocBidupdated == docBid
&& ...
}
So are these 2 reads, 1 per rule, or no reads at all?
firebaser here
I had to check with our team for this. The first feedback is that it doesn't count against the maximum number of calls you can make in a single security rule evaluation run.
So the thinking is that it likely also won't count against documents read, since it doesn't actually read the document. That said: I'm asking around a bit more to see if I can get this confirmed, so hold on tight.
Are you using two different documents?
If it is the case, then two reads will be performed.
Are these two Firestore rules different at all in the number of reads that they spend from my quota? Note that isWebAdmin() does an exists(), which eats away from my read quota.
// example 1
match /companies/{company} {
// rule 1
allow list, write: if isWebAdmin();
// rule 2
allow get: if isInCompany(company)
// when isInCompany is true, this is short-circuited away
|| isWebAdmin();
}
vs.
// example 2
match /companies/{company} {
// rule 1
allow read, write: if isWebAdmin();
// rule 2
allow get: if isInCompany(company);
}
Here is my (possibly faulty) reasoning: For most get requests isInCompany(company) will be true and isWebAdmin() will be false. Therefore, in example 2, even though the user is authorized to get with rule 2, rule 1 will also execute because get is also a read. So, while trying to give the admin access, I'm spending more reads for regular users who have access.
In example 1, I separate out get and list and treat them separately. In get requests, it will not run rule 1 at all. When running rule 2, since isInCompany(company) is true, isWebAdmin() won't execute because of short circuiting. So, in the common case I saved a read by avoiding calling isWebAdmin().
Is this correct? If so, simply slapping admin privileges adds gets for each user's regular operation. I find this a bit inconvenient. I guess if this is not the case, we should be billed by only the "effective" rule, not everything that was tested. Is that the case instead?
With Firebase security rules, boolean expressions do short circuit, which is a valid way of optimizing the costs of your rules. Use the more granular rules in example 1 for that.
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.
I see that .validate rules can be used to check if certain data should be written depending on certain condition, such as:
".validate": "newData.isString()"
But is it possible to correct or modify the actual data? For example if we want a string to be saved always, it would be something like this:
".validate": "newData.isString() ? true : newData = ''; true"
If not, what would be the best alternative for this use case?
No, that is not the point of the Database rules. .validate will only check for the format.
If you want to modify the uploaded data, you have a nice example over here:
https://firebase.google.com/docs/functions/database-events
Integrating Cloud Functions is fairly easy and does exactly the job what you want.