Transcript from the "Structural Security Rules" Lesson
>> And so now let's let's go and let's say that I'm gonna skip over these expenses ones for now because it's the same kind of thing. And for the sake of time, let's get into some more structural things. So I want to make sure that a user must have a first name and an email.
[00:00:15] So now I'm starting to look at things that are more like schema. So inside of my rules file, I wanna say, allow read, write if isUserOwned, but I also will say, allow read if isUserOwned. But I'm gonna split it out and say, allow write if isUserOwned, but also also if, write some other functions so it says isUserObject And so up here I'll write a function called, isUserObject.
[00:00:52] And forth here I can check to see, okay, is the data that's coming in, does it have what the data that I want it to have? And there's multiple ways of actually doing this. There's sort of, hey, I just wanna make sure the keys exist kind of way and there's also I wanna make sure that the values the data types are what they are.
[00:01:11] So one way to do that is I can say, all right, let's return request.resource, this is the data that is being attempted to be saved to the database. And then I can call .data, and then now I have this special object called .keys. And keys is a list type so fire this this SciGirls is a full language full of all of these types that are embedded into it.
[00:01:41] And so with keys, I can call .hasAll, and I can say, make sure that it has first, and also make sure that it has email. And now, when I write that in, let's write a test for that. So I can say a user must have a email, so let's do as an authenticated user.
[00:02:12] Name is David as an email, so I will expect this to work, so let's like comment on these tests. So that's what we do at test anyways, right? We just comment them out, we don't want them anymore. Alright, and now it's the NPM run test. What's that? We have permission denied.
[00:02:34] I thought I could get there but it won't let me get there, why is that? That's interesting, well, when you're debugging these kinds of problems. One of the things you can do is you go into the FireStore emulator and we can click this thing called Requests. And this is the request monitor and while the emulator runs and while you process requests against it, it will log each and every one of these and tell you their operation.
[00:03:03] So I tried to write this one, and we can see that it even tells us on the line that it failed on, and it gives us all the information of what happened. So in this case, we can see that we look at requests.auth. Okay, I'm david_123, so that worked, I like that.
[00:03:22] So if I look at requests.resource, it has a name and an email. I call it name, that's a problem because I was gonna call it first. So it rejected it because it says, hey, you have to have all of these keys. You can have more than these keys if you want, but you have to have these keys.
[00:03:43] Sorry, okay, cool, I see where I messed up there. So now instead of name let me change that to first. Now let's save and let's run this test again, hey, look at that six test failed, I don't know why that one's running. Why is that happening? That's weird, let me look why as to that is.
[00:04:12] Well, we can see in the request.resource, in that second test we wrote, we are writing name, so, our test is now invalid. So We actually in this test, we can see that we can either change this just to say first, so it keeps the same types or we could just use the other test.
[00:04:33] I don't like to have duplicate tests because this test test one thing, whereas this test right here tests both the authenticated context and the shape of the object. So it kinda is a nice two for one, you could do them separately if you wanted to, but you always run the risk of them getting out of sync.
[00:04:50] That's it's more of a testing thing than a Firebase thing. But always be aware that sometimes you can write tests as you lock down your schema that will fail upstream. So just something important to know. Another thing we can do is we also can check types in the sense of saying hey an expense has to have a cost greater than zero.
[00:05:08] We don't want people putting negative costs in and get money back on their expense reports or something like that. So how would we do something like that? Well, the same thing with expenses. We can say okay, allow read if it's UserOwned, but we'll allow write if it's isUserOwned(request.data.UID.
[00:05:34] And I will write a function up here and say, function costs more than zero and we'll return request.resource.data.cost is greater than zero. I also could say like is number and then chain on and say request.resource.data > 0. And this is operator right here will check to see to make sure that it is that type.
[00:06:15] So if someone tried to put a cost in as a string, it would reject it. And so you can also in the same way where I'm specifying keys, I'm not checking to see if first is a string. I'm not checking to see if an email is an email, but I could do that all in here.
[00:06:28] And so instead of using keys, I could say, request.data.cost, this number and request.data.first is string. And I can even, inside of email, I can even do a regex in there and say and it matches this regex of an email. Which I can't write regex hope there's a Frontend Masters course on that because I really need to brush up on my regex.
[00:06:50] But you can do that, you can write regex and say, boom, only allow this for authorized emails. So now that we have that we'll say, && costsMoreThanZero. So I'm gonna save, and now let's see here. I will just start from scratch here. So expenses have to be user owned, so we'll say, const is testEnv.authenticatedcontext(), and we can pass in the user ID, so david_123.
[00:07:25] And then we can say, const user expenseDoc = context.firestore.doc('expenses/some expense'). I can check to see if something succeeds. And actually I wanna see if it fails, so, I'm gonna pay some field code. And I'm gonna say, expect that a cost of -11 is denied. Now, let's run our tests.
[00:08:10] And we can see that, yeah, it passes. So that's great. So I'm starting to be able to do more and more complex things. Another thing that you can do is you can specify that only certain keys have updated. So let's say it was really important to you that, hey, after your expense gets created, we do not allow that date to be changed due to whatever auditing reason.
[00:08:37] And that needs ID 79 form in order to be updated or whatever that sounds legitimate. But we wanna make sure that we can update dates, so to do that keys also has even more that we can do. So we can say function dateIsNotUpdated(). And what we can do here say, okay, requests.resource.data.keys(), and then we actually can do something every day called a diff.
[00:09:17] And with a diff, what is the syntax for diff? So with this we can say, we can call data.diff, that's a diff and then unchanged keys My gosh, where's my code? Here it is, data.diff. And then we can check the unchanged keys and we could say, hasOnly, and it could be hasOnly('first") or something like that.
[00:09:47] We only allow first to the, or in this case what I think, yeah, has only created at or something like that. So, there's also a bunch of other operators that you can use in this case, but we can say, hey, we'll only allow these ones to be updated.
[00:10:02] So it'd be like first and RX will be, would be 'cost', 'category' and 'uid'. Maybe we can allow who changes it or maybe we don't allow that. So only costs and category can change afterwards. And so with expenses I could say, allow write, but I also can say this for, if UserOwned, push like the end on that side, && costMoreThanZero() && dateIsNotUpdated().
[00:10:36] So that's one thing you can do. I'm not gonna run this test because I do want to cover one more very important bit of security rules. And that is Role Based Access Control.