Activity-based authorization in modular systems

There are some materials on the Web concerning the fact that role-based authentication is probably not the best option while implementing system security infrastructure. I find this blog post quite exhaustive: http://lostechies.com/derickbailey/2011/05/24/dont-do-role-based-authorization-checks-do-activity-based-checks/.

So basically you need a component which determines whether user X is authorized to perform action Y. But that is the simplest case scenario. Probably, in practice you need to determine whether user X is authorized to perform action Y on object V. For example the project manager can change the project schedule but the other users cannot. Probably you need some service which you could inject into your code in business logic layer, application logic layer or even UI logic layer (for example to hide “Change project schedule” button). This service could define few methods like “IsUserAuthorizedToChangeProjectSchedule(IPrincipal user, int projectId)” but at the end your service would declare dozens or even hundreds of methods and would have many many reasons to change (Interface Segregation Principle and Single Responsibility Principle would be violated). This solution would be very problematic in modular applications because it would force you to create a single service responsible for implementing the authorization rules of many modules. Of course you could create multiple services e.g. ISalesAuthorizationService or ICRMAuthorizationService but I’d like to present a different approach.

Basically we can abstract any authorization rule request as a pair of activity name (because it’s the activity-based authorization which I’m writing about) and one or more parameters.  Based on this assumption lets create IAuthorizationService interface:

public interface IAuthorizationService
{
	/// <summary>
	/// Exceptions:
	/// AccessDeniedException
	/// </summary>
	void Authorize(string action, params object[] parameters);

	bool IsAuthorized(string action, params object[] parameters);
}

This interface should be declared in a core assembly – the assembly which is referenced by all of your application modules e.g. ERP.Core.Interfaces so each module can call Authorize and IsAuthorized methods. Authorize and IsAuthorized methods are very similar. The first one will throw exception when currently logged user can’t perform some activity and the other will return true if she is authorized and false if she is not.

So we have a single point of authorization but we want authorization logic of each module to resist in its module assembly. To make it possible we need to implement IAuthorizationService in chain-of-responsibility manner so i.e. when the Authorize method is being invoked with action parameter equals to “ProjectsModule.ChangeProjectSchedule” the service dispatches this call to e.g. ERP.ProjectsModule.Security.ProjectsAuthorizationRulesService. To make this mechanism even more flexible we could register these module specific services at runtime by using IoC. It’s a good idea to remove magic-strings issue by placing activity-names in module-scoped constant fields.

Here’s a code fragment which shows Spring.NET based implementation of mechanisms described above :

PluggableAuthorizationService is implementation of IAuthorizationService interface which does the magic. It exposes Plugins public property which accepts the array of strings where each string is fully qualified authorization plugin type name. During initialization this service scans registered types of plugins for public methods decorated with AuthorizationEndpointAttribute. When IsAuthorized method of this service is called it searches for cached methods with proper activity name and executes them in the correct order (depending on Order property of AuthorizationEndpointAttribute).

public class PluggableAuthorizationService : IAuthorizationService, IApplicationContextAware
{
	private bool _initialized = false;
	private object _syncLock = new object();
	private Dictionary<string, List<Tuple<Type, int, MethodInfo>>> _actions = new Dictionary<string, List<Tuple<Type, int, MethodInfo>>>();

	public string[] Plugins { get; set; }

	public IApplicationContext ApplicationContext { get; set; }

	public void Authorize(string action, params object[] parameters)
	{
		if (!IsAuthorized(action, parameters))
			throw new AccessDeniedException();
	}

	public bool IsAuthorized(string action, params object[] parameters)
	{
		EnsureIsInitialized();

		if (!_actions.ContainsKey(action))
			throw new Exception("Authorization endpoint '" + action + "' not found");

		List<Tuple<Type, int, MethodInfo>> method = _actions[action];

		if (!method.Any())
			return false;

		foreach (var methodValidation in method)
		{
			var plugin = ApplicationContext.GetObjectsOfType(methodValidation.Item1)
				.Values
				.OfType<object>()
				.Single();

			bool result = false;

			if (methodValidation.Item3.GetParameters().Count() == 1)
				result = (bool)methodValidation.Item3.Invoke(plugin, new object[] { action });
			else
				result = (bool)methodValidation.Item3.Invoke(plugin, new object[] { action, parameters ?? new object[] { } });

			if (!result)
				return false;
		}
		return true;
	}

	private void EnsureIsInitialized()
	{
		if (!_initialized)
		{
			lock (_syncLock)
			{
				if (!_initialized)
				{
					Initialize();
					_initialized = true;
				}
			}
		}
	}

	private void Initialize()
	{
		if (Plugins == null) throw new Exception("Object property not initialized - Plugins");

		var types = Plugins
			.Select(x => Type.GetType(x));

		foreach (var type in types)
		{
			var allMethods = type.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly);
			foreach (var potentialEndpoint in allMethods)
			{
				var endpointAttrs =
					potentialEndpoint.GetCustomAttributes(false)
					.OfType<AuthorizationEndpointAttribute>();

				foreach (var endpointAttr in endpointAttrs)
				{
					if (!(potentialEndpoint.ReturnType == typeof(bool) &&
						!potentialEndpoint.IsStatic &&
						potentialEndpoint.GetParameters().Count() > 0 &&
						potentialEndpoint.GetParameters()[0].ParameterType == typeof(string) &&
						potentialEndpoint.IsPublic))
					{                            
						continue;
					}

					if (!_actions.ContainsKey(endpointAttr.Action))
						_actions[endpointAttr.Action] = new List<Tuple<Type, int, MethodInfo>>();

					_actions[endpointAttr.Action].Add(new Tuple<Type, int, MethodInfo>(type, endpointAttr.Order, potentialEndpoint));
				}
			}

			foreach (var key in _actions.Keys)
				_actions[key].OrderBy(x => x.Item2);
		}
	}
}

Spring.NET XML configuration file fragment which shows how authorization plugins are registered.

<object name="authorizationService" type="ERP.Base.Security.PluggableAuthorizationService, ERP.Base" scope="application">
	<property name="Plugins">
	  <list>
		<value>ERP.Projects.Application.Services.ProjectsAuthorizationPlugin, ERP.Projects</value>
		<value>ERP.Partners.Application.Services.PartnersAuthorizationPlugin, ERP.Partners</value>
		<value>ERP.Tasks.Application.Services.TasksAuthorizationPlugin, ERP.Tasks</value>
		<value>ERP.Documentation.Application.Services.DocsAuthorizationPlugin, ERP.Documentation</value>
		<value>ERP.Messaging.Application.Services.MessagesAuthorizationPlugin, ERP.Messaging</value>
	  </list>
	</property>
</object>

Module activity names constants class:

public static class ModuleActivityNames
{
	public const string Calendar_List = "Projects.Calendar.List";

	public const string Projects_List = "Projects.Projects.List";
	public const string Project_Create = "Projects.Project.Create";
	public const string ProjectData_Read = "Projects.Project.Data.Read";
	public const string ProjectData_Edit = "Projects.Project.Data.Edit";
	public const string Project_ChangeStatus = "Projects.Project.ChangeStatus";
	public const string ProjectSchedule_Read = "Projects.Project.Schedule.Read";
	public const string ProjectSchedule_Edit = "Projects.Project.Schedule.Edit";
	public const string ProjectCollaboration_Read = "Projects.Project.Collaboration.Read";
	public const string ProjectCollaboration_Edit = "Projects.Project.Collaboration.Edit";

	public const string Reports_Run = "Projects.Reports.Run";
}

Projects module authorization rules logic:

public class ProjectsAuthorizationPlugin
    {
        public IProjectsQuery ProjectsQuery { get; set; }

        private ERPPrincipal Principal
        {
            get { return Thread.CurrentPrincipal as ERPPrincipal; }
        }

        [AuthorizationEndpoint(Action = ModuleActionNames.Calendar_List)]
        [AuthorizationEndpoint(Action = ModuleActionNames.Projects_List)]
        [AuthorizationEndpoint(Action = ModuleActionNames.Reports_Run, Order = 1)]
        [AuthorizationEndpoint(Action = ModuleActionNames.Project_Create, Order = 1)]
        [AuthorizationEndpoint(Action = ModuleActionNames.ProjectData_Read, Order = 1)]
        [AuthorizationEndpoint(Action = ModuleActionNames.ProjectData_Edit, Order = 1)]
        [AuthorizationEndpoint(Action = ModuleActionNames.ProjectSchedule_Read, Order = 1)]
        [AuthorizationEndpoint(Action = ModuleActionNames.ProjectSchedule_Edit, Order = 1)]
        [AuthorizationEndpoint(Action = ModuleActionNames.ProjectCollaboration_Read, Order = 1)]
        [AuthorizationEndpoint(Action = ModuleActionNames.ProjectCollaboration_Edit, Order = 1)]
        [AuthorizationEndpoint(Action = ModuleActionNames.Project_ChangeStatus, Order = 1)]
        public bool VerifyBasePermission(string action, params object[] dummy)
        {
            if (Principal == null) return false;
            return true;
        }

        [AuthorizationEndpoint(Action = ModuleActionNames.Reports_Run, Order = 2)]
        public bool VerifyReportPermissions(string action, params object[] dummy)
        {
            return Principal.IsInRole(UserRole.Administrator);
        }

        [AuthorizationEndpoint(Action = ModuleActionNames.ProjectData_Read, Order = 2)]
        [AuthorizationEndpoint(Action = ModuleActionNames.ProjectData_Edit, Order = 2)]
        [AuthorizationEndpoint(Action = ModuleActionNames.ProjectSchedule_Read, Order = 2)]
        [AuthorizationEndpoint(Action = ModuleActionNames.ProjectSchedule_Edit, Order = 2)]
        [AuthorizationEndpoint(Action = ModuleActionNames.ProjectCollaboration_Read, Order = 2)]
        [AuthorizationEndpoint(Action = ModuleActionNames.ProjectCollaboration_Edit, Order = 2)]
        [AuthorizationEndpoint(Action = ModuleActionNames.Project_ChangeStatus, Order = 2)]
        public bool VerifyPerProjectPermission(string action, params object[] prm)
        {
            if (Principal == null) return false;
            if (Principal.IsInRole(UserRole.Administrator))
                return true;

            int projectId = Convert.ToInt32(prm[0]);

            var project = ProjectsQuery.Query(projectId);
            if (project.ProjectManagerId == Principal.UserId)
                return true;

            var permissions = ProjectsQuery.QueryPermissions(projectId, Principal.UserId);

            switch (action)
            {
                case ModuleActionNames.ProjectData_Read:
                    return permissions.Any(x => x.Permission == ProjectPermission.ReadProjectData);
                case ModuleActionNames.ProjectData_Edit:
                    return permissions.Any(x => x.Permission == ProjectPermission.EditProjectData);
                case ModuleActionNames.ProjectCollaboration_Read:
                    return permissions.Any(x => x.Permission == ProjectPermission.ReadProjectCollaboration);
                case ModuleActionNames.ProjectCollaboration_Edit:
                    return permissions.Any(x => x.Permission == ProjectPermission.EditProjectCollaboration);
                case ModuleActionNames.ProjectSchedule_Read:
                    return permissions.Any(x => x.Permission == ProjectPermission.ReadProjectSchedule);
                case ModuleActionNames.ProjectSchedule_Edit:
                    return permissions.Any(x => x.Permission == ProjectPermission.EditProjectSchedule);
                case ModuleActionNames.Project_ChangeStatus:
                    return false;
                default:
                    return false;
            }
        }

        [AuthorizationEndpoint(Action = ModuleActionNames.Project_Create, Order = 2)]
        public bool VerifyCanCreateProjects(string action)
        {
            if (Principal == null) return false;

            return Principal.IsInRole(UserRole.ProjectManager);
        }
    }

Authorization service usage examples:

if (AuthorizationService.IsAuthorized(ERP.Projects.Interfaces.Application.ModuleActionNames.Projects_List))
        projectsModuleItem.ChildItems.Add(new MenuItem("My projects", String.Empty, String.Empty, "~/Projects/Default.aspx"));

and

public partial class Default : ERPPageBase
{
        protected int SelectedProjectId {get;set;}
	public override void Authorize()
	{
		AuthorizationService.Authorize(ModuleActionNames.ProjectSchedule_Edit, SelectedProjectId);
	}

	protected void Page_Load(object sender, EventArgs e)
	{
		DoSomePageLogic();
	}
}

Mechanism described above requires some consequence in order of parameters passed to authorization methods but it’s possible to modify it to use key-value collections or to match parameters by their names. Despite this disadvantage I find this approach working really nice in mid to large, modular systems.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: