Multi-tenant application allow one user to access many tenant accounts?
Asked Answered
M

4

7

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)?

Mortarboard answered 15/4, 2013 at 23:4 Comment(0)
M
2

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.

Meyerhof answered 15/4, 2013 at 23:42 Comment(0)
A
2

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.

Anabelanabella answered 23/4, 2013 at 20:30 Comment(2)
Sorry, to clarify: I dont need both tabs to work at the same time. I just need that the first tab refuses to load any further data because the user is now logged into the 2nd tenant account. Also I do not want to have the tenant id in the URL, but I do have it available in the page's form collection if that helps.Mortarboard
@JK, then you could follow a similar approach but recovering the tenantId from the request (Which is basically what the others already suggested). The problem is that you would need to make sure the tenantId is sent in every AJAX requestAnabelanabella
M
0

You can write you own filter:

How do I get certain code to execute before every single controller action in ASP.NET MVC 2? Executing code before any action

Of course there is no ready answer for you question. You need to write you own logic, how to handle tenantId. For example on each action, check if it doesnt equal as current session tenant id make redirect. Or put it in cookie and check every time in filter whether id's are equal. It's up to you. From my point of view cookie is more preferrable. But it eat traffic.

Meyers answered 18/4, 2013 at 13:37 Comment(0)
L
-1

You can apply a filter at the controller level and check the tenantid that is being sent. Controller level filters shouldn't be any more difficult than action filters. For my project I needed to check authorisation in a similar manner but I overrode the controller class and then inherited from my own controller class as I had some very special needs.

Where are you going to store the tenant Id on the client side? It seems to me that you should be using a session object to do this.

Length answered 16/4, 2013 at 1:35 Comment(1)
I already have a base controller class that all controllers inherit from. How can I use this to add a filter? And surely that would only affect ActionResult methods (they are from form submits so I can check any of the form's fields), but not JsonResult ones (because these are AJAX submits and they only contain the fields actually submitted, not the entire form collection object)Mortarboard

© 2022 - 2024 — McMap. All rights reserved.