Skip to content

AngularJS: A Simple, Flexible Filter for Per-page Authorization

2014 April 30
by Alec

One very common use case in any role-based application is to restrict certain operations to users with predefined roles. Say, we have an administration page for managing users, that only users with the “Administrator” role can access. We likely want to hide any links that go to this page from any unauthorized roles, and also prevent direct access through the URL. These could both be implemented with a simple filter:

/* Accepts either a role string such as "Author", a role expression such as
   "'Author' || 'Manager'", or even something like 
  "('Author' && 'Manager') || 'Administrator'"
*/
filter('hasRole', function($parse, Constants) {
  function evalExpression(roleName) {
    var roles = Constants.get("currentUser").roles;
    return _.findWhere(roles, {roleName: roleName}) !== undefined;
  }
 
  return function(roleExpr) {
    // Replace all instances of " with '
    roleExpr = roleExpr.replace(/"/g, "'");
    if(roleExpr.indexOf("'") < 0) {
      return evalExpression(roleExpr);
    } 
 
    var newExpr = roleExpr.replace(/'(\w+)'/g, "evalExpression('$1')");
    return $parse(newExpr)({
      evalExpression: evalExpression,
    });
  };
})

Let’s go through what’s going on above. In this example, we pull a roles array from the current user which is accessible from a generic Constants service. I have a Constants service in every Angular app I write, mainly to keep application-wide constants such as role names, common date formatting strings, and other static data provided by the server. Alternatively, you could easily add a ‘user’ parameter to the filter and pass that to the evalExpression function for additional flexibility.

In the actual returned filter implementation function, after the string.replace function, we check to see if the role expression string has a quote. If it doesn’t, we assume that the expression is just a simple role, and call evalExpression with it. This function uses underscore’s findWhere utility to compare the role with the roleName attribute on each of the user’s roles. Otherwise, if the expression does have a quote, we use a little regular expression trickery to replace all quoted text with a call to the evalExpression function, with the replaced text as the single paramater. For instance, if we use the filter like so:

$filter('hasRole')("'Planner' || 'Administrator'")

the expression in the parameter would become

"evalExpression('Planner') || evalExpression('Administrator')"

which retains the original logic in the expression. The nice thing about this implementation is that the evalExpression function can be as complex as you like, and it will still work, assuming all required parameters are provided.

Finally, we use Angular’s wonderful $parse service to evaluate the just-built expression. $parse returns a function that can be called with a scope object containing values for any variables referenced in the expression. In this case, the only variable is the evalExpression function itself.

Usage

In my last post I talked about ways to enhance Angular’s routing mechanism. This filter would be a prime candidate to use in the $routeChangeSuccess event callback discussed in that post. Something like:

// app.js
.config(['$routeProvider', function($routeProvider) {
  $routeProvider.when('/facilities/:id', {
    templateUrl: '/templates/facility/view.html', 
    controller: 'FacilityController',
    app: {
      loading: "facility",
      roles: "'Manager' || 'Administrator'"
    }
  });
})
.controller('AppController', function($rootScope, $filter, $location, Messages) {
  $rootScope.$on("$routeChangeSuccess", function(event, current, previous) {
    if(current.app.roles && !$filter('hasRole')(current.app.roles)) {
      Messages.add(Messages.error, "You are not authorized to view that page.");
      $location.path('/');
    }
    ...
  });
});

Which is just one example of a route definition which is restricted to users with either a Manager or Administrator role. The AppController is expected to be an application-wide base controller as explained here.

Another great usage is in a template itself. One caveat: Since the role expression requires each role to be in single quotes, we have to escape each single quote, like so:

<a class="btn btn-primary right" href="#/facility/{{ facility.id }}/edit"
    ng-show="!creatingFacility && ('\'Planner\' || \'Administrator\'' | hasRole)">
  Edit facility
</a>

Don’t forget to enclose the filter expression in parenthesis (as above) when it is part of another logical expression.

One Response

Trackbacks and Pingbacks

  1. The Evolution and Rise of Angular.JS - All About Web

Comments are closed.