Security

Security Rules Basics for Cloud Firestore

Aravind Chowdary
Firebase Developers
7 min readMar 3, 2024

--

With Firebase, you can perform user authentication and database actions entirely from your frontend code. This means our database is exposed to the client any hacker would easily gain access to the private data of our apps, right?

Well no, that’s where Firebase Security Rules come in and fix this open hole (mmmm??) in your database. These security rules can be applied to the Firebase Realtime Database, Cloud Firestore, and Cloud Storage.

In a traditional (non-serverless) architecture, you implement your server which authenticates the user with tokens and validates that session on every request to your database.

While this approach is pretty similar to Security Rules, the advantage of using Firebase is that you don’t have to write, run, and maintain your backend infrastructure — Google and Firebase will do that for you.

Security Rules allow you to write policies to define who has access to what in your database using an easy-to-learn language called CEL (Common Expression Language). Whenever a user sends a request to your Firestore database that request is routed through this policy where every request is denied by default and it looks for the first rule to allow it.

You can write these rules in the IDE or just directly in the Firebase console. An added advantage of the Firebase console is that you’ll have access to the history of your previous rules.

Let’s look at some key concepts to better understand how Firebase Security Rules work.

Match

Before we write any rules we need to define which path in the database a rule applies to.

When you first create a new Firestore database instance in your project, you will notice that by default there is a match block pointing to the root of your database.

There are three main types of matching patterns:

  • single document match
  • collection match
  • or a hierarchy of collection and sub-collection match

Single Document Match

Imagine you have a lot of users and each of them gets a unique uuid from Firebase Authentication and you want to allow users to access only the data they own.

Here is how you can write a Firestore Security Rule to keep the data secure and accessible to the owner of it.

match /users/{uid} {
allow read, write: if uid == request.auth.uid
}

This rule defines that every uid (which is a variable in this code snippet) in the users collection is to be allowed to read and write if the uid that is the document ID matches the uid provided by Firebase Auth in the request made.

In short, you’re matching the document ID with the UID of the requesting user to check if they are allowed to access the collection or not.

Remember : The rules on parent collection don’t apply to sub collections of it.

If you had an items collection nested under the users collection, the user match rule doesn’t apply for that as it’s isolated with its own security policies.

You might want to apply the same rules for all the collections under a parent collection, and that’s where recursive wild card is used.

Collection Match

match /users/{docId=**} {
allow read, write;
}

In the above snippet if the user is allowed to read the user collection, then it applies to all of its subcollections too. This way you don’t have to write a separate set of rules for all the collections under a parent collection.

If you want to define security rules to sub-collections then you can do that too.

match /users/{uid} {
allow read, write: if uid == request.auth.uid

match /users/{uid}/photos/{pid} {
allow read, write;
}
}

In this case, you’re defining rules for the photos collection which is under the users (parent) collection.

Allow — Read, Write, & Beyond

Every match block will contain two or more allowed blocks. I would like to take allow as a function that takes in two arguments, the first one is the operation that we want to allow, like read, write, update, or delete. The second parameter is a predicate or a boolean condition based on the application logic to allow a particular operation on that collection match.

Let’s look at an example

  match /users/{docId=**} {

allow read, write;

allow get;
allow list;

allow create;
allow update;
allow delete;

}

Here with allow read, write you are allowing single and multiple document querying (reads) and creations (writes) to the users collection.

To get granular control over the actions allowed to be performed you can use allow get to allow querying of a single document and allow list for multi-document read operations.

Now similarly, allow create allows users to only create the document in the collection and doesn’t allow them to perform modifications to it, and if you add allow update which allows modification of a document this is granular to the write condition. allow delete gives you control over who can delete a document in the collection.

For example

match /users/{docId=**} {
allow list, delete: true;
}

This will allow users to query multiple documents in the collection and permit them to delete them.

Remember — Rules look for the FIRST allow, meaning if something is allowed before in the security rules cannot be un-allowed in the below statements.

match /users/{docId=**} {
allow read, write:true;
allow delete: false;
}

In the above snippet, you already allowed full read and write access to the documents in the collections, so allow delete is obsolete in these rules.

Now that we know the fundamentals of allow, let’s look at conditions.

Conditions

Conditions are the backbone of Firebase Security Rules — they are statements that result in a boolean result. The Firebase Securtity Rules environment contains a bunch of objects and helper methods that we can use to evaluate incoming requests, read other items in the database, check timestamps, and various other operations.

Let’s look at a couple of concrete examples.

The general syntax is similar to most modern programming languages. We can start by adding an if keyword and some kind of logic that evaluates to either true or false.

If the condition evaluates to false the operation will always be denied by the rules. This is the default behavior of the Firestore database (in production mode).

In many cases, you will need to check multiple conditions before allowing an operation. For example, you might have to check if a user is logged in, has a timestamp property, or if they are the admin when performing a database read or a write.

You can use logical AND (&&) for checking multiple conditions. This is called chained conditions. All the chained conditions must be true to allow operations.

Similarly, we have a logical OR (||) operator. In this case, only one of the condition must be true to allow the operation.

These are the conditions you’ll use the most, there are bunch of other operators that are supported.

Request and resource

Now there is an important concept that you need to understand at this point: request and resource data.

The request represents the incoming data from the client-side application. That means the user from the client side is trying to read or write to the database and we have to evaluate the information in that request to allow or restrict the access.

request.auth: contains the JSON web token information with the user’s authentication information from Firebase Authentication. That’s where you find the email address, name, uid, and custom claims that you set for a user.

request.resource: this is the actual data that the user is trying to write to the database. As an example, when a user tries to create a new document the resource will contain the actual payload that was sent from the client application.

The requestion object has a bunch of other properties that might come in useful:

request.time: Timestamp of the request

request.path: Path to the document

request.method: Methods like read, write, delete, update…

These are pretty self-explanatory so I’ll skip a detailed explanation of these properties on the resource.

In addition to the request we also have a global object called resource. This represents the data that already exists in your database. Which might come in handy when the user is updating or deleting a document.

Keep in mind:

resource != request.resource

Top-level resource is the object with your existing database, and request.resource is from the request user made

In this example, you are checking if the username in the existing database is the same as the username in the payload from the request sent from the client.

This rule checks if the user isn’t allowed to update something that cannot be updated.

Resources

--

--