Permission-based access in Google Firestore
Define your own roles and permissions!
I have a Firestore database in my project. I store user data as entries in my database and I want to create my own user roles and permissions and using them in database rules for access control. For this reason, I went with Firestore and not Realtime Database — it allows for more flexible database rules.
Inspired by Fireship's role-based database access.
Let's start with talking about about pros and cons of this approach
Pros 🟢
- Define custom permissions.
- Enclose the permissions into roles.
- Allow database operations only to users with certain permissions.
- Easy to grasp and implement.
Cons 🔴
- Cannot do proper role hierarchy — different roles are not related to each other in any way.
Permission-based vs. Role-based
Another important distinction is, that this approach is Permission based. Although we will introduce few roles later, these are just containers for sets of permissions. You can check out comparison of these approaches here.
The main difference is, that we assign roles to users, but allow actions according to permissions! This gives us best of both worlds — central control of roles (adjust all admins at once) and fine-grain control of locations access.
Creating users
For creating users I'm using Firebase authentication and Google sign in. For every new user that signs in into my web app, I create a small account — just a database entry, really.
- Create
users
collection in your Firestore database - Save each user that logs in in a document named with his UID. Sign in with Google gives you the UID for free, but you can probably generate it too.
- Give every newly created user a default role — mine is
viewer
. It is advisable that the default role has very low privileges.
In your Firestore, user document can look like this
/users/foo :email: "foo@email.com"
role: "viewer"
name: "John Doe"
Each attribute is a separate string field. John's UID here is foo
and he has a viewer role.
Creating roles and permissions
For roles, we will create new roles
collection with roles
document (feel free to pick a better name). Inside, we will create an array of strings field for each role. Name of the role is the name of the field at the same time. And inside the array, there are permissions for each user role.
Database rules!
Finally, we implement database access rules, that take these permissions into consideration.
We will create couple of helper functions:
- Simple check if the incoming request is authorized
- Fetch user role from database with request's ID. This ID has to correspond to user UID — name of his document. Notice how we are
get
ting a snapshot from database and then calling.data.role
on it. - Get the array of permissions with
user.role
key. userCan
function ties all of these together. It's argument is a single permission to allow actions on part of the database.
Usage
Together with rules' granular operations we can have a really fine control over user permissions.
match /users/{userId=*} {
allow read: if isSignedIn() && request.auth.uid == userId;
allow create: if isSignedIn() && request.auth.uid == userId;
allow update: if isSignedIn() && userCan("manageUsers");
allow delete: if false;
}
With this setup a user can create and read his own account details. Only user that has manageUsers
permission (currently only admin) can change user details (to prevent users from changing their roles).
Additionally, make the rules publicly known and immutable:
match /roles/roles {
allow read: if true
allow write: if false
}
And there you have it! This is by no means perfect solution for every use case under the sun. But it should give you a hint about Firestore rules' inner workings and what can one do with them. If you iterate on my idea and make it better, please let me know, I would love to hear that :)
I hope this post inspired you! Take my notes, run with them and create something great! ☀️