Spotted a bug? Have a great idea? Help us make google.dev great!

This codelab relies on completing the previous codelab, Getting Started with the Firebase Local Emulator Suite.

What you'll build

In this codelab, you'll run tests against the local Firestore emulator and use those tests to write Firebase Security Rules, the configuration settings that help keep your app secure against unwanted access.

What you'll learn

  • How Firebase Security Rules work
  • How to test Firestore Security Rules against a local emulator

In the first codelab, the rules we worked on locally were in Test mode; anyone could read or write to any document in the database. Test mode is useful when starting a project, because you can iterate on the data structures quickly without causing permission errors.

Then we went to the console and created an instance in Locked mode, meaning no users could read or write to it:

Neither Test mode or Locked mode is ideal for a normal production app. Now that you've built the client, you'll tailor the security rules to make sure to only allow users to read or write to their parts of the database.

In the editor, open the file emulators-codelab/codelab-initial-state/firestore.rules. You'll see that we have two sets of rules, one that uses the glob syntax (document=**) and applies to all documents in the database:

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

    // ...  
  }
}

And another one for the documents in the items collection. A rule like this should not be used in production, but we use it in this codelab to let the web app generate the initial set of data. We'll leave these rules alone for the duration of the codelab:

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

    match /items/{itemID} {
      allow read; // Users can read items
      // Never do this in production
      allow create; // Client app creates seed data to create a smooth codelab
    }
  }
}

During this codelab, we'll first lock down the database, and then gradually add access until all the access users will need is granted. The rule that applies to the entire database grants all access to all users. Update the rule to deny access by setting the condition to false:

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

    // ...
  }
}

Start the emulators

On the command line, make sure you're in emulators-codelab/codelab-initial-state/. You may still have the emulators running. If not, start the emulators:

$ firebase emulators:start

Once the emulators are running, you can run tests locally against them.

Run the tests

On the command line in a new terminal tab from the directory emulators-codelab/codelab-initial-state/, run the mocha tests in the functions directory, and scroll to the top of the output:

# This runs the tests in functions/test.js
$ npm --prefix=functions test

> functions@ test .../emulators-codelab/codelab-initial-state/functions
> mocha

  shopping carts
    1) can be created by the cart owner

  shopping carts
    2) can be read, updated, and deleted by the cart owner

  shopping cart items
    3) can be read by the cart owner
    4) can be added by the cart owner

  adding an item to the cart recalculates the cart total. 
    - should sum the cost of their items

  0 passing (550ms)
  1 pending
  4 failing
  
  . . .

Right now we have four failures. As you build the rules file, you can measure progress by watching more tests pass.

The first test failure is for the test case that a shopping cart can only be created by the shopping cart owner. The specific test is:

functions/test.js

  it('can be created by the cart owner', async () => {
    await firebase.assertSucceeds(db.doc("carts/alicesCart").set({
      ownerUID: "alice",
      total: 0
    }));
  })

Let's make this test pass. In the editor, open the security rules file, firestore.rules, and update the wildcard match statement to match /carts/{cartID}:

firestore.rules

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // UPDATE THIS LINE
    match /carts/{cartID} {
      allow read, write: if false;
    }

    // ...
  }
}

Next, replace the allow statement to allow create operations only if the user who is making the request is the user listed as the cart owner:

firestore.rules

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /carts/{cartID} {
      // UPDATE THIS LINE
      allow create: if request.auth.uid == request.resource.data.ownerUID;
    }

    // ...
  }
}

This rule means "let a cart be created if the user who is creating the cart is the same as the user listed as the cart's owner".

We use two objects that are available in the context of every rule:

  • The request object contains data and metadata about the operation that is being attempted.
  • If a Firebase project is using Firebase Authentication, the request.auth object describes the user who is making the request.

The Emulator Suite automatically updates the rules whenever firestore.rules is saved. You can confirm that the emulator has the updated the rules by looking in the tab running the emulator for the message Rules updated.:

Rerun the tests, and check that one more test passes.

$ npm --prefix=functions test

> functions@ test .../emulators-codelab/codelab-initial-state/functions
> mocha

  shopping carts
    ✓ can be created by the cart owner (86ms)

  shopping carts
    1) can be read, updated, and deleted by the cart owner

  shopping cart items
    2) can be read by the cart owner
    3) can be added by the cart owner

  adding an item to the cart recalculates the cart total. 
    - should sum the cost of their items

  1 passing (482ms)
  1 pending
  3 failing

Good job! You have one passing test!

The next test covers the case of a cart owner having the ability to read, update, or delete their cart. In this exercise, you'll test that a user can read their cart. The test is:

functions/test.js

  it("cart can be read by the cart owner", async () => {
    await firebase.assertSucceeds(db.doc("carts/alicesCart").get());
  }).timeout(1000);

In our rules file, firestore.rules, you'll add a second allow statement to cover cases of reading, updating, and deleting a cart:

firestore.rules

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /carts/{cartID} {
      allow create: if request.auth.uid == request.resource.data.ownerUID;
      // ADD THIS LINE
      allow read, update, delete: if request.auth.uid == resource.data.ownerUID;
    }

    // ...
  }
}

In this rule, instead of comparing the user that make the request to the cart's owner, the condition compares the user with the data we already persisted. This lets you use the same rule for the case of reads and updates.

Now we can rerun the test. Scroll to the top of the output and see that one more test passes:

$ npm --prefix=functions test

> functions@ test .../emulators-codelab/codelab-initial-state/functions
> mocha

  shopping carts
    ✓ can be created by the cart owner (81ms)

  shopping carts
    ✓ can be read, updated, and deleted by the cart owner (42ms)

  shopping cart items
    1) can be read by the cart owner
    2) can be added by the cart owner

  adding an item to the cart recalculates the cart total. 
    - should sum the cost of their items

  2 passing (505ms)
  1 pending
  2 failing

Awesome! Now you have two passing tests. Let's move on to the next test failure!

Right now, although cart owners read and write to their cart, they can't read or write individual items in their cart. That's because while owners have access to the cart document, they don't have access to the cart's subcollection of items.

This is a broken state for users.

Return to the web UI, which is running on http://localhost:5000, and try to add something to your cart. You get a Permission Denied error, visible from the debug console, because we haven't yet granted users access to created documents in the items subcollection.

The next test confirms that users can add items to their cart:

it("items can be added by the cart owner",  async () => {
    await firebase.assertSucceeds(db.doc("carts/alicesCart/items/lemon").set({
      name: "lemon",
      price: .99
    }));
}).timeout(1000);


The cart owner should be able to write to that subcollection. So we can write a rule that allows access if the current user has the same UID as the ownerUID on the cart document. Since there's no need to specify different rules for create, update, delete, you can use a write rule, which applies to all requests that modify data.

The get in the conditional is reading a value from Firestore–in this case, the ownerUID on the cart document.

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /carts/{cartID} {
      allow create: if request.auth.uid == request.resource.data.ownerUID;
      allow read, update, delete: if request.auth.uid == resource.data.ownerUID;
    }

    // ADD THESE LINES
    match /carts/{cartID}/items/{itemID} {
      allow write: if 
        get(/databases/$(database)/documents/carts/$(cartID))
        .data.ownerUID == request.auth.uid;
    }

    // ...
  }
}

Note: Read operations that happen in the rules are still subject to billing.

Now we can rerun the test. Scroll to the top of the output and check that one more test passes:

$ npm --prefix=functions test

> functions@ test .../emulators-codelab/codelab-initial-state/functions
> mocha

  shopping carts
    ✓ can be created by the cart owner (78ms)

  shopping carts
    ✓ can be read, updated, and deleted by the cart owner (45ms)

  shopping cart items
    ✓ can be added by the cart owner (50ms)
    1) can be read by the cart owner

  adding an item to the cart recalculates the cart total. 
    - should sum the cost of their items


  3 passing (401ms)
  1 pending
  1 failing

Almost there! Let's fix the last test failure!

The last failing test verifies that a cart owner can read documents from the items subcollection of their cart:

  it("items can be read by the cart owner", async () => {
    await firebase.assertSucceeds(db.doc("carts/alicesCart/items/milk").get());
  }).timeout(1000);

To pass this test, you need to grant read access in addition to the write access for the items subcollection:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /carts/{cartID} {
      allow create: if request.auth.uid == request.resource.data.ownerUID;
      allow read, update, delete: if request.auth.uid == resource.data.ownerUID;
    }
    match /carts/{cartID}/items/{itemID} {
      // UPDATE THIS LINE to include "read"
      allow read, write: if 
        get(/databases/$(database)/documents/carts/$(cartID))
        .data.ownerUID == request.auth.uid;
    }

    // ...
  }
}

Now you can rerun the tests triumphantly!

$ npm --prefix=functions test

> functions@ test .../emulators-codelab/codelab-initial-state/functions
> mocha

  shopping carts
    ✓ can be created by the cart owner (85ms)

  shopping carts
    ✓ can be read, updated, and deleted by the cart owner (42ms)

  shopping cart items
    ✓ can be added by the cart owner (50ms)
    ✓ can be read by the cart owner

  adding an item to the cart recalculates the cart total. 
    - should sum the cost of their items

  4 passing (395ms)
  1 pending

These tests are the bare minimum for the rules we wrote. You can find more complete tests in functions/test_all.js

Notice that we have one remaining test that's marked as pending. That test covers the interaction between Firestore and Functions. We'll fix that test in the next codelab.

Return to the web front end built in the first codelab and add an item to the cart. This is an important step to confirm that our tests and rules match the functionality required by the client. (Remember that the last time we tried out the UI users were unable to add items to their cart!)

The client automatically reloads the rules when the firestore.rules is saved. So, if the emulators are still running, try adding something to the cart, and confirm that you can add items.

Nice work! You just improved the security of your app, an essential step for getting it ready for production! If this were a production app, we could add these tests to our continuous integration pipeline. This would give us confidence going forward that our shopping cart data will have these access controls, even if others are modifying the rules.

But wait, there's more!

In the next codelab you'll learn:

  • How to write a function triggered by a Firestore event
  • How to create tests that work across multiple emulators