skip to Main Content

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?

2

Answers


  1. Normally you should not get:

    Missing or Insufficient Permissions.

    Even if the document doesn’t exist. However, the following rule:

    // 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;
    }
    

    Cannot be evaluated as true if the document doesn’t exist and therefore the Firebase servers will reject the operation and you’ll indeed get a "Missing or Insufficient Permissions" when you set a listener on the non-existing document.

    So in your case, instead of directly checking the UID, you should use exists to check if the document exists.

    Login or Signup to reply.
  2. 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)
              )
            )
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search