Click here to Skip to main content
15,867,141 members
Articles / Programming Languages / Visual Basic

Extending the Behavior of MVC AuthorizeAttribute for Activity-based Authorization

Rate me:
Please Sign up or sign in to vote.
5.00/5 (1 vote)
25 Jan 2013CPOL2 min read 51.7K   7  
Extending the behavior of MVC AuthorizeAttribute for activity-based authorization

If you’re familiar with NetSqlAzMan or CanCan, you know that checking permissions based on a user’s activities is easier to manage and more flexible than working with the roles a user is in. Whatever method you take to add activity based authorization, if you are working in MVC, you will run into the issue that AuthorizeAttribute only cares about Users and Roles. The good news is that you can inherit from AuthorizeAttribute and easily adapt it to account for activity-based authorization.

If you didn’t know already, you can browse the code for ASP.NET on CodePlex. Here’s the code for AuthorizeAttribute:

C#
1: // Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. 
   // See License.txt in the project root for license information.
   2:  
   3: using System.Diagnostics.CodeAnalysis;
   4: using System.Linq;
   5: using System.Security.Principal;
   6: using System.Web.Mvc.Properties;
   7:  
   8: namespace System.Web.Mvc
   9: {
  10:     [SuppressMessage("Microsoft.Performance", "CA1813:AvoidUnsealedAttributes", 
              Justification = "Unsealed so that subclassed types can set properties 
              in the default constructor or override our behavior.")]
  11:     [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, 
          Inherited = true, AllowMultiple = true)]
  12:     public class AuthorizeAttribute : FilterAttribute, IAuthorizationFilter
  13:     {
  14:         private readonly object _typeId = new object();
  15:  
  16:         private string _roles;
  17:         private string[] _rolesSplit = new string[0];
  18:         private string _users;
  19:         private string[] _usersSplit = new string[0];
  20:  
  21:         public string Roles
  22:         {
  23:             get { return _roles ?? String.Empty; }
  24:             set
  25:             {
  26:                 _roles = value;
  27:                 _rolesSplit = SplitString(value);
  28:             }
  29:         }
  30:  
  31:         public override object TypeId
  32:         {
  33:             get { return _typeId; }
  34:         }
  35:  
  36:         public string Users
  37:         {
  38:             get { return _users ?? String.Empty; }
  39:             set
  40:             {
  41:                 _users = value;
  42:                 _usersSplit = SplitString(value);
  43:             }
  44:         }
  45:  
  46:         // This method must be thread-safe 
              // since it is called by the thread-safe OnCacheAuthorization() method.
  47:         protected virtual bool AuthorizeCore(HttpContextBase httpContext)
  48:         {
  49:             if (httpContext == null)
  50:             {
  51:                 throw new ArgumentNullException("httpContext");
  52:             }
  53:  
  54:             IPrincipal user = httpContext.User;
  55:             if (!user.Identity.IsAuthenticated)
  56:             {
  57:                 return false;
  58:             }
  59:  
  60:             if (_usersSplit.Length > 0 && !_usersSplit.Contains(
                         user.Identity.Name, StringComparer.OrdinalIgnoreCase))
  61:             {
  62:                 return false;
  63:             }
  64:  
  65:             if (_rolesSplit.Length > 0 && !_rolesSplit.Any(user.IsInRole))
  66:             {
  67:                 return false;
  68:             }
  69:  
  70:             return true;
  71:         }
  72:  
  73:         private void CacheValidateHandler(HttpContext context, 
                      object data, ref HttpValidationStatus validationStatus)
  74:         {
  75:             validationStatus = OnCacheAuthorization(new HttpContextWrapper(context));
  76:         }
  77:  
  78:         public virtual void OnAuthorization(AuthorizationContext filterContext)
  79:         {
  80:             if (filterContext == null)
  81:             {
  82:                 throw new ArgumentNullException("filterContext");
  83:             }
  84:  
  85:             if (OutputCacheAttribute.IsChildActionCacheActive(filterContext))
  86:             {
  87:                 // If a child action cache block is active, we need to fail immediately, 
                      // even if authorization would have succeeded. 
  88:                 // The reason is that there's no way to hook a callback to rerun
  89:                 // authorization before the fragment is served from the cache, 
                      // so we can't guarantee that this
  90:                 // filter will be re-run on subsequent requests.
  91:                 throw new InvalidOperationException
                            (MvcResources.AuthorizeAttribute_CannotUseWithinChildActionCache);
  92:             }
  93:  
  94:             bool skipAuthorization = filterContext.ActionDescriptor.IsDefined
                                     (typeof(AllowAnonymousAttribute), inherit: true)
  95:                                || filterContext.ActionDescriptor.ControllerDescriptor.IsDefined(
                                     typeof(AllowAnonymousAttribute), inherit: true);
  96:  
  97:             if (skipAuthorization)
  98:             {
  99:                 return;
 100:             }
 101:  
 102:             if (AuthorizeCore(filterContext.HttpContext))
 103:             {
 104:                 // ** IMPORTANT **
 105:                 // Since we're performing authorization at the action level, 
                      // the authorization code runs after the output caching module. 
 106:                 // In the worst case, this could allow an authorized user to cause the page 
 107:                 // to be cached, then an unauthorized user would later be served the
 108:                 // cached page. We work around this by telling proxies not to cache 
                      // the sensitive page, then we hook our custom authorization code into 
 109:                 // the caching mechanism so that we have
 110:                 // the final say on whether a page should be served from the cache.
 111:  
 112:                 HttpCachePolicyBase cachePolicy = filterContext.HttpContext.Response.Cache;
 113:                 cachePolicy.SetProxyMaxAge(new TimeSpan(0));
 114:                 cachePolicy.AddValidationCallback(CacheValidateHandler, null /* data */);
 115:             }
 116:             else
 117:             {
 118:                 HandleUnauthorizedRequest(filterContext);
 119:             }
 120:         }
 121:  
 122:         protected virtual void HandleUnauthorizedRequest(AuthorizationContext filterContext)
 123:         {
 124:             // Returns HTTP 401 - see comment in HttpUnauthorizedResult.cs.
 125:             filterContext.Result = new HttpUnauthorizedResult();
 126:         }
 127:  
 128:         // This method must be thread-safe since it is called by the caching module.
 129:         protected virtual HttpValidationStatus OnCacheAuthorization(HttpContextBase httpContext)
 130:         {
 131:             if (httpContext == null)
 132:             {
 133:                 throw new ArgumentNullException("httpContext");
 134:             }
 135:  
 136:             bool isAuthorized = AuthorizeCore(httpContext);
 137:             return (isAuthorized) ? HttpValidationStatus.Valid : 
                                    HttpValidationStatus.IgnoreThisRequest;
 138:         }
 139:  
 140:         internal static string[] SplitString(string original)
 141:         {
 142:             if (String.IsNullOrEmpty(original))
 143:             {
 144:                 return new string[0];
 145:             }
 146:  
 147:             var split = from piece in original.Split(',')
 148:                         let trimmed = piece.Trim()
 149:                         where !String.IsNullOrEmpty(trimmed)
 150:                         select trimmed;
 151:             return split.ToArray();
 152:         }
 153:     }
 154: }

The method that manages whether or not a user is authorized is the AuthorizeCore method:

C#
protected virtual bool AuthorizeCore(HttpContextBase httpContext)
   2: {
   3:     if (httpContext == null)
   4:     {
   5:         throw new ArgumentNullException("httpContext");
   6:     }
   7:  
   8:     IPrincipal user = httpContext.User;
   9:     if (!user.Identity.IsAuthenticated)
  10:     {
  11:         return false;
  12:     }
  13:  
  14:     if (_usersSplit.Length > 0 && !_usersSplit.Contains(user.Identity.Name, 
                StringComparer.OrdinalIgnoreCase))
  15:     {
  16:         return false;
  17:     }
  18:  
  19:     if (_rolesSplit.Length > 0 && !_rolesSplit.Any(user.IsInRole))
  20:     {
  21:         return false;
  22:     }
  23:  
  24:     return true;
  25: }

Going through the code:

  1. If the httpContext is null, error out.
  2. If the user isn’t authenticated, return false.
  3. If the required users list isn’t empty and the user isn’t in that list, return false.
  4. If the required roles list isn’t empty and the user isn’t in a role in that list, return false.
  5. If we made it this far, the user is good to go.

To add activities, we simply need an additional step:

  • If the activity list isn’t empty and the user doesn’t have permission for that activity, return false.

To do this, we can simply subclass AuthorizeAttribute and override the AuthorizeCore method:

In VB:

VB.NET
 1: Protected Overrides Function AuthorizeCore(httpContext As HttpContextBase) As Boolean
 2:     If httpContext Is Nothing Then
 3:         Throw New ArgumentNullException("httpContext")
 4:     End If
 5:
 6:     Dim user As IPrincipal = httpContext.User
 7:     If Not user.Identity.IsAuthenticated Then
 8:         Return False
 9:     End If
10:
11:     If Actions.Length > 0 Then
12:         Dim component = New SiteActionsComponent()
13:         Dim roles = component.GetRolesForSiteActions(Actions)
14:         If Not roles.Any(user.IsInRole) Then
15:             Return False
16:         End If
17:     End If
18:
19:     Return MyBase.AuthorizeCore(httpContext)
20: End Function

In C#:

C#
 1: protected override bool AuthorizeCore(HttpContextBase httpContext)
 2: {
 3:     if (httpContext == null)
 4:     {
 5:         throw new ArgumentNullException("httpContext");
 6:     }
 7:
 8:     IPrincipal user = httpContext.User;
 9:     if (!user.Identity.IsAuthenticated)
10:     {
11:         return false;
12:     }
13:
14:     if (Actions.Length > 0)
15:     {
16:         var component = new SiteActionsComponent();
17:         var roles = component.GetRolesForSiteActions(Actions);
18:         if(!roles.Any(user.IsInRole))
19:         {
20:             return false;
21:         }
22:     }
23:
24:     return base.AuthorizeCore(httpContext);
25: }

This overridden method gives us the following:

  1. if the httpContext is null, error out.
  2. If the activity list isn’t empty and the user doesn’t have permission for that activity, return false.
  3. If we made it this far, check the base method (the steps above).

In the example above, the SiteActionsComponent is a business component that provides the lists of roles that the user could be in to satisfy the need for the listed actions. The example comes from a project that uses activities in combination with WebSecurity, and I wanted to avoid additional complication such as custom security providers / principals. You will need a similar provider in order to use this method.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
United States United States
Stacy is a VB.NET developer with over two years experience in Microsoft Development. His experiences include Visual FoxPro 9, VB6, VB.NET for frameworks 1.1, 2.0, and 3.5.

Stacy maintains the blog http://www.wtfnext.com on VB.NET technologies and his personal life, and he is active in the Baton Rouge and Second Life DNUGs.

Comments and Discussions

 
-- There are no messages in this forum --