I'm writing a multi-tenant application using a one database per tenant model. I have allowed each user account to access multiple tenants (as long as that tenant has given them access)
Each page sent to the browser includes the current TenantId in Site.Master
<%= Html.Hidden("TenantId") %>
But when any request is made from the browser (submit button, AJAX GET or AJAX POST), this TenantId is NOT actually checked to see if it matches the user's current TenantId.
Now if the user opens one tab, with TenantId = 1, then connects to another tenant in another tab with TenantId = 2, then switches back to the first tab and it has access to data from Tenant 2.
What can I do to fix this? I have a large number of existing ActionResult and JsonResult methods and I do not want to go through every one of them and add
if (request.TenantId != user.CurrentTenantId) return false
Because that would be a large amount of duplicated effort
Can I change my base controller to always read the value of TenantId? That could work for submitted requests (ActionResult), but what about AJAX requests?
How can I check the TenantId of the page inside of JsonResult actions without changing every single existing AJAX method (there are a lot of them)?
You could check your in the Application_Request event in the Global.asax.cs file. If what you need is populated via MVC model binding, then maybe write a custom ActionFilter to check it and register it with all actions via a GlobalFilter.
If I understood correctly, users could have simultaneously open 2 different tabs, each with a different tenant. And each page should display data relevant to each tenant.
So that means a solution involving a cookie or the session needs to be discarded as the tenant is specific to each browser tab.
And reading your answer to Cyril Gupta´s suggestion, I understand the hidden tenantId on each page may not be submitted on every AJAX request. Of course, one solution could be to modify your application and make sure this is always the case with every AJAX request. Otherwise that would also discard a global filter based on the request parameters as the tenantId may not always be there.
I think the best option then is to add a segment in the URL holding the tenantId. For example replacing the default route with something like the following route (If you have many different routes you would need to be very careful to avoid route collision):
routes.MapRoute(
name: "Default",
url: "{tenant}/{controller}/{action}/{id}",
defaults: new { tenant = "defaultTenant", controller = "Home", action = "Index", id = UrlParameter.Optional }
);
This way you can make sure the tenant will always be submitted on each request, and you could also have 2 different tabs with different tenants displaying each the appropriate data.
There are different options on how to recover the value of the route segment.
The binding will automatically populate the value on any parameter named "tenant" on your action method, or any parameter named "tenant" in a model class that is a parameter of the action method: public ActionResult Foo(FooModel model, string tenant) { //both tenant and model.tenant will contain the value of the URL segment return View(); }
You could also write a filter that access the value of the route parameter (RouteData is a property of the ActionExecutingContext
and ActionExecutedContext
class received as parameters of the filter methods), and performs some logic. The filter would then be set as a global filter in your application or to your base controller:
public class FooFilterAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var tenant = filterContext.RouteData.Values["tenant"]
//do whatever you need to do before executing the action, based on the tenant
}
public override void OnActionExecuted(ActionExecutedContext filterContext)
{
var tenant = filterContext.RouteData.Values["tenant"]
//do whatever you need to do after executing the action, based on the tenant
}
}
The final option is to directly access the RouteData parameter on your base controller class. (As RouteData is a property of the base MVC Controller
class)
As long as you are using Html and Ajax helpers for URL generation, the tenant segment of the URL will be maintained in your links. However if you have jquery code directly sending AJAX calls with hardcoded URLs, you would then need to update that code so the new url segment is taken into account.
Finally, in case the tenantId values are something not very user friendly like an integer, you could have unique names for each tenant and use the names in the URL. You would then add some logic that maps it to the integer value your application need.
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