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
I solved it by using writeFields
. Please try this rule.
allow write: if !('role' in request.writeFields);
In my case, I use list
to restrict updating fields. It works, too.
allow update: if !(['leader', '_created'] in request.writeFields);
With this single function you can check if a fields are/aren't being created/modified.
function incomingDataHasFields(fields) {
return ((
request.writeFields == null
&& request.resource.data.keys().hasAll(fields)
) || (
request.writeFields != null
&& request.writeFields.hasAll(fields)
));
}
Usage:
match /xxx/{xxx} {
allow create:
if incomingDataHasFields(['foo']) // allow creating a document that contains 'foo' field
&& !incomingDataHasFields(['bar', 'baz']); // but don't allow 'bar' and 'baz' fields to be created
Since reference to writeFields in documentation has disappeared, I had to come up new way to do what we could do with writeFields.
function isSameProperty(request, resource, key) {
return request.resource.data[key] == resource.data[key]
}
match /myCollection/{id} {
// before version !request.writeFields.hasAny(['property1','property2','property3', 'property4']);
allow update: isSameProperty(request, resource, 'property1')
&& isSameProperty(request, resource, 'property2')
&& isSameProperty(request, resource, 'property3')
&& isSameProperty(request, resource, 'property4')
}
This might seem like over kill but for updating documents where you might have other fields that are not user generated, eg. roles, created etc. you need functions that can test that those fields don't change. Hence meet these three FNs.
function hasOnlyFields(fields) {
if request.resource.data.keys().hasOnly(fields)
}
function hasNotChanged(fields) {
return (fields.size() < 1 || equals(fields[0]))
&& (fields.size() < 2 || equals(fields[1]))
&& (fields.size() < 3 || equals(fields[2]))
&& (fields.size() < 4 || equals(fields[3]))
&& (fields.size() < 5 || equals(fields[4]))
&& (fields.size() < 6 || equals(fields[5]))
&& (fields.size() < 7 || equals(fields[6]))
&& (fields.size() < 8 || equals(fields[7]))
&& (fields.size() < 9 || equals(fields[8]))
}
function equals(field) {
return field in request.resource.data && field in resource.data && request.resource.data[field] == request.resource.data[field]
}
So on update of say user document, where the user may only update their name, age, and address, but not roles and email you can do:
allow update: if hasOnlyFields(['name', 'age', 'address']) && hasNotChanged(['email', 'roles'])
Note the hasNotChanged can check up to 9 fields. Also these aren't the only checks you would want to do. You'd need to check the the types and ownership of the document as well.
So in the end, it seems I was assuming that resource.data.nonExistentField == null
would return false
, when it actually returns an Error
(according to this and my tests). So my original solution may have been running into that. This is puzzling because the opposite should work according to the docs, but maybe the docs are referring to a value being "non-existent", rather than the key -- a subtle distinction.
I still don't have 100% clarity, but this is what I ended up with that worked:
function isAddingRole() {
return !('role' in resource.data) && 'role' in request.resource.data;
}
function isChangingRole() {
return 'role' in resource.data && 'role' in request.resource.data && resource.data.role != request.resource.data.role;
}
function isEditingRole() {
return isAddingRole() || isChangingRole();
}
Another thing that still puzzles me is that, according to the docs, I shouldn't need the && 'role' in request.resource.data
part in isChangingRole()
, because it should be inserted automatically by Firestore. Though this didn't seem to be the case, as removing it causes my write to fail for permissions issues.
It could likely be clarified/improved by breaking the write out into the create
, update
, and delete
parts, instead of just allow write: if !isEditingOwnRole() && (isOwnDocument() || isAdmin());
.
Found this rule to work quite well:
function propChanged(key) {
// Prop changed if key in req but not res, or if key req and res have same value
return (
(key in request.resource.data) && !(key in resource.data)
) || (
(key in request.resource.data) && request.resource.data[key] != resource.data[key]
);
}