Reputation: 967
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.
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;
Upvotes: 7
Views: 3500
Reputation: 702
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
Upvotes: 10
Reputation: 31
In Firestore, request.resource
doesn't represent document after update (well, for create it does, since all fields are specified there). It represents incoming data, data you want changed in the document, with maybe some additional fields...
To get the document after the fields in question are updated, you need to use getAfter(/databases/$(database)/documents/path-to-doc).data
So expected data should be changed to:
function expectedData(path) {
return getAfter(/databases/$(database)/documents/$(path));
}
Path
represents path which you actually have control over, while database
variable is defined at the top of the rules by default for the database name...
Upvotes: 0