Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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:

  • 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;

like image 313
czphilip Avatar asked Jan 17 '19 04:01

czphilip


People also ask

How do you update firestore rules?

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.

How do I set rules for firestore database?

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.


1 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

like image 52
Tom Bailey Avatar answered Sep 26 '22 19:09

Tom Bailey