Access control in ASP.NET MVC depending on input parameters / service level?

Preamble: This is a bit of a philosophical question. I'm looking more for the “right” way to do it, rather than the “way” to do it.

Suppose I have some products, and an ASP.NET MVC application running CRUD for these products: -

mysite.example/products/1 mysite.example/products/1/edit 

I use the repository template, so it doesn't matter where these products come from: -

 public interface IProductRepository { IEnumberable<Product> GetProducts(); .... } 

My repository also describes the list of users and what products they are managers for (many, many between users and products). Elsewhere in the application, Super-Admin runs CRUD for users and manages the relationship between Users and Products that they are allowed to manage.

Anyone is allowed to view any product, but only users who are designated as "admins" for a particular product are allowed to call, for example, the "Change" action.

How should it work with ASP.NET MVC? If I missed something, I can’t use the built-in ASP.NET authorization attribute, because at first I would need a different role for each product, and secondly, I won’t know what role to check until I find I’ve removed my product from repository.

Obviously, you can generalize this scenario to most content management scenarios - for example, Users are allowed to edit their own Forum posts. StackOverflow users are allowed to edit their own questions - if they do not have 2000 or more posts ...

The simplest solution, for example, would be something like: -

 public class ProductsController { public ActionResult Edit(int id) { Product p = ProductRepository.GetProductById(id); User u = UserService.GetUser(); // Gets the currently logged in user if (ProductAdminService.UserIsAdminForProduct(u, p)) { return View(p); } else { return RedirectToAction("AccessDenied"); } } } 

My problems:

  • Some of this code will need to be repeated - imagine that depending on the User-Products relationship there are several operations (Update, Delete, SetStock, Order, CreateOffer). You will have to copy-paste several times.
  • This is not very easy to verify - you have to prototype four objects for each test with my account.
  • Actually, the controller’s “job” doesn’t seem to check if the user is allowed to perform the action. I would prefer a more flexible (e.g. AOP through attributes) solution. However, does this necessarily mean that you have to double-select the product (once in the AuthorizationFilter and again in the controller)?
  • Would it be better to return 403 if the user is not allowed to make this request? If so, how do I do this?

I will probably continue this update as I get ideas myself, but I really want to hear yours!

Thanks in advance!

Edit

Just add some details here. The problem I am facing is that I want the "Only users with permission to edit products" business rule, which should be contained in one and only one place. I feel that the same code that determines whether the user can execute GET or POST for the Edit action should also be responsible for determining whether to display the Edit link in the Index or Details views. It may not be possible / impossible, but I feel that it should be ...

Edit 2

Running bounty on this. I received good and useful answers, but nothing that I was comfortable with "accepting." Keep in mind that I'm looking for a good clean method for business logic to determine whether the "Edit" link will be displayed on the index view in the same place that determines whether the product request / edit / 1 is allowed or not. I would like the pollution in my method of action to reach an absolute minimum. Ideally, I am looking for an attribute-based solution, but I agree that this is not possible.

+41
asp.net-mvc
Aug 26 '09 at 14:54
source share
8 answers

First of all, I think that you halfway understood this, because you said that

since at first I would need a different role for each product, and secondly, I won’t know what role to check until I retrieve my product from the repository

I saw that many attempts to create role-based protection do what she never intended to do, but you have already passed this point, so cool :)

An alternative to role-based security is ACL-based security, and I think this is what you need here.

You still need to get the ACL for the product and then check if the user has the correct permission for the product. It is so sensitive to context and heavy interaction that I believe that the purely declarative approach is too inflexible and too implicit (i.e. you cannot understand how many database reads are associated with adding one attribute to some code).

I think such scenarios are best modeled by a class that encapsulates the ACL logic, allowing you to either request a solution or make an assertion based on the current context - something like this:

 var p = this.ProductRepository.GetProductById(id); var user = this.GetUser(); var permission = new ProductEditPermission(p); 

If you just want to find out if the user can edit the product, you can send a request:

 bool canEdit = permission.IsGrantedTo(user); 

If you just want the user to have rights to continue, you can post a statement:

 permission.Demand(user); 

This should then throw an exception if permission is not granted.

All this assumes that the Product class ( p variable) has an associated ACL, for example:

 public class Product { public IEnumerable<ProductAccessRule> AccessRules { get; } // other members... } 

You might want to take a look at System.Security.AccessControl.FileSystemSecurity for inspiration regarding ACL modeling.

If the current user matches Thread.CurrentPrincipal (which is the case in ASP.NET MVC, IIRC), you can simply use the resolution methods above to:

 bool canEdit = permission.IsGranted(); 

or

 permission.Demand(); 

because the user will be implicit. You can take a look at System.Security.Permissions.PrincipalPermission for inspiration.

+29
Aug 26 '09 at 17:44
source share
— -

From what you are describing, it looks like you need some form of user access control, not role-based permissions. If so, then this should be implemented in your business logic. Your script sounds like you can implement it at your service level.

Basically, you need to implement all the functions in the ProductRepository from the point of view of the current user, and the products will be marked with permissions for this user.

Sounds harder than it really is. First, you need a token user interface that contains information about the uid user and the list of roles (if you want to use roles). You can use IPrincipal or create your own line by line

 public interface IUserToken { public int Uid { get; } public bool IsInRole(string role); } 

Then in your controller, you parse the user token into your repository constructor.

 IProductRepository ProductRepository = new ProductRepository(User); //using IPrincipal 

If you are using FormsAuthentication and a custom IUserToken, then you can create a Wrapper around IPrincipal so that your ProductRepository is created as follows:

 IProductRepository ProductRepository = new ProductRepository(new IUserTokenWrapper(User)); 

Now all your IProductRepository functions should be able to access the user's token in order to check permissions. For example:

 public Product GetProductById(productId) { Product product = InternalGetProductById(UserToken.uid, productId); if (product == null) { throw new NotAuthorizedException(); } product.CanEdit = ( UserToken.IsInRole("admin") || //user is administrator UserToken.Uid == product.CreatedByID || //user is creator HasUserPermissionToEdit(UserToken.Uid, productId) //other custom permissions ); } 

If you are interested in knowing a list of all products, in the data access code you can request a request based on permission. In your case, the left join is to see if the many-to-many table contains UserToken.Uid and productId. If the right side of the connection is present, you know that the user has permission for this product, and then you can install your Product.CanEdit boolean.

Using this method, you can use the following, if you want, in your view (where Model is your product).

 <% if(Model.CanEdit) { %> <a href="/Products/1/Edit">Edit</a> <% } %> 

or in your controller

 public ActionResult Get(int id) { Product p = ProductRepository.GetProductById(id); if (p.CanEdit) { return View("EditProduct"); } else { return View("Product"); } } 

The advantage of this method is that security is built into your service level (ProductRepository), so it is not processed by your controllers and cannot be bypassed by your controller.

The main thing is that security is placed in your business logic, and not in your controller.

+16
Aug 27 '09 at 17:02
source share

Copy paste solutions do get tedious after a while, and it is really annoying to maintain. I would probably go with a custom attribute, doing what you need. You can use the excellent .NET Reflector to see how AuthorizeAttribute is implemented and execute your own logic.

What he does is inherit FilterAttribute and implement IAuthorizationFilter. I cannot verify this at the moment, but something like this should work.

 [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = true, AllowMultiple = true)] public class ProductAuthorizeAttribute : FilterAttribute, IAuthorizationFilter { public void OnAuthorization(AuthorizationContext filterContext) { if (filterContext == null) { throw new ArgumentNullException("filterContext"); } object productId; if (!filterContext.RouteData.Values.TryGetValue("productId", out productId)) { filterContext.Result = new HttpUnauthorizedResult(); return; } // Fetch product and check for accessrights if (user.IsAuthorizedFor(productId)) { HttpCachePolicyBase cache = filterContext.HttpContext.Response.Cache; cache.SetProxyMaxAge(new TimeSpan(0L)); cache.AddValidationCallback(new HttpCacheValidateHandler(this.Validate), null); } else filterContext.Result = new HttpUnauthorizedResult(); } private void Validate(HttpContext context, object data, ref HttpValidationStatus validationStatus) { // The original attribute performs some validation in here as well, not sure it is needed though validationStatus = HttpValidationStatus.Valid; } } 

Perhaps you can also save the product / user that you select in the filterContext.Controller.TempData file so that you can get it in the controller or save it in the cache.

Edit: I just noticed part of the edit link. The best way I can think of is to separate the authorization part from the attribute and make HttpHelper for it, which you can use in your view.

+3
Sep 11 '09 at 8:57
source share

I tend to think that authorization is part of your business logic (or at least outside the controller logic). I agree with kevingessner above, since authorization verification should be part of the call to retrieve the item. In the OnException method, you can show the login page (or everything that you configured in the web.config file) as follows:

 if (...) { Response.StatusCode = 401; Response.StatusDescription = "Unauthorized"; HttpContext.Response.End(); } 

And instead of making calls to UserRepository.GetUserSomehowFromTheRequest () in all action methods, I would do it once (for example, overriding the Controller.OnAuthorization method), then bind this data somewhere in the base class of the controller for later use (for example , property).

+1
Aug 27 '09 at 3:00
source share

I think this is unrealistic and breaks the separation of problems, expect the controller / code model to control what the view renders. Controller / model code can set a flag in the view model, which can use the view to determine what it should do, but I don’t think you should expect one method to be used by both the controller / model and the view to control access and rendering the model.

Having said that, you can approach this in one of two ways - both will include a view model that contains some annotations used by the view in addition to the real model. In the first case, you can use the attribute to control access to the action. This would be my advantage, but it would be related to decorating each method independently - if all actions in the controller do not have the same access attributes.

I developed the role or owner attribute for this purpose only. It checks that the user is in a specific role or is the owner of the data created by this method. Ownership, in my case, is controlled by the presence of a foreign key relationship between the user and the data, that is, you have a ProductOwner table, and there should be a row containing the product / owner pair for the product and the current user. It differs from the usual AuthorizeAttribute in that when verification of ownership or role is not performed, the user is redirected to the error page, and not to the login page. In this case, each method needs to set a flag in the view model, which indicates that the model can be edited.

Alternatively, you can implement the same code in the ActionExecuting / ActionExecuted methods of the controller (or the base controller, so that it is applied consistently to all controllers). In this case, you will need to write code to determine which action is performed, so you know whether to interrupt the action based on ownership of the product. The same method would set a flag indicating that the model could be edited. In this case, you probably need a model hierarchy so that you can apply the model as an editable model so that you can set the property regardless of the specific type of model.

This parameter seems to be more related to me than using the attribute, and possibly more complicated. In the case of an attribute, you can configure it to use different table and property names as attributes for the attribute and use reflection to get the correct data from your repository based on the attribute properties.

+1
Sep 13 '09 at 23:11
source share

Answering my own question (eep!), Chapter 1 Professional ASP.NET MVC 1.0 (NerdDinner tutorial) recommends a similar solution for mine above:

 public ActionResult Edit(int id) { Dinner dinner = dinnerRepositor.GetDinner(id); if(!dinner.IsHostedBy(User.Identity.Name)) return View("InvalidOwner"); return View(new DinnerFormViewModel(dinner)); } 

Besides the fact that I’m hungry at my dinner, this actually doesn’t add anything, because in this tutorial the code that implements the business rule is repeated immediately in the corresponding POST-action method and in the “Details” view (in fact, in the child partial type of detail)

Does this violate SRP? If the business rule has changed (so that, for example, anyone with RSVP'd can edit lunch), you will have to change the GET and POST methods, as well as the View methods (both GET and POST and View for the delete operation), although this is technically a separate business rule).

Pulls logic into some kind of permission arbitrage object (as I did above) as good as it gets?

0
Aug 26 '09 at 15:19
source share

You are on the right track, but you can encapsulate the entire rights check on one method, for example GetProductForUser , which accepts the product, user and required permission. Throwing an exception caught in the OnException handler of the controller, the processing is performed in one place:

 enum Permission { Forbidden = 0, Access = 1, Admin = 2 } public class ProductForbiddenException : Exception { } public class ProductsController { public Product GetProductForUser(int id, User u, Permission perm) { Product p = ProductRepository.GetProductById(id); if (ProductPermissionService.UserPermission(u, p) < perm) { throw new ProductForbiddenException(); } return p; } public ActionResult Edit(int id) { User u = UserRepository.GetUserSomehowFromTheRequest(); Product p = GetProductForUser(id, u, Permission.Admin); return View(p); } public ActionResult View(int id) { User u = UserRepository.GetUserSomehowFromTheRequest(); Product p = GetProductForUser(id, u, Permission.Access); return View(p); } public override void OnException(ExceptionContext filterContext) { if (typeof(filterContext.Exception) == typeof(ProductForbiddenException)) { // handle me! } base.OnException(filterContext); } } 

You just need to provide ProductPermissionService.UserPermission to return the user permission for this product. Using the Permission enumeration (I think I have the correct syntax ...) and comparing permissions with < , Admin permissions imply access permissions, which is almost always correct.

0
Aug 26 '09 at 18:33
source share

You can use an implementation based on XACML. Thus, you can supplant authorization, as well as have a repository for your policies outside of your code.

0
Nov 15 '11 at 20:11
source share



All Articles