blog   |   26 May, 2023

Firebase: Cheatsheet for Firestore security rules

Explore this guide for a range of security rules, from beginner to advanced levels, complete with illustrative examples.

Firestore securit rules can govern different read and write actions within an application.

For read operations, rules can be targeted to specific read types:

  • allow read: Covers both collections and documents.
  • allow get: Applied when retrieving a single document.
  • allow list: Applied when querying a collection.

Write operations can also be scoped in the following manner:

  • allow create: Applied when creating new data via docRef.set() or collectionRef.add().
  • allow update: Applied when modifying data through docRef.update() or set().
  • allow delete: Applied during data deletion via docRef.delete().
  • allow write: Extends rules to cover create, update, and delete actions collectively.

Understanding resource and request

Firestore provides access to a range of special variables utilized for rule composition. Two important variables to understand before continueing are request and resource

  • request encompasses incoming data, including authentication details and timestamps.
  • resource refers to the existing data being requested.

It's important to note that the request object also contains a parameter named resource. To avoid the confusion between resource and request.resource we can write helper methods for identification.

function existingData() {
return resource.data;
}
function incomingData() {
return request.resource.data;
}

These methods can then be used to define the security rules. Here's a silly example:

match /products/{productId} {
allow update: if existingData().locked == false;
allow delete: if incomingData().price == 10;
}

Basic security rules

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

3. Allow access to documents in a collection
match /posts/{documentId} {
    allow read: if true; // Allows anyone to read documents in this collection
}
4. Allow access to all collections except one
match /{document=**} {
    allow read, write: if false;
}

match /{collection}/{docId} {
    allow read: if collection != 'posts';
}

 

5. Check if write request includes a field
match /users/{documentId} {
    allow write: if request.resource.data.name != null; 
// Ensures 'name' field exists in the write request }
6. Check if the field exists and its type in a write request
match /users/{documentId} {
    allow write: if request.resource.data.name is string; 
// Ensures 'name' field exists in the write request and its type is string }
7. Check if the value exists in an array property of a document
match /users/{documentId} {
    allow read: if request.resource.data.book in resource.data.books
// resource.data.books is of type array }
8. Allow access if the request does not contain a field
match /users/{userId} {
  allow update: if !('age' in request.resource.data);
}
9. Ensure that the request data does not alter a specific field
match /users/{userId} {
  allow update: if request.resource.data.age == resource.data.age;
}
10. Only allow updating specific fields
match /users/{userId} {
  allow update: if request.resource.data.diff(resource.data).changedKeys().hasOnly(['email', 'phone']);
}
11. Deny access after a certain duration
match /posts/{postId} {
  allow update: if request.time < resource.data.createdAt + duration.value(100, 's');
// Notice that we are utilizing the createdAt timestamp from the document }
12. Make a document read-only after certain duration
match /posts/{postId} {
  allow read: if true; // Always allowing read
  allow update, delete: if request.time < resource.data.createdAt + duration.value(180, 's');
// Allow update only if the current time is within 180 seconds of the document's creation time
}
13. Allow access only before a certain date

match /posts/{postId} {
  allow read, write: if request.time < timestamp.date(2020, 6, 22);
}
14. Allow access if request contains any of the attributes
match /posts/{postId} {
  allow write: if request.resource.data.keys().hasOnly(['field1', 'field2']);
}
14. Limit array modifications to incremental updates
allow write: if request.resource.data.arrayField.size() == resource.data.arrayField.size() + 1
             && 
(
request.resource.data.arrayField[request.resource.data.arrayField.size() - 1]
== (resource.data.arrayField[resource.data.arrayField.size() - 1] + 1)
);

 

User and role based rules

1. Allow access only when authenticated
match /posts/{postId} {
    allow read: if request.auth != null;
}
2. Allow access to only the owner of the document
match /posts/{postId} {
    allow write: if request.auth.uid == resource.data.owner_id;
}

3. Allow access only when user's email is verified

match /posts/{postId} {
  allow write: if request.auth.token.email_verified;
}
4. Allow access based on user's authentication provider

match /posts/{postId} {
  allow read: if request.auth.token.firebase.sign_in_provider == 'google.com';
}

5. Deny operations to anonymous users

match /posts/{postId} {
  allow create: if request.auth.uid != null 
                && request.auth.token.firebase.sign_in_provider != 'anonymous';
}
6. Allow acces when BelongsTo/HasOne relation exists

match /posts/{userId} {
    allow read: if request.auth.uid == userId;
}

Or we can create a method that would work on all collection documents

match /posts/{userId} {
    allow read: if belongsTo(userId);
}

function belongsTo(userId) { return request.auth.uid == userId }
7. Allow acces with HasMany relation
match /posts/{postId} {
    allow write: if requestMatchesUID();
    allow update: if requestMatchesUID() && resourceMatchesUID();
    allow delete: if resourceMatchesUID();
}

function requestMatchesUID() {
    return request.auth.uid == request.resource.data.uid;
}

function resourceMatchesUID() {
    return request.auth.uid == resource.data.uid;
}
8. Allow write acces to user with attribute isAdmin

match /posts/{postId} {
    allow write: if get(/databases/$(database)/documents/users/$(request.auth.uid)).data.isAdmin == true;
    allow read
: true;
}
9. Allow access to users based on their role

match /posts/{postId} {
 allow read
: if get(/databases/$(database)/documents/users/$(request.auth.uid)).data.role == "Reader"
 allow write
: if get(/databases/$(database)/documents/users/$(request.auth.uid)).data.role == "Writer"
}

10. Allow access to users based on array of roles

match /posts/{postId} {
   allow read: if get(/databases/$(database)/documents/users/$(request.auth.uid))
.data.roles.hasAny(["Reader"]); allow write: if get(/databases/$(database)/documents/users/$(request.auth.uid))
.data.roles.hasAny(["Writer", "Admin"]); }
11. Allow access to admin or the list of allowed users

match /posts/{postId} {
   allow write: if request.auth.token.role == 'admin' || (request.auth.uid in resource.data.allowedUsers);
}
12. Limit access to users signed in before a certain date
match /posts/{postId} {
   allow read: if request.auth.time < timestamp.date(2023, 12, 12);
}

Common functions 

function isSignedIn() {
    return request.auth != null;
}

function emailVerified() {
    return request.auth.token.email_verified;
}

function userExists() {
    return exists(/databases/$(database)/documents/users/$(request.auth.uid));
}
    
function isUser(userId) {
    return request.auth.uid == userId;
}
function getUserData() {
    return get(/databases/$(database)/documents/accounts/$(request.auth.uid)).data
}