Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
259 views
in Technique[技术] by (71.8m points)

c# - ASP.NET Core MVC Different Routing Based On Request Identity (Single Tenant + Multi Tenant in Same Site)

Before anybody says anything, I'm aware that this is a reasonably ridiculous requirement, however it's what the client wants.

Background

We have a brownfield system for data management that allows access for two different types of user, we'll call them Tenant and Admin.

  • A Tenant user can only access their own data and gains their TenantContext from the TenantId claim in their identity.
  • An Admin user can access all data for all Tenants, along with a handful of admin only views, and acquires the TenantContext of a particular Tenant by means of a mechanism driven by session state.
  • There are also several views that do not require TenantContext (typically static info etc.) that can be accessed by both types of user, and some that can be accessed by anonymous users also.

We want to move away from this stateful implementation in favour of an entirely stateless system.

Problem

The issue we're faced with is how to identify which TenantContext an Admin user is working within without storing this in session state. The obvious solution would be to prefix all urls which require a TenantContext with the TenantId, for example https://example.com/{tenantId}/somecontroller/someaction, however doing so would of course affect the urls for Tenant users also, who currently have no awareness or need to be aware of their TenantId, as a Tenant user can only be assigned to a single TenantId. Unfortunately this is something which the client has firmly stated is unacceptable.

So the question that needs to be answered is, "how to build a website that is both single tenanted and multi-tenanted, dependent on the user that is currently making the request?"

My first thoughts turned to a bit of url-rewriting middleware, which I've semi-successfully implemented:

public enum UserRole
{
    Tenant,
    Admin
}

public class IdentityBasedMultiTenantMiddleware
{
    private readonly RequestDelegate _next;

    public IdentityBasedMultiTenantMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task InvokeAsync(HttpContext httpContext)
    {
        if (!(httpContext.User?.Identity?.IsAuthenticated ?? false))
        {
            await _next(httpContext);
            return;
        }

        if (!Enum.TryParse<UserRole>(httpContext.User.FindFirst("Role")?.Value, out var role))
        {
            await _next(httpContext);
            return;
        }

        if (role != UserRole.Admin)
        {
            await _next(httpContext);
            return;
        }

        if (httpContext.Request.Path.Value == "/"
            || httpContext.Request.Path.Value.StartsWith("/admin"))
        {
            await _next(httpContext);
            return;
        }

        var pathSegments = httpContext.Request.Path.Value.Split("/", StringSplitOptions.RemoveEmptyEntries);
            
        if (!Guid.TryParse(pathSegments.FirstOrDefault(), out var tenantId))
        {
            httpContext.Response.Redirect("/admin/tenantsearch");
            return;
        }

        httpContext.Items.Add("TenantId", tenantId);

        httpContext.Request.Path = httpContext.Request.Path.Value.Remove(0, tenantId.ToString().Length + 1);
        httpContext.Request.PathBase = $"/{tenantId}";

        await _next(httpContext);
    }
}

NB. I'm adding the tenantId to the httpContext.Items collection as a lazy way to demonstrate the concept.

However, there are problems with this approach:

  • As you can see, I've had to prefix all of the admin routes with /admin and hard-code a condition to handle this. It would also be required for all views that don't require a TenantContext, leaving a rather nasty maintenance overhead for managing any route that doesn't require TenantContext.
  • When viewing any page as an Admin user, once TenantContext has been established by adding it to the url, for example https://example.com/{tenantId}/somecontroller/someaction, because of the way the url-rewriting is working, all links throughout the site (not just the ones that require TenantContext) are prefixed with {tenantId} - although I suspect this specific problem could be overcome with a custom implementation of IUrlHelperFactory, but of course would require mirroring the same conditions and incur the same maintenance overhead for admin pages etc. also.

I then went on to reverse the problem whilst still using url-rewriting. What if the routes that required TenantContext always included the TenantId, and we rewrote those routes for the Tenant users only? Unfortunately this incurred the same problem as the first url-rewriting solution, so far as routes that required TenantContext would need to be identified in the middleware and conditions to handle them hard-coded.

Ultimately I believe this to be a limitation of the url-rewriting approach, as we're working at a HttpContext level and not a Routing level, where we'd potentially have more context about the mapped controller/action to make a decision.

I've gone on to investigate how this might be achieved using the MVC routing engine but I can't see a way forward - is there anything here that I've missed? Perhaps dynamic controller routing? All thoughts would be appreciated.

A couple of other gotchas:

  • We're using a mix of attribute routing ([Route("someroute")]/[HttpGet("someroute")]) and convention based routing for the existing site. We're in the process of moving to purely attribute based routing.
  • We've also looked into building a tag helper that appends a querystring containing the TenantId to links for Admin users - this approach works but I think it's dirty, difficult to maintain (easy to break) and ultimately the incorrect way to achieve this.
question from:https://stackoverflow.com/questions/65602792/asp-net-core-mvc-different-routing-based-on-request-identity-single-tenant-mu

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)
Waitting for answers

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...