24
votes

I would like to implement "write" security rules in Firebase depending on users roles.
My data structure is like this:

+ myapp
  + users
    + john
      + email: "[email protected]"
      + roles
        + administrator: true
    + mary
      + email: "[email protected]"
      + roles
        + moderator: true
    + ...
  + documents
    + -JVmo6wZM35ZQr0K9tJu
      + ...
    + -JVr56hVTZxlAI5AgUaS
      + ...
    + ...

I want - for example - that only administrator users can write documents.
These are the rules I've come to:

{
  "rules": {
    ".read": true,
    "$documents": {
      ".write": "root.child('users').child(auth.uid).child('roles').child('administrator').val() === true"
    }
  }
}

But it doesn't work: not even administrator users can write documents...
Is my understanding of Firebase security rules totally flawed?

UPDATE: Just before Jenny's answer (believe it or not :-), I did implement the exact same solution he provides (of course based on Kato's comment).
Though, making some tests, I could not let the rules structure

{
  "rules": {
    "documents" {
      "$document" {
        ".read": "root.child('users').child(auth.uid).child('roles').child('documents').child('read').val() === true",
        ".write": "root.child('users').child(auth.uid).child('roles').child('documents').child('write').val() === true"
      }
    }
  }
}

work... I always got a warning like this:

"FIREBASE WARNING: on() or once() for /documents failed: Error: permission_denied: Client doesn't have permission to access the desired data. "

So I came up with this structure, instead:

{
  "rules": {
    "documents" {
      ".read": "root.child('users').child(auth.uid).child('roles').child('documents').child('read').val() === true",
      ".write": "root.child('users').child(auth.uid).child('roles').child('documents').child('write').val() === true"
    }
  }
}

Which indeed works, for me: if I set a roles/customers/read node to true on a user he can read all documents, otherwise he can't (and the same for write).

My doubts now are:

  • why I could not let the first rule (as suggested by Kato) work?
  • do you see any possible security hole in a rule like the one I did came up with?
  • are rules using "$" variables necessary/useful even if you don't have to allow/deny the readability/writeability of each single document based on it's key, but you just want to allow/deny the readability/writeability of a node as a whole?
1
Based on the names of your user records, I'd guess that they don't match auth.uid (which is probably a simple login id, such as twitter:2544215). Also, your .write rule should probably be under documents/$document instead of $documents (off of root), otherwise you'll be allowing access to any path on root.Kato
Thanks, Kato!... I will for sure change the key of my users (from username to uid). Is this a common practice (indexing users by uid)? What if I would like to allow for multiple authentication providers for the same user (say, 'twitter' and 'password')? I should result with the same user under multiple keys... :-( But, first of all, should I add a user to my Users collection if she signs in with a 'social' provider, or should I just sign her in?MarcoS
All of these questions are proprietary to your use case. You probably want to store some data about your users, regardless of how they log in. I can't think of any sites that allow you to log into one account from multiple providers (i.e. separate identities) other than aggregate services that combine feed content from various services. If you want to take that route, generate your own tokens and ids.Kato
No, I don't want to take that route... I just thought it was a common practice (and expected by common users). If I can accept just one provider per user, everything is simpler, and I can use uid to index my users... Thanks for explaining...MarcoS
You could accomplish multiple authentication providers per user by using more than one node to store them. It'd have some potentially dangerous assumptions baked into it, though, since you'd need a common identifier upon which to join them (like email). But I digress. If you would like to explore that, ask another question and I'll answer there :)mimming

1 Answers

28
votes

Based on the names of your user records, they don't match auth.uid, which is probably a Simple Login id, such as twitter:2544215.

Start by adjusting your users to be stored by their Simple Login uid:

+ myapp
  + users
    + twitter:2544215
      + email: "[email protected]"
      + roles
        + administrator: true
    + twitter:2544216
      + email: "[email protected]"
      + roles
        + moderator: true
    + ...
  + documents
    + -JVmo6wZM35ZQr0K9tJu
      + ...
    + -JVr56hVTZxlAI5AgUaS
      + ...
    + ...

Next, add a security rule so that administrators can access documents. You have a couple options here, depending on your specific use case.

  1. To give administrators write access to contents of each document:

    {
      "rules": {
        "documents": {
          "$documents": {
            ".write": "root.child('users').child(auth.uid).child('roles').child('administrator').val() === true"
          }
        }
      }
    }
    
  2. Or, alternatively, give them access to the whole collection:

    {
      "rules": {
        "documents": {
          ".write": "root.child('users').child(auth.uid).child('roles').child('administrator').val() === true"
        }
      }
    }
    

The difference between these two being the $documents variable that moves the security rule one step further into the hierarchy.

(This was mostly just an aggregation of comments by @Kato into answer form)