MIKE MCDONALD: It is--- do you have a copy of "War and Peace"
or something?
This is garbage.
[MUSIC PLAYING]
Hey, everyone.
Welcome to the Firebase Pro Series.
My name is Mike McDonald.
And I'm the product manager for Firebase security rules.
I helped build security rules.
And I've secured more apps than I can count.
Today we're going to dive deeper into how security rules work
and show an advanced use case to get
you more comfortable building secure production
ready applications.
As you may already know, security rules
protect your data in both cloud storage and Cloud Firestore
from unauthorized access.
What you may not know is that it can do so much more.
Using security rules, you can define new resource types,
validate incoming data, and execute business logic.
We'll demonstrate how this works by building a simple chat
app using Cloud Firestore.
Because this is Google, we're going
to build a new group chat app.
We'll start off with some fundamentals before diving
into our advanced material.
The first thing that we'll do when
we want to start our chat app is to find
what a chat message looks like.
It's in JSON, it has a username, a user ID, a created at time
stamp, and message text.
Since we'll be writing these messages to Cloud Firestore,
we want to ensure that Firestore knows exactly what a chat
message looks like so we can store all users
and all messages in the same way.
Within Firestore, chat messages live within a chat room
collection and contain the information
that we showed before.
We're going to go ahead and write some logic
to ensure that all chat messages look the same.
First, we'll only allow creation if the message
has certain properties.
The request resource data represents our message
and we'll first ensure that the size only
contains those four properties.
So the size is going to be four, because we
have four properties.
The user name we're going to check and make sure
is a string, and then we'll replicate this for the user
ID, which is also a string, the created at,
which is going to be a time stamp,
and finally the message text, which
is also going to be a string.
This is a lot of really nice logic.
Let's actually take this logic, bundle it up, and transform it
into a function so that we can reuse it.
So we'll go ahead, copy all of that, actually just
the important pieces, and move it up into a function.
We're going to call this function isChatMessage.
And we'll pass in that message contents, so the request
resource data, which is exactly what we're
writing to Firestore.
Just adding it in code.
It's good fun.
A function is composed of a return statement and a number
of additional conditions.
So as you can see, we're checking
those exact same conditions and returning whether or not
something is going to be a function.
Using functions makes it really easy
to add additional validation logic later.
Say if we want to make a system like Twitter or SMS,
where we limit all messages to, say, 160 characters,
we can simply add an additional condition
to the end of isChatMessage message function.
Here we'll do 160 characters.
OK.
Right now anyone can read or write any message in any room.
And they can claim to be anyone.
There is absolutely no authentication checks
in our system.
We'd like to add two additional functionalities.
The first is that we can verify that a user is, in fact, who
they claim to be.
And the second one is that we want
to let members of a chat room have one of two distinct roles.
They can either be a reader, where they can only
view messages, where they can be an editor, where they can
both create and view messages.
The first problem, verifying the message
creator is who they say they are,
is relatively easy to solve.
We can simply add an additional condition
to the end of our isChatMessage statement
where we say the request resource data's user ID, which
we already defined in our message,
is going to match the auth user ID that comes in
from Firebase authentication.
That's really easy.
The second problem is going to be a little more tricky.
We like to ensure, in order to create a message,
that a user is an editor.
To do this, we'll actually create a sibling collection
within a room called users.
And these are going to have documents whose
ID is a Firebase auth user ID.
And within that document, they'll have a single role.
And that role is going to be either an editor or a reader.
You can go over to our JSON and see exactly how that's defined.
As you can see, it's literally just
a single property with a field and a value
where the value is either editor or reader.
Similar to what we did in our messages collection,
we can also validate that, when someone is creating a new user,
that the format is correct.
We'll create another function.
This one is called isUser.
We'll check that the size is one,
because there's only going to be one field called role.
And we'll check that the role property is either
editor or reader.
We don't have to check that it's a string because we're just
checking the string values here.
Once we know what roles our user has,
we can make a call to check that a user has a particular role.
For this, we'll create a new function called getRoleForUser.
we'll use the built in Firestore get
method that returns a document at a particular location
in a different part of our database.
At this point, we'll actually look to the users collection
for a particular user ID document that contains
that role and will fetch it.
From there, we can add that getRoleForUser call
to our creation check and ensure that the user, in order
to create a document, is an editor.
After that, it's easy to extend that model
to add support for readers.
We'll simply, in our allow get or list, say if getRoleForUser
is in either reader or editor.
And that would allow both readers and editors
to view individual messages or list messages.
But wait.
How does a first user in a chat room
get permissions to create any messages?
In computer science, this is a problem known as bootstrapping.
We need to enable a user to create a new room
and, at the same time, give them the permissions required
to give other users access to create messages in that room.
We can do this by adding a condition to user creation that
allows a new type of user, an admin user,
to be created if a room doesn't already exist.
Once that room exists, we'll let that admin
create new users provided that they're either
a reader or an editor.
We'll accomplish this by writing an isAdminUser function.
It's going to look really similar to our isUser function.
As a matter of fact, we'll copy and paste
the contents of our isUser function
and simply check if the role is an admin instead
of an editor or a user.
We'll then go into our users collection
and change our condition from isUser to isAdminUser.
The next thing that we want to do
is add an existence check to see if our room already exists.
To do that, we'll create a function called roomExists.
And similar to our get call, we're
going to use another call that's built into Firestore called
exists.
And exists takes a path and it returns whether or not
a document exists there.
So we'll check if any documents exist within the room
and return yes or no.
Now, things are going to get a little tricky.
We have those two conditions.
If the room doesn't exist.
We'll call not roomExists on our room.
We also want to ensure that the user performing this action
is an admin.
And lastly, we want to check that the user performing
the action is doing it on behalf of themselves.
So you can only become an admin if you yourself
are becoming the admin.
That's the first half of our condition.
The second condition is going to be what happens
when a room is already created.
And that's going to be ORed in with our existing condition.
The first thing that we'll check is
if the user is an admin in the existing room.
We can do that by calling getRole for user
and checking that that's admin.
We'll then do similar checks.
We'll call isUser.
We want to make sure that they're creating
either an editor or a reader.
And lastly, we're going to ensure
that the user doing the action is not
the user who is being created.
Right.
You don't want a case where you the admin
are giving you yourself a different set
of permissions in that room.
You want to only be able to create readers or editors who
are not yourself.
That brings us to our finished product.
It's a bare bones, but very secure group chat app.
Let's take a look at what that looks like in real life.
As you can see, on the left side of our screen
we have an app called Pro Chat.
We're all professionals here.
We're building a really secure chat app.
I can type in a user name.
In this case, I'll just use my name,
and I'll submit the chat message.
But before I do that, on the right side of the screen,
we actually have the Cloud Firestore console.
And we can see we've created rooms.
We have a public room.
And within that we have a number of users.
Well, actually, I think we only have one user and that's me.
We can take a look at that user.
And as you can see, it has a role of editor.
That means when I click Submit, I'll
be allowed to both read and write chat messages.
Let's go over, click Submit, and boom, there we have it.
Our message was added.
Again you can see in the database we added that message.
We now have a message is collection
and you can see the hello YouTube chat message there.
To prove that I'm not crazy, let's do that again.
That was really cool, wasn't it?
Submit that message.
And immediately over in the Firestore console,
you see that message added and we can validate it.
Let's go back to our users and ensure
that I'm not lying to you by changing the role to reader.
Let's try adding a message now.
See if I can still write.
Do do do.
No.
Permission denied.
That's working exactly as intended.
Within Firestore we don't have any messages.
Let's try reducing my permissions
to something that wouldn't let me read or write.
So let's actually call a new role, the no permissions role,
and update my user.
And as you can see, immediately Firestore
gave me a missing or insufficient permissions error
as well.
And as you can see when I refresh the page,
I can no longer see messages.
To prove that again I was not lying,
we can switch back to editor, I can write my name again,
and I can submit new messages.
In this case, I'll actually see all of my previous messages,
because I can now read them again.
And we'll submit that, see it added over there
on the right side, and all of our messages are back.
Great.
Now let's take a look at the Rules tab.
And we can see the exact set of rules that we already shipped.
We have the isChatMessage function, both the user
and isAdminUser functions, and the additional information
around our getRoleForUser and roomExists functions
defining our messages in the messages collection,
and also defining our users in the user collection.
As you can see, security rules are their own very flexible
and powerful language.
We covered a bunch of different security scenarios--
defining new resource types, including users
and chat messages; validating our inputs,
say requiring that all chat messages have
a text of less than 160 characters;
and ensuring that our business logic functioned properly.
Along the way, we also secured our app
for both individuals as well as groups.
If you want to learn more about security rules,
check out our official documentation.
Also leave a comment if you want us to cover a specific security
scenario in a future video.
That's all for today.
Make sure to like and subscribe and we'll
see you on the next episode of The Firebase Pro Series.