firebasegoogle-cloud-firestorefirebase-security

Firestore rules for "arrayUnion" update giving permission denied error


In my code I'm updating a user's "decks" list by adding one item:

const deckId = 'fake-deck-id';
await updateDoc(doc(db, 'users', userId), {
  decks: arrayUnion(deckId),
});

However, this is giving a permission-denied error. Here are my firestore rules:

  // Allow updates only to the "decks" and "votes" lists
  allow update: if request.auth != null &&
  request.auth.uid == userId &&
  (
  (
  // Allow updates to "decks" using array operations
  request.resource.data.keys().hasOnly(['decks']) &&
  request.resource.data.decks is list &&
  request.resource.data.decks.size() == resource.data.decks.size() + 1
  ) ||
  (
  // Allow updates to "votes" using array operations
  request.resource.data.keys().hasOnly(['votes']) &&
  request.resource.data.votes is list &&
  request.resource.data.votes.size() == resource.data.votes.size() + 1
  )
  );

I can't figure out why this isn't working. Does anyone have any ideas?


Solution

  • I think you may be misunderstanding how request.resource works. The request.resource contains the entire document as it will exist after the write operation (if that operation is allowed).

    So this line in your rules:

    request.resource.data.keys().hasOnly(['decks']) &&
    

    This says that the document after it has been updated may only contain a decks field. If there are any other fields in the document after the update, the update is rejected.

    If you only want the decks field to be updated, you'll need to use affectedKeys function. There are some good examples of this in the Firebase documentation on restricting what fields can be updated too. For example, here's how it allows only certain fields to be updated:

    service cloud.firestore {
      match /databases/{database}/documents {
        match /restaurant/{restId} {
        // Allow a client to update only these 6 fields in a document
          allow update: if (request.resource.data.diff(resource.data).affectedKeys()
            .hasOnly(['name', 'location', 'city', 'address', 'hours', 'cuisine']));
        }
      }
    }