No success with Firestore .data access at get() path

3,487 views
Skip to first unread message

Danny Pier

unread,
Oct 16, 2017, 12:01:50 AM10/16/17
to Firebase Google Group
Hi,

I know it's in beta, but this looks possible due to the documentation and can't get things to work.

Here is my rule set:

service cloud.firestore {
  match /databases/{database}/documents {
    match /users/{userId} {
      allow read, write: if request.auth.uid == userId;
      match /{allChildren=**} {
        allow read, write: if request.auth.uid == userId;
      }
    }
    match /domain_group/{allChildren=**} {
      allow read, write: if get(/databases/$(database)/documents/users/$(request.auth.uid)).data.role == 20;
    }
  }
}

I know 100% that the role parameter in that document is equal to 20. The rule evaluates to false with all fields in the document. For example, data.firstName == "Kaylo" which I also 100% know to be the case also fails.

If I switch the same path to exists() from get(), then the rule is successful, so Firestore confirms for me that the document is indeed where I think it is.

What am I missing, any suggestions?

Thanks,
-Danny

MMLLEVVY

unread,
Oct 16, 2017, 10:54:31 AM10/16/17
to Firebase Google Group
I can confirm that issue. I just reported it to firebase support team. It seems, that when fetching single document, security rules function get() returns nothing or error is thrown.

I've made a test case:

The rules are very simple:
service cloud.firestore {
  match /databases/{database}/documents {
    
    match /rules/{ruleId} {
      allow read: 
          if exists(/databases/$(database)/documents/rules2/$(ruleId)) && 
          get(/databases/$(database)/documents/rules2/$(ruleId)).data.visible == true;
    }
    
    match /rules2/{ruleId} {
      allow read, write: if true;
    }
  }
}

And the data:
/rules/rule1 = {id: "test1"}
/rules/rule2 = {id: "test2"}
/rules2/rule1 = {id: "test1", visible: true}
/rules2/rule2 = {id: "test2"}

Mike McDonald

unread,
Oct 16, 2017, 1:10:45 PM10/16/17
to Firebase Google Group
This is due to some brokenness of get().data.<property> because (during EAP) we supported this as get().<property>.

In order to solve this, you need to add a condition somewhere that references resource.data or request.resource.data, which will let Rules know to use the appropriate get().data.<property> syntax.

Something as simple as:

service cloud.firestore {
  match /databases/{database}/documents {
    
    match /rules/{ruleId} {
      allow read: 
          if exists(/databases/$(database)/documents/rules2/$(ruleId)) && 
          get(/databases/$(database)/documents/rules2/$(ruleId)).data.visible == true;
    }
    
    match /rules2/{ruleId} {
      allow read, write: if true;
    }

    match /rules3/{ruleId} {
      allow read: if resource.data.propertyThatDoesntExist && false;
    }
  }
}

This will be fixed appropriately in the near future, we promise :)

Thanks,
--Mike

MMLLEVVY

unread,
Oct 18, 2017, 4:48:29 PM10/18/17
to Firebase Google Group
Mike, thanks, but your solution doesn't work... at least not in all cases. Try following rules:
service cloud.firestore {
  match /databases/{database}/documents {
   
    match /rules/{ruleId} {
      allow read:
        if 
          (exists(/databases/$(database)/documents/rules2/$(ruleId)/subcollection/$(ruleId)/subcollection/$(ruleId)) 
          && "someNotExistingField" in get(/databases/$(database)/documents/rules2/$(ruleId)/subcollection/$(ruleId)/subcollection/$(ruleId)).data
          && get(/databases/$(database)/documents/rules2/$(ruleId)/subcollection/$(ruleId)/subcollection/$(ruleId)).data.someNotExistingField == true)
          ||
          (
          exists(/databases/$(database)/documents/rules2/$(ruleId)) 
          && "visible" in get(/databases/$(database)/documents/rules2/$(ruleId)).data
          && get(/databases/$(database)/documents/rules2/$(ruleId)).data.visible == true
          )
    }
    
    match /rules2/{ruleId} {
      allow read, write: if true;
    }
    
        match /rules3/{ruleId} {
      allow read: if resource.data.propertyThatDoesntExist && false;
        }    
  }
}

And the data:
/rules/test1 = {id: "test1"}
/rules/test2 = {id: "test2"}
/rules2/test1 = {id: "test1", visible: true}
/rules2/test1/subcollection/test1/subcollection/test1 = {id: "test1"}
/rules2/test2 = {id: "test2"}

Now, try to access /rules/test1 (using firebase-js-sdk .doc().get()) - it will fail, probably because of an error in get(), "in" statement or not existing field... can't tell, cause no simulator, no logs... 
Working example:

MMLLEVVY

unread,
Oct 30, 2017, 3:28:29 PM10/30/17
to Firebase Google Group
Hi, anyone from firebase can look at this issue? It is really urgent, as I cannot properly secure data access. I made fully reproducible test case - please look at it and either tell me where I make mistake or confirm that it is a bug.

Steps to reproduce:
1. Download following files and save them in one directory:

(You can adjust firebase settings to match your firebase account)

2. Please set firestore rules as follows:
service cloud.firestore {
  match /databases/{database}/documents {
    
    match /rules/{ruleId} {
      
      // this rule should always fail, cause "notExistingField" not exists
      allow read: if 
       exists(/databases/$(database)/documents/rules2/$(ruleId)/subcollection1/$(ruleId)/subcollection2/$(ruleId)) 
       && "notExistingField" in get(/databases/$(database)/documents/rules2/$(ruleId)/subcollection1/$(ruleId)/subcollection2/$(ruleId)).data
       && get(/databases/$(database)/documents/rules2/$(ruleId)/subcollection1/$(ruleId)/subcollection2/$(ruleId)).data.notExistingField != null
      ;
          
      // this rule will fail for "test2", but for "test1" should return true
      allow read: if
        exists(/databases/$(database)/documents/rules2/$(ruleId)) 
        && "visible" in get(/databases/$(database)/documents/rules2/$(ruleId)).data
        && get(/databases/$(database)/documents/rules2/$(ruleId)).data.visible == true
      ;
          
     allow write: if true;
    }
    
    match /rules2/{ruleId=**} {
      allow read, write: if true;
    } 
    
    //  Mike's fix for get() issues
    match /rules3/{ruleId} {
      allow read: if resource.data.propertyThatDoesntExist && false;
    }    
  }
}

3. Run test-rules.html in latest Chrome browser

Expected result:
It should be possible to read /rules/test1 document

Actual result:
Not able to read /rules/test1 - "missing or insufficient permissions" error is thrown.
create /rules/test1
create /rules/test2
create /rules2/test1
create /rules2/test1/subcollection1/test1
create /rules2/test1/subcollection1/test1/subcollection2/test1
create /rules2/test1/subcollection1/test1/subcollection2/test1/subcollection3/test1
create /rules2/test2
1: FirebaseError: [code=permission-denied]: Missing or insufficient permissions.
2: FirebaseError: [code=permission-denied]: Missing or insufficient permissions.

You can see results below:

Possible cause of the issue:
Look at the rule for "/rules/{ruleId}" - there are two "allow read" expressions. First one should return false always, but the second one should return true for document "/rules/test1" and according to the docs if at least one allow read expression returns true it will be granted to read the document... but for some reasons both expressions fails and error is thrown. When you comment first expression or first expression's 2nd and 3rd line (those starting with &&) it will work as expected, you will be able to read document /rules/test1.

Ps. I reported this issue to firebase support, but no luck getting the answer/help.

Kato Richardson

unread,
Oct 31, 2017, 2:35:22 PM10/31/17
to Firebase Google Group
Marek,

Do you have sample data to go along with that? Can you narrow the scope by finding out which of the three conditions in that rule is failing? (i.e. if you run them one at a time, does any particular one fail?)

☼, Kato

--
You received this message because you are subscribed to the Google Groups "Firebase Google Group" group.
To unsubscribe from this group and stop receiving emails from it, send an email to firebase-talk+unsubscribe@googlegroups.com.
To post to this group, send email to fireba...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/firebase-talk/a38f8516-dc76-4c09-8d6a-880d967a72e3%40googlegroups.com.

For more options, visit https://groups.google.com/d/optout.



--

Kato Richardson | Developer Programs Eng | kato...@google.com | 775-235-8398

MMLLEVVY

unread,
Nov 1, 2017, 2:33:05 AM11/1/17
to Firebase Google Group
Hi Kato, I'm glad that you answered. 

In my previous post I described step by step how to replicate the issue - setting the rules, adding the data and fetching the data. Just download those files:

In test-rules.js you should modify firebase settings, to match your firebase account (project id etc). When you run test-rules.html in latest Chrome, you will notice that this is complete test case - first, the data are added to firestore and after that, data are fetched from the firestore. Of course, before you run the test case you must set firestore rules:

service cloud.firestore {
  match /databases/{database}/documents {
    
    match /rules/{ruleId} {
      
      // this rule should always fail, cause "notExistingField" not exists
             allow read: if 
      exists(/databases/$(database)/documents/rules2/$(ruleId)/subcollection1/$(ruleId)/subcollection2/$(ruleId)) 
      && "notExistingField" in get(/databases/$(database)/documents/rules2/$(ruleId)/subcollection1/$(ruleId)/subcollection2/$(ruleId)).data
      && get(/databases/$(database)/documents/rules2/$(ruleId)/subcollection1/$(ruleId)/subcollection2/$(ruleId)).data.notExistingField != null
      ;
          
      // this rule will fail for "test2", but for "test1" should return true
      allow read: if
        exists(/databases/$(database)/documents/rules2/$(ruleId)) 
        && "visible" in get(/databases/$(database)/documents/rules2/$(ruleId)).data
        && get(/databases/$(database)/documents/rules2/$(ruleId)).data.visible == true
      ;
          
      allow write: if true;
    }
    
    match /rules2/{ruleId=**} {
      allow read, write: if true;
    }
    
    // Mike's fix for get() issues
    match /rules3/{ruleId} {
      allow read: if resource.data.propertyThatDoesntExist && false;
    }    
  }
}


Now, the test case is trying to fetch two documents - /rules/test1 and /rules/test2. According to the rules, first document (/rules/test1) should be fetched without any issue. Second document (/rules/test2) should fail. But actually, fetching both documents fails. As I pointed in my previous post it is caused by error (at least that is my assumption) in following allow rule :

      // this rule should always fail, cause "notExistingField" not exists
             allow read: if 
       exists(/databases/$(database)/documents/rules2/$(ruleId)/subcollection1/$(ruleId)/subcollection2/$(ruleId)) 
       && "notExistingField" in get(/databases/$(database)/documents/rules2/$(ruleId)/subcollection1/$(ruleId)/subcollection2/$(ruleId)).data
       && get(/databases/$(database)/documents/rules2/$(ruleId)/subcollection1/$(ruleId)/subcollection2/$(ruleId)).data.notExistingField != null
      ;

This rule should return false, but second rule:

      // this rule will fail for "test2", but for "test1" should return true
      allow read: if
        exists(/databases/$(database)/documents/rules2/$(ruleId)) 
        && "visible" in get(/databases/$(database)/documents/rules2/$(ruleId)).data
        && get(/databases/$(database)/documents/rules2/$(ruleId)).data.visible == true
      ;

... should return true for document /rules/test1 and according to the docs "an operation is allowed if any rule exists that would allow that operation". 

In my option, the issue is related with get() and ".data" and the fact that it tries to get document from deep subcollection:
"notExistingField" in get(/databases/$(database)/documents/rules2/$(ruleId)/subcollection1/$(ruleId)/subcollection2/$(ruleId)).data

Maybe I do something wrong, but I just can't see it and I read the docs so many times, I checked all other options (in terms of how to check for key existence in .data, which is a map, right?)... 

Thanks for help.

To unsubscribe from this group and stop receiving emails from it, send an email to firebase-tal...@googlegroups.com.

To post to this group, send email to fireba...@googlegroups.com.

Kato Richardson

unread,
Nov 1, 2017, 11:16:38 PM11/1/17
to Firebase Google Group
Hi Marek,

It seems to be working for me. My complete rules are included in the example.

☼, Kato



To unsubscribe from this group and stop receiving emails from it, send an email to firebase-talk+unsubscribe@googlegroups.com.

To post to this group, send email to fireba...@googlegroups.com.

For more options, visit https://groups.google.com/d/optout.

Kato Richardson

unread,
Nov 2, 2017, 12:01:11 AM11/2/17
to Firebase Google Group
I linked to the simplified version, but for posterity, here's the longer version with all subcollections: http://jsfiddle.net/5vnm1avj/5/

☼, Kato

MMLLEVVY

unread,
Nov 2, 2017, 4:39:04 AM11/2/17
to Firebase Google Group
Kato,
but you changed security rules and as result the "notExistingField" expression do not execute get() and "in", cause exists() returns false...

Look at the data that were added to the database:
create /rules/test1
create /rules/test2
create /rules2/test1
create /rules2/test1/subcollection1/test1
create /rules2/test1/subcollection1/test1/subcollection2/test1
create /rules2/test1/subcollection1/test1/subcollection2/test1/subcollection3/test1
create /rules2/test2

I highlighted "subcollection1" and "subcollection2". Now, look at the first part of you security rule:
exists(/databases/$(database)/documents/rules2/$(ruleId)/subcollection/$(ruleId)/subcollection/$(ruleId))

You check for existence of not existing path and it returns false, which will stop the execution of other parts of the expression. In the rest of the expression you made the same mistake - not pointing to subcollection1 and subcollection2. Please paste following rules and you will see that fetching /rules/test1 will not work:
service cloud.firestore {
  match /databases/{database}/documents {
    match /rules/{ruleId} {
      allow read: if 
          (
          exists(/databases/$(database)/documents/rules2/$(ruleId)/subcollection1/$(ruleId)/subcollection2/$(ruleId)) 
          && "someNotExistingField" in get(/databases/$(database)/documents/rules2/$(ruleId)/subcollection1/$(ruleId)/subcollection2/$(ruleId)).data
          && get(/databases/$(database)/documents/rules2/$(ruleId)/subcollection1/$(ruleId)/subcollection2/$(ruleId)).data.someNotExistingField == true
          )
          ||
          (
          exists(/databases/$(database)/documents/rules2/$(ruleId)) 
          && "visible" in get(/databases/$(database)/documents/rules2/$(ruleId)).data
          && get(/databases/$(database)/documents/rules2/$(ruleId)).data.visible == true
          )
      ;
          
      allow write: if true;
    }
    
    match /rules2/{ruleId=**} {
      allow read, write: if true;
    }
    
    // Mike's fix for get() issues
    match /rules3/{ruleId} {
      allow read: if resource.data.propertyThatDoesntExist && false;
    }
  }
}

The above is taken from your fiddle, but I changed "subcollection" to subcollection1 and subcollection2, cause those collections exists and now exists() fill return true, but the other two parts of the expression will throw an error.



--

Kato Richardson | Developer Programs Eng | kato...@google.com | 775-235-8398

MMLLEVVY

unread,
Nov 6, 2017, 12:26:06 PM11/6/17
to Firebase Google Group
Kato, any news regarding this issue? Did you test rules from my previous post?

MMLLEVVY

unread,
Jan 4, 2018, 11:01:47 AM1/4/18
to Firebase Google Group
Kato, the issue with with firestore security rules still exists, any plans to fix it? You didn't respond to my post (2017-11-02), where I pointed how to reproduce this issue. Thanks.

Kato Richardson

unread,
Jan 4, 2018, 1:14:23 PM1/4/18
to Firebase Google Group
So I just spent about 40 minutes looking at this, but I still can't tell what's going on here. I just can't tell what things like this are trying to do in order to decide what should and shouldn't work:

exists(/databases/$(database)/documents/rules2/$(ruleId)/subcollection1/$(ruleId)/subcollection2/$(ruleId))

Is there a real-world scenario we can work from here? Does it need to be this complex to reproduce the conditions you've described? Can we simplify this a bit and give it some meaningful context?

I know this is additional work at your end, but I can't be much help in the time constraints I have unless this is more digestible.

☼, Kato

Kato Richardson

unread,
Jan 4, 2018, 1:28:01 PM1/4/18
to Firebase Google Group
Err, forgot to include: Thanks for pursuing this. I appreciate you championing this. It does look odd, and I was able to reproduce roughly what you've described, I think. But I don't really know what that means since I'm not following what the rules are attempting to accomplish and can't really decide if they should work : )

Kato Richardson

unread,
Jan 4, 2018, 7:54:22 PM1/4/18
to Firebase Google Group
Mike McDonald mentioned this Stack Overflow post, which looks related (Thanks Mike!).  Definitely using more than 3 get/exists calls in a post here.

So maybe cutting back to my last question, we should focus on a real world use case and add some context and see if we can come up with an alternative layout.

☼, Kato


On Thu, Jan 4, 2018 at 11:14 AM, Kato Richardson <kato...@google.com> wrote:
Reply all
Reply to author
Forward
0 new messages