RBAC with Google Firestore
This post will explain how to implement role based access control (RBAC) using the Google Firestore serverless database. Firebase and Firestore in particular with the concept presented in this post offers the most seamless integration of serverless infrastructure with a mobile client at this point. It has become my go to backend for all minor web apps I build.
- Firestore basics
- Firestore Security Rules
- RBAC example scenario
- Required security rules for RBAC
- Implementing security rules for RBAC
- Summary
Firestore basics
Firestore is database that is part of Googles Firebase suite for mobile app development. It is currently in beta and has the potential to replace the current Firebase Realtime Database due to its superior API and features. > Cloud Firestore is a flexible, scalable database for mobile, web, and server development from Firebase and Google Cloud Platform
For those that never used a Firebase database; it is a NoSQL document oriented database. Firestore allows you to nest documents by creating multiple collections inside a document.
The Firebase suite is built for mobile development and provides SDKs for all major languages. JavaScript/Node.js, Swift, Objective C, Android, Java, Python, Ruby, Go. The SDKs allow add, query or delete data as well as other operations required when interacting with a database as a client. One feature I really like is the possiblity to register your client to receive updates automatically. This allows you to build three way data binding in realtime applications easily. This is a feature I used in my first project with Firebase.
In combination with the Firebase authentication provider you can limit access the database to people that are logged in. The auth provider also provides an SDK and requires only a few lines of code to implement in a web app.
Firestore Security Rules
The ability to create a detailed rule set make Firestore enable use cases for a serverless database without any backend code and still keeping data secure. It is also the foundation for building a role based access control.
All roles and authorization rules will be enforced by the Firestore server
Security rules are written in a JavaScript-like syntax but have their own methods. First you nest match
operators to specify the document level you want to be affected by this rule. Use {wildcards}
that can later on be referenced in the rule definition. Granting/denying access is done via an allow <method> if
statement that grants access if it returns true
or otherwise blocks the transaction.
service cloud.firestore {
match /databases/{database}/documents {
allow read, write: if <some_condition>;
allow delete: if false;
}
}
There are five methods that can be specified:
get
: retrieve a single documentlist
: read an entire collection or perform queriescreate
: write to non existing documentsupdate
: write to existing documentsdelete
: remove a document
The modifying operations 3-5 can be addressed using the write
method instead of specifying them individually, read
applies both get and list. If multiple rules match for a request only one needs to resolve to true
for the request to be successful.
// allow anyone to read but only signed in users to create/update; only a specific user can delete
service cloud.firestore {
match /databases/{database}/documents {
match /posts/{document=**} {
allow read: if true;
allow create, update: if request.auth != null;
allow delete: if request.auth.uid == 'anoff_io';
}
}
}
RBAC example scenario
We will setup RBAC for a simple content site with posts that can be commented with the following roles:
- admin: can assign roles
- writer: can create new posts, can modify its own posts
- editor: can edit any post, delete comments
- user: can create and modify its own comments, can modify his user settings
On the root level of Firestore we add three different collections. One holds the content, one for user details and one that implements the roles per user. The reason to separate role assignments from the user document is to easily allow users to modify their own details without giving them the possibility to grant themselves new roles.
Required security rules for RBAC
Given the collection setup and the above role definition we can define the rules we need to implement for each collection.
User | Roles | Posts | Comments | |
---|---|---|---|---|
get | anyone | user (own), admin | anyone | anyone |
list | noone | admin | anyone | anyone |
create | noone | noone | writer | user |
update | user (own profile) | admin | writer (own), editor | user (own) |
delete | user (own profile) | noone | writer (own) | user (own), editor |
Implementing security rules for RBAC
We start with a default DENY ALL policy setup for the collections in our project.
service cloud.firestore {
match /databases/{database}/documents {
// this addresses any entry in the user collection
match /users/{user} {
allow read, write: if false;
}
// rules for the roles setup
match /roles/{user} {
allow read, write: if false;
}
match /posts/{post} {
allow read, write: if false;
}
match /posts/{post}/comments/{comment} {
allow read, write: if false;
}
}
}
RBAC helper functions
We start by implementing a few custom functions that help us define role based rules.
service cloud.firestore {
match /databases/{database}/documents {
...
// the request object contains info about the authentication status of the requesting user
// if the .auth property is not set, the user is not signed in
function isSignedIn() {
return request.auth != null;
}
// return the current users entry in the roles collection
function getRoles() {
return get(/databases/$(database)/documents/roles/$(request.auth.uid)).data
}
// check if the current user has a specific role
function hasRole(role) {
return isSignedIn() && getRoles()[role] == true;
}
// check if the user has any of the given roles (list)
function hasAnyRole(roles) {
return isSignedIn() && getRoles().keys().hasAny(roles);
}
}
}
With these functions in the security rules you can now easily implement security roles based on the users roles.
Users collection
First we make sure only the user itself can modify its data and anyone can view a specific user profile to enable them to see who posted comments.
match /users/{user} {
// anyone can see a specific users profile data (name, email etc), in a real scenario you might want to make this more granular
allow get: if true;
// noone can query for users
allow list, create: if false;
// users can modify their own data
allow update, delete: if request.auth.uid == user;
}
Roles collection
Next we enforce the rules for the role collection which should only allow admins to set roles. Of course a user should be able to retrieve its own designated roles in order to switch frontend features accordingly.
match /roles/{user} {
allow get: if request.auth.uid == user || hasRole('admin');
allow list: if hasRole('admin');
allow update: if hasRole('admin');
allow create, delete: if false;
}
Posts collection
This one is a little trickier because we first need to figure out who actually created the post if we want to enforce the update rule. Any writer should be able to create a new post but only update their own. An editor on the other hand should be able to update anyones post but not create one.
match /posts/{post} {
allow get, list: if true;
allow create: if hasRole('writer');
// check if the post author is identical to requesting user
allow update: if (hasRole('writer') && resource.data.author == request.auth.uid) || hasRole('editor');
allow delete: if hasRole('writer') && resource.data.author == request.auth.uid;
}
Comments collection
Make sure that users can modify or delete their comments and editors can moderate (delete) anyones comments. Anyone should be able to see read the available comments.
match /posts/{post}/comments/{comment} {
allow get, list: if true;
allow create: if hasRole('user');
// check if the comment author is identical to requesting user
allow update: if resource.data.author == request.auth.uid);
allow delete: if hasRole('editor') || (hasRole('user') && resource.data.author == request.auth.uid);
}
Summary
You created a Firestore ruleset that enforces data privacy and integrity by limiting access to resources based according to a role design. To bring RBAC to your application you need to consider a few things:
- Deploy the security rules into your Firebase project, highly recommend using the CLI or Travis
- Implement a Firebase function that creates an entry in the
users
androles
collection for each new user using the authentication trigger - Log in to your app and then manually grant yourself admin rights via the Firebase console
To see a similar design implemented you can check out my techradar project which also showcases how to implement RBAC on the frontend and setting up Travis CI to deploy Firebase rules. There are additional features available to secure Firestore data for example checking which parts of a resource are being changed in an update process to allow users to update only specific properties.
Drop me a message on Twitter if you have any feedback about this post. Also check out my previous post on creating PlantUML diagrams.