My Firestore Security Rules Reference Notes
Develop a SaaS product like Google Drive using these Firestore security rules
Updated: 23 May 2020
1. Basic Understanding of Firestore Security Rules
1.1.
Read and write to all documents using wildcard match /{document=**}
Benefit: Faster early stage development without caring about security
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// START
match /{document=**} {
allow read, write: if true;
}
// END
// All below snippets will replace the START and END segment }
}
1.2.
Read all documents in giveaway_campaigns collections using match /deeptent_giveaways/{campaignId}
is going to tell our rules to match this wildcard path identifier.
Benefit: Enhance security control over what collection everyone can read
match /deeptent_giveaways/{giveawayId} {
allow read: if true;
}
1.3.
Read all documents in giveaway_campaigns collections using match /deeptent_giveaways/7v2ilw2sxhj7
is going to tell our rules to match this specific path.
Benefit: Tighten security control over what collection & document everyone can read
match /deeptent_giveaways/7v2ilw2sxhj7 {
allow read: if true;
}
1.4.
Check that the write request’s resource has a certain field and data type before writing to Firestore i.e. age and age is number
match /deeptent_giveaways/{giveawayId} {
allow write: if isIntAge();
}
function isAgeInt() {
return request.resource.data.age is int;
}
1.5.
Check that the write request’s resource has a certain field and data type before writing to Firestore i.e. name and name is string
match /deeptent_giveaways/{giveawayId} {
allow write: if isStringName();
}
function isNameString() {
return request.resource.data.name is string;
}
1.6.
Only when the document field visibility is ‘public’ then user can read
match /deeptent_giveaways/{giveawayId} {
allow read: if isPublic();
}
function isPublic() {
return resource.data.visibility == 'public'
}
1.7.
Understand the difference between incoming request data vs. existing data.
match /deeptent_giveaways/{giveawayId} {
allow update: if existingData().locked == false;
allow delete: if incomingData().price > 10;
}function existingData() {
return resource.data;
}function incomingData() {
return request.resource.data;
}
2. Intermediate Understanding of Firestore Security Rules
2.1.
Check that the update request data does not contain a field
Benefit: Protect a field that matters to you
match /deeptent_giveaways/{giveawayId} {
allow update: if notHaving('age');
}function notHaving(field) {
return !(field in request.resource.data);
}
2.2.
Check that the update request data does not modify a field. The technique we use is incoming-field-value-equal-existing-field-value e.g. request.resource.data.age == resource.data.age
Benefit: Protect the value of a field that matters to you
match /deeptent_giveaways/{giveawayId} {
allow update: if notModifying('age');
}function notModifying(field) {
return request.resource.data[field] == resource.data[field];
}
2.3.
Check that the update request data does not contain or modify a field
match /deeptent_giveaways/{giveawayId} {
allow update: if notUpdating('age');
}function notUpdating(field) {
return notHaving(field) || notModifying(field);
}function notHaving(field) {
return !(field in request.resource.data);
}function notModifying(field) {
return request.resource.data[field] == resource.data[field];
}
2.4.
Check that the update request data updates specific fields only. The technique here is using rules.MapDiff.
Benefit: Tighten update control
match /deeptent_giveaways/{giveawayId} {
allow update: if updateNameAgeOnly();
}function updateNameAgeOnly() {
return request.resource.data.diff(resource.data).changedKeys().hasOnly(['name', 'age']);
}
2.5.
Validate incoming update request data using time validation
Use case: Forum do lock threads more than e.g. 6 months old. The thread can be moved from ‘hot data’ to ‘cold data’ after certain age.
match /deeptent_giveaways/{giveawayId} {
allow create: if isNow();
allow update: if isValidDuration();
}function isNow() {
return request.resource.data.createdAt == request.time;
}function isValidDuration() {
return if request.time < resource.data.createdAt + duration.value(180, 's')
}
2.6.
Validate incoming update request data using time validation such that no read or write after 22 Jun 2020.
Benefit: Tighten update control to allow update within a specific time e.g. One-Time Password OTP
Use Case: You do not want people to use your SaaS after a certain date
match /deeptent_giveaways/{giveawayId} {
allow write: if isValidPeriod();
}function isValidPeriod() {
return request.time < timestamp.date(2020, 6, 22);
}
2.7.
Check auth token data to see if email is verified
match /deeptent_giveaways/{giveawayId} {
allow write: if isEmailVerified();
}function isEmailVerified() {
return request.auth.token.email_verified;
}
3. Advance Understanding of Firestore Security Rules
To develop a Software-as-a-Service product like Deeptent, LucidChart, and Google Drive, here is how you can do it with Firestore.
For example we have this data structure and it’s nested as below.
Collection (id = 'deeptent_giveaways')
-- Document (id = '7v2ilw2sxhj7')
---- Collection (id = 'giveaway_chances')
------- Document (id = '2wj8ldicjem')
The document 7v2ilw2sxhj7 has the follow field names and field values
{
"name": "QR Giveaway for MORE feedback",
"description": "It's the only digital giveaway management and lucky draw system that you’ll ever need for your live events or places.",
"keywords": ['attention', 'feedback', 'lucky draw', 'giveaway', 'qr code']
"url": "https://www.deeptent.com",
"members":{
"uid_1":"owner",
"uid_2":"editor",
"uid_3":"editor"
}
}
So we allow only members who are owner and editors to
- Update document 7v2ilw2sxhj7
- Read and update document 2wj8ldicjem
We will use interpolate technique $( )
dollar sign with parentheses. this is going to interpolate our variables. E.g. $(database)
, $(request.auth.uid)
match /deeptent_giveaways/{giveawayId} {
allow update: if isOneOfRoles(resource, ['owner', 'editor']);
}match /deeptent_giveaways/{giveawayId}/giveaway_chances/{chanceId} {
allow read: if isOneOfRoles(get(/databases/$(database)/documents/deeptent_giveaways/$(giveawayId)), ['owner', 'editor']);
allow update: if isOneOfRoles(get(/databases/$(database)/documents/deeptent_giveaways/$(giveawayId)), ['owner', 'editor']);}function isSignedIn() {
return request.auth.uid != null;
}function getRole(rsc) {
// Read from the "roles" map in the resource (rsc).
return rsc.data.members[request.auth.uid];
}function isOneOfRoles(rsc, array) {
// Determine if the user is one of an array of roles
return isSignedIn() && (getRole(rsc) in array);
}
4. Beyond Firestore Security Rules Understanding
4.1.
Firestore security rules can be simple yet complex. Beyond that, there is Firestore query. There are many query operators which are really confusing for me. So here is my comparison.
First, let’s start with this data structure represented as an array of objects. Each object is a Firestore document i.e. Apple is a document, Banana is a document, and Orange is a document.
Comparing Firestore query operators:
- array-contains-any does NOT preserve the order.
- array-contains-any does NOT reduce the number of billed document reads. If your array contains 10 items, it will still cost 10 document reads. If you perform 10 array-contains-any queries, each with 10 array items, it will still cost 100 reads. So if you need N documents, there is no shortcut to make those N document reads cost less than the cost of N document reads.
Code snippet A for <, <=, >, >=
.where("calories", ">=", 50)
.where("calories", "<=", 90)
Code snippet B for ==
.where("colour", "==", "Yellow")
Code snippet C for in
.where("colour", "in", ["Yellow", "Red"])
Code snippet D for array-contains
.where("nutrition_facts", "array-contains", "Iron")
Code snippet E for array-contains-any
.where("nutrition_facts", "array-contains-any", ["Iron", "Calcium"])
4.2.
It’s important to take note of the limitations when writing Firestore security rules. These limitations are available at https://firebase.google.com/docs/firestore/quotas I am placing it here as a reminder for myself.
My Reflection
I am proud to have picked Google Cloud Platform’s Firebase (i.e. Firestore, Function, Hosting, Storage, and Authentication) for my SaaS product. GCP Firebase is automated and amazingly manageable to develop my Software-as-a-Service even for a one-man show. Using Google Firebase’s fleet of products for my SaaS product saved me time that I can focus on my sales and marketing.
Future Updates
My latest discovery is Firestore MapDiff Security Rule.
I will continue to monitor for new development at
Cheers.