Protect your Firestore data with Firebase Security Rules

1. Before you begin

Cloud Firestore, Cloud Storage for Firebase, and the Realtime Database rely on configuration files you write to grant read and write access. That configuration, called Security Rules, can also act as a kind of schema for your app. It's one of the most important parts of developing your application. And this codelab will walk you through it.

Prerequisites

  • A simple editor such as Visual Studio Code, Atom, or Sublime Text
  • Node.js 8.6.0 or higher (to install Node.js, use nvm ; to check your version, run node --version )
  • Java 7 or higher (to install Java use these instructions ; to check your version, run java -version )

What you'll do

In this codelab, you will secure a simple blog platform built on Firestore. You will use the Firestore emulator to run unit tests against the Security Rules, and ensure that the rules allow and disallow the access you expect.

You'll learn how to:

  • Grant granular permissions
  • Enforce data and type validations
  • Implement Attribute Based Access Control
  • Grant access based on authentication method
  • Create custom functions
  • Create time-based Security Rules
  • Implement a deny list and soft deletes
  • Understand when to denormalize data to meet multiple access patterns

2. Set up

This is a blogging application. Here's a high level summary of the application functionality:

Draft blog posts:

  • Users can create draft blog posts, which live in the drafts collection.
  • The author can continue to update a draft until it's ready to be published.
  • When it's ready to be published, a Firebase Function is triggered that creates a new document in the published collection.
  • Drafts can be deleted by the author or by site moderators

Published blog posts:

  • Published posts can't be created by users, only via a function.
  • They can only be soft-deleted, which updates a visible attribute to false.

Comments

  • Published posts allow comments, which are a subcollection on each published post.
  • To reduce abuse, users must have a verified email address and not be on a denyist in order to leave a comment.
  • Comments can only be updated within an hour after it's posted.
  • Comments can be deleted by the comment author, the author of the original post, or by moderators.

In addition to access rules, you'll create Security Rules that enforce required fields and data validations.

Everything will happen locally, using the Firebase Emulator Suite.

Get the source code

In this codelab, you'll start off with tests for the Security Rules, but mimimal Security Rules themselves, so the first thing you need to do is clone the source to run the tests:

$ git clone https://github.com/FirebaseExtended/codelab-rules.git

Then move into the initial-state directory, where you will work for the remainder of this codelab:

$ cd codelab-rules/initial-state

Now, install the dependencies so you can run the tests. If you're on a slower internet connection this may take a minute or two:

# Move into the functions directory, install dependencies, jump out.
$ cd functions && npm install && cd -

Get the Firebase CLI

The Emulator Suite you'll use to run the tests is part of the Firebase CLI (command-line interface) which can be installed on your machine with the following command:

$ npm install -g firebase-tools

Next, confirm that you have the latest version of the CLI. This codelab should work with version 8.4.0 or higher but later versions include more bug fixes.

$ firebase --version
9.10.2

3. Run the tests

In this section, you'll run the tests locally. This means it is time to boot up the Emulator Suite.

Start the Emulators

The application you'll work with has three main Firestore collections: drafts contain blog posts that are in progress, the published collection contains the blog posts that have been published, and comments are a subcollection on published posts. The repo comes with unit tests for the Security Rules that define the user attributes and other conditions required for a user to create, read, update, and delete documents in drafts , published and comments collections. You'll write the Security Rules to make those tests pass.

To start, your database is locked down: reads and writes to the database are universally denied, and all the tests fail. As you write Security Rules, the tests will pass. To see the tests, open functions/test.js in your editor.

On the command line, start the emulators using emulators:exec and run the tests:

$ firebase emulators:exec --project=codelab --import=.seed "cd functions; npm test"

Scroll to the top of the output:

$ firebase emulators:exec --project=codelab --import=.seed "cd functions; npm test"
i  emulators: Starting emulators: functions, firestore, hosting
⚠  functions: The following emulators are not running, calls to these services from the Functions emulator will affect production: auth, database, pubsub
⚠  functions: Unable to fetch project Admin SDK configuration, Admin SDK behavior in Cloud Functions emulator may be incorrect.
i  firestore: Importing data from /Users/user/src/firebase/rules-codelab/initial-state/.seed/firestore_export/firestore_export.overall_export_metadata
i  firestore: Firestore Emulator logging to firestore-debug.log
⚠  hosting: Authentication error when trying to fetch your current web app configuration, have you run firebase login?
⚠  hosting: Could not fetch web app configuration and there is no cached configuration on this machine. Check your internet connection and make sure you are authenticated. To continue, you must call firebase.initializeApp({...}) in your code before using Firebase.
i  hosting: Serving hosting files from: public
✔  hosting: Local server: http://localhost:5000
i  functions: Watching "/Users/user/src/firebase/rules-codelab/initial-state/functions" for Cloud Functions...
✔  functions[publishPost]: http function initialized (http://localhost:5001/codelab/us-central1/publishPost).
✔  functions[softDelete]: http function initialized (http://localhost:5001/codelab/us-central1/softDelete).
i  Running script: pushd functions; npm test
~/src/firebase/rules-codelab/initial-state/functions ~/src/firebase/rules-codelab/initial-state

> functions@ test /Users/user/src/firebase/rules-codelab/initial-state/functions
> mocha

(node:76619) ExperimentalWarning: Conditional exports is an experimental feature. This feature could change at any time


  Draft blog posts
    1) can be created with required fields by the author
    2) can be updated by author if immutable fields are unchanged
    3) can be read by the author and moderator

  Published blog posts
    4) can be read by everyone; created or deleted by no one
    5) can be updated by author or moderator

  Comments on published blog posts
    6) can be read by anyone with a permanent account
    7) can be created if email is verfied and not blocked
    8) can be updated by author for 1 hour after creation
    9) can be deleted by an author or moderator


  0 passing (848ms)
  9 failing

...

Right now there are 9 failures. As you build the rules file, you can measure progress by watching more tests pass.

4. Create blog post drafts.

Because the access for draft blog posts is so different from the access for published blog posts, this blogging app stores draft blog posts in a separate collection, /drafts . Drafts can only be accessed by the author or a moderator, and has validations for required and immutable fields.

Opening the firestore.rules file, you'll find a default rules file:

  rules_version 
  
 = 
  
 '2' 
 ; 
 service 
  
 cloud 
 . 
 firestore 
  
 { 
  
 match 
  
 /databases/{database 
 } 
 / 
 documents 
  
 { 
  
 match 
  
 /{document=** 
 } 
  
 { 
  
 allow 
  
 read, 
  
 write 
 : 
  
 if 
  
 false 
 ; 
  
 } 
  
 } 
 } 
 

The match statement, match /{document=**} , is using the ** syntax to recursively apply to all documents in subcollections. And because it's at the top level, right now the same blanket rule applies to all requests, no matter who is making the request or what data they're trying to read or write.

Start by removing the inner-most match statement and replacing it with match /drafts/{draftID} . (Comments of the structure of documents can be helpful in rules, and will be included in this codelab; they're always optional.)

  rules_version 
  
 = 
  
 '2' 
 ; 
 service 
  
 cloud 
 . 
 firestore 
  
 { 
  
 match 
  
 / 
 databases 
 / 
 { 
 database 
 } 
 / 
 documents 
  
 { 
  
 match 
  
 / 
 drafts 
 / 
 { 
 draftID 
 } 
  
 { 
  
 // 
  
 `authorUID` 
 : 
  
 string 
 , 
  
 required 
  
 // 
  
 `content` 
 : 
  
 string 
 , 
  
 optional 
  
 // 
  
 `createdAt` 
 : 
  
 timestamp 
 , 
  
 required 
  
 // 
  
 `title` 
 : 
  
 string 
 , 
 < 
 50 
  
 characters 
 , 
  
 required 
  
 // 
  
 `url` 
 : 
  
 string 
 , 
  
 optional 
  
 } 
  
 } 
 } 
 

The first rule you'll write for drafts will control who can create the documents. In this application, drafts can only be created by the person listed as the author. Check that the UID of the person making the request is the same UID listed in the document.

The first condition for the create will be:

 request.resource.data.authorUID == request.auth.uid 

Next, documents can only be created if they include the three required fields, authorUID , createdAt , and title . (The user doesn't supply the createdAt field; this is enforcing that the app must add it before trying to create a document.) Since you only need to check that the attributes are being created, you can check that request.resource has all those keys:

 request.resource.data.keys().hasAll([
  "authorUID",
  "createdAt",
  "title"
]) 

The final requirement for creating a blog post is that the title can't be more than 50 characters long:

 request.resource.data.title.size() < 50 

Since all these conditions must be true, concatenate these together with logical AND operator, && . The first rule becomes:

  rules_version 
  
 = 
  
 '2' 
 ; 
 service 
  
 cloud 
 . 
 firestore 
  
 { 
  
 match 
  
 / 
 databases 
 / 
 { 
 database 
 } 
 / 
 documents 
  
 { 
  
 match 
  
 / 
 drafts 
 / 
 { 
 draftID 
 } 
  
 { 
  
 // 
  
 `authorUID` 
 : 
  
 string 
 , 
  
 required 
  
 // 
  
 `content` 
 : 
  
 string 
 , 
  
 optional 
  
 // 
  
 `createdAt` 
 : 
  
 timestamp 
 , 
  
 required 
  
 // 
  
 `title` 
 : 
  
 string 
 , 
 < 
 50 
  
 characters 
 , 
  
 required 
  
 // 
  
 `url` 
 : 
  
 string 
 , 
  
 optional 
  
 allow 
  
 create 
 : 
  
 if 
  
 // 
  
 User 
  
 creating 
  
 document 
  
 is 
  
 draft 
  
 author 
  
 request 
 . 
 auth 
 . 
 uid 
  
 == 
  
 request 
 . 
 resource 
 . 
 data 
 . 
 authorUID 
  
&&  
 // 
  
 Must 
  
 include 
  
 title 
 , 
  
 author 
 , 
  
 and 
  
 url 
  
 fields 
  
 request 
 . 
 resource 
 . 
 data 
 . 
 keys 
 (). 
 hasAll 
 ( 
 [ 
  
 "authorUID" 
 , 
  
 "createdAt" 
 , 
  
 "title" 
  
 ] 
 ) 
  
&&  
 // 
  
 Title 
  
 must 
  
 be 
 < 
 50 
  
 characters 
  
 long 
  
 request 
 . 
 resource 
 . 
 data 
 . 
 title 
 . 
 size 
 () 
 < 
 50 
 ; 
  
 } 
  
 } 
 } 
 

In the terminal, rerun the tests and confirm that the first test passes.

5. Update blog post drafts.

Next, as authors refine their draft blog posts, they'll edit the draft documents. Create a rule for the conditions when a post can be updated. First, only the author can update their drafts. Note that here you check the UID that's already written, resource.data.authorUID :

 resource.data.authorUID == request.auth.uid 

The second requirement for an update is that two attributes, authorUID and createdAt should not change:

 request.resource.data.diff(resource.data).unchangedKeys().hasAll([
    "authorUID",
    "createdAt"
]); 

And finally, the title should be 50 characters or fewer:

 request.resource.data.title.size() < 50; 

Since these conditions all need to be met, concatenate them together with && :

  allow 
  
 update 
 : 
  
 if 
  
 // 
  
 User 
  
 is 
  
 the 
  
 author 
 , 
  
 and 
  
 resource 
 . 
 data 
 . 
 authorUID 
  
 == 
  
 request 
 . 
 auth 
 . 
 uid 
  
&&  
 // 
  
 `authorUID` 
  
 and 
  
 `createdAt` 
  
 are 
  
 unchanged 
  
 request 
 . 
 resource 
 . 
 data 
 . 
 diff 
 ( 
 resource 
 . 
 data 
 ). 
 unchangedKeys 
 (). 
 hasAll 
 ( 
 [ 
  
 "authorUID" 
 , 
  
 "createdAt" 
  
 ] 
 ) 
  
&&  
 // 
  
 Title 
  
 must 
  
 be 
 < 
 50 
  
 characters 
  
 long 
  
 request 
 . 
 resource 
 . 
 data 
 . 
 title 
 . 
 size 
 () 
 < 
 50 
 ; 
 

The complete rules become:

  rules_version 
  
 = 
  
 '2' 
 ; 
 service 
  
 cloud 
 . 
 firestore 
  
 { 
  
 match 
  
 / 
 databases 
 / 
 { 
 database 
 } 
 / 
 documents 
  
 { 
  
 match 
  
 / 
 drafts 
 / 
 { 
 draftID 
 } 
  
 { 
  
 // 
  
 `authorUID` 
 : 
  
 string 
 , 
  
 required 
  
 // 
  
 `content` 
 : 
  
 string 
 , 
  
 optional 
  
 // 
  
 `createdAt` 
 : 
  
 timestamp 
 , 
  
 required 
  
 // 
  
 `title` 
 : 
  
 string 
 , 
 < 
 50 
  
 characters 
 , 
  
 required 
  
 // 
  
 `url` 
 : 
  
 string 
 , 
  
 optional 
  
 allow 
  
 create 
 : 
  
 if 
  
 // 
  
 User 
  
 creating 
  
 document 
  
 is 
  
 draft 
  
 author 
  
 request 
 . 
 auth 
 . 
 uid 
  
 == 
  
 request 
 . 
 resource 
 . 
 data 
 . 
 authorUID 
  
&&  
 // 
  
 Must 
  
 include 
  
 title 
 , 
  
 author 
 , 
  
 and 
  
 url 
  
 fields 
  
 request 
 . 
 resource 
 . 
 data 
 . 
 keys 
 (). 
 hasAll 
 ( 
 [ 
  
 "authorUID" 
 , 
  
 "createdAt" 
 , 
  
 "title" 
  
 ] 
 ) 
  
&&  
 // 
  
 Title 
  
 must 
  
 be 
 < 
 50 
  
 characters 
  
 long 
  
 request 
 . 
 resource 
 . 
 data 
 . 
 title 
 . 
 size 
 () 
 < 
 50 
 ; 
  
 allow 
  
 update 
 : 
  
 if 
  
 // 
  
 User 
  
 is 
  
 the 
  
 author 
 , 
  
 and 
  
 resource 
 . 
 data 
 . 
 authorUID 
  
 == 
  
 request 
 . 
 auth 
 . 
 uid 
  
&&  
 // 
  
 `authorUID` 
  
 and 
  
 `createdAt` 
  
 are 
  
 unchanged 
  
 request 
 . 
 resource 
 . 
 data 
 . 
 diff 
 ( 
 resource 
 . 
 data 
 ). 
 unchangedKeys 
 (). 
 hasAll 
 ( 
 [ 
  
 "authorUID" 
 , 
  
 "createdAt" 
  
 ] 
 ) 
  
&&  
 // 
  
 Title 
  
 must 
  
 be 
 < 
 50 
  
 characters 
  
 long 
  
 request 
 . 
 resource 
 . 
 data 
 . 
 title 
 . 
 size 
 () 
 < 
 50 
 ; 
  
 } 
  
 } 
 } 
 

Rerun your tests and confirm that another test passes.

6. Delete and read drafts: Attribute Based Access Control

Just as authors can create and update drafts, they can also delete drafts.

 resource.data.authorUID == request.auth.uid 

Additionally, authors with an isModerator attribute on their auth token are allowed to delete drafts:

 request.auth.token.isModerator == true 

Since either of these conditions are sufficient for a delete, concatenate them with a logical OR operator, || :

 allow delete: if resource.data.authorUID == request.auth.uid || request.auth.token.isModerator == true 

The same conditions apply to reads, so that permission can be added to the rule:

 allow read, delete: if resource.data.authorUID == request.auth.uid || request.auth.token.isModerator == true 

The full rules are now:

  rules_version 
  
 = 
  
 '2' 
 ; 
 service 
  
 cloud 
 . 
 firestore 
  
 { 
  
 match 
  
 / 
 databases 
 / 
 { 
 database 
 } 
 / 
 documents 
  
 { 
  
 match 
  
 / 
 drafts 
 / 
 { 
 draftID 
 } 
  
 { 
  
 // 
  
 `authorUID` 
 : 
  
 string 
 , 
  
 required 
  
 // 
  
 `content` 
 : 
  
 string 
 , 
  
 optional 
  
 // 
  
 `createdAt` 
 : 
  
 timestamp 
 , 
  
 required 
  
 // 
  
 `title` 
 : 
  
 string 
 , 
 < 
 50 
  
 characters 
 , 
  
 required 
  
 // 
  
 `url` 
 : 
  
 string 
 , 
  
 optional 
  
 allow 
  
 create 
 : 
  
 if 
  
 // 
  
 User 
  
 creating 
  
 document 
  
 is 
  
 draft 
  
 author 
  
 request 
 . 
 auth 
 . 
 uid 
  
 == 
  
 request 
 . 
 resource 
 . 
 data 
 . 
 authorUID 
  
&&  
 // 
  
 Must 
  
 include 
  
 title 
 , 
  
 author 
 , 
  
 and 
  
 url 
  
 fields 
  
 request 
 . 
 resource 
 . 
 data 
 . 
 keys 
 (). 
 hasAll 
 ( 
 [ 
  
 "authorUID" 
 , 
  
 "createdAt" 
 , 
  
 "title" 
  
 ] 
 ) 
  
&&  
 // 
  
 Title 
  
 must 
  
 be 
 < 
 50 
  
 characters 
  
 long 
  
 request 
 . 
 resource 
 . 
 data 
 . 
 title 
 . 
 size 
 () 
 < 
 50 
 ; 
  
 allow 
  
 update 
 : 
  
 if 
  
 // 
  
 User 
  
 is 
  
 the 
  
 author 
 , 
  
 and 
  
 resource 
 . 
 data 
 . 
 authorUID 
  
 == 
  
 request 
 . 
 auth 
 . 
 uid 
  
&&  
 // 
  
 `authorUID` 
  
 and 
  
 `createdAt` 
  
 are 
  
 unchanged 
  
 request 
 . 
 resource 
 . 
 data 
 . 
 diff 
 ( 
 resource 
 . 
 data 
 ). 
 unchangedKeys 
 (). 
 hasAll 
 ( 
 [ 
  
 "authorUID" 
 , 
  
 "createdAt" 
  
 ] 
 ) 
  
&&  
 // 
  
 Title 
  
 must 
  
 be 
 < 
 50 
  
 characters 
  
 long 
  
 request 
 . 
 resource 
 . 
 data 
 . 
 title 
 . 
 size 
 () 
 < 
 50 
 ; 
  
 allow 
  
 read 
 , 
  
 delete 
 : 
  
 if 
  
 // 
  
 User 
  
 is 
  
 draft 
  
 author 
  
 resource 
 . 
 data 
 . 
 authorUID 
  
 == 
  
 request 
 . 
 auth 
 . 
 uid 
  
 || 
  
 // 
  
 User 
  
 is 
  
 a 
  
 moderator 
  
 request 
 . 
 auth 
 . 
 token 
 . 
 isModerator 
  
 == 
  
 true 
 ; 
  
 } 
  
 } 
 } 
 

Rerun your tests and confirm that another test now passes.

7. Reads, creates, and deletes for published posts: denormalizing for different access patterns

Because access patterns for the published posts and draft posts are so different, this app denormalizes the posts into separate draft and published collections. For instance, published posts can be read by anyone but can't be hard hard-deleted, while drafts can be deleted but can only be read by the author and moderators. In this app, when a user wants to publish a draft blog post, a function is triggered that will create the new published post.

Next, you'll write the rules for published posts. The simplest rules to write are that published posts can be read by anyone, and can't be created or deleted by anyone. Add these rules:

  match 
  
 / 
 published 
 / 
 { 
 postID 
 } 
  
 { 
  
 // 
  
 `authorUID` 
 : 
  
 string 
 , 
  
 required 
  
 // 
  
 `content` 
 : 
  
 string 
 , 
  
 required 
  
 // 
  
 `publishedAt` 
 : 
  
 timestamp 
 , 
  
 required 
  
 // 
  
 `title` 
 : 
  
 string 
 , 
 < 
 50 
  
 characters 
 , 
  
 required 
  
 // 
  
 `url` 
 : 
  
 string 
 , 
  
 required 
  
 // 
  
 `visible` 
 : 
  
 boolean 
 , 
  
 required 
  
 // 
  
 Can 
  
 be 
  
 read 
  
 by 
  
 everyone 
  
 allow 
  
 read 
 : 
  
 if 
  
 true 
 ; 
  
 // 
  
 Published 
  
 posts 
  
 are 
  
 created 
  
 only 
  
 via 
  
 functions 
 , 
  
 never 
  
 by 
  
 users 
  
 // 
  
 No 
  
 hard 
  
 deletes 
 ; 
  
 soft 
  
 deletes 
  
 update 
  
 `visible` 
  
 field 
 . 
  
 allow 
  
 create 
 , 
  
 delete 
 : 
  
 if 
  
 false 
 ; 
 } 
 

Adding these to the existing rules, the entire rules file becomes:

  rules_version 
  
 = 
  
 '2' 
 ; 
 service 
  
 cloud 
 . 
 firestore 
  
 { 
  
 match 
  
 / 
 databases 
 / 
 { 
 database 
 } 
 / 
 documents 
  
 { 
  
 match 
  
 / 
 drafts 
 / 
 { 
 draftID 
 } 
  
 { 
  
 // 
  
 `authorUID` 
 : 
  
 string 
 , 
  
 required 
  
 // 
  
 `content` 
 : 
  
 string 
 , 
  
 optional 
  
 // 
  
 `createdAt` 
 : 
  
 timestamp 
 , 
  
 required 
  
 // 
  
 `title` 
 : 
  
 string 
 , 
 < 
 50 
  
 characters 
 , 
  
 required 
  
 // 
  
 `url` 
 : 
  
 string 
 , 
  
 optional 
  
 allow 
  
 create 
 : 
  
 if 
  
 // 
  
 User 
  
 creating 
  
 document 
  
 is 
  
 draft 
  
 author 
  
 request 
 . 
 auth 
 . 
 uid 
  
 == 
  
 request 
 . 
 resource 
 . 
 data 
 . 
 authorUID 
  
&&  
 // 
  
 Must 
  
 include 
  
 title 
 , 
  
 author 
 , 
  
 and 
  
 url 
  
 fields 
  
 request 
 . 
 resource 
 . 
 data 
 . 
 keys 
 (). 
 hasAll 
 ( 
 [ 
  
 "authorUID" 
 , 
  
 "createdAt" 
 , 
  
 "title" 
  
 ] 
 ) 
  
&&  
 // 
  
 Title 
  
 must 
  
 be 
 < 
 50 
  
 characters 
  
 long 
  
 request 
 . 
 resource 
 . 
 data 
 . 
 title 
 . 
 size 
 () 
 < 
 50 
 ; 
  
 allow 
  
 update 
 : 
  
 if 
  
 // 
  
 User 
  
 is 
  
 the 
  
 author 
 , 
  
 and 
  
 resource 
 . 
 data 
 . 
 authorUID 
  
 == 
  
 request 
 . 
 auth 
 . 
 uid 
  
&&  
 // 
  
 `authorUID` 
  
 and 
  
 `createdAt` 
  
 are 
  
 unchanged 
  
 request 
 . 
 resource 
 . 
 data 
 . 
 diff 
 ( 
 resource 
 . 
 data 
 ). 
 unchangedKeys 
 (). 
 hasAll 
 ( 
 [ 
  
 "authorUID" 
 , 
  
 "createdAt" 
  
 ] 
 ) 
  
&&  
 // 
  
 Title 
  
 must 
  
 be 
 < 
 50 
  
 characters 
  
 long 
  
 request 
 . 
 resource 
 . 
 data 
 . 
 title 
 . 
 size 
 () 
 < 
 50 
 ; 
  
 allow 
  
 read 
 , 
  
 delete 
 : 
  
 if 
  
 // 
  
 User 
  
 is 
  
 draft 
  
 author 
  
 resource 
 . 
 data 
 . 
 authorUID 
  
 == 
  
 request 
 . 
 auth 
 . 
 uid 
  
 || 
  
 // 
  
 User 
  
 is 
  
 a 
  
 moderator 
  
 request 
 . 
 auth 
 . 
 token 
 . 
 isModerator 
  
 == 
  
 true 
 ; 
  
 } 
  
 match 
  
 / 
 published 
 / 
 { 
 postID 
 } 
  
 { 
  
 // 
  
 `authorUID` 
 : 
  
 string 
 , 
  
 required 
  
 // 
  
 `content` 
 : 
  
 string 
 , 
  
 required 
  
 // 
  
 `publishedAt` 
 : 
  
 timestamp 
 , 
  
 required 
  
 // 
  
 `title` 
 : 
  
 string 
 , 
 < 
 50 
  
 characters 
 , 
  
 required 
  
 // 
  
 `url` 
 : 
  
 string 
 , 
  
 required 
  
 // 
  
 `visible` 
 : 
  
 boolean 
 , 
  
 required 
  
 // 
  
 Can 
  
 be 
  
 read 
  
 by 
  
 everyone 
  
 allow 
  
 read 
 : 
  
 if 
  
 true 
 ; 
  
 // 
  
 Published 
  
 posts 
  
 are 
  
 created 
  
 only 
  
 via 
  
 functions 
 , 
  
 never 
  
 by 
  
 users 
  
 // 
  
 No 
  
 hard 
  
 deletes 
 ; 
  
 soft 
  
 deletes 
  
 update 
  
 `visible` 
  
 field 
 . 
  
 allow 
  
 create 
 , 
  
 delete 
 : 
  
 if 
  
 false 
 ; 
  
 } 
  
 } 
 } 
 

Rerun the tests, and confirm that another test passes.

8. Updating published posts: Custom functions and local variables

The conditions to update a a published post are:

  • it can only be done by the author or moderator, and
  • it must contain all the required fields.

Since you have already written conditions for being an author or a moderator, you could copy and paste the conditions, but over time that could become difficult to read and maintain. Instead, you'll create a custom function that encapsulates the logic for being either an author or moderator. Then, you'll call it from multiple conditions.

Create a custom function

Above the match statement for drafts, create a new function called isAuthorOrModerator that takes as arguments a post document (this will work for either drafts or published posts) and the user's auth object:

 rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {

    // Returns true if user is post author or a moderator
    function isAuthorOrModerator(post, auth) {

    }

    match /drafts/{postID} {
      allow create: ...
      allow update: ...
      ...
    }

    match /published/{postID} {
      allow read: ...
      allow create, delete: ...
    }
  }
} 

Use local variables

Inside the function, use the let keyword to set isAuthor and isModerator variables. All functions must end with a return statement, and ours will return a boolean indicating if either variable is true:

 function isAuthorOrModerator(post, auth) {
  let isAuthor = auth.uid == post.authorUID;
  let isModerator = auth.token.isModerator == true;
  return isAuthor || isModerator;
} 

Call the function

Now you'll update the rule for drafts to call that function, being careful to pass in resource.data as the first argument:

   
 // Draft blog posts 
  
 match 
  
 / 
 drafts 
 / 
 { 
 draftID 
 } 
  
 { 
  
 ... 
  
 // Can be deleted by author or moderator 
  
 allow 
  
 read 
 , 
  
 delete 
 : 
  
 if 
  
 isAuthorOrModerator 
 ( 
 resource 
 . 
 data 
 , 
  
 request 
 . 
 auth 
 ); 
  
 } 
 

Now you can write a condition for updating published posts that also uses the new function:

  allow 
  
 update 
 : 
  
 if 
  
 isAuthorOrModerator 
 ( 
 resource 
 . 
 data 
 , 
  
 request 
 . 
 auth 
 ); 
 

Add validations

Some of the fields of a published post shouldn't be changed, specifically url , authorUID , and publishedAt fields are immutable. The other two fields, title and content , and visible must still be present after an update. Add conditions to enforce these requirements for updates to published posts:

  // Immutable fields are unchanged 
 request 
 . 
 resource 
 . 
 data 
 . 
 diff 
 ( 
 resource 
 . 
 data 
 ). 
 unchangedKeys 
 (). 
 hasAll 
 ([ 
  
 "authorUID" 
 , 
  
 "publishedAt" 
 , 
  
 "url" 
 ]) 
  
&& // Required fields are present 
 request 
 . 
 resource 
 . 
 data 
 . 
 keys 
 (). 
 hasAll 
 ([ 
  
 "content" 
 , 
  
 "title" 
 , 
  
 "visible" 
 ]) 
 

Create a custom function on your own

And finally, add a condition that the title be under 50 characters. Because this is reused logic, you could do this by creating a new function, titleIsUnder50Chars . With the new function, the condition for updating a published post becomes:

  allow 
  
 update 
 : 
  
 if 
  
 isAuthorOrModerator 
 ( 
 resource 
 . 
 data 
 , 
  
 request 
 . 
 auth 
 ) 
  
&&  
 // 
  
 Immutable 
  
 fields 
  
 are 
  
 unchanged 
  
 request 
 . 
 resource 
 . 
 data 
 . 
 diff 
 ( 
 resource 
 . 
 data 
 ) 
 . 
 unchangedKeys 
 () 
 . 
 hasAll 
 ( 
 [ 
  
 "authorUID" 
 , 
  
 "publishedAt" 
 , 
  
 "url" 
  
 ] 
 ) 
  
&&  
 // 
  
 Required 
  
 fields 
  
 are 
  
 present 
  
 request 
 . 
 resource 
 . 
 data 
 . 
 keys 
 () 
 . 
 hasAll 
 ( 
 [ 
  
 "content" 
 , 
  
 "title" 
 , 
  
 "visible" 
  
 ] 
 ) 
  
&&  
 titleIsUnder50Chars 
 ( 
 request 
 . 
 resource 
 . 
 data 
 ); 
 

And the complete rule file is:

  rules_version 
  
 = 
  
 '2' 
 ; 
 service 
  
 cloud 
 . 
 firestore 
  
 { 
  
 match 
  
 / 
 databases 
 / 
 { 
 database 
 } 
 / 
 documents 
  
 { 
  
 // 
  
 Returns 
  
 true 
  
 if 
  
 user 
  
 is 
  
 post 
  
 author 
  
 or 
  
 a 
  
 moderator 
  
 function 
  
 isAuthorOrModerator 
 ( 
 post 
 , 
  
 auth 
 ) 
  
 { 
  
 let 
  
 isAuthor 
  
 = 
  
 auth 
 . 
 uid 
  
 == 
  
 post 
 . 
 authorUID 
 ; 
  
 let 
  
 isModerator 
  
 = 
  
 auth 
 . 
 token 
 . 
 isModerator 
  
 == 
  
 true 
 ; 
  
 return 
  
 isAuthor 
  
 || 
  
 isModerator 
 ; 
  
 } 
  
 function 
  
 titleIsUnder50Chars 
 ( 
 post 
 ) 
  
 { 
  
 return 
  
 post 
 . 
 title 
 . 
 size 
 () 
 < 
 50 
 ; 
  
 } 
  
 // 
  
 Draft 
  
 blog 
  
 posts 
  
 match 
  
 / 
 drafts 
 / 
 { 
 draftID 
 } 
  
 { 
  
 // 
  
 `authorUID` 
 : 
  
 string 
 , 
  
 required 
  
 // 
  
 `content` 
 : 
  
 string 
 , 
  
 optional 
  
 // 
  
 `createdAt` 
 : 
  
 timestamp 
 , 
  
 required 
  
 // 
  
 `title` 
 : 
  
 string 
 , 
 < 
 50 
  
 characters 
 , 
  
 required 
  
 // 
  
 `url` 
 : 
  
 string 
 , 
  
 optional 
  
 allow 
  
 create 
 : 
  
 if 
  
 // 
  
 User 
  
 creating 
  
 document 
  
 is 
  
 draft 
  
 author 
  
 request 
 . 
 auth 
 . 
 uid 
  
 == 
  
 request 
 . 
 resource 
 . 
 data 
 . 
 authorUID 
  
&&  
 // 
  
 Must 
  
 include 
  
 title 
 , 
  
 author 
 , 
  
 and 
  
 url 
  
 fields 
  
 request 
 . 
 resource 
 . 
 data 
 . 
 keys 
 (). 
 hasAll 
 ( 
 [ 
  
 "authorUID" 
 , 
  
 "createdAt" 
 , 
  
 "title" 
  
 ] 
 ) 
  
&&  
 titleIsUnder50Chars 
 ( 
 request 
 . 
 resource 
 . 
 data 
 ); 
  
 allow 
  
 update 
 : 
  
 if 
  
 // 
  
 User 
  
 is 
  
 the 
  
 author 
 , 
  
 and 
  
 resource 
 . 
 data 
 . 
 authorUID 
  
 == 
  
 request 
 . 
 auth 
 . 
 uid 
  
&&  
 // 
  
 `authorUID` 
  
 and 
  
 `createdAt` 
  
 are 
  
 unchanged 
  
 request 
 . 
 resource 
 . 
 data 
 . 
 diff 
 ( 
 resource 
 . 
 data 
 ). 
 unchangedKeys 
 (). 
 hasAll 
 ( 
 [ 
  
 "authorUID" 
 , 
  
 "createdAt" 
  
 ] 
 ) 
  
&&  
 titleIsUnder50Chars 
 ( 
 request 
 . 
 resource 
 . 
 data 
 ); 
  
 // 
  
 Can 
  
 be 
  
 read 
  
 or 
  
 deleted 
  
 by 
  
 author 
  
 or 
  
 moderator 
  
 allow 
  
 read 
 , 
  
 delete 
 : 
  
 if 
  
 isAuthorOrModerator 
 ( 
 resource 
 . 
 data 
 , 
  
 request 
 . 
 auth 
 ); 
  
 } 
  
 // 
  
 Published 
  
 blog 
  
 posts 
  
 are 
  
 denormalized 
  
 from 
  
 drafts 
  
 match 
  
 / 
 published 
 / 
 { 
 postID 
 } 
  
 { 
  
 // 
  
 `authorUID` 
 : 
  
 string 
 , 
  
 required 
  
 // 
  
 `content` 
 : 
  
 string 
 , 
  
 required 
  
 // 
  
 `publishedAt` 
 : 
  
 timestamp 
 , 
  
 required 
  
 // 
  
 `title` 
 : 
  
 string 
 , 
 < 
 50 
  
 characters 
 , 
  
 required 
  
 // 
  
 `url` 
 : 
  
 string 
 , 
  
 required 
  
 // 
  
 `visible` 
 : 
  
 boolean 
 , 
  
 required 
  
 // 
  
 Can 
  
 be 
  
 read 
  
 by 
  
 everyone 
  
 allow 
  
 read 
 : 
  
 if 
  
 true 
 ; 
  
 // 
  
 Published 
  
 posts 
  
 are 
  
 created 
  
 only 
  
 via 
  
 functions 
 , 
  
 never 
  
 by 
  
 users 
  
 // 
  
 No 
  
 hard 
  
 deletes 
 ; 
  
 soft 
  
 deletes 
  
 update 
  
 `visible` 
  
 field 
 . 
  
 allow 
  
 create 
 , 
  
 delete 
 : 
  
 if 
  
 false 
 ; 
  
 allow 
  
 update 
 : 
  
 if 
  
 isAuthorOrModerator 
 ( 
 resource 
 . 
 data 
 , 
  
 request 
 . 
 auth 
 ) 
  
&&  
 // 
  
 Immutable 
  
 fields 
  
 are 
  
 unchanged 
  
 request 
 . 
 resource 
 . 
 data 
 . 
 diff 
 ( 
 resource 
 . 
 data 
 ). 
 unchangedKeys 
 (). 
 hasAll 
 ( 
 [ 
  
 "authorUID" 
 , 
  
 "publishedAt" 
 , 
  
 "url" 
  
 ] 
 ) 
  
&&  
 // 
  
 Required 
  
 fields 
  
 are 
  
 present 
  
 request 
 . 
 resource 
 . 
 data 
 . 
 keys 
 (). 
 hasAll 
 ( 
 [ 
  
 "content" 
 , 
  
 "title" 
 , 
  
 "visible" 
  
 ] 
 ) 
  
&&  
 titleIsUnder50Chars 
 ( 
 request 
 . 
 resource 
 . 
 data 
 ); 
  
 } 
  
 } 
 } 
 

Rerun the tests. At this point, you should have 5 passing tests and 4 failing ones.

9. Comments: Subcollections and sign-in provider permissions

The published posts allow comments, and the comments are stored in a subcollection of the published post ( /published/{postID}/comments/{commentID} ). By default the rules of a collection don't apply to subcollections. You don't want the same rules that apply to the parent document of the published post to apply to the comments; you'll craft different ones.

To write rules for accessing the comments, start with the match statement:

  match 
  
 / 
 published 
 / 
 { 
 postID 
 } 
 / 
 comments 
 / 
 { 
 commentID 
 } 
  
 { 
  
 // 
  
 `authorUID` 
 : 
  
 string 
 , 
  
 required 
  
 // 
  
 `comment` 
 : 
  
 string 
 , 
 < 
 500 
  
 characters 
 , 
  
 required 
  
 // 
  
 `createdAt` 
 : 
  
 timestamp 
 , 
  
 required 
  
 // 
  
 `editedAt` 
 : 
  
 timestamp 
 , 
  
 optional 
 

Reading comments: Can't be anonymous

For this app, only users that have created a permanent account, not an anonymous account can read the comments. To enforce that rule, look up the sign_in_provider attribute that's on each auth.token object:

  allow 
  
 read 
 : 
  
 if 
  
 request 
 . 
 auth 
 . 
 token 
 . 
 firebase 
 . 
 sign_in_provider 
  
 != 
  
 "anonymous" 
 ; 
 

Rerun your tests, and confirm that one more test passes.

Creating comments: Checking a deny list

There are three conditions for creating a comment:

  • a user must have a verified email
  • the comment must be fewer than 500 characters, and
  • they can't be on a list of banned users, which is stored in firestore in the bannedUsers collection. Taking these conditions one at a time:
 request.auth.token.email_verified == true 
 request.resource.data.comment.size() < 500 
  !exists(/databases/$(database)/documents/bannedUsers/$(request.auth.uid)); 
 

The final rule for creating comments is:

  allow 
  
 create 
 : 
  
 if 
  
 // 
  
 User 
  
 has 
  
 verified 
  
 email 
  
 ( 
 request 
 . 
 auth 
 . 
 token 
 . 
 email_verified 
  
 == 
  
 true 
 ) 
  
&&  
 // 
  
 UID 
  
 is 
  
 not 
  
 on 
  
 bannedUsers 
  
 list 
  
 !( 
 exists 
 (/ 
 databases 
 /$( 
 database 
 )/ 
 documents 
 / 
 bannedUsers 
 /$( 
 request 
 . 
 auth 
 . 
 uid 
 )); 
 

The entire rules file is now:

  For 
  
 bottom 
  
 of 
  
 step 
  
 9 
 rules_version 
  
 = 
  
 '2' 
 ; 
 service 
  
 cloud 
 . 
 firestore 
  
 { 
  
 match 
  
 / 
 databases 
 / 
 { 
 database 
 } 
 / 
 documents 
  
 { 
  
 // 
  
 Returns 
  
 true 
  
 if 
  
 user 
  
 is 
  
 post 
  
 author 
  
 or 
  
 a 
  
 moderator 
  
 function 
  
 isAuthorOrModerator 
 ( 
 post 
 , 
  
 auth 
 ) 
  
 { 
  
 let 
  
 isAuthor 
  
 = 
  
 auth 
 . 
 uid 
  
 == 
  
 post 
 . 
 authorUID 
 ; 
  
 let 
  
 isModerator 
  
 = 
  
 auth 
 . 
 token 
 . 
 isModerator 
  
 == 
  
 true 
 ; 
  
 return 
  
 isAuthor 
  
 || 
  
 isModerator 
 ; 
  
 } 
  
 function 
  
 titleIsUnder50Chars 
 ( 
 post 
 ) 
  
 { 
  
 return 
  
 post 
 . 
 title 
 . 
 size 
 () 
 < 
 50 
 ; 
  
 } 
  
 // 
  
 Draft 
  
 blog 
  
 posts 
  
 match 
  
 / 
 drafts 
 / 
 { 
 draftID 
 } 
  
 { 
  
 // 
  
 `authorUID` 
 : 
  
 string 
 , 
  
 required 
  
 // 
  
 `content` 
 : 
  
 string 
 , 
  
 optional 
  
 // 
  
 `createdAt` 
 : 
  
 timestamp 
 , 
  
 required 
  
 // 
  
 `title` 
 : 
  
 string 
 , 
 < 
 50 
  
 characters 
 , 
  
 required 
  
 // 
  
 `url` 
 : 
  
 string 
 , 
  
 optional 
  
 allow 
  
 create 
 : 
  
 if 
  
 // 
  
 User 
  
 is 
  
 author 
  
 request 
 . 
 auth 
 . 
 uid 
  
 == 
  
 request 
 . 
 resource 
 . 
 data 
 . 
 authorUID 
  
&&  
 // 
  
 Must 
  
 include 
  
 title 
 , 
  
 author 
 , 
  
 and 
  
 createdAt 
  
 fields 
  
 request 
 . 
 resource 
 . 
 data 
 . 
 keys 
 (). 
 hasAll 
 ( 
 [ 
  
 "authorUID" 
 , 
  
 "createdAt" 
 , 
  
 "title" 
  
 ] 
 ) 
  
&&  
 titleIsUnder50Chars 
 ( 
 request 
 . 
 resource 
 . 
 data 
 ); 
  
 allow 
  
 update 
 : 
  
 if 
  
 // 
  
 User 
  
 is 
  
 author 
  
 resource 
 . 
 data 
 . 
 authorUID 
  
 == 
  
 request 
 . 
 auth 
 . 
 uid 
  
&&  
 // 
  
 `authorUID` 
  
 and 
  
 `createdAt` 
  
 are 
  
 unchanged 
  
 request 
 . 
 resource 
 . 
 data 
 . 
 diff 
 ( 
 resource 
 . 
 data 
 ). 
 unchangedKeys 
 (). 
 hasAll 
 ( 
 [ 
  
 "authorUID" 
 , 
  
 "createdAt" 
  
 ] 
 ) 
  
&&  
 titleIsUnder50Chars 
 ( 
 request 
 . 
 resource 
 . 
 data 
 ); 
  
 // 
  
 Can 
  
 be 
  
 read 
  
 or 
  
 deleted 
  
 by 
  
 author 
  
 or 
  
 moderator 
  
 allow 
  
 read 
 , 
  
 delete 
 : 
  
 if 
  
 isAuthorOrModerator 
 ( 
 resource 
 . 
 data 
 , 
  
 request 
 . 
 auth 
 ); 
  
 } 
  
 // 
  
 Published 
  
 blog 
  
 posts 
  
 are 
  
 denormalized 
  
 from 
  
 drafts 
  
 match 
  
 / 
 published 
 / 
 { 
 postID 
 } 
  
 { 
  
 // 
  
 `authorUID` 
 : 
  
 string 
 , 
  
 required 
  
 // 
  
 `content` 
 : 
  
 string 
 , 
  
 required 
  
 // 
  
 `publishedAt` 
 : 
  
 timestamp 
 , 
  
 required 
  
 // 
  
 `title` 
 : 
  
 string 
 , 
 < 
 50 
  
 characters 
 , 
  
 required 
  
 // 
  
 `url` 
 : 
  
 string 
 , 
  
 required 
  
 // 
  
 `visible` 
 : 
  
 boolean 
 , 
  
 required 
  
 // 
  
 Can 
  
 be 
  
 read 
  
 by 
  
 everyone 
  
 allow 
  
 read 
 : 
  
 if 
  
 true 
 ; 
  
 // 
  
 Published 
  
 posts 
  
 are 
  
 created 
  
 only 
  
 via 
  
 functions 
 , 
  
 never 
  
 by 
  
 users 
  
 // 
  
 No 
  
 hard 
  
 deletes 
 ; 
  
 soft 
  
 deletes 
  
 update 
  
 `visible` 
  
 field 
 . 
  
 allow 
  
 create 
 , 
  
 delete 
 : 
  
 if 
  
 false 
 ; 
  
 allow 
  
 update 
 : 
  
 if 
  
 isAuthorOrModerator 
 ( 
 resource 
 . 
 data 
 , 
  
 request 
 . 
 auth 
 ) 
  
&&  
 // 
  
 Immutable 
  
 fields 
  
 are 
  
 unchanged 
  
 request 
 . 
 resource 
 . 
 data 
 . 
 diff 
 ( 
 resource 
 . 
 data 
 ). 
 unchangedKeys 
 (). 
 hasAll 
 ( 
 [ 
  
 "authorUID" 
 , 
  
 "publishedAt" 
 , 
  
 "url" 
  
 ] 
 ) 
  
&&  
 // 
  
 Required 
  
 fields 
  
 are 
  
 present 
  
 request 
 . 
 resource 
 . 
 data 
 . 
 keys 
 (). 
 hasAll 
 ( 
 [ 
  
 "content" 
 , 
  
 "title" 
 , 
  
 "visible" 
  
 ] 
 ) 
  
&&  
 titleIsUnder50Chars 
 ( 
 request 
 . 
 resource 
 . 
 data 
 ); 
  
 } 
  
 match 
  
 / 
 published 
 / 
 { 
 postID 
 } 
 / 
 comments 
 / 
 { 
 commentID 
 } 
  
 { 
  
 // 
  
 `authorUID` 
 : 
  
 string 
 , 
  
 required 
  
 // 
  
 `createdAt` 
 : 
  
 timestamp 
 , 
  
 required 
  
 // 
  
 `editedAt` 
 : 
  
 timestamp 
 , 
  
 optional 
  
 // 
  
 `comment` 
 : 
  
 string 
 , 
 < 
 500 
  
 characters 
 , 
  
 required 
  
 // 
  
 Must 
  
 have 
  
 permanent 
  
 account 
  
 to 
  
 read 
  
 comments 
  
 allow 
  
 read 
 : 
  
 if 
  
 ! 
 ( 
 request 
 . 
 auth 
 . 
 token 
 . 
 firebase 
 . 
 sign_in_provider 
  
 == 
  
 "anonymous" 
 ); 
  
 allow 
  
 create 
 : 
  
 if 
  
 // 
  
 User 
  
 has 
  
 verified 
  
 email 
  
 request 
 . 
 auth 
 . 
 token 
 . 
 email_verified 
  
 == 
  
 true 
  
&&  
 // 
  
 Comment 
  
 is 
  
 under 
  
 500 
  
 characters 
  
 request 
 . 
 resource 
 . 
 data 
 . 
 comment 
 . 
 size 
 () 
 < 
 500 
  
&&  
 // 
  
 UID 
  
 is 
  
 not 
  
 on 
  
 the 
  
 block 
  
 list 
  
 ! 
 exists 
 ( 
 / 
 databases 
 / 
 $ 
 ( 
 database 
 ) 
 / 
 documents 
 / 
 bannedUsers 
 / 
 $ 
 ( 
 request 
 . 
 auth 
 . 
 uid 
 )); 
  
 } 
  
 } 
 } 
 

Rerun the tests, and make sure one more test passes.

10. Updating comments: Time-based rules

The business logic for comments is that they can be edited by the comment author for a hour after creation. To implement this, use the createdAt timestamp.

First, to establish that the user is the author:

 request.auth.uid == resource.data.authorUID 

Next, that the comment was created within the last hour:

 (request.time - resource.data.createdAt) < duration.value(1, 'h'); 

Combining these with the logical AND operator, the rule for updating comments becomes:

  allow 
  
 update 
 : 
  
 if 
  
 // 
  
 is 
  
 author 
  
 request 
 . 
 auth 
 . 
 uid 
  
 == 
  
 resource 
 . 
 data 
 . 
 authorUID 
  
&&  
 // 
  
 within 
  
 an 
  
 hour 
  
 of 
  
 comment 
  
 creation 
  
 ( 
 request 
 . 
 time 
  
 - 
  
 resource 
 . 
 data 
 . 
 createdAt 
 ) 
 < 
 duration 
 . 
 value 
 ( 
 1 
 , 
  
 'h' 
 ); 
 

Rerun the tests, and make sure one more test passes.

11. Deleting comments: checking for parent ownership

Comments can be deleted by the comment author, a moderator, or the author of the blog post.

First, because the helper function you added earlier checks for an authorUID field that could exist on either a post or a comment, you can reuse the helper function to check if the user is the author or moderator:

 isAuthorOrModerator(resource.data, request.auth) 

To check if the user is the blog post author, use a get to look up the post in Firestore:

 request.auth.uid == get(/databases/$(database)/documents/published/$(postID)).data.authorUID 

Because any of these conditions are sufficient, use a logical OR operator between them:

  allow 
  
 delete 
 : 
  
 if 
  
 // 
  
 is 
  
 comment 
  
 author 
  
 or 
  
 moderator 
  
 isAuthorOrModerator 
 ( 
 resource 
 . 
 data 
 , 
  
 request 
 . 
 auth 
 ) 
  
 || 
  
 // 
  
 is 
  
 blog 
  
 post 
  
 author 
  
 request 
 . 
 auth 
 . 
 uid 
  
 == 
  
 get 
 (/ 
 databases 
 /$( 
 database 
 )/ 
 documents 
 / 
 published 
 /$( 
 postID 
 )) 
 . 
 data 
 . 
 authorUID 
 ; 
 

Rerun the tests, and make sure one more test passes.

And the entire rules file is:

  rules_version 
  
 = 
  
 '2' 
 ; 
 service 
  
 cloud 
 . 
 firestore 
  
 { 
  
 match 
  
 / 
 databases 
 / 
 { 
 database 
 } 
 / 
 documents 
  
 { 
  
 // 
  
 Returns 
  
 true 
  
 if 
  
 user 
  
 is 
  
 post 
  
 author 
  
 or 
  
 a 
  
 moderator 
  
 function 
  
 isAuthorOrModerator 
 ( 
 post 
 , 
  
 auth 
 ) 
  
 { 
  
 let 
  
 isAuthor 
  
 = 
  
 auth 
 . 
 uid 
  
 == 
  
 post 
 . 
 authorUID 
 ; 
  
 let 
  
 isModerator 
  
 = 
  
 auth 
 . 
 token 
 . 
 isModerator 
  
 == 
  
 true 
 ; 
  
 return 
  
 isAuthor 
  
 || 
  
 isModerator 
 ; 
  
 } 
  
 function 
  
 titleIsUnder50Chars 
 ( 
 post 
 ) 
  
 { 
  
 return 
  
 post 
 . 
 title 
 . 
 size 
 () 
 < 
 50 
 ; 
  
 } 
  
 // 
  
 Draft 
  
 blog 
  
 posts 
  
 match 
  
 / 
 drafts 
 / 
 { 
 draftID 
 } 
  
 { 
  
 // 
  
 `authorUID` 
 : 
  
 string 
 , 
  
 required 
  
 // 
  
 `content` 
 : 
  
 string 
 , 
  
 optional 
  
 // 
  
 `createdAt` 
 : 
  
 timestamp 
 , 
  
 required 
  
 // 
  
 `title` 
 : 
  
 string 
 , 
 < 
 50 
  
 characters 
 , 
  
 required 
  
 // 
  
 `url` 
 : 
  
 string 
 , 
  
 optional 
  
 allow 
  
 create 
 : 
  
 if 
  
 // 
  
 User 
  
 is 
  
 author 
  
 request 
 . 
 auth 
 . 
 uid 
  
 == 
  
 request 
 . 
 resource 
 . 
 data 
 . 
 authorUID 
  
&&  
 // 
  
 Must 
  
 include 
  
 title 
 , 
  
 author 
 , 
  
 and 
  
 createdAt 
  
 fields 
  
 request 
 . 
 resource 
 . 
 data 
 . 
 keys 
 (). 
 hasAll 
 ( 
 [ 
  
 "authorUID" 
 , 
  
 "createdAt" 
 , 
  
 "title" 
  
 ] 
 ) 
  
&&  
 titleIsUnder50Chars 
 ( 
 request 
 . 
 resource 
 . 
 data 
 ); 
  
 allow 
  
 update 
 : 
  
 if 
  
 // 
  
 User 
  
 is 
  
 author 
  
 resource 
 . 
 data 
 . 
 authorUID 
  
 == 
  
 request 
 . 
 auth 
 . 
 uid 
  
&&  
 // 
  
 `authorUID` 
  
 and 
  
 `createdAt` 
  
 are 
  
 unchanged 
  
 request 
 . 
 resource 
 . 
 data 
 . 
 diff 
 ( 
 resource 
 . 
 data 
 ). 
 unchangedKeys 
 (). 
 hasAll 
 ( 
 [ 
  
 "authorUID" 
 , 
  
 "createdAt" 
  
 ] 
 ) 
  
&&  
 titleIsUnder50Chars 
 ( 
 request 
 . 
 resource 
 . 
 data 
 ); 
  
 // 
  
 Can 
  
 be 
  
 read 
  
 or 
  
 deleted 
  
 by 
  
 author 
  
 or 
  
 moderator 
  
 allow 
  
 read 
 , 
  
 delete 
 : 
  
 if 
  
 isAuthorOrModerator 
 ( 
 resource 
 . 
 data 
 , 
  
 request 
 . 
 auth 
 ); 
  
 } 
  
 // 
  
 Published 
  
 blog 
  
 posts 
  
 are 
  
 denormalized 
  
 from 
  
 drafts 
  
 match 
  
 / 
 published 
 / 
 { 
 postID 
 } 
  
 { 
  
 // 
  
 `authorUID` 
 : 
  
 string 
 , 
  
 required 
  
 // 
  
 `content` 
 : 
  
 string 
 , 
  
 required 
  
 // 
  
 `publishedAt` 
 : 
  
 timestamp 
 , 
  
 required 
  
 // 
  
 `title` 
 : 
  
 string 
 , 
 < 
 50 
  
 characters 
 , 
  
 required 
  
 // 
  
 `url` 
 : 
  
 string 
 , 
  
 required 
  
 // 
  
 `visible` 
 : 
  
 boolean 
 , 
  
 required 
  
 // 
  
 Can 
  
 be 
  
 read 
  
 by 
  
 everyone 
  
 allow 
  
 read 
 : 
  
 if 
  
 true 
 ; 
  
 // 
  
 Published 
  
 posts 
  
 are 
  
 created 
  
 only 
  
 via 
  
 functions 
 , 
  
 never 
  
 by 
  
 users 
  
 // 
  
 No 
  
 hard 
  
 deletes 
 ; 
  
 soft 
  
 deletes 
  
 update 
  
 `visible` 
  
 field 
 . 
  
 allow 
  
 create 
 , 
  
 delete 
 : 
  
 if 
  
 false 
 ; 
  
 allow 
  
 update 
 : 
  
 if 
  
 isAuthorOrModerator 
 ( 
 resource 
 . 
 data 
 , 
  
 request 
 . 
 auth 
 ) 
  
&&  
 // 
  
 Immutable 
  
 fields 
  
 are 
  
 unchanged 
  
 request 
 . 
 resource 
 . 
 data 
 . 
 diff 
 ( 
 resource 
 . 
 data 
 ). 
 unchangedKeys 
 (). 
 hasAll 
 ( 
 [ 
  
 "authorUID" 
 , 
  
 "publishedAt" 
 , 
  
 "url" 
  
 ] 
 ) 
  
&&  
 // 
  
 Required 
  
 fields 
  
 are 
  
 present 
  
 request 
 . 
 resource 
 . 
 data 
 . 
 keys 
 (). 
 hasAll 
 ( 
 [ 
  
 "content" 
 , 
  
 "title" 
 , 
  
 "visible" 
  
 ] 
 ) 
  
&&  
 titleIsUnder50Chars 
 ( 
 request 
 . 
 resource 
 . 
 data 
 ); 
  
 } 
  
 match 
  
 / 
 published 
 / 
 { 
 postID 
 } 
 / 
 comments 
 / 
 { 
 commentID 
 } 
  
 { 
  
 // 
  
 `authorUID` 
 : 
  
 string 
 , 
  
 required 
  
 // 
  
 `createdAt` 
 : 
  
 timestamp 
 , 
  
 required 
  
 // 
  
 `editedAt` 
 : 
  
 timestamp 
 , 
  
 optional 
  
 // 
  
 `comment` 
 : 
  
 string 
 , 
 < 
 500 
  
 characters 
 , 
  
 required 
  
 // 
  
 Must 
  
 have 
  
 permanent 
  
 account 
  
 to 
  
 read 
  
 comments 
  
 allow 
  
 read 
 : 
  
 if 
  
 ! 
 ( 
 request 
 . 
 auth 
 . 
 token 
 . 
 firebase 
 . 
 sign_in_provider 
  
 == 
  
 "anonymous" 
 ); 
  
 allow 
  
 create 
 : 
  
 if 
  
 // 
  
 User 
  
 has 
  
 verified 
  
 email 
  
 request 
 . 
 auth 
 . 
 token 
 . 
 email_verified 
  
 == 
  
 true 
  
&&  
 // 
  
 Comment 
  
 is 
  
 under 
  
 500 
  
 characters 
  
 request 
 . 
 resource 
 . 
 data 
 . 
 comment 
 . 
 size 
 () 
 < 
 500 
  
&&  
 // 
  
 UID 
  
 is 
  
 not 
  
 on 
  
 the 
  
 block 
  
 list 
  
 ! 
 exists 
 ( 
 / 
 databases 
 / 
 $ 
 ( 
 database 
 ) 
 / 
 documents 
 / 
 bannedUsers 
 / 
 $ 
 ( 
 request 
 . 
 auth 
 . 
 uid 
 )); 
  
 allow 
  
 update 
 : 
  
 if 
  
 // 
  
 is 
  
 author 
  
 request 
 . 
 auth 
 . 
 uid 
  
 == 
  
 resource 
 . 
 data 
 . 
 authorUID 
  
&&  
 // 
  
 within 
  
 an 
  
 hour 
  
 of 
  
 comment 
  
 creation 
  
 ( 
 request 
 . 
 time 
  
 - 
  
 resource 
 . 
 data 
 . 
 createdAt 
 ) 
 < 
 duration 
 . 
 value 
 ( 
 1 
 , 
  
 'h' 
 ); 
  
 allow 
  
 delete 
 : 
  
 if 
  
 // 
  
 is 
  
 comment 
  
 author 
  
 or 
  
 moderator 
  
 isAuthorOrModerator 
 ( 
 resource 
 . 
 data 
 , 
  
 request 
 . 
 auth 
 ) 
  
 || 
  
 // 
  
 is 
  
 blog 
  
 post 
  
 author 
  
 request 
 . 
 auth 
 . 
 uid 
  
 == 
  
 get 
 ( 
 / 
 databases 
 / 
 $ 
 ( 
 database 
 ) 
 / 
 documents 
 / 
 published 
 / 
 $ 
 ( 
 postID 
 )). 
 data 
 . 
 authorUID 
 ; 
  
 } 
  
 } 
 } 
 

12. Next steps

Congratulations! You've written the Security Rules that made all the tests pass and secured the application!

Here are some related topics to dive into next:

  • Blog post : How to code review Security Rules
  • Codelab : walking through local first development with the Emulators
  • Video : How to use set up CI for emulator-based tests using GitHub Actions
Design a Mobile Site
View Site in Mobile | Classic
Share by: