28

ASP.NET MVC has good support for role-based security, but the usage of strings as role names is maddening, simply because they cannot be strongly-typed as enumerations.

For example, I have an "Admin" role in my app. The "Admin" string will now exist in the Authorize attribute of my action, in my master page (for hiding a tab), in my database (for defining the roles available to each user), and any other place in my code or view files where I need to perform special logic for admin or non-admin users.

Is there a better solution, short of writing my own authorization attribute and filter, that would perhaps deal with a collection of enumeration values?

MikeWyatt
  • 7,842
  • 10
  • 50
  • 71

6 Answers6

53

Using magic strings gives you the flexibility to declare multiple roles in the Authorize attribute (e.g. [Authorize(Roles = "Admin, Moderator")] which you tend to lose as you go to a strongly typed solution. But here's how you can maintain this flexibility while still getting everything strongly typed.

Define your roles in an enum that uses bit flags:

[Flags]
public enum AppRole {
    Admin = 1,
    Moderator = 2,
    Editor = 4,
    Contributor = 8,
    User = 16
}

Override AuthorizeAttribute:

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class MyAuthorizeAttribute : AuthorizeAttribute {

    public AppRole AppRole { get; set; }

    public override void OnAuthorization(AuthorizationContext filterContext) {
        if (AppRole != 0)
            Roles = AppRole.ToString();

        base.OnAuthorization(filterContext);
    }

}

Now if you can use MyAuthorizeAttribute like this:

[MyAuthorize(AppRole = AppRole.Admin | AppRole.Moderator | AppRole.Editor)]
public ActionResult Index() {

    return View();
}

The above action will only authorize users that are in at least one of the roles listed (Admin, Moderator, or Editor). The behavior is the same as MVC's default AuthorizeAttribute, except without the magic strings.

If you use this technique, here's an extension method on IPrincipal that may also be useful:

public static class PrincipalExtensions {

    public static bool IsInRole(this IPrincipal user, AppRole appRole) {

        var roles = appRole.ToString().Split(',').Select(x => x.Trim());
        foreach (var role in roles) {
            if (user.IsInRole(role))
                return true;
        }

        return false;
    }
}

You can use this extension method like this:

public ActionResult Index() {
    var allowed = User.IsInRole(AppRole.Admin | AppRole.Moderator | AppRole.Editor);

    if (!allowed) {
       // Do Something
    }

    return View();
}
Johnny Oshika
  • 54,741
  • 40
  • 181
  • 275
22

I usually use a class with a bunch of string constants. It's not a perfect solution, since you need to remember to stick to using it everywhere, but at least it gets rid of the possibility of typos.

static class Role {
    public const string Admin = "Admin";
}
Matti Virkkunen
  • 63,558
  • 9
  • 127
  • 159
  • I went with this solution due to it's simplicity. The code changes were minimal, since I only had to replace hard-coded strings with constant references. – MikeWyatt May 14 '10 at 21:01
12

Although it doesn't use enums, I've used the solution below, where we sub-class the Authorize filter to take in variable length role name arguments in the constructor. Using this together with role names declared in const variables somewhere, we avoid magic strings:

public class AuthorizeRolesAttribute : AuthorizeAttribute
{
    public AuthorizeRolesAttribute(params string[] roles) : base()
    {
        Roles = string.Join(",", roles);
    }
}

public class MyController : Controller
{
    private const string AdministratorRole = "Administrator";
    private const string AssistantRole = "Assistant";

    [AuthorizeRoles(AdministratorRole, AssistantRole)]
    public ActionResult AdminOrAssistant()
    {                        
        return View();
    }
}

(I blogged about this in a little bit more detail - http://tech-journals.com/jonow/2011/05/19/avoiding-magic-strings-in-asp-net-mvc-authorize-filters)

JonoW
  • 14,029
  • 3
  • 33
  • 31
  • How would you customize this further making it a function or delegate? i.e. user=> user.Role == AssistantRole || user.Role == BigGuy... Some actions may want one role and not the other, some may want 2 roles or a 3rd role, I hope I am clear??? :) – Haroon Jul 01 '11 at 11:27
4

I took JohnnyO's response but changed the enumeration items to use the DescriptionAttribute to specify the string value for the role. This comes in handy if you want your role string to be different from the Enum name.

The enum example:

[Flags]
public enum AppRole
{
    [Description("myRole_1")]
    RoleOne = 1,
    [Description("myRole_2")]
    RoleTwo = 2
}

The extension method:

public static bool IsInRole(this IPrincipal user, AppRole appRole)
{
    var roles = new List<string>();
    foreach (var role in (AppRole[])Enum.GetValues(typeof(AppRole)))
        if ((appRole & role) != 0)
            roles.Add(role.ToDescription());

    return roles.Any(user.IsInRole);
}

The custom attribute:

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class AppAuthorizeAttribute : AuthorizeAttribute
{
    public AppRole AppRoles { get; set; }

    public override void OnAuthorization(AuthorizationContext filterContext)
    {
        var roles = new List<string>();
        foreach (var role in (AppRole[])Enum.GetValues(typeof(AppRole)))
            if((AppRoles & role) != 0)
                roles.Add(role.ToDescription());

        if (roles.Count > 0)
            Roles = string.Join(",", roles);

        base.OnAuthorization(filterContext);
    }
}

Extension method to get the description value:

public static string ToDescription(this Enum value)
{
    var da = (DescriptionAttribute[])
             (value.GetType().GetField(value.ToString()))
                 .GetCustomAttributes(typeof (DescriptionAttribute), false);
    return da.Length > 0 ? da[0].Description : value.ToString();
}
Alex
  • 9,250
  • 11
  • 70
  • 81
3

It's not that hard to customize AuthorizeAttribute in the way you suggest.

Subtype it, add a custom property for your enum type, and call ToString() on the passed value. Put that in the regular roles property. This should take just a few lines of code, and AuthorizeAttribute still does all the real work.

+1 for Matti, too, since consts are also a good choice.

Craig Stuntz
  • 125,891
  • 12
  • 252
  • 273
2

I have used a static class defining a bunch of string constants as suggested by Matti and on my current project I use the below extension method with an enum. Both approaches work very well.

public static class EnumerationExtension
{
  public static string GetName(this Enum e)
  {
    return Enum.GetName(e.GetType(), e);
  }
}
ScottS
  • 8,455
  • 3
  • 30
  • 50