skip to Main Content

In Firestore, I have data within a sub-collection called ‘exclusiveB’, and a field of this collection’s documents is ‘creditCard’ which accepts boolean values.

Within the same Firestore referred to above, I have another sub-collection called ‘exclusiveA’.

I wrote a security rule for enabling the ‘exclusiveA’ collection to be read by the current user: if the current user has a uid, and if details concerning the current-user in sub-collection ‘exclusiveB’ confirm field-value ‘creditCard’ as false.

The result received is that Firestore has denied the read permission (despite the ‘creditCard’ field-value is false).  Can anyone explain why and offer a solution please?

Please note that the parent collections ‘users’, ‘userA’, and ‘userB’ are empty except for a single value that confirms the iD of the document holding sub-collections, and, ‘exclusiveB’ documents feature a field that confirms the current-user’s uid, and another field that confirms the document’s iD.

Photos of the Firestore formation are as follows:

enter image description here

enter image description here

enter image description here

enter image description here

The security rules are:

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 read if user: (1) has a uid, (2) has creditcard = false
        allow get: if request.auth.uid != null && get(/databases/$(database)/documents/users/{docId}/userB/{docIdB}/exclusiveB/$(request.auth.uid)).data.creditCard == false;
  }
  
  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;
  }

  }

}

The Flutter code for the Firestore query at the user-interface level 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.

2

Answers


  1. Chosen as BEST ANSWER

    In the end, I found that calling exists(/databases/) on the data relevant to the get(/databases/) call, first, activated the perfectly written get(/databases/) call. It seems like a fault, but another way of looking at it could be to say that Firestore will accept a get(/databases/) call if it is aware of the relevant data ahead of the get(/databases/) call.

    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 && exists(/databases/$(database)/documents/users/$(docId)/userB/$(docId2)/exclusiveB/$(request.auth.uid)) &&
          get(/databases/$(database)/documents/users/$(docId)/userB/$(docId2)/exclusiveB/$(request.auth.uid)).data.creditCard == false;
      }


  2. This query:

      final _dataSource = FirebaseFirestore.instance
          .collection(
              'users/l9NzQFjN4iB0JlJaY3AI/userA/2VzSHur3RllcF5PojT61/exclusiveA');
    
      _dataSource.where('preferredCurrency', isEqualTo: currency).snapshots().distinct(),
    

    And this query:

        final exclusiveBcollection = await _dB.collection('users/l9NzQFjN4iB0JlJaY3AI/userB/mv6YgmAfIDEkUNmstrzg/exclusiveB')
            .where('uid', isEqualTo: currentUserID).get().then((QuerySnapshot querySnapshot) {
    

    Both attempt to list documents the subcollection, from the perspective of security rules. But your rules only allow individual document get operations:

      match /users/{docId}/userA/{docId2}/exclusiveA/{docId3} {
            allow get: if request.auth.uid != null && get(/databases/$(database)/documents/users/{docId}/userB/{docIdB}/exclusiveB/$(request.auth.uid)).data.creditCard == false;
      }
      
      match /users/{docId}/userB/{docId2}/exclusiveB/{xcluB} {
            allow get: if resource.data.uid == request.auth.uid;
      }
    

    If you want to query (list) a collection, you will need to add that permission. You can do that by specifying list, or just using read, which is a combination of get and list, as discussed in the documentation.

      match /users/{docId}/userA/{docId2}/exclusiveA/{docId3} {
            allow read: if request.auth.uid != null && get(/databases/$(database)/documents/users/{docId}/userB/{docIdB}/exclusiveB/$(request.auth.uid)).data.creditCard == false;
      }
      
      match /users/{docId}/userB/{docId2}/exclusiveB/{xcluB} {
            allow read: if resource.data.uid == request.auth.uid;
      }
    
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search