Monday, March 9, 2015

Using a Route Naming Convention to Control View Access in AngularJS

Suppose for a moment that you have an AngularJS single-page application, one with view routes managed with then ngRoute module, that is used by users with different roles.  A user in your company's Sales group has access to certain areas of the application, while a user in Accounting works in other parts of the application.  And there are also some areas of the application that are common to all users.

Now, you already have the navigation menu wired up so that users only see the navigation links appropriate to their user roles.  And even if a Sales user somehow ends up in a view meant for an Accounting user, the server answering the REST calls for the data powering that view is going to check the security token sent with the request and isn't going to honor that request.  But you'd still like to keep users out of UI views that aren't meant for them.

You could do a user access check at the start of each controller, or perhaps within the resolve property of each route, but that would be repetitive and it's something you could forget to do on occasion.

What's cool about the route URLs you can construct with the ngRoute $routeProvider is that they don't have any relation to the actual file structure in your application.  You could define a route like so:


$routeProvider.
  when( '/the/moon/is/full/and/the/night/is/foggy', {
  templateUrl: 'views/fullmoon.html',
  controller: 'moonController'
 })

...and it's perfectly okay as long as the templateUrl and controller are legit.

So let's use that fact about route URLs to solve the challenge presented by having the route URL determine user access to the view attached to the route.

Pretend you have a sessionService module in your Angular application that manages the current session and stores the current user's information in a user object, and that user object has a "roles" property that is an array of all of the security roles that user has. If you add the following code in your main application module (the module named in the ng-app attribute in your application)...


.constant( 'authRoles', [ 'sales', 'accounting' ] )

.run( [ '$rootScope', '$location', 'sessionService', 'authRoles', function( $rootScope, $location, sessionService, authRoles ) {

    $rootScope.$on( '$routeChangeStart', function ( event, next, current ) {
        if( sessionService.isActive() ) {
            var targetRoute = $location.path();
            if( targetRoute.split( '/' )[1] == 'auth' ) {
                var routeRole = targetRoute.split( '/' ).length > 2 ? targetRoute.split( '/' )[2] : '';
                 if( sessionService.getUser() == undefined ) {
                    $location.path( '/login' );
                } else if ( routeRole && authRoles.indexOf( routeRole ) > -1 && sessionService.getUser().roles.indexOf( routeRole ) == -1 ) {
                    $location.path( '/unauthorized' );
                }
            }
        }
    });

 }])

...then when a route change is initiated, the requested route URL will be parsed to determine if the route is secured in any way. Any route URL beginning with "auth" is only accessible to authenticated users, and any URL where the location segment after "auth" matches one of the user roles defined in the "authRoles" constant (in this case, "sales" and "accounting") will only be accessible to a user having that role in their "roles" array.

So the given users attempting to navigate to the given routes will get the given results:

User Route Result
Unauthenticated user /home Routed to "home" view
Unauthenticated user /auth/viewAccount Routed to "/login" (login page)
User with Sales role /auth/viewAccount Routed to "viewAccount" view
User with Sales role /auth/sales/customer/1 Routed to detail page for customer 1
User with Sales role /auth/accounting/stats Routed to "/unauthorized" ("you are not authorized" page)
User with Accounting role /auth/viewAccount Routed to "viewAccount" view
User with Accounting role /auth/accounting/stats Routed to the "stats" view
User with Accounting role /auth/sales/propects Routed to "/unauthorized" ("you are not authorized" page)
User with Accounting role /auth/marketing/addAccount Routed to the "addAccount" view, as "marketing" isn't a recognized role
User with Sales and Accounting roles /auth/accounting/stats Routed to the "stats" view

 

What if you had three roles (Sales, Accounting, HR) and you had a route that you wanted to only be accessible to the Sales and Accounting users?  In that scenario, you'd either have to create a new user role that Sales and Accounting folks would both have, or you'd only secure the route with "auth" and fall back to doing a manual role check in the route's controller or resolve property.  So it's not a technique that solves every scenario, but it's a good first step.

Monday, March 2, 2015

Introducing Sparker: A Codebase Library Management Tool Showcasing AngularJS, Protractor, and Grunt Techniques

Sometimes projects take on a life of their own, and you end up with something unexpected.

I set out to create an template for CRUD-focused single page AngularJS web applications, something I and perhaps my colleagues could use as a foundation for writing new applications.  But under the momentum of self-applied scope creep, what I ended up creating was a Grunt-powered codebase library management tool, with my original template concept as the first codebase of potentially multiple foundational codebases.

After sitting back and looking at what I ended up with, I decided to name the project Sparker for two reasons:

  • I didn't want to use terms like "templates", "scaffolds", or "foundations" for the codebases housed in the project (partly because the project encourages creating demo codebases to accompany the "clean slate" template codebases).  So the codebases are organized into "sparks" and Sparker is the tool for managing them.

  • I wanted to focus on the inspirational aspect of the project:  that, at worst, the techniques in the sparks and in Sparker could "spark" ideas in other developers for how to approach certain problems.

At the end of the day, I see Sparker as a functional proof-of-concept, something individuals or particular teams can play around with or use in their shops to maintain and build off of their own spark libraries, but not something adopted for widespread use by the developer community.  It's not designed to compete with things like Yeoman or Express.js because, like I said, it didn't come out of a design.  But it was fun to develop, and I think there's value in sharing it.

Some of the things you'll find in Sparker today:

  • An example of how to organize Grunt tasks and task options into separate files, and how to execute two different sets of tasks from the same directory/Gruntfile.

  • A Grunt build task that determines which resource files to concatenate based on the <link> and <script> tags in your index.html file.

  • A technique for executing the login process and running certain sets of Protractor tests based on the user role being tested.

  • Convention-based Angular routes for restricting user access based on authentication state/user roles.

  • Example of mocking REST calls within a demo Angular application using the ngMockE2E module.

  • Examples of Angular unit tests and Protractor tests that utilize page objects.

Sparker is available on GitHub at https://github.com/bcswartz/Sparker.