Search code examples
firebasegoogle-cloud-platformgoogle-cloud-firestorefirebase-security

If a firebase rule checks resource values, will it always give permission denied when the document does not exist?


I was writing a rule for my "users" collection that looks like this:

    match /users/{userId} {

      // Returns true if auth's email is empty and user's 
      // insensitiveEmail is empty or non-existent
      function emailDoesNotExist(resource) {
        return request.auth.token.email.size() == 0 && 
        (!fieldExists(resource, "insensitiveEmail") || resource.data.insensitiveEmail.size() == 0);
      }
    
      // Returns true if auth's email matches user's email
      function emailMatches(resource) {
          return request.auth.token.email == resource.data.insensitiveEmail;
      }
    
      // Returns true if the auth uid matches the resource's uid 
      function uidMatchesResource(resource) {
          return request.auth.uid == resource.data.uid && request.auth.uid == resource.id;
      }

      allow get: if request.auth != null
        && request.auth.uid == userId
        && uidMatchesResource(resource)
        && (emailDoesNotExist(resource) || emailMatches(resource))

I'm getting [firestore/permission-denied] The caller does not have permission to execute the specified operation. errors in a case where the user id being requested does not exist in the database.

Is that because I'm trying to do comparisons against resource and resource doesn't exist?

And if that's so, is there a way I can validate data in returned documents without preventing requests that result in 0 returned documents?


Solution

  • To allow your query to access documents that don't exist, perform a null check against resource and short-circuit the result before the other checks are resolved.

    Additionally, your email check can be greatly simplified with the use of Map#get().

    match /users/{userId} {
      // ...
    
      // Returns true if auth's email matches the stored
      // insensitiveEmail value (if any).
      // - has an email that matches stored value? true
      // - has no email and stored value is missing? true
      // - has no email and stored value is ""? true
      // - otherwise? false
      // note: while request.auth.token.get("email", "") could also be used,
      //       the token should probably always have the "email" claim and
      //       throw an error when it doesn't.
      function emailMatches(resource) {
        return request.auth.token.email == resource.data.get("insensitiveEmail", "")
      }
        
      // Returns true if the auth uid matches the resource's uid 
      function uidMatchesResource(resource) {
        return request.auth.uid == resource.data.uid
            && request.auth.uid == resource.id; // <- this line could be removed because of request.auth.uid == userId check
      }
    
      // note spread out for clarity, will need flattening
      allow get: if request.auth != null
            && request.auth.uid == userId
            && (
              resource == null // permit read when document does not exist
              || (
                uidMatchesResource(resource) // or when the uid & email match in existing document
                && emailMatches(resource)
              )
            )
    }