czphilip
czphilip

Reputation: 967

Firestore rules - Allow only update certain field of a document

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:

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 }
function isAddingRequester() {
  return expectedData().connectedUsers[requesterId()] != null
}
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


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;

Upvotes: 7

Views: 3500

Answers (2)

Tom Bailey
Tom Bailey

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

ArdentAngel
ArdentAngel

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

Related Questions