skip to Main Content

I’m trying to set Firestore rules, allowing user to update only certain fields inside of a nested object. I know if it’s a top level key, we can simply use something like this:

allow update: if request.auth.uid == userID &&
    request.resource.data.diff(resource.data).affectedKeys().hasOnly(["key1", "key2"]);

However, I’m stuck with a problem restricting updates for keys inside an object, lets call it "someData". This is what I have tried, it seems logical, but it doesn’t work:

    allow update: if request.auth.uid == userID &&
 request.resource.data.diff(resource.data).affectedKeys().hasOnly("someData", "otherData") &&
         (!("someData" in request.resource.data.keys()) ||
              (request.resource.data["someData"].diff(resource.data["someData"])
                 .affectedKeys().hasOnly(["key1", "key2"]));

Or like this:

 allow update: if request.auth.uid == userID &&
request.resource.data.diff(resource.data).affectedKeys().hasOnly("someData", "otherData") &&
         (!("someData" in request.resource.data.keys()) ||
              (request.resource.data.someData.diff(resource.data.someData)
                 .affectedKeys().hasOnly(["key1", "key2"]));

Neither works.

Is there a way protecting nested fields?

Thanks!

I tried to test in test environment using "mocha", tried with real Firestore, tried to put this logic in separate function. Nothing works.

Firebase Docs only say about top level keys of the document.
I found one solution here, similar to my first example, but in real life it doesn’t seem to be working.

Am I doing anything wrong?

Update with more specific details

These are my original rules (working on a taxi booking app):

allow update: if request.auth.uid == userID && 
        editOnlyFields(["name", "phoneNumber", "driversData", "waitingDriverApproval", "lastUpdate"]) &&
        request.resource.data.lastUpdate == request.time &&
        (!("name" in request.resource.data.keys()) || 
          request.resource.data.name is string) &&
        (!("phoneNumber" in request.resource.data.keys()) || 
          request.resource.data.phoneNumber is string) &&
        (!("waitingDriverApproval" in request.resource.data.keys()) || 
          (request.resource.data.waitingDriverApproval is bool && request.resource.data.waitingDriverApproval == true)) &&
        //nested driversData (pain in the butt)
        (!("driversData" in request.resource.data.keys()) ||
          (request.resource.data.driversData.keys().hasOnly(["licensingAuthority", "amountOfVehicles", "DVLACheckCode", "DVLALicence", "PHBadge", "PHDriversLicence"])) && 
            (!("licensingAuthority" in request.resource.data.driversData.keys()) || 
              request.resource.data.driversData.licensingAuthority is string) &&
            (!("amountOfVehicles" in request.resource.data.driversData.keys()) || 
              request.resource.data.driversData.amountOfVehicles is int) &&
            (!("DVLACheckCode" in request.resource.data.driversData.keys()) || 
              (request.resource.data.driversData.DVLACheckCode.keys().hasOnly(["code", "pending"]) &&
                isValidDVLACheckCode(request.resource.data.driversData.DVLACheckCode))) && 
            (!("DVLALicence" in request.resource.data.driversData.keys()) || 
              (request.resource.data.driversData.DVLALicence.keys().hasOnly(["downloadUrl", "pending"]) &&
                isValidDriversPaperwork(request.resource.data.driversData.DVLALicence))) &&
            (!("PHBadge" in request.resource.data.driversData.keys()) || 
              (request.resource.data.driversData.PHBadge.keys().hasOnly(["downloadUrl", "pending"]) && 
                isValidDriversPaperwork(request.resource.data.driversData.PHBadge))) &&
            (!("PHDriversLicence" in request.resource.data.driversData.keys()) || 
              (request.resource.data.driversData.PHDriversLicence.keys().hasOnly(["downloadUrl", "pending"]) &&
                isValidDriversPaperwork(request.resource.data.driversData.PHDriversLicence))));

}

function editOnlyFields(allowedFields) {
      return request.resource.data.diff(resource.data).affectedKeys().hasOnly(allowedFields);
    }
  

It all went well, till I started working on admin part. the admin has auth token custom claim isBoss: true

Rules allow read and write if isBoss()

match /{document=**} {
      allow read, write: if isBoss();
    }
function isBoss() {
      return request.auth.token.isBoss == true;
    }

Now with original rules, I am facing a problem – when admin changes anything inside user’s driversData, something that user wouldn’t be authorized to do, like approvedDriver: true, then user is not able to update their driversData because request object contains all fields that would be present after update.

So, I started rules update with replacing this line:

request.resource.data.driversData.keys().hasOnly(allowedFields)

With this one:

(editOnlyFieldsNested("driversData", ["licensingAuthority", "amountOfVehicles", "DVLACheckCode", "DVLALicence", "PHBadge", "PHDriversLicence"]))

Where

function editOnlyFieldsNested(nestedObj, allowedFields) {
      return request.resource.data[nestedObj].diff(resource.data[nestedObj]).affectedKeys().hasOnly(allowedFields);
    }

When I run my Firestore emulator, this is the function that fails:

it("Can update a user document only with allowed fields", async () => {
    await testEnv.withSecurityRulesDisabled((context) => {
      return context.firestore().collection("users").doc(myId).set({
        name: "User Abc",
        email: "[email protected]",
        createdAt: serverTimestamp(),
      });
    });
    const testDoc = myUser.firestore().collection("users").doc(myId);
    await assertSucceeds(
      testDoc.update({
        name: "New Name",
        phoneNumber: "after",
        driversData: { amountOfVehicles: 1, licensingAuthority: "Bristol" },
        lastUpdate: serverTimestamp(),
      })
    );
  });

If I run this with old rules (without MapDiff), it passes.
If I use new rules with editOnlyFieldsNested() it only passes if I comment out driversData property, otherwise it gives me an error:

 Can update a user document only with allowed fields:



    FirebaseError: 7 PERMISSION_DENIED: 
Property isBoss is undefined on object. for 'update' @ L8, evaluation error at L17:24 for 'update' @ L17, Property isBoss is undefined on object. for 'update' @ L8, Property driversData is undefined on object. for 'update' @ L17

Perhaps it can be something to do on driversData undefined on object…
Why?
I’m stuck on this for a couple days, my laptop is covered with tears..

Any advise would be gratefully appreciated

2

Answers


  1. Chosen as BEST ANSWER

    I think, the main problem lies in the fact that .diff() method tries to compare 2 objects - request and resource. If resource doesn't have driversData object, test (and real request) will fail.

    I see 2 ways of solving. Either create all users with empty driversData, that also has nested objects, or rewrite rules that also work if driversData doesn't exist.


  2. The problem is in this code:

    testDoc.update({
      name: "New Name",
      phoneNumber: "after",
      driversData: { amountOfVehicles: 1, licensingAuthority: "Bristol" },
      lastUpdate: serverTimestamp(),
    })
    

    This sets driversData to { amountOfVehicles: 1, licensingAuthority: "Bristol" } and removes the existing data in driversData (which is why your rules reject it).

    If you want to update nested properties, use dot notation:

    testDoc.update({
      name: "New Name",
      phoneNumber: "after",
      "driversData.amountOfVehicles": 1, 
      "driversData.licensingAuthority": "Bristol",
      lastUpdate: serverTimestamp(),
    })
    

    Also see the documentation on updating nested properties.


    Here’s an example of me using the . notation in the rules playground when I was trying to reproduce your problem:

    enter image description here

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search