1

My app stores the user's role (subscriber, editor or admin) as a "roles" map within their user doc in the users collection of Firestore. The role map looks like this:

// Roles map for a subscriber
{
subscriber: true 
}

// Roles map for a editor
{
editor: true 
}

// Roles map for administrator
{
administrator: true 
}

I can also put multiple (or brand new) role fields within the user doc's role map as needed.

I want users to be able to update their profile without being able to change the role map. So for security rules I have tried ensuring this by checking that the user is the owner, and that the before and after roles are all the same. But I always get a permission error when trying to update. Here are the rules:

match /users/{userUid} {
  allow update: if 
  (
    isOwner(userUid) &&
    (
      (request.resource.data.roles.administrator == resource.data.roles.administrator) &&
      (request.resource.data.roles.editor == resource.data.roles.editor) &&
      (request.resource.data.roles.subscriber == resource.data.roles.subscriber)
    )
  );
}

The first isOwner(user) condition looks like this:

function isOwner(uid) {
    return (isSignedIn() && (request.auth.uid == uid));
}

I am confident this part is working because when I run it with only that, it works.

I suspect my issue may be that in cases where one of the roles fields (e.g. subscriber) doesn't exist before or after the write, it fails the equality check. So I also tried adding a condition to allow if the existing object doesn't have that field, but it still doesn't work:

match /users/{userUid} {
  allow update: if 
  (
    isOwner(userUid) &&
    (
      (!request.resource.data.roles.administrator || request.resource.data.roles.administrator == resource.data.roles.administrator) &&
      (!request.resource.data.roles.editor || request.resource.data.roles.editor == resource.data.roles.editor) &&
      (!request.resource.data.roles.subscriber || request.resource.data.roles.subscriber == resource.data.roles.subscriber)
    )
  );
}

Thanks in advance for any help.


UPDATE 1

I found a simpler solution using writeFields which works, but writeFields is deprecated so this is not a long-term solution:

allow update: if isOwner(userUid) && !('roles' in request.writeFields)

Again, writeFields is deprecated so should not be used.


UPDATE 2

This solution ended up working for me, replacing role with roles to match my case: https://stackoverflow.com/a/48214390/4407512

Greg
  • 619
  • 1
  • 8
  • 15
  • It would be helpful to see the *entire* document contents (maybe a screenshot?), and the *entire* rule (including the match spec), along with the code for the query that fails. We should be able to completely reproduce the situation from what you've given, and see it fail exactly like you are. – Doug Stevenson Apr 20 '19 at 19:21
  • Also, did you test this in the console simulator? Did it give you a more specific error message that might indicate what specifically failed? – Doug Stevenson Apr 20 '19 at 19:22
  • Thanks for the good questions. I had not tried the simulator, and of course should have. When I do, it gives the error: "Property administrator is undefined on object". So that explains it. I need to have *all* roles identified in the roles map, but only set "true" for the one(s) that should be assigned to the user. Also, the update process must include each of those same roles, so that the equality check on each (ensuring no roles have changed) will pass. Unless there's another way to write the logic? I updated the original post to include the match spec. – Greg Apr 22 '19 at 13:53

2 Answers2

1

You can now use map diffs to solve this without using the deprecated 'writeFields'

allow update: if isOwner(userUid) && !('roles' in request.resource.data.diff(resource.data).affectedKeys());
Scott Crossen
  • 949
  • 8
  • 7
0

request.resource.data is a Map type object. As you said, you can ensure that the properties always exist, or you can check to see if the property exists in the Map before using them. The in operation on Map objects will let you know if a property exists. See the documentation on Map for more details.

"administrator" in request.resource.data   // true if administrator property exists
Doug Stevenson
  • 297,357
  • 32
  • 422
  • 441
  • Thanks Doug. In the simulator, if `roles` is not included in the incoming document, the rules skip all the equality checks on the properties of roles (e.g. `request.resource.data.roles.administrator == resource.data.roles.administrator`). But the moment `roles` is included in the incoming document, the checks are run. So, since only admins should ever include roles in the incoming document, I changed the rule to `allow update if: isOwner(userUid) && request.resource.data.roles.administrator == resource.data.roles.administrator` and it works (equality is skipped if the user doesn't add roles). – Greg Apr 25 '19 at 10:35
  • If you try to reference a property of a document that doesn't exist, it results in an error, and your rule will reject access. That's why you should check if something exists before using it. – Doug Stevenson Apr 25 '19 at 12:53
  • I [found](https://stackoverflow.com/questions/52262195/firestore-rules-validate-data-does-not-have-field) a simpler solution using a rule I didn't know existed: `writeFields`. Now my rule is simply: `allow update: if isOwner(userUid) && !('roles' in request.writeFields)`. Strange that there is no reference to `writeFields` in the docs. I searched and it is only mentioned on an [index page](https://firebase.google.com/docs/reference/rules/index-all) with no details. – Greg Apr 26 '19 at 00:23
  • writeFields is undocumented, deprecated and you shouldn't use it any more. It's not guaranteed to work the way you expect. – Doug Stevenson Apr 26 '19 at 05:01
  • Well that’s too bad then. Is there a similar method that could let me simply check if the incoming document has a roles object? – Greg Apr 26 '19 at 13:54
  • Did my suggestion of using `"foo" in request.resource.data` not work? I don't see that you tried it. – Doug Stevenson Apr 26 '19 at 14:07
  • I tried that without success in my app, nor in the simulator. I tried several variants, as well as with just resource.data, without success either. – Greg Apr 27 '19 at 14:54
  • I ended up using [this](https://stackoverflow.com/a/48214390/4407512) and it works for me. – Greg Apr 27 '19 at 15:03
  • Looks like you are using the `in` operator to check for existence, as I suggested. – Doug Stevenson Apr 28 '19 at 08:31
  • I did try that and intuitively expected it to work, but without any luck. It needed to be followed by the rest of the logic in the response link: return 'roles' in resource.data && 'roles' in request.resource.data && resource.data.roles != request.resource.data.roles; – Greg Apr 29 '19 at 10:59
  • Right, it's not going to work by itself. It's just an expression you can use in your boolean logic to find which fields changed. – Doug Stevenson Apr 29 '19 at 15:41