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

33,419

Solution 1

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.

Solution 2

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.

Share:
33,419
Oleg Belousov
Author by

Oleg Belousov

Founder of https://n.exchange - the fastest and easiest API for instant crypto exchange

Updated on May 22, 2020

Comments

  • Oleg Belousov
    Oleg Belousov about 4 years

    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?.

  • Ravi Ram
    Ravi Ram over 10 years
    What if the roles is an (array) object with many roles? Like roles = {'Admin', 'Members', 'Editor'}
  • m59
    m59 over 10 years
    @DavidKEgghead looks like the OP thought of that and made a directive to deal with it. I was thinking along the lines of ng-if="userIs('admin moderator contributor')
  • Oleg Belousov
    Oleg Belousov over 10 years
    I also have a unit test for this directive, I will post it right now
  • Oleg Belousov
    Oleg Belousov over 10 years
    Posted the unit-tests, please let me know if there is anything else I can help you with :)
  • abhijeet nigoskar
    abhijeet nigoskar over 9 years
    @OlegTikhonov hmm...I have a similar implementation but in my case expect(element).toBeUndefined() fails as it is actually defined. Isn't it that in your case the test passes only 'cause you're having a timeout(..) around, who's function is never called??
  • Oleg Belousov
    Oleg Belousov over 9 years
    Unfortunately, this hack does not work on newer version of Jasmine, and you need to define an async test with passing done a parameter to the test case, and then calling it inside of the timeout function. I shall post an example. Perhaps a longer timeout will be required as well.
  • abhijeet nigoskar
    abhijeet nigoskar over 9 years
    @OlegTikhonov But does it actually run?? Because I'm not getting an undefined element when I execute it, but rather it is always defined, given you store it in the elem variable. The element.remove() only removes it from some parent element, which in this case doesn't exist. Instead I had to wrap it in the html, something like '<div><span data-restrict data-access="admin recruiter scouter"></span></div>'. In the expect I then check whether the span is present or not. Am I missing something here?
  • Oleg Belousov
    Oleg Belousov over 9 years
    Tests ran for me prefectly with [email protected] with angular [email protected] [email protected] and [email protected]. Since then I suppose the APIs changed a bit, and now you have to define an async test explicitly. Other than that I don't see a problem, but if you'll suggest an edit with a more correct assertion, I will approve it.
  • LordTribual
    LordTribual over 9 years
    This approach isn't any good because it can be easily manipulated by unchecking the display: none; property. The button will then be visible and clicked. This requires server-side validation in any case even though this should be done anyways. I'd go with @OlegTikhonov 's aproach because this will remove all elements which shouldn't be seen by the current user role.
  • m59
    m59 over 9 years
    @LordTribual I suggested ng-if, so you would not be able to unhide the element. It simply wouldn't be in the DOM. The directive in Oleg's answer ought to use ng-if as I suggested rather than manually manipulating the DOM directly with .remove(). If using a version of Angular where ng-if isn't implemented, you could either shim it or just use ng-show. There's nothing to worry about if the element is just hidden rather than absent. If your server is that insecure, then anyone that knows to open the console and show the element could also just send bad requests directly to the server.
  • nir
    nir about 8 years
    Why do you remove children before removing the element? Remove the element removes the entire elements tree (element and its children) from DOM, doesn't it?
  • Oleg Belousov
    Oleg Belousov almost 8 years
    This is a hack for IE, don't ask me why, but it did not work otherwise on older IE versions.
  • teuber789
    teuber789 about 7 years
    I know this thread is old, but I have a question regarding this.
  • Oleg Belousov
    Oleg Belousov about 7 years
    Shoot, go ahead
  • teuber789
    teuber789 about 7 years
    I know this thread is old, but I have a question regarding this. The problem is that this still allows administrative content to be viewed by regular users in the browser (just open up the console and look for all the elements that have "restrict admin" tags). Is there a way to avoid this?
  • Aman Gupta
    Aman Gupta over 6 years
    This is by far the best solution..!! Is there any better way now in '17 ??
  • Martin Shishkov
    Martin Shishkov almost 6 years
    I really like this solution, however it doesn't seem to work on elements that get repeated via ng-repeat, any ideas on why that might be?