diff --git a/EstateReportingAPI.IntegrationTests/CustomWebApplicationFactory.cs b/EstateReportingAPI.IntegrationTests/CustomWebApplicationFactory.cs index 8b38112..2775dcf 100644 --- a/EstateReportingAPI.IntegrationTests/CustomWebApplicationFactory.cs +++ b/EstateReportingAPI.IntegrationTests/CustomWebApplicationFactory.cs @@ -30,6 +30,7 @@ public CustomWebApplicationFactory(string databaseConnectionString) { DatabaseConnectionString = databaseConnectionString; Environment.SetEnvironmentVariable("InTestMode", "true"); + Environment.SetEnvironmentVariable("AppSettings__DisableAuthorisation", "true"); } public string DefaultUserId { get; set; } = "1"; diff --git a/EstateReportingAPI.IntegrationTests/EstateEndpointTests.cs b/EstateReportingAPI.IntegrationTests/EstateEndpointTests.cs index b6c7b48..728a6ab 100644 --- a/EstateReportingAPI.IntegrationTests/EstateEndpointTests.cs +++ b/EstateReportingAPI.IntegrationTests/EstateEndpointTests.cs @@ -130,13 +130,26 @@ public async Task EstateEndpoint_EstateIdClaim_ReturnsEstate() } [Fact] - public async Task EstateEndpoint_MissingAuthentication_ReturnsUnauthorized() + public async Task EstateEndpoint_MissingAuthenticationAndEstateContext_ReturnsForbidden() { using HttpRequestMessage request = new(HttpMethod.Get, this.BaseRoute); using HttpResponseMessage response = await this.Client.SendAsync(request); - response.StatusCode.ShouldBe(HttpStatusCode.Unauthorized); + response.StatusCode.ShouldBe(HttpStatusCode.Forbidden); + } + + [Fact] + public async Task EstateEndpoint_DisabledAuthorisationWithEstateHeader_ReturnsEstate() + { + await this.helper.AddEstate("Test Estate", "Ref1"); + + using HttpRequestMessage request = new(HttpMethod.Get, this.BaseRoute); + request.Headers.Add("estateId", this.TestId.ToString()); + + using HttpResponseMessage response = await this.Client.SendAsync(request); + + response.StatusCode.ShouldBe(HttpStatusCode.OK); } private async Task SendEstateRequest( diff --git a/EstateReportingAPI.IntegrationTests/TenantContextEndpointTests.cs b/EstateReportingAPI.IntegrationTests/TenantContextEndpointTests.cs index a369b40..26a1300 100644 --- a/EstateReportingAPI.IntegrationTests/TenantContextEndpointTests.cs +++ b/EstateReportingAPI.IntegrationTests/TenantContextEndpointTests.cs @@ -31,6 +31,19 @@ public async Task ProtectedEndpoint_DifferentEstateHeader_ReturnsForbidden(strin response.StatusCode.ShouldBe(HttpStatusCode.Forbidden); } + [Fact] + public async Task ProtectedEndpoint_RepeatedEstateHeaders_ReturnsForbidden() + { + using HttpRequestMessage request = new(HttpMethod.Get, "api/estates"); + request.Headers.Authorization = new AuthenticationHeaderValue("Test"); + request.Headers.Add("estateId", this.TestId.ToString()); + request.Headers.Add("estateId", Guid.NewGuid().ToString()); + + using HttpResponseMessage response = await this.Client.SendAsync(request); + + response.StatusCode.ShouldBe(HttpStatusCode.Forbidden); + } + [Theory] [InlineData("health")] [InlineData("healthui")] diff --git a/EstateReportingAPI/Common/EstateContext.cs b/EstateReportingAPI/Common/EstateContext.cs index 017e624..22a2b76 100644 --- a/EstateReportingAPI/Common/EstateContext.cs +++ b/EstateReportingAPI/Common/EstateContext.cs @@ -29,41 +29,107 @@ public EstateContextMiddleware(RequestDelegate next) _next = next; } - public async Task InvokeAsync(HttpContext httpContext,EstateContext estateContext) + public async Task InvokeAsync( + HttpContext httpContext, + EstateContext estateContext, + IConfiguration configuration) { - // Health endpoints or anonymous endpoints do not need estate context. - if (httpContext.GetEndpoint()?.Metadata - .GetMetadata() != null) + if (IsAnonymousEndpoint(httpContext)) { await _next(httpContext); return; } - if (httpContext.User.Identity?.IsAuthenticated != true) + bool disableAuthorisation = configuration.GetValue("AppSettings:DisableAuthorisation"); + bool isAuthenticated = httpContext.User.Identity?.IsAuthenticated == true; + + if (isAuthenticated == false && disableAuthorisation == false) { httpContext.Response.StatusCode = StatusCodes.Status401Unauthorized; return; } + if (TryGetHeaderEstateId(httpContext, out Guid? headerEstateId) == false) + { + httpContext.Response.StatusCode = StatusCodes.Status403Forbidden; + return; + } + + if (isAuthenticated) + { + await HandleAuthenticatedRequestAsync(httpContext, estateContext, headerEstateId); + return; + } + + await HandleUnauthenticatedRequestAsync(httpContext, estateContext, headerEstateId); + } + + private static bool IsAnonymousEndpoint(HttpContext httpContext) + { + return httpContext.GetEndpoint()?.Metadata.GetMetadata() != null; + } + + private static bool TryGetHeaderEstateId(HttpContext httpContext, out Guid? estateId) + { + estateId = null; + + if (httpContext.Request.Headers.TryGetValue("estateId", out var headerValues) == false) + { + return true; + } + + if (headerValues.Count != 1) + { + return false; + } + + if (Guid.TryParse(headerValues[0], out Guid parsedEstateId) == false) + { + return false; + } + + if (parsedEstateId == Guid.Empty) + { + return false; + } + + estateId = parsedEstateId; + return true; + } + + private async Task HandleUnauthenticatedRequestAsync( + HttpContext httpContext, + EstateContext estateContext, + Guid? headerEstateId) + { + if (headerEstateId.HasValue == false) + { + httpContext.Response.StatusCode = StatusCodes.Status403Forbidden; + return; + } + + estateContext.SetEstate(headerEstateId.Value); + await _next(httpContext); + } + + private async Task HandleAuthenticatedRequestAsync( + HttpContext httpContext, + EstateContext estateContext, + Guid? headerEstateId) + { string? estateClaim = httpContext.User.FindFirst("estateId")?.Value; - if (!Guid.TryParse(estateClaim, out Guid authenticatedEstateId) || + if (Guid.TryParse(estateClaim, out Guid authenticatedEstateId) == false || authenticatedEstateId == Guid.Empty) { httpContext.Response.StatusCode = StatusCodes.Status403Forbidden; return; } - // Backwards compatibility: if the header is supplied, it must agree - // with the authenticated claim. - if (httpContext.Request.Headers.TryGetValue("estateId", out var headerValue)) + if (headerEstateId.HasValue && headerEstateId.Value != authenticatedEstateId) { - if (!Guid.TryParse(headerValue.SingleOrDefault(), out Guid headerEstateId) || - headerEstateId != authenticatedEstateId) - { - httpContext.Response.StatusCode = StatusCodes.Status403Forbidden; - return; - } + httpContext.Response.StatusCode = StatusCodes.Status403Forbidden; + return; } estateContext.SetEstate(authenticatedEstateId);