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;
+ }
+ }
}