8

I have a very simple scenario. I want to decorate my controllers/actions with a custom authorization attribute. Authorization should be granted if any of the attributes is valid. For example,

[MyAuth(1)]
[MyAuth(2)]
public class MyController : Controller
{
    ...
}

I cannot combine the parameters into a single authorization attribute. The above example is a simplified example, only.

If either attribute authorizes the user, I want the user to be authorized. I assumed that ActionFilterAttribute or AuthorizeAttribute would have the means to see what other filters have been executed and are waiting to be executed, but no such luck.

How can I accomplish this? Since the attributes don't seem to have any awareness, maybe an HttpModule? A custom ControllerActionInvoker?

  • Why would you like to build an `HttpModule`? As I hack i would go with http://msdn.microsoft.com/en-us/library/system.web.httpcontext.items(v=vs.110).aspx . But why are you not able to combine the parameters? – Andreas Nov 04 '13 at 22:24
  • I think if you use [MyAuth(Roles = "1")] [MyAuth(Roles = "2")], the frameowrk should aware that Roles 1, and Roles 2 authorized. Is there a particular reason that you want to avoid creating single Authorize attribute which accept multiple params? – Spock Nov 04 '13 at 22:39
  • @Andreas, I don't want to build an `HttpModule`. I was throwing it out there as a possible solution. –  Nov 04 '13 at 22:46
  • @Raj, because the attribute takes 4 parameters. I *can't* combine them unless I supply the parameters as a string to be parsed, which is undesirable. –  Nov 04 '13 at 22:47
  • I think you can still achieve the desired behavior with one attribute. Just wondering what stopping you creating strongly typed properties in yr authorize attr? In yr custom auth attr. you can process them accordingly. Or am I missing something here? http://stackoverflow.com/questions/1148312/asp-net-mvc-decorate-authorize-with-multiple-enums – Spock Nov 05 '13 at 02:00

3 Answers3

6

I managed to get this to work last night. My solution is below. The attribute is pretty standard and I've trimmed the actual authorization parts. The interesting stuff happens in HasAssignedAcccessActionInvoker.

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
public class RequiresAssignedAccess : AuthorizeAttribute
{
    public int AccessType { get; private set; }
    public int IdType { get; private set; }
    public int IdValue { get; private set; }
    public int Level { get; private set; }

    public RequiresAssignedAccess(int accessType, int idType, int idValue, int level)
    {
        ...
    }

    protected override bool AuthorizeCore(HttpContextBase httpContext)
    {
        if (!base.AuthorizeCore(httpContext))
            return false;

        bool retval = ...

        return retval;
    }
}

HasAssignedAcccessActionInvoker inherits from the standard action invoker, but I overrode the InvokeAuthorizationFilters method to add the authorization logic we need. The standard invoker just spins through the authorization filters and if any of them returns a result, it breaks the loop.

public class HasAssignedAcccessActionInvoker : ControllerActionInvoker
{
    protected override AuthorizationContext InvokeAuthorizationFilters(ControllerContext controllerContext, IList<IAuthorizationFilter> filters, ActionDescriptor actionDescriptor)
    {
        AuthorizationContext authCtx = new AuthorizationContext(controllerContext, actionDescriptor);

        /*
         * If any of the filters are RequiresAssignedAccess, default this to false.  One of them must authorize the user.
         */
        bool hasAccess = !filters.Any(f => f is RequiresAssignedAccess);

        foreach (IAuthorizationFilter current in filters)
        {
            /*
             * This sets authorizationContext.Result, usually to an instance of HttpUnauthorizedResult
             */
            current.OnAuthorization(authCtx);

            if (current is RequiresAssignedAccess)
            {
                if (authCtx.Result == null)
                {
                    hasAccess = true;
                }
                else if (authCtx.Result is HttpUnauthorizedResult)
                {
                    authCtx.Result = null;
                }

                continue;
            }

            if (authCtx.Result != null)
                break;
        }

        if (!hasAccess && authCtx.Result == null)
            authCtx.Result = new HttpUnauthorizedResult();

        return authCtx;
    }
}

I had to look at MVC's internals with ILSpy to figure this out. For reference, this is the overridden version of that method:

protected virtual AuthorizationContext InvokeAuthorizationFilters(ControllerContext controllerContext, IList<IAuthorizationFilter> filters, ActionDescriptor actionDescriptor)
{
    AuthorizationContext authorizationContext = new AuthorizationContext(controllerContext, actionDescriptor);
    foreach (IAuthorizationFilter current in filters)
    {
        current.OnAuthorization(authorizationContext);
        if (authorizationContext.Result != null)
        {
            break;
        }
    }
    return authorizationContext;
}

Lastly, to wire this up and make everything possible, our controllers inherit from BaseController, which now returns the new invoker.

public class BaseController : Controller
{
    protected override IActionInvoker CreateActionInvoker()
    {
        return new HasAssignedAcccessActionInvoker();
    }
}
  • unfortunately the `ApiController` does not implement the `InvokeAuthorizationFilters` method, do you know how to achieve the "OR-behaviour" on WebApi projects? – wodzu Nov 03 '14 at 15:40
0

As far as I know, you cannot chain [Authorize] attributes in the manner that you want because they all have to pass (AND) and not (OR) behavior. However, the combining of the items into one does not cause you to have to do some magic string manipulation, regardless of the number of parameters that you need to pass to it. You can define your own set of parameters that are available to the Authorize attribute.

public class SuperCoolAuthorize : AuthorizationAttribute
{
    public string Parameter1{get;set;}
    public string Parameter2{get;set;}
    public int Parameter3{get;set;}
    public string Parameter4{get;set;}
    public override void OnAuthorization(AuthorizationContext filterContext)
    {
        // your custom behaviour
    }
}

And on your controller/action method

[Authorize(Parameter1 = "Foo", Parameter2 = "Bar", Parameter3 = 47, Parameter4 = string.Empty)
public ActionResult MyControllerAction(){
...
}

A great post on some other considerations on custom Authorizing attributes I came across in helping to formulate this answer.

Community
  • 1
  • 1
Tommy
  • 39,592
  • 10
  • 90
  • 121
  • 1
    This does not answer my question. –  Nov 05 '13 at 16:51
  • @Amy - Your question is - I want to decorate my action with multiple authorize attributes, and if any one passes - then authorized. My answer - it is not possible (with resources) as well as an alternative that shows your thoughts on having a single authorize statement are incorrect. – Tommy Nov 05 '13 at 17:00
  • 1
    It is possible. I did it last night and am submitting my solution as an answer. –  Nov 06 '13 at 17:29
0
public class AuthUserAttribute : AuthorizeAttribute {

public string[] SecurityGroups;
public string Groups { get; set; }

protected override bool AuthorizeCore(HttpContextBase httpContext) {
  bool valid = false;

  var user = UserInformation.Current;

  if (user.SecurityGroups.Select(x => x).Intersect(this.SecurityGroups).Any()) {
    valid = true;
  }

  if (user.SecurityGroups.Select(x => x).Intersect(new string[] { "IT Administrators" }).Any()) {
    valid = true;
  }

  return valid;
}

public override void OnAuthorization(AuthorizationContext filterContext) {
  if (!this.AuthorizeCore(filterContext.HttpContext)) {
    if (UserInformation.Current.SecurityGroups.Count == 0) {
      filterContext.Result = new RedirectResult(string.Format("/oa?ReturnUrl={0}", filterContext.HttpContext.Request.RawUrl));
    }
    else {
      filterContext.Result = new RedirectResult(string.Format("/oa/user/permissions?ReturnUrl={0}", filterContext.HttpContext.Request.RawUrl));
    }
  }
  else {
    base.OnAuthorization(filterContext);
  }
}

}

then I decorate with

[AuthUser(SecurityGroups = new string[] { "Data1", "Data2" })]
public ActionResult ForYourEyesOnly() {


}

We'll see if anyone catches the Bond reference. LOL

Damon Drake
  • 839
  • 1
  • 10
  • 23