Roles in Firebase

Firebase provides a JSON-based language for us to restrict different parts of a back-end database to different users based on their auth token id (auth.uid).

That’s handy enough for restricting a user to only be able to see their data but what if you want to assign some roles to distinguish between user types and give those roles different permission levels throughout your Firebase database?

Well, that’s something I’ve been flirting with over the last couple of days with some limited success but today I stumbled on a project built by Firebase themselves called Firechat and I just wanted to draw people’s attention to it as it has a pretty useful rules.json file that gives a good example of how to implement read and write permissions at different levels of the database tree.

I don’t really like that the default permissions in Firebase are read and write on everything. I do like that in this example they are both set to false for everything and then you have to explicitly set how you want to handle permissions for everything down the chain.

So in this example we’re looking at chat rooms which have users and moderators. Here’s the rules file on Github: https://github.com/firebase/firechat/blob/master/rules.json

And here’s the Firechat app in action: https://firechat.firebaseapp.com/

It’s pretty useful to see a full app built by the Firebase developers to get some insight into how they use it and/or expect it to be used. I think this is a better example to work from than the Gist here which comes high in Google at the moment.

{
  // Firechat sample security rules
  "rules": {
    // By default, make all data private unless specified otherwise.
    ".read": false,
    ".write": false,
    "room-metadata": {
      ".read": true,
      "$roomId": {
        // Append-only by anyone, and admins can add official rooms, and edit or remove rooms as well.
        ".write": "(auth != null) && (!data.exists() || root.child('moderators').hasChild(auth.uid) || data.child('createdByUserId').val() === auth.uid)",
        ".validate": "newData.hasChildren(['name','type'])",
        "id": {
          ".validate": "(newData.val() === $roomId)"
        },
        "createdByUserId": {
          ".validate": "(auth.uid === newData.val())"
        },
        "numUsers": {
          ".validate": "(newData.isNumber())"
        },
        "type": {
          ".validate": "('public' === newData.val()) || 'private' === newData.val() || ('official' === newData.val() && (root.child('moderators').hasChild(auth.uid)))"
        },
        // A list of users that may read messages from this room.
        "authorizedUsers": {
          ".write": "(auth != null) && (!data.exists() || root.child('moderators').hasChild(auth.uid) || data.hasChild(auth.uid))"
        }
      }
    },
    "room-messages": {
      "$roomId": {
        // A list of messages by room, viewable by anyone for public rooms, or authorized users for private rooms.
        ".read": "(root.child('room-metadata').child($roomId).child('type').val() != 'private' || root.child('room-metadata').child($roomId).child('authorizedUsers').hasChild(auth.uid))",
        "$msgId": {
          // Allow anyone to append to this list and allow admins to edit or remove.
          ".write": "(auth != null) && (data.val() === null || root.child('moderators').hasChild(auth.uid)) && (root.child('room-metadata').child($roomId).child('type').val() != 'private' || root.child('room-metadata').child($roomId).child('authorizedUsers').hasChild(auth.uid)) && (!root.child('suspensions').hasChild(auth.uid) || root.child('suspensions').child(auth.uid).val() < now)",
          ".validate": "(newData.hasChildren(['userId','name','message','timestamp']))"
        }
      }
    },
    "room-users": {
      "$roomId": {
        ".read": "(root.child('room-metadata').child($roomId).child('type').val() != 'private' || root.child('room-metadata').child($roomId).child('authorizedUsers').hasChild(auth.uid))",
        "$userId": {
          // A list of users by room, viewable by anyone for public rooms, or authorized users for private rooms.
          ".write": "(auth != null) && ($userId === auth.uid || root.child('moderators').hasChild(auth.uid))",
          "$sessionId": {
            ".validate": "(!newData.exists() || newData.hasChildren(['id','name']))"
          }
        }
      }
    },
    "users": {
      // A list of users and their associated metadata, which can be updated by the single user or a moderator.
      "$userId": {
        ".write": "(auth != null) && (auth.uid === $userId || (root.child('moderators').hasChild(auth.uid)))",
        ".read": "(auth != null) && (auth.uid === $userId || (root.child('moderators').hasChild(auth.uid)))",
        ".validate": "($userId === newData.child('id').val())",
        "invites": {
          // A list of chat invitations from other users, append-only by anyone.
          "$inviteId": {
            // Allow the user who created the invitation to read the status of the invitation.
            ".read": "(auth != null) && (auth.uid === data.child('fromUserId').val())",
            ".write": "(auth != null) && (!data.exists() || $userId === auth.uid || data.child('fromUserId').val() === auth.uid)",
            ".validate": "newData.hasChildren(['fromUserId','fromUserName','roomId']) && (newData.child('id').val() === $inviteId)"
          }
        },
        "notifications": {
          // A list of notifications, which can only be appended to by moderators.
          "$notificationId": {
            ".write": "(auth != null) && (data.val() === null) && (root.child('moderators').hasChild(auth.uid))",
            ".validate": "newData.hasChildren(['fromUserId','timestamp','notificationType'])",
            "fromUserId": {
              ".validate": "newData.val() === auth.uid"
            }
          }
        }
      }
    },
    "user-names-online": {
      // A mapping of active, online lowercase usernames to sessions and user ids.
      ".read": true,
      "$username": {
        "$sessionId": {
          ".write": "(auth != null) && (!data.exists() || !newData.exists() || data.child('id').val() === auth.uid)",
          "id": {
            ".validate": "(newData.val() === auth.uid)"
          },
          "name": {
            ".validate": "(newData.isString())"
          }
        }
      }
    },
    "moderators": {
      ".read": "(auth != null)"
    },
    "suspensions": {
      ".write": "(auth != null) && (root.child('moderators').hasChild(auth.uid))",
      ".read": "(auth != null) && (root.child('moderators').hasChild(auth.uid))"
    }
  }
}

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>