Ready to get started?

Join Adocasts Plus for $8.00/mo, or sign into your account to get access to all of our lessons.

robot mascot smiling

Applying Our Server-Side Authorization Checks

In this lesson, we'll use our access controls to add authorization checks to our controllers where needed. This will help ensure members can't update, delete, or invite users.

Published
Jan 31
Duration
13m 0s

Developer, dog lover, and burrito eater. Currently teaching AdonisJS, a fully featured NodeJS framework, and running Adocasts where I post new lessons weekly. Professionally, I work with JavaScript, .Net C#, and SQL Server.

Adocasts

Burlington, KY

Get the Code

Download or explore the source code for this lesson on GitHub

Repository

Ready to get started?

Join Adocasts Plus for $8.00/mo, or sign into your account to get access to all of our lessons.

Join The Discussion! (12 Comments)

Please sign in or sign up for free to join in on the dicussion.

  1. Commented 3 months ago

    Hi Tom, I'm currently building a task management application, and I find it pretty similiar to your project in termes of "architecture". I have a workspace model that could be assimilated to your organization model. So when it comes to setting the active workspace feature I did the same thing as you did, same for ACL, abilities etc. So basically if the authenticated user is not a member of a workspace, it doesn't appear in his list of workspaces and then cannot set it as active.

    But I'm facing a problem when I tried to get access to a workspace page, that the authenticated user is not a member of by putting the workspace id in the URL. Since my user has another active workspace in which his role is ADMIN, then, the user can access the workspace even if he's not a member of it.

    I was asking myself if you managed it, if I missed something, or if it's "normal" and I need to modify how I check the user's role and stop using the active workspace ?

    Sorry, I think this is not very clear, pretty hard to explain it since I'm a bit lost in all of those concepts 😅

    1

    Please sign in or sign up for free to reply

    1. Commented 3 months ago

      Hi emilien! I think I'm following correctly, but let me know if this sounds off base.

      So, we manage this with our organizations by querying the active org through the authenticated user, which ensures the user is a member of the org. If the user sets an id of an org that they aren't a member of, our query won't find a match and will instead overwrite the user's active org to the first org found that they're a member of.

      For our application, the active organization is determined via a cookie. It sounds like you might have this set up as a route parameter.

      If that's the case, ensure you attempt to query the workspace through the authenticated user. If a workspace is not found, you'll want to either:

      1. Throw an unauthorized exception to prevent the user from working with the workspace. You can then offer some button to take them back to safety.

      2. Or, find a valid workspace for the user and redirect them to it. You'll also want to notify them that they did not have access to the previously requested workspace.

      The first option there probably being the best as it will ensure the user knows what happened. You don't want to continue onward as the URL contains an invalid id for the user, which could accidentally be used to load something or on subsequent requests sent from whatever page is rendered as a result.

      Hopefully that's at least a little on point and helps a little. If not, please feel free to share your query/flow, or a representation of it, and I can try and see if anything stands out.

      1

      Please sign in or sign up for free to reply

      1. Commented 3 months ago

        Hi Tom, thanks for your reply. After reading my first comment, I think I was not clear at all 😅

        I also use a cookie for the active workspace.

        I'll try to explain my problem a little bit better :

        • user A is member and admin of workspace A → id = 1

        • user B is member and admin of workspace B → id = 2

        Everything is fine for the moment, according to my ACL if the user A goes to '/workspaces/1' he can see the page. Same for user B if '/workspaces/2'. This URL will trigger the show() method in WorkspacesController. You won't see the use of the ACL in this method in the repo cause I did not commited, but I used it like you do in your tutorial.

        My problem is that if for some reasons, the user B goes to '/workspaces/1', he can still view the workspace, however he's not a member of it. I reproduced the same logic as you did to get the active workspace

        I probably have missed something or there is a point that I do not understand. Maybe you already pointed it out in your answer, and if that's the case, I'm sorry. But I'm a bit confused, it's the first time that I have to work with permissions.

        1

        Please sign in or sign up for free to reply

        1. Commented 3 months ago

          Alrighty, I think I see what's going on! Where your project differs is that your /workspaces/:id route merely renders a page. In our project, and even in your main branch, this instead sets the active org/workspace in our PlotMyCourse & on your main branch, then redirects back to the dashboard.

          Where I think the confusion comes in is the /:id in your /workspaces/:id route is merely symbolic since you're actually loading the workspace via the user's cookie. So, the displayed workspace is whatever is stored in the cookie and not what is provided via the route parameter id. Things do seem to still be working, the issue is that the id in the url can differ from what is actually displayed. Which, can lead to issues if you were to ever actually use the url's id for something.

          So, to prevent that possibility, I would either

          1. Stick to the cookie and get rid of the /:id in your /workspaces/:id route to instead have something like: /workspace or /workspaces/show

          2. Or, drop the cookie and instead use the id route parameter.

          To add, there isn't an issue with using /workspaces/:id/active to set the active workspace. The issue is that the new /workspaces/:id route that shows the workspace has two spots it could pull the id from; the id route parameter in the url and the cookie, and those can differ from one another.

          Here's a video walkthrough of the behavior I was seeing on your feat/workspace-roles branch. This shows how user-a could request workspace b via /workspaces/2 but actually still saw workspace a; meaning the id in the url did not match the workspace used.

          Here's another walkthrough of the behavior I saw on your main branch. In this branch everything works fine.

          Hope this helps!!

          1

          Please sign in or sign up for free to reply

          1. Commented 3 months ago

            Of course this helps, could have spent a lot of time on this. That is so obvious with your explanation… Thanks a lot. Oh I feel so dumb, I created a route with a parameter that I don't use…

            I think I also need to take time to document the flow of setting an active workspace etc and understand it much better. Completely forgot that the middleware kind of "block" access to workspaces that the user is not a member of.

            Thanks again Tom ! Loving your tutorials and your fast answers to comments ! 😊

            1

            Please sign in or sign up for free to reply

            1. Commented 3 months ago

              Awesome, glad to hear that helped clear things up! Ah, please don't feel dumb about that, it is just a small oversight. We've all done a lot worse!! 😊

              That sounds like a great plan! Having a document/diagram of how things work would definitely help. That's something I'd like to make a more concerted effort to do in-lesson.

              Anytime emilien!! Always happy to help where I can! 😊

              1

              Please sign in or sign up for free to reply

              1. Commented 3 months ago

                I've tried to make a flowchart of the middleware and how it uses both, GetActive and SetActive actions. It is related to my use case (workspace and not organization) though, but I think the logic stays the same.

                I don't know if you'll find it useful, but just in case, here it is. Let me know if there are any errors or inconsistencies. It was really interesting to do and taught me a lot, maybe I could help with this kind of thing if needed, don't hesitate, it would be a pleasure.

                1

                Please sign in or sign up for free to reply

                1. Commented 3 months ago

                  Oh nice! Yeah, this looks spot on, nicely done emilien! Thank you, I really appreciate that offer! I'm happy to hear that going through this was fruitful!! 😃

                  1

                  Please sign in or sign up for free to reply

  2. Commented 27 days ago

    Hi Tom, after creating our exceptions like ForbiddenException, I guess that the best way to use them in handler.ts with error instance of ForbiddenException but what about a custom specific exception like a DB Conflict without vine for example. It feels weird to add specific errors there with a flashErrors but wrap the action in a try catch feels messy… Also I am probably missing a clean way to handle the exception properly when I don't need to redirect the user but add an error to display.

    Just an example to illustrate:

    // action_example.ts
    throw new ConflictException('You or one of your associate already have this item registered.')
    
    // handler.ts
    async handle(error: unknown, ctx: HttpContext) {
      if (error instanceof ConflictException) {
        ctx.session.flashErrors({ [error.code!]: error.message }) // to access it using errors.E_CONFLICT
        return ctx.response.redirect().back()
      }
      return super.handle(error, ctx)
    }
    
    // conflict_exception.ts
    import { Exception } from '@adonisjs/core/exceptions'
    
    export default class ConflictException extends Exception {
      static status = 409
      static code = 'E_CONFLICT'
    }
    Copied!

    1

    Please sign in or sign up for free to reply

    1. Commented 27 days ago

      Hi memsbdm!

      Unless you're looking to handle these exceptions a specific way, the built in status page functionality of AdonisJS should suffice for these! These status pages work the same way in Inertia as they do in standard AdonisJS.

      Any status codes within the range specified via the statusPages object, which is located within your app/exceptions/handle.ts file, will render out the designated page for that status code to alert your user of the exception.

      import app from '@adonisjs/core/services/app'
      import { HttpContext, ExceptionHandler } from '@adonisjs/core/http'
      import type { StatusPageRange, StatusPageRenderer } from '@adonisjs/core/types/http'
      
      export default class HttpExceptionHandler extends ExceptionHandler {
        // ...
      
        /**
         * Status pages are used to display a custom HTML pages for certain error
         * codes. You might want to enable them in production only, but feel
         * free to enable them in development as well.
         */
        protected renderStatusPages = app.inProduction
      
        /**
         * Status pages is a collection of error code range and a callback
         * to return the HTML contents to send as a response.
         */
        protected statusPages: Record<StatusPageRange, StatusPageRenderer> = {
          '404': (error, { inertia }) => inertia.render('errors/not_found', { error }),
          '500..599': (error, { inertia }) => inertia.render('errors/server_error', { error }),
        }
      
        // ...
      }
      Copied!

      If, as shown in your example, you're looking to redirect the user back to the previous page when the exception is raised and alert them via a toast (or something similar) instead. You could create an exception with a handle method doing exactly that, then extend that exception for your ConflictException and any others you've created.

      I believe that'd look something like the below!

      // app/exceptions/exception_redirect_handler
      export default class ExceptionRedirectHandler extends Exception {
        async handle(error: unknown, ctx: HttpContext) {
          ctx.session.flashErrors({ [error.code!]: error.message }) // to access it using errors.E_CONFLICT
          return ctx.response.redirect().back()
        }
      }
      Copied!
      export default class ConflictException extends ExceptionRedirectHandler {
        static status = 409
        static code = 'E_CONFLICT'
      }
      Copied!
      • app
      • exceptions
      • conflect_exception.ts
      1

      Please sign in or sign up for free to reply

      1. Commented 26 days ago

        Thank you!!! It makes totally sense now 🫶🏼

        1

        Please sign in or sign up for free to reply

        1. Commented 26 days ago

          Anytime! Awesome, I'm happy to hear it's making sense now!! 😊

          0

          Please sign in or sign up for free to reply