Making sense of Firestore Security Rules - only allowing update for certain fields

181

I only want user1 to be able to update this single field of user2's.

A general way of allowing updation of only certain fields can be with checking diff of request.resource.data with resource.data.

Example documents that need to be updated have 3 field: name, city, standingBalance. We want anyone to change standingBalance but only the corresponding user to change his name and city.

The below example is considering that user1 should have complete write access to user1's sendRequest & only single field update access to user2's receivedRequests, there won't be much change in other scenarios.

match /userMetadata/{uid} {
  allow read: if uid == request.auth.uid || uid == 'PREVIEW';
  allow write: if uid == request.auth.uid && uid != 'PREVIEW';
  match /receivedRequests/{req} {
    allow read: if uid == request.auth.uid;
    allow write: if uid == request.auth.uid;
    allow update: if uid == request.auth.uid 
                 || (request.resource.data.keys().hasOnly(['name','city','standingBalance']) 
                     && request.resource.data.name == resource.data.name
                     && request.resource.data.city == resource.data.city
                     && request.resource.data.standingBalance != resource.data.standingBalance
                     && request.auth.uid != null
                    )
  }
  match /sentRequests/{req} {
    allow read: if uid == request.auth.uid;
    allow write: if request.auth != request.auth.uid || request.auth.uid != 'PREVIEW';
  }

Checking with hasOnly ensures that no other key is added to the document. Also, I am unaware of what 'PREVIEW' is so do verify the rules yourself.

Having a fieldsChanged variable in firestore rules would be a good feature request ;)

Share:
181
AlexK
Author by

AlexK

Updated on December 21, 2022

Comments

  • AlexK
    AlexK over 1 year

    I am trying to use the Firestore security rules to edit some data, as follows, with a transaction in Flutter:

      Future sendRequest(uidSend, uidRec, pid, title) async {
        final crSend = ChatRequest(uid: uidRec, pid: pid, title: title);
        var _lsSend = List();
        _lsSend.add(crSend.toJson());
    
        final crRec = ChatRequest(uid: uidSend, pid: pid, title: title);
        var _lsRec = List();
        _lsRec.add(crRec.toJson());
    
        final uMSendDocref = userMetadataCollection.document(uidSend);
        final uMRecDocref = userMetadataCollection.document(uidRec);
    
        Firestore.instance.runTransaction((transaction) async {
          await transaction.update(uMSendDocref, <String, dynamic>{
            "sentRequests": FieldValue.arrayUnion(
              _lsSend,
            ),
          });
          await transaction.update(uMRecDocref, <String, dynamic>{
            "receivedRequests": FieldValue.arrayUnion(
              _lsRec,
            ),
          });
        });
      }
    

    Notice that user1 is trying to update both his/her own data, as well as user2's. However, I only want user1 to be able to update this single field of user2's. I make my Firestore rules as such:

        match /userMetadata/{uid} {
          allow read: if uid == request.auth.uid || uid == 'PREVIEW';
          allow write: if uid == request.auth.uid && uid != 'PREVIEW';
          match /receivedRequests {
            allow read: if uid == request.auth.uid;
            allow write: if request.auth != null && request.auth.uid != 'PREVIEW';
          }
          match /sentRequests {
            allow read: if uid == request.auth.uid;
            allow write: if request.auth != null && request.auth.uid != 'PREVIEW';
          }
        }
    

    receivedRequests (and sentRequests) only require that a user has a non-null auth to edit, ie, any user should be able to edit. However, I get a permissions error when running my transaction. Why is that? Perhaps I am misunderstanding Firestore rules? Perhaps the transaction is trying to do a read? Any thoughts?

    UPDATE:

    I tried using a batch:

      Future sendRequest(uidSend, uidRec, pid, title) async {
        //update own uM with post
        //update other uM with user
    
        final crSend = ChatRequest(uid: uidRec, pid: pid, title: title);
        var _lsSend = List();
        _lsSend.add(crSend.toJson());
    
        final crRec = ChatRequest(uid: uidSend, pid: pid, title: title);
        var _lsRec = List();
        _lsRec.add(crRec.toJson());
    
        final uMSendDocref = userMetadataCollection.document(uidSend);
        final uMRecDocref = userMetadataCollection.document(uidRec);
    
        var batch = Firestore.instance.batch();
    
        batch.updateData(uMSendDocref, <String, dynamic>{
          "sentRequests": FieldValue.arrayUnion(
            _lsSend,
          ),
        });
    
        batch.updateData(uMRecDocref, <String, dynamic>{
          "receivedRequests": FieldValue.arrayUnion(
            _lsRec,
          ),
        });
    
        return await batch.commit();
      }
    

    Still does not work. Something is either incredibly unintuitive with Firestore, or there is a serious bug.

    Another thing to note: some of the userMetadata might not currently have the fields that are being updated.