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.

No comments:

Post a Comment