9

I'm using Unity.MVC4 dependency injection for accessing my services. Everything works as it should when injecting into my Controller constructor, but what I would like to do now is to use property injection in my filter class so I can access my database from the inside.

Before I started this question I Googled around and tried different examples but I couldn't find a solution that worked for me..

Bootstrapper.cs

public static class Bootstrapper
{
    public static IUnityContainer Initialise()
    {
        var container = BuildUnityContainer();

        DependencyResolver.SetResolver(new UnityDependencyResolver(container));

        return container;
    }

    private static IUnityContainer BuildUnityContainer()
    {
        var container = new UnityContainer();
        container.RegisterType<IAccountRepository, AccountRepository>();
        container.RegisterType<IAdministrationRepository, AdministrationRepository>();
        container.RegisterType<IUploadDirectlyRepository, UploadDirectlyRepository>();
        container.RegisterType<IUserRepository, UserRepository>();
        container.RegisterType<INewsRepository, NewsRepository>();
        container.RegisterType<IContactRepository, ContactRepository>();

        // register all your components with the container here
        // it is NOT necessary to register your controllers

        // e.g. container.RegisterType<ITestService, TestService>();    
        RegisterTypes(container);

        return container;
    }

    public static void RegisterTypes(IUnityContainer container)
    {

    }
}

Application_Start

public class MvcApplication : System.Web.HttpApplication
    {
        protected void Application_Start()
        {
            AreaRegistration.RegisterAllAreas();

            WebApiConfig.Register(GlobalConfiguration.Configuration);
            FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
            RouteConfig.RegisterRoutes(RouteTable.Routes);
            BundleConfig.RegisterBundles(BundleTable.Bundles);

            Bootstrapper.Initialise();
        }
    }

Working example

public class UserController : Controller
{
    private readonly IUserRepository _userRepository;

    public UserController(IUserRepository userRepository)
    {
        _userRepository = userRepository;
    }

    public ActionResult GetUser(int userID)
    {
        var user = _userRepository.GetUser(userID)

        return View(user);
    }
}

The following code that I'm about to show you is for the filter attribute that I would like to use on my actions. I want to pass in a parameter of type string array so I can validate if the current user is allowed to access the action.

In my application there are two types of users, Account owner and Guest. All actions are fully open for account owners, but for guests it varies from action to action. For an example, an action can require you to have atleast one of three permissions, (read, write and edit).

Filter:

public class ClaimsAuthorizeAccountAccess : AuthorizeAttribute
{
    private IAccountRepository _accountRepository { get; set; }
    private String[] _permissions { get; set; }

    public ClaimsAuthorizeAccountAccess(IAccountRepository accountRepository, params String[] permissions)
    {
        _permissions = permissions;
        _accountRepository = accountRepository;
    }

    public override void OnAuthorization(AuthorizationContext filterContext)
    {
        if (HttpContext.Current.User.IsInRole("Account Owner"))
        {
            base.OnAuthorization(filterContext);
        }
        else
        {
            ClaimsIdentity claimsIdentity = (ClaimsIdentity)HttpContext.Current.User.Identity;
            List<AccountLinkPermissionDTO> accountLinkPermissions = new List<AccountLinkPermissionDTO>();

            int accountOwnerID = 0;
            Int32.TryParse(claimsIdentity.Claims.Where(c => c.Type == "AccountOwnerID").Select(c => c.Value).SingleOrDefault(), out accountOwnerID);
            int guestID = 0;
            Int32.TryParse(claimsIdentity.Claims.Where(c => c.Type == ClaimTypes.Sid).Select(c => c.Value).SingleOrDefault(), out guestID);

            //NULL
            accountLinkPermissions = _accountRepository.GetAccountLinkPermissions(accountOwnerID, guestID);

            if (accountLinkPermissions != null)
            {
                List<string> accountLinkPermissionsToString = accountLinkPermissions.Select(m => m.Permission.Name).ToList();
                int hits = accountLinkPermissionsToString.Where(m => _permissions.Contains(m)).Count();

                if (hits > 0)
                {
                    base.OnAuthorization(filterContext);
                }
            }
            else
            {
                //Guest doesnt have right permissions

                filterContext.Result = new RedirectToRouteResult(
                new RouteValueDictionary {
                        { "action", "AccessDenied" },
                        { "controller", "Account" }});
            }
        }
    }
}

If I were to use this filter it would look something like..

[ClaimsAuthorizeAccountAccess("File read", "File write, File edit")]
public ActionResult Files()
{
    return View();
}

However this does not work because the filter expects two parameters, (IRepository and string[]). It's also not possible to use constructor injection here, obviously.

I then tried implementing John Allers solution that can be found here. It looked promising but it gave me this error:

An exception of type 'Microsoft.Practices.Unity.ResolutionFailedException' occurred in Microsoft.Practices.Unity.dll but was not handled in user code

Additional information: Resolution of the dependency failed, type = "Fildela.ClaimsAuthorizeAccountAccess", name = "(none)".

Exception occurred while: while resolving.

Exception is: InvalidOperationException - The property _accountRepository on type Fildela.ClaimsAuthorizeAccountAccess is not settable.


At the time of the exception, the container was:

Resolving Fildela.ClaimsAuthorizeAccountAccess,(none)

Any suggestion on how to solve this bad boy?

Thanks!

NightOwl888
  • 55,572
  • 24
  • 139
  • 212
Reft
  • 2,333
  • 5
  • 36
  • 64
  • You need a public setter for your property. Did you convert _accountRepository to public or are you trying to inject a private property? – stephen.vakil Aug 26 '15 at 20:09
  • One option is to make a separate attribute from your custom authorize attribute that has no behavior (which you would use to mark your actions and controllers with), and then register your authorize attribute as a global filter (which reads the attributes on your controllers and actions) so you can inject it in your composition root. See http://blog.ploeh.dk/2014/06/13/passive-attributes/ – NightOwl888 Aug 26 '15 at 20:11
  • Hi! Change it from private to public? Yes I have tried that but Im afraid it did not help :/ Do I need to use the custom filter provider or are u just supposed to add [Dependency] and it should all work? – Reft Aug 26 '15 at 20:11

3 Answers3

15

As per the post Passive Attributes, the DI-friendly solution is to separate the AuthorizeAttribute into 2 parts:

  1. An attribute that contains no behavior to flag your controllers and action methods with.
  2. A DI-friendly class that implements IAuthorizationFilter and contains the desired behavior.

For our purposes, we just inherit AuthorizeAttribute to take advantage of some of its built in functionality.

Note that if you take this approach, it doesn't make much sense to use property injection for your database dependencies. Constructor injection is always a better choice, anyway.

ClaimsIdentityAuthorizeAttribute

First of all, we have our attribute that has no behavior to flag our controllers and actions with. We add a little bit of smartness to parse the permissions out into an array so that doesn't have to be done on every authorization check.

[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false)]
public class ClaimsAuthorizeAccountAccess : Attribute
{
    private readonly string[] _permissionsSplit;

    public ClaimsAuthorizeAccountAccess(string permissions)
    {
        _permissionsSplit = SplitString(value);
    }

    internal string[] PermissionsSplit
    {
        get { return this._permissionsSplit; }
    }

    internal static string[] SplitString(string original)
    {
        if (string.IsNullOrEmpty(original))
        {
            return new string[0];
        }
        return (from piece in original.Split(new char[] { ',' })
                let trimmed = piece.Trim()
                where !string.IsNullOrEmpty(trimmed)
                select trimmed).ToArray<string>();
    }
}

ClaimsIdentityAuthorizationFilter

Next, we have our authorization filter which will act as a global filter.

We add a WhiteListMode which is true by default because that is the recommended way to configure security (controllers and actions require a login unless they are given an AllowAnonymousAttribute). Fortunately, the framework for that is built into AuthorizeAttribute so we just use it as a flag whether or not to check globally.

We also add an extension point where our custom authorization service can be injected. The 2 most likely things to change are:

  1. The test to determine whether the action is authorized.
  2. The action to take when the user is not authorized.

So those are the things that we add to our service. You could refactor this into 2 separate services, if desired.

public class ClaimsIdentityAuthorizationFilter : AuthorizeAttribute
{
    private readonly IAuthorizationService _authorizationService;
    private string _permissions;
    private string[] _permissionsSplit = new string[0];
    private bool _whiteListMode = true;

    public ClaimsIdentityAuthorizationFilter(IAuthorizationService authorizationService)
    {
        if (authorizationService == null)
            throw new ArgumentNullException("authorizationService");

        this._authorizationService = authorizationService;
    }

    // Hide users and roles, since we aren't using them.
    [Obsolete("Not applicable in this class.")]
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
    [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)]
    new public string Roles { get; set; }

    [Obsolete("Not applicable in this class.")]
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
    [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)]
    new public string Users { get; set; }

    public string Permissions
    {
        get
        {
            return (this._permissions ?? string.Empty);
        }
        set
        {
            this._permissions = value;
            this._permissionsSplit = SplitString(value);
        }
    }

    public bool WhiteListMode
    {
        get { return this._whiteListMode; }
        set { this._whiteListMode = value; }
    }

    internal static string[] SplitString(string original)
    {
        if (string.IsNullOrEmpty(original))
        {
            return new string[0];
        }
        return (from piece in original.Split(new char[] { ',' })
                let trimmed = piece.Trim()
                where !string.IsNullOrEmpty(trimmed)
                select trimmed).ToArray<string>();
    }

    private ClaimsAuthorizeAccountAccess GetClaimsAuthorizeAccountAccess(ActionDescriptor actionDescriptor)
    {
        ClaimsAuthorizeAccountAccess result = null;

        // Check if the attribute exists on the action method
        result = (ClaimsAuthorizeAccountAccess)actionDescriptor
            .GetCustomAttributes(attributeType: typeof(ClaimsAuthorizeAccountAccess), inherit: true)
            .SingleOrDefault();

        if (result != null)
        {
            return result;
        }

        // Check if the attribute exists on the controller
        result = (ClaimsAuthorizeAccountAccess)actionDescriptor
            .ControllerDescriptor
            .GetCustomAttributes(attributeType: typeof(ClaimsAuthorizeAccountAccess), inherit: true)
            .SingleOrDefault();

        return result;
    }

    protected override bool AuthorizeCore(HttpContextBase httpContext)
    {
        var actionDescriptor = httpContext.Items["ActionDescriptor"] as ActionDescriptor;
        if (actionDescriptor != null)
        {
            var authorizeAttribute = this.GetClaimsAuthorizeAccountAccess(actionDescriptor);

            // If the authorization attribute exists
            if (authorizeAttribute != null)
            {
                // Run the authorization based on the attribute
                return this._authorizationService.HasPermission(
                    httpContext,
                    authorizeAttribute.PermissionsSplit);
            }
            else if (this.WhiteListMode)
            {
                // Run the global authorization
                return this._authorizationService.HasPermission(
                    httpContext,
                    this._permissionsSplit);
            }
        }

        return true;
    }

    public override void OnAuthorization(AuthorizationContext filterContext)
    {
        // Pass the current action descriptor to the AuthorizeCore
        // method on the same thread by using HttpContext.Items
        filterContext.HttpContext.Items["ActionDescriptor"] = filterContext.ActionDescriptor;
        base.OnAuthorization(filterContext);
    }

    protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
    {
        filterContext.Result = this._authorizationService.GetUnauthorizedHandler(filterContext);
    }
}

IAuthorizationService

public interface IAuthorizationService
{
    bool HasPermission(HttpContextBase httpContext, string[] permissions);
    ActionResult GetUnauthorizedHandler(AuthorizationContext filterContext);
}

ClaimsIdentityAuthorizationService

So now we do the advanced customization to support claims. We separate this so there is a seam we can use to inject another instance if the business logic changes in the future.

public class ClaimsIdentityAuthorizationService : IAuthorizationService
{
    private IAccountRepository _accountRepository { get; set; }

    public ClaimsIdentityAuthorizationService(IAccountRepository accountRepository)
    {
        if (accountRepository == null)
            throw new ArgumentNullException("accountRepository");

        _accountRepository = accountRepository;
    }

    public bool HasPermission(HttpContextBase httpContext, string[] permissions)
    {
        if (httpContext == null)
        {
            throw new ArgumentNullException("httpContext");
        }
        IPrincipal user = httpContext.User;
        if (!user.Identity.IsAuthenticated)
        {
            return false;
        }
        if (!user.IsInRole("Account Owner"))
        {
            ClaimsIdentity claimsIdentity = (ClaimsIdentity)user.Identity;
            List<AccountLinkPermissionDTO> accountLinkPermissions = new List<AccountLinkPermissionDTO>();

            int accountOwnerID = 0;
            Int32.TryParse(claimsIdentity.Claims.Where(c => c.Type == "AccountOwnerID").Select(c => c.Value).SingleOrDefault(), out accountOwnerID);
            int guestID = 0;
            Int32.TryParse(claimsIdentity.Claims.Where(c => c.Type == ClaimTypes.Sid).Select(c => c.Value).SingleOrDefault(), out guestID);

            //NULL
            accountLinkPermissions = _accountRepository.GetAccountLinkPermissions(accountOwnerID, guestID);

            if (accountLinkPermissions != null)
            {
                List<string> accountLinkPermissionsToString = accountLinkPermissions.Select(m => m.Permission.Name).ToList();
                int hits = accountLinkPermissionsToString.Where(m => permissions.Contains(m)).Count();

                if (hits == 0)
                {
                    return false;
                }
            }
            else
            {
                return false;
            }
        }
        return true;
    }

    public ActionResult GetUnauthorizedHandler(AuthorizationContext filterContext)
    {
        //Guest doesnt have right permissions
        return new RedirectToRouteResult(
            new RouteValueDictionary {
                    { "action", "AccessDenied" },
                    { "controller", "Account" }
            });
    }
}

Usage

Register your filter globally and inject its dependencies with your container.

public class FilterConfig
{
    public static void RegisterGlobalFilters(GlobalFilterCollection filters, IUnityContainer container)
    {
        filters.Add(new HandleErrorAttribute());
        filters.Add(container.Resolve<IAuthorizationFilter>());
    }
}

NOTE: If you need any of the filter's dependencies to have a lifetime shorter than singleton, you will need to use a GlobalFilterProvider as in this answer.

Startup

public class MvcApplication : System.Web.HttpApplication
{
    protected void Application_Start()
    {
        var container = Bootstrapper.Initialise();

        AreaRegistration.RegisterAllAreas();

        WebApiConfig.Register(GlobalConfiguration.Configuration);
        FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters, container);
        RouteConfig.RegisterRoutes(RouteTable.Routes);
        BundleConfig.RegisterBundles(BundleTable.Bundles);
    }
}

Bootstrapper

public static class Bootstrapper
{
    public static IUnityContainer Initialise()
    {
        var container = BuildUnityContainer();

        DependencyResolver.SetResolver(new UnityDependencyResolver(container));

        return container;
    }

    private static IUnityContainer BuildUnityContainer()
    {
        var container = new UnityContainer();
        container.RegisterType<IAccountRepository, AccountRepository>();
        container.RegisterType<IAdministrationRepository, AdministrationRepository>();
        container.RegisterType<IUploadDirectlyRepository, UploadDirectlyRepository>();
        container.RegisterType<IUserRepository, UserRepository>();
        container.RegisterType<INewsRepository, NewsRepository>();
        container.RegisterType<IContactRepository, ContactRepository>();

        // Register the types for the authorization filter
        container.RegisterType<IAuthorizationFilter, ClaimsIdentityAuthorizationFilter>(
            // Not sure whether you want white list or black list
            // but here is where it is set.
            new InjectionProperty("WhiteListMode", true),
            // For white list security, you can also set the default
            // permissions that every action gets if it is not overridden.
            new InjectionProperty("Permissions", "read"));
        container.RegisterType<IAuthorizationService, ClaimsIdentityAuthorizationService>();

        // register all your components with the container here
        // it is NOT necessary to register your controllers

        // e.g. container.RegisterType<ITestService, TestService>();    
        RegisterTypes(container);

        return container;
    }

    public static void RegisterTypes(IUnityContainer container)
    {

    }
}

And then in your controller, for black list security, you will need to decorate every action (or controller) to lock it down.

public class HomeController : Controller
{

    // This is not secured at all
    public ActionResult Index()
    {
        return View();
    }

    [ClaimsAuthorizeAccountAccess("read")]
    public ActionResult About()
    {
        ViewBag.Message = "Your application description page.";

        return View();
    }

    [ClaimsAuthorizeAccountAccess("read,edit")]
    public ActionResult Contact()
    {
        ViewBag.Message = "Your contact page.";

        return View();
    }
}

For white list security, you only need to decorate the actions that everyone has access to with AllowAnonymous or add a ClaimsIdentityAuthorizeAttribute with more or less restrictive permissions than the global or controller level.

public class HomeController : Controller
{
    // This is not secured at all
    [AllowAnonymous]
    public ActionResult Index()
    {
        return View();
    }

    // This is secured by ClaimsAuthorizeAccountAccess (read permission)
    public ActionResult About()
    {
        ViewBag.Message = "Your application description page.";

        return View();
    }

    [ClaimsAuthorizeAccountAccess("read,edit")]
    public ActionResult Contact()
    {
        ViewBag.Message = "Your contact page.";

        return View();
    }
}
Community
  • 1
  • 1
NightOwl888
  • 55,572
  • 24
  • 139
  • 212
  • Thank you so much for taking the time to give me this great detailed explanation. But! I was looking for a more quick and dirty solution, (Attribute dependency injection). I dont like the use of global filters aswell, is it possible to do this without it? However, you definitely deserve an upvote! Here you go and thanks again. – Reft Aug 27 '15 at 18:57
  • Well, the only form of "attribute dependency injection" that exists is to use a static service locator (as in Ufuk's answer). It is quick and dirty, but if you do it too many times you will have a DI configuration that is difficult to maintain because you will have to analyze the source code to figure out what dependencies they require rather than just looking at the constructor. Microsoft also recommends against using `DependencyResolver.Current` in application. Global filters are the best solution because you can register them with the container and inject them using constructor injection. – NightOwl888 Aug 27 '15 at 19:08
8

You can't inject dependencies as constructor parameters to action filters because they are implemented as attributes in C#. You need to resolve them using DependencyResolver.Current. It's a kind of Service Locator and it's not cool but you don't have a choice really. ASP.NET MVC doesn't use the DI container to create action filter instances.

public ClaimsAuthorizeAccountAccess(params string[] permissions)
{
    _permissions = permissions;
    _accountRepository = DependencyResolver.Current.GetService<IAccountRepository>();
} 
NightOwl888
  • 55,572
  • 24
  • 139
  • 212
Ufuk Hacıoğulları
  • 37,978
  • 12
  • 114
  • 156
  • 2
    I like this solution alot just because it's so simple and easy. However I agree that its not cool enough for accepting this as an answer;) I will however upvote this! Thanks! – Reft Aug 27 '15 at 18:28
  • 3
    Not true. Action filters and authorization filters are implemented as interfaces. The problem is that Microsoft in their infinite wisdom combined the attributes and filters into single base classes, `ActionFilterAttribute` and `AuthorizeAttribute`. But there is nothing stopping one from separating the interfaces from the attributes if one is so inclined (and doesn't want to resort to calling `DependencyResolver.Current`, which is even frowned upon by Microsoft). Attributes are only DI-friendly if they contain no behavior, but global filters can be injected without issue. – NightOwl888 Sep 13 '15 at 20:17
  • I like this answer much better than the accepted answer. It works and it's short and simple. Nice one. – Rocklan May 21 '18 at 02:18
5

First install official package, Unity.Mvc instead of Unity.MVC4. This package automatically installs and register UnityFilterAttributeFilterProvider which we need it for attribute's dependency injection. You could check if your Unity configured well by looking App_Start > UnityMvcActivator's Start method. You must see following two line:

public static void Start()
{
    // other codes

    FilterProviders.Providers.Remove(FilterProviders.Providers.OfType<FilterAttributeFilterProvider>().First());
    FilterProviders.Providers.Add(new UnityFilterAttributeFilterProvider(container));
}

Now you could add [Dependency] attribute to filter's public properties.

public class ClaimsAuthorizeAccountAccess : AuthorizeAttribute
{
    [Dependency]
    public IAccountRepository AccountRepository { get; set; }
    private String[] _permissions { get; set; }

    public ClaimsAuthorizeAccountAccess(params String[] permissions)
    {
        _permissions = permissions;
    }
}
NightOwl888
  • 55,572
  • 24
  • 139
  • 212
Sam FarajpourGhamari
  • 14,601
  • 4
  • 52
  • 56
  • I will gladly accept this as an answer but before I do, could you please explain more about these different packages? Why are there different versions for this? They look pretty much the same.. Either way I followed your instructions and its all working now. I uinstalled my old unity package and installed the new one, and removed the bootstrapper class, correct? Thanks! – Reft Aug 27 '15 at 18:32
  • In the past Microsoft did not prepare any package for MVC project so some others prepared. But now Microsoft officially prepare MVC package for unity. – Sam FarajpourGhamari Aug 27 '15 at 18:39