Firestore rules - Allow only update certain field of a document

Viewed 2696

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 }
  • check if the requester's uid is added after the update.
function isAddingRequester() {
  return expectedData().connectedUsers[requesterId()] != null
}
  • check if only 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()
}
  • check if all other fields of the user document are not changed after the update.
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?


Update - 2018/01/17 3PM

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;

2 Answers

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

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...

Related