12
\$\begingroup\$

I am trying to provide a fluent API for authorization based on roles. As you will see I separated my implementation in two related Interfaces the Session and the Query. The session provides all roles, permissions and user information, so one may query it later via the Query object.

I also separated the implementation in two concepts of roles, which are "normal" roles (via SimpleRbacSession) and parameterised roles (via SpecializedRbacSession). In "normal" roles, one simply states that the user have it. In the parameterised roles one states that user have a role for one specific resource. Consider the following examples:

All teachers may lecture a lesson. Here the role would be teachers and the action would be lecture. This is a "normal" role

All teachers of a subject can publish the exam results of that subject in college platform. In this case you need to provide which subject that is taught by the Teacher. This is a parameterized role because one needs to provide the resource subject.

Please note that none of either implementations have a role hierarchy support (yet).

This code block contains all interfaces and some delegate definitions as well.

public delegate IEnumerable<string> GetUserPermissions(IPrincipal user, object resource);
public delegate IEnumerable<string> GetUserRoles(IPrincipal user, object resource);
public delegate bool IsUserInRole(IPrincipal user, object resource);

public interface IRbacSession
{
   IDictionary<string, Predicate<IPrincipal>> UserRoles { get; }
   IEnumerable<Role> RolePermissions { get; }
   IRbacQuery Query { get; }
   void AddPermission(string roleName, string action);
   void UserIsInRoleIf(string role, Predicate<IPrincipal> predicate);
}


public interface ISpecializedRbacSession : IRbacSession
{
   IDictionary<Type, GetUserRoles> UserRolesForType { get; }
   IDictionary<Type, GetUserPermissions> UserPermissions { get; }
   new ISpecializedRbacQuery Query { get; }
   void AddUserRoleForTypeIf<T>(string role, IsUserInRole predicate);
   void AddPermission<T>(string roleName, string action);
}
public interface IRbacQuery
{
   IEnumerable<string> GetUserRoles(IPrincipal user);
   IEnumerable<string> GetUserPermissions(IPrincipal user); 
   bool IsUserInRole(IPrincipal user, string role);
   bool IsUserAbleTo(IPrincipal user, string action);
   IEnumerable<string> GetRolePermissions(string roleName);
   bool IsRoleAbleTo(string roleName, string action);
}

public interface ISpecializedRbacQuery : IRbacQuery
{
   IEnumerable<string> GetUserRoles<T>(IPrincipal user, T resource);
   IEnumerable<string> GetUserPermissions<T>(IPrincipal user, T resource); 
   bool IsUserInRole<T>(IPrincipal user, string role, T resource);
   bool IsUserAbleTo<T>(IPrincipal user, string action, T resource);
}

The following code block contains some utilitary methods used on the implementation.

public class DoubleKeyDictionary<K1, K2, V>: Dictionary<K1, IDictionary<K2, V>>
{
   private IEqualityComparer<K2> keyComparer; 
   public DoubleKeyDictionary(IEqualityComparer<K1> key1Comparer, IEqualityComparer<K2> key2Comparer):base(key1Comparer)
   {
       this.keyComparer = key2Comparer;
   }
   public DoubleKeyDictionary() { } 
   public V this[K1 key1,K2 key2]
   {
       get { return this[key1][key2]; }
       set
       {
           this[key1][key2] = value;
       }
   }

   public void Add(K1 k1, K2 k2, V v)
   {
       if (this[k1] == null)
       {
           this[k1] = new Dictionary<K2, V>(keyComparer);
       }
       this[k1].Add(k2, v);
   }
}

public static class Utils{
    public static V TryGetOrValue<K, V>(this IDictionary<K, V> dictionary, K key, V value) where V : class
    {
        V ret;
        if (dictionary.TryGetValue(key, out ret))
            return ret ?? value;
        return value;
    }

    public static V? TryGetOrValue<K, V>(this IDictionary<K, V?> dictionary, K key, V? value) where V : struct
    {
        V? ret;
        if (dictionary.TryGetValue(key, out ret))
            return ret ?? value;
        return value;
    }

    public static V TryGetOrAdd<K, V>(this IDictionary<K, V> dictionary, K key, V value) where V : class
    {
        V ret;
        if (dictionary.TryGetValue(key, out ret))
            return ret ?? value;
        dictionary.Add(key, value);
        return value;
    }

    public static V TryGetOrEmpty<K, V>(this IDictionary<K, V> dictionary, K key) where V : class
    {
        return dictionary.TryGetOrValue(key, null);
    }

    public static V? TryGetOrEmpty<K, V>(this IDictionary<K, V?> dictionary, K key) where V : struct
    {
        return dictionary.TryGetOrValue(key, null);
    }

    public static V Memorize<V>(this Func<V> eval) where V : class
    {
        V v = null;
        return new Func<V>(() => v ?? (v = eval()))();
    }
}

This is the impelemtantion of the non specialized RBAC version:

Edit: I forgot to include the Role class

public class Role
{
   public Role(string name)
   {
       this.Name = name;
       this.Actions = new HashSet<string>(StringComparer.InvariantCultureIgnoreCase);
   }
   public string Name { get; private set; }

   public ICollection<string> Actions { get; private set; }
}

public class SimpleRbacQuery : IRbacQuery
{
   protected readonly IRbacSession session;

   public SimpleRbacQuery(IRbacSession session)
   {
       this.session = session;
   }

   public virtual IEnumerable<string> GetUserRoles(IPrincipal user)
   {
       return session.UserRoles
           .Where(role => role.Value(user))
           .Select(role => role.Key);
   }

   public virtual IEnumerable<string> GetUserPermissions(IPrincipal user)
   {
       return GetUserRoles(user).SelectMany(GetRolePermissions);
   }

   public virtual bool IsUserInRole(IPrincipal user, string role)
   {
       var userRole = session.UserRoles.TryGetOrEmpty(role);
       if (userRole == null)
       {
           throw new KeyNotFoundException(string.Format("The roleName {0} is not defined", role));
       }
       return userRole(user);
   }

   public bool IsUserAbleTo(IPrincipal principal, string action)
   {
       return GetUserPermissions(principal).Contains(action);
   }

   public virtual IEnumerable<string> GetRolePermissions(string roleName)
   {
       Role role = session.RolePermissions.SingleOrDefault(r => r.Name == roleName);
       if (role == null)
       {
           return role.Actions;
       }
       return new string[0];
   }

   public virtual bool IsRoleAbleTo(string roleName, string action)
   {
       return GetRolePermissions(roleName).Contains(action);
   }
}

public class SimpleRbacSession : IRbacSession
{
   private readonly IDictionary<string, Predicate<IPrincipal>> userRoles;
   private readonly ICollection<Role> rolePermissions;
   public IRbacQuery Query
   {
       get { return Utils.Memorize(() => new SimpleRbacQuery(this)); }
   }
   public SimpleRbacSession()
   {
       userRoles = new Dictionary<string, Predicate<IPrincipal>>();
       rolePermissions = new HashSet<Role>();
   }

   IDictionary<string, Predicate<IPrincipal>> IRbacSession.UserRoles
   {
       get
       {
           return userRoles;
       }
   }

   IEnumerable<Role> IRbacSession.RolePermissions
   {
       get
       {
           return rolePermissions;
       }
   }

   public void AddPermission(string roleName, string action)
   {
       Role role = rolePermissions.SingleOrDefault(r => r.Name == roleName);
       if (role == null)
       {
           role = new Role(roleName);
       }
       role.Actions.Add(action);
   }

   public void UserIsInRoleIf(string role, Predicate<IPrincipal> predicate)
   {
       userRoles.Add(role, predicate);
   }
}

And the implementation of specialized version:

public class SpecializedRbacQuery : SimpleRbacQuery, ISpecializedRbacQuery
{ 
   public SpecializedRbacQuery(ISpecializedRbacSession session) : base(session)
   {

   }

   private ISpecializedRbacSession Session
   {
       get { return (ISpecializedRbacSession)session; }
   }

   public virtual IEnumerable<string> GetUserRoles<T>(IPrincipal user, T resource)
   {
       var userRoles = Session.UserRolesForType.TryGetOrEmpty(typeof (T));
       if (userRoles == null)
       {
           return new string[0];
       }
       return userRoles(user, resource);
   }

   public virtual bool IsUserInRole<T>(IPrincipal user, string role, T resource)
   {
       return GetUserRoles(user, resource).Contains(role);
   }

   public virtual IEnumerable<string> GetUserPermissions<T>(IPrincipal user, T resource)
   {
       if (!Session.UserPermissions.ContainsKey(typeof(T)))
       {
           return new string[0];
       }
       return Session.UserPermissions[typeof(T)](user, resource);
   }

   public virtual bool IsUserAbleTo<T>(IPrincipal user, string action, T resource)
   {
       return GetUserPermissions(user, resource).Contains(action);
   }
}
public class SpecializedRbacSession : SimpleRbacSession, ISpecializedRbacSession
{
   private class MyQuery : SpecializedRbacQuery
   {
       public MyQuery(SpecializedRbacSession session)
           : base(session)
       {
       }

       private SpecializedRbacSession Session
       {
           get { return (SpecializedRbacSession)session; }
       }

       public override bool IsUserInRole<T>(IPrincipal user, string role, T resource)
       {
           var rolesForType = Session.roleAssignment.TryGetOrEmpty(typeof(T));
           if (rolesForType == null)
           {
               return false;
           }
           var assignment = rolesForType.TryGetOrEmpty(role);
           if (assignment == null)
           {
               return false;
           }
           return assignment(user, resource);
       }
   }
   private readonly IDictionary<Type, IDictionary<string, IsUserInRole>> roleAssignment =
       new DoubleKeyDictionary<Type, string, IsUserInRole>(null, StringComparer.InvariantCultureIgnoreCase);

   private readonly IDictionary<Type, ICollection<Role>> permissionAssignment = new Dictionary<Type, ICollection<Role>>();
   IDictionary<Type, GetUserRoles> ISpecializedRbacSession.UserRolesForType
   {
       get { return GetUserRolesFromAssignment(); }
   }

   IDictionary<Type, GetUserPermissions> ISpecializedRbacSession.UserPermissions
   {
       get
       {
           return GetUserPermissionsFromAssignment();
       }
   }

   public IDictionary<Type, IDictionary<string, IsUserInRole>> RoleAssignment
   {
       get { return roleAssignment; }
   }

   public void AddUserRoleForTypeIf<T>(string role, IsUserInRole predicate)
   {
       IDictionary<string, IsUserInRole> roleAssign;
       if (!roleAssignment.ContainsKey(typeof (T)))
       {
           roleAssign = new Dictionary<string, IsUserInRole>();
           roleAssignment.Add(typeof (T), roleAssign);
       }
       else
       {
           roleAssign = roleAssignment[typeof (T)];
       }
       roleAssign.Add(role, predicate);
   }

   public void AddPermission<T>(string roleName, string action)
   {
       ICollection<Role> roles = permissionAssignment.TryGetOrAdd(typeof (T), new HashSet<Role>());
       Role role = roles.SingleOrDefault(r => r.Name == action);
       if (role != null)
       {
           role.Actions.Add(action);
       }
       else
       {
           role = new Role(roleName);
           role.Actions.Add(action);
           roles.Add(role);
       }
   }

   private IDictionary<Type, GetUserPermissions> GetUserPermissionsFromAssignment()
   {
       IDictionary<Type, GetUserPermissions> userPermissions = new Dictionary<Type, GetUserPermissions>();
       IDictionary<Type, GetUserRoles> userRole = GetUserRolesFromAssignment();
       foreach (var permission in permissionAssignment)
       {
           var permissionContext = permission;
           GetUserPermissions getUserPermissions = (user, resource) =>
           {
               //for every roleName that user have check their permissions
               return userRole[permissionContext.Key](user, resource)
                       .SelectMany(role => permissionContext.Value.SelectMany(r => r.Actions));
           };
           userPermissions.Add(permission.Key, getUserPermissions);
       }
       return userPermissions;
   }

   private IDictionary<Type, GetUserRoles> GetUserRolesFromAssignment()
   {
       IDictionary<Type, GetUserRoles> userRoles = new Dictionary<Type, GetUserRoles>();
       foreach (var assignment in roleAssignment)
       {
           var assignmentContext = assignment;
           GetUserRoles getUserRoles = (user, resource) =>
           {
               return assignmentContext.Value
                   .Where(e => e.Value(user, resource))
                   .Select(e => e.Key);
           };
           userRoles.Add(assignment.Key, getUserRoles);
       }
       return userRoles;
   }

   public new ISpecializedRbacQuery Query
   {
       get
       {
           return Utils.Memorize(() => new SpecializedRbacQuery(this));
       }
   }
}

And finally the fluent API for using it:

public class Rbac
{
    public Rbac(IRbacSession session)
    {
        User = new User(session);
        What = new What(session);
        Is = new Is(session);
        Do = new Do(session);
        Can = new Can(session);
    }
    public User User { get; private set; }
    public What What { get; private set; }
    public Is Is { get; private set; }
    public Do Do { get; private set; }
    public Can Can { get; private set; }

}
public class User
{
    private readonly IRbacSession session;
    internal User(IRbacSession session)
    {
        this.session = session;
    }
    public UserRole Is(string role)
    {
        return new UserRole(session, role);
    }
    [Browsable(false)]
    [EditorBrowsable(EditorBrowsableState.Never)]
    public class UserRole
    {
        internal string Role { get; private set; }
        internal IRbacSession Session { get; private set; }
        internal UserRole(IRbacSession session, string role)
        {
            this.Session = session;
            this.Role = role;
        }

        public void If(Predicate<IPrincipal> predicate)
        {
            Session.UserIsInRoleIf(Role, predicate);
        }

        public UserRoleResource<T> Of<T>()
        {
            return new UserRoleResource<T>(this);
        }
    }

    [Browsable(false)]
    [EditorBrowsable(EditorBrowsableState.Never)]
    public class UserRoleResource<T>
    {
        private readonly UserRole userRole;

        internal UserRoleResource(UserRole role)
        {
            this.userRole = role;
        }
        public void If(IsUserInRole predicate)
        {
            ISpecializedRbacSession session = userRole.Session as ISpecializedRbacSession;
            if (session == null)
            {
                throw new NotSupportedException();
            }
            session.AddUserRoleForTypeIf<T>(userRole.Role, predicate);
        }
    }

}

public class What
{
    internal What(IRbacSession session)
    {
        Can = new _Can(session);
    }

    public _Can Can { get; private set; }

    public class _Can
    {
        internal _Can(IRbacSession session)
        {
            Role = new _Role(session);

        }
        public _Role Role { get; private set; }
        public class _Role
        {
            private readonly IRbacSession session;
            internal _Role(IRbacSession session)
            {
                this.session = session;
            }
            public IEnumerable<string> Do(string role)
            {
                return session.Query.GetRolePermissions(role);
            } 
        }
        public _User User { get; private set; }
        public class _User
        {
            private readonly IRbacSession session;
            internal _User(IRbacSession session)
            {
                this.session = session;
            }
            public IEnumerable<string> Do(IPrincipal principal)
            {
                return session.Query.GetUserPermissions(principal);
            }
        }    
    }

}

public class Is
{
    private readonly IRbacSession session;
    internal Is(IRbacSession session)
    {
        this.session = session;
    }
    public Principal User(IPrincipal principal)
    {
        return new Principal(session, principal);
    }

    [Browsable(false)]
    [EditorBrowsable(EditorBrowsableState.Never)]
    public class Principal
    {
        internal IRbacSession Session { get; private set; }
        internal IPrincipal User { get; private set; }
        internal Principal(IRbacSession session, IPrincipal user)
        {
            this.Session = session;
            this.User = user;
        }
        public UserRole A(string role)
        {
            return new UserRole(this, role) { Result = Session.Query.IsUserInRole(User, role) };
        }

        [Browsable(false)]
        [EditorBrowsable(EditorBrowsableState.Never)]
        public class UserRole
        {
            private readonly Principal principal;
            private readonly string role;
            internal UserRole(Principal principal, string role)
            {
                this.principal = principal;
                this.role = role;
            }
            public bool Result { get; set; }

            public bool Of<T>(T resource)
            {
                ISpecializedRbacSession session = principal.Session as ISpecializedRbacSession;
                if (session == null)
                {
                    throw new NotSupportedException();
                }
                Result = session.Query.IsUserInRole(principal.User, role, resource);
                return Result;
            }
        }
    }
}

public class Do
{
    private readonly IRbacSession session;
    internal Do(IRbacSession session)
    {
        this.session = session;
    }
    public _Action A(string name)
    {
        return new _Action(session, name);
    }
    [Browsable(false)]
    [EditorBrowsable(EditorBrowsableState.Never)]
    public class _Action
    {
        internal string Action { get; private set; }
        internal IRbacSession Session { get; private set; }

        internal _Action(IRbacSession session, string action)
        {
            this.Session = session;
            this.Action = action;
        }

        public void Requires(string role)
        {
            Session.AddPermission(role, Action);
        }

        public ActionRequirements<T> Of<T>()
        {
            return new ActionRequirements<T>(this);
        }

        [Browsable(false)]
        [EditorBrowsable(EditorBrowsableState.Never)]
        public class ActionRequirements<T>
        {
            private readonly _Action action;

            internal ActionRequirements(_Action action)
            {
                this.action = action;
            }

            public void Requires(string role)
            {
                ISpecializedRbacSession session = action.Session as ISpecializedRbacSession;
                if (session == null)
                {
                    throw new NotSupportedException();
                }
                session.AddPermission<T>(role, action.Action);
            }
        }
    }
}

public class Can
{
    private readonly IRbacSession session;
    internal Can(IRbacSession session)
    {
        this.session = session;
    }
    public UserAction User(IPrincipal principal)
    {
        return new UserAction(session, principal);
    }
    [Browsable(false)]
    [EditorBrowsable(EditorBrowsableState.Never)]
    public class UserAction
    {
        internal IRbacSession Session { get; private set; }
        internal IPrincipal Principal { get; private set; }
        internal UserAction(IRbacSession session, IPrincipal principal)
        {
            this.Session = session;
            this.Principal = principal;
        }

        public ActionResource Do(string action)
        {
            return new ActionResource(this, action){Result = Session.Query.IsUserAbleTo(Principal, action)};
        }
        [Browsable(false)]
        [EditorBrowsable(EditorBrowsableState.Never)]
        public class ActionResource
        {
            private readonly UserAction userAction;
            private readonly string action;
            internal ActionResource(UserAction userAction, string action)
            {
                this.userAction = userAction;
                this.action = action;
            }

            public bool Result { get; set; }

            public bool The<T>(T resource)
            {
                ISpecializedRbacSession session = userAction.Session as ISpecializedRbacSession;
                if (session == null)
                {
                    throw new NotSupportedException();
                }
                Result = session.Query.IsUserAbleTo(userAction.Principal, action, resource);
                return Result;
            }
        }
    }
}

I am looking for any suggestions of improvements, either on the fluent API or the implementation itself. I am also looking for any violations of the concept of RBAC in this implementation.

\$\endgroup\$
  • \$\begingroup\$ Have you taken a look at the asp.net identity claims stuff? It seems capable of achieving a lot of these things and is fairly simple and standardized. \$\endgroup\$ – George Mauer Jun 3 '14 at 20:49
  • \$\begingroup\$ I'll be looking into that. The only mechanism I had contact so far in .NET about authorization/authentication is Role provider and Membership provider. But I would like a review on this code anyway. \$\endgroup\$ – Bruno Costa Jun 3 '14 at 21:00
  • \$\begingroup\$ I realize the post is long enough as is, but it would be nice if you could add a small snippet showing how the fluent API is consumed in some example/test code. \$\endgroup\$ – Mathieu Guindon Jun 3 '14 at 22:37
8
+100
\$\begingroup\$
public delegate IEnumerable<string> GetUserPermissions(IPrincipal user, object resource);
public delegate IEnumerable<string> GetUserRoles(IPrincipal user, object resource);
public delegate bool IsUserInRole(IPrincipal user, object resource);

I seldom create new delegate types, I tend to use Action and Func delegates instead, but I like how it makes an enjoyable read, especially here:

IDictionary<Type, GetUserRoles> UserRolesForType { get; }
IDictionary<Type, GetUserPermissions> UserPermissions { get; }

Why is the former called UserRolesForType? Then why isn't the latter called UserPermissionsForType? Consistency is your friend!


We could consider the distinction between "normal" and "parameterized" queries as superfluous:

public interface IRbacQuery
{
    IEnumerable<string> GetUserRoles(IPrincipal user);
    IEnumerable<string> GetUserRoles<T>(IPrincipal user, T resource);

    IEnumerable<string> GetUserPermissions(IPrincipal user); 
    IEnumerable<string> GetUserPermissions<T>(IPrincipal user, T resource); 

    bool IsUserInRole(IPrincipal user, string role);
    bool IsUserInRole<T>(IPrincipal user, string role, T resource);

    bool IsUserAbleTo(IPrincipal user, string action);
    bool IsUserAbleTo<T>(IPrincipal user, string action, T resource);

    IEnumerable<string> GetRolePermissions(string roleName);
    bool IsRoleAbleTo(string roleName, string action);
}

The implementating types would expose both variants:

public virtual IEnumerable<string> GetUserRoles(IPrincipal user)
{
    return session.UserRoles
                  .Where(role => role.Value(user))
                  .Select(role => role.Key);
}

public virtual IEnumerable<string> GetUserRoles<T>(IPrincipal user, T resource)
{
    var userRoles = session.UserRolesForType.TryGetOrEmpty(typeof (T));
    if (userRoles == null)
    {
        return new string[0];
    }
    return userRoles(user, resource);
}

The generic overload would no longer need to cast session into an ISpecializedRbacSession (assuming GetRolesForType becomes a member of IRbacSession), and it seems to me the client code would get simpler... unless you already only expose the "specialized" interface, so the client code already sees both overloads?


The nesting of the types in the last code block is interesting:

  • Rbac
  • User
    • UserRole
    • UserRoleResource<T>
  • What
    • _Can
      • _Role
      • _User
  • Is
    • Principal
      • UserRole
  • Do
    • _Action
      • ActionRequirements<T>
  • Can
    • UserAction
      • ActionResource

I'd have to spend a great deal of time on this code to understand the presence of an _ underscore prefix on some of the public classes you have here, and why User, What, Is, Do and Can aren't themselves nested under Rbac, because there's strong coupling going on here:

public Rbac(IRbacSession session)
{
    User = new User(session);
    What = new What(session);
    Is = new Is(session);
    Do = new Do(session);
    Can = new Can(session);
}

I like the constructor-injected session though.


As for the implementations themselves, I find the dictionary assignations could use some var:

IDictionary<Type, GetUserPermissions> userPermissions = new Dictionary<Type, GetUserPermissions>();

Compare to:

var userPermissions = new Dictionary<Type, GetUserPermissions>();

In the same method, seeing this line:

foreach (var permission in permissionAssignment)

Made me scroll all the way up to this declaration:

private readonly IDictionary<Type, ICollection<Role>> permissionAssignment = new Dictionary<Type, ICollection<Role>>();

If permissionAssignment had been called _permissionAssignment, I would have known at a glance that it was a private field, ...but that's just me being used to that underscore and being a bit nitpicky, the code looks good, really.

\$\endgroup\$
  • \$\begingroup\$ In my first implementation I also used Func, but I realized that the code could become more readable and had more meaning if I created delegates. I separated the RBAC from specialized rbac because these are different versions of RBAC Model and I just tried to map that on my code. I usually like to deny the usage of var but I guess that's a situation that is ok to use it. The only convention that I do not respect from c# is exactly the field naming convention, I perhaps should use this every time I refer to a field. \$\endgroup\$ – Bruno Costa Jun 4 '14 at 7:58

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.