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: 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: 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: 88: 89: 90: 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: 105: 106: 107: 108: 109: 110: 111:
112: HttpCachePolicyBase cachePolicy = filterContext.HttpContext.Response.Cache;
113: cachePolicy.SetProxyMaxAge(new TimeSpan(0));
114: cachePolicy.AddValidationCallback(CacheValidateHandler, null );
115: }
116: else
117: {
118: HandleUnauthorizedRequest(filterContext);
119: }
120: }
121:
122: protected virtual void HandleUnauthorizedRequest(AuthorizationContext filterContext)
123: {
124: 125: filterContext.Result = new HttpUnauthorizedResult();
126: }
127:
128: 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
is null
, 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
is null
, 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.