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.