Firestore's get(/databases/) call with data from a sub-collection

156 views
Skip to first unread message

M Mathems

unread,
May 3, 2023, 11:28:28 AMMay 3
to google-cloud-firestore-discuss
This question is about using Firestore's get(/databases/) call where the relevant data is within a sub-collection.

Structure wise, the Firestore is set out as follows:

                                                            /users/

                           /userA/                                                          /userB/

/signInData/ /otherDetails/ /exclusiveA/          /signInData/ /otherDetails/ /exclusiveB/

Attached photos also illustrate the Firestore formation.

So, the get(/databases/) call is meant to permit a 'get' kind of read of the 'exclusiveA' sub-collection, but only if the 'currency' field's value within documents concerning userB (held within sub-collection 'exclusiveB') is equal to the 'preferredCurrency' field value of documents within sub-collection 'exclusiveA'.  This is intended to create exclusive reading, where non-matching documents are not permitted to be read.

The Firestore rules below result in a denial, and the error message is: [cloud_firestore/permission-denied] The caller does not have permission to execute the specified operation.

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {  
 
  match /users {
      // if a rule isn't specified, Firestore denies by default
  allow read;
  }
 
  match /users/{docId}/userA {
  allow read;
  }
 
  match /users/{docId}/userB {
  allow read;
  }
 
  match /users/{docId}/userA/{docId2}/exclusiveA/{docId3} {
  allow get: if get(/databases/$(database)/documents/users/{docId}/userB/{docId2}/exclusiveB/$(request.auth.uid)).data.currency == resource.data.preferredCurrency;
  }
 
  match /users/{docId}/userB/{docId2}/exclusiveB/{xcluB} {
  allow get: if resource.data.uid == request.auth.uid;
  }
 
  match /users/{docId}/userA/{docId2}/otherDetails/{id} {
  allow read: if request.auth.uid == resource.data.id;
  }
 
  match /users/{docId}/userB/{docId2}/otherDetails/{id} {
  allow read: if request.auth.uid == resource.data.id;
  }

  }

}


Please note: the 'users' collection is empty, but recently I added a 'docId' field to the collection, and that field confirms the document-id of the document holding the 'userA' and 'userB' sub-collections.  Similarly, the 'userA' and 'userB' sub-collections are empty.

Is the composing of my rules sound and does Firestore's get(/databases/) function work as designed?

With thanks. 
Screenshot 2023-04-19 at 12.21.39.png
Screenshot 2023-04-19 at 12.23.09.png
Screenshot 2023-04-19 at 12.22.17.png
Screenshot 2023-04-19 at 12.24.19.png

Denver Coneybeare

unread,
May 3, 2023, 2:16:54 PMMay 3
to google-cloud-firestore-discuss
Could you share the code that you are using to execute the query that is getting rejected? That will be helpful for determining why it's getting rejected by security rules.

M Mathems

unread,
May 3, 2023, 2:41:10 PMMay 3
to google-cloud-firestore-discuss

Hi Denver

Thank you for your response.

The code is:

import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart' as auth;
import 'package:cloud_firestore/cloud_firestore.dart';

class StreamResults extends StatefulWidget {
  @override
  State<StreamResults> createState() => _StreamResultsState();
}

class _StreamResultsState extends State<StreamResults> {
  final _auth = auth.FirebaseAuth.instance;
  final _dB = FirebaseFirestore.instance;
  final _dataSource = FirebaseFirestore.instance
      .collection(
          'users/l9NzQFjN4iB0JlJaY3AI/userA/2VzSHur3RllcF5PojT61/exclusiveA');

  String? vehicleMake, vehicleTitle, currentUserID, pCurrency, currency, uid, docRefID;
  int? maxSpeed, pullStrength;

  void getIdentity() {
    currentUserID = _auth.currentUser!.uid;
    print('The current user\'s ID is: $currentUserID.');
  }
  
  Future<void> matchDetails() async {
    final exclusiveBcollection = await _dB.collection('users/l9NzQFjN4iB0JlJaY3AI/userB/mv6YgmAfIDEkUNmstrzg/exclusiveB')
        .where('uid', isEqualTo: currentUserID).get().then((QuerySnapshot querySnapshot) {
          querySnapshot.docs.forEach((doc) {
            currency = doc['currency'];
            uid = doc['uid'];
            docRefID = doc['docRefID'];
          });
    });
  }

  @override
  void initState() {
    getIdentity();
    matchDetails();
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return StreamBuilder<QuerySnapshot>(
        stream: _dataSource.where('preferredCurrency', isEqualTo: currency).snapshots().distinct(),
        builder: (context, snapshot) {
          if (!snapshot.hasData) {
            CircularProgressIndicator();
          } else if (snapshot.hasData) {
            final retrievedData = snapshot.data!.docs;
            for (var specific in retrievedData) {
              maxSpeed = specific['topSpeed'];
              vehicleTitle = specific['vehicleName'];
              pullStrength = specific['horsePower'];
              vehicleMake = specific['vehicleBrand'];
              pCurrency = specific['preferredCurrency'];
              print('The vehicle\'s maximum speed = $maxSpeed.');
              print('The vehicle\'s pulling strength = $pullStrength bph.');
              print(
                  'The vehicle\'s brand is $vehicleMake, and its model-name is $vehicleTitle.');
              print('The preferred currency = $pCurrency.');
            }
          }
          return Column(
            children: [
              Text('The vehicle\'s maximum speed = $maxSpeed.'),
              Text('The vehicle\'s pulling strength = $pullStrength bph.'),
              Text(
                  'The vehicle\'s brand is $vehicleMake, and its model-name is $vehicleTitle.'),
            ],
          );
        });
  }
}

With thanks.

Denver Coneybeare

unread,
May 4, 2023, 10:47:14 AMMay 4
to google-cloud-firestore-discuss
Can you reduce that code sample to JUST the query that is problematic? I'm not sure I fully understand what I'm looking at, especially since I don't know Flutter or Dart. 

M Mathems

unread,
May 4, 2023, 11:30:07 AMMay 4
to google-cloud-firestore-discuss
No probs.

The main code for the query is in two parts:

The instance of the exclusiveA collection:

 final _dataSource = FirebaseFirestore.instance.collection('users/l9NzQFjN4iB0JlJaY3AI/userA/2VzSHur3RllcF5PojT61/exclusiveA');

And, the calling of that instance as a query featuring a 'where' clause (within the StreamBuilder widget):

stream: _dataSource.where('preferredCurrency', isEqualTo: currency).snapshots().distinct(),

With thanks.

Denver Coneybeare

unread,
May 4, 2023, 11:32:36 AMMay 4
to M Mathems, google-cloud-firestore-discuss
Are both of those queries failing with the "permission denied" errors?

--
You received this message because you are subscribed to the Google Groups "google-cloud-firestore-discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an email to google-cloud-firestor...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/google-cloud-firestore-discuss/81085ca0-7455-4f81-bfcf-bf8af1131f94n%40googlegroups.com.

M Mathems

unread,
May 4, 2023, 11:34:31 AMMay 4
to google-cloud-firestore-discuss
Yes, and it's really just one query.

M Mathems

unread,
May 5, 2023, 8:43:25 AMMay 5
to google-cloud-firestore-discuss
Hi Denver

Could you emulate my circumstance above within a Firestore and Flutter interface near you (if not Flutter then the language of your preference) and draft the corresponding firestore rule to test and demonstrate the correct get(/databases/) call please?  The main thing here is for the relevant data to be two sub-collections deep from the parent.  With some photos I could then replicate what you show and finally understand - if that's possible.

Following some guidance received, I have changed the get(/databases/) call of my enquiry to one that would permit the current user to access every document within the collection instead of permitting selective reading:

match /users/{docId}/userA/{docId2}/exclusiveA/{docId3} {
   // allow read if user: (1) has a uid, (2) has creditcard = false
   allow read: if request.auth.uid != null && get(/databases/$(database)/documents/users/$(docId)/userB/$(docId2)/exclusiveB/$(request.auth.uid)).data.creditCard == false;
  }

On Thursday, 4 May 2023 at 16:32:36 UTC+1 dcon...@google.com wrote:

M Mathems

unread,
May 8, 2023, 2:24:30 PMMay 8
to google-cloud-firestore-discuss
I have just read the excerpt below authored by ke...@google.com [Todd K] and I suspect that he's answered the very query I have raised here (of course, I'll need to check, but I am certainly getting closer):

This is a good question. Long story short, this has to do with the get call. 

As you're aware, security rules need to "prove" that a query can be allowed without having to look at every single piece of underlying data. But I suspect the system isn't sophisticated enough to make the leap of "That get call is basically the same as each  document I'd be trying to retrieve from the query", so that call basically fails because it thinks it would need to make that call too many times -- once per document getting returned. On the other hand, it does know that resource.data is the same as the data that's getting returned from the query, so this set of rules should work:

match /tests/{testId} {
allow read: if resource.data.members[request.auth.uid] != null;
}


But what about querying subdirectories? In this case, you are allowed to make the get call, because you'd really only be making that call once for each subdirectory you're querying, so your complete rules should look a little something like this:

match /tests/{testId} {
allow read: if resource.data.members[request.auth.uid] != null;
match /mySub/{subDoc} {
allow read: if get(/databases/$(database)/documents/tests/$(testId)).data.members[request.auth.uid] != null;
}
}

M Mathems

unread,
May 8, 2023, 5:40:10 PMMay 8
to google-cloud-firestore-discuss
I think I have succeeded but I'm not sure because the emulator I was using before seems to be not working now, but an alternative emulator isn't showing the permission-denied error and the 'read' is allowed.  The change I made was on the basis of Todd's key to how Firestore thinks.  I called exists( ) before calling get( ) as the parent collections are empty.  I should believe that that satisfied Firestore to allow the read.

This could mean that I can now move forwards from here.

A special s/o to Todd for dropping those gems.  Thank you : ) 

M Mathems

unread,
May 9, 2023, 4:30:30 AMMay 9
to google-cloud-firestore-discuss
Despite the alternative emulator granting the read, strangely it later finds that Firestore denies the read and issues this error message:W/Firestore(24205): (24.4.3) [Firestore]: Listen for Query(target=Query(users/l9NzQFjN4iB0JlJaY3AI/userA/2VzSHur3RllcF5PojT61/exclusiveA order by __name__);limitType=LIMIT_TO_FIRST) failed: Status{code=PERMISSION_DENIED, description=Missing or insufficient permissions., cause=null}
D/EGL_emulation(24205): app_time_stats: avg=15753.46ms min=383.96ms max=31122.96ms count=2

So, this issue is not resolved yet.

Denver Coneybeare

unread,
May 13, 2023, 12:58:03 AMMay 13
to google-cloud-firestore-discuss
(apologies if my response gets posted twice; my last post seems to have disappeared into the ether).

After re-reading your opening post, I saw a problem with this line of your security rules:

allow get: if get(/databases/$(database)/documents/users/{docId}/userB/{docId2}/exclusiveB/$(request.auth.uid)).data.currency == resource.data.preferredCurrency;

Specifically, the path specified to get() must be a concrete path and may not contain wildcards. So I think you may want to replace {docId} with $(docId) and {docId2} with $(docId2).

I also recommend writing a suite of tests for your security rules using the Firestore emulator and the "rules-unit-testing" package, as documented here: https://firebase.google.com/docs/firestore/security/test-rules-emulator. The unit tests will help in (at least) two ways: (1) It will provide a repeatable suite of tests to make sure that changes you make don't break existing behaviors and (2) it will help you quickly "explore" how the rules behave. You can also include the debug() function (https://firebase.google.com/docs/reference/rules/rules.debug) to get some extra information. It's also useful to know that the Firestore emulator's output includes the security rules that were involved in allowing and denying requests, which may be helpful.

I hope this helps.

M Mathems

unread,
May 15, 2023, 8:57:21 AMMay 15
to google-cloud-firestore-discuss

Thank you for bringing this mistake to my attention.  I understand and agree, and in future I will draft in the same way you have advised.
Reply all
Reply to author
Forward
0 new messages