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/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 0fbfe4fd8..47f80b5f6 100644 --- a/ConcernsCaseWork/ConcernsCaseWork/Extensions/StartupExtension.cs +++ b/ConcernsCaseWork/ConcernsCaseWork/Extensions/StartupExtension.cs @@ -1,211 +1,213 @@ -using ConcernsCaseWork.API.Contracts.Configuration; -using ConcernsCaseWork.API.StartupConfiguration; -using ConcernsCaseWork.Authorization; -using ConcernsCaseWork.Logging; -using ConcernsCaseWork.Pages.Validators; -using ConcernsCaseWork.Redis.Base; -using ConcernsCaseWork.Redis.Configuration; -using ConcernsCaseWork.Redis.Nti; -using ConcernsCaseWork.Redis.NtiWarningLetter; -using ConcernsCaseWork.Redis.Trusts; -using ConcernsCaseWork.Redis.Users; -using ConcernsCaseWork.Service.CaseActions; -using ConcernsCaseWork.Service.Cases; -using ConcernsCaseWork.Service.Decision; -using ConcernsCaseWork.Service.FinancialPlan; -using ConcernsCaseWork.Service.Nti; -using ConcernsCaseWork.Service.NtiUnderConsideration; -using ConcernsCaseWork.Service.NtiWarningLetter; -using ConcernsCaseWork.Service.Permissions; -using ConcernsCaseWork.Service.Records; -using ConcernsCaseWork.Service.TargetedTrustEngagement; -using ConcernsCaseWork.Service.Teams; -using ConcernsCaseWork.Service.TrustFinancialForecast; -using ConcernsCaseWork.Service.Trusts; -using ConcernsCaseWork.Services.Actions; -using ConcernsCaseWork.Services.Cases; -using ConcernsCaseWork.Services.Cases.Create; -using ConcernsCaseWork.Services.Decisions; -using ConcernsCaseWork.Services.FinancialPlan; -using ConcernsCaseWork.Services.Nti; -using ConcernsCaseWork.Services.NtiUnderConsideration; -using ConcernsCaseWork.Services.NtiWarningLetter; -using ConcernsCaseWork.Services.PageHistory; -using ConcernsCaseWork.Services.Records; -using ConcernsCaseWork.Services.Teams; -using ConcernsCaseWork.Services.Trusts; -using ConcernsCaseWork.UserContext; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.DataProtection; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Newtonsoft.Json.Linq; -using StackExchange.Redis; -using System; -using System.Net.Mime; -using System.Threading.Tasks; - -namespace ConcernsCaseWork.Extensions -{ - public static class StartupExtension - { - private static IConnectionMultiplexer _redisConnectionMultiplexer; - //private static ILogger _logger; - - - public static void AddRedis(this IServiceCollection services, IConfiguration configuration) - { - - try - { - var vCapConfiguration = JObject.Parse(configuration["VCAP_SERVICES"]) ?? throw new Exception("AddRedis::VCAP_SERVICES missing"); - var redisCredentials = vCapConfiguration["redis"]?[0]?["credentials"] ?? throw new Exception("AddRedis::Credentials missing"); - var password = (string)redisCredentials["password"] ?? throw new Exception("AddRedis::Credentials::password missing"); - var host = (string)redisCredentials["host"] ?? throw new Exception("AddRedis::Credentials::host missing"); - var port = (string)redisCredentials["port"] ?? throw new Exception("AddRedis::Credentials::port missing"); - var tls = (bool)redisCredentials["tls_enabled"]; - - /*_logger.LogInformation("Starting Redis Server Host - {Host}", host); - _logger.LogInformation("Starting Redis Server Port - {Port}", port); - _logger.LogInformation("Starting Redis Server TLS - {Tls}", tls);*/ - - var redisConfigurationOptions = new ConfigurationOptions { Password = password, EndPoints = { $"{host}:{port}" }, Ssl = tls, AsyncTimeout = 15000, SyncTimeout = 15000 }; - - var preventThreadTheftStr = configuration["PreventRedisThreadTheft"] ?? "false"; - if (bool.TryParse(preventThreadTheftStr, out bool preventThreadTheft) && preventThreadTheft) - { - // https://stackexchange.github.io/StackExchange.Redis/ThreadTheft.html - ConnectionMultiplexer.SetFeatureFlag("preventthreadtheft", true); - } - - _redisConnectionMultiplexer = ConnectionMultiplexer.Connect(redisConfigurationOptions); - services.AddDataProtection().PersistKeysToStackExchangeRedis(_redisConnectionMultiplexer, "DataProtectionKeys"); - - services.AddStackExchangeRedisCache( - options => - { - options.ConfigurationOptions = redisConfigurationOptions; - options.InstanceName = $"Redis-{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT")}"; - options.ConnectionMultiplexerFactory = () => Task.FromResult(_redisConnectionMultiplexer); - }); - - } - catch (Exception ex) - { - // _logger.LogError("AddRedis::Exception::{Message}", ex.Message); - throw new Exception($"AddRedis::Exception::{ex.Message}"); - } - } - - /// - /// HttpFactory for Trams API - /// - /// - /// - /// - public static void AddTramsApi(this IServiceCollection services, IConfiguration configuration) - { - var tramsApiEndpoint = configuration["trams:api_endpoint"]; - var tramsApiKey = configuration["trams:api_key"]; - - if (string.IsNullOrEmpty(tramsApiEndpoint) || string.IsNullOrEmpty(tramsApiKey)) - throw new Exception("AddTramsApi::missing configuration"); - - //_logger.LogInformation("Starting Trams API Endpoint - {TramsApiEndpoint}", tramsApiEndpoint); - - services.AddHttpClient("TramsClient", client => - { - client.BaseAddress = new Uri(tramsApiEndpoint); - client.DefaultRequestHeaders.Add("ApiKey", tramsApiKey); - client.DefaultRequestHeaders.Add("ContentType", MediaTypeNames.Application.Json); - }); - } - - /// - /// HttpFactory for Concerns API - /// - /// - /// - /// - public static void AddConcernsApi(this IServiceCollection services, IConfiguration configuration) - { - services.AddConcernsApiProject(configuration); - } - - public static void AddInternalServices(this IServiceCollection services) - { - services.AddSingleton(); - services.AddSingleton(); - // Web application services - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - - // api services - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - - - // Redis services - services.AddSingleton(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - - services.AddScoped(); - - services.AddHttpContextAccessor(); - services.AddScoped(); - services.AddSingleton(); - } - - public static void AddConfigurationOptions(this IServiceCollection services, IConfiguration configuration) - { - var test = configuration.GetSection(TrustSearchOptions.Cache); - services.Configure(configuration.GetSection(CacheOptions.Cache)); - services.Configure(configuration.GetSection(TrustSearchOptions.Cache)); - services.Configure(configuration.GetSection(SiteOptions.Site)); - services.Configure(configuration.GetSection("FakeTrusts")); - } - } -} \ No newline at end of file +using ConcernsCaseWork.API.Contracts.Configuration; +using ConcernsCaseWork.API.StartupConfiguration; +using ConcernsCaseWork.Authorization; +using ConcernsCaseWork.Logging; +using ConcernsCaseWork.Pages.Validators; +using ConcernsCaseWork.Redis.Base; +using ConcernsCaseWork.Redis.Configuration; +using ConcernsCaseWork.Redis.Nti; +using ConcernsCaseWork.Redis.NtiWarningLetter; +using ConcernsCaseWork.Redis.Trusts; +using ConcernsCaseWork.Redis.Users; +using ConcernsCaseWork.Service.CaseActions; +using ConcernsCaseWork.Service.Cases; +using ConcernsCaseWork.Service.Decision; +using ConcernsCaseWork.Service.FinancialPlan; +using ConcernsCaseWork.Service.Nti; +using ConcernsCaseWork.Service.NtiUnderConsideration; +using ConcernsCaseWork.Service.NtiWarningLetter; +using ConcernsCaseWork.Service.Permissions; +using ConcernsCaseWork.Service.Records; +using ConcernsCaseWork.Service.TargetedTrustEngagement; +using ConcernsCaseWork.Service.Teams; +using ConcernsCaseWork.Service.TrustFinancialForecast; +using ConcernsCaseWork.Service.Trusts; +using ConcernsCaseWork.Services.Actions; +using ConcernsCaseWork.Services.Cases; +using ConcernsCaseWork.Services.Cases.Create; +using ConcernsCaseWork.Services.Decisions; +using ConcernsCaseWork.Services.FinancialPlan; +using ConcernsCaseWork.Services.Nti; +using ConcernsCaseWork.Services.NtiUnderConsideration; +using ConcernsCaseWork.Services.NtiWarningLetter; +using ConcernsCaseWork.Services.PageHistory; +using ConcernsCaseWork.Services.Records; +using ConcernsCaseWork.Services.Teams; +using ConcernsCaseWork.Services.Trusts; +using ConcernsCaseWork.UserContext; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Newtonsoft.Json.Linq; +using StackExchange.Redis; +using System; +using System.Net.Mime; +using System.Threading.Tasks; + +namespace ConcernsCaseWork.Extensions +{ + public static class StartupExtension + { + private static IConnectionMultiplexer _redisConnectionMultiplexer; + //private static ILogger _logger; + + + public static void AddRedis(this IServiceCollection services, IConfiguration configuration) + { + + try + { + var vCapConfiguration = JObject.Parse(configuration["VCAP_SERVICES"]) ?? throw new Exception("AddRedis::VCAP_SERVICES missing"); + var redisCredentials = vCapConfiguration["redis"]?[0]?["credentials"] ?? throw new Exception("AddRedis::Credentials missing"); + var password = (string)redisCredentials["password"] ?? throw new Exception("AddRedis::Credentials::password missing"); + var host = (string)redisCredentials["host"] ?? throw new Exception("AddRedis::Credentials::host missing"); + var port = (string)redisCredentials["port"] ?? throw new Exception("AddRedis::Credentials::port missing"); + var tls = (bool)redisCredentials["tls_enabled"]; + + /*_logger.LogInformation("Starting Redis Server Host - {Host}", host); + _logger.LogInformation("Starting Redis Server Port - {Port}", port); + _logger.LogInformation("Starting Redis Server TLS - {Tls}", tls);*/ + + var redisConfigurationOptions = new ConfigurationOptions { Password = password, EndPoints = { $"{host}:{port}" }, Ssl = tls, AsyncTimeout = 15000, SyncTimeout = 15000 }; + + var preventThreadTheftStr = configuration["PreventRedisThreadTheft"] ?? "false"; + if (bool.TryParse(preventThreadTheftStr, out bool preventThreadTheft) && preventThreadTheft) + { + // https://stackexchange.github.io/StackExchange.Redis/ThreadTheft.html + ConnectionMultiplexer.SetFeatureFlag("preventthreadtheft", true); + } + + _redisConnectionMultiplexer = ConnectionMultiplexer.Connect(redisConfigurationOptions); + services.AddDataProtection().PersistKeysToStackExchangeRedis(_redisConnectionMultiplexer, "DataProtectionKeys"); + + services.AddStackExchangeRedisCache( + options => + { + options.ConfigurationOptions = redisConfigurationOptions; + options.InstanceName = $"Redis-{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT")}"; + options.ConnectionMultiplexerFactory = () => Task.FromResult(_redisConnectionMultiplexer); + }); + + services.AddHealthChecks().AddRedis(_redisConnectionMultiplexer); + } + catch (Exception ex) + { + // _logger.LogError("AddRedis::Exception::{Message}", ex.Message); + throw new Exception($"AddRedis::Exception::{ex.Message}"); + } + } + + /// + /// HttpFactory for Trams API + /// + /// + /// + /// + public static void AddTramsApi(this IServiceCollection services, IConfiguration configuration) + { + var tramsApiEndpoint = configuration["trams:api_endpoint"]; + var tramsApiKey = configuration["trams:api_key"]; + + if (string.IsNullOrEmpty(tramsApiEndpoint) || string.IsNullOrEmpty(tramsApiKey)) + throw new Exception("AddTramsApi::missing configuration"); + + //_logger.LogInformation("Starting Trams API Endpoint - {TramsApiEndpoint}", tramsApiEndpoint); + + services.AddHttpClient("TramsClient", client => + { + client.BaseAddress = new Uri(tramsApiEndpoint); + client.DefaultRequestHeaders.Add("ApiKey", tramsApiKey); + client.DefaultRequestHeaders.Add("ContentType", MediaTypeNames.Application.Json); + client.DefaultRequestHeaders.Add("User-Agent", "RecordConcernsSupportTrusts/1.0"); + }); + } + + /// + /// HttpFactory for Concerns API + /// + /// + /// + /// + public static void AddConcernsApi(this IServiceCollection services, IConfiguration configuration) + { + services.AddConcernsApiProject(configuration); + } + + public static void AddInternalServices(this IServiceCollection services) + { + services.AddSingleton(); + services.AddSingleton(); + // Web application services + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + + // api services + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + + + // Redis services + services.AddSingleton(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + + services.AddScoped(); + + services.AddHttpContextAccessor(); + services.AddScoped(); + services.AddSingleton(); + } + + public static void AddConfigurationOptions(this IServiceCollection services, IConfiguration configuration) + { + var test = configuration.GetSection(TrustSearchOptions.Cache); + services.Configure(configuration.GetSection(CacheOptions.Cache)); + services.Configure(configuration.GetSection(TrustSearchOptions.Cache)); + services.Configure(configuration.GetSection(SiteOptions.Site)); + services.Configure(configuration.GetSection("FakeTrusts")); + } + } +} diff --git a/ConcernsCaseWork/ConcernsCaseWork/Pages/Health.cshtml b/ConcernsCaseWork/ConcernsCaseWork/Pages/Health.cshtml deleted file mode 100644 index 1e83a5be7..000000000 --- a/ConcernsCaseWork/ConcernsCaseWork/Pages/Health.cshtml +++ /dev/null @@ -1,6 +0,0 @@ -@page -@model ConcernsCaseWork.Pages.HealthModel -@{ - 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 56609aebb..1e8b09cac 100644 --- a/ConcernsCaseWork/ConcernsCaseWork/Startup.cs +++ b/ConcernsCaseWork/ConcernsCaseWork/Startup.cs @@ -9,15 +9,12 @@ 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; @@ -35,205 +32,207 @@ 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.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.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; + } + } }