I’m trying to set Firestore rules, allowing user to update only certain fields inside of a nested object. I know if it’s a top level key, we can simply use something like this:
allow update: if request.auth.uid == userID &&
request.resource.data.diff(resource.data).affectedKeys().hasOnly(["key1", "key2"]);
However, I’m stuck with a problem restricting updates for keys inside an object, lets call it "someData". This is what I have tried, it seems logical, but it doesn’t work:
allow update: if request.auth.uid == userID &&
request.resource.data.diff(resource.data).affectedKeys().hasOnly("someData", "otherData") &&
(!("someData" in request.resource.data.keys()) ||
(request.resource.data["someData"].diff(resource.data["someData"])
.affectedKeys().hasOnly(["key1", "key2"]));
Or like this:
allow update: if request.auth.uid == userID &&
request.resource.data.diff(resource.data).affectedKeys().hasOnly("someData", "otherData") &&
(!("someData" in request.resource.data.keys()) ||
(request.resource.data.someData.diff(resource.data.someData)
.affectedKeys().hasOnly(["key1", "key2"]));
Neither works.
Is there a way protecting nested fields?
Thanks!
I tried to test in test environment using "mocha", tried with real Firestore, tried to put this logic in separate function. Nothing works.
Firebase Docs only say about top level keys of the document.
I found one solution here, similar to my first example, but in real life it doesn’t seem to be working.
Am I doing anything wrong?
Update with more specific details
These are my original rules (working on a taxi booking app):
allow update: if request.auth.uid == userID &&
editOnlyFields(["name", "phoneNumber", "driversData", "waitingDriverApproval", "lastUpdate"]) &&
request.resource.data.lastUpdate == request.time &&
(!("name" in request.resource.data.keys()) ||
request.resource.data.name is string) &&
(!("phoneNumber" in request.resource.data.keys()) ||
request.resource.data.phoneNumber is string) &&
(!("waitingDriverApproval" in request.resource.data.keys()) ||
(request.resource.data.waitingDriverApproval is bool && request.resource.data.waitingDriverApproval == true)) &&
//nested driversData (pain in the butt)
(!("driversData" in request.resource.data.keys()) ||
(request.resource.data.driversData.keys().hasOnly(["licensingAuthority", "amountOfVehicles", "DVLACheckCode", "DVLALicence", "PHBadge", "PHDriversLicence"])) &&
(!("licensingAuthority" in request.resource.data.driversData.keys()) ||
request.resource.data.driversData.licensingAuthority is string) &&
(!("amountOfVehicles" in request.resource.data.driversData.keys()) ||
request.resource.data.driversData.amountOfVehicles is int) &&
(!("DVLACheckCode" in request.resource.data.driversData.keys()) ||
(request.resource.data.driversData.DVLACheckCode.keys().hasOnly(["code", "pending"]) &&
isValidDVLACheckCode(request.resource.data.driversData.DVLACheckCode))) &&
(!("DVLALicence" in request.resource.data.driversData.keys()) ||
(request.resource.data.driversData.DVLALicence.keys().hasOnly(["downloadUrl", "pending"]) &&
isValidDriversPaperwork(request.resource.data.driversData.DVLALicence))) &&
(!("PHBadge" in request.resource.data.driversData.keys()) ||
(request.resource.data.driversData.PHBadge.keys().hasOnly(["downloadUrl", "pending"]) &&
isValidDriversPaperwork(request.resource.data.driversData.PHBadge))) &&
(!("PHDriversLicence" in request.resource.data.driversData.keys()) ||
(request.resource.data.driversData.PHDriversLicence.keys().hasOnly(["downloadUrl", "pending"]) &&
isValidDriversPaperwork(request.resource.data.driversData.PHDriversLicence))));
}
function editOnlyFields(allowedFields) {
return request.resource.data.diff(resource.data).affectedKeys().hasOnly(allowedFields);
}
It all went well, till I started working on admin part. the admin has auth token custom claim isBoss: true
Rules allow read and write if isBoss()
match /{document=**} {
allow read, write: if isBoss();
}
function isBoss() {
return request.auth.token.isBoss == true;
}
Now with original rules, I am facing a problem – when admin changes anything inside user’s driversData
, something that user wouldn’t be authorized to do, like approvedDriver: true
, then user is not able to update their driversData
because request object contains all fields that would be present after update.
So, I started rules update with replacing this line:
request.resource.data.driversData.keys().hasOnly(allowedFields)
With this one:
(editOnlyFieldsNested("driversData", ["licensingAuthority", "amountOfVehicles", "DVLACheckCode", "DVLALicence", "PHBadge", "PHDriversLicence"]))
Where
function editOnlyFieldsNested(nestedObj, allowedFields) {
return request.resource.data[nestedObj].diff(resource.data[nestedObj]).affectedKeys().hasOnly(allowedFields);
}
When I run my Firestore emulator, this is the function that fails:
it("Can update a user document only with allowed fields", async () => {
await testEnv.withSecurityRulesDisabled((context) => {
return context.firestore().collection("users").doc(myId).set({
name: "User Abc",
email: "[email protected]",
createdAt: serverTimestamp(),
});
});
const testDoc = myUser.firestore().collection("users").doc(myId);
await assertSucceeds(
testDoc.update({
name: "New Name",
phoneNumber: "after",
driversData: { amountOfVehicles: 1, licensingAuthority: "Bristol" },
lastUpdate: serverTimestamp(),
})
);
});
If I run this with old rules (without MapDiff), it passes.
If I use new rules with editOnlyFieldsNested()
it only passes if I comment out driversData
property, otherwise it gives me an error:
Can update a user document only with allowed fields:
FirebaseError: 7 PERMISSION_DENIED:
Property isBoss is undefined on object. for 'update' @ L8, evaluation error at L17:24 for 'update' @ L17, Property isBoss is undefined on object. for 'update' @ L8, Property driversData is undefined on object. for 'update' @ L17
Perhaps it can be something to do on driversData undefined on object…
Why?
I’m stuck on this for a couple days, my laptop is covered with tears..
Any advise would be gratefully appreciated
2
Answers
I think, the main problem lies in the fact that
.diff()
method tries to compare 2 objects -request
andresource
. If resource doesn't havedriversData
object, test (and real request) will fail.I see 2 ways of solving. Either create all users with empty
driversData
, that also has nested objects, or rewrite rules that also work ifdriversData
doesn't exist.The problem is in this code:
This sets
driversData
to{ amountOfVehicles: 1, licensingAuthority: "Bristol" }
and removes the existing data indriversData
(which is why your rules reject it).If you want to update nested properties, use dot notation:
Also see the documentation on updating nested properties.
Here’s an example of me using the
.
notation in the rules playground when I was trying to reproduce your problem: