1

I would like to implement a whitelist approach that will apply an [Authorize(Roles = "Admin")] attribute by default. Then I would like to specify [AllowAnonymous] or [AllowMember] on whitelisted actions.

So I need to create an attribute similar to AllowAnonymous, but only giving access the "Member" role. (Like AllowAnonymous, it should override any Authorize attributes that may be in effect on controllers as global filters.)

I initially tried to inherit from AllowAnonymousAttribute, but I find it is sealed. I've googled "Inherit allowanonymous" but the answers find me out of my depth.

Am I wise in my approach, and how can I create an attribute such as that?


Update

Following NightOwl888's advice and some code from this page, I have:

  1. Created two Attributes, one to allow members, the other the public

  2. Inherited the AuthorizeAttribute to create a new one that I will apply as a global filter

  3. Inserted a couple of methods into the AuthorizeCore() method that check for the attributes and return true

I hope I'm not doing anything daft in the code below... I'd appreciate a heads up (or down) if it looks ok (or not).

Thanks.


namespace FP.Codebase.Attributes
{
    public class AllowPublicAccessAttribute : Attribute
    {}



    public class AllowMemberAccessAttribute : Attribute
    {}



    public class MyAuthorizeAttribute : AuthorizeAttribute
    {
        public override void OnAuthorization(AuthorizationContext filterContext)
        {
            filterContext.HttpContext.Items["ActionDescriptor"] = filterContext.ActionDescriptor;
            base.OnAuthorization(filterContext);
        }



        private bool IsAllowPublicAccessAttributeAppliedToAction(ActionDescriptor actionDescriptor)
        {
            return (actionDescriptor.IsDefined(typeof(AllowPublicAccessAttribute), inherit: true)
                || actionDescriptor.ControllerDescriptor.IsDefined(typeof(AllowPublicAccessAttribute), inherit: true));
        }



        private bool IsAllowMemberAccessAttributeAppliedToAction(ActionDescriptor actionDescriptor)
        {
            return (actionDescriptor.IsDefined(typeof(AllowMemberAccessAttribute), inherit: true)
                || actionDescriptor.ControllerDescriptor.IsDefined(typeof(AllowMemberAccessAttribute), inherit: true));
        }



        protected override bool AuthorizeCore(HttpContextBase httpContext)
        {
            var actionDescriptor = httpContext.Items["ActionDescriptor"] as ActionDescriptor;
            if (httpContext == null)
            {
                throw new ArgumentNullException("httpContext");
            }

            IPrincipal user = httpContext.User;

            if (IsAllowPublicAccessAttributeAppliedToAction(actionDescriptor))
            {
                return true;
            }
            if (IsAllowMemberAccessAttributeAppliedToAction(actionDescriptor) && user.IsInRole("Member"))
            {
                return true;
            }

            if (!user.Identity.IsAuthenticated)
            {
                return false;
            }

            var _usersSplit = SplitString(Users);
            var _rolesSplit = SplitString(Roles);

            if (_usersSplit.Length > 0 && !_usersSplit.Contains(user.Identity.Name, StringComparer.OrdinalIgnoreCase))
            {
                return false;
            }

            if (_rolesSplit.Length > 0 && !_rolesSplit.Any(user.IsInRole))
            {
                return false;
            }

            return true;
        }



        // copied from https://github.com/ASP-NET-MVC/aspnetwebstack/blob/master/src/System.Web.Mvc/AuthorizeAttribute.cs
        internal static string[] SplitString(string original)
        {
            if (String.IsNullOrEmpty(original))
            {
                return new string[0];
            }

            var split = from piece in original.Split(',')
                        let trimmed = piece.Trim()
                        where !String.IsNullOrEmpty(trimmed)
                        select trimmed;
            return split.ToArray();
        }
    }
}
Community
  • 1
  • 1
Martin Hansen Lennox
  • 2,837
  • 2
  • 23
  • 64

1 Answers1

1

All of the behavior of AllowAnonymous attribute is coded into the AuthorizeAttribute.OnAuthorize method. So, if you want to reuse this behavior, the best approach is to inherit AuthorizeAttribute and override the AuthorizeCore method instead.

NightOwl888
  • 55,572
  • 24
  • 139
  • 212
  • Thanks for that, appreciated. I took your advice and put together a new global attribute, posted in the question. Does that look about right to you? – Martin Hansen Lennox Sep 13 '15 at 19:22
  • Close. Keep in mind that the code inside `AuthorizeAttribute` may be called by multiple threads at the same time. So, setting a class-level variable with the current context could cause issues between users. Have a look at the `OnAuthorization` method in [this answer](http://stackoverflow.com/questions/32235660/unity-inject-dependencies-into-mvc-filter-class-with-parameters/32254851#32254851) (in the `ClaimsIdentityAuthorizationService`) for an approach to fix that. – NightOwl888 Sep 13 '15 at 19:31
  • Cool, thanks once more. I updated the example by adding `filterContext.HttpContext.Items["ActionDescriptor"] = filterContext.ActionDescriptor;` into the `OnAuthorisation()` method and accessing this from the `AuthorizeCore()` method. Is that a thread safe approach? Sorry for all the questions, I'm pretty noob and playing with authorization scares me. – Martin Hansen Lennox Sep 13 '15 at 20:15
  • `Is that a thread safe approach?` - Yes. The `HttpContext.Items` collection is guaranteed to always exist in the same request that called it (which may be even more granular than on the same thread). It seems odd to have to do so. It was clearly a design flaw by Microsoft not to use the `AuthorizationContext` in `AuthorizeCore`. Fortunately, they have fixed it in MVC 6 by passing the same context type all the way through. – NightOwl888 Sep 13 '15 at 20:23
  • It seems I was attempting something far more complex than I first thought... thanks v much for the info and advice, it's great to get it working :) – Martin Hansen Lennox Sep 13 '15 at 20:28