From 3de37a91ee1ce3cc93dbdd173d3a6f641d0a483d Mon Sep 17 00:00:00 2001 From: Dharm Date: Fri, 27 Dec 2024 15:43:26 +0000 Subject: [PATCH 1/3] DP-1035 Dynamic FTS return URLs for integration environment --- .../FtsUrlServiceTests.cs | 21 ++++++-- .../Pages/OneLoginTest.cs | 48 ++++++++++++++++++- .../Constants/FeatureFlags.cs | 1 + .../CO.CDP.OrganisationApp/FtsUrlService.cs | 8 +++- .../Pages/OneLogin.cshtml.cs | 25 +++++++--- Frontend/CO.CDP.OrganisationApp/Session.cs | 1 + .../appsettings.AwsIntegration.json | 3 +- .../CO.CDP.OrganisationApp/appsettings.json | 4 +- 8 files changed, 97 insertions(+), 14 deletions(-) diff --git a/Frontend/CO.CDP.OrganisationApp.Tests/FtsUrlServiceTests.cs b/Frontend/CO.CDP.OrganisationApp.Tests/FtsUrlServiceTests.cs index 6de546262..5f385fdb0 100644 --- a/Frontend/CO.CDP.OrganisationApp.Tests/FtsUrlServiceTests.cs +++ b/Frontend/CO.CDP.OrganisationApp.Tests/FtsUrlServiceTests.cs @@ -7,16 +7,31 @@ namespace CO.CDP.OrganisationApp.Tests; public class FtsUrlServiceTests { + private readonly Mock _sessionMock; private readonly Mock _configurationMock; private readonly Mock _cookiePreferencesService; private readonly IFtsUrlService _service; public FtsUrlServiceTests() { + _sessionMock = new(); _configurationMock = new Mock(); _cookiePreferencesService = new Mock(); _configurationMock.Setup(c => c["FtsService"]).Returns("https://example.com/"); - _service = new FtsUrlService(_configurationMock.Object, _cookiePreferencesService.Object); + _service = new FtsUrlService(_configurationMock.Object, _cookiePreferencesService.Object, _sessionMock.Object); + } + + [Fact] + public void BuildUrl_WhenFtsServiceOriginSessionIsSet_BaseServiceUrlOriginShouldBeSessionValue() + { + _sessionMock.Setup(s => s.Get(Session.FtsServiceOrigin)).Returns("https://example1.com/"); + _configurationMock.Setup(c => c["FtsService"]).Returns("https://example2.com/"); + var service = new FtsUrlService(_configurationMock.Object, _cookiePreferencesService.Object, _sessionMock.Object); + CultureInfo.CurrentUICulture = new CultureInfo("en-GB"); + + var result = service.BuildUrl("test-endpoint"); + + result.Should().Be("https://example1.com/test-endpoint?language=en_GB&cookies_accepted=unknown"); } [Fact] @@ -24,7 +39,7 @@ public void Constructor_ShouldThrowException_WhenFtsServiceIsNotConfigured() { _configurationMock.Setup(c => c["FtsService"]).Returns((string?)null); - Action action = () => new FtsUrlService(_configurationMock.Object, _cookiePreferencesService.Object); + Action action = () => new FtsUrlService(_configurationMock.Object, _cookiePreferencesService.Object, _sessionMock.Object); action.Should().Throw() .WithMessage("FtsService is not configured."); @@ -34,7 +49,7 @@ public void Constructor_ShouldThrowException_WhenFtsServiceIsNotConfigured() public void BuildUrl_ShouldTrimTrailingSlashFromBaseServiceUrl() { _configurationMock.Setup(c => c["FtsService"]).Returns("https://example.com/"); - var service = new FtsUrlService(_configurationMock.Object, _cookiePreferencesService.Object); + var service = new FtsUrlService(_configurationMock.Object, _cookiePreferencesService.Object, _sessionMock.Object); var endpoint = "test-endpoint"; CultureInfo.CurrentUICulture = new CultureInfo("en-GB"); diff --git a/Frontend/CO.CDP.OrganisationApp.Tests/Pages/OneLoginTest.cs b/Frontend/CO.CDP.OrganisationApp.Tests/Pages/OneLoginTest.cs index 40574616d..de4afbb14 100644 --- a/Frontend/CO.CDP.OrganisationApp.Tests/Pages/OneLoginTest.cs +++ b/Frontend/CO.CDP.OrganisationApp.Tests/Pages/OneLoginTest.cs @@ -1,4 +1,5 @@ using CO.CDP.OrganisationApp.Authentication; +using CO.CDP.OrganisationApp.Constants; using CO.CDP.OrganisationApp.Models; using CO.CDP.OrganisationApp.Pages; using CO.CDP.OrganisationApp.WebApiClients; @@ -9,7 +10,9 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; +using Microsoft.FeatureManagement; using Moq; using System.Security.Claims; @@ -24,6 +27,8 @@ public class OneLoginTest private readonly Mock oneLoginAuthorityMock = new(); private readonly Mock authService = new(); private readonly Mock authorityClientMock = new(); + private readonly Mock featureManagerMock = new(); + private readonly Mock configMock = new(); private const string urn = "urn:fdc:gov.uk:2022:7wTqYGMFQxgukTSpSI2GodMwe9"; [Fact] @@ -48,6 +53,45 @@ public async Task OnGetSignIn_WhenRedirectUrlProvides_ShouldReturnAuthChallangeW .Which.Properties!.RedirectUri.Should().Be("/one-login/user-info?redirectUri=%2Forg%2F1"); } + [Fact] + public async Task OnGetSignIn_WhenOriginIsNotNull_ShouldSetSessionWithFtsServiceOriginKey() + { + configMock.Setup(c => c["FtsServiceAllowedOrigins"]).Returns("http://example1.com,http://example2.com"); + featureManagerMock.Setup(f => f.IsEnabledAsync(FeatureFlags.AllowDynamicFtsOrigins)).ReturnsAsync(true); + var origin = "http://example1.com"; + var model = GivenOneLoginModel("sign-in"); + + var result = await model.OnGetAsync("/org/1", origin: origin); + + sessionMock.Verify(s => s.Set(Session.FtsServiceOrigin, origin), Times.Once); + } + + [Fact] + public async Task OnGetSignIn_WhenOriginIsNotListedInConfiguration_ShouldNotSetSessionWithFtsServiceOriginKey() + { + configMock.Setup(c => c["FtsServiceAllowedOrigins"]).Returns("http://example1.com,http://example2.com"); + featureManagerMock.Setup(f => f.IsEnabledAsync(FeatureFlags.AllowDynamicFtsOrigins)).ReturnsAsync(true); + var origin = "http://example3.com"; + var model = GivenOneLoginModel("sign-in"); + + var result = await model.OnGetAsync("/org/1", origin: origin); + + sessionMock.Verify(s => s.Set(Session.FtsServiceOrigin, origin), Times.Never); + } + + [Fact] + public async Task OnGetSignIn_WhenAllowDynamicFtsOriginsFeatureIsDisabled_ShouldNotSetSessionWithFtsServiceOriginKey() + { + configMock.Setup(c => c["FtsServiceAllowedOrigins"]).Returns("http://example1.com,http://example2.com"); + featureManagerMock.Setup(f => f.IsEnabledAsync(FeatureFlags.AllowDynamicFtsOrigins)).ReturnsAsync(false); + + var model = GivenOneLoginModel("sign-in"); + + var result = await model.OnGetAsync("/org/1", origin: "http://example1.com"); + + sessionMock.Verify(s => s.Set(Session.FtsServiceOrigin, "origin"), Times.Never); + } + [Fact] public async Task OnGetUserInfo_OnSuccessfulAuthentication_ShouldRetrieveUserProfile() { @@ -320,7 +364,9 @@ private OneLoginModel GivenOneLoginModel(string pageAction) logoutManagerMock.Object, oneLoginAuthorityMock.Object, authorityClientMock.Object, - new Mock>().Object) + new Mock>().Object, + featureManagerMock.Object, + configMock.Object) { PageAction = pageAction }; } } \ No newline at end of file diff --git a/Frontend/CO.CDP.OrganisationApp/Constants/FeatureFlags.cs b/Frontend/CO.CDP.OrganisationApp/Constants/FeatureFlags.cs index 19e20fad1..54793e8aa 100644 --- a/Frontend/CO.CDP.OrganisationApp/Constants/FeatureFlags.cs +++ b/Frontend/CO.CDP.OrganisationApp/Constants/FeatureFlags.cs @@ -3,4 +3,5 @@ namespace CO.CDP.OrganisationApp.Constants; public static class FeatureFlags { public const string Consortium = "Consortium"; + public const string AllowDynamicFtsOrigins = "AllowDynamicFtsOrigins"; } \ No newline at end of file diff --git a/Frontend/CO.CDP.OrganisationApp/FtsUrlService.cs b/Frontend/CO.CDP.OrganisationApp/FtsUrlService.cs index 1410bb96c..812cd5de2 100644 --- a/Frontend/CO.CDP.OrganisationApp/FtsUrlService.cs +++ b/Frontend/CO.CDP.OrganisationApp/FtsUrlService.cs @@ -7,10 +7,14 @@ public class FtsUrlService : IFtsUrlService { private readonly string _ftsService; private readonly ICookiePreferencesService _cookiePreferencesService; + private readonly ISession _session; - public FtsUrlService(IConfiguration configuration, ICookiePreferencesService cookiePreferencesService) + public FtsUrlService(IConfiguration configuration, ICookiePreferencesService cookiePreferencesService, ISession session) { - var ftsService = configuration["FtsService"] ?? throw new InvalidOperationException("FtsService is not configured."); + _session = session; + var ftsService = _session.Get(Session.FtsServiceOrigin) + ?? configuration["FtsService"] + ?? throw new InvalidOperationException("FtsService is not configured."); _ftsService = ftsService.TrimEnd('/'); _cookiePreferencesService = cookiePreferencesService; } diff --git a/Frontend/CO.CDP.OrganisationApp/Pages/OneLogin.cshtml.cs b/Frontend/CO.CDP.OrganisationApp/Pages/OneLogin.cshtml.cs index 91e116114..0c0b888b0 100644 --- a/Frontend/CO.CDP.OrganisationApp/Pages/OneLogin.cshtml.cs +++ b/Frontend/CO.CDP.OrganisationApp/Pages/OneLogin.cshtml.cs @@ -9,6 +9,7 @@ using Microsoft.AspNetCore.Authentication.OpenIdConnect; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.FeatureManagement; using System.Net; namespace CO.CDP.OrganisationApp.Pages; @@ -22,16 +23,18 @@ public class OneLoginModel( ILogoutManager logoutManager, IOneLoginAuthority oneLoginAuthority, IAuthorityClient authorityClient, - ILogger logger) : PageModel + ILogger logger, + IFeatureManager featureManager, + IConfiguration config) : PageModel { [BindProperty(SupportsGet = true)] public required string PageAction { get; set; } - public async Task OnGetAsync(string? redirectUri = null) + public async Task OnGetAsync(string? redirectUri = null, string? origin = null) { return PageAction.ToLower() switch { - "sign-in" => SignIn(redirectUri), + "sign-in" => await SignIn(redirectUri, origin), "user-info" => await UserInfo(redirectUri), "sign-out" => await SignOut(), _ => Redirect("/"), @@ -68,8 +71,18 @@ public async Task OnPostAsync(string logout_token) return Page(); } - private IActionResult SignIn(string? redirectUri = null) + private async Task SignIn(string? redirectUri = null, string? origin = null) { + if (!string.IsNullOrWhiteSpace(origin) + && await featureManager.IsEnabledAsync(FeatureFlags.AllowDynamicFtsOrigins)) + { + var allowedOrigins = config["FtsServiceAllowedOrigins"] ?? ""; + if (allowedOrigins.Split(",", StringSplitOptions.RemoveEmptyEntries).Contains(origin)) + { + session.Set(Session.FtsServiceOrigin, origin); + } + } + var uri = "/one-login/user-info"; if (Helper.ValidRelativeUri(redirectUri)) { @@ -84,7 +97,7 @@ private async Task UserInfo(string? redirectUri = null) var userInfo = await httpContextAccessor.HttpContext!.AuthenticateAsync(); if (!userInfo.Succeeded) { - return SignIn(); + return await SignIn(redirectUri); } var urn = userInfo.Principal.FindFirst(JwtClaimTypes.Subject)?.Value; @@ -93,7 +106,7 @@ private async Task UserInfo(string? redirectUri = null) if (urn == null) { - return SignIn(); + return await SignIn(redirectUri); } await logoutManager.RemoveAsLoggedOut(urn); diff --git a/Frontend/CO.CDP.OrganisationApp/Session.cs b/Frontend/CO.CDP.OrganisationApp/Session.cs index 578770e63..166278ca8 100644 --- a/Frontend/CO.CDP.OrganisationApp/Session.cs +++ b/Frontend/CO.CDP.OrganisationApp/Session.cs @@ -8,6 +8,7 @@ public class Session(IHttpContextAccessor httpContextAccessor) : ISession public const string RegistrationDetailsKey = "RegistrationDetails"; public const string ConnectedPersonKey = "ConnectedPerson"; public const string ConsortiumKey = "Consortium"; + public const string FtsServiceOrigin = "FtsServiceOrigin"; public T? Get(string key) { diff --git a/Frontend/CO.CDP.OrganisationApp/appsettings.AwsIntegration.json b/Frontend/CO.CDP.OrganisationApp/appsettings.AwsIntegration.json index 098242d82..f145844d8 100644 --- a/Frontend/CO.CDP.OrganisationApp/appsettings.AwsIntegration.json +++ b/Frontend/CO.CDP.OrganisationApp/appsettings.AwsIntegration.json @@ -4,6 +4,7 @@ "DiagnosticPage": { "Enabled": false, "Path": "" - } + }, + "AllowDynamicFtsOrigins": true } } diff --git a/Frontend/CO.CDP.OrganisationApp/appsettings.json b/Frontend/CO.CDP.OrganisationApp/appsettings.json index 2878d914b..0f19399bc 100644 --- a/Frontend/CO.CDP.OrganisationApp/appsettings.json +++ b/Frontend/CO.CDP.OrganisationApp/appsettings.json @@ -21,6 +21,7 @@ "DataSharingService": "", "EntityVerificationService": "", "FtsService": "", + "FtsServiceAllowedOrigins": "", "OneLogin": { "Authority": "", "ClientId": "", @@ -56,7 +57,8 @@ }, "SharedSessions": false, "Consortium": false, - "ContentSecurityPolicy": true + "ContentSecurityPolicy": true, + "AllowDynamicFtsOrigins": false }, "CompaniesHouse": { "Url": "", From a592e61dd356c2be28e4da250b3790c33ed567dd Mon Sep 17 00:00:00 2001 From: Dharm Date: Fri, 27 Dec 2024 16:13:22 +0000 Subject: [PATCH 2/3] Remove FtsServiceOrigin session if exists --- Frontend/CO.CDP.OrganisationApp/Pages/OneLogin.cshtml.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Frontend/CO.CDP.OrganisationApp/Pages/OneLogin.cshtml.cs b/Frontend/CO.CDP.OrganisationApp/Pages/OneLogin.cshtml.cs index 0c0b888b0..230d8834a 100644 --- a/Frontend/CO.CDP.OrganisationApp/Pages/OneLogin.cshtml.cs +++ b/Frontend/CO.CDP.OrganisationApp/Pages/OneLogin.cshtml.cs @@ -73,6 +73,7 @@ public async Task OnPostAsync(string logout_token) private async Task SignIn(string? redirectUri = null, string? origin = null) { + session.Remove(Session.FtsServiceOrigin); if (!string.IsNullOrWhiteSpace(origin) && await featureManager.IsEnabledAsync(FeatureFlags.AllowDynamicFtsOrigins)) { From 834f6e214848db30c00745c48a0c61435f242f56 Mon Sep 17 00:00:00 2001 From: Dharm Date: Tue, 7 Jan 2025 11:10:43 +0000 Subject: [PATCH 3/3] Updated FtsServiceAllowedOrigins list for integration environment --- .../CO.CDP.OrganisationApp/appsettings.AwsIntegration.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Frontend/CO.CDP.OrganisationApp/appsettings.AwsIntegration.json b/Frontend/CO.CDP.OrganisationApp/appsettings.AwsIntegration.json index f145844d8..5211c6a6a 100644 --- a/Frontend/CO.CDP.OrganisationApp/appsettings.AwsIntegration.json +++ b/Frontend/CO.CDP.OrganisationApp/appsettings.AwsIntegration.json @@ -6,5 +6,6 @@ "Path": "" }, "AllowDynamicFtsOrigins": true - } -} + }, + "FtsServiceAllowedOrigins": "https://www-integration.find-tender.service.gov.uk,https://www-tpp-preview.find-tender.service.gov.uk,https://www-tpp.find-tender.service.gov.uk,https://www-preview.find-tender.service.gov.uk,https://test-findtender.nqc.com,https://truk-alpha.nqc.com,https://truk-performance.nqc.com,https://truk-prod.nqc.com,https://nadeemshafi2.nqc.com,https://wallsm.nqc.com,https://humaarif.nqc.com,https://andrewtaberner.nqc.com,https://kaylemwood.nqc.com,https://davidchiu.nqc.com,https://anudeepjami.nqc.com,https://akmalnazir.nqc.com,https://martamajewska.nqc.com,https://kaichan2.nqc.com,https://stanvolcere.nqc.com" +} \ No newline at end of file