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: