Extending the Behavior of MVC AuthorizeAttribute for Activity-based Authorization





5.00/5 (1 vote)
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:
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:
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:
- If the
httpContext
isnull
, error out. - If the user isn’t authenticated, return
false
. - If the required users list isn’t empty and the user isn’t in that list, return
false
. - If the required roles list isn’t empty and the user isn’t in a role in that list, return
false
. - 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:
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#:
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:
- if the
httpContext
isnull
, error out. - If the activity list isn’t empty and the user doesn’t have permission for that activity, return
false
. - 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.