DOUG STEVENSON: Hey, everyone.
Welcome back to this series on Firebase security
rules for Cloud Firestore.
My name is Doug, and in the last two videos,
I talked about matching documents
with both regular and glob wildcards
in order to write expressions that allow access
to those documents.
So far, I've only talked about generalized read
and write access to matched documents,
like you see in the rules here.
This is good for many apps, but you
should know there are more granular rules
than just read and write.
And sometimes, it's important to distinguish between them
in order to properly control access
to suit the needs of your app.
So let's go over that.
In a previous video, you saw rules
that looked like this, that allow read access
to everyone to all documents in a collection called users.
This rule also limits the right access
to only authenticated users whose
uid matches the document's ID.
So if the user's uid matches the document ID,
they can do whatever they want with this document.
But do you really want to let your users do anything
with this document?
If you fully trust them, sure, but there's always a chance
some malicious user might try to cause problems for your app
by making changes that break the assumptions of your app.
On top of that, you might even have a bug in your code
that does something dangerous that you aren't anticipating.
Yes, unfortunately, bugs in your app
can unexpectedly destroy or mangle data,
so you might want to defend against that
using security rules.
For the purpose of data security,
it's a good idea to assume that malicious or buggy code
might make changes that you don't want.
So you should be very specific about operations
on your database that are allowed.
Just like in the previous videos,
let's say I have a collection called users
and it contains documents for each authenticated user
in my app.
I'd like users to be able to update
the contents of this document.
That's OK.
But what I really don't want them to do
is create or delete that document.
That's something I want to do on my own,
only when the user account is created or deleted.
And I'll manage that on the backend, implemented
with a Cloud Functions trigger.
For example, imagine that documents in this collection
contain a timestamp field that records when the user last
launched my app, like so.
If I want to use this field in a query
to find all users who have launched
my app since a certain point in time,
a missing lastLaunch field might make this query inaccurate,
so I clearly don't want to allow this
to happen, neither intentionally nor by accident.
OK.
Let's go back to those security rules for the users collection.
With the right access allowed in this rule,
the user is allowed to create, update, and delete
the document.
And as I said, that's more control than I want to allow.
Fortunately, write access can be broken down
into those three more granular types of access.
It looks like this.
You can see here that create, update, and delete access
can be called out individually in security rules.
Write access is the same as the combination
of create, update, delete.
And for my case, full write access
isn't what I want to allow.
So if you want the user to be able to modify their document
but not create or delete it, you should modify your rules
to look like this.
Create and delete permission are no longer
allowed here, since the default behavior
is to reject any type of access not specifically allowed.
So if the user tries to delete the document,
the Cloud Firestore SDK will generate a permission error
and the operation will fail on the client.
You will need to determine for yourself
which documents the user should be able to create, update,
and delete in your app.
The requirements will vary with each use
case and implementation, so what I'm doing here
isn't necessarily the best for your app.
You have to think it through for each of your use cases.
Oh, one more thing before moving on.
I mentioned before that I want to use
Cloud Functions to automatically create and delete
the user document.
This is OK because the Firebase Admin SDK and other Cloud
Firestore server SDKs always bypass security rules.
So even if you deny access to the client,
your server code will always have access, so
keep that in mind when designing your security rules.
OK, so we've seen that write permission breaks
down into the more granular create, update,
delete permissions.
You should know that read permission also
breaks down into the more granular
get and list permissions.
In the same way that write permission implies
create, update, delete, read permission
implies get and list.
This breakdown for reads is a bit different than for writes.
Here's how it works.
With get access, a client is able to request
a single matching document using the get operation
on a document reference when using the client SDK.
However, with only get permission,
clients can't perform queries on the collection.
You can see here that a query for all documents in the user's
collection using the collection reference
is going to fail with these rules in place.
Now, if list permission is allowed,
the client is able to query and filter the documents matched
by the rule.
Denying get access also prevents clients from individually
fetching a document by its ID.
Now you might be wondering, why would you
allow individual document gets but deny queries?
In the case of this user's collection,
maybe you don't want users to be able to discover
some other user's data simply by performing a query,
and that's a very reasonable thing to deny.
Also, you might not want to allow someone
to pull down the entire contents of a very large collection
in one query, which could be costly.
For these cases, disallowing list access
might be the best thing to do.
But if you do want to allow queries,
but only certain kinds of queries,
security rules can help you out.
You've already seen, with security rules,
there is a special global object called request that gives you
some information about the incoming request that's being
checked by security rules.
We've used that to access the authenticated user id.
Another request property is called
a query, which is an object that contains
some information about the query currently being
performed against a collection.
It doesn't apply to document gets, only queries.
And here's how you can use it.
Let's say you have a very large collection called posts
and you want to restrict the number of documents
that a client can request from it for a single query.
You can modify your rules to enforce that.
If a client app requests all the documents in the collection,
the query and the rule that allows it
might look something like this.
Now, instead of allowing the query
to read an unlimited number of documents
in one shot, which could be expensive for you
and time-consuming for the user, you
might want to deny queries like this
and only allow a maximum of 20 documents per query.
To implement that, you would change the rule
to look like this.
Notice that the rule is using request.query.limit
to check the document limit coming from the client,
and deny the request if that limit is greater than 20
or no limit is specified at all.
To get this query to work again, all we have to do
is add a valid limit.
Any value between 1 and 20 is allowed by the rule.
And with this change to the query in place,
it will be allowed.
There's one important thing to point out here.
This rule for limit doesn't actually
stop malicious code from pulling down the entire contents
of the collection.
Clients can still use the Firestore API
to paginate through the results, fetching 20 documents per page,
until the entire collection is read.
But this rule will at least stop you
from paying for an unexpected mistake with limit
in your own code.
It's possible with security rules
to control the document fields on which ordering is allowed.
You can do this by using the orderBy property
on the request.query object.
So if you only want to allow user documents
to be sorted by that lastLaunch timestamp
we saw before, in chronological order,
you can change the rule like this.
Notice that orderBy is an object with key-value pairs.
The keys are the names of the fields used for ordering,
and the values are the direction of the sort.
ASC-- A-S-C-- denotes ascending order, the default smallest
to largest, and DESC is for a descending
sort in the opposite direction.
To make this query work again, I'll
add an orderBy clause on the lastLaunch field
as required by the rule, and we're good again.
If you're thinking of using a list
rule to disallow certain documents from a query,
I'll repeat what I've said before in the series--
security rules are not filters.
You can't check the contents of each document
in a query result set to determine if it
should be given to the client.
That, unfortunately, doesn't scale.
But you can do that with a get method rule
for an individual document.
I'll say more about this in a future video in the series,
so stay tuned for that.
OK.
We've seen in this video that read and write
access in security rules both break down into more
granular types of access, which you can use as you see fit.
It's definitely worthwhile to think
about this level of control so that users are required
to interact with collections and documents
in exactly the way you expect.
As usual, be sure to click the links in the description below
to read more about security rules,
and subscribe right here to the Firebase channel
on YouTube to find out when that next video in the series
is ready.
In that video, I'll talk about the various data
types you have available in security rules and a few ways
to make use of them.
And I'll see you next time.
[MUSIC PLAYING]