My Goal
I want to ONLY allow users to update a specific field in the user documents of others.
My user document
/* BEFORE */
{
id: 'uid1',
profile: { /* a map of personal info */ },
connectedUsers: {
uid2: true,
uid3: true,
}
}
/* AFTER */
{
id: 'uid1',
profile: { /* a map of personal info */ },
connectedUsers: {
uid2: true,
uid3: true,
uid4: true, // <--- added.
}
}
The request
const selfUserId = 'uid4';
db.runTransaction(function(transaction) {
return transaction.update(userDocRef).then(function(userDoc) {
if (!userDoc.exists) { throw "Document does not exist!"; }
transaction.update(userDocRef, 'connectedUsers.${selfUserId}', true);
});
}
My understanding of how rules work:
request.resource.dara
is the entire
target document after
the change.
For update
operation, the above remains true. I don't quite understand what the Docs mean by:
For update operations that only modify a subset of the document fields, the
request.resource
variable will contain the pending document state after the operation.
ref
My rules: (See update below)
function existingData() { return resource.data }
function expectedData() { return request.resource.data }
uid
is added after the update.function isAddingRequester() {
return expectedData().connectedUsers[requesterId()] != null
}
1
or 0
item is added to connectedUsers
after the update. 0
for if the requester is already in the list.function isAddingOneAtMost() {
return expectedData().connectedUsers.size() == existingData().connectedUsers.size() + 1
|| expectedData().connectedUsers.size() == existingData().connectedUsers.size()
}
function isNotChangingOtherFields() {
return expectedData().id == existingData().id
&& expectedData().profile == existingData().profile
}
My questions
Are my understandings of how Firestore rules work correct? What does the doc referenced above mean by pending document state
?
Are my rule implementations reflecting my intentions? I am confused after searching around and learned that the simulator may have bug.
in my isNotChangingOtherFields
funciton, am I able to compare the profile
object directly with the ==
operator?
Removed existingData()
and expectedData()
.
function isAddingRequester() {
return request.resource.data.connectedUsers[requesterId()] != null
}
function isAddingOneAtMost() {
return (request.resource.data.connectedUsers.size() == resource.data.connectedUsers.size() + 1)
|| (request.resource.data.connectedUsers.size() == resource.data.connectedUsers.size()) // NOTE: if the requester is already in the list.
}
function isNotChangingOtherFields() {
return request.resource.data.profile == resource.data.profile
&& request.resource.data.id == resource.data.id
}
function isNotAddingOtherFields() {
return request.resource.data.size() == resource.data.size()
}
Debugging results
Interestingly, the results are NOT the same in the simlator and in production.
// PASSED in simulator & production:
allow update: if isAddingRequester();
// PASSED in simulator but NOT production:
allow update: if isNotChangingOtherFields();
// PASSED in simulator but NOT production:
allow update: if isNotAddingOtherFields();
// FAILED in both simulator AND production:
allow update: if isAddingOneAtMost();
// NOTE: inserted 2 mock data before update.
// PASSED in simulator:
allow update: if resource.data.connectedUsers.size() == 2;
// FAILED in simulator:
allow update: if request.resource.data.connectedUsers.size() == 3;
// PASSED in simulator:
allow update: if request.resource.data.connectedUsers.size() == 1;
Question
If request.resource
is the document after the update, why is request.resource.data.connectedUsers.size()
1 instead of 3
(2 existing + 1 the new added)?
Related finding (from simulator)
If I have a function:
expectedData() { return request.resource.data }
And I got such unexpected results:
// PASSED:
allow update: if request.resource.data.id == expectedData().id;
// FAILED if the order is changed.
allow update: if expectedData().id == request.resource.data.id;
Edit and update your rulesOpen the Firebase console and select your project. Then, select Realtime Database, Cloud Firestore or Storage from the product navigation, then click Rules to navigate to the Rules editor. Edit your rules directly in the editor.
Cloud Firestore provides a rules simulator that you can use to test your ruleset. You can access the simulator from the Rules tab in the Cloud Firestore section of the Firebase console. The rules simulator lets you simulate authenticated and unauthenticated reads, writes, and deletes.
It looks like there is a really helpful MapDiff type that might be able to simplify your rules. Sample code from the docs:
// Compare two Map objects and return whether the key "a" has been
// affected; that is, key "a" was added or removed, or its value was updated.
request.resource.data.diff(resource.data).affectedKeys().hasOnly(["a"]);
https://firebase.google.com/docs/reference/rules/rules.MapDiff
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With