diff --git a/ConcernsCaseWork/ConcernsCaseWork.API/ConcernsCaseWork.API.csproj b/ConcernsCaseWork/ConcernsCaseWork.API/ConcernsCaseWork.API.csproj index 12c4a43e5..3d9d9044b 100644 --- a/ConcernsCaseWork/ConcernsCaseWork.API/ConcernsCaseWork.API.csproj +++ b/ConcernsCaseWork/ConcernsCaseWork.API/ConcernsCaseWork.API.csproj @@ -24,6 +24,7 @@ + diff --git a/ConcernsCaseWork/ConcernsCaseWork.API/Startup.cs b/ConcernsCaseWork/ConcernsCaseWork.API/Startup.cs index a3f5024ca..1aa1b77fe 100644 --- a/ConcernsCaseWork/ConcernsCaseWork.API/Startup.cs +++ b/ConcernsCaseWork/ConcernsCaseWork.API/Startup.cs @@ -1,50 +1,53 @@ -using ConcernsCaseWork.API.Extensions; -using ConcernsCaseWork.API.Middleware; -using ConcernsCaseWork.API.StartupConfiguration; -using ConcernsCaseWork.Middleware; -using Microsoft.AspNetCore.Mvc.ApiExplorer; - -namespace ConcernsCaseWork.API -{ - /// - /// THIS STARTUP ISN'T USED WHEN API IS HOSTED THROUGH WEBSITE. It is used when running API tests - /// - public class Startup - { - public Startup(IConfiguration configuration) - { - Configuration = configuration; - } - - public IConfiguration Configuration { get; } - - public void ConfigureServices(IServiceCollection services) - { - services.AddConcernsApiProject(Configuration); - } - - public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IApiVersionDescriptionProvider provider) - { - app.UseConcernsCaseworkSwagger(provider); - - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - - app.UseMiddleware(); - app.UseMiddleware(); - app.UseMiddleware(); - app.UseMiddleware(); - app.UseMiddleware(); - - app.UseHttpsRedirection(); - - app.UseRouting(); - - app.UseAuthorization(); - - app.UseConcernsCaseworkEndpoints(); - } - } -} +using ConcernsCaseWork.API.Extensions; +using ConcernsCaseWork.API.Middleware; +using ConcernsCaseWork.API.StartupConfiguration; +using ConcernsCaseWork.Middleware; +using Microsoft.AspNetCore.Mvc.ApiExplorer; + +namespace ConcernsCaseWork.API +{ + /// + /// THIS STARTUP ISN'T USED WHEN API IS HOSTED THROUGH WEBSITE. It is used when running API tests + /// + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + public void ConfigureServices(IServiceCollection services) + { + services.AddConcernsApiProject(Configuration); + } + + public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IApiVersionDescriptionProvider provider) + { + app.UseConcernsCaseworkSwagger(provider); + + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseMiddleware(); + app.UseMiddleware(); + app.UseMiddleware(); + app.UseMiddleware(); + app.UseMiddleware(); + + app.UseHttpsRedirection(); + + app.UseRouting(); + + app.UseAuthorization(); + + app.UseConcernsCaseworkEndpoints(); + + // Add Health Checks + app.UseHealthChecks("/health"); + } + } +} diff --git a/ConcernsCaseWork/ConcernsCaseWork.API/StartupConfiguration/DatabaseConfigurationExtensions.cs b/ConcernsCaseWork/ConcernsCaseWork.API/StartupConfiguration/DatabaseConfigurationExtensions.cs index 1bf86e7da..cfa7bc30e 100644 --- a/ConcernsCaseWork/ConcernsCaseWork.API/StartupConfiguration/DatabaseConfigurationExtensions.cs +++ b/ConcernsCaseWork/ConcernsCaseWork.API/StartupConfiguration/DatabaseConfigurationExtensions.cs @@ -1,16 +1,18 @@ -using ConcernsCaseWork.Data; - -namespace ConcernsCaseWork.API.StartupConfiguration; - -public static class DatabaseConfigurationExtensions -{ - public static IServiceCollection AddDatabase(this IServiceCollection services, IConfiguration configuration) - { - var connectionString = configuration.GetConnectionString("DefaultConnection"); - services.AddDbContext(options => - options.UseConcernsSqlServer(connectionString) - ); - - return services; - } -} \ No newline at end of file +using ConcernsCaseWork.Data; +using Microsoft.Extensions.Diagnostics.HealthChecks; +namespace ConcernsCaseWork.API.StartupConfiguration; + +public static class DatabaseConfigurationExtensions +{ + public static IServiceCollection AddDatabase(this IServiceCollection services, IConfiguration configuration) + { + var connectionString = configuration.GetConnectionString("DefaultConnection"); + services.AddDbContext(options => + options.UseConcernsSqlServer(connectionString) + ); + services.AddHealthChecks() + .AddDbContextCheck("Concerns Database"); + + return services; + } +} diff --git a/ConcernsCaseWork/ConcernsCaseWork.Tests/ExpectedSecurityConfig.json b/ConcernsCaseWork/ConcernsCaseWork.Tests/ExpectedSecurityConfig.json index faef48b12..c6cad2e16 100644 --- a/ConcernsCaseWork/ConcernsCaseWork.Tests/ExpectedSecurityConfig.json +++ b/ConcernsCaseWork/ConcernsCaseWork.Tests/ExpectedSecurityConfig.json @@ -1,9 +1,5 @@ { "Endpoints": [ - { - "Route": "/Health", - "ExpectedSecurity": "AllowAnonymous" - }, { "Route": "/AccessDenied", "ExpectedSecurity": "AllowAnonymous" @@ -33,4 +29,4 @@ "ExpectedSecurity": "AllowAnonymous" } ] -} \ No newline at end of file +} diff --git a/ConcernsCaseWork/ConcernsCaseWork.Tests/Security/AuthorizeAttributeTests.cs b/ConcernsCaseWork/ConcernsCaseWork.Tests/Security/AuthorizeAttributeTests.cs index 7c4287807..3ed522208 100644 --- a/ConcernsCaseWork/ConcernsCaseWork.Tests/Security/AuthorizeAttributeTests.cs +++ b/ConcernsCaseWork/ConcernsCaseWork.Tests/Security/AuthorizeAttributeTests.cs @@ -1,77 +1,55 @@ -using ConcernsCaseWork.Pages; -using ConcernsCaseWork.Pages.Base; -using FluentAssertions; -using FluentAssertions.Execution; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc.RazorPages; -using NUnit.Framework; -using System; -using System.Linq; -using System.Reflection; - -namespace ConcernsCaseWork.Tests.Security -{ - public class AuthorizeAttributeTests - { - public AuthorizeAttributeTests() - { - this.UnauthorizedPages = new Type[] - { - typeof(HealthModel), - typeof(ErrorPageModel), - }; - } - - public Type[] UnauthorizedPages { get; } - - [Test] - public void All_Pages_Include_Authorize_Attribute() - { - var pages = this.GetAllPagesExceptUnauthorizedPages(); - - pages.Length.Should().BeGreaterThan(0); - - using (var scope = new AssertionScope()) - { - foreach (Type page in pages) - { - var authAttributes = page.GetCustomAttributes(); - if (authAttributes == null || !authAttributes.Any()) - { - scope.AddPreFormattedFailure($"Could not find [Authorize] attribute and no exemption for the following page type: {page.Name} ({page.FullName})"); - } - } - } - } - - [Test] - public void Open_Pages_Do_Not_Require_Authorization() - { - Type[] UnauthorizedPages = new[] - { - typeof(HealthModel), - }; - - using (var scope = new AssertionScope()) - { - foreach (Type page in this.UnauthorizedPages) - { - var authAttribute = page.GetCustomAttribute(); - if (authAttribute != null) - { - scope.AddPreFormattedFailure($"Expected page to be open and not require authorisation. But found [Authorize] attribute on the following page type: {page.Name} ({page.FullName})"); - } - } - } - } - - private Type[] GetAllPagesExceptUnauthorizedPages() - { - return Assembly - .GetAssembly(typeof(AbstractPageModel)) - .GetTypes() - .Where(x => x.IsAssignableTo(typeof(PageModel)) && !this.UnauthorizedPages.Contains(x)) - .ToArray(); - } - } -} \ No newline at end of file +using ConcernsCaseWork.Pages; +using ConcernsCaseWork.Pages.Base; +using FluentAssertions; +using FluentAssertions.Execution; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc.RazorPages; +using NUnit.Framework; +using System; +using System.Linq; +using System.Reflection; + +namespace ConcernsCaseWork.Tests.Security +{ + public class AuthorizeAttributeTests + { + public AuthorizeAttributeTests() + { + this.UnauthorizedPages = new Type[] + { + typeof(ErrorPageModel), + }; + } + + public Type[] UnauthorizedPages { get; } + + [Test] + public void All_Pages_Include_Authorize_Attribute() + { + var pages = this.GetAllPagesExceptUnauthorizedPages(); + + pages.Length.Should().BeGreaterThan(0); + + using (var scope = new AssertionScope()) + { + foreach (Type page in pages) + { + var authAttributes = page.GetCustomAttributes(); + if (authAttributes == null || !authAttributes.Any()) + { + scope.AddPreFormattedFailure($"Could not find [Authorize] attribute and no exemption for the following page type: {page.Name} ({page.FullName})"); + } + } + } + } + + private Type[] GetAllPagesExceptUnauthorizedPages() + { + return Assembly + .GetAssembly(typeof(AbstractPageModel)) + .GetTypes() + .Where(x => x.IsAssignableTo(typeof(PageModel)) && !this.UnauthorizedPages.Contains(x)) + .ToArray(); + } + } +} diff --git a/ConcernsCaseWork/ConcernsCaseWork/ConcernsCaseWork.csproj b/ConcernsCaseWork/ConcernsCaseWork/ConcernsCaseWork.csproj index ed2817c2a..a383efe9b 100644 --- a/ConcernsCaseWork/ConcernsCaseWork/ConcernsCaseWork.csproj +++ b/ConcernsCaseWork/ConcernsCaseWork/ConcernsCaseWork.csproj @@ -33,6 +33,7 @@ + diff --git a/ConcernsCaseWork/ConcernsCaseWork/Extensions/StartupExtension.cs b/ConcernsCaseWork/ConcernsCaseWork/Extensions/StartupExtension.cs index f8fefee03..47f80b5f6 100644 --- a/ConcernsCaseWork/ConcernsCaseWork/Extensions/StartupExtension.cs +++ b/ConcernsCaseWork/ConcernsCaseWork/Extensions/StartupExtension.cs @@ -89,6 +89,7 @@ public static void AddRedis(this IServiceCollection services, IConfiguration con options.ConnectionMultiplexerFactory = () => Task.FromResult(_redisConnectionMultiplexer); }); + services.AddHealthChecks().AddRedis(_redisConnectionMultiplexer); } catch (Exception ex) { diff --git a/ConcernsCaseWork/ConcernsCaseWork/Pages/Health.cshtml b/ConcernsCaseWork/ConcernsCaseWork/Pages/Health.cshtml deleted file mode 100644 index 56f3f1ef2..000000000 --- a/ConcernsCaseWork/ConcernsCaseWork/Pages/Health.cshtml +++ /dev/null @@ -1,8 +0,0 @@ -@page -@using Microsoft.AspNetCore.Authorization -@model ConcernsCaseWork.Pages.HealthModel -@attribute [AllowAnonymous] -@{ - Layout = null; - @Model.BuildGuid -} diff --git a/ConcernsCaseWork/ConcernsCaseWork/Pages/Health.cshtml.cs b/ConcernsCaseWork/ConcernsCaseWork/Pages/Health.cshtml.cs deleted file mode 100644 index 94a7f1e4d..000000000 --- a/ConcernsCaseWork/ConcernsCaseWork/Pages/Health.cshtml.cs +++ /dev/null @@ -1,20 +0,0 @@ -using ConcernsCaseWork.Attributes; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.RazorPages; -using System.Reflection; - - -namespace ConcernsCaseWork.Pages -{ - [ResponseCache(NoStore = true, Duration = 0)] - public class HealthModel : PageModel - { - public string BuildGuid { get; set; } - - public void OnGet() - { - var assembly = Assembly.GetEntryAssembly(); - this.BuildGuid = assembly.GetCustomAttribute().BuildGuid; - } - } -} diff --git a/ConcernsCaseWork/ConcernsCaseWork/Program.cs b/ConcernsCaseWork/ConcernsCaseWork/Program.cs index 75980a2a4..61ecf43f9 100644 --- a/ConcernsCaseWork/ConcernsCaseWork/Program.cs +++ b/ConcernsCaseWork/ConcernsCaseWork/Program.cs @@ -1,24 +1,23 @@ -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Hosting; - - -namespace ConcernsCaseWork -{ - public class Program - { - public static void Main(string[] args) - { - - CreateHostBuilder(args).Build().Run(); - } - - private static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder - .ConfigureKestrel(options => options.AddServerHeader = false) - .UseStartup(); - }); - } -} +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + + +namespace ConcernsCaseWork +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + private static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder + .ConfigureKestrel(options => options.AddServerHeader = false) + .UseStartup(); + }); + } +} diff --git a/ConcernsCaseWork/ConcernsCaseWork/Startup.cs b/ConcernsCaseWork/ConcernsCaseWork/Startup.cs index 273bb2491..e265fe02c 100644 --- a/ConcernsCaseWork/ConcernsCaseWork/Startup.cs +++ b/ConcernsCaseWork/ConcernsCaseWork/Startup.cs @@ -9,236 +9,234 @@ using ConcernsCaseWork.Security; using ConcernsCaseWork.Services.PageHistory; using ConcernsCaseWork.UserContext; -using FluentAssertions.Common; using Microsoft.AspNetCore.Authentication.Cookies; -using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.HttpOverrides; -using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc.ApiExplorer; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Hosting; using Microsoft.FeatureManagement; -using Microsoft.Identity.Web; -using System; +using Microsoft.Identity.Web; +using System; using System.Security.Claims; -using System.Threading; +using System.Threading; namespace ConcernsCaseWork { - public class Startup - { - public Startup(IConfiguration configuration) - { - Configuration = configuration; - } - - private IConfiguration Configuration { get; } - - // This method gets called by the runtime. Use this method to add services to the container. - public void ConfigureServices(IServiceCollection services) - { - services.AddRazorPages(options => - { - options.Conventions.AuthorizeFolder("/"); - options.Conventions.AllowAnonymousToPage("/Health"); - options.Conventions.AllowAnonymousToPage("/AccessDenied"); - options.Conventions.AllowAnonymousToPage("/Maintenance"); - options.Conventions.AllowAnonymousToPage("/Accessibility"); - options.Conventions.AllowAnonymousToPage("/Cookies"); - options.Conventions.AllowAnonymousToPage("/PrivacyPolicy"); - options.Conventions.AllowAnonymousToPage("/NotFound"); - options.Conventions.AllowAnonymousToPage("/Error"); - options.Conventions.AddPageRoute("/home", ""); - options.Conventions.AddPageRoute("/notfound", "/error/404"); - options.Conventions.AddPageRoute("/notfound", "/error/{code:int}"); - options.Conventions.AddPageRoute("/case/management/action/NtiWarningLetter/add", "/case/{urn:long}/management/action/NtiWarningLetter/add"); - options.Conventions.AddPageRoute("/case/management/action/NtiWarningLetter/addConditions", "/case/{urn:long}/management/action/NtiWarningLetter/conditions"); - options.Conventions.AddPageRoute("/case/management/action/Nti/add", "/case/{urn:long}/management/action/nti/add"); - options.Conventions.AddPageRoute("/case/management/action/Nti/addConditions", "/case/{urn:long}/management/action/nti/conditions"); - - - // TODO: - // Consider adding: options.Conventions.AuthorizeFolder("/"); - }).AddViewOptions(options => - { - options.HtmlHelperOptions.ClientValidationEnabled = false; - }); - - services.AddFeatureManagement(); - - // Configuration options - services.AddConfigurationOptions(Configuration); - - // Azure AD - services.AddAuthorization(options => - { - options.DefaultPolicy = SetupAuthorizationPolicyBuilder().Build(); - options.AddPolicy("CanDelete", builder => - { - builder.RequireClaim(ClaimTypes.Role, Claims.CaseDeleteRoleClaim); - }); - }); - - services.AddMicrosoftIdentityWebAppAuthentication(Configuration); - services.Configure(CookieAuthenticationDefaults.AuthenticationScheme, - options => - { - options.Cookie.Name = ".ConcernsCasework.Login"; - options.Cookie.HttpOnly = true; - options.Cookie.IsEssential = true; - options.ExpireTimeSpan = TimeSpan.FromMinutes(int.Parse(Configuration["AuthenticationExpirationInMinutes"])); - options.SlidingExpiration = true; - options.Cookie.SecurePolicy = CookieSecurePolicy.Always; // in A2B this was only if string.IsNullOrEmpty(Configuration["CI"]), but why not always? - options.AccessDeniedPath = "/access-denied"; - }); - - services.AddAntiforgery(options => - { - options.Cookie.SecurePolicy = CookieSecurePolicy.Always; - }); - - // Redis - services.AddRedis(Configuration); - - // APIs - services.AddTramsApi(Configuration); - services.AddConcernsApi(Configuration); - - // AutoMapper - services.ConfigureAndAddAutoMapper(); - - - // Route options - services.Configure(options => { options.LowercaseUrls = true; }); - - // Internal Service - services.AddInternalServices(); - - // Session - services.AddSession(options => - { - options.IdleTimeout = TimeSpan.FromHours(24); - options.Cookie.Name = ".ConcernsCasework.Session"; - options.Cookie.HttpOnly = true; - options.Cookie.IsEssential = true; - options.Cookie.SecurePolicy = CookieSecurePolicy.Always; - }); - - services.AddRouting(options => - { - options.ConstraintMap.Add("fpEditModes", typeof(FinancialPlanEditModeConstraint)); - }); - services.AddApplicationInsightsTelemetry(options => - { - options.ConnectionString = Configuration["ApplicationInsights:ConnectionString"]; - }); - // Enforce HTTPS in ASP.NET Core - // @link https://learn.microsoft.com/en-us/aspnet/core/security/enforcing-ssl? - services.AddHsts(options => - { - options.Preload = true; - options.IncludeSubDomains = true; - options.MaxAge = TimeSpan.FromDays(365); - }); - } - - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IApiVersionDescriptionProvider provider, IMapper mapper) - { - // Ensure we do not lose X-Forwarded-* Headers when behind a Proxy - var forwardOptions = new ForwardedHeadersOptions { - ForwardedHeaders = ForwardedHeaders.All, - RequireHeaderSymmetry = false - }; - forwardOptions.KnownNetworks.Clear(); - forwardOptions.KnownProxies.Clear(); - app.UseForwardedHeaders(forwardOptions); - - AbstractPageModel.PageHistoryStorageHandler = app.ApplicationServices.GetService(); - - app.UseConcernsCaseworkSwagger(provider); - - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - else - { - app.UseExceptionHandler("/Error"); - } - - app.UseMiddleware(); - app.UseMiddleware(); - - // Security headers - app.UseSecurityHeaders( - SecurityHeadersDefinitions.GetHeaderPolicyCollection(env.IsDevelopment())); - app.UseHsts(); - - // Combined with razor routing 404 display custom page NotFound - app.UseStatusCodePagesWithReExecute("/error/{0}"); - - app.UseHttpsRedirection(); - - app.UseStaticFiles(); - - // Enable session for the application - app.UseSession(); - - - app.UseMiddleware(); - - - app.UseRouting(); - - app.UseAuthentication(); - app.UseAuthorization(); - app.UseMiddleware(); - app.UseMiddleware(); - app.UseMiddleware(); - app.UseMiddlewareForFeature(FeatureFlags.IsMaintenanceModeEnabled); - - app.UseConcernsCaseworkEndpoints(); - - app.UseEndpoints(endpoints => - { - endpoints.MapRazorPages(); - }); - - mapper.CompileAndValidate(); - - // If our application gets hit really hard, then threads need to be spawned - // By default the number of threads that exist in the threadpool is the amount of CPUs (1) - // Each time we have to spawn a new thread it gets delayed by 500ms - // Setting the min higher means there will not be that delay in creating threads up to the min - // Re-evaluate this based on performance tests - // Found because redis kept timing out because it was delayed too long waiting for a thread to execute - ThreadPool.SetMinThreads(400, 400); - } - - /// - /// Builds Authorization policy - /// Ensure authenticated user and restrict roles if they are provided in configuration - /// - /// AuthorizationPolicyBuilder - private AuthorizationPolicyBuilder SetupAuthorizationPolicyBuilder() - { - var policyBuilder = new AuthorizationPolicyBuilder(); - var allowedRoles = Configuration.GetSection("AzureAd")["AllowedRoles"]; - policyBuilder.RequireAuthenticatedUser(); - // Allows us to add in role support later. - if (!string.IsNullOrWhiteSpace(allowedRoles)) - { - policyBuilder.RequireClaim(ClaimTypes.Role, allowedRoles.Split(',')); - } - - return policyBuilder; - } - } + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + private IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddRazorPages(options => + { + options.Conventions.AuthorizeFolder("/"); + options.Conventions.AllowAnonymousToPage("/AccessDenied"); + options.Conventions.AllowAnonymousToPage("/Maintenance"); + options.Conventions.AllowAnonymousToPage("/Accessibility"); + options.Conventions.AllowAnonymousToPage("/Cookies"); + options.Conventions.AllowAnonymousToPage("/PrivacyPolicy"); + options.Conventions.AllowAnonymousToPage("/NotFound"); + options.Conventions.AllowAnonymousToPage("/Error"); + options.Conventions.AddPageRoute("/home", ""); + options.Conventions.AddPageRoute("/notfound", "/error/404"); + options.Conventions.AddPageRoute("/notfound", "/error/{code:int}"); + options.Conventions.AddPageRoute("/case/management/action/NtiWarningLetter/add", "/case/{urn:long}/management/action/NtiWarningLetter/add"); + options.Conventions.AddPageRoute("/case/management/action/NtiWarningLetter/addConditions", "/case/{urn:long}/management/action/NtiWarningLetter/conditions"); + options.Conventions.AddPageRoute("/case/management/action/Nti/add", "/case/{urn:long}/management/action/nti/add"); + options.Conventions.AddPageRoute("/case/management/action/Nti/addConditions", "/case/{urn:long}/management/action/nti/conditions"); + + + // TODO: + // Consider adding: options.Conventions.AuthorizeFolder("/"); + }).AddViewOptions(options => + { + options.HtmlHelperOptions.ClientValidationEnabled = false; + }); + + services.AddFeatureManagement(); + + // Configuration options + services.AddConfigurationOptions(Configuration); + + // Azure AD + services.AddAuthorization(options => + { + options.DefaultPolicy = SetupAuthorizationPolicyBuilder().Build(); + options.AddPolicy("CanDelete", builder => + { + builder.RequireClaim(ClaimTypes.Role, Claims.CaseDeleteRoleClaim); + }); + }); + + services.AddMicrosoftIdentityWebAppAuthentication(Configuration); + services.Configure(CookieAuthenticationDefaults.AuthenticationScheme, + options => + { + options.Cookie.Name = ".ConcernsCasework.Login"; + options.Cookie.HttpOnly = true; + options.Cookie.IsEssential = true; + options.ExpireTimeSpan = TimeSpan.FromMinutes(int.Parse(Configuration["AuthenticationExpirationInMinutes"])); + options.SlidingExpiration = true; + options.Cookie.SecurePolicy = CookieSecurePolicy.Always; // in A2B this was only if string.IsNullOrEmpty(Configuration["CI"]), but why not always? + options.AccessDeniedPath = "/access-denied"; + }); + + services.AddAntiforgery(options => + { + options.Cookie.SecurePolicy = CookieSecurePolicy.Always; + }); + + // Redis + services.AddRedis(Configuration); + + // APIs + services.AddTramsApi(Configuration); + services.AddConcernsApi(Configuration); + + // AutoMapper + services.ConfigureAndAddAutoMapper(); + + // Route options + services.Configure(options => { options.LowercaseUrls = true; }); + + // Internal Service + services.AddInternalServices(); + + // Session + services.AddSession(options => + { + options.IdleTimeout = TimeSpan.FromHours(24); + options.Cookie.Name = ".ConcernsCasework.Session"; + options.Cookie.HttpOnly = true; + options.Cookie.IsEssential = true; + options.Cookie.SecurePolicy = CookieSecurePolicy.Always; + }); + + services.AddRouting(options => + { + options.ConstraintMap.Add("fpEditModes", typeof(FinancialPlanEditModeConstraint)); + }); + services.AddApplicationInsightsTelemetry(options => + { + options.ConnectionString = Configuration["ApplicationInsights:ConnectionString"]; + }); + // Enforce HTTPS in ASP.NET Core + // @link https://learn.microsoft.com/en-us/aspnet/core/security/enforcing-ssl? + services.AddHsts(options => + { + options.Preload = true; + options.IncludeSubDomains = true; + options.MaxAge = TimeSpan.FromDays(365); + }); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IApiVersionDescriptionProvider provider, IMapper mapper) + { + // Ensure we do not lose X-Forwarded-* Headers when behind a Proxy + var forwardOptions = new ForwardedHeadersOptions { + ForwardedHeaders = ForwardedHeaders.All, + RequireHeaderSymmetry = false + }; + forwardOptions.KnownNetworks.Clear(); + forwardOptions.KnownProxies.Clear(); + app.UseForwardedHeaders(forwardOptions); + + AbstractPageModel.PageHistoryStorageHandler = app.ApplicationServices.GetService(); + + app.UseConcernsCaseworkSwagger(provider); + + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + else + { + app.UseExceptionHandler("/Error"); + } + + app.UseMiddleware(); + app.UseMiddleware(); + + // Security headers + app.UseSecurityHeaders( + SecurityHeadersDefinitions.GetHeaderPolicyCollection(env.IsDevelopment())); + app.UseHsts(); + + // Combined with razor routing 404 display custom page NotFound + app.UseStatusCodePagesWithReExecute("/error/{0}"); + + app.UseHttpsRedirection(); + + app.UseStaticFiles(); + + // Enable session for the application + app.UseSession(); + + + app.UseMiddleware(); + + + app.UseRouting(); + + app.UseAuthentication(); + app.UseAuthorization(); + app.UseMiddleware(); + app.UseMiddleware(); + app.UseMiddleware(); + app.UseMiddlewareForFeature(FeatureFlags.IsMaintenanceModeEnabled); + + app.UseConcernsCaseworkEndpoints(); + + app.UseEndpoints(endpoints => + { + endpoints.MapRazorPages(); + }); + + mapper.CompileAndValidate(); + + // If our application gets hit really hard, then threads need to be spawned + // By default the number of threads that exist in the threadpool is the amount of CPUs (1) + // Each time we have to spawn a new thread it gets delayed by 500ms + // Setting the min higher means there will not be that delay in creating threads up to the min + // Re-evaluate this based on performance tests + // Found because redis kept timing out because it was delayed too long waiting for a thread to execute + ThreadPool.SetMinThreads(400, 400); + + // Add Health Checks + app.UseHealthChecks("/health"); + } + + /// + /// Builds Authorization policy + /// Ensure authenticated user and restrict roles if they are provided in configuration + /// + /// AuthorizationPolicyBuilder + private AuthorizationPolicyBuilder SetupAuthorizationPolicyBuilder() + { + var policyBuilder = new AuthorizationPolicyBuilder(); + var allowedRoles = Configuration.GetSection("AzureAd")["AllowedRoles"]; + policyBuilder.RequireAuthenticatedUser(); + // Allows us to add in role support later. + if (!string.IsNullOrWhiteSpace(allowedRoles)) + { + policyBuilder.RequireClaim(ClaimTypes.Role, allowedRoles.Split(',')); + } + + return policyBuilder; + } + } }