Lesson Description
The "Permission Services Layer" Lesson is part of the full, Permission Systems that Scale course featured in this preview video. Here's what you'd learn in this lesson:
Kyle introduces the concept of a services layer to centralize all permission logic, then walks through refactoring the app's document actions and data access layer so that authorization, validation, and database calls all live in one place rather than scattered throughout the codebase.
Transcript from the "Permission Services Layer" Lesson
[00:00:00]
>> Kyle Cook: So we've kind of seen the problems with this ad hoc system. The next thing we want to do is we want to try to centralize our permission system, and this doesn't even implement anything fancy. We're not doing role-based access control or any other fancy access control. We're just trying to centralize our permission system, because currently, as you've seen, our permissions live across our entire project.
[00:00:17]
What we want to do is we want to put them all in one place, so when we update them, we only go to one place. And the whole idea behind this is going to be a services layer. Now it doesn't really matter what you name this, this is just the name I gave it. Services layer is kind of a generic name that can be used for anything, but in our case, a service layer is essentially a call that will not only authorize that the user has permission, but it will also execute whatever we want.
[00:00:37]
So if we were to create a document, it'll first check the user has permission to create that document, and then it'll actually do the database call to actually create that document for us. So it's essentially one layer that handles all of our authorization and it handles all of our different logic for doing whatever we want to do. Now, a basic service may look something like this. This would be a create document service.
[00:00:58]
You can see it takes in the exact same parameters, for example, that our actual actions coming from our forms take in, so it takes in our data from our form as well as the project we're creating this document for, and then it does all of our authorization checks. For example, does the user have permission to actually create a document? If they don't, we throw some type of error. Then we do our validation.
[00:01:17]
Is the data being passed up from the user valid? Because as we know, we never want to trust what the client is giving to us. And then finally, we execute that command by calling on a database or whatever else we want to do. So essentially our goal is to take all of our code and we're going to put it inside this service layer, and I'm going to show you essentially an example of creating that myself, and then I'm going to let you be able to create your own type of services so you can see the differences between these different actions.
[00:01:42]
This essentially allows us to simplify our code as well. Here is what our create document action will look like when we're done, and you'll notice there are no permissions or anything inside this file. All it does is call our service, handle any errors, and then do whatever we need with the data, for example, redirecting the user, printing something to the screen. It doesn't matter. We just have data that comes back and errors that come back and then we handle those things.
[00:02:02]
We don't care anything about permissions or anything like that. So let's go ahead and actually try to refactor some of this data before we dive a little bit further. So we'll start with that create document one. We'll go and we'll create a brand new folder for this. We'll just call this services. There we go, and inside of here we'll create a brand new file. I'll just call this document.ts. This is where all of our different document related services are going to go.
[00:02:25]
Now we know that our very first one is going to be create document and in this particular workshop, I'm going to be ending every single service with the word service just so it's clear when we look at the code which ones are services and which ones aren't. You can choose however you want to architect your code in your real world project, but you want to have some way to distinguish a service-based call from a traditional database call because database calls will not be authenticated or authorized, I mean, and these services will have authorization built into them.
[00:02:52]
Now we know that we want this create document service to essentially act the same as our action for creating a document, so we can just take the project ID and the data information and copy them in as the two parameters we're going to be taking in and just make sure we import these document form values as well, which if we look at that, that's just everything coming directly from our actual document itself from the form.
[00:03:13]
There we go. Then if we look back at that page, you can see what's going on inside of here. We essentially want to emulate the exact same behavior that's happening inside of here, so I will just actually copy all of this code because we're going to want to do pretty much everything inside of here. Inside of our document. So first, let's make sure this is an async function. And we want to get whatever the current user is, and then we can throw some type of error back to the user.
[00:03:35]
If we take a look at the example here, we're just throwing an error, I believe, yeah, we're just throwing a simple error, new authentication or new error right here, super simple. Depending on your own system, you could use like errors as results or something else, but to keep this as simple as possible, we're just going to be throwing a simple error, and then inside of our action, we're wrapping everything in this fancy function I called trifunction.
[00:03:57]
All it does is it catches your errors and returns them as an error object right here with a message that is the string of the error, or it'll return to you whatever the return result of calling that function is. So it essentially just allows us to get our error as a string which we can return back to the UI and display that if we have some type of error. Now what we can do, once our user is properly authenticated, we can then come in and validate the data coming from the actual client.
[00:04:21]
So here is validating all that data from the client, and before we do that, as we know from our example, we want to make sure we authorize that the user has permission to do this. So what we can do is we can just add in a really simple check here that just says if the user.role is equal to author or the user.role is equal to admin. Actually, I believe it'd be easier if we just say if the role is not equal to author and the role is not equal to admin.
[00:04:49]
Those are the only two people that have permission, so if they don't have permission, we'll throw a new unauthorized error, or actually I think I just called it authorization error, there we go. And this is just a nice handy error that essentially returns to us a pre-coded message of missing permission, so we can see that inside of our UI if we have an issue. And since this is a permission, let's make sure we leave a comment on it so it's easy for us to come back and update as we evolve through our permission system.
[00:05:13]
Next we're updating to make sure that data is correct, and we want to make sure we throw this as an error, so we'll just say throw new error. And we're going to put that data directly inside of there like that. Now what we need to do is we need to call our create document function, so we can clean up a lot of our code. We want to call create document, which we know calls our data access layer. Currently, if we go into our data access layer, you can see it's handling permissions, but we're trying to take all of our permission code and put it in one place.
[00:05:40]
So instead of making our data access layer double duty where it handles permissions and handles database queries and mutations, we're going to make it extremely simple where it doesn't do any permission handling at all. Our data access layer is pure and all it does is call our database and create something, edit something, or get data for us. It's not going to do any permission checking at all. So we've now taken our permissions out of here, but that's OK because we've moved these permissions into this services layer, which is what we're always going to be using when we interact with our database and any other code in our system.
[00:06:12]
Now we can just return the creating of this document just like that. And if I make sure I get rid of the comment there, that should clean up all those different errors, and we've essentially created this document service, and if we take a look, it should look exactly the same as what we have here. So we've created a brand new document service that handles our permissions. It handles all of our error validation for our user input, and it does the database mutation or query for us.
[00:06:34]
Now the nice thing is we can go into our action and we can replace our code here with that service. We no longer need to worry about validating the user information because all that's already handled for us. We just need to call create document service, whoops, there we go, and we want to pass it along essentially the exact same parameters that come into this function. So we have our project ID and our data just like that.
[00:06:58]
And now you can see our action has again been simplified. We're no longer worried about permissions or handling user input, we're just calling a service, the service does all of that for us, and then we handle just the errors or the return value itself. Essentially, it just simplifies drastically what our code is doing, and we've taken permissions that were maybe scattered in multiple places and moved them into one single centralized location.
[00:07:18]
Now the nice thing is we can do this with all of the rest of the functions inside of our application. So let's do the exact same thing with update as well. The nice thing is doing this is relatively straightforward. We'll just copy this code and instead of a create action, this is going to be our update document service. And if we look here, we want to see what's being passed in as our parameters.
[00:07:37]
You can see it's almost the same. We just have a document ID as well. So let's make sure we take that in. We'll just paste those in. Otherwise the exact parameters are pretty much the same. We want to make sure we set up our permissions properly again. The only person that does not have access to editing is our viewer, so we can say if the user's role is equal to viewer, then we're going to throw some type of authorization error, and down here is where we actually want to call out to update our document itself.
[00:08:02]
So we're going to say update document coming from our data access layer. And we'll come in here and we'll pretty much copy down the exact same code that we have here. Essentially all I'm doing is taking my code in my action and I'm moving it into this service to try to simplify it as much as possible. So by pasting that down, we've gotten rid of all of our errors, and now we have essentially a service that's handling our updating and we have a service that's handling our creating as well.
[00:08:26]
Now if we go into our document, we can clean a lot of this up, so let's just come into here. Get rid of all of that code right there, make sure we pass in the things that we need, which is our document ID and our data, and I believe actually we don't even need the project ID inside this particular section, so we'll just completely get rid of that project ID because we're not using it inside this particular section.
[00:08:46]
Now we can come back into here, it looks like all of our errors have been completely handled, so all of our code for updating is now handled as well. We can essentially do the exact same thing for delete. Again, it'll be pretty much the same. We copy over the parameters we want to use, so we'll come into here, minimize this down. Copy this over, and we'll do the same thing for delete. Let's make sure we call this delete document and get it from the correct location, and again, let's make sure we copy in the parameters we need.
[00:09:14]
We don't need the project ID, we just need the document ID, so actually that's quite simple, there we go, and the only people that have access to delete are admins, so if the user's role is not equal to admin, then we're going to throw that permission error. We don't have any data to validate because they're just passing along the ID that they want to delete, and I believe we can get rid of all this data portion here and it's going to just work fine.
[00:09:34]
Give that a quick save and we go over to our document section here. You can see all we're doing is handling errors and redirecting the user. Same thing here and same thing here. Now the last step is to go into our data access layer and clean up all those permissions, because again we've now moved them out of our data access layer and put them directly inside of that services layer. So let's completely get rid of these.
[00:09:52]
We're going to completely get rid of these permissions here, and now all these functions are essentially pure functions that just call out to our database and do something with that data, whether it's creating, updating or deleting. We'll even get rid of these imports since we no longer need them. And same thing here, we have a few imports we can just clean up just to make our code a little bit cleaner.
[00:10:12]
There we go. And now we have everything we want done, let's just make sure that we call our actual service functions. There we go, so we actually have our permissions like we expect them to service, and we'll clean up those imports as well. There we go, and that's how simple it is to essentially convert over to the services model for our actions. And the really great thing about moving to this services model is now our permissions for all of this is handled in one single location.
[00:10:35]
If we want to say, hey, who is able to create a document, we just go to the document service and that's going to tell us all the permissions that this person has access to. It's still not perfectly centralized because we are spread across multiple different files, and you'll see when we start dealing with the UI we still have copy paste problems, but it's at least one step in the right direction. And really where the benefit of this service layer comes in is when you start dealing with queries to get data as well, because now we can make sure we only return the data that the user actually has access to instead of just returning all of the data and then later narrowing it down.
[00:11:05]
So let's go inside of our services, and we want to make sure we essentially reimplement all of our data access layer functions. So we'll go into this query section. We'll make sure we minimize these down, and essentially I want to create versions for these three different files that do my different permission handling for me inside them. Essentially what we want to do next though is to work on the query section, which is where I think a lot of the power for this services-based system comes in.
[00:11:25]
I'm going to copy the code for this over, but really all I'm trying to get is the actual function names themselves. Let me make sure I'm in the right folder here, my services. And this is where we're going to handle all of our queries. Like I said, I don't actually want any of the database code. I just want the function names because essentially I want them to be wrappers around the actual code we're going to be calling.
[00:11:44]
There we go. So we have get document by ID, get project documents, and get document with user info. So to be able to get a document by ID, we know that users have access to any document as long as they're logged in. So this is a relatively simple check. All we want to do is check, is the user logged in. If they are, give them that document data back so we can come into here. We can say if the user is authenticated, then they have access to the data and we'll just say that this is a permission check right here.
[00:12:10]
Then we can call out to our original data access layer for us. So get document by ID, pass it in the ID, and essentially we're just wrapping that data access layer and putting our permission on top of it. And again, our data access layer stays entirely pure where it's just calling our database and not doing anything else on top of it. We can essentially do the exact same thing with our next section for getting the project documents as well.
[00:12:33]
And this will just call out to the exact same function of the name get project documents, and of course I need to make sure I call these service, otherwise we're going to have errors, so let me just copy that down. There we go. And then let's make sure we import all of these. There we go, pass it in the project ID, and then we can do the exact same thing here, which is just getting a document with user info.
[00:13:01]
It's essentially the same thing as our previous function, but it's for specifically getting a little bit of additional user information, get documents with user info. There we go, and passed along the ID. So these queries are relatively straightforward because all we're doing is just making sure that user's logged in because for the most part our current implementation just kind of gives a wide access to people saying they can access all of the documents across all the pages, but as we start to evolve our permission system, these sections will become a lot more complicated with what we actually restrict the user from doing.
[00:13:27]
Now we don't actually need to modify this queries page because we weren't actually handling permissions in here because again, like I said, every single user could view every document as long as it's within the project that they have access to. So this is kind of your first step. The next step is to actually use this inside of our UI. So if we go to our pages folder inside of our app, so we can say app here, I want to go into our dashboard, let's just go to one of the pages, for example, our project page itself, and actually let's go to a document page.
[00:13:54]
No, project page is fine because we access the documents here. Now this get project by documents or get project documents will be automatically scoped to the documents the user has permission to, so if they're not logged in, we'll return that data saying that they don't have the permission, it'll throw an error and we'll get redirected however we need to. Now this is much more apparent though when we start dealing with projects as well.
[00:14:13]
Another thing, if we go over to the document view page, again, we don't currently have an if check in here preventing users from accessing documents that they don't have access to because they have access to all of them, but this data or this service layer would handle that for us. So what we want to do is everywhere we're accessing code from our data access layer, we want to replace that with a service.
[00:14:31]
So I'm just going to do a quick search here for everywhere we access our data access layer and replace that with our service, which will give us that automatic authentication for us or authorization, I'm sorry. So here, let's just make sure we completely remove our data access layer and replace it with that service layer. There we go. So essentially the signature for all these functions is identical, so the nice thing is it's just drag and drop replacement between them.
[00:14:55]
Same thing here, let's get rid of that one and replace it with the exact same version, but the service side of it, and then finally here we can do the exact same thing, but again replace it with the service version. So now, later, if we want to restrict what documents a user has access to, we can do that directly inside our service and it'll automatically reflect that change across all of our different pages.
Learn Straight from the Experts Who Shape the Modern Web
- 250+In-depth Courses
- Industry Leading Experts
- 24Learning Paths
- Live Interactive Workshops