Writing Conditions for Security Rules

This guide builds on the structuring security rules guide to show how to add conditions to your Cloud Firestore Security Rules. If you are not familiar with the basics of Cloud Firestore Security Rules, see the getting started guide.

The primary building block of Cloud Firestore Security Rules is the condition. A condition is a boolean expression that determines whether a particular operation should be allowed or denied. Use security rules to write conditions that check user authentication, validate incoming data, or even access other parts of your database.

Authentication

One of the most common security rule patterns is controlling access based on the user's authentication state. For example, your app may want to allow only signed-in users to write data:

service cloud.firestore {
  match /databases/{database}/documents {
    // Allow the user to access documents in the "cities" collection
    // only if they are authenticated.
    match /cities/{city} {
      allow read, write: if request.auth.uid != null;
    }
  }
}

Another common pattern is to make sure users can only read and write their own data:

service cloud.firestore {
  match /databases/{database}/documents {
    // Make sure the uid of the requesting user matches the 'author_id' field
    // of the document
    match /users/{user} {
      allow read, write: if request.auth.uid == resource.data.author_id;
    }
  }
}

If your app uses Firebase Authentication, the request.auth variable contains the authentication information for the client requesting data. For more information about request.auth, see the reference documentation.

Data validation

Many apps store access control information as fields on documents in the database. Cloud Firestore Security Rules can dynamically allow or deny access based on document data:

service cloud.firestore {
  match /databases/{database}/documents {
    // Allow the user to read data if the document has the 'visibility'
    // field set to 'public'
    match /cities/{city} {
      allow read: if resource.data.visibility == 'public';
    }
  }
}

The resource variable refers to the requested document, and resource.data is a map of all of the fields and values stored in the document. For more information on the resource variable, see the reference documentation.

When writing data, you may want to compare incoming data to existing data. In this case, if your ruleset allows the pending write, the request.resource variable contains the future state of the document. For update operations that only modify a subset of the document fields, the request.resource variable will still contain the full document state prior to the write. You can check the field values in request.resource to prevent unwanted or inconsistent data updates:

service cloud.firestore {
  match /databases/{database}/documents {
    // Make sure all cities have a positive population and
    // the name is not changed
    match /cities/{city} {
      allow update: if request.resource.data.population > 0
                    && request.resource.data.name == resource.data.name;
    }
  }
}

Access other documents

Using the get() and exists() functions, your security rules can evaluate incoming requests against other documents in the database. The get() and exists() functions both expect fully specified document paths. When using variables to construct paths for get() and exists(), you need to explicitly escape variables using the $(variable) syntax.

In the example below, the database variable is captured by the match statement match /databases/{database}/documents and used to form the path:

service cloud.firestore {
  match /databases/{database}/documents {
    match /cities/{city} {
      // Make sure a 'users' document exists for the requesting user before
      // allowing any writes to the 'cities' collection
      allow create: if exists(/databases/$(database)/documents/users/$(request.auth.uid))

      // Allow the user to delete cities if their user document has the
      // 'admin' field set to 'true'
      allow delete: if get(/databases/$(database)/documents/users/$(request.auth.uid)).data.admin == true
    }
  }
}

Custom functions

As your security rules become more complex, you may want to wrap sets of conditions in functions that you can reuse across your ruleset. Security rules support custom functions. The syntax for custom functions is a bit like JavaScript, but security rules functions are written in a domain-specific language that has some important limitations:

  • Functions can contain only a single return statement. They cannot contain any additional logic. For example, they cannot create intermediate variables, execute loops, or call external services.
  • Functions can automatically access functions and variables from the scope in which they are defined. For example, a function defined within the service cloud.firestore scope has access to the resource variable and built-in functions such as get() and exists().
  • Functions may call other functions but may not recurse. The total call stack depth is limited to 10.

A function is defined with the function keyword and takes zero or more arguments. For example, you may want to combine the two types of conditions used in the examples above into a single function:

service cloud.firestore {
  match /databases/{database}/documents {
    // True if the user is signed in or the requested data is 'public'
    function signedInOrPublic() {
      return request.auth.uid != null || resource.data.visibility == 'public';
    }

    match /cities/{city} {
      allow read, write: if signedInOrPublic();
    }

    match /users/{user} {
      allow read, write: if signedInOrPublic();
    }
  }
}

Using functions in your security rules makes them more maintainable as the complexity of your rules grows. For more information on developer-defined functions, see the reference documentation.

Security rules and query results

When your app sends a query, Cloud Firestore evaluates your security rules to check whether your app has permission to read all of the possible query results. If your app has permission, Cloud Firestore returns the query results; otherwise, Cloud Firestore returns an error.

When Cloud Firestore checks your security rules against all possible query results, it does not examine the data that actually exists within your documents. For example, if your security rules check that a document's visibility field is equal to public, and your query does not include a where() clause similar to where("visibility", "==", "public"), the query will fail. The failure occurs because the query could, in theory, return documents with a different value in the public field, which would violate the security rules.

Here's another example: Consider a diary app with a primary collection named posts. Each post document has an owner_id field set to the owner's user ID. The app's security rules are configured so that users can access only their own posts:

service cloud.firestore {
  match /databases/{database}/documents {
    match /posts/{post} {
      allow read: if resource.data.owner_id == request.auth.uid;
    }
  }
}

The following query will fail because the security rules check the value of the owner_id field, but the query does not. As a result, Cloud Firestore cannot guarantee that all possible query results will satisfy the allow read condition:

db.collection("posts").orderBy("timestamp");

In contrast, the following query will succeed, because the query conditions guarantee that all possible results will satisfy the allow read condition:

db.collection("posts").where("owner_id", "==", myUserId).orderBy("timestamp");

Next steps