29

I'm implementing a recipe book in Firestore where every user is able to see all the recipes all users created but only the original author of the recipe is allowed to edit or delete the recipe. Any user is also allowed to create a new recipe.

My problem is that I am unable to setup the permissions a subcollection to "listen" on a field of the subcollections parentdocument.

Each recipe document contains three things. A field called name where the name of the recipe is stored, a field called creatorUID where the request.auth.uid of the creators uid is stored and a subcollection called ingredients containing documents with some random fields.

service cloud.firestore {
  match /databases/{database}/documents {

    function isSignedIn() {
      return request.auth != null;
    }

    match /ListOfRecipes/{recipe} {
        allow read, create: if isSignedIn();
        allow update, delete: if resource.data.creatorUID == request.auth.uid;

        match /{list=**} {
            allow read: if isSignedIn();
            // Should return true if recipe.creatorUID has same value as request.auth.uid
            allow write: if recipe.creatorUID == request.auth.uid;
        }
    }
  }
}

The problem is that with these rules it only works to create the recipe document. The subcollection and it's documents are not created since the db says

FirebaseError: [code=permission-denied]: Missing or insufficient permissions. FirebaseError: Missing or insufficient permissions.

The calls is made from Angular client and it's official library.

Dan McGrath
  • 41,220
  • 11
  • 99
  • 130
Payerl
  • 1,042
  • 2
  • 16
  • 33

3 Answers3

64

Rules don't cascade, so you'll need to perform whatever checks you need for the document being captured by the Rules.

Generally speaking, {x=**} rules are more often a mistake and the usage of =** only for extremely specific use cases.

From your question, I'm assuming your data mode is something like this:

/ListofRecipes/{recipe_document}/List/{list_document}

In this case, you'll need your Rules to be configured something like this:

service cloud.firestore {
  match /databases/{database}/documents {

    function isSignedIn() {
      return request.auth != null;
    }

    match /ListOfRecipes/{recipe} {
        allow read, create: if isSignedIn();
        allow update, delete: if resource.data.creatorUID == request.auth.uid;

        function recipeData() {
            return get(/databases/$(database)/documents/ListOfRecipes/$(recipe)).data
        }

        match /List/{list} {
            allow read: if isSignedIn();
            allow write: if recipeData().creatorUID == request.auth.uid;
        }
    }
  }
}
Jacobo Koenig
  • 11,728
  • 9
  • 40
  • 75
Dan McGrath
  • 41,220
  • 11
  • 99
  • 130
  • 12
    @Payerl This is a great solution, however, there is one issue with this that you should be VERY aware of. Each time you run the `recipeData()` function it counts against the number of reads that you're billed for. So if that sub-collection is written to often it will get SUPER expensive very quickly. – DevShadow Jul 22 '19 at 02:40
  • Is there any better way to do this? – tensor Jun 06 '20 at 08:34
  • can you confirm that this still works? I've got the feeling that lately (since today actually) the nested path also executes the parent rule and fails in your example when trying to access resource.data.creatorUID. – Martin Cremer Jun 22 '20 at 13:03
4

Dan's answer above works great! Just for reference, in my case I only needed the root parent document ID, you can use the variable from the match statement above the nested one, like this:

service cloud.firestore {
  match /databases/{database}/documents {

    function isSignedIn() {
      return request.auth != null;
    }

    match /ListOfRecipes/{recipeID} {
        allow read, create: if isSignedIn();
        allow update, delete: if resource.data.creatorUID == request.auth.uid;

        match /List/{list} {
            allow read: if isSignedIn();
            allow write: if  recipeID == 'XXXXX';
        }
    }
  }
}
sMyles
  • 2,418
  • 1
  • 30
  • 44
  • Do you not need to write it as `$(recipeID)` per Dan's `recipeData()` function? Do both `recipeID` and `$(recipeID)` work here, or only `recipeID`? – Sam Mar 02 '20 at 14:13
1

Building upon Dan's answer, you should be able to reduce the number of reads on your database for update and delete on the subcollection by adding the creatorUID to the subcollection document.

You'll have to restrict create to just the creator and make sure the creatorUID is set. Here's my modification of Dan's rules:

service cloud.firestore {
  match /databases/{database}/documents {

    function isSignedIn() {
      return request.auth != null;
    }

    match /ListOfRecipes/{recipe} {
        allow read, create: if isSignedIn();
        allow update, delete: if resource.data.creatorUID == request.auth.uid;

        function recipeData() {
            return get(/databases/$(database)/documents/ListOfRecipes/$(recipe)).data
        }

        match /List/{list} {
            allow read: if isSignedIn();
            allow update, delete: if resource.data.creatorUID == request.auth.uid;
            allow create: if recipeData().creatorUID == request.auth.uid
                          && request.resource.data.creatorUID == request.auth.uid;
        }
    }
  }
}
Cody
  • 650
  • 9
  • 16