I'm building a simple multi-user (multi-tenant?) App with ASP.NET MVC3 and EF4, one database, one code base, all users access the app using the same URL. Once a User is logged in they should only have access to their data, I'm using the default asp.NET membership provider and have added a ‘UserId’ Guid field on each of the data tables. Obviously I don't want user A to have any access to user B’s data so I have been adding the following to nearly every action on my controllers.
public ActionResult EditStatus(int id)
{
if (!Request.IsAuthenticated)
return RedirectToAction("Index", "Home");
var status = sService.GetStatusById(id);
// check if the logged in user has access to this status
if (status.UserId != GetUserId())
return RedirectToAction("Index", "Home");
.
.
.
}
private Guid GetUserId()
{
if (Membership.GetUser() != null)
{
MembershipUser member = Membership.GetUser();
Guid id = new Guid(member.ProviderUserKey.ToString());
return id;
}
return Guid.Empty;
}
This repetition is definitely feeling wrong and there must be a more elegant way of ensuring my users can't access each other's data – what am I missing?
what am I missing?
A custom model binder:
public class StatusModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
// Fetch the id from the RouteData
var id = controllerContext.RouteData.Values["id"] as string;
// TODO: Use constructor injection to pass the service here
var status = sService.GetStatusById(id);
// Compare whether the id passed in the request belongs to
// the currently logged in user
if (status.UserId != GetUserId())
{
throw new HttpException(403, "Forbidden");
}
return status;
}
private Guid GetUserId()
{
if (Membership.GetUser() != null)
{
MembershipUser member = Membership.GetUser();
Guid id = new Guid(member.ProviderUserKey.ToString());
return id;
}
return Guid.Empty;
}
}
and then you would register this model binder in Application_Start
:
// Could use constructor injection to pass the repository to the model binder
ModelBinders.Binders.Add(typeof(Status), new StatusModelBinder());
and finally
// The authorize attribute ensures that a user is authenticated.
// If you want it to redirect to /Home/Index as in your original
// example if the user is not authenticated you could write a custom
// Authorize attribute and do the job there
[Authorize]
public ActionResult EditStatus(Status status)
{
// if we got that far it means that the user has access to this resource
// TODO: do something with the status and return some view
...
}
Conclusion: We've put this controller on a diet which is the way controllers should be :-)
Trying to get my head around this implementation (I'm having exactly the same question), I found a similar approach described in Scott Hanselman't post
http://www.hanselman.com/blog/IPrincipalUserModelBinderInASPNETMVCForEasierTesting.aspx
public class IPrincipalModelBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
if (controllerContext == null)
{
throw new ArgumentNullException("controllerContext");
}
if (bindingContext == null)
{
throw new ArgumentNullException("bindingContext");
}
IPrincipal p = controllerContext.HttpContext.User;
return p;
}
}
void Application_Start()
{
RegisterRoutes(RouteTable.Routes); //unrelated, don't sweat this line.
ModelBinders.Binders[typeof(IPrincipal)] = new IPrincipalModelBinder();
}
[Authorize]
public ActionResult Edit(int id, IPrincipal user)
{
Dinner dinner = dinnerRepository.FindDinner(id);
if (dinner.HostedBy != user.Identity.Name)
return View("InvalidOwner");
var viewModel = new DinnerFormViewModel {
Dinner = dinner,
Countries = new SelectList(PhoneValidator.Countries, dinner.Country)
};
return View(viewModel);
}
For a total MVC noob as myself, that was somewhat easier to understand.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With