Granular permissions with specific requirements for the MVC site

I don't like built-in membership providers. I decided to quit mine. I am trying to come up with a good method for performing authorization at the action level. Here are the requirements I'm trying to fulfill:

  • Using the attribute - I like it because it manages at a very high level in the call stack and is a good place to organize permissions.
  • No magic lines. This is the reason I am deviating from existing role providers. I do not want to leave the lines around that cannot be easily renamed.
  • Permissions must consist of one other permit. Example: ReadWrite has permission for Read . Same as or with listing.

NOTE. Some people find this set of requirements too broad (see comments). I don’t think so, I think they are quite simple.

The biggest showstopper is the use of attributes. There can only be “constant expressions, type expressions, or expressions to create an attribute type array”.

I thought maybe something like this so that the operations have static access. Inside the attribute, it "converts" the int to the actual resolution or something else ...:

 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 the composition. I’m sure you are thinking, “Why don’t you go through the transfer route?” well, I want to plan things to change and don’t want to limit 32 (int) or 64 (long) operations and have to do bulk rewriting later (also in db, which is just ugly).

Also, if there is a better alternative than attributes for actions / controllers, then I am all ear for suggestions.

EDIT: Also from this post I read about the BitArray class. This seems ugly, especially with arbitrary storage in the database.

+8
c # asp.net-mvc permissions
source share
3 answers

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

This is a long answer and is just a starting point. You need to figure out how to assign roles to users and how to recreate them in AuthenticateRequest .

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

Beautify Controller Actions

I started decorating the two default actions of the HomeController :

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

Then, all users of the ReadWrite role are granted access. I have chosen to use an enumeration here as a safe placeholder for magic strings. The role of this listing is nothing but a placeholder. There are no composite enumeration values ​​that need to be supported elsewhere. More on this later.

 public enum Role { Read, Write, ReadWrite } 

Implement a new authorization attribute

Since the lines have disappeared, 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); } } 

RoleSet wraps a set of enum values ​​and checks if the IPrincipal 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; } } 

Support roles

CompositeRoleSet is that composite roles are registered and processed. CreateDefault() is where all the composites are registered. Resolve() will take a list of roles (enumeration values) and convert the composites into their single copies.

 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

We need an authenticated user to work. I cheated and encoded it 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 that understands all of 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"); } } } 
+5
source share

It looks like you want something very flexible and no matter what you might need for a security check. So it depends on "how far you are ready to go."

To help you in the right direction, I highly recommend that you look towards Access Control based on requirements . And take this article as a starting point and an example of ASP.NET MVC.

But remember that this is a complex theme. Very flexible (even allowing Federated Access Control without any code changes), but complicated.

We had to go this way to make our applications completely inaccessible for these “correct checks”. All our systems know that they need to “require” certain actions and request them based on the provided user identification (which is also a “requirement”). Roles, permissions and other requirements can be easily translated to those applications that correspond to specific applications that make sense for our applications. Full flexibility.

PS It does not solve the technical problems of "magic lines" and the like (you should think that it depends on your situation), but it gives you a very flexible access control architecture.

0
source share

So @thomas seems to have a good answer, but it wraps your requirement for using enums more, taking it into roles that IPricipal will understand. My solution is bottom-up, so you can use the thomas solution on top of mine to implement IPrincipal

I really needed something similar to what you want, and I was always afraid by using forms authentication (yes, you are scared too, and I know that, but listen to me). Therefore, I always issued my own cheap authentication using forms, but a lot of things changed when I studied mvc (in the last couple of weeks) Forms auth is very separate and very flexible. Essentially, you don't really use auth forms, but just include your own logic in the system.

So, this is how I dealt with this, ( beware, I'm a student myself ).

Summary:

  • You are going to override some auth classes to authenticate your users (you can even mock this)
  • Then you will create IIdentity .
  • Download the GenericPrincipal list of roles in rows (I know no magic lines ... keep reading)

Once you do this, MVC understands enough to give you what you want! Now you can use [Authorize(Roles = "Write,Read")] on any controller, and MVC will do almost everything. Now, without the magic lines, all you need to do is create a wrapper around this attribute.


Long answer

You are using the Internet application template that comes with MVC. So first you start by creating an MVC project, in a new dialog box, say you want an Internet application .

When validating the application, it will have one main class that overrides forms authentication. IMembershipService Remove the localhipProvider __provider_ variable and in this class you should add logic to the ValidateUser method if possible. (Try adding fake authentication to one user / pass) Also see the default vv application created in VS.

Introduce IIdentity

  public class MyIdentity : IIdentity { public MyIdentity(string username) { _username = username;//auth from the DB here. //load up the Roles from db or whatever } string _username; public User UserData { get; set; } #region IIdentity Members public string AuthenticationType { get { return "MyOwn.Authentication"; } } public bool IsAuthenticated { get { return true; } } public string Name { get { return _username; } } #endregion public string[] Roles { get { return //get a list of roles as strings from your Db or something. } } } 

Remember that we still use the default web application template that comes with the MVC project.

So, now AccountController.LogOn () should look like this:

  [HttpPost] public virtual ActionResult LogOn(LogOnModel model, string returnUrl) { if (ModelState.IsValid) { if (MembershipService.ValidateUser(model.UserName, model.Password)) { FormsService.SignIn(model.UserName, model.RememberMe); FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(model.UserName, model.RememberMe, 15); string encTicket = FormsAuthentication.Encrypt(ticket); this.Response.Cookies.Add(new HttpCookie(FormsAuthentication.FormsCookieName, encTicket)); if (Url.IsLocalUrl(returnUrl)) { return Redirect(returnUrl); } else { return RedirectToAction("Index", "Home"); } } else { ModelState.AddModelError("", "The user name or password provided is incorrect."); } } 

So what you do is set a form ticket, for example a session, and then we will read it for each request, for example: Put this in Global.asax.cs

 public override void Init() { this.PostAuthenticateRequest += new EventHandler(MvcApplication_PostAuthenticateRequest); base.Init(); } void MvcApplication_PostAuthenticateRequest(object sender, EventArgs e) { HttpCookie authCookie = HttpContext.Current.Request.Cookies[FormsAuthentication.FormsCookieName]; if (authCookie != null) { string encTicket = authCookie.Value; if (!String.IsNullOrEmpty(encTicket)) { FormsAuthenticationTicket ticket = FormsAuthentication.Decrypt(encTicket); MyIdentity id = new MyIdentity(ticket.Name); //HERE is where the magic happens!! GenericPrincipal prin = new GenericPrincipal(id, id.Roles); HttpContext.Current.User = prin; } } } 

I asked one question: how effective and correct is the method above here .

Ok, now you're almost done, you can decorate your controllers as follows: [Authorize(Roles="RoleA,RoleB")] (more on the lines later)

There is one small problem here, if you decorate the controller using AuthorizeAttribute , and the registered user does not have specific permission, instead of specifying "access denied" by default, the user will be redirected to the login page to log in again. You fix it as follows (I changed this from SO answer):

 public class RoleAuthorizeAttribute : AuthorizeAttribute { protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext) { // Returns HTTP 401 // If user is not logged in prompt if (!filterContext.HttpContext.User.Identity.IsAuthenticated) { base.HandleUnauthorizedRequest(filterContext); } // Otherwise deny access else { filterContext.Result = new RedirectToRouteResult(@"Default", new RouteValueDictionary{ {"controller","Account"}, {"action","NotAuthorized"} }); } } } 

Now all you do is add another wrapper around AuthorizeAttribute to support strong types that will translate into strings that Principal expects. See this article for more details .

I plan to update my application to use strong types later, then I will update this answer.

I hope this helps.

0
source share

Source: https://habr.com/ru/post/649846/


All Articles