Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
17 changes: 15 additions & 2 deletions EstateReportingAPI.IntegrationTests/EstateEndpointTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Comment thread
StuartFerguson marked this conversation as resolved.
Resolved
}

[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<HttpResponseMessage> SendEstateRequest(
Expand Down
13 changes: 13 additions & 0 deletions EstateReportingAPI.IntegrationTests/TenantContextEndpointTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down
96 changes: 81 additions & 15 deletions EstateReportingAPI/Common/EstateContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<IAllowAnonymous>() != null)
if (IsAnonymousEndpoint(httpContext))
{
await _next(httpContext);
return;
}

if (httpContext.User.Identity?.IsAuthenticated != true)
bool disableAuthorisation = configuration.GetValue<Boolean>("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<IAllowAnonymous>() != 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);
Expand Down
Loading