loading...
Cover image for Role-based Authorization and the View

Role-based Authorization and the View

mxoliver profile image Oliver Originally published at Medium on ・4 min read

Over the past few weeks I have been working on building a client-side wikipedia-esque web application using Node called Wikology. In Wikology, all standard members can create, edit, and read public wikis. Users can also upgrade to a premium account (using the Stripe API client for payment) and create private wikis which can only be read and edited by their collaborators.

In the past, when handling role based authorization and which CRUD (create, read, update, delete) functions user role’s are authorized for, all users, whether they were signed-in or not, had read access to everything. This meant that the majority of the authorization took place between the controllers and the model. I could easily pass in a policy class to my query files and check if a user was authorized before allowing them to continue with a CRUD action, and if they weren’t, I could flash an unauthorized notice and redirect them. With Wikology, the introduction of private wikis meant that read access was not a given for all users, and thus, role authorization had to be implemented not just between the model and controller, but also in the view.

So how does one implement role based authorization for CRUD methods in the view part of an application? If you are a seasoned developer this may seem like an obvious answer, but this was my second ever client-side application and I was about to reacquaint myself with the MVC framework on a whole other level.

The key to working with the View side of the Model-View-Controller framework is to think about how you are passing information to the view.


show(req, res, next){
        wikiQueries.getWiki(req.params.id, (err, wiki) => {
            if(err || wiki == null){
                res.redirect(404, "/wikis");
            } else if(wiki.private === false || 
(wiki.private == true && wiki.userId == req.user.id)){

                    wiki.body = markdown.toHTML(wiki.body);
                    wiki.title = markdown.toHTML(wiki.title);
                    res.render("wikis/show", 
                     {wiki: wiki, collaborator: null}
                    );
            } else if (wiki.private == true) {

               collaboratorQueries.collaboratorAccess(wiki.id,
               req.user.id, (err, collaborator) => {
                    if(err || collaborator == null) {

                        req.flash("notice", "You do not have permission
                        to view this wiki.");

                        res.redirect('/wikis');

                    } else {

                        wiki.body = markdown.toHTML(wiki.body);
                        wiki.title = markdown.toHTML(wiki.title);

                        res.render("wikis/show", 
                        {wiki: wiki, collaborator: collaborators);
                    }
                })  
            }
        })
    },

This is the show method which handles the view for each individual wiki. Let’s break this down:

  1. Get the wiki — in order to do anything we need to retrieve the specific instance that matches the given wikiId.

  2. Check if the wiki is private and if so, check if we are the owner — if the wiki is public, or if we wrote the wiki, we have guaranteed read access and can go ahead and render the view. Here we are passing the wiki from step 1 in as {wiki: wiki } and the collaborators as null because they aren’t relevant to the show method if we are the owner or if the wiki is public.

  3. If it is private and we are not the owner, we need to retrieve the collaborators for this wiki — each Collaborator instance has a one-to-one relationship with User, and a one-to-many relationship with Wiki (eg. each collaborator instance only has one user and one wiki, but a wiki can have many collaborator instances). The specific query here is happening on my collaboratorQuery file (shown below) but the concept is fairly straightforward. I pass in the wikiId from step 1, as well as req.user.id which is the id of the user making the request. Notice I’m not trying to retrieve all of the collaborators for this wiki, I only need to know if the current user is a collaborator in order to determine their read access.


collaboratorAccess(wikiId, userId, callback){
        Collaborator.findOne({where: {wikiId: wikiId, userId: userId}})
        .then((collaborator) => {
            callback(null, collaborator);
        })
        .catch((err) => {
            callback(err);
        })
    }


If there is an error, or if a collaborator instance doesn’t exist between the current user and the wiki, the user is redirected and sent a message letting them know that they are not authorized to view that wiki.

However, if everything succeeds we can go ahead and call on the view to render the wiki. This time, we pass in {wiki: wiki, collaborator: collaborator} which not only passes the wiki to the view, but passes the collaborator instance that links the current user as a collaborator with the wiki. By passing the collaborator instance to the view I can make sure the current user has access to view the private wiki before it renders. Ideally the user would be redirected before they even reach the view, but if they were to enter the route manually (eg. a former collaborator who no longer has read access but knows the url route) this would double-check their authorization before rendering.

This same strategy can be used for the rest of the CRUD methods, passing in authorized users as collaborator instances in order to determine what should be rendered in the view. Other examples include, determining whether the edit and delete buttons should be visible to a given user, or which private wiki titles should appear on a user’s index page (eg. the ones that they own or collaborate on).

Whenever I am stuck on a problem, the most effective question I can ask myself is “Who knows what and how?” Essentially — Does this component have the information it needs to do what I am asking it to do? Where is that information coming from and how is it accessing that knowledge?
and remember if you’ve been staring at the same problem and getting nowhere — take a step back so you can see the forest for it’s trees.

— MxOliver

Posted on by:

mxoliver profile

Oliver

@mxoliver

JavaScript developer passionate about coding solutions for a better world. (They/Them pronouns)

Discussion

pic
Editor guide