Authorization in Phoenix web applications using Role Based Access Control (RBAC)

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
in this video we're gonna work on this web app you see right here called the waitlist app which is mostly functional except for one big missing feature but I'll get to the missing feature in just a moment here so you might be wondering what's this waitlist application for well one example of where this app could be used is in a restaurant where you need to place walk-in customers on a waitlist so you can serve them on a first-come first-served basis now one of the interesting things about this app is that this page we're looking at the waitlist page updates itself in real time so what exactly do I mean by real time updates well let me show you I'm gonna open up two separate browser windows with two different signed-in users and I want you to watch what happens when I add a name and a party size and one of the windows and then I click the Save button okay so basically the new party was added in both windows in real time and additionally if I click either the seat or cancel buttons the party is removed from the list in every browser that has this page open let me ask you a slightly tangental question how many programming languages do you think I had to use to create this real-time waitlist page I bet a lot of you guessed two languages obviously elixir and probably JavaScript this would be a reasonable guess but it's wrong the only language I use to create this real-time page is elixir okay so how's this possible don't you need JavaScript or some other front-end language to make rich real-time web apps well no I didn't need to use JavaScript because I'm using the relatively new Phoenix alive view library which allows you to build rich real-time user experiences but with a greatly simplified programming model using only server-side elixir by the way if you'd like to learn more about Phoenix live view you can check out my intro video that I created on it which is linked to on this page now there's one other part to this app which is the list of users that you see here this user's page is just a conventional server rendered page and really there's only two things you can do on this user page you can see all the registered users and you can edit a user if needed now obviously this a party has user authentication set up and given the fact that we're looking at users and you can see a signed in user here and you can see a sign out link here and here's interesting thing about authentication in this app it only took me about two or three minutes to set up because I'm using the excellent power authentication library you can learn more about the power thent ocation library from my video on pal that's also linked to on this page ok now that you've got a basic sense of what this simple app does I want to circle back to something I mentioned a moment ago which is that there's a big missing feature in this app so what's missing well you can probably guess based on the title for this video it's missing proper authorization features so what do I mean by authorization well let's take a quick look at authorization in a related topic authentication let's start with the working definition of what these two terms mean authentication refers to the process of proving you are who you say you are so for example the most common way of authenticating a user is by having them enter a unique identifier like an email address and then proving that they are this identity by entering a secret password now authorization is separate but related to authentication here's what I mean whereas authentication is a process that proves you are who you say you are authorization defines what you're allowed to do in fact you could say authorization is a combination of identification plus permissions okay so let's consider authorization in the context of this waitlist app imagine we're using this waitlist app in a typical restaurant and there are basically four types of users for this app one of the users will call a kiosk user and in other words it's an account you could use if you had a self-serve kiosk or customers could add their own name to the waitlist now keep in mind this kiosk user should have very limited permissions in this app in other words they should be able to add their name to this list but they shouldn't be able to click the seat or cancel buttons and they definitely shouldn't be able to do anything related to users the next type of user could be a host or hostess which can both add customers to the wait list and they can either seat or cancel a waiting list party but they shouldn't be able to view or edit users the next type of user might be a server which can only view the wait list and they can do nothing with users now the last type of user might be a manager who should be able to do pretty much everything in this app so the problem with this app is that currently every user can do everything in this app which isn't good but we're gonna fix that in this video okay so how do we fix this security related problem in other words how do we limit each of these users to only performing certain actions in this application well imagine we've got these four users shown here and we've got these permissions here that can be applied to users and parties so how do we set up each user with only the appropriate permissions well one way of handling this would be to specify a set of permissions on a user by user basis so for example you'd have to somehow pull up an individual user and say they only have permission to do these actions and you'd have to follow the same process for all three users ok this process would be totally doable when there are only four users but what if you had 50 users or a hundred users or more well that's when this type of authorization scheme starts to become unmanageable as you can probably imagine okay so what's another option that's pop fallujah below the next approach I'd consider is called role based access control which I'll briefly explain right now role based access control is similar to the first approach except it adds a level of indirection between the users and the permissions this bit of indirection is what's called roles now instead of mapping users to permissions will map roles to permissions then we'll assign each user to one or more roles by the way this app already has users mapped to four roles as I outlined a moment ago which you can see right here so the nice thing about this approach is when you've got many users you don't have to map each user to a bunch of permissions you just have to map the user to an appropriate role which already has the correct permissions this is typically an easier way to manage access control now role based access control is a pretty good option for a few different reasons such as it's pretty well known as it's been in use for around three decades and it's widely used and it's relatively simple however there are plenty of shortcomings in role based access control for example in scenarios where there are many different users with many permission permutations you might end up with roll explosion we've got so many different roles that maintenance and auditing becomes a nightmare additionally sometimes a role based access control is just insufficient so for example you might need to limit access based on other attributes such as time of day or the user's IP address and so on for these types of scenarios a common security scheme used is called attribute based access control which gives you more features but it adds a lot of complexity now keep in mind we've briefly looked at three different approaches but there are many other kinds of access control models as shown here but let me give you a bit of hard-learned advice on which approaches to consider I'll let you in on the costly mistake that I've made once or twice often when I'm working on a new project the initial security and authorization requirements are a bit vague and largely undefined I think this initial vagueness is pretty common in many projects now a couple of times in the past when I've run into this vagueness related to authorization I created a solution that could handle every scenario that I could think of but here's the thing the vast majority of those scenarios never got used and all that work turned out to be a big waste of time so here's a word of advice in the absence of clear direction start by designing an authorization system that's as simple as possible and only add features to that system when it's abundantly clear that the feature is necessary actually that's pretty good general advice as well now in my opinion implementing a role based access control system is relatively simple and often it's completely adequate and it feels like we're following the 80/20 rule in other words implementing rule based access control takes about 20 percent of the effort of a more sophisticated system and generally it covers about 80% of the use cases you'll typically need so in my opinion role based access control is a pretty good first choice and that's what we'll be implementing in this waitlist app now before we get started coding I want to set some expectations and then I want to do a bit of planning this video is designed for programmers who are already familiar with Alex are in Phoenix so if you're relatively new to these technologies it might be a bit of a struggle to follow along and you might be better off learning the basics before continuing with this video and here's a shameless plug for the newbies you should check out my course Alex are in Phoenix for beginners which is linked to on this page ok now let's do a bit of planning so given my earlier description of the types of users this waitlist app will have well design the system around the four roles shown here now there's a couple of things I want to consider next which is first combining users roles and permissions into access control rules and secondly the applying in these access control rules were appropriate for this first option here we could set up some tables and create some admin pages to allow managing the roles and permissions and so on but this is already going to be a longer video so in the interest of time I'd like to keep things as simple as possible so instead of putting this stuff in the database we're just going to define these rules in code which you'll see in just a moment here now the second consideration is how we'll apply these access control rules in other words do we apply these rules and controllers or maybe in the model or context or maybe somewhere else alright so what's the right answer to this question I mean where should we apply these access control rules well in my opinion there isn't a definitive answer to this question it's a bit of a gray area and it largely depends on the project so for example in a simple app like the one we're working on I'm inclined to apply the access control rules and controllers as it's a relatively simple solution but if I was working on an app in the healthcare industry which contains patient information I might be more inclined to apply these access control rules in the or context or maybe even in the database but these options are almost guaranteed to result in a much more complex solution so if you go these routes you'll want to make sure it's truly necessary okay so to summarize that approach we'll be taking in this video the access control rules will be defined in code at development time and the application of these access control rules will be applied in controllers when controllers are involved however in our app we've got this live view which doesn't use a controller so we'll also need to apply the access control rules in the live view somehow but I'll cover that later in the video now generally speaking the next step I'd take would be to look for a library to handle the implementation of our role based access control but I'll be honest with you authorization is one area where I rarely find a library that feels just right to me I've looked at several popular authorization libraries in elixir but none of them felt like a good fit for me now to be clear I'm not saying the existing libraries are bad no many of them looked quite good they just didn't closely match what I had in mind so we're gonna write our own simple authorization library which is actually a pretty good learning exercise so given that we're going to implement our own authorization library let's put a bit of thought into how it should work and in particular we'll look at it from a consumer point of view and in other words we'll answer the question what should be the public functions or API for our library okay so let's think about how we'll design our authorization API in terms of roles resources and actions now by rules I mean things like the kiosk post server and manager roles and when I say resources I'm talking about dinner parties or users and when I say actions I'm talking about the things that can be done to resources like maybe crud operations creating reading updating and deleting resources ok so what should the authorization API look like for creating access control rules to help answer this question let me give you a concrete example of an access control rule we might want to set up let's say we want to set up the kiosk role so that it can create new party records and read parties okay let me rephrase what I just said in rough pseudocode we want to grant the kiosk role the ability to create a party and read a party so what should this pseudocode look like in elixir well first I'll say we should have a function named grant which takes a role so grant the role kiosk and what's returned from this grant function call is a piece of authorization data which will define in a moment here next I'll pipe the result of grant to a create function which represents the allowed action and I'll pass in the resource the action is authorized on such as the party lastly I'll pipe the return value from the create function to a read function which again represents the action to allow and I'll pass in the resource this action is allowed on which again will be the party ok this code seems pretty decent grant the role kiosk the ability to create parties and read parties and keep in mind we could continue to pipe to other allowed actions on to the end of this pipeline all right we still need to define what type of data is returned from a grant and passed into and out of the create and read functions but let's circle back to the data we'll need in a moment next let's think about how we'll check or apply the access control rules that we set up with this code right here so here's an example scenario I want to consider well let's say we need to check if the kiosk role is allowed to view or read parties here's what the pseudocode might look like for this scenario can the kiosk role read parties now let's translate this into elixir code all right I like the readability this can the roll kiosk read the party resource but what should this can function do well I think it should return the access control rules for the rule kiosk so for example look at our planned API for creating the access control rules now what if we just put this expression here in a can function like this yeah I think this should work okay basically calling the can function will return our access control rule and then we can check the rule with functions like this read function which will return a boolean value indicating if the reads are allowed or not allowed okay I think these are decent example api's for creating access rules and checking the access rules but the next thing we need to do is figure out what type of data we should be using with these functions so here's what I'm thinking in terms of the data type we'll need let's say that calling the grant function returns what I'll call an authorization struct with the role field set to the passed role okay what else should be in this struct well let's look at this great function call here which should take the authorization struct as the first parameter and the resource as the second parameter so for this scenario I'm thinking the authorization struct could have a create film that's set to a map then when the create function is called will add the passed resource as a key to the map and will set its value to true similarly I think the authorization struct should have a read field that's set to a map and when we call the read function the passed resource should get added to the read map and its value should be set to true now keep in mind we can add additional fields to the authorization struct for additional actions such as updating deleting resources and so on the next thing to think about in our new authorization struct is this expression here so basically calling the cam function should return an authorization struct like this which gets passed in the other read function here which simply looks at this read field to see if there's a party resource and it'll return this value here okay hopefully this little thought exercise is giving you a sense of where we're headed so let's start coding I'll start by writing a few tests which should help solidify how our authorization API should work so I'll start by creating a new directory under test named waitlist then I'll create an authorization test file and I'll define the authorization test module next I'll import a module name to waitlist authorization which doesn't actually exist yet but we'll create it in a moment now I'll write our first test to see if a roll can read a resource and I'll assert that the following expression should be true can the roll kiosk read the resource party but to use the party schema module like this I need a Lea set off screen all right a few other tests which are pretty similar to this test except that these tests will deal with the create update and delete actions I'm gonna do one more thing as well I'm going to step out this can function and I'll build a rule just for these tests so I'll say grant this role the ability to read parties and create parties additionally I'll modify the import line right here to exclude the can function will eventually be writing in this authorization module okay so I'll run our tests which fail as expected because the authorization module doesn't exist so let's go create it first I'll define a struct which will be this struct we discussed earlier next I'll create a grant function that takes a role and it returns a new authorization struct with the role field set to the past role and of course to use this shorthand I need to alias this module now I'll write this read function which takes an authorization struct and a resource then what I want to do in this function is add this resource to the read field of the authorization struct so to do this I'll create a variable named updated read and I'll bind it to the value returned by the following expression I'll start with the authorization struct and I'll pipe it to a map get to fetch the read map then I'll update the read map by putting the resource in the read map as a new key and I'll set its value to true lastly I'll update the authorization struct calling map put passing in the authorization struct and setting the read field to the newly updated read map okay now I'll write a create function which will be similar to the read function it'll take an authorization struct and a resource then I'll create a variable named updated create which I'll bind to the result of the following expression I'll start with the authorization struct and I'll call map get to fetch the create map then I'll pipe it to a call to a map put to add in the past resource as a new key and I'll set its value to true lastly I'll call map put passing in the authorization struct and I'll update it scrape field to the newly updated create map hmm take a look at these two functions these are almost identical functions except for the field names you see here let's refactor these functions to use a helper function let me show you what I mean I'm going to write a private a helper function named put action which is going to be almost identical to both of these functions it'll take an authorization struct an action and a resource then I'll create a variable named updated action which I'll bind to the result of the following expression I'll start with the authorization struct which I'll pipe to map get and I'll fetch the action then I'll pipe the result to a call to map foot and I'll pass in the key resource and a value of true lastly I'll call map put passing in the authorization struct the action and the updated action map okay now I can get rid of both of these nearly identical function bodies and instead I'll just have each of these functions call the put action function passing in the authorizations tracked the appropriate action and the resource all right off screen I created update and delete functions which are nearly identical to the create and read functions now let's create the other read function so I'll create the read check function that returns a boolean value and it'll accept an authorization struct in the resource then in the body of this function I'll call map get passing in the authorization struct read map then the key to lookup the resource and lastly a default value of false if this resource isn't in the read map off-screen I'll go ahead and add create update and delete functions which are very similar to the read function ok I think we should be able to pass these tests let's give it a try and it worked alright I'm not gonna write tests for every other scenario because it's kind of repetitive and boring for a video so let's move on to defining the access control rules for each role in this app so in our authorization module I'll create a can function that pattern matches on the roll kiosk then I'll grant this role the ability to create parties and read parties and of course I need to alias the party schema module now off-screen all add can function clauses for the server role the host role and the manager role and I added an all function which you can see used here and implemented here ok I think we're done with the authorization module now we just need to go use it so let's start by using it in the user controller so let's start in the index function by first creating a role and binding it to the current users role next I'll import the functions from our wait list authorization module then using an if I'll say can the role read users but to use this shorthand for user I'll need to alias the wait list users user module so if this is true then we'll fetch the users and render them otherwise I'll add a flash message of type error that says you aren't authorized then I'll redirect them to somewhere hmm what do you think about this code we just wrote is it okay or not okay I mean do you like it or not what's your gut telling you personally I don't really like where this seems to be headed I mean we've barely started applying our new access control rules and it already feels overly verbose and prone to error let's undo the code I just wrote and let's approach this a different way instead of adding our authorization code to each action in our controller let's create a plug module that we can use in this controller in fact let's add this new plug to this controller right now by king plug followed by the name of our plug which we'll call authorize and I'll pass it the resource or schema module name wait list users user now let's go create this authorize plug module so I'll create an init callback which just returns the options untouched then I'll create the call callback function which takes a connection struct and the options from here so let me explain what we're going to do here in the call function now keep in mind we need three pieces of data to perform our authorization checks we need to know that users role we need to know the relevant resource and we need to know the requested action on the resource now it can access the role which is in the connection assigns current user role and we can access the resource by calling the keyword modules get function passing in the options and the keyword resource and lastly we can access the action by calling the action name function which I need to import from the Phoenix controller module next I'll call a new function name check passing in the action role and resource this check function doesn't exist but I'll create it right now so first I'll create a private check function clause matching on the index action then in the body of the function I'll say can this passed role do what okay which function should we use here well since we're dealing with the index action it's a read operation so I'll call the read function with the passed resource of course to use these two functions and need to import our authorization module off-screen we'll add a few other check function clauses for creating updating deleting and last but not least add a default check function Clause that just returns false this Clause will get called for any action that's not one of these actions basically this last function will deny all other requests by default okay so these check function classes will all return a boolean value so up here in the call function I'll pipe the boolean value returned from a call check to a new function named maybe continue and I'll pass in the connection struct this may be continued function doesn't exist but let's go create it right now so if the may be continued function is passed a true value as the first parameter then we'll just return the connection struct which allows the request to continue on to the appropriate controller action however if the may be continued function is passed a false value then I'll take the connection struct and pipe it to put flash then I'll say the flash message should be an error message that says you're not authorized to do that next I'll redirect the user to the page path index action and lastly I'll pipe the result to halt to interrupt the request now to use routes like this I need to alias the routes helper module as follows okay I think this should work let's give it a try so sign in as a user with the kiosk role and I'll try to navigate to the users and as you can see we couldn't access the list of users as you'd expect cool for good measure I'll sign out and sign in as a manager and I'll try and access users and this time it worked as expected ok the last thing we need to do is apply our authorization code to the waitlist which again is a Phoenix live view the code for our weightless live view is in waitlist web live now if you're not familiar with Phoenix live use I'd recommend watching my video on live views that's linked to on this page however I'll provide a quick overview of the important parts as it relates to what we're working on if you look at our live view there's basically three user events that can occur a form save event a seat event and a cancel event now if we scroll down a bit in the live view file you'll notice three corresponding handle event function clauses one for save events one for seed events and one for cancel events so what we need to do is add our access control code to these three callback functions as appropriate I'll start by importing the authorization module then in the save event handler I'll create a role variable and bind it to the value and socket assigns current user role then in this with expression I'll add our authorization check as the first match Clause by saying can this role create parties and I'll match on the return value saying it should be true now if the return value of this expression isn't true then float jumps to the else block or I'll match on the value false and overturn a tuple with the no reply atom and I'll put a message on the socket of type error which says you're not authorized to do this next I'll do a similar thing in the seat event handler adding a role variable and binding it to the current user role then I'll say can the role update parties and I'll match the return value on true and if it doesn't return true I'll add a false clause in the else block which returns a tuple with the no reply atom and I'll put a message on the socket that says you're not authorized to seed parties and lastly I'll do pretty much the same thing and they cancel even handler I think that should do it let's try it on the browser so I'll navigate to the waitlist with a kiosk user and all the temp to add a party and it worked now I'll try and see the party and that didn't work as you expect nor does canceling a party however if I sign out and sign in as a manager I am able to seat and cancel parties okay if you want to look through the code in this video you'll find the git repo listed below hey if you like this video please subscribe and if you're somewhat new to Lexar in phoenix and you'd like to learn more check out my course elixir in phoenix for beginners thanks for watching and I'll see you next time
Info
Channel: knowthen
Views: 6,794
Rating: 4.97193 out of 5
Keywords: Elixir, Phoenix, Authorization, Role Based Access Control
Id: 6TlcVk-1Tpc
Channel Id: undefined
Length: 27min 12sec (1632 seconds)
Published: Thu Feb 27 2020
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.