Here is the purpose of each document field:

expiration : a timestamp that indicates when the document should no longer be publicly accessible

: a timestamp that indicates when the document should no longer be publicly accessible owner : always contains the UID, assigned by Firebase Authentication, of the user who created the post

: always contains the UID, assigned by Firebase Authentication, of the user who created the post removed : the post was moderated by an admin and should no longer appear in any user query

: the post was moderated by an admin and should no longer appear in any user query text: self-explanatory

One thing you might want to do with these documents, from a security perspective, is to write a security rule to satisfy the requirements of the expiration field. That would look like this:

match /posts/{id} {

allow read: if resource.data.expiration > request.time;

}

This rule, on its own, enforces that users can only read documents whose expiration field later than the current time. Note that request.time contains the timestamp of time that the query was made. It’s actually the same value as a Firestore server timestamp that you would also use for writing timestamp fields.

With this rule in place, you might try to query the posts collection like this:

firestore.collection("posts").get()

and expect that it only yield the documents whose expiration is less than the current time. However, you’ll find that this query actually fails with the standard error “Missing or insufficient permissions” every single time, no matter the contents of the collection. This can be confusing at first. The important thing we’re going to explore in this post is that this query fails every time because security rules are not filters.

The key insight as to why this query fails every time is this: The query above is actually asking for every single document in the posts collection. However, the rules absolutely require that the client only ask for documents matching the given constraints in the rules, and no more.

But why this behavior? Why can’t security rules check the document contents?

It’s theoretically possible that security rules could have been implemented to filter out documents that don’t match its constraints. But that would be 1) massively inefficient, and 2) massively expensive in terms of money you pay per query. Why? If the rule really checked every matching document before sending it to the client, it would effectively cost a read for every document in the collection. For large collections, you can see how this would be a huge problem. Firestore was designed to scale massively, with potentially billions of documents in a single collection. Scanning billions of documents would be bad all around, for sure. Bad, much like how an unindexed query on a SQL database might require a table scan to read every row. So, it’s just not allowed in order to preserve scalability.

Security rules require that “queries are all or nothing”

As the quoted Firebase documentation above says, “queries are all or nothing”. This means that if the query neglects to ask for only documents allowed by the rules, then the query simply fails immediately. It doesn’t matter if there aren’t actually any documents that fail the check — the rules reject the query simply because there could be documents that fail the check.

Another way of thinking about “all or nothing” is like this. If you write a rule that makes some demands on the contents of a document, the client is obliged to match those demands by using matching filters in the query. If the query’s filter doesn’t match the rules, the query is always rejected.

(The one exception to this is for queries made by backend SDKs, such as the Firebase Admin SDK, or any of the other Cloud SDKs. Backend SDKs authenticated with a service account always bypass security rules. Only the mobile and web clients, or any request using a Firebase Auth user token are bound by security rules.)

Make the client’s query match the rules

So, what do we have to do on the client to make the query match these rules?

match /posts/{id} {

allow read: if resource.data.expiration > request.time;

}

In this case, we can add a range filter that uses the server timestamp as a lower bound on the expiration field (using JavaScript web client syntax here):

firestore

.collection("posts")

.where("expiration", ">", firestore.FieldValue.serverTimestamp())

.get()

// PASS: expiration timestamp minimum matches

This query now says “give me all the documents that expire in the future, using the server’s sense of current time”. This satisfies the rule, because the rules engine can quickly assess that the client is asking for exactly the range of documents that would be allowed by the rule. But the range of the filter doesn’t have to be exact.

Range checks must be within the bounds allowed by the rule (but not necessarily exact)

With the above rule, it’s not entirely necessary to pass in just the server timestamp token here. The rule will actually allow any range of timestamp values later than the current server timestamp, since those timestamps are also guaranteed to satisfy the rule. So assuming the client’s clock is reasonably accurate, this query to get all posts expiring in the next 24 hours is OK:

firestore

.collection("posts")

.where("expiration", ">", firestore.FieldValue.serverTimestamp())

.where("expiration", "<", new Date(Date.now() + 24*60*60*1000))

.get()

// PASS: all possible values for expiration are in bounds

But this query trying to get which posts just expired in the past hour will fail:

firestore

.collection("posts")

.where("expiration", "<=",firestore.FieldValue.serverTimestamp())

.where("expiration", ">", new Date(Date.now() — 60*60*1000))

.get()

// FAIL, expiration minimum range out of bounds

So, queries with range checks don’t have to use the exact values coded into the rule, but they do have to be within the bounds allowed by the rule.

(Side note: if you have absolutely no trust in the client’s clock for queries like this, you should route your queries through Cloud Functions or some other backend where you know its sense of time is accurate.)

Allowing multiple conditions with a logical OR

Security rules are smart enough to allow many different types of conditions that would allow a query to succeed. Let’s change the rule so that the user can also query for their own posts, no matter the expiration :

match /posts/{id} {

allow read: if

resource.data.expiration > request.time ||

resource.data.owner == request.auth.uid;

}

Now we have a logical OR ( || ) with two possible conditions that could be satisfied to allow the query to pass. The prior passing queries that used a range filter on the expiration field will still work. In addition, the following query that filters only on the current user’s UID will also pass:

firestore

.collection("posts")

.where("owner", "==", firebase.auth().currentUser.uid)

.get()

// PASS: filter for owner is sufficient

You could also add a filter to that query that asks for only posts that have already expired in the past:

firestore

.collection("posts")

.where("owner", "==", firebase.auth().currentUser.uid)

.where("expiration", "<", firestore.FieldValue.serverTimestamp())

.get()

// PASS: filter for owner is sufficient, expiration is irrelevant

This query still satisfies the rule, since it allows the current user to always access their own documents, no matter the time of expiration. Obviously, removing the owner filter (or passing a UID other than one’s own) will cause this query to fail.

Requiring multiple filters with logical AND