For the life of me, I cannot understand why the following is resulting in a false
for allowing writes. Assume my users
collection is empty to start
Here's a function that doesn't trigger security rule errors like "Property name is undefined on object".
Features/properties:
function notUpdated(key) {
return !(key in request.resource.data)
|| (
(key in resource.data)
&& request.resource.data[key] == resource.data[key]
);
}
Explanation
1: If the field isn't present on request.resource.data
, it means that the field isn't present on the request nor on the existing resource. (Remember that request.resource.data
represents the resource after a successful write operation, i.e. the "future" document.) If the field doesn't exist anywhere, allow the write.
2: If the field does exist on either the incoming resource or the existing resource, we need to do another check. First, check if the field is present on the existing resource. If it doesn't, the update is denied. If it does, continue to check if the request field is equal to the existing field. If they're equal, allow the write. At this point it's impossible for request.resource.data[field]
to trigger a reference error; if the field is present on resource.data
, it's present on request.resource.data
as well.
The solution of Tom Bailey (https://stackoverflow.com/a/48177722/5727205) did look promising.
But in my case I needed to prevent a field from being edited, and could have the case, that the field simply does not exist on the existing data. Thereby I added a check if the field exists.
This solution does check two checks:
function isNotUpdatingField(fieldName) {
return
( !(fieldName in request.resource.data) && !(fieldName in resource.data) ) ||
request.resource.data[fieldName] == resource.data[fieldName];
}
Your rules will be a lot more readable and maintainable if you create a custom function to check for updates. For example:
service cloud.firestore {
match /databases/{database}/documents {
function isUpdatingField(fieldName) {
return (!(fieldName in resource.data) && fieldName in request.resource.data) || resource.data[fieldName] != request.resource.data[fieldName];
}
match /users/{userId} {
// Read rules here ...
allow write: if !isUpdatingField("role") && !isUpdatingField("adminOnlyAttribute");
}
}
}