9

I have notification records where there is a text and a list of users (max 10).

{text: "Beware of the dog", users: [ uid1, uid2, uid3, ... ]}

When a user read/acknowledge the notification, I want to remove him from the list of users who can see the notification (then he won't get any anymore).

For that, when the user press the "hide notification button", he send a request to update the notification record with:

users: FieldValue.arrayRemove(uid)

I want to enfore with security rules that the user:

  • Doesn't change other part of the notification record.
  • Send its uid and only its uid in the arrayRemove part.

Tried with

allow update: if 
    request.auth.uid != null 
    && request.auth.uid in resource.data.users 
    && request.resource.size() == 1 
    && request.resource.data.users != null;
  • The request.resource.size == 1 doesn't work. Can't figure out why as I have only one field in my request.
  • I have no way to ensure the arrayRemove is strictly limited to its uid.

Any hint, help, idea well appreciated.

Franck
  • 257
  • 2
  • 10

2 Answers2

16

I had a similar situation and it was quite a brain teaser. This is what did the trick for me:

allow update: if 
    request.auth.uid != null 
    && request.resource.data.diff(resource.data).affectedKeys().hasOnly([data])
    && request.resource.data.users.size() == resource.data.users.size() - 1
    && resource.data.users.removeAll(request.resource.data.users)[0] == request.auth.uid

Specifically:

  1. The first rule is the one you had - checks for user authentication
  2. The second rule checks the difference between the new and the old document and makes sure that the only key affected is the one named data
  3. The third rule makes sure that the new array is exactly 1 item shorter than the old one (so that only 1 item can be removed at a time)
  4. The fourth rule subtracts the new array (now reduced by 1 uid) from the old one with removeAll() and returns an array with their difference. In this case, it returns an array which contains only the single uid that you chose to arrayRemove(). Then we simply check that uid - which can only exist at position [0] - and make sure it is equal to the uid of the authenticated user.
Konstantinos T.
  • 415
  • 5
  • 11
  • shouldn't your 3rd rule be just `request.resource.data.users.size() == 1` because there should only be one item in the request? – Anand Rockzz Mar 27 '21 at 17:28
  • ah! looks like `FieldValue.arrayUnion/FieldValue.arrayRemove` does some magic to the `request.resource.data` before validation occurs. Looks like it has all previous values. Your #3 makes total sense now. Thank you – Anand Rockzz Mar 27 '21 at 17:49
  • The second line should be request.resource.data.diff(resource.data).affectedKeys().hasOnly(['users']) – Edwin Liu Apr 09 '21 at 02:38
1

I don't think this is possible without a loop, which doesn't exist in security rules. Well: if you know all users, you might be able to enumerate all options, essentially unfolding the impossible loop. But even if this is possible in security rules, the rules are going to be incredibly verbose.

I'd recommend creating a subcollection where each UID is stored in a separate document. In that subcollection you can then implement your requirement by only allowing the user to only delete their own document.

Frank van Puffelen
  • 565,676
  • 79
  • 828
  • 807
  • 1
    ok, thanks for the answer. I guess Firestore will evolve and provide a solution to this at some point. – Franck Feb 20 '19 at 14:51