22

I am using Stripe for payments. For this, I have the following data model in Firestore:

Users/{userId}/payments/{document}

each {document} is an object that looks like:

{
  amount: 55
  token: {...}
  charge: {...}
}

Users must be able to to write the token field (this is what gets passed to the server), but I don't want users to be able to write the charge field.

Currently my rules allow any user to read and write to this document:

match /payments/{documents} {
  allow read, write: if request.auth.uid == userId;
}

What Firestore Rules will achieve my desired security?

Frank van Puffelen
  • 565,676
  • 79
  • 828
  • 807
astrojams1
  • 1,523
  • 2
  • 15
  • 29
  • 3
    https://stackoverflow.com/questions/47738514/firestore-security-for-the-update-of-specific-field-within-document - https://stackoverflow.com/questions/47922055/cloud-firestore-security-rules-single-protected-field-in-a-document – Doug Stevenson Jan 18 '18 at 05:51

4 Answers4

31

I believe something along the following would work, it allows clients to update fields except for charge, as well as create documents that don't have the charge field.

service cloud.firestore {
  match /databases/{database}/documents {
    function valid_create() {
        return !(request.resource.data.keys().hasAll(["charge"]));
    }

    function valid_update() {
        return request.resource.data.charge == resource.data.charge
               || (valid_create()
                  && !(resource.data.keys().hasAll(["charge"])))
    }

    match /payments/{userId} {
        allow read: if request.auth.uid == userId;
        allow create: if request.auth.uid == userId
                        && valid_create(); 
        allow update: if request.auth.uid == userId
                        && valid_update(); 
    }
  }
}
Ahmad Awais
  • 33,440
  • 5
  • 74
  • 56
Dan McGrath
  • 41,220
  • 11
  • 99
  • 130
  • 3
    You're referring to `userId` but it's not defined. For clarity you might want to change `{database}` to `{userId}`. – Paul Dec 28 '18 at 12:27
  • Great answer. I'd only update the `valid_create|update` methods with a check for an admin user namely: `return request.auth.token.role === 'admin' && !(request.resource...)` – JoeManFoo Dec 15 '19 at 23:46
8

The accepted answer does not work on update, in which request.resource.data returns a new map that's already merged with existing data. Because of that, request.resource.data.keys().hasAll([<<field>>]) always return true.

Regardless, what works for me is using .diff():

function meta_is_modified() {
      return 'meta' in resource.data.diff(request.resource.data).affectedKeys();
    }

Here's another version with multiple protected fields:

function invalid_modification() {
  return resource.data.diff(request.resource.data).affectedKeys().hasAny(['field1', 'field2'].toSet());
}
Derek Nguyen
  • 11,294
  • 1
  • 40
  • 64
6

Set type was announced, along with some other cool stuff.


Using Sets to ensure that a document only has fields "a", "b", and "c":

request.resource.data.keys().toSet() == ["a", "b", "c"].toSet()

Similarly, you could make sure that a document only has specified keys, but not others:

(request.resource.data.keys().toSet() - ["required","and","opt","keys"].toSet()).size == 0?`
m.spyratos
  • 3,823
  • 2
  • 31
  • 40
1

Thanks for the redirection to set type, it helps a lot. In my project I had to validate requests with any authorized key (one, many or all), but no extra key.

Here is what I did ; maybe it could help :

(request.resource.data.keys().toSet().intersection(
    ["a","b","c","d"].toSet()
)).size() == request.resource.data.keys().toSet().size();

Basicaly, I compared set and the intersection between my request set keys and a set which contained authorized keys. Since I wanted all the request fields to match those keys, primary set and intersection set must be equals.