The Firestore Emulator Requests Monitor allows you to see requests to your local Firestore Emulator in real-time, and drill down to the details of each request, such as method, path, and Firebase Security Rules evaluation. You can access the Requests Monitor right now from the Emulator UI if you have the latest Firebase CLI running. (If not, it's never too late to upgrade!)
Requests Monitor helps you understand your request traffic in detail, and puts Firebase Security Rules front and center (just like security should be in your production app). Ever wonder what collections are pulled in for the amazing pizza tracker feature in your app? Forgot about how that cute button is backed by 400 lines of code changes Firestore? Worried about changes to security rules breaking production apps? The Requests Monitor has answers to all of these questions, plus more!
First, start the Emulator Suite, then navigate to the Firestore tab in the Emulator UI and you'll be greeted with two views: the default "Data" view is the familiar data viewer and editor you know and love, and the "Requests" view is just a click away.
Each client request to the Firestore Emulator will be added to the table as a new row. For example, if you connect your app to the Firestore Emulator and create a new document, it will show as a CREATE request on the table in real time. This works regardless of which platform your app is on -- be it Android, iOS, web, or anything else. And if you ever forget to open this page before you make those requests, don't worry -- we've got you covered. The last few requests will be kept for you to review when you navigate to the Requests page.
CREATE
The Requests View is a great help in developing security rules. A checkmark indicates if the request has been allowed by your security rules; denials and errors are also displayed. If you follow best practices and keep your rules locked down as much as possible, you'll certainly see some denials when you develop new features, and those are the perfect opportunity to learn! To take the guesswork out of updating security rules, just click on any request to see the evaluation details view.
On this page, you'll see your current local security rules on the left, and some information about the specific request on the right. Statements that allow, deny, or error will be highlighted. You may also run into situations where the request is not yet covered by any of the allow statements and is thus denied by default. Ever wonder why a request is denied where it should be allowed, or the reverse? The panel on the right can help. You can see the existing resource, the would-be version of the document (i.e. request.resource) and other important fields in the request. If your goal is to modify the security rules to align with changes to your app, you can also use those fields as inspiration for new conditions that your rules should gate on.
resource
request.resource
While we're at it, you'll notice the security rules on the evaluation details page are not modifiable -- that's because they are snapshots at the time when the request happened. To make rules changes, just directly modify the firebase.rules file with your favorite editor or IDE and the Firebase CLI will automatically apply the changes to the Firestore Emulator. And if you make another request, it will show up in the table view as a different row with new rules and results. Sometimes, it may be helpful to compare the old and new rules and see differences in how they were evaluated.
firebase.rules
For those of you who are familiar with the Security Rules Playground in Firebase Console, you may miss the simulated requests feature here. But in the emulator world, there's no need to guess what a request should look like -- you can just simply make that request from your app using the Firestore SDK in your favorite programming language. The Request Monitor always shows you faithfully how that request is represented in Security Rules and the actual decision of your rules. Any client request is fair game -- even lists and queries that are hard to simulate in production. We think you will eventually get used to it and love this new interactive development workflow as much as we do.
While you enjoy the new Requests Monitor, just keep in mind that only client requests are shown in the table. Admin requests bypass any security rules and therefore don't appear in the list. Similarly, if your rules depend on other documents in the Firestore (e.g. get(...) or exists(...)), only the main request is shown but not the dependent fetches, even though those dependent fetches count towards your quotas and billing in production. Just remember emulated benchmarks are not an indicator of production in terms of performance or cost estimation.
get(...)
exists(...)
We've already heard some developers asking if this feature will also be available in Firebase Console. While we cannot say for sure, recording production requests will certainly create a huge challenge to your app's Firestore performance and security. Aaaand, well, you know, production is not the best place to test out changes, especially security-related changes. We recommend developing and testing locally before rolling out to production, and the Firebase Emulator Suite is always seeking ways to help.
With the Firestore Emulator and Requests Monitor, you can see your prototyping path more clearly. In fact, you have a better view into unit and integration testing as well: just make sure to keep the Monitor open and run your tests against the same (emulated) Project ID that your app connects to. You only need to deploy when you feel comfortable with your changes.
Feel free to play around with the Firestore Emulator Requests Monitor, and let us know what you think!
Firebase Security Rules gate your user's access to and enforce validations for Firestore, Firebase Storage, and the Realtime Database. It's important to code review the Security Rules, just like you code review the application code. Because these rules are written in a domain-specific language, that puts some people in the position of code reviewing something they don't feel like they understand.
If you're finding yourself in that position, don't worry! This post will walk through how to approach reviewing and giving good feedback on Security Rules. The examples will be from Firestore Security Rules, and are mostly applicable to Storage as well. If you're reviewing Realtime Database Security Rules, although these principles apply, the rules use a different language, so the examples will be different.
When you're looking at Security Rules, check first for any top level rules that apply to everything. A document can match multiple rules, and if any rule grants access, access is granted. In the example below, there's a specific rule that only grants authors access to post documents, but the global rule lets anyone on the internet read or write to any place in your database:
post
rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { match /{document=**} { allow read, write: if true; } match /posts/{post} { allow read, write: if request.resource.data.authorUID == request.auth.uid ; } } }
It's important to look for the global match statement match /{document=**} throughout the entire rules file, not just at the top. If there are thorough rules but also a global match statement, universal access will still be granted through the global match statement.
match /{document=**}
There are a very small number of valid use cases for global match statements. For example, granting access to admin users:
rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { match /{document=**} { allow read, write: if isAdmin(); } } }
If you find a global rule that is read, write: if true, you can stop your review and ask them to fix it. If you find any other condition on a global rule, make sure it makes sense for your application.
read, write: if true
The next thing to do is to look at the data that you're storing. What is Personally Identifiable Information (PII) that should only be accessed by that particular user?
Security Rules apply to entire documents, not just fields. You should be able to look at each kind of document that you have and describe which kind of users should be able to read it, create it, update it, and delete it.
If you find documents where it's fine for most of the document to be read by anyone, but a few fields need to remain private, break those fields into their own document; the easiest way to do this is usually to create a subcollection off the existing document.
Now check the Security Rules for each of the documents that you identified as containing PII. The best practice for PII is that it is keyed by the user's ID, and only that user is allowed access:
match /secrets/{uid} { allow create, update: if request.auth.uid == uid; }
This pattern only works if there's a max of one document in the collection for each user. For cases of multiple documents per user, you could create subcollections of a user document:
match /users/{uid}/secrets/{secret} { allow create, update: if request.auth.uid == uid; }
Or you could include the id of the user who should have access as an attribute, and restrict access to that user:
match /secrets/{secret} { allow create, update: if request.auth.uid == request.resource.data.uid; }
Ideally, you would check that each document is as locked down as possible, but if you have limited time, the most important thing to check is that PII lives in separate documents and those PII documents are only accessible to that user.
Just like application changes should come with test changes, so should Security Rules. Security Rules tests run against the Firebase Emulator Suite, and can be included in your CI setup.
If you're not yet testing Security Rules, check out this documentation to get started, this video for an overview of testing, and this blog post and video on adding your tests to CI.
If the Security Rules are fully tested, there should be a lot of tests. For each kind of document, there are usually four different kinds of access: read, create, update, and delete. (These are just the most common permissions to grant; create, update, and delete can be rolled up into write, and read can be broken into get and list.) For each permission, there should be a test of the happy path, granting permission, and a test for each situation that should deny access.
read
create
update
delete
write
get
list
Say I have one rule:
// firestore.rules allow update: if // User is the author resource.data.authorUID == request.auth.uid && // `authorUID` and `createdAt` are unchanged request.resource.data.diff(request.resource.data).unchangedKeys().hasAll([ "authorUID", "createdAt" ]) && // Title must be < 50 characters long request.resource.data.title.size < 50;
To completely test this, I would write tests around granting access and each way that access could be denied:
// test.js const firebase = require("@firebase/rules-unit-testing"); const dbAuthorAuth = firebase.initializeTestApp({ projectId: TEST_FIREBASE_PROJECT_ID, auth: { uid: "author", email: "alice@example.com" } }).firestore(); const dbOtherAuth = firebase.initializeTestApp({ projectId: TEST_FIREBASE_PROJECT_ID, auth: { uid: "other", email: "otto@example.com" } }).firestore(); describe("blog posts", () => { before(async () => { dbAuthorAuth.doc("drafts/12345").set({ authorUID: "author", createdAt: Date.now(), title: "Make an apple", content: "TODO!" }); }); it("can be updated by author if immutable fields are unchanged", async () => { await firebase.assertSucceeds(dbAuthorAuth.doc("drafts/12345").update({ title: "Make an app", content: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat." })); }); it("cannot be updated by anyone other than the author", async () => { await firebase.assertFails(dbOtherAuth.doc("drafts/12345").update({ title: "Make an app", content: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat." })); }); it("cannot be updated by author if the author ID is changed", async () => { await firebase.assertFails(authorDb.doc("drafts/12345").update({ authorUID: "New Person" title: "Make an app", content: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat." })); }); it("cannot be updated by author if the created date is changed", async () => { await firebase.assertFails(authorDb.doc("drafts/12345").update({ createdAt: Date.now(), title: "Make an app", content: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat." })); }); it("cannot be updated if the title is over 50 characters", async () => { await firebase.assertFails(authorDb.doc("drafts/12345").update({ title: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.", content: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat." })); });
As the reviewer, look through their test cases, and make sure they have tested each kind of document. If they've written good test descriptions, that's the easiest place to understand what the Security Rules are doing. Similarly, if they've written code comments in their rules, that can also be a good entry point to understanding the Security Rules.
Subcollections don't inherit the rules from the parent document, so make sure they're specifically covered in the Security Rules. Look at the subcollections you're storing in Firestore; if all documents should have the same access that a parent document has, then that parent document should use the glob syntax: match /post/{id=**}.
match /post/{id=**}
What's more common is that a subcollection has been broken out into a subcollection because a different person needs to read it, or someone else is allowed to write to it. In that case, check the Security Rules to make sure it has its own match statement. Nesting is only a stylistic difference; whether you nest your match statements or not, make sure this is a match statement for the documents in the subcollections:
// Nested match statements are fine match /post/{postID} { allow read: if ... match /comments/{commentID} { allow read: if ... } } // Unnested match statements are fine match /post/{postID} { allow read: if ... } match /comments/{commentID} { allow read: if ... }
If you're reviewing Realtime Database Security Rules, child nodes inherit from parent nodes, the opposite of the behavior in Firestore.
Security Rules can enforce type and data validations for specific fields in addition to preventing unwanted access. If you know that some documents have required fields, immutable fields, fields that must be a timestamp, or a field that must contain data in a specific range, check that those are enforced in the Security Rules for those documents. For example, in this rule, I'm checking that the immutable fields of authorUID, publishedAt, and url aren't changed by an update, and that the required fields of content, title, and visible are still present:
authorUID
publishedAt
url
content
title
visible
allow update: if // Immutable fields are unchanged request.resource.data.diff(request.resource.data).unchangedKeys().hasAll([ "authorUID", "publishedAt", "url" ]) && // Required fields are present request.resource.data.keys().hasAll([ "content", "title", "visible" ]);
If you use the more granular permissions of create, update, and delete in place of write, make sure that validations apply to both create and update.
Because the Admin SDK authorizes using service account credentials, all requests from the Admin SDK, including Cloud Functions for Firebase, bypass Security Rules. To give a thorough security review for a Firebase app, it's also necessary to look at any other writes that are happening to your backend via the Admin SDK.
For example, if I have great Firestore Security Rules, but a Cloud Function exports data to a storage bucket that has no Security Rules, that would be a terrible way to treat my user's data. The flip side of this is that Cloud Functions can be used in situations where Security Rules don't work, for example, returning individual fields from documents. See this blog post for tips to use Cloud Functions in conjunction with Security Rules.
Our team is driven by the belief that apps have drastically improved the way we live, work, learn, and socialize, keeping us connected to each other and plugged into the information we need. Now more than ever, we understand the importance of supporting our developer community by ensuring you have the technology and resources you need to keep your business up and running. Whether you’re a high-growth startup or a global enterprise, we’re still here to help you build and operate your app.
If you're using Cloud Firestore or Cloud Storage for Firebase, you're also using Security Rules. (If you're using the default rules instead of tailoring them to your app, this is where to start!) We're excited to announce that in the last few months we've released some substantial improvements to the tools for writing and debugging Rules, improvements to the Rules language itself, and increases to the size limits for Rules!. These are a few of the great new features. Check out the Security Rules Release Notes for a comprehensive list of everything we've released.
We've released several improvements to make the rules language more expressive and succinct. One particularly verbose pattern was comparing the new values of a document to existing values. The new Set type available in Rules is purpose-built for these comparisons, and also has methods for functionality you'd expect for a Set, like getting the intersection, union, or difference between Sets. For example:
Set type
Allow a user to create a document if the document has required and optional fields, but not others:
allow create: if (request.resource.data.keys().toSet() .hasOnly(["required","and","optional","keys"])
Sets come with == and in operators and hasAll, hasAny, hasOnly, difference, intersection, union, and size methods.
==
in
hasAll
hasAny
hasOnly
difference
intersection
union
size
Sets are most useful in conjunction with the Map class, and because the request and resource objects are both structured as maps, you're probably already familiar with it. Map recently got a few new methods, diff and get, that will hopefully open the door to more concise rules for everyone. Here's how they work:
Map
request
diff
Map.diff() is called on one map, and takes the second map as an argument: map1.diff(map2). It returns a MapDiff object, and all of the MapDiff methods, like addedKeys, changedKeys, or affectedKeys return a Set object.
map1.diff(map2)
addedKeys
changedKeys
affectedKeys
Set
Map.diff() can solve some verbose patterns like checking which fields changed before and after a request. For example, this rule allows an update if the "maxLevel" field was the only field changed:
allow update: if request.resource.data.diff(resource.data).changedKeys().hasOnly(["maxLevel"]);
In the next example, posts have a field indicating the user role required to modify the post. We'll use Map.get() to get the "roleToEdit" field. If the document doesn't have the field, it will default to the "admin" role. Then we'll compare that to the role that's on the user's custom claims:
"roleToEdit"
"admin"
allow update, delete: if resource.data.get("roleToEdit", "admin") == request.auth.token.role;
Keep in mind that because Sets are not ordered but Lists are. You can convert a List to a Set, but you can't convert a Set to a List.
Local variables have been one of the most requested features in Rules, and they're now available within functions. You can declare a variable using the keyword let, and you can have up to 10 local variables per function.
let
Say you're commonly checking that a user meets the same three conditions before granting access: that they're an owner of the product or an admin user, that they successfully answered a challenge question, and that they meet the karma threshold.
rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { match /products/{product} { allow read: if true; allow write: if (exists(/databases/$(database)/documents/admins/$(request.auth.uid)) || exists(/databases/$(database)/documents/product/owner/$(request.auth.uid))) && get(/databases/$(database)/documents/users/$(request.auth.uid)) .data.passChallenge == true && get(/databases/$(database)/documents/users/$(request.auth.uid)) .data.karma > 5; } match /categories/{category} { allow read: if true; allow write: if (exists(/databases/$(database)/documents/admins/$(request.auth.uid)) || exists(/databases/$(database)/documents/product/owner/$(request.auth.uid))) && get(/databases/$(database)/documents/users/$(request.auth.uid)) .data.passChallenge == true && get(/databases/$(database)/documents/users/$(request.auth.uid)) .data.karma > 5; } match /brands/{brand} { allow read, write: if (exists(/databases/$(database)/documents/admins/$(request.auth.uid)) || exists(/databases/$(database)/documents/product/owner/$(request.auth.uid))) && get(/databases/$(database)/documents/users/$(request.auth.uid)) .data.passChallenge == true && get(/databases/$(database)/documents/users/$(request.auth.uid)) .data.karma > 5; } } }
Those conditions, along with the paths I'm using for lookups can all now become variables in a function, which creates more readable rules:
rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { function privilegedAccess(uid, product) { let adminDatabasePath = /databases/$(database)/documents/admins/$(uid); let userDatabasePath = /databases/$(database)/documents/users/$(uid); let ownerDatabasePath = /databases/$(database)/documents/$(product)/owner/$(uid); let isOwnerOrAdmin = exists(adminDatabasePath) || exists(ownerDatabasePath); let meetsChallenge = get(userDatabasePath).data.get("passChallenge", false) == true; let meetsKarmaThreshold = get(userDatabasePath).data.get("karma", 1) > 5; return isOwnerOrAdmin && meetsChallenge && meetsKarmaThreshold; } match /products/{product} { allow read: if true; allow write: if privilegedAccess(); } match /categories/{category} { allow read: if true; allow write: if privilegedAccess(); } match /brands/{brand} { allow read, write: if privilegedAccess(); } } }
You can see at a glance that the same conditions grant access to write to documents in the three different collections.
The updated version also uses map.get() to fetch the karma and passChallenge fields from the user data, which helps keep the new function concise. In this example, if there is no karma field for a user, then the get returns false. Keep in mind that Map.get() fetches a specific field, and is separate from the DocumentReference.get() that fetches a document.
map.get()
karma
passChallenge
Map.get()
DocumentReference.get()
This is the first time we've introduced an if/else control flow, and we hope it will make rules smoother and more powerful.
if/else
Here's an example of using a ternary operator to specify complex conditions for a write. A user can update a document in two cases: first, if they're an admin user, they need to either set the field overrideReason or approvedBy. Second, if they're not an admin user, then the update must include all the required fields:
overrideReason
approvedBy
allow update: if isAdminUser(request.auth.uid) ? request.resource.data.keys().toSet().hasAny(["overrideReason", "approvedBy"]) : request.resource.data.keys().toSet().hasAll(["all", "the", "required", "fields"])
It was possible to express this before the ternary, but this is a much more concise expression.
And finally, here's a feature for those of you with longer rules. Until now, rules files had to be smaller than 64 KB. (To be more specific, the compiled AST of the rules file had to be smaller than 64 KB, and you wouldn't know you were within the limit until you tried to deploy the rules.) This limit was holding some developers back, and once you reached the limit, you had to start making tradeoffs in your rules. We definitely wanted to fix this.
Since this is one of the limits that helps rules return a decision in nanoseconds, we wanted to find a way to increase the limit without sacrificing performance. We optimized how we compile and store the Rules file, and we were able to quadruple the limit to 256 KB!
The limits on rules are in place to keep rules fast enough to return a decision in nanoseconds, but we work hard to keep them workable. Let us know if you start to outgrow any of them
All of these features are informed by the feedback we hear from you about what's great, what's hard, and what's confusing about Firestore Security Rules, so keep letting us know what you think!
Hello, Cloud Firestore developers! We wanted to let you know about some useful new querying features we've added to Cloud Firestore this week. Starting with… in queries!
With the in query, you can query a specific field for multiple values (up to 10) in a single query. You do this by passing a list containing all the values you want to search for, and Cloud Firestore will match any document whose field equals one of those values.
in queries are a good way to run simple OR queries in Cloud Firestore. For instance, if the database for your E-commerce app had a customer_orders collection, and you wanted to find which orders had a "Ready to ship", "Out for delivery" or "Completed" status, this is now something you can do with a single query, like so:
customer_orders
We've launched another feature similar to the in query, the array-contains-any query. This feature allows you to perform array-contains queries against multiple values at the same time.
array-contains-any
array-contains
For example, if your app had a products collection, and those documents contained an array of categories that every item belongs in, you could now look for items that were in the "Appliances" or "Electronics" category, by passing these values into a single array-contains-any query.
Note that the baby monitor document will only be returned once in your query, even though it matches with multiple categories.
These queries are also supported in the Firebase console, which gives you the ability to try them out on your dataset before you start modifying your client code.
This also seems like a good time to remind you that you can apply filters directly in the Firebase console. Neat, huh?
Security rule behavior for these queries is pretty straightforward. Cloud Firestore will look at each potential value passed in for your in or array-contains-any operation and make sure your query would be allowed for that value. If any value is not allowed, the entire query fails.
For example, if your project was set up with these security rules…
match /projects/{project} { allow read: if resource.data.status != "secret"; ... }
This request would work…
db.collection("projects").where("status", "in", ["public", "unlisted"]);
...but this entire request would fail, because it's possible that our query will return documents that are forbidden in our security rules.
db.collection("projects").where("status", "in", ["public", "unlisted", "secret"]);
Not sure why we couldn't just send you back the allowed documents? Make sure to review the 'Rules are not filters' section of this video.
While we're excited to have you unlock the potential of in queries and array-contains-any queries, you should know about a few important limitations:
I think there's a lot of exciting things you can do now with in queries, and we're looking forward to hearing what new functionality you've added to your apps. So make sure you've upgraded your client libraries to the latest versions to take advantage of the new features, check out the documentation, and happy databasing!
Firebase is a great platform for developing rich mobile and web applications by leveraging the power and scale of the Google Cloud Platform. Firebase provides a wide range of composable backend services and SDKs that help developers overcome numerous challenges in app development including user authentication, data storage, notification delivery, and more.
In addition to the services and the SDKs, Firebase also offers security rules -- a powerful mechanism that helps enforce the security and logical correctness of your apps. The backend services use security rules to authorize and validate the requests made by client apps, and make sure they adhere to the policies that app developers have put in place. Today, you can use security rules to govern how your users interact with the Firebase Realtime Database, Cloud Storage, and Cloud Firestore. Rules in these Firebase products help you achieve two critical goals:
If you're using any Firebase product mentioned above, rules are essential. But you may have questions about how they fit into your application architecture. In order to shed some light on this subject, we'd like to share a few tips related to security rules and the Admin SDK.
As you explore security rules in depth, you will eventually discover that requests from the Firebase Admin SDK are not gated by rules. The Admin SDK is initialized with a service account, which gives the SDK full access to your data. In Firebase Realtime Database, you can scope the Admin SDK's privileges to a user ID, and enforce rules as usual. But other products, most notably Google Cloud Firestore, don't support this feature yet. You should be mindful about that when implementing server-side data access operations.
Because of the elevated privileges of service accounts and the Admin SDK, you should also make sure that they only get deployed in environments that you trust with administrative control of your project. Typical environments include servers controlled by the developers, and managed cloud environments like Google Cloud Functions and App Engine. On the other hand, end-user devices and web browsers where the application code is open for modification are inherently untrusted, and the Admin SDK should never be deployed in them.
Many applications have data that is critical to the operation of the app, but should never be modified by the users. Consider a forum app that promotes user engagement by awarding its participants points (think StackOverflow). Each forum post needs to be scored in near real-time so the users can track their progress, but the users themselves should never be able to change anyone's points, not even their own.
The simplest way to protect such application-managed data is to specify a rule that prevents all writes to the data from the users.
service cloud.firestore { match /databases/{database}/documents { function isAuthorized() { // Some function that grants users read access. } match /scores/{uid}/{document} { allow write: if false; // Nothing gets past me (except Admin of course). allow read: if isAuthorized(); // Grant read access as necessary. } } }
Then you can use the Admin SDK to implement backend services that keep the data up-to-date. For instance, you can implement a serverless function using Cloud Functions for Firebase that automatically executes whenever a user posts something in the forum. This function can determine how many points to award the user, and update the respective entries in the read-only scores collection.
scores
import * as admin from 'firebase-admin'; admin.initializeApp(); export const updateScores = functions.firestore.document('posts/{userId}/{postId}') .onCreate((snapshot, context) => { const score = calculateScore(snapshot); const userId = context.params.userId; const doc = admin.firestore().collection('scores').document(userId); return admin.firestore().runTransaction((txn) => { return txn.get(doc).then((snap) => { const current = snap.data().total || 0; txn.set(doc, {total: current + score}, {merge: true}); }); }); });
Since Cloud Functions is a trusted environment, the backend code can continue to update your data, while keeping users from doing something they are not allowed to.
Many applications need to deal with users in different roles. What individual users can do in the app usually depends on their roles. Let's take a MOOC (Massively Open Online Courses) app as an example, where there are teachers, students and TAs. Teachers and TAs should be able to view and update course content, but students should only be able to view the material.
In a Firebase app, user roles can be managed by setting custom claims on user accounts. This is a privileged operation that can only be performed in a backend environment, typically using the Admin SDK. Custom claims are additional information that we associate with user accounts in Firebase Auth, and this information becomes available in the ID tokens that Firebase issues to users upon sign in. You can inspect these claims via security rules to facilitate role-based access control.
Going back to our example MOOC app, we can use the following backed code to grant a user teacher role.
import * as admin from 'firebase-admin'; admin.initializeApp(); async function grantTeacherRole(userId: string) { await admin.auth().setCustomUserClaims(userId, {role: 'teacher'}); }
Now you can define a rule that only allows teachers and TAs write-access to the courses collection.
courses
service cloud.firestore { match /databases/{database}/documents { function isTeacher() { return request.auth.token.role == "teacher"; } function isTA() { return request.auth.token.role == "ta"; } match /courses/{doc} { allow write: if isTeacher() || isTA(); // Only teachers and TAs can write. allow read: if true; // But anybody can read. } } }
Note that Firebase client SDKs cache ID tokens up to an hour. Therefore changes to a user's custom claims may take up to an hour to take effect.
Sometimes you want the ability to withhold some data from users until an administrator or a backend service determines it's time to release the data. For example, consider an automated process that grades tests in a MOOC app. You would want this process to finish grading all the tests before any scores are shared with the students. In this case the grading process should be able to update any Firestore document, and it can be deployed in a trusted backend environment. Therefore you can use the Admin SDK to implement it.
To make sure the intermediate states of data are not visible to users, you can create each document with a visibility attribute set to "private". Then in security rules, restrict access to only those documents whose visibility attribute is set to "public". Here's what this rule would look like:
visibility
"private"
"public"
service cloud.firestore { match /databases/{database}/documents { function isPublic() { return resource.data.visibility == "public"; } match /grades/{document} { allow read: if isPublic(); // Cannot read unless marked as "public". allow write: if false; // Nobody except Admin can update the documents. } } }
With the above rules configuration in place, all documents created with the visibility attribute set to "private" are inaccessible to the end-users. When the backend process is ready to release a document to the users, it can use the Admin SDK to change the visibility attribute of the target document to "public".
import * as admin from 'firebase-admin'; admin.initializeApp(); async function gradeTests() { // Create a new document and continue to write to it. const doc = await createNewDoc(); await updateGrades(doc); // Later, make the document visible when ready. await releaseGrades(doc); } async function createNewDoc() { const doc = admin.firestore().collection('grades').document(); // Make the new doc hidden by default await doc.set({visibility: 'private'}); return doc; } async function releaseGrades(doc) { await doc.update({visibility: 'public'}); }
As soon as the visibility attribute is set to "public" on a document, it will start appearing in matching Firestore queries executed by users.
It can be tempting to write rules that apply across collections -- for example, writing a rule that grants teachers in the MOOC app access to all documents in the database. In such situations, remember that rules will grant access to a document if any match statement in the rules configuration grants access. When multiple rules apply to the same document, it is easy to forget that any rule that allows access, overrides all the other rules that deny access.
service cloud.firestore { match /databases/{database}/documents { match /reports/{document} { // This rule is intended to selectively grant users read-only access to the // documents in the 'reports' collection. But the rule below inadvertently // grants teachers read-write access to these documents. allow read: if isConditionMet(); allow write: if false; } match /{document=**} { // This rule matches all documents in the database, including the 'reports' // collection. In case of teachers, this will override the previous rule. allow read, write: if isTeacher(); } } }
To avoid accidentally granting users access to protected data in your app, you should try to write rules in a manner so that each document only matches a single rules statement. One way to make that easier is to avoid wildcards that match collections ({document=**}), and only use wildcards that match documents ({document}). If you're tempted to define overarching rules, consider if the Admin SDK would be a better fit, because as we learned in the Tip #1, requests from the Admin SDK bypasses security rules.
{document=**}
{document}
As your apps grow and evolve over time, you may implement various administrative tools to manage your app's data. For example, you may want to implement a tool that backs up certain Firestore collections or RTDB paths. Or you are trying to meet specific privacy requirements, and you want to implement a service that deletes user data when the data becomes obsolete or when the users demand it. Such tools typically require unrestricted access to large portions of your database.
At first this may look like a good reason to have a relaxed set of security rules. But you should strive to write the most detailed and restrictive rules that describe the access patterns of your app. Administrative tools should be implemented using the Admin SDK, and deployed in a privileged environment that you control. This way your admin tools can retain full access to all the data, while closely regulating what end-users can do in the app.
In some situations you may want to temporarily deny a user access to data. Perhaps the monitoring infrastructure of your forum app has just detected a user posting spam, and you want to prevent that user from posting any more content until you can conduct a thorough investigation of the incident. You can use security rules to implement a simple access control list (ACL) on top of Firestore, and use the Admin SDK to dynamically manage it. You would start by declaring a rule like the following:
service cloud.firestore { match /databases/{database}/documents { function isBlackListed() { return exists(/databases/$(database)/documents/blacklist/$(request.auth.uid)) } // Collections are closed for reads and writes by default. This match block // is included for clarity. match /blacklist/{entry} { allow read: if false; allow write: if false; } match /posts/{postId} { allow write: if !isBlackListed() } } }
This mentions a Firestore collection named blacklist that no user can read or write. It also uses the exists built-in function to check if a document with a given key exists in the Firestore database. If you haven't seen this pattern before, built-in functions like exists and get enable us to access Firestore documents from security rules. In this case, if we find a user ID in the blacklist collection, we prevent the corresponding user from writing to the posts collection. Now we can use the Admin SDK to add users to the blacklist collection, and revoke their write-access:
blacklist
exists
posts
await revokeWriteAccess('bAdAgEnT'); async function revokeWriteAccess(userId) { const user = admin.firestore().collection('blacklist').document(userId) await user.set({ reason: 'possible bad agent', blacklisted_at: admin.firestore.FieldValue.serverTimestamp(), }); }
Since you have already locked down all access to the blacklist collection, you can rest assured that only the Admin SDK (i.e. our backend code) can make modifications to it. Changes to the ACL take effect immediately. To grant a user access to the data again, simply remove the corresponding document with the user ID from the blacklist collection.
Note that we are also writing the current timestamp to the same document when adding a user to the blacklist. If you want, you can write a rule that references this property to automatically grant blacklisted users access after a cool off period.
service cloud.firestore { function isTwoDaysElapsed() { return request.time > timestamp.value(get(/databases/$(database)/documents/ blacklist/$(request.auth.uid)).data.blacklisted_at.seconds*1000) + duration.value(2, 'd'); } match /databases/{database}/documents { match /posts/{postId} { // allow if blacklisted more than 2 days ago allow write: if isTwoDaysElapsed(); } } }
Firebase takes a declarative approach to ensuring the security and logical correctness of your apps. By keeping the rules separate from application code, you can easily update your security policies, while keeping the application code simple. As many developers know by experience, code changes are harder to make, and even harder to test and deploy. But with Firebase, you can rapidly iterate on your rules without having to touch the application code at all. Moreover, you can patch any detected security vulnerabilities instantly, without having to go through a long and arduous app rollout.
You can also use rules in conjunction with the Firebase Admin SDK to implement sophisticated use cases that involve server-side code. Admin SDK is not subjected to rules checks, but this extra degree of freedom enables some useful patterns that can be applied to multiple real world applications. You can implement any server-side components using the Admin SDK and deploy them in trusted environments like Google Cloud Functions, while subjecting the client-side apps to stricter constraints.
Read more about Firebase security rules and the Admin SDK in our documentation. If you have used these tools to solve any interesting problems, we'd love to hear about your experience. Happy coding with Firebase!
We all know that securing your database from malicious or misguided clients is critical. And with Security Rules for Cloud Firestore, you can create a very powerful access control system with simple matching syntax and logic.
Until now, however, testing your rules was difficult. You couldn't test your rules before deploying them, and running arbitrary tests against these rules (to ensure they worked the way you expected) wasn't easy, either.
Today, we're pleased to announce our first big step in making it easier to secure your Cloud Firestore database with the release of the Rules Simulator.
With the new Rules Simulator in the Firebase console, you can test your rules as you write them — and before you deploy!
The simulator lets you test document reads, writes, and deletes against any part of your Cloud Firestore database. It will also let you simulate being signed in with a particular userID, so you can better test user-based access control. And for more sophisticated auth-based security, the simulator will help you build authentication tokens for various providers, giving you a preview of what that token will look like and allowing you to directly map the shape of the token to the rules you are writing.
The simulator tests against the rules as they are currently drafted in your editor, not as they are in deployment, allowing you to rapidly test different rules with different types of requests. This means that next time you click the publish button, you can have more confidence that your rules are protecting your data and doing exactly what you expect them to do!
You can get started today with the simulator by navigating over to the Rules section of the Firestore panel in the console.
We've also significantly increased the number of get(), exists() and getAfter() calls you can make in each security rule. For a single document request, you can now make 10 document access calls (up from 3). For multi-resource requests, such as batched writes, you'll be able to make a total of 20 document access calls for all documents in that request.
get()
exists()
getAfter()
Check out our documentation for more information and examples.
Firestore Security Rules may look like JavaScript, but they're actually a purpose-built language with its own unique syntax and behavior. While we've always provided guides to help you write security rules, many developers mentioned to us that it is too hard to discover all of the functions, types, and edge-cases that you need to learn in order to write complex rules.
That's why we published comprehensive reference documentation on the security rules language and the built-in types and functions that it provides. We hope this will enable you to be more confident when writing advanced rules conditions.
The Firebase console gives you the power to make changes to your Cloud Firestore Security Rules directly from your browser! Unfortunately, that also means your team has the ability to break your security rules directly from your browser, maybe without remembering exactly what they changed. Well fear no more; Firebase has your back!
Recently, we added Version History to Cloud Firestore Security Rules. For any Cloud Firestore project, you can now browse all previously published versions of Rules, view the differences between the previous and current versions, and edit the current version -- either to a complete rollback to an earlier version or to a mix of rules.
Now, when your team introduces a bad security rule, it's a snap to find out what the previous working version was and get back to it.
To start using version history for Security Rules, simply navigate to the Rules tab of Cloud Firestore, and start browsing your version history in the left-hand column. From any older version, you can click "Compare to Latest" to view the diff and edit the latest version.
Every deploy that includes Rules is considered a new version; you'll see unchanged versions if you've deployed unchanged rules several times from the command line. To avoid that, you can call deploy --only from the Firebase CLI to deploy only the things you've changed. For example, I often make edits to my project's Cloud Functions without changing anything else, so I end up running firebase deploy --only functions from the command-line tool to make sure only the changes to my Cloud Functions get submitted.
deploy --only
firebase deploy --only functions
Reach out to us via Twitter @Firebase or via our other support channels to let us know what you think! We're looking forward to adding this to other Firebase features, and would like to get your feedback before we do.