Serverless API Security: Firestore VS Parse
As you’re building your app, you might want to limit and control access to your stored data. I’m going to present some basic authorization methods using Firestore and Parse.
As a Software Architect at Loadmll.io, I love to simplify “complex” and make them developer-friendly.
Let’s start with a brief description of each service:
Firestore
Cloud Firestore is a flexible, scalable database for mobile, web, and server development from Firebase and Google Cloud Platform. It keeps your data in-sync across client apps using real-time listeners and offers offline support for mobile and web. Full docs here.
Parse
Parse Server is an open-source version of the Parse backend that can be deployed to any infrastructure that can run Node.js. Parse Server lets you use MongoDB or Postgres as a database. Full docs here.
Both services provide developers a built-in security framework:
Security Rules on Firebase
Firebase Security Rules allow you to control access to your stored data. The flexible syntax means that you can create rules that match anything you can think of. You can limit all write operations to the entire database to a specific document.
Service and database declaration
Cloud Firestore Security Rules always begin with the following declaration:
service cloud.firestore {
match /databases/{database}/documents {
// ...
}
}
The match /databases/{database}/documents
declaration specifies that rules should match any Cloud Firestore database in the project.
rules consist of a match
statement specifying a document path and an allow
expression detailing when reading the specified data is allowed
Testing using Rules-Simulator
Firestore has a really cool simulator which makes it easier to test and validate your security rules.
First example: Read user is allowed
This example uses one rule with three expressions. Creating a user is permitted only to admins. Update and delete operations are allowed to admins or to the requesting user himself, and read is allowed to all “signedIn” users.
Second example: Unauthorized user deletion
Here you can see that the delete user request failed, as I tried to delete userId=1234 with a non-admin user.
Accept or Reject Queries
Your security rules can also accept or reject queries based on their constraints. The request.query
variable contains the limit
, offset
, and orderBy
properties of a query. For example, your security rules can deny any query that doesn't limit the maximum number of documents retrieved to a certain range:
allow list: if request.query.limit <= 10;
Nesting match statements
Data in Cloud Firestore is organized into collections of documents, and each document may extend the hierarchy through subcollections.
When nesting match
statements, the path of the inner match
statement is always relative to the path of the outer match
statement. The following rulesets are therefore equivalent:
service cloud.firestore {
match /databases/{database}/documents {
match /users/{userId} {
match /posts/{postId} {
allow read, write: if <condition>;
}
}// equivalent to:
match /users/{userId}/posts/{postId} {
allow read, write: if <condition>;
}}
}
Recursive wildcards
If you want rules to apply to an arbitrarily deep hierarchy, use the recursive wildcard syntax, {name=**}
. For example:
service cloud.firestore {
match /databases/{database}/documents {
match /users/{document=**} {
allow read, write: if <condition>;
}
}
}
Security on Parse
Parse takes a slightly different approach. When using Parse, you can specify what operations are allowed per class. This lets you restrict the ways in which clients can access or modify your classes. The most common use-case is to limit client from creating new classes on Parse.
Once restricted, classes may only be created from the Data Browser or with a masterKey.
Parse has a lot to offer, but I’m going to focus on 3 Parse classes:
- ParseACL
- ParseUser
- ParseRole
ParseACL — Access Control Lists
A ParseACL is used to control which users and roles can access or modify a particular object. You can grant read and write permissions separately to specific users or groups of users.
Security For ParseUser Objects
The ParseUser
class is secured by default. Data stored in a ParseUser
can only be modified by that user. By default, the data can still be read by any client.
Back4App is a backend-as-a-service platform powered by Parse Open Source which you can use to build your app faster, host it and keep full control over your Backend.
Security For Other Objects
We can use a Parse.ACL
to limit the read and write access to a specific user:
const Test = Parse.Object.extend("Test");
const privateTest = new Test();
privateTest.set("description", "This test is private!");
privateTest.setACL(new Parse.ACL(Parse.User.current()));
privateTest.save();
Permissions can also be granted to a group of users:
const Test = Parse.Object.extend("Test");
const groupTest = new Test();
const groupACL = new Parse.ACL();for (let i = 0; i < users.length; i++) {
groupACL.setReadAccess(users[i], true);
groupACL.setWriteAccess(users[i], true);
}groupTest.setACL(groupACL);
groupTest.save();
All of this is great, but what if you’re trying to implement something not so trivial?
Please welcome Roles!
Parse supports a form of Role-based Access Control, which is more sophisticated than simple ACL.
Roles provide a logical way of grouping users with common access privileges to your Parse data. Roles are named objects that contain users and other roles. Any permission granted to a role is implicitly granted to its users.
This feature allows you to limit the access without having to manually grant permission to every resource for each user.
Security for Role Objects
To create a new Parse.Role
, you would write:
const roleACL = new Parse.ACL();
roleACL.setPublicReadAccess(true);
const role = new Parse.Role("Admin", roleACL);
role.save();// By specifying no write privileges for the ACL, we can ensure the role cannot be altered.
You can add users and roles that should inherit your new role’s permissions through the “users” and “roles” relations on Parse.Role
:
const role = new Parse.Role("Admin", roleACL);
role.getUsers().add(usersToAddToRole);
role.getRoles().add(rolesToAddToRole);
role.save();
Conclusion and a short summary
I tried to summarize my comparison in a table:
Parse provides a wider range of options, by allowing the relationship between objects, users, and their permissions using ACL and roles.
It is also good to know that Parse has more advanced features like using sessionToken
in order to check the fulfillment of the ParseObject
’s ACL, or to use amasterKey
which can override any ACLs, and it’ll bypass all the security mechanisms.
In addition, Parse allows you to split the cloud code into separate files and elegantly manage your permissions. There is a graphical display for each object, making it very easy for debugging.
Firebase provides the security rules for managing the permissions by direct access to documents. The fact that the permissions can be configured in a way that is nested allows for very readable code writing. The simulator for the UI and the emulator as npm
package significantly increases the ability to test the rules and thus has a huge gap on Parse.
My personal recommendation is to thoroughly understand how these security rules work before your app grows to a significant user base. It is challenging to write these rules while your users are already using your app. I ran into several cases where developers were forced to change the structure of the database due to difficulties in writing security rules.
I hope that this article will help you choose the best service for your specific needs.