Why are my Firebase Firestore Security Rules allowing a .set() operation on an existing document, but the .set() has missing fields

Viewed 102

This is my first question asked on stackoverflow, so please let me know if I am missing some aspect of the question that is preferred or required.

My goal is to understand why the mocha test shown below is passing when the created document is missing fields required by the firestore rules listed below.

Mocha Test Code (Note that the project ID is checked and working but omitted here)

const assert = require("assert");
const firebase = require("@firebase/rules-unit-testing");
const { firestore } = require("firebase-admin");
const { debug } = require("console");

const MY_PROJECT_ID = "####";

const myID = "user_abc";
const theirID = "user_def";

const myAuth = {uid: myID, email: "abc@gmail.com"};
const theirAuth = {uid: theirID};

function getFirestore(auth) {
    return firebase.initializeTestApp({projectId: MY_PROJECT_ID, auth: auth}).firestore();
}


function getAdminFirestore() {
    return firebase.initializeAdminApp({projectId: MY_PROJECT_ID}).firestore();
}


beforeEach(async ()=>{
    await firebase.clearFirestoreData({projectId:MY_PROJECT_ID});
});


describe("Our Social App", () => {

    it("Can overwrite an existing post with allowed fields", async()=>{
        const postPath = "/posts/post_123";
        const admin = getAdminFirestore();
        await admin.doc(postPath).set({authorID:myID, content:"content", 
                                       visibility:"private",
                                       headline:"headline"});

        const db = getFirestore(myAuth);
        const docRef = db.doc(postPath);
        await firebase.assertSucceeds(docRef.set({}));
    });

});

after(async ()=>{
    await firebase.clearFirestoreData({projectId:MY_PROJECT_ID});
});

Firestore Rules

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {

    function documentFieldsCheckOut(requiredFields, allowedFields){
      let requiredAndAllowed = requiredFields.concat(allowedFields);
      return request.resource.data.keys().hasAll(requiredFields) &&
             request.resource.data.keys().hasOnly(requiredAndAllowed);
    }

    function updateHasOnlyAllowedFields(allowedFields){
      return debug(request.resource.data.keys()).hasOnly(allowedFields);
    }

    match /{document=**} {
      allow read, write: if false;
    }

    match /posts/{postID} {
   
      allow update: if ((resource.data.authorID == request.auth.uid) || userIsModerator()) 
                        && updateHasOnlyAllowedFields(["visibility", "content"]);
      
      allow create: if (request.resource.data.authorID == request.auth.uid) &&
                        documentFieldsCheckOut(["authorID", "content", "visibility", "headline"], 
                                                ["photo", "location", "tags"]);
    }

 }
}

Note: Commenting out await admin.doc(postPath).set({authorID:myID, content:"content", visibility:"private", headline:"headline"}); causes the test to fail as expected.
Additionally, changing the .set to .update in await firebase.assertSucceeds(docRef.set({})); also causes the test to fail as expected.

1 Answers

If I understand you correctly you're asking why this call succeeds:

const admin = getAdminFirestore();
admin.doc(postPath).set({authorID:myID, content:"content", 
                                   visibility:"private",
                                   headline:"headline"});

Calls using administrative privileges bypass the security rules of your database. So no matter what your security rules, this call will not be rejected by them.


The second write operation is different:

const db = getFirestore(myAuth);
const docRef = db.doc(postPath);
docRef.set({})

This time you're going though a non-admin ref as far as I can see, which means the write will have to adhere to the security rules to be accepted.

Related