1

I make a notes-style app with collaborative features and want to set-up safe security rules. The problem is: my security rules do not work with my database query. The Firebase security rules simulator shows correct results when I test access to notes for authenticated users. But in the app I get the message "Listener at /notes failed: permission_denied".

I've found some nice examples of security rules here Firebase: Security rules for a collaborative app and here https://gist.github.com/katowulf/4741111 but these are not that different from what I have. I suspect the problem could either be in the query or in the lack of indexing rules (".indexOn").

My query is:

- (void)setupQuery {    

    NSString *email = [FirebaseAuthorization shared].currentUserEmail;

    self.reference = [[[FIRDatabase database] reference] child:@"notes"];

    NSString *query = [NSString stringWithFormat:@"document/users/%@", email.MD5String];

    self.query = [[self.reference queryOrderedByChild:query] queryEqualToValue:@(YES)];

}

My security rules are:

{
  "rules": {
    "notes" : {

      "$note_id" : {
        ".read": "data.child('document/users/'+root.child('users/'+auth.uid+'/email_md5').val()).val() === true",
        ".write": "(data.child('document/users/'+root.child('users/'+auth.uid+'/email_md5').val()).val() === true) || (root.child('users/'+auth.uid+'/email').val() === root.child('users/'+newData.child('document/author').val()+'/email').val())",

        ".indexOn": "document/users"
        }      
    },
    "users" : {
      "$user_id" : {
        ".read": "auth.uid === $user_id",
        ".write": " ( auth.uid == newData.val() ) || ( auth.uid == $user_id )"
      }    
    }
    }
  }

And my database structure is:

{
"notes" : {
  "NOTE_ID_1" : {
    "document" : {
      "author" : "user_id_1",
      "users" : {
        "email_1_md5@email-com" : true,
        "email_2_md5@email-com" : true
        }
      }
    }
  "NOTE_ID_2" : {
    "document" : {
      "author" : "user_id_2",
      "users" : {
        "email_3_md5@email-com" : true,
        "email_4_md5@email-com" : true,
        }
      }
    }
}
"users" : {
  "iser_id_1" : {
    "email" : "email_1@email.com",
    "email_md5" : "email_2_md5@email-com"
    }
  }
}

I expect the query to work with tight security rules (each user has access to his notes only), but now it only works with loose security rules (each user has access to all notes).

ncvo
  • 11
  • 2

1 Answers1

1

Firebase security rules don't filter the data on your behalf. They merely enforce that all data access is authorized.

This it the code for your query

self.reference = [[[FIRDatabase database] reference] child:@"notes"];

NSString *query = [NSString stringWithFormat:@"document/users/%@", email.MD5String];

self.query = [[self.reference queryOrderedByChild:query] queryEqualToValue:@(YES)];

In order to run this code, the user must have some form of read permission at notes. Since nobody has read permission on that node, the rules engine rejects the read.

This is known as rules are not filters in the documentation, and in many other questions about the topic.


Note that you can nowadays use security rules to allow reads with certain queries, through query-based rules. When using this, you build the relevant query in your code, and then use security rules to only allow data access with that specific query.

In your case this would be a .read rule on /notes that matches the query you're building. It'd be something like this for your use-case

{
  "rules": {
    "notes" : {
      ".read": "
        query.orderByChild == 'document/users/'+auth.uid &&
        query.equalTo == true
      "
    }
  }
}

Note though that I'm not sure whether this condition actually matches your query.

Even if it does, the lack of an index would make the security point moot (as without an index all filtering is done on the client). For more on this, see my answer here: Firebase query if child of child contains a value. Such an inverted index would also allow you to implement security in your fan-out (write) logic, instead of in the read operation, ensuring much better read scalability.

Frank van Puffelen
  • 565,676
  • 79
  • 828
  • 807
  • Thank you, Frank. I'm trying the rule you proposed, but have an error message: Invalid == expression: right operand must be an ordering or string literal when comparing against an ordering. – ncvo Sep 18 '19 at 10:36
  • Yeah, I was afraid of something like that. You might want to try `('document/users/'+auth.uid)`, but at this point I'm guessing. Given me note on the performance, I'd recommend looking at an inverted index. – Frank van Puffelen Sep 18 '19 at 13:59
  • It gives the same result. Inverted index shall be simpler, but I'm afraid it is less robust. – ncvo Sep 18 '19 at 19:12
  • And inverted index is probably less safe. If any user can write into the list of accessible notes of this user, then a third party could modify access of this user to notes shared to him. – ncvo Sep 18 '19 at 19:24
  • Frank, do you think it's easier to implement for Firestore? It'd be an investment, but there still is some flexibility before the app is released – ncvo Sep 18 '19 at 20:08
  • On Cloud Firestore you'd use an array to store the `users` and then use `array-contains` to query for only users that have access. – Frank van Puffelen Sep 18 '19 at 22:29