Max Mikkelsen
Max Mikkelsen

Reputation: 3

Firestore basic security rules not working

Please be humble as I'm very new to iOS swift programming and Firestore.

I have these two structs in Swift:

import FirebaseFirestoreSwift
import Foundation


struct WashingMachine: Codable {
    @DocumentID var id: String?
    var brand = "unknown"
    var price = 0
    var boughtDate: Date
        
  }
import FirebaseFirestoreSwift
import Foundation

struct PrivateData: Codable {
    @DocumentID var id: String?
    var roles: Dictionary<String, String>
}

I have tried to follow Firestore's guidelines in creating server security so only the logged in user with the role of 'Owner', given in the PrivateData subcollection can access a washingMachine document.

My rules in Firestore look like this:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /washingMachine/{docID} {
    
        allow read: if isSignedIn() && get(/databases/$(database)/documents/washingMachine/$(docID)/privateData/$(docID)).data.roles[request.auth.uid] == "Owner";
    
        function isSignedIn() {
        return request.auth != null;
      }
    
      allow create: if isSignedIn();
        match /privateData/{docID} {
            allow create: if isSignedIn();
      }      
    }
  }
}

I have no problem in creating the collection washingMachine and a subcollection privateData to that. If I take away get(/databases/$(database)/documents/washingMachine/$(docID)/privateData/$(docID)).data.roles[req in my security rules, I can read the washingMachine documents. I have however not been able to read the washingMachine document when I add the additional check that the user is represented in the subcollection privateData --> roles.

Anyone who can guide me?


Updated information after trying the proposed security update by @trndjc

When using these proposed security rules, I can no longer create the privateData subcollection:

rules_version = '2';

service cloud.firestore {

match /databases/{database}/documents {

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

    match /washingMachine/{docID} {
        allow read: if isSignedIn() && get(/databases/$(database)/documents/washingMachine/$(docID)/privateData/$(docID)).data.roles[request.auth.uid] == "Owner";
        allow create: if isSignedIn();
    }

    match /privateData/{docID} {
        allow create: if isSignedIn();
    }
}

}

My function in SwiftUI code looks like this (sorry for the hardcoding):

func addDefaultWashingMachine(userId: String) {
    let privateData = PrivateData(
        roles: [userId: "Owner"])
    let washingMachine = WashingMachine(boughtDate: Date.now)
    let collectionRef = db.collection("washingMachine")

    do {
        let newDocReference = try collectionRef.addDocument(from: washingMachine)
        print(newDocReference.debugDescription)

        let subCollectionRef = db.collection("washingMachine").document(newDocReference.documentID).collection("privateData")

        let subDocReference = try subCollectionRef.addDocument(from: privateData)
        
    } catch {
        print(error.localizedDescription)
    }
}

The result is that only the washingMachine collection is created, but the subcollection privateData isn't. The following error message is given in the printouts.

<FIRDocumentReference: 0x28222c630> 2023-06-18 19:45:47.107265+0200 MyTestApp[7196:3316489] 10.9.0 - [FirebaseFirestore][I-FST000001] WriteStream (107e47088) Stream error: 'Permission denied: Missing or insufficient permissions.' 2023-06-18 19:45:47.108332+0200 MyTestApp[7196:3316489] 10.9.0 - [FirebaseFirestore][I-FST000001] Write at washingMachine/E8uAMthVmZ2ZSvji37iX/privateData/ZULclTBPGovQpijy0PW6 failed: Missing or insufficient permissions.


Modified the proposed security rules like this to being able to add subcollection privateData:

rules_version = '2';

service cloud.firestore {

match /databases/{database}/documents {

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

    match /washingMachine/{docID} {
        allow read: if isSignedIn() && get(/databases/$(database)/documents/washingMachine/$(docID)/privateData/$(docID)).data.roles[request.auth.uid] == "Owner";
        allow create: if isSignedIn();
        
        match /privateData/{docID} {
        allow create: if isSignedIn();
        }
        
    }       
}

}

I still get the following error when trying to read the document:

2023-06-18 20:26:46.331840+0200 MyTestApp[7211:3330007] 10.9.0 - [FirebaseFirestore][I-FST000001] Listen for query at washingMachine failed: Missing or insufficient permissions. Error getting documents: Error Domain=FIRFirestoreErrorDomain Code=7 "Missing or insufficient permissions." UserInfo={NSLocalizedDescription=Missing or insufficient permissions.}

I call it by using these functions:

func findAllWashingMachinesInCollection() {
    
    db.collection("washingMachine").whereField("brand", isEqualTo: "unknown").getDocuments() { (querySnapshot, err) in
        if let err = err {
            print("Error getting documents: \(err)")
        } else {
            for document in querySnapshot!.documents {
                print("\(document.documentID) => \(document.data())")
                self.fetchWashingMachine(documentId: document.documentID)
                
            }
        }
    }
}


func fetchWashingMachine(documentId: String) {
    let docRef = db.collection("washingMachine").document(documentId)
    
    docRef.getDocument(as: WashingMachine.self) { result in
        switch result {
        case .success(let washingMachine):
            self.washingMachine = washingMachine
            print("WASHING: \(washingMachine.brand)")
            self.errorMessage = nil
        case .failure(let error):
            self.errorMessage = "Error decoding document: \(error.localizedDescription)"
        }
    }
}

The work well if I remove get(/databases/$(database)/documents/washingMachine/$(docID)/privateData/$(docID)).data.roles[request.auth.uid] == "Owner" in the security rules.

Upvotes: 0

Views: 79

Answers (2)

Frank van Puffelen
Frank van Puffelen

Reputation: 599716

Unfortunately what you're trying to do isn't possible with Firebase security rules. Security rules don't filter data, but instead "only" ensure that your code only accesses data that it is permitted to.

So when we look at the check in your rule:

get(/databases/$(database)/documents/washingMachine/$(docID)/privateData/$(docID)).data.roles[request.auth.uid] == "Owner";

This requires that:

  • Either the rules engine check each document's private data and only return the documents for which the user is the owner, which violates the rules are not filters limitation that I mentioned above.
  • Or your code will have to include the same condition, so only requesting the documents for which it is the owner. But that isn't possible, as a query can only filter based on the data in the documents that it returns.

The workaround is to duplicate the data that you want to filter on (so the roles) in the parent document, and refer to that both in your security rules and in the query.

Upvotes: 1

ctrl freak
ctrl freak

Reputation: 12385

I don't know if you have the roles properly assigned, if the collection and document paths are correct, or if the data in the database reflects these rules, but I do know that your security rules aren't properly written. You must declare the functions outside of the match rules. I've spaced them out here to better illustrate how the rules should look.

rules_version = '2';

service cloud.firestore {

    match /databases/{database}/documents {

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

        match /washingMachine/{docID} {
            allow read: if isSignedIn() && get(/databases/$(database)/documents/washingMachine/$(docID)/privateData/$(docID)).data.roles[request.auth.uid] == "Owner";
            allow create: if isSignedIn();
        }

        match /privateData/{docID} {
            allow create: if isSignedIn();
        }
    }
}

Upvotes: 0

Related Questions