Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Access Control in ASP.NET MVC depending on input parameters / service layer?

Tags:

asp.net-mvc

Preamble: this is a bit of a philosophical question. I'm looking more for the "right" way to do this rather than "a" way to do this.

Let's imagine I have some products, and an ASP.NET MVC application performing CRUD on those products:-

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

I'm using the repository pattern, so it doesn't matter where these products come from:-

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

Also my Repository describes a list of Users, and which products they are managers for (many-many between Users and Products). Elsewhere in the application, a Super-Admin is performing CRUD on Users and managing the relationship between Users and the Products they are permitted to manage.

Anyone is allowed to view any product, but only users who are designated as "admins" for a particular product are allowed to invoke e.g. the Edit action.

How should I go about implementing that in ASP.NET MVC? Unless I've missed something, I can't use the built-in ASP.NET Authorize attribute as first I'd need a different role for every product, and second I won't know which role to check for until I've retrieved my Product from the Repository.

Obviously you can generalise from this scenario to most content-management scenarios - e.g. Users are only allowed to edit their own Forum Posts. StackOverflow users are only allowed to edit their own questions - unless they've got 2000 or more rep...

The simplest solution, as an 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 issues:

  • Some of this code will need to be repeated - imagine there are several operations (Update, Delete, SetStock, Order, CreateOffer) depending on the User-Products relationship. You'd have to copy-paste several times.
  • It's not very testable - you've got to mock up by my count four objects for every test.
  • It doesn't really seem like the controller's "job" to be checking whether the user is allowed to perform the action. I'd much rather a more pluggable (e.g. AOP via attributes) solution. However, would that necessarily mean you'd have to SELECT the product twice (once in the AuthorizationFilter, and again in the Controller)?
  • Would it be better to return a 403 if the user isn't allowed to make this request? If so, how would I go about doing that?

I'll probably keep this updated as I get ideas myself, but I'm very eager to hear yours!

Thanks in advance!

Edit

Just to add a bit of detail here. The issue I'm having is that I want the business rule "Only users with permission may edit products" to be contained in one and only one place. I feel that the same code which determines whether a user can GET or POST to the Edit action should also be responsible for determining whether to render the "Edit" link on the Index or Details views. Maybe that's not possible/not feasible, but I feel like it should be...

Edit 2

Starting a bounty on this one. I've received some good and helpful answers, but nothing that I feel comfortable "accepting". Bear in mind that I'm looking for a nice clean method to keep the business logic that determines whether or not the "Edit" link on the index view will be displayed in the same place that determines whether or not a request to Products/Edit/1 is authorised or not. I'd like to keep the pollution in my action method to an absolute minimum. Ideally, I'm looking for an attribute-based solution, but I accept that may be impossible.

like image 419
Iain Galloway Avatar asked Aug 26 '09 14:08

Iain Galloway


1 Answers

First of all, I think you already half-way figured it, becuase you stated that

as first I'd need a different role for every product, and second I won't know which role to check for until I've retrieved my Product from the Repository

I've seen so many attempts at making role-based security do something it was never intended to do, but you are already past that point, so that's cool :)

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

You will still need to retrieve the ACL for a product and then check if the user has the right permission for the product. This is so context-sensitive and interaction-heavy that I think that a purely declarative approach is both too inflexible and too implicit (i.e. you may not realize how many database reads are involved in adding a single attribute to some code).

I think scenarios like this are best modeled by a class that encapsulates the ACL logic, allowing you to either Query for decision or making 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 know whether the user can edit the product, you can issue a Query:

bool canEdit = permission.IsGrantedTo(user); 

If you just want to ensure that the user has rights to continue, you can issue an Assertion:

permission.Demand(user); 

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

This all assumes that the Product class (the variable p) has an associated ACL, like this:

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

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

If the current user is the same as Thread.CurrentPrincipal (which is the case in ASP.NET MVC, IIRC), you can simplyfy the above permission methods to:

bool canEdit = permission.IsGranted(); 

or

permission.Demand(); 

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

like image 150
Mark Seemann Avatar answered Oct 05 '22 10:10

Mark Seemann