Thursday, April 11, 2013

Improved DotNetNuke Role Checking

I try to avoid magic strings at all costs and I try to always write unit-testable code.  The DNN 7 framework UserInfo class relies on magic strings (role names) and is not easily unit tested because its methods are not virtual nor does UserInfo implement an interface.  I developed a small reusable framework to overcome both of these DNN UserInfo shortcomings.

The UserInfoAdapter class adapts UserInfo so that unit testing is simplified for methods relying on a UserInfo instance.

  1. /// <summary>
  2. /// Adapts the DNN UserInfo class so that methods relying on UserInfo
  3. /// can be unit-tested.
  4. /// </summary>
  5. public class UserInfoAdapter
  6. {
  7.     private readonly UserInfo _userInfo;
  8.  
  9.     /// <summary>
  10.     /// Construct a new UserInfoAdapter and provide the UserInfo
  11.     /// instance to be adapted.
  12.     /// </summary>
  13.     /// <param name="userInfo">UserInfo instance.</param>
  14.     public UserInfoAdapter(UserInfo userInfo)
  15.     {
  16.         _userInfo = userInfo;
  17.     }
  18.  
  19.     internal UserInfoAdapter()
  20.         : this(null)
  21.     {
  22.     }
  23.  
  24.     /// <summary>
  25.     /// Gets a boolean indicating if the adapted UserInfo instance is null.
  26.     /// </summary>
  27.     public virtual bool IsUserInfoNull { get { return _userInfo == null; } }
  28.  
  29.     /// <summary>
  30.     /// Gets the Roles string array from the adapted UserInfo instance.
  31.     /// </summary>
  32.     public virtual string[] Roles { get { return _userInfo.Roles; } }
  33.  
  34.     /// <summary>
  35.     /// Gets the IsSuperUser boolean value from the adapted UserInfo instance.
  36.     /// </summary>
  37.     public virtual bool IsSuperUser { get { return _userInfo.IsSuperUser; } }
  38. }

The RoleName base class provides a mechanism for overcoming role name magic strings by encapsulating each DNN role name in one place.  Each implementation of RoleName provides a Role value that is identical to a DNN Roles table RoleName value.

  1. /// <summary>
  2. /// Provides testable mechanism for retrieving user role information
  3. /// along with tight database role integration.
  4. /// </summary>
  5. public abstract class RoleName
  6. {
  7.     /// <summary>
  8.     /// Gets the role associated with the RoleName instance.
  9.     /// Returned value should match exactly to a DNN database Roles.RoleName value.
  10.     /// </summary>
  11.     public abstract string Role { get; }
  12.  
  13.     /// <summary>
  14.     /// Gets false to indicate that this is not a SuperUser role.
  15.     /// </summary>
  16.     public virtual bool IsSuperUserRole { get { return false; } }
  17. }

Here are sample RoleName implementations for the Admin role and SuperUser status.

  1. /// <summary>
  2. /// SuperUser role.
  3. /// </summary>
  4. public class SuperUserRole : RoleName
  5. {
  6.     /// <summary>
  7.     /// Gets a value indicating a SuperUser role.  This is not a
  8.     /// DNN Roles.RoleName value.
  9.     /// </summary>
  10.     public override string Role { get { return "SuperUser"; } }
  11.  
  12.     /// <summary>
  13.     /// Gets true to indicate that this is a SuperUser role.
  14.     /// </summary>
  15.     public override bool IsSuperUserRole { get { return true; } }
  16. }
  17.  
  18. /// <summary>
  19. /// Administrators role.
  20. /// </summary>
  21. public class AdminRole : RoleName
  22. {
  23.     /// <summary>
  24.     /// Gets the DNN database Roles.RoleName value for an Administrator user.
  25.     /// </summary>
  26.     public override string Role { get { return "Administrators"; } }
  27. }

RoleNames can be configured to work like enumerators by declaring them as static instances:

  1. public static class Role
  2. {
  3.     private static RoleName _superUser = new SuperUserRole();
  4.     public static RoleName SuperUser { get { return _superUser; } }
  5.  
  6.     private static RoleName _admin = new AdminRole();
  7.     public static RoleName Admin { get { return _admin; } }
  8.  
  9.     private static RoleName _regUser = new RegisteredUserRole();
  10.     public static RoleName RegisteredUser { get { return _regUser; } }
  11.  
  12.     private static RoleName _subscriber = new SubscriberRole();
  13.     public static RoleName Subscriber { get { return _subscriber; } }
  14. }

With these classes now in place, it is easy to write an IsInRole extension method override for the DNN UserInfo class.  I also created a RoleComparer implementation of IEqualityComparer<string> to ensure role equality is case-insensitive.

  1. public static class UserInfoUtil
  2. {
  3.     /// <summary>
  4.     /// Returns a boolean value indicating whether or not the User
  5.     /// is in one of the supplied roles.
  6.     /// </summary>
  7.     /// <param name="user">A UserInfo instance.</param>
  8.     /// <param name="roles">One or more roles.</param>
  9.     /// <returns>true if the User is in one or more of the supplied roles;
  10.     /// otherwise returns false.</returns>
  11.     public static bool IsInRole(this UserInfo user, params RoleName[] roles)
  12.     {
  13.         return IsInRole(new UserInfoAdapter(user), roles,
  14.             IsSuperUserRole, IsNonSuperUserRole);
  15.     }
  16.  
  17.     internal static bool IsInRole(UserInfoAdapter aUser, RoleName[] roles,
  18.         Func<UserInfoAdapter, RoleName[], bool> isSuperUserRole,
  19.         Func<UserInfoAdapter, RoleName[], bool> isNonSuperUserRole)
  20.     {
  21.         if (aUser.IsUserInfoNull) return false;
  22.         bool isInRole = isSuperUserRole(aUser, roles);
  23.         if (!isInRole)
  24.             isInRole = isNonSuperUserRole(aUser, roles);
  25.         return isInRole;
  26.     }
  27.  
  28.     internal static bool IsSuperUserRole(UserInfoAdapter aUser, RoleName[] roles)
  29.     {
  30.         RoleName supervisor = roles.FirstOrDefault(r => r.IsSuperUserRole == true);
  31.         if (supervisor != null)
  32.         {
  33.             return aUser.IsSuperUser;
  34.         }
  35.         else return false;
  36.     }
  37.  
  38.     internal static bool IsNonSuperUserRole(UserInfoAdapter aUser, RoleName[] roles)
  39.     {
  40.         bool isInRole = false;
  41.         string[] userRoles = aUser.Roles;
  42.         int i = 0;
  43.         while (i < roles.Length && !isInRole)
  44.         {
  45.             isInRole = userRoles.Contains(roles[i].Role, new RoleComparer());
  46.             i++;
  47.         }
  48.         return isInRole;
  49.     }
  50. }
  51.  
  52. /// <summary>
  53. /// Compares two roles for equality.
  54. /// </summary>
  55. internal class RoleComparer : IEqualityComparer<string>
  56. {
  57.     /// <summary>
  58.     /// Returns a boolean value indicating equality between
  59.     /// two roles.
  60.     /// </summary>
  61.     /// <param name="roleOne">First role.</param>
  62.     /// <param name="roleTwo">Second role.</param>
  63.     /// <returns>true if the two roles are equal using a case-insensitive
  64.     /// test; otherwise returns false.</returns>
  65.     public bool Equals(string roleOne, string roleTwo)
  66.     {
  67.         return (string.Compare(roleOne, roleTwo, true) == 0);
  68.     }
  69.  
  70.     /// <summary>
  71.     /// Returns a hash code for the role.
  72.     /// </summary>
  73.     /// <param name="role">A user role.</param>
  74.     /// <returns>The hash code of the role string.</returns>
  75.     public int GetHashCode(string role)
  76.     {
  77.         return role.GetHashCode();
  78.     }
  79. }

Here are the unit tests for the UserInfoUtil methods.  These tests use the NUnit and Rhino.Mocks frameworks.

  1. [TestFixture]
  2. public class UserInfoUtilTests
  3. {
  4.     #region Variables and Constants
  5.     private UserInfoAdapter _stubUserInfo;
  6.     private RoleName _suRole;
  7.     private RoleName _adminRole;
  8.     private RoleName _otherRole;
  9.     private Func<UserInfoAdapter, RoleName[], bool> _stubIsSuperUserRole;
  10.     private Func<UserInfoAdapter, RoleName[], bool> _stubIsNonSuperUserRole;
  11.     #endregion
  12.  
  13.  
  14.     #region Setup
  15.  
  16.     [TestFixtureSetUp]
  17.     public void InitFixture()
  18.     {
  19.         _suRole = new TestSuperUserRole();
  20.         _adminRole = new TestAdminRole();
  21.         _otherRole = new TestOtherRole();
  22.     }
  23.  
  24.     [SetUp]
  25.     public void InitTest()
  26.     {
  27.         _stubUserInfo = MockRepository.GenerateStub<UserInfoAdapter>();
  28.         _stubUserInfo.Stub(ui => ui.IsUserInfoNull).Return(false);
  29.  
  30.         _stubIsSuperUserRole = MockRepository.GenerateStub<Func<UserInfoAdapter, RoleName[], bool>>();
  31.         _stubIsNonSuperUserRole = MockRepository.GenerateStub<Func<UserInfoAdapter, RoleName[], bool>>();
  32.     }
  33.  
  34.     #endregion
  35.  
  36.  
  37.     #region Unit Tests
  38.  
  39.     [Test]
  40.     public void IsInRole_IsUserInfoNull_True_Returns_False()
  41.     {
  42.         var stubUserInfo = MockRepository.GenerateStub<UserInfoAdapter>();
  43.         stubUserInfo.Stub(ui => ui.IsUserInfoNull).Return(true);
  44.         Assert.That(UserInfoUtil.IsInRole(stubUserInfo, null, _stubIsSuperUserRole, _stubIsNonSuperUserRole),
  45.             Is.EqualTo(false));
  46.     }
  47.  
  48.     [Test]
  49.     public void IsInRole_IsUserInfoNull_False_IsInSuperUserRole_True_Returns_True()
  50.     {
  51.         _stubIsSuperUserRole.Stub(f => f(Arg<UserInfoAdapter>.Is.Equal(_stubUserInfo), Arg<RoleName[]>.Is.Anything)).Return(true);
  52.         Assert.That(UserInfoUtil.IsInRole(_stubUserInfo, null, _stubIsSuperUserRole, _stubIsNonSuperUserRole),
  53.             Is.EqualTo(true));
  54.     }
  55.  
  56.     [TestCase(true)]
  57.     [TestCase(false)]
  58.     public void IsInRole_IsUserInfoNull_False_IsInSuperUserRole_False_Returns_IsNonSuperUserRole_Value(bool isNonSuperUserRole)
  59.     {
  60.         _stubIsSuperUserRole.Stub(f => f(Arg<UserInfoAdapter>.Is.Equal(_stubUserInfo), Arg<RoleName[]>.Is.Anything)).Return(false);
  61.         _stubIsNonSuperUserRole.Stub(f => f(Arg<UserInfoAdapter>.Is.Equal(_stubUserInfo), Arg<RoleName[]>.Is.Anything)).Return(isNonSuperUserRole);
  62.         Assert.That(UserInfoUtil.IsInRole(_stubUserInfo, null, _stubIsSuperUserRole, _stubIsNonSuperUserRole),
  63.             Is.EqualTo(isNonSuperUserRole));
  64.     }
  65.                 
  66.     [Test]
  67.     public void IsSuperUserRole_SuperUserRole_Not_Provided_Returns_False()
  68.     {
  69.         Assert.That(UserInfoUtil.IsSuperUserRole(_stubUserInfo, new RoleName[] { _adminRole, _otherRole }),
  70.             Is.EqualTo(false));
  71.     }
  72.  
  73.     [TestCase(true)]
  74.     [TestCase(false)]
  75.     public void IsSuperUserRole_SuperUserRole_Provided_Returns_IsSuperUser_Value_From_User(bool isSuperUser)
  76.     {
  77.         _stubUserInfo.Stub(ui => ui.IsSuperUser).Return(isSuperUser);
  78.         Assert.That(UserInfoUtil.IsSuperUserRole(_stubUserInfo, new RoleName[] { _suRole }),
  79.             Is.EqualTo(isSuperUser));
  80.         Assert.That(UserInfoUtil.IsSuperUserRole(_stubUserInfo, new RoleName[] { _suRole, _adminRole, _otherRole }),
  81.             Is.EqualTo(isSuperUser));
  82.     }
  83.         
  84.     [Test]
  85.     public void IsNonSuperUserRole_User_Not_In_Provided_Roles_Returns_False()
  86.     {
  87.         _stubUserInfo.Stub(ui => ui.Roles).Return(new string[] { "X", "Y" });
  88.         Assert.That(UserInfoUtil.IsNonSuperUserRole(_stubUserInfo, new RoleName[] { _adminRole, _otherRole }),
  89.             Is.EqualTo(false));
  90.     }
  91.  
  92.     [Test]
  93.     public void IsNonSuperUserRole_User_In_Provided_Roles_Regardless_Of_Case_Returns_True()
  94.     {
  95.         _stubUserInfo.Stub(ui => ui.Roles).Return(new string[] { "A", "o" });
  96.         Assert.That(UserInfoUtil.IsNonSuperUserRole(_stubUserInfo, new RoleName[] { _adminRole, _otherRole }),
  97.             Is.EqualTo(true));
  98.         Assert.That(UserInfoUtil.IsNonSuperUserRole(_stubUserInfo, new RoleName[] { _adminRole }),
  99.             Is.EqualTo(true));
  100.         Assert.That(UserInfoUtil.IsNonSuperUserRole(_stubUserInfo, new RoleName[] { _otherRole }),
  101.             Is.EqualTo(true));
  102.     }
  103.  
  104.     #endregion
  105.  
  106.     public class TestSuperUserRole : RoleName
  107.     {
  108.         public override string Role { get { return "SU"; } }
  109.         public override bool IsSuperUserRole { get { return true; } }
  110.     }
  111.  
  112.     public class TestAdminRole : RoleName
  113.     {
  114.         public override string Role { get { return "A"; } }
  115.     }
  116.  
  117.     public class TestOtherRole : RoleName
  118.     {
  119.         public override string Role { get { return "O"; } }
  120.     }
  121. }

Here is an example of how to implement.  This method returns true if the current DNN user is either a SuperUser or an Administrator.

  1. public bool HasAdminRole()
  2. {
  3.     return DotNetNuke.Entities.Users.UserController.GetCurrentUserInfo().IsInRole(Role.SuperUser, Role.Admin);
  4. }

No comments:

Post a Comment