Firestore Security rules
We define the security rules in the Firebase console or in a firestore.rules file, referenced by firebase.json. Firebase doesn't bill reads and writes denied by security rules.
rules version
rules_version = "2"
firestore scope
We start by scoping the rules to cloud.firestore
service cloud.firestore {
// ...
}
database scope
We scope the rules to the current database. The security rules only affect the current database, so this can be seen as superfluous. But we use the wildcard when querying separate documents (see below).
match /databases/{database}/documents {
// ...
}
set rules for a given collection
We target a collection. The document ID wildcard holds the requested document's ID. We can name it with what the document represents:
match /users/{user_id} { // document ID wildcard
// ...
}
operations and condition
allow operation, operation: if condition;
operations
read
create
update
delete
authentication, user ID
If the user is not authenticated, request.auth is null. We can filter out unauthenticated users:
allow read: if request.auth != null;
The user's authentication uid (if logged in) is available as request.auth.uid:
request.auth.uid
Note: if auth is null, trying to read uid triggers a failsafe mechanism that denies the request.
authorize specific documents
Document ID based authorization: We authorize the operation if the document's ID matches some condition:
match /players/{player_id} {
allow read: if request.auth.uid == player_id;
}
Field value based authorization: we authorize the operation based on the value of a specific field. Either from the requested document or from the uploaded document.
- resource.data represents the requested document.
- request.resource.data represents the uploaded document
- For example, we check the requested document's owner property against auth.uid. In this case, we ignore the uploaded document's data which can be tampered with.
match /planets/{planet_id} {
allow read: if request.auth.uid == resource.data.owner.id;
}
If the document is missing the field, the request is denied.
authorization based on separate documents
We read a document with get(). It is a billed read.
first example (do not use)
This unlocks a pattern where we read authorization data in a separate document, such as in the user document, which would store the user's entitlements:
get(/databases/$(database) / documents / users / $(request.auth.uid)).data.rank
For example, we read the user's rank in the database to authorize a write on any character:
match /characters/{character_id} {
allow update: if get(/databases/$(database)/documents/users/$(request.auth.uid)).data.rank == "Game Master";
}
Note: We should use Firebase Auth custom claims instead:
request.auth.token.rank == "Game Master";
second example
For example, we read the player's character's zone to determine if it can read the requested overworld character:
match /overworld_characters/{overworld_character} {
allow read: if get(/databases/$(database)/documents/characters/$(request.auth.uid)).data.zone == resource.data.zone;
}
Note:
- if a query matches 10 documents, the get() is run only once, so it only triggers a single read
- if a query, by its nature, could match at least one document that is not authorized by the security rule, the entire query is rejected. This forces the client to query only authorized documents. In the example, it forces the client to use a where() clause instead of fetching all overworld_characters. That is, we cannot rely on security rules to filter documents out of a broad query.
payload validation
request.resource.data is the request's payload. We can validate critical fields:
// simple check
request.resource.data.age >= 0
// check against auth uid.
request.auth.uid == request.resource.data.uid;
// check both requested document and uploaded document
allow update,delete: if
request.auth.uid == resource.data.uid
&&
request.auth.uid == request.resource.data.uid;
Alternative: We can perform validation in Cloud Functions and forbid writes coming from the client.