Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Granular permissions with certain requirements for an MVC site

I don't like the built in membership providers. I've decided to roll my own. I'm trying to come up with a good method for performing authorization at the action level. Here are the requirements that I'm trying to go by:

  1. Attribute usage - I like this since it controls at a very high level in the call stack and is a nice place to organize permissions.
  2. No magic strings - This is a reason why I'm straying away from the current role providers. I don't want to leave strings lying around that can't be easily renamed.
  3. Permissions should can be composed of one other permission. Example: ReadWrite has permission for Read. Just like or'ing with an enum.

NOTE: Some think this set of requirements is too broad (see comments). I don't think so, I think they're fairly straightforward.

The biggest showstopper is attribute usage. There can only be "constant expressions, typeof expressions or array creation expression of an attribute parameter type".

I was thinking of perhaps having something like this to make operations have static access. Inside of the attribute, it would "convert" the int to the actual Permission or something...:

public static class Operations
{
    public static class SectionA
    {
        public const int Read = 1;
        public const int ReadWrite = 2;
    }

    public static class SectionB
    {
        // ... and so on...
    }
}

But it really limits composition. I'm sure you're thinking "why don't you go the enum route?" well I want to plan for things to change and don't want to limit to 32 (int) or 64 (long) operations and have to do a massive rewrite later (also in the db that's just ugly).

Also, if there is a better alternative than attributes on actions/controllers, then I'm all ears for suggestions.

EDIT: Also from this post, I've read about the BitArray class. It seems kind of ugly, especially with the arbitrary storage in the database.

like image 775
TheCloudlessSky Avatar asked Dec 05 '10 16:12

TheCloudlessSky


1 Answers

First of all, I have to thank you for sucking me into answering this ;)

This is a long answer, and is only a starting point. You have to figure out how to assign roles to users and how to recreate them in the AuthenticateRequest.

If this does not answer your question, I hope it will be an inspiration. Enjoy!

Decorate the controller actions

I started to decorate the two actions in the default HomeController:

    [AuthorizeRoles(Role.Read)]
    public ActionResult Index()
    {
        ViewData["Message"] = "Welcome to ASP.NET MVC!";

        return View();
    }

    [AuthorizeRoles(Role.Write)]
    public ActionResult About()
    {
        return View();
    }

All users in the ReadWrite role should then be granted access. I opted here to use an enum as a type safe placeholder for the magic strings. The role of this enum is nothing else than being a placeholder. There are no composite enum values, that has to be maintained somewhere else. More on that later.

public enum Role
{
    Read,
    Write,
    ReadWrite
}

Implement a new authorization attribute

Since the strings are gone, I need a new authorize attribute:

public class AuthorizeRolesAttribute : AuthorizeAttribute
{
    private readonly RoleSet authorizedRoles;

    public AuthorizeRolesAttribute(params Role[] roles)
    {
        authorizedRoles = new RoleSet(roles);
    }

    protected override bool AuthorizeCore(HttpContextBase httpContext)
    {
        return authorizedRoles.Includes(httpContext.User);
    }
}

The RoleSet wraps a set of enum values and verifies if an IPrincipal is a member of one of them:

public class RoleSet
{
    public RoleSet(IEnumerable<Role> roles)
    {
        Names = roles.Select(role => role.ToString());
    }

    public bool Includes(IPrincipal user)
    {
        return Names.Any(user.IsInRole);
    }

    public bool Includes(string role)
    {
        return Names.Contains(role);
    }

    public IEnumerable<string> Names { get; private set; }
}

Maintain roles

The CompositeRoleSet is where composite roles are registered and handled. CreateDefault() is where all composites are registered. Resolve() will take a list of roles (enum values) and convert the composites to their single counterparts.

public class CompositeRoleSet
{
    public static CompositeRoleSet CreateDefault()
    {
        var set = new CompositeRoleSet();
        set.Register(Role.ReadWrite, Role.Read, Role.Write);
        return set;
    }

    private readonly Dictionary<Role, Role[]> compositeRoles = new Dictionary<Role, Role[]>();

    private void Register(Role composite, params Role[] contains)
    {
        compositeRoles.Add(composite, contains);
    }

    public RoleSet Resolve(params Role[] roles)
    {
        return new RoleSet(roles.SelectMany(Resolve));
    }

    private IEnumerable<Role> Resolve(Role role)
    {
        Role[] roles;
        if (compositeRoles.TryGetValue(role, out roles) == false)
        {
            roles = new[] {role};
        }

        return roles;
    }
}

Wiring it up

We need an authenticated user to work on. I cheated and hard-coded one in global.asax:

    public MvcApplication()
    {
        AuthenticateRequest += OnAuthenticateRequest;
    }

    private void OnAuthenticateRequest(object sender, EventArgs eventArgs)
    {
        var allRoles = CompositeRoleSet.CreateDefault();
        var roles = allRoles.Resolve(Role.ReadWrite);
        Context.User = new ApplicationUser(roles);
    }

Finally, we need an IPrincipal which understand all this:

public class ApplicationUser : IPrincipal
{
    private readonly RoleSet roles;

    public ApplicationUser(RoleSet roles)
    {
        this.roles = roles;
    }

    public bool IsInRole(string role)
    {
        return roles.Includes(role);
    }

    public IIdentity Identity
    {
        get { return new GenericIdentity("User"); }
    }
}
like image 168
Thomas Eyde Avatar answered Nov 09 '22 07:11

Thomas Eyde