From 747d9f1d9464e6bb26c2db4b32b1a4daab5343fe Mon Sep 17 00:00:00 2001 From: Jezz Santos Date: Tue, 2 Apr 2024 20:11:39 +1300 Subject: [PATCH] Fixed Multi-Tenanted middleware to accomodate Organizations APIs. --- .../Endpoints/MultiTenancyFilterSpec.cs | 48 ++- .../RequestTenantDetectiveSpec.cs | 291 +++++++++++++++++- .../Endpoints/MultiTenancyFilter.cs | 29 +- .../HttpConstants.cs | 3 - .../RequestTenantDetective.cs | 175 +++++++++-- .../ITenantedRequest.cs | 9 + .../Organizations/GetOrganizationRequest.cs | 2 +- .../Pipeline/MultiTenancyMiddleware.cs | 7 +- 8 files changed, 501 insertions(+), 63 deletions(-) diff --git a/src/Infrastructure.Web.Api.Common.UnitTests/Endpoints/MultiTenancyFilterSpec.cs b/src/Infrastructure.Web.Api.Common.UnitTests/Endpoints/MultiTenancyFilterSpec.cs index 67780ade..dd3c2474 100644 --- a/src/Infrastructure.Web.Api.Common.UnitTests/Endpoints/MultiTenancyFilterSpec.cs +++ b/src/Infrastructure.Web.Api.Common.UnitTests/Endpoints/MultiTenancyFilterSpec.cs @@ -27,7 +27,7 @@ public MultiTenancyFilterSpec() } [Fact] - public async Task WhenInvokeAndWrongRequestDelegateSignature_ThenContinuesPipeline() + public async Task WhenInvokeAndWrongRequestDelegateSignature_ThenNoRewriteAndContinuesPipeline() { var httpContext = new DefaultHttpContext { @@ -41,7 +41,7 @@ public async Task WhenInvokeAndWrongRequestDelegateSignature_ThenContinuesPipeli } [Fact] - public async Task WhenInvokeAndUnTenantedRequestInDelegateSignature_ThenContinuesPipeline() + public async Task WhenInvokeAndUnTenantedRequestInDelegateSignature_ThenNoRewriteAndContinuesPipeline() { var httpContext = new DefaultHttpContext { @@ -56,7 +56,7 @@ public async Task WhenInvokeAndUnTenantedRequestInDelegateSignature_ThenContinue } [Fact] - public async Task WhenInvokeAndNoTenantInTenancyContext_ThenContinuesPipeline() + public async Task WhenInvokeAndNoTenantInTenancyContext_ThenNoRewriteAndContinuesPipeline() { var httpContext = new DefaultHttpContext { @@ -72,7 +72,7 @@ public async Task WhenInvokeAndNoTenantInTenancyContext_ThenContinuesPipeline() } [Fact] - public async Task WhenInvokeAndEmptyTenantInTenancyContext_ThenContinuesPipeline() + public async Task WhenInvokeAndEmptyTenantInTenancyContext_ThenNoRewriteAndContinuesPipeline() { _tenancyContext.Setup(tc => tc.Current) .Returns(string.Empty); @@ -90,7 +90,8 @@ public async Task WhenInvokeAndEmptyTenantInTenancyContext_ThenContinuesPipeline } [Fact] - public async Task WhenInvokeAndTenantIdInRequestAlreadyPopulated_ThenContinuesPipeline() + public async Task + WhenInvokeForTenantedRequestAndTenantIdInRequestAlreadyPopulated_ThenNoRewriteAndContinuesPipeline() { _tenancyContext.Setup(tc => tc.Current) .Returns("atenantid"); @@ -102,7 +103,7 @@ public async Task WhenInvokeAndTenantIdInRequestAlreadyPopulated_ThenContinuesPi { new { }, new TestTenantedRequest { - OrganizationId = "anorganizationid" + OrganizationId = "anoldorganizationid" } }; var context = new DefaultEndpointFilterInvocationContext(httpContext, args); @@ -110,11 +111,11 @@ public async Task WhenInvokeAndTenantIdInRequestAlreadyPopulated_ThenContinuesPi await _filter.InvokeAsync(context, _next.Object); _next.Verify(n => n.Invoke(context)); - context.Arguments[1].As().OrganizationId.Should().Be("anorganizationid"); + context.Arguments[1].As().OrganizationId.Should().Be("anoldorganizationid"); } [Fact] - public async Task WhenInvokeAndTenantIdInRequestIsEmpty_ThenPopulatesAndContinuesPipeline() + public async Task WhenInvokeForTenantedRequestAndTenantIdInRequestIsEmpty_ThenRewritesAndContinuesPipeline() { _tenancyContext.Setup(tc => tc.Current) .Returns("atenantid"); @@ -138,10 +139,41 @@ public async Task WhenInvokeAndTenantIdInRequestIsEmpty_ThenPopulatesAndContinue context.Arguments[1].As().OrganizationId.Should().Be("atenantid"); } + [Fact] + public async Task + WhenInvokeForUnTenantedOrganizationRequestAndTenantIdInRequestIsEmpty_ThenRewritesAndContinuesPipeline() + { + _tenancyContext.Setup(tc => tc.Current) + .Returns("atenantid"); + var httpContext = new DefaultHttpContext + { + RequestServices = _serviceProvider.Object + }; + + var args = new object[] + { + new { }, new TestUnTenantedOrganizationRequest + { + Id = null + } + }; + var context = new DefaultEndpointFilterInvocationContext(httpContext, args); + + await _filter.InvokeAsync(context, _next.Object); + + _next.Verify(n => n.Invoke(context)); + context.Arguments[1].As().Id.Should().Be("atenantid"); + } + private class TestUnTenantedRequest : IWebRequest; private class TestTenantedRequest : IWebRequest, ITenantedRequest { public string? OrganizationId { get; set; } } + + private class TestUnTenantedOrganizationRequest : IWebRequest, IUnTenantedOrganizationRequest + { + public string? Id { get; set; } + } } \ No newline at end of file diff --git a/src/Infrastructure.Web.Api.Common.UnitTests/RequestTenantDetectiveSpec.cs b/src/Infrastructure.Web.Api.Common.UnitTests/RequestTenantDetectiveSpec.cs index 06b7272b..d249d450 100644 --- a/src/Infrastructure.Web.Api.Common.UnitTests/RequestTenantDetectiveSpec.cs +++ b/src/Infrastructure.Web.Api.Common.UnitTests/RequestTenantDetectiveSpec.cs @@ -15,11 +15,11 @@ namespace Infrastructure.Web.Api.Common.UnitTests; public class RequestTenantDetectiveSpec { [Trait("Category", "Unit")] - public class GivenAnUntenantedRequestDto + public class GivenAnyRequestDto { private readonly RequestTenantDetective _detective; - public GivenAnUntenantedRequestDto() + public GivenAnyRequestDto() { _detective = new RequestTenantDetective(); } @@ -75,19 +75,56 @@ public async Task WhenDetectTenantAsyncAndTenantIdInHeader_ThenReturnsTenantId() result.Value.TenantId.Should().Be("atenantid"); result.Value.ShouldHaveTenantId.Should().BeFalse(); } + } + + [Trait("Category", "Unit")] + public class GivenAnUntenantedRequestDto + { + private readonly RequestTenantDetective _detective; + + public GivenAnUntenantedRequestDto() + { + _detective = new RequestTenantDetective(); + } [Fact] - public async Task WhenDetectTenantAsyncAndTenantIdInQueryString_ThenReturnsTenantId() + public async Task WhenDetectTenantAsyncAndTenantIdInQueryStringAsOrganizationId_ThenReturnsTenantId() { var httpContext = new DefaultHttpContext { Request = { + Method = HttpMethods.Get, Query = new QueryCollection(new Dictionary { - { nameof(ITenantedRequest.OrganizationId), "atenantid" } - }), - Method = HttpMethods.Get + { nameof(IUnTenantedOrganizationRequest.Id), "anid" }, + { nameof(ITenantedRequest.OrganizationId), "anorganizationid" }, + { nameof(RequestTenantDetective.RequestWithTenantIds.TenantId), "atenantid" } + }) + } + }; + + var result = + await _detective.DetectTenantAsync(httpContext, typeof(TestUnTenantedRequest), CancellationToken.None); + + result.Should().BeSuccess(); + result.Value.TenantId.Should().Be("anorganizationid"); + result.Value.ShouldHaveTenantId.Should().BeFalse(); + } + + [Fact] + public async Task WhenDetectTenantAsyncAndTenantIdInQueryStringAsTenantId_ThenReturnsTenantId() + { + var httpContext = new DefaultHttpContext + { + Request = + { + Method = HttpMethods.Get, + Query = new QueryCollection(new Dictionary + { + { nameof(IUnTenantedOrganizationRequest.Id), "anid" }, + { nameof(RequestTenantDetective.RequestWithTenantIds.TenantId), "atenantid" } + }) } }; @@ -100,7 +137,7 @@ public async Task WhenDetectTenantAsyncAndTenantIdInQueryString_ThenReturnsTenan } [Fact] - public async Task WhenDetectTenantAsyncAndTenantIdInJsonBody_ThenReturnsTenantId() + public async Task WhenDetectTenantAsyncAndTenantIdInJsonBodyAsOrganizationId_ThenReturnsTenantId() { var httpContext = new DefaultHttpContext { @@ -110,7 +147,34 @@ public async Task WhenDetectTenantAsyncAndTenantIdInJsonBody_ThenReturnsTenantId ContentType = HttpContentTypes.Json, Body = new MemoryStream(Encoding.UTF8.GetBytes(new { - OrganizationId = "atenantid" + Id = "anid", + OrganizationId = "anorganizationid", + TenantId = "atenantid" + }.ToJson()!)) + } + }; + + var result = + await _detective.DetectTenantAsync(httpContext, typeof(TestUnTenantedRequest), CancellationToken.None); + + result.Should().BeSuccess(); + result.Value.TenantId.Should().Be("anorganizationid"); + result.Value.ShouldHaveTenantId.Should().BeFalse(); + } + + [Fact] + public async Task WhenDetectTenantAsyncAndTenantIdInJsonBodyAsTenantId_ThenReturnsTenantId() + { + var httpContext = new DefaultHttpContext + { + Request = + { + Method = HttpMethods.Post, + ContentType = HttpContentTypes.Json, + Body = new MemoryStream(Encoding.UTF8.GetBytes(new + { + Id = "anid", + TenantId = "atenantid" }.ToJson()!)) } }; @@ -124,7 +188,7 @@ public async Task WhenDetectTenantAsyncAndTenantIdInJsonBody_ThenReturnsTenantId } [Fact] - public async Task WhenDetectTenantAsyncAndTenantIdInFormUrlEncodedBody_ThenReturnsTenantId() + public async Task WhenDetectTenantAsyncAndTenantIdInFormUrlEncodedBodyAsTenantId_ThenReturnsTenantId() { var httpContext = new DefaultHttpContext { @@ -134,7 +198,8 @@ public async Task WhenDetectTenantAsyncAndTenantIdInFormUrlEncodedBody_ThenRetur ContentType = HttpContentTypes.FormUrlEncoded, Body = await new FormUrlEncodedContent(new List> { - new(nameof(ITenantedRequest.OrganizationId), "atenantid") + new(nameof(IUnTenantedOrganizationRequest.Id), "anid"), + new(nameof(RequestTenantDetective.RequestWithTenantIds.TenantId), "atenantid") }).ReadAsStreamAsync() } }; @@ -146,6 +211,31 @@ public async Task WhenDetectTenantAsyncAndTenantIdInFormUrlEncodedBody_ThenRetur result.Value.TenantId.Should().Be("atenantid"); result.Value.ShouldHaveTenantId.Should().BeFalse(); } + + [Fact] + public async Task WhenDetectTenantAsyncAndTenantIdInFormUrlEncodedBodyAsOrganizationId_ThenReturnsTenantId() + { + var httpContext = new DefaultHttpContext + { + Request = + { + Method = HttpMethods.Post, + ContentType = HttpContentTypes.FormUrlEncoded, + Body = await new FormUrlEncodedContent(new List> + { + new(nameof(IUnTenantedOrganizationRequest.Id), "anid"), + new(nameof(ITenantedRequest.OrganizationId), "anorganizationid") + }).ReadAsStreamAsync() + } + }; + + var result = + await _detective.DetectTenantAsync(httpContext, typeof(TestUnTenantedRequest), CancellationToken.None); + + result.Should().BeSuccess(); + result.Value.TenantId.Should().Be("anorganizationid"); + result.Value.ShouldHaveTenantId.Should().BeFalse(); + } } [Trait("Category", "Unit")] @@ -159,7 +249,7 @@ public GivenATenantedRequestDto() } [Fact] - public async Task WhenDetectTenantAsyncButNoHeaderQueryOrBody_ThenReturnsNoTenantId() + public async Task WhenDetectTenantAsyncAndMissingOrganizationId_ThenReturnsNoTenantId() { var httpContext = new DefaultHttpContext { @@ -173,6 +263,179 @@ public async Task WhenDetectTenantAsyncButNoHeaderQueryOrBody_ThenReturnsNoTenan result.Value.TenantId.Should().BeNone(); result.Value.ShouldHaveTenantId.Should().BeTrue(); } + + [Fact] + public async Task WhenDetectTenantAsyncAndTenantIdInQueryString_ThenReturnsTenantId() + { + var httpContext = new DefaultHttpContext + { + Request = + { + Method = HttpMethods.Get, + Query = new QueryCollection(new Dictionary + { + { nameof(ITenantedRequest.OrganizationId), "atenantid" } + }) + } + }; + + var result = + await _detective.DetectTenantAsync(httpContext, typeof(TestTenantedRequest), CancellationToken.None); + + result.Should().BeSuccess(); + result.Value.TenantId.Should().Be("atenantid"); + result.Value.ShouldHaveTenantId.Should().BeTrue(); + } + + [Fact] + public async Task WhenDetectTenantAsyncAndTenantIdInJsonBody_ThenReturnsTenantId() + { + var httpContext = new DefaultHttpContext + { + Request = + { + Method = HttpMethods.Post, + ContentType = HttpContentTypes.Json, + Body = new MemoryStream(Encoding.UTF8.GetBytes(new + { + OrganizationId = "atenantid" + }.ToJson()!)) + } + }; + + var result = + await _detective.DetectTenantAsync(httpContext, typeof(TestTenantedRequest), CancellationToken.None); + + result.Should().BeSuccess(); + result.Value.TenantId.Should().Be("atenantid"); + result.Value.ShouldHaveTenantId.Should().BeTrue(); + } + + [Fact] + public async Task WhenDetectTenantAsyncAndTenantIdInFormUrlEncodedBody_ThenReturnsTenantId() + { + var httpContext = new DefaultHttpContext + { + Request = + { + Method = HttpMethods.Post, + ContentType = HttpContentTypes.FormUrlEncoded, + Body = await new FormUrlEncodedContent(new List> + { + new(nameof(ITenantedRequest.OrganizationId), "anorganizationid") + }).ReadAsStreamAsync() + } + }; + + var result = + await _detective.DetectTenantAsync(httpContext, typeof(TestTenantedRequest), CancellationToken.None); + + result.Should().BeSuccess(); + result.Value.TenantId.Should().Be("anorganizationid"); + result.Value.ShouldHaveTenantId.Should().BeTrue(); + } + } + + [Trait("Category", "Unit")] + public class GivenATenantedOrganizationRequestDto + { + private readonly RequestTenantDetective _detective; + + public GivenATenantedOrganizationRequestDto() + { + _detective = new RequestTenantDetective(); + } + + [Fact] + public async Task WhenDetectTenantAsyncAndMissingId_ThenReturnsNoTenantId() + { + var httpContext = new DefaultHttpContext + { + Request = { Method = HttpMethods.Get } + }; + + var result = + await _detective.DetectTenantAsync(httpContext, typeof(TestUnTenantedOrganizationRequest), + CancellationToken.None); + + result.Should().BeSuccess(); + result.Value.TenantId.Should().BeNone(); + result.Value.ShouldHaveTenantId.Should().BeTrue(); + } + + [Fact] + public async Task WhenDetectTenantAsyncAndTenantIdInQueryString_ThenReturnsTenantId() + { + var httpContext = new DefaultHttpContext + { + Request = + { + Method = HttpMethods.Get, + Query = new QueryCollection(new Dictionary + { + { nameof(IUnTenantedOrganizationRequest.Id), "atenantid" } + }) + } + }; + + var result = + await _detective.DetectTenantAsync(httpContext, typeof(TestUnTenantedOrganizationRequest), + CancellationToken.None); + + result.Should().BeSuccess(); + result.Value.TenantId.Should().Be("atenantid"); + result.Value.ShouldHaveTenantId.Should().BeTrue(); + } + + [Fact] + public async Task WhenDetectTenantAsyncAndTenantIdInJsonBody_ThenReturnsTenantId() + { + var httpContext = new DefaultHttpContext + { + Request = + { + Method = HttpMethods.Post, + ContentType = HttpContentTypes.Json, + Body = new MemoryStream(Encoding.UTF8.GetBytes(new + { + Id = "atenantid" + }.ToJson()!)) + } + }; + + var result = + await _detective.DetectTenantAsync(httpContext, typeof(TestUnTenantedOrganizationRequest), + CancellationToken.None); + + result.Should().BeSuccess(); + result.Value.TenantId.Should().Be("atenantid"); + result.Value.ShouldHaveTenantId.Should().BeTrue(); + } + + [Fact] + public async Task WhenDetectTenantAsyncAndTenantIdInFormUrlEncodedBody_ThenReturnsTenantId() + { + var httpContext = new DefaultHttpContext + { + Request = + { + Method = HttpMethods.Post, + ContentType = HttpContentTypes.FormUrlEncoded, + Body = await new FormUrlEncodedContent(new List> + { + new(nameof(IUnTenantedOrganizationRequest.Id), "atenantid") + }).ReadAsStreamAsync() + } + }; + + var result = + await _detective.DetectTenantAsync(httpContext, typeof(TestUnTenantedOrganizationRequest), + CancellationToken.None); + + result.Should().BeSuccess(); + result.Value.TenantId.Should().Be("atenantid"); + result.Value.ShouldHaveTenantId.Should().BeTrue(); + } } [UsedImplicitly] @@ -180,4 +443,10 @@ public class TestUnTenantedRequest : UnTenantedRequest; [UsedImplicitly] public class TestTenantedRequest : TenantedRequest; + + [UsedImplicitly] + public class TestUnTenantedOrganizationRequest : UnTenantedRequest, IUnTenantedOrganizationRequest + { + public string? Id { get; set; } + } } \ No newline at end of file diff --git a/src/Infrastructure.Web.Api.Common/Endpoints/MultiTenancyFilter.cs b/src/Infrastructure.Web.Api.Common/Endpoints/MultiTenancyFilter.cs index 35f24276..f4275cd2 100644 --- a/src/Infrastructure.Web.Api.Common/Endpoints/MultiTenancyFilter.cs +++ b/src/Infrastructure.Web.Api.Common/Endpoints/MultiTenancyFilter.cs @@ -9,8 +9,11 @@ namespace Infrastructure.Web.Api.Common.Endpoints; /// -/// Provides a request filter that rewrites the tenant ID into request argument -/// of the current of the current EndPoint +/// Provides a request filter that rewrites the tenant ID into the request parameter +/// of the current of the current EndPoint, if it is either: +/// 1. a +/// 2. a +/// but no rewrite for an untenanted request /// public class MultiTenancyFilter : IEndpointFilter { @@ -42,26 +45,26 @@ CancellationToken cancellationToken return Result.Ok; } - if (requestDto.Value is not ITenantedRequest tenantedRequest) - { - return Result.Ok; - } - var tenantId = tenancyContext.Current; - if (tenantId.NotExists()) + if (tenantId.HasNoValue()) { return Result.Ok; } - var organizationId = tenantId; - if (organizationId.HasNoValue()) + if (requestDto.Value is ITenantedRequest tenantedRequest) { - return Result.Ok; + if (tenantedRequest.OrganizationId.HasNoValue()) + { + tenantedRequest.OrganizationId = tenantId; + } } - if (tenantedRequest.OrganizationId.HasNoValue()) + if (requestDto.Value is IUnTenantedOrganizationRequest unTenantedOrganizationRequest) { - tenantedRequest.OrganizationId = organizationId; + if (unTenantedOrganizationRequest.Id.HasNoValue()) + { + unTenantedOrganizationRequest.Id = tenantId; + } } return Result.Ok; diff --git a/src/Infrastructure.Web.Api.Common/HttpConstants.cs b/src/Infrastructure.Web.Api.Common/HttpConstants.cs index 475a1876..9a768343 100644 --- a/src/Infrastructure.Web.Api.Common/HttpConstants.cs +++ b/src/Infrastructure.Web.Api.Common/HttpConstants.cs @@ -1,5 +1,3 @@ -using Infrastructure.Web.Api.Interfaces; - namespace Infrastructure.Web.Api.Common; /// @@ -45,7 +43,6 @@ public static class HttpQueryParams { public const string APIKey = "apikey"; public const string Format = "format"; - public const string Tenant = nameof(ITenantedRequest.OrganizationId); } /// diff --git a/src/Infrastructure.Web.Api.Common/RequestTenantDetective.cs b/src/Infrastructure.Web.Api.Common/RequestTenantDetective.cs index 8a353e83..767a3bd6 100644 --- a/src/Infrastructure.Web.Api.Common/RequestTenantDetective.cs +++ b/src/Infrastructure.Web.Api.Common/RequestTenantDetective.cs @@ -11,16 +11,20 @@ namespace Infrastructure.Web.Api.Common; /// /// Provides a detective that determines the tenant of the request from data within the request, -/// in either from the field in the body, -/// from the query string or from the header. +/// from one of these sources: +/// 1. The field in the body, +/// 2. The field in the body, +/// 3. The query string, +/// 4. The header. /// public class RequestTenantDetective : ITenantDetective { public async Task> DetectTenantAsync(HttpContext httpContext, Optional requestDtoType, CancellationToken cancellationToken) { - var shouldHaveTenantId = IsTenantedRequest(requestDtoType); - var (found, tenantIdFromRequest) = await ParseTenantIdFromRequestAsync(httpContext.Request, cancellationToken); + var shouldHaveTenantId = IsTenantedRequest(requestDtoType, out var type); + var (found, tenantIdFromRequest) = + await ParseTenantIdFromRequestAsync(httpContext.Request, type, cancellationToken); if (found) { return new TenantDetectionResult(shouldHaveTenantId, tenantIdFromRequest); @@ -29,20 +33,34 @@ public async Task> DetectTenantAsync(HttpCo return new TenantDetectionResult(shouldHaveTenantId, null); } - private static bool IsTenantedRequest(Optional requestDtoType) + private static bool IsTenantedRequest(Optional requestDtoType, out RequestDtoType type) { + type = RequestDtoType.UnTenanted; if (!requestDtoType.HasValue) { return false; } - return requestDtoType.Value.IsAssignableTo(typeof(ITenantedRequest)); + if (requestDtoType.Value.IsAssignableTo(typeof(ITenantedRequest))) + { + type = RequestDtoType.Tenanted; + return true; + } + + if (requestDtoType.Value.IsAssignableTo(typeof(IUnTenantedOrganizationRequest))) + { + type = RequestDtoType.UnTenantedOrganization; + return true; + } + + return false; } /// /// Attempts to locate the tenant ID from the request query, or header, or body /// private static async Task<(bool HasTenantId, string? tenantId)> ParseTenantIdFromRequestAsync(HttpRequest request, + RequestDtoType type, CancellationToken cancellationToken) { if (request.Headers.TryGetValue(HttpHeaders.Tenant, out var tenantIdFromHeader)) @@ -54,19 +72,56 @@ private static bool IsTenantedRequest(Optional requestDtoType) } } - if (request.Query.TryGetValue(HttpQueryParams.Tenant, out var tenantIdFromQueryString)) + if (type.IsTenanted) { - var value = GetFirstStringValue(tenantIdFromQueryString); - if (value.HasValue()) + if (request.Query.TryGetValue(nameof(ITenantedRequest.OrganizationId), out var tenantIdFromQueryString)) { - return (true, value); + var value = GetFirstStringValue(tenantIdFromQueryString); + if (value.HasValue()) + { + return (true, value); + } + } + } + + if (type.IsUnTenantedOrganization) + { + if (request.Query.TryGetValue(nameof(IUnTenantedOrganizationRequest.Id), out var tenantIdFromQueryString)) + { + var value = GetFirstStringValue(tenantIdFromQueryString); + if (value.HasValue()) + { + return (true, value); + } + } + } + + if (type.IsUntenanted) + { + if (request.Query.TryGetValue(nameof(ITenantedRequest.OrganizationId), out var tenantIdFromQueryString)) + { + var value = GetFirstStringValue(tenantIdFromQueryString); + if (value.HasValue()) + { + return (true, value); + } + } + + if (request.Query.TryGetValue(nameof(RequestWithTenantIds.TenantId), out var tenantIdFromQueryString2)) + { + var value = GetFirstStringValue(tenantIdFromQueryString2); + if (value.HasValue()) + { + return (true, value); + } } } var couldHaveBody = new HttpMethod(request.Method).CanHaveBody(); if (couldHaveBody) { - var (found, tenantIdFromRequestBody) = await ParseTenantIdFromRequestBodyAsync(request, cancellationToken); + var (found, tenantIdFromRequestBody) = + await ParseTenantIdFromRequestBodyAsync(request, type, cancellationToken); if (found) { return (true, tenantIdFromRequestBody); @@ -77,7 +132,7 @@ private static bool IsTenantedRequest(Optional requestDtoType) } private static async Task<(bool HasTenantId, string? tenantId)> ParseTenantIdFromRequestBodyAsync( - HttpRequest request, CancellationToken cancellationToken) + HttpRequest request, RequestDtoType type, CancellationToken cancellationToken) { if (request.Body.Position != 0) { @@ -89,18 +144,37 @@ private static bool IsTenantedRequest(Optional requestDtoType) try { var requestWithTenantId = - await request.ReadFromJsonAsync(typeof(RequestWithTenantId), cancellationToken); + await request.ReadFromJsonAsync(typeof(RequestWithTenantIds), cancellationToken); request.RewindBody(); - if (requestWithTenantId is RequestWithTenantId tenantId) + if (requestWithTenantId is RequestWithTenantIds requestWithTenantIds) { - if (tenantId.OrganizationId.HasValue()) + if (type.IsTenanted) { - return (true, tenantId.OrganizationId); + if (requestWithTenantIds.OrganizationId.HasValue()) + { + return (true, requestWithTenantIds.OrganizationId); + } } - if (tenantId.TenantId.HasValue()) + if (type.IsUnTenantedOrganization) { - return (true, tenantId.TenantId); + if (requestWithTenantIds.Id.HasValue()) + { + return (true, requestWithTenantIds.Id); + } + } + + if (type.IsUntenanted) + { + if (requestWithTenantIds.OrganizationId.HasValue()) + { + return (true, requestWithTenantIds.OrganizationId); + } + + if (requestWithTenantIds.TenantId.HasValue()) + { + return (true, requestWithTenantIds.TenantId); + } } } } @@ -113,12 +187,49 @@ private static bool IsTenantedRequest(Optional requestDtoType) if (request.ContentType == HttpContentTypes.FormUrlEncoded) { var form = await request.ReadFormAsync(cancellationToken); - if (form.TryGetValue(nameof(ITenantedRequest.OrganizationId), out var tenantId)) + + if (type.IsTenanted) { - var value = GetFirstStringValue(tenantId); - if (value.HasValue()) + if (form.TryGetValue(nameof(ITenantedRequest.OrganizationId), out var tenantId1)) { - return (true, value); + var value = GetFirstStringValue(tenantId1); + if (value.HasValue()) + { + return (true, value); + } + } + } + + if (type.IsUnTenantedOrganization) + { + if (form.TryGetValue(nameof(IUnTenantedOrganizationRequest.Id), out var tenantId1)) + { + var value = GetFirstStringValue(tenantId1); + if (value.HasValue()) + { + return (true, value); + } + } + } + + if (type.IsUntenanted) + { + if (form.TryGetValue(nameof(ITenantedRequest.OrganizationId), out var tenantId1)) + { + var value = GetFirstStringValue(tenantId1); + if (value.HasValue()) + { + return (true, value); + } + } + + if (form.TryGetValue(nameof(RequestWithTenantIds.TenantId), out var tenantId2)) + { + var value = GetFirstStringValue(tenantId2); + if (value.HasValue()) + { + return (true, value); + } } } } @@ -132,13 +243,29 @@ private static bool IsTenantedRequest(Optional requestDtoType) } /// - /// Defines a request that could have a tenant ID within it + /// Defines a request that could have a tenant ID within it, + /// in any of these properties /// // ReSharper disable once MemberCanBePrivate.Global - internal class RequestWithTenantId : ITenantedRequest + internal class RequestWithTenantIds : ITenantedRequest, IUnTenantedOrganizationRequest { public string? TenantId { get; [UsedImplicitly] set; } public string? OrganizationId { get; set; } + + public string? Id { get; set; } + } + + internal class RequestDtoType + { + public static readonly RequestDtoType Tenanted = new() { IsTenanted = true }; + public static readonly RequestDtoType UnTenanted = new() { IsUntenanted = true }; + public static readonly RequestDtoType UnTenantedOrganization = new() { IsUnTenantedOrganization = true }; + + public bool IsTenanted { get; init; } + + public bool IsUntenanted { get; init; } + + public bool IsUnTenantedOrganization { get; init; } } } \ No newline at end of file diff --git a/src/Infrastructure.Web.Api.Interfaces/ITenantedRequest.cs b/src/Infrastructure.Web.Api.Interfaces/ITenantedRequest.cs index 7cbc1d07..f9eed5b7 100644 --- a/src/Infrastructure.Web.Api.Interfaces/ITenantedRequest.cs +++ b/src/Infrastructure.Web.Api.Interfaces/ITenantedRequest.cs @@ -6,4 +6,13 @@ namespace Infrastructure.Web.Api.Interfaces; public interface ITenantedRequest { public string? OrganizationId { get; set; } +} + +/// +/// Defines a request for a specific tenant, for Organization requests that are untenanted +/// Only to be used by the Organizations subdomain +/// +public interface IUnTenantedOrganizationRequest +{ + public string? Id { get; set; } } \ No newline at end of file diff --git a/src/Infrastructure.Web.Api.Operations.Shared/Organizations/GetOrganizationRequest.cs b/src/Infrastructure.Web.Api.Operations.Shared/Organizations/GetOrganizationRequest.cs index 50119511..ece4291b 100644 --- a/src/Infrastructure.Web.Api.Operations.Shared/Organizations/GetOrganizationRequest.cs +++ b/src/Infrastructure.Web.Api.Operations.Shared/Organizations/GetOrganizationRequest.cs @@ -4,7 +4,7 @@ namespace Infrastructure.Web.Api.Operations.Shared.Organizations; [Route("/organizations/{Id}", ServiceOperation.Get, AccessType.Token)] [Authorize(Roles.Platform_Standard)] -public class GetOrganizationRequest : UnTenantedRequest +public class GetOrganizationRequest : UnTenantedRequest, IUnTenantedOrganizationRequest { public required string Id { get; set; } } \ No newline at end of file diff --git a/src/Infrastructure.Web.Hosting.Common/Pipeline/MultiTenancyMiddleware.cs b/src/Infrastructure.Web.Hosting.Common/Pipeline/MultiTenancyMiddleware.cs index c9accc98..62c6396a 100644 --- a/src/Infrastructure.Web.Hosting.Common/Pipeline/MultiTenancyMiddleware.cs +++ b/src/Infrastructure.Web.Hosting.Common/Pipeline/MultiTenancyMiddleware.cs @@ -15,9 +15,10 @@ namespace Infrastructure.Web.Hosting.Common.Pipeline; /// /// Provides middleware to detect the tenant of incoming requests. -/// Detects the current tenant from either the request itself (as "OrganizationId"), -/// or, if missing, extracts the "DefaultOrganizationId" from the authenticated user -/// and rewrites that value into the body of POST/PUT/PATCH requests, of it has an "OrganizationId" +/// Detects the current tenant using the , +/// and if required and missing, then extracts the "DefaultOrganizationId" from the authenticated user +/// and sets the tenant. +/// Downstream, an endpoint filter will rewrite the required, missing tenant into the request /// public class MultiTenancyMiddleware {