Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Displaying different content within a single view based on the user's role

Let's assume that we have a menu within my angular SPA application, now I want the basic options to be displayed to all of the users, such as home, about us, carrier opportunities etc.

I would also like to have several other options, such as manage users, mange posts etc, that will be displayed only to an admin.

Let's also assume that we have an API access point that provides me with the user role, or better yet, that the user role is within the object that retrieved from /api/users/me.

What would be the best way to encapsulate those management tools from being viewed by regular users?.

Is there some kind of inheritance among views? like in Django?, is there any way to hide the DOM elements from the unauthorized user?(yes, I know that it's client side).

I'd really prefer not to use different views for the menu, since it's supposed to be a generic component.

I suppose that if the answer to all my previous question is no, the question that remains is: what is the best implementation for this? a custom directive("E" + "A") say:

<limitedAccss admin>Edit page</limitedAccess>
 <limitedAccss user>view page</limitedAccess>

or perhaps just using the regular ng-show with a condition on the user object?.

like image 442
Oleg Belousov Avatar asked Aug 04 '13 13:08

Oleg Belousov


2 Answers

The solution is in this fiddle:

http://jsfiddle.net/BmQuY/3/

var app = angular.module('myApp', []);

app.service('authService', function(){

  var user = {};
  user.role = 'guest';
  return{
    getUser: function(){
      return user;
    },
    generateRoleData: function(){
      /* this is resolved before the 
         router loads the view and model.
         It needs to return a promise. */
      /* ... */
    }
  }
});

app.directive('restrict', function(authService){
    return{
        restrict: 'A',
        priority: 100000,
        scope: false,
        compile:  function(element, attr, linker){
            var accessDenied = true;
            var user = authService.getUser();

            var attributes = attr.access.split(" ");
            for(var i in attributes){
                if(user.role == attributes[i]){
                    accessDenied = false;
                }
            }


            if(accessDenied){
                element.children().remove();
                element.remove();           
            }


            return function linkFn() {
                /* Optional */
            }
        }
    }
});

if you want to use this directive with IE 7 or 8, you'll need to remove the element's children manually, otherwise an error will be throw:

  angular.forEach(element.children(), function(elm){
    try{
      elm.remove();
    }
    catch(ignore){}
  });

Example of possible usage:

<div data-restrict access='superuser admin moderator'><a href='#'>Administrative options</a></div>

Unit test using Karma + Jasmine: Attention: the done callback function is only available for Jasmine 2.0, if you are using 1.3, you should use waitsFor instead.

  describe('restrict-remove', function(){
    var scope, compile, html, elem, authService, timeout;
    html = '<span data-restrict data-access="admin recruiter scouter"></span>';
    beforeEach(function(){
      module('myApp.directives');
      module('myApp.services');
      inject(function($compile, $rootScope, $injector){
        authService = $injector.get('authService');
        authService.setRole('guest');
        scope = $rootScope.$new();
        // compile = $compile;
        timeout = $injector.get('$timeout');
        elem = $compile(html)(scope);
        elem.scope().$apply();
      });
    });
    it('should allow basic role-based content discretion', function(done){
        timeout(function(){
          expect(elem).toBeUndefined(); 
          done(); //might need a longer timeout;
        }, 0);
    });
  });
  describe('restrict-keep', function(){
    var scope, compile, html, elem, authService, timeout;
    html = '<span data-restrict data-access="admin recruiter">';
    beforeEach(function(){
      module('myApp.directives');
      module('myApp.services');
      inject(function($compile, $rootScope, $injector){
        authService = $injector.get('authService');
        timeout = $injector.get('$timeout');
        authService.setRole('admin');
        scope = $rootScope.$new();
        elem = $compile(html)(scope);
        elem.scope().$apply();
      });
    });

    it('should allow users with sufficient priviledsges to view role-restricted content', function(done){
      timeout(function(){
        expect(elem).toBeDefined();
        expect(elem.length).toEqual(1);
        done(); //might need a longer timeout;
      }, 0)
    })
  });

A generic access control directive for elements, without using ng-if(only since V1.2 - currently unstable), or ng-show which doesn't actually remove the element from the DOM.

like image 62
Oleg Belousov Avatar answered Oct 25 '22 08:10

Oleg Belousov


ng-ifis definitely the way I would do it! Just put the moderation tools throughout the view where they belong and they will appear if the user should have them. ng-show/ng-hide are ok too, if you're using a version of angular previous to 1.1.5.

Live demo! (click here)

It is VERY important that make sure your backend/server/api will not honor a request just because your js made a call for a moderator action!! Always have the server validate their authorization on each call.

like image 30
m59 Avatar answered Oct 25 '22 08:10

m59