diff --git a/src/App.Metrics.AspNetCore.Health.Core/Internal/Authorization/AuthorizedHealthAuthorizationFilter.cs b/src/App.Metrics.AspNetCore.Health.Core/Internal/Authorization/AuthorizedHealthAuthorizationFilter.cs new file mode 100644 index 00000000..40017d40 --- /dev/null +++ b/src/App.Metrics.AspNetCore.Health.Core/Internal/Authorization/AuthorizedHealthAuthorizationFilter.cs @@ -0,0 +1,15 @@ +// +// Copyright (c) Allan Hardy. All rights reserved. +// + +using System.Threading; +using Microsoft.AspNetCore.Http; + +namespace App.Metrics.AspNetCore.Health +{ + public class AuthorizedHealthAuthorizationFilter : IHealthAuthorizationFilter + { + /// + public bool Authorized(HttpContext context, CancellationToken token = default) { return true; } + } +} diff --git a/src/App.Metrics.AspNetCore.Health.Core/Internal/Authorization/IHealthAuthorizationFilter.cs b/src/App.Metrics.AspNetCore.Health.Core/Internal/Authorization/IHealthAuthorizationFilter.cs new file mode 100644 index 00000000..e588b1e9 --- /dev/null +++ b/src/App.Metrics.AspNetCore.Health.Core/Internal/Authorization/IHealthAuthorizationFilter.cs @@ -0,0 +1,14 @@ +// +// Copyright (c) Allan Hardy. All rights reserved. +// + +using System.Threading; +using Microsoft.AspNetCore.Http; + +namespace App.Metrics.AspNetCore.Health +{ + public interface IHealthAuthorizationFilter + { + bool Authorized(HttpContext context, CancellationToken token = default); + } +} \ No newline at end of file diff --git a/src/App.Metrics.AspNetCore.Health.Core/Internal/Authorization/NotAuthorizedHealthAuthorizationFilter.cs b/src/App.Metrics.AspNetCore.Health.Core/Internal/Authorization/NotAuthorizedHealthAuthorizationFilter.cs new file mode 100644 index 00000000..4e502061 --- /dev/null +++ b/src/App.Metrics.AspNetCore.Health.Core/Internal/Authorization/NotAuthorizedHealthAuthorizationFilter.cs @@ -0,0 +1,15 @@ +// +// Copyright (c) Allan Hardy. All rights reserved. +// + +using System.Threading; +using Microsoft.AspNetCore.Http; + +namespace App.Metrics.AspNetCore.Health +{ + public class NotAuthorizedHealthAuthorizationFilter : IHealthAuthorizationFilter + { + /// + public bool Authorized(HttpContext context, CancellationToken token = default) { return false; } + } +} \ No newline at end of file diff --git a/src/App.Metrics.AspNetCore.Health.Core/Internal/Extensions/AppMetricsMiddlewareHealthChecksLoggerExtensions.cs b/src/App.Metrics.AspNetCore.Health.Core/Internal/Extensions/AppMetricsMiddlewareHealthChecksLoggerExtensions.cs index 62fc2353..cac5577c 100644 --- a/src/App.Metrics.AspNetCore.Health.Core/Internal/Extensions/AppMetricsMiddlewareHealthChecksLoggerExtensions.cs +++ b/src/App.Metrics.AspNetCore.Health.Core/Internal/Extensions/AppMetricsMiddlewareHealthChecksLoggerExtensions.cs @@ -33,6 +33,14 @@ public static void MiddlewareExecuting(this ILogger logger) } } + public static void MiddlewareAuthorizationInvalid(this ILogger logger) + { + if (logger.IsEnabled(LogLevel.Trace)) + { + logger.LogTrace(AppMetricsEventIds.Middleware.MiddlewareAuthorizationInvalidId, $"Invalid authorization App Metrics Health Middleware {typeof(TMiddleware).FullName}"); + } + } + private static class AppMetricsEventIds { public static class Middleware @@ -40,6 +48,7 @@ public static class Middleware public const int MiddlewareExecutedId = 1; public const int MiddlewareExecutingId = 2; public const int MiddlewareErrorId = 3; + public const int MiddlewareAuthorizationInvalidId = 4; } } } diff --git a/src/App.Metrics.AspNetCore.Health.Endpoints/Builder/HealthApplicationBuilderExtensions.cs b/src/App.Metrics.AspNetCore.Health.Endpoints/Builder/HealthApplicationBuilderExtensions.cs index b6f426c3..8d57d85a 100644 --- a/src/App.Metrics.AspNetCore.Health.Endpoints/Builder/HealthApplicationBuilderExtensions.cs +++ b/src/App.Metrics.AspNetCore.Health.Endpoints/Builder/HealthApplicationBuilderExtensions.cs @@ -3,6 +3,7 @@ // using System; +using App.Metrics.AspNetCore.Health; using App.Metrics.AspNetCore.Health.Endpoints; using App.Metrics.AspNetCore.Health.Endpoints.Middleware; using App.Metrics.Health; @@ -112,7 +113,8 @@ private static void UseHealthMiddleware( appBuilder => { var responseWriter = HealthAspNetCoreHealthEndpointsServiceCollectionExtensions.ResolveHealthResponseWriter(app.ApplicationServices, formatter); - appBuilder.UseMiddleware(responseWriter, endpointsOptionsAccessor.Value.Timeout); + var healthAuthorizationFilter = app.ApplicationServices.GetService() ?? new AuthorizedHealthAuthorizationFilter(); + appBuilder.UseMiddleware(responseWriter, endpointsOptionsAccessor.Value.Timeout, healthAuthorizationFilter); }); } } diff --git a/src/App.Metrics.AspNetCore.Health.Endpoints/Middleware/HealthCheckEndpointMiddleware.cs b/src/App.Metrics.AspNetCore.Health.Endpoints/Middleware/HealthCheckEndpointMiddleware.cs index 46cc2f46..bde4cbf9 100644 --- a/src/App.Metrics.AspNetCore.Health.Endpoints/Middleware/HealthCheckEndpointMiddleware.cs +++ b/src/App.Metrics.AspNetCore.Health.Endpoints/Middleware/HealthCheckEndpointMiddleware.cs @@ -19,6 +19,7 @@ public class HealthCheckEndpointMiddleware { private readonly IRunHealthChecks _healthCheckRunner; private readonly IHealthResponseWriter _healthResponseWriter; + private readonly IHealthAuthorizationFilter _healthAuthorizationFilter; private readonly ILogger _logger; private readonly TimeSpan _timeout; @@ -28,12 +29,14 @@ public HealthCheckEndpointMiddleware( ILoggerFactory loggerFactory, IRunHealthChecks healthCheckRunner, IHealthResponseWriter healthResponseWriter, + IHealthAuthorizationFilter healthAuthorizationFilter, TimeSpan timeout) // ReSharper restore UnusedParameter.Local { _healthCheckRunner = healthCheckRunner; _logger = loggerFactory.CreateLogger(); _healthResponseWriter = healthResponseWriter ?? throw new ArgumentNullException(nameof(healthResponseWriter)); + _healthAuthorizationFilter = healthAuthorizationFilter ?? new AuthorizedHealthAuthorizationFilter(); _timeout = timeout <= TimeSpan.Zero ? TimeSpan.FromSeconds(20) : timeout; } @@ -49,9 +52,18 @@ public async Task Invoke(HttpContext context) { try { + if (!_healthAuthorizationFilter.Authorized(context)) + { + _logger.MiddlewareAuthorizationInvalid(); + context.Response.StatusCode = StatusCodes.Status401Unauthorized; + await context.Response.WriteAsync("Invalid authorization.", cancellationToken: cancellationTokenSource.Token); + } + else + { var healthStatus = await _healthCheckRunner.ReadAsync(cancellationTokenSource.Token); await _healthResponseWriter.WriteAsync(context, healthStatus, cancellationTokenSource.Token); + } } catch (OperationCanceledException e) { diff --git a/test/App.Metrics.AspNetCore.Health.Integration.Facts/Middleware/HealthCheckEndpointAuthorizationMiddleware.cs b/test/App.Metrics.AspNetCore.Health.Integration.Facts/Middleware/HealthCheckEndpointAuthorizationMiddleware.cs new file mode 100644 index 00000000..bbd8c59e --- /dev/null +++ b/test/App.Metrics.AspNetCore.Health.Integration.Facts/Middleware/HealthCheckEndpointAuthorizationMiddleware.cs @@ -0,0 +1,31 @@ +// +// Copyright (c) Allan Hardy. All rights reserved. +// + +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using App.Metrics.AspNetCore.Health.Integration.Facts.Startup; +using FluentAssertions; +using Xunit; + +namespace App.Metrics.AspNetCore.Health.Integration.Facts.Middleware +{ + public class HealthCheckEndpointAuthorizationMiddleware : IClassFixture> + { + public HealthCheckEndpointAuthorizationMiddleware(StartupTestFixture fixture) + { + Client = fixture.Client; + } + + private HttpClient Client { get; } + + [Fact] + public async Task Returns_correct_response_headers_not_authorized() + { + var result = await Client.GetAsync("/health"); + + result.StatusCode.Should().Be(HttpStatusCode.Unauthorized); + } + } +} \ No newline at end of file diff --git a/test/App.Metrics.AspNetCore.Health.Integration.Facts/Startup/NotAuthorizedHealthTestStartup.cs b/test/App.Metrics.AspNetCore.Health.Integration.Facts/Startup/NotAuthorizedHealthTestStartup.cs new file mode 100644 index 00000000..52c57013 --- /dev/null +++ b/test/App.Metrics.AspNetCore.Health.Integration.Facts/Startup/NotAuthorizedHealthTestStartup.cs @@ -0,0 +1,37 @@ +// +// Copyright (c) Allan Hardy. All rights reserved. +// + +using App.Metrics.AspNetCore.Health.Endpoints; +using App.Metrics.Health; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace App.Metrics.AspNetCore.Health.Integration.Facts.Startup +{ + // ReSharper disable ClassNeverInstantiated.Global + public class NotAuthorizedHealthTestStartup : TestStartup + // ReSharper restore ClassNeverInstantiated.Global + { + public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) + { + app.UseHealthEndpoint(); + + SetupAppBuilder(app, env, loggerFactory); + } + + public void ConfigureServices(IServiceCollection services) + { + services.AddSingleton(); + + var appMetricsMiddlewareHealthCheckOptions = new HealthEndpointsOptions(); + + SetupServices( + services, + appMetricsMiddlewareHealthCheckOptions, + healthChecks: new[] { HealthCheckResult.Healthy() }); + } + } +} \ No newline at end of file