From 7ad4a16d48ecbb7f48ce956b90bce95265d9ea69 Mon Sep 17 00:00:00 2001 From: FrostyApeOne Date: Thu, 22 Oct 2026 15:08:49 +0100 Subject: [PATCH 1/2] Improve authentication and validation of the issuer --- .../Outbox/TenantOutboxDeliveryService.cs | 26 ++- .../DatabaseTenantConfigurationProvider.cs | 85 ++++++- ...atabaseTenantConfigurationProviderTests.cs | 210 ++++++++++++++++++ terraform/.terraform.lock.hcl | 107 --------- 4 files changed, 317 insertions(+), 111 deletions(-) create mode 100644 src/Tests/GovUK.Dfe.FlexForms.Application.Tests/Services/DatabaseTenantConfigurationProviderTests.cs delete mode 100644 terraform/.terraform.lock.hcl diff --git a/src/GovUK.Dfe.FlexForms.Infrastructure/Messaging/Outbox/TenantOutboxDeliveryService.cs b/src/GovUK.Dfe.FlexForms.Infrastructure/Messaging/Outbox/TenantOutboxDeliveryService.cs index fb15ae36..88bc3715 100644 --- a/src/GovUK.Dfe.FlexForms.Infrastructure/Messaging/Outbox/TenantOutboxDeliveryService.cs +++ b/src/GovUK.Dfe.FlexForms.Infrastructure/Messaging/Outbox/TenantOutboxDeliveryService.cs @@ -15,7 +15,8 @@ namespace GovUK.Dfe.FlexForms.Infrastructure.Messaging.Outbox; /// Runs one MassTransit per distinct tenant EA /// database. The built-in delivery service is disabled because it has no tenant context and would /// only ever drain the first tenant's database. Tenants sharing a database share a loop. -/// The set of loops is reconciled whenever tenant configuration is refreshed. +/// The set of loops is reconciled whenever tenant configuration changes, and again after +/// if any loop failed to start. /// public sealed class TenantOutboxDeliveryService( IServiceProvider provider, @@ -26,6 +27,8 @@ public sealed class TenantOutboxDeliveryService( { private const string ConnectionStringName = "DefaultConnection"; + internal static readonly TimeSpan StartRetryDelay = TimeSpan.FromMinutes(1); + private readonly Dictionary _running = new(StringComparer.OrdinalIgnoreCase); private readonly Channel _reconcileSignal = Channel.CreateBounded( @@ -77,6 +80,8 @@ private async Task ReconcileAsync(CancellationToken cancellationToken) foreach (var key in _running.Keys.Where(k => !desired.ContainsKey(k)).ToList()) await StopDeliveryAsync(key); + var anyStartFailed = false; + foreach (var (key, tenant) in desired) { if (_running.ContainsKey(key)) @@ -88,9 +93,26 @@ private async Task ReconcileAsync(CancellationToken cancellationToken) } catch (Exception ex) when (ex is not OperationCanceledException) { - logger.LogError(ex, "Failed to start outbox delivery for tenant {TenantName}", tenant.Name); + anyStartFailed = true; + logger.LogError(ex, "Failed to start outbox delivery for tenant {TenantName}. Retrying in {RetryDelay}", + tenant.Name, StartRetryDelay); } } + + if (anyStartFailed) + _ = SignalReconcileAfterDelayAsync(cancellationToken); + } + + private async Task SignalReconcileAfterDelayAsync(CancellationToken cancellationToken) + { + try + { + await Task.Delay(StartRetryDelay, cancellationToken); + _reconcileSignal.Writer.TryWrite(true); + } + catch (OperationCanceledException) + { + } } private async Task StartDeliveryAsync(string key, TenantConfiguration tenant, CancellationToken cancellationToken) diff --git a/src/GovUK.Dfe.FlexForms.Infrastructure/Services/DatabaseTenantConfigurationProvider.cs b/src/GovUK.Dfe.FlexForms.Infrastructure/Services/DatabaseTenantConfigurationProvider.cs index dd1e0e7d..c1459838 100644 --- a/src/GovUK.Dfe.FlexForms.Infrastructure/Services/DatabaseTenantConfigurationProvider.cs +++ b/src/GovUK.Dfe.FlexForms.Infrastructure/Services/DatabaseTenantConfigurationProvider.cs @@ -1,3 +1,5 @@ +using System.Security.Cryptography; +using System.Text; using System.Text.Json; using GovUK.Dfe.FlexForms.Domain.Tenancy; using GovUK.Dfe.FlexForms.Infrastructure.Database; @@ -13,6 +15,12 @@ namespace GovUK.Dfe.FlexForms.Infrastructure.Services; /// Loads tenant configuration from the tenant config database and caches it in memory. /// Implements IHostedService to load at startup and refresh periodically. /// Uses volatile dictionary swaps for lock-free reads. +/// +/// The snapshot is only swapped, and only fired, +/// when the loaded data differs from the current snapshot. Subscribers rebuild expensive state +/// (OIDC metadata caches, auth registries, options) on every notification, so an unconditional +/// notify on each periodic refresh would discard those caches every interval. +/// /// public class DatabaseTenantConfigurationProvider( IServiceScopeFactory scopeFactory, @@ -30,6 +38,10 @@ public class DatabaseTenantConfigurationProvider( private DateTimeOffset? _lastRefreshedUtc; + private string? _snapshotFingerprint; + + private readonly SemaphoreSlim _refreshGate = new(1, 1); + private Timer? _refreshTimer; private bool _disposed; @@ -56,6 +68,9 @@ public IReadOnlyCollection GetAllTenants() /// public async Task RefreshAsync(CancellationToken cancellationToken = default) { + // Serialised so the fingerprint comparison and snapshot swap cannot interleave between + // the timer and admin-triggered refreshes. + await _refreshGate.WaitAsync(cancellationToken); try { using var scope = scopeFactory.CreateScope(); @@ -72,8 +87,11 @@ public async Task RefreshAsync(CancellationToken cancellationToken = default) var newTenantsById = new Dictionary(); var newTenantsByOrigin = new Dictionary(StringComparer.OrdinalIgnoreCase); + using var fingerprint = IncrementalHash.CreateHash(HashAlgorithmName.SHA256); - foreach (var entity in tenantEntities) + // Deterministic order: the query has no ORDER BY, and both the fingerprint and the + // last-wins origin index must not depend on whatever order the database returns. + foreach (var entity in tenantEntities.OrderBy(t => t.Id)) { var configPairs = new List>(); @@ -122,11 +140,24 @@ public async Task RefreshAsync(CancellationToken cancellationToken = default) { newTenantsByOrigin[origin] = tenantConfig; } + + AppendTenantToFingerprint(fingerprint, entity.Id, entity.Name, configPairs, origins); + } + + var newFingerprint = Convert.ToHexString(fingerprint.GetHashAndReset()); + _lastRefreshedUtc = DateTimeOffset.UtcNow; + + if (string.Equals(newFingerprint, _snapshotFingerprint, StringComparison.Ordinal)) + { + logger.LogDebug( + "Tenant configuration unchanged ({TenantCount} active tenants for target '{Target}'); keeping current snapshot", + newTenantsById.Count, targetApplication); + return; } _tenantsById = newTenantsById; _tenantsByOrigin = newTenantsByOrigin; - _lastRefreshedUtc = DateTimeOffset.UtcNow; + _snapshotFingerprint = newFingerprint; logger.LogInformation( "Tenant configuration refreshed. Loaded {TenantCount} active tenants for target '{Target}'", @@ -142,6 +173,56 @@ public async Task RefreshAsync(CancellationToken cancellationToken = default) logger.LogError(ex, "Failed to refresh tenant configuration from database. Retaining previous configuration"); } + finally + { + _refreshGate.Release(); + } + } + + /// + /// Feeds everything that ends up in a into the snapshot + /// fingerprint. Values are length-prefixed so adjacent fields cannot run into each other, + /// and null is distinguished from empty string. + /// + private static void AppendTenantToFingerprint( + IncrementalHash hash, + Guid tenantId, + string tenantName, + List> configPairs, + string[] origins) + { + AppendFingerprintValue(hash, tenantId.ToString("N")); + AppendFingerprintValue(hash, tenantName); + + // Stable sort: duplicate keys keep their relative order, which decides last-wins. + AppendFingerprintValue(hash, configPairs.Count.ToString()); + foreach (var pair in configPairs.OrderBy(p => p.Key, StringComparer.Ordinal)) + { + AppendFingerprintValue(hash, pair.Key); + AppendFingerprintValue(hash, pair.Value); + } + + AppendFingerprintValue(hash, origins.Length.ToString()); + foreach (var origin in origins.Order(StringComparer.OrdinalIgnoreCase)) + { + AppendFingerprintValue(hash, origin); + } + } + + private static void AppendFingerprintValue(IncrementalHash hash, string? value) + { + Span length = stackalloc byte[sizeof(int)]; + if (value is null) + { + BitConverter.TryWriteBytes(length, -1); + hash.AppendData(length); + return; + } + + var bytes = Encoding.UTF8.GetBytes(value); + BitConverter.TryWriteBytes(length, bytes.Length); + hash.AppendData(length); + hash.AppendData(bytes); } /// diff --git a/src/Tests/GovUK.Dfe.FlexForms.Application.Tests/Services/DatabaseTenantConfigurationProviderTests.cs b/src/Tests/GovUK.Dfe.FlexForms.Application.Tests/Services/DatabaseTenantConfigurationProviderTests.cs new file mode 100644 index 00000000..8802f50d --- /dev/null +++ b/src/Tests/GovUK.Dfe.FlexForms.Application.Tests/Services/DatabaseTenantConfigurationProviderTests.cs @@ -0,0 +1,210 @@ +using GovUK.Dfe.FlexForms.Domain.Tenancy; +using GovUK.Dfe.FlexForms.Domain.Tenancy.Entities; +using GovUK.Dfe.FlexForms.Infrastructure.Database; +using GovUK.Dfe.FlexForms.Infrastructure.Services; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using NSubstitute; + +namespace GovUK.Dfe.FlexForms.Application.Tests.Services; + +public class DatabaseTenantConfigurationProviderTests +{ + private static readonly Guid TenantA = Guid.Parse("11111111-1111-4111-8111-111111111111"); + private static readonly Guid TenantB = Guid.Parse("22222222-2222-4222-8222-222222222222"); + + private readonly string _databaseName = Guid.NewGuid().ToString(); + private readonly ServiceProvider _services; + private readonly ITenantConfigurationChangedNotifier _notifier = Substitute.For(); + + public DatabaseTenantConfigurationProviderTests() + { + _services = new ServiceCollection() + .AddDbContext(o => o.UseInMemoryDatabase(_databaseName)) + .BuildServiceProvider(); + } + + private DatabaseTenantConfigurationProvider CreateProvider() + => new( + _services.GetRequiredService(), + Substitute.For>(), + changeNotifier: _notifier); + + private async Task WithDbAsync(Func action) + { + using var scope = _services.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + await action(db); + await db.SaveChangesAsync(); + } + + private static TenantEntity Tenant(Guid id, string name, string dfeSignInJson, params string[] origins) + => new() + { + Id = id, + Name = name, + Settings = + { + new TenantSettingEntity + { + Id = Guid.NewGuid(), + TenantId = id, + Category = "DfESignIn", + Target = "Api", + Settings = dfeSignInJson + } + }, + FrontendOrigins = origins + .Select(o => new TenantFrontendOriginEntity { Id = Guid.NewGuid(), TenantId = id, Origin = o }) + .ToList() + }; + + [Fact] + public async Task RefreshAsync_FirstLoad_SwapsSnapshotAndNotifies() + { + await WithDbAsync(db => + { + db.Tenants.Add(Tenant(TenantA, "A", """{"ClientId":"client-a"}""", "https://a.example.com")); + return Task.CompletedTask; + }); + + var provider = CreateProvider(); + + await provider.RefreshAsync(); + + _notifier.Received(1).Notify(); + Assert.Equal("client-a", provider.GetTenant(TenantA)!.Settings["DfESignIn:ClientId"]); + Assert.Same(provider.GetTenant(TenantA), provider.GetTenantByOrigin("https://a.example.com")); + } + + [Fact] + public async Task RefreshAsync_Unchanged_DoesNotNotifyOrReplaceSnapshot() + { + await WithDbAsync(db => + { + db.Tenants.Add(Tenant(TenantA, "A", """{"ClientId":"client-a"}""", "https://a.example.com")); + db.Tenants.Add(Tenant(TenantB, "B", """{"ClientId":"client-b"}""")); + return Task.CompletedTask; + }); + + var provider = CreateProvider(); + await provider.RefreshAsync(); + var firstSnapshot = provider.GetTenant(TenantA); + var firstRefreshedUtc = provider.LastRefreshedUtc; + _notifier.ClearReceivedCalls(); + + await provider.RefreshAsync(); + await provider.RefreshAsync(); + + _notifier.DidNotReceive().Notify(); + Assert.Same(firstSnapshot, provider.GetTenant(TenantA)); + Assert.True(provider.LastRefreshedUtc >= firstRefreshedUtc); + } + + [Fact] + public async Task RefreshAsync_SettingValueChanged_NotifiesWithNewValue() + { + await WithDbAsync(db => + { + db.Tenants.Add(Tenant(TenantA, "A", """{"ClientId":"client-a"}""")); + return Task.CompletedTask; + }); + + var provider = CreateProvider(); + await provider.RefreshAsync(); + _notifier.ClearReceivedCalls(); + + await WithDbAsync(async db => + { + var setting = await db.TenantSettings.SingleAsync(s => s.TenantId == TenantA); + setting.Settings = """{"ClientId":"client-a-rotated"}"""; + }); + + await provider.RefreshAsync(); + + _notifier.Received(1).Notify(); + Assert.Equal("client-a-rotated", provider.GetTenant(TenantA)!.Settings["DfESignIn:ClientId"]); + } + + [Fact] + public async Task RefreshAsync_TenantDeactivated_Notifies() + { + await WithDbAsync(db => + { + db.Tenants.Add(Tenant(TenantA, "A", """{"ClientId":"client-a"}""")); + db.Tenants.Add(Tenant(TenantB, "B", """{"ClientId":"client-b"}""")); + return Task.CompletedTask; + }); + + var provider = CreateProvider(); + await provider.RefreshAsync(); + _notifier.ClearReceivedCalls(); + + await WithDbAsync(async db => + { + var tenant = await db.Tenants.SingleAsync(t => t.Id == TenantB); + tenant.IsActive = false; + }); + + await provider.RefreshAsync(); + + _notifier.Received(1).Notify(); + Assert.Null(provider.GetTenant(TenantB)); + Assert.Equal(1, provider.ActiveTenantCount); + } + + [Fact] + public async Task RefreshAsync_FrontendOriginAdded_Notifies() + { + await WithDbAsync(db => + { + db.Tenants.Add(Tenant(TenantA, "A", """{"ClientId":"client-a"}""", "https://a.example.com")); + return Task.CompletedTask; + }); + + var provider = CreateProvider(); + await provider.RefreshAsync(); + _notifier.ClearReceivedCalls(); + + await WithDbAsync(db => + { + db.TenantFrontendOrigins.Add(new TenantFrontendOriginEntity + { + Id = Guid.NewGuid(), + TenantId = TenantA, + Origin = "https://a2.example.com" + }); + return Task.CompletedTask; + }); + + await provider.RefreshAsync(); + + _notifier.Received(1).Notify(); + Assert.NotNull(provider.GetTenantByOrigin("https://a2.example.com")); + } + + [Fact] + public async Task RefreshAsync_ValueMovedBetweenAdjacentFields_IsDetectedAsChange() + { + await WithDbAsync(db => + { + db.Tenants.Add(Tenant(TenantA, "A", """{"Ab":"c"}""")); + return Task.CompletedTask; + }); + + var provider = CreateProvider(); + await provider.RefreshAsync(); + _notifier.ClearReceivedCalls(); + + await WithDbAsync(async db => + { + var setting = await db.TenantSettings.SingleAsync(s => s.TenantId == TenantA); + setting.Settings = """{"A":"bc"}"""; + }); + + await provider.RefreshAsync(); + + _notifier.Received(1).Notify(); + } +} diff --git a/terraform/.terraform.lock.hcl b/terraform/.terraform.lock.hcl deleted file mode 100644 index 3e5db973..00000000 --- a/terraform/.terraform.lock.hcl +++ /dev/null @@ -1,107 +0,0 @@ -# This file is maintained automatically by "terraform init". -# Manual edits may be lost in future updates. - -provider "registry.terraform.io/azure/azapi" { - version = "1.15.0" - constraints = "~> 1.13" - hashes = [ - "h1:pO/phGY+TxMEKQ+ffYj+vUIvG5A1tno/sZYDb/yyA/w=", - "zh:0627a8bc77254debc25dc0c7b62e055138217c97b03221e593c3c56dc7550671", - "zh:2fe045f07070ef75d0bec4b0595a74c14394daa838ddb964e2fd23cc98c40c34", - "zh:343009f39c957883b2c06145a5954e524c70f93585f943f1ea3d28ef6995d0d0", - "zh:53fe9ab54485aaebc9b91e27a10bce2729a1c95b1399079e631dc6bb9e3f27dc", - "zh:63c407e7dc04d178d4798c17ad489d9cc92f7d1941d7f4a3f560b95908b6107b", - "zh:7d6fc2b432b264f036bb80ab2b2ba67f80a5d98da8a8c322aa097833dad598c9", - "zh:7ec49c0a8799d469eb6e2a1f856693f9862f1b73f5ed70adc1b346e5a4c6458d", - "zh:889704f10319d301d677539d788fc82a7c73608ab78cb93e1280ac2be39e6e00", - "zh:90b4b07405b7cde9ebae3b034cb5bb5dd18484d1b95bd250f905451f1e86ac3f", - "zh:92aa9c241a8cb2a6d81ad47bc007c119f8b818464a960ebaf39008766c361e6b", - "zh:f28fbd0a2c59e239b53067bc1adc691be444876bcb2d4f78d310f549724da6e0", - "zh:ffb15e0ddfa505d0e9b75341570199076ae574887124f398162b1ead9376b25f", - ] -} - -provider "registry.terraform.io/hashicorp/archive" { - version = "2.8.0" - constraints = "~> 2.6" - hashes = [ - "h1:WB6H5ksIZiyq1lQlD/PWeh+tn4FLsbSjVnRW3+4xe2Y=", - "zh:0d14713fdc259fb377d0b899ad3c650a34194bd52194c863303ef22a65a580e2", - "zh:369b56040c7a8085d04e7e8ffac1e2b321a3170e502f788819bc34b868ec016f", - "zh:4d1a3b983ed6af5a52bfe12794674ae55cbadfa6021b37106ade68b433ad216a", - "zh:5c547549e26e083573c78a966ca68ce6d7df6bb8f3948f66a575f07da46b74ea", - "zh:6de093e62a975eb19a5e3017ce38e6e3cb639c17b79648d2000e0a8348f0e997", - "zh:7267936c2cdbc448efeb594d73e6b56a53d6a7ae14fe88cdd2a4133adc3302f0", - "zh:7482f023050ed426b4b45116e1761643bc33b1fd4ce4a6fab207ae2571f35940", - "zh:76bbd93b234e5a2927d98b511d86565700f549b570871a194c35f944b96cefb7", - "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", - "zh:c6afc4bc1f002bac9c173007dd4da05fde788cd14c2916089f958c33fedb0dfa", - "zh:d3ba40bd806a3a08e9237dece679193c99afb2085de6b45d7f5d1f673cfcd368", - "zh:e1ad7ded53ecd6f0e5b473a3b44eae2b2e885653a56050ab583d387332be02e4", - "zh:e93e78575ce82be6084cc153c24ba8f385dc8d6880888ee66e918460c870953d", - ] -} - -provider "registry.terraform.io/hashicorp/azurerm" { - version = "4.81.0" - constraints = "~> 4.0, ~> 4.37" - hashes = [ - "h1:XhToZua4gtih1Kv8RdStcfND83G4Tmb6GZFT4jEUhDU=", - "zh:0732e7b74264ddfa2b90ba69d01c283d3cbae9f72ed3e506c6ac92529fed7fd3", - "zh:12afb524e232fe4e3d6161927724af5dfa4831d71edd9c174917ca9b7377bfae", - "zh:169d619ae202c4145e02fb706fb7c3679445ab3e3ff722edbf89597517a8c92e", - "zh:6beb95a3ef2f2d9c76abaa48e5450e90686a3fb6a47f1cb0ff7c5e94b6960151", - "zh:705e075fb5ffc4bf66fd7cbabf1a65007a41621e80030a2c158a4c83b6046216", - "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", - "zh:79a8d17fefe647040fcb9ee8821a4f09f395427c4fd49493489b9a93a9a1038e", - "zh:8cc3f900b3774c0ae37ae42365c4579a199cf9e5edf88e476fdf5ab1048f84ea", - "zh:dec373b9390fa95e257291acd018ed65a7d512b428645d35e22cdbe8b245a08b", - "zh:e60f1e9fb45df6defade2855ed6e68547409ea75d30655c556adb0c08579749b", - "zh:f901d12ec82f3f8b5880a27b5cbcd7bd0d97e60c9367a2d7ed82fdd1157b39ff", - "zh:facf68ea5bf0f2b8ba720e7fba5f86492e1d4c591100460bb91c3f79f391f4b6", - ] -} - -provider "registry.terraform.io/hashicorp/null" { - version = "3.3.1" - constraints = "~> 3.2" - hashes = [ - "h1:m5FqidbIgh+E9OigiZh8/xbkvpUQFSj3hZo/jqNLCLQ=", - "zh:08c59776542ea16e5a8545752787b17ff412922182b4cfabe16139197be8ac44", - "zh:123109cc7e5ed6d515787fbc212f2a3fd5e75647bb24ab7c801ccd4d4ed42451", - "zh:14b3fa4372754b54844b41d5dbd4671a292d8d6828b90169061feb4d7b15dd05", - "zh:56a4daaa3212f57b764bf3d1f333141c6610c5f21abb240e0111221f7c7fa4d4", - "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", - "zh:7e888a026dbacd2474a42264227ae35f639780f0f0c613529d10a95cd61988b3", - "zh:85a53646267e87d600df7124e4767ffde9bba3b6356d45d961618bdd68131cc7", - "zh:8ffa0e9c7c39b2ab0905b472465d6e35ef0b776b3f6273bb34c150340b61bff1", - "zh:9846510a1841530d4403f4818e233f91e3b3bade7441047599fbf800742f65be", - "zh:afa98d44860875f037c6def0a7e6ff208e042712ba771f620482b143cd336891", - "zh:bdca130d9ef27488ae0b13bc8fd8019e8bbdd4f2ceff29da066bd333165d68c5", - "zh:cb3b94cbca88210dd0d1f11e2b8a89333f48c3857faf8f70f589072ce7c28610", - "zh:f0c0ba87925fe32f84b80f7513b1efb1b0866f51f899ba825e95ad59ff09b018", - ] -} - -provider "registry.terraform.io/statuscakedev/statuscake" { - version = "2.2.2" - constraints = "~> 2.1" - hashes = [ - "h1:nVaJkDBk4sv0yWFzg3p+yeJGzE8mB4KJv3Q6/UgU164=", - "zh:0916313344c579d6e05d70f88129a10fe48f7dabe0e61cad17874d6c496f288d", - "zh:0d491ff72c2eda6482855033ca2146c5ace1663d07cb3da7253b59ed2e2ec6f4", - "zh:11fffbce18eb3d3c283e877242f477e0c561342c19090240b60af7d948bd84ac", - "zh:1c3e89cf19118fc07d7b04257251fc9897e722c16e0a0df7b07fcd261f8c12e7", - "zh:1c6116092c59bc0010e147dc7832ae981d528f235cef563e5ae05a93ef8bac5c", - "zh:1f13a543b1d32cc1f1e3d2ed5ca83445f088787c335690fe20dee1203488a8bc", - "zh:23f55fd0714696c3863f892646a79a780cea1923c0c4d2b1064df735ba4156f5", - "zh:288fbb4431ac12014aa5aab10ee7166dfe71fd1158464e06e8a527aa4919e64e", - "zh:2a0746d3c8cdc7d0875df0f4605d81130e6da0ce6196d0f5f8661fe9191bc2ab", - "zh:31d660756abc53d252906fe1239fad58ecb6c1d0dbb087408a8af266be1f9ae4", - "zh:6765281d1b7efb41085c5375660b6c6b271babc5d09b2c030bec7176f7a7bfb6", - "zh:6d7204eadb667c1f2cab762a7a97234cf47452f0ecad680f5106c8fd02524c87", - "zh:b70a97b0eba471d683e23ce8744a5c67ef9952086e2d6f5825b72f32b6caff89", - "zh:df89ee4aaba88faea33d33384a36ea52588b5514644d335f724378a682d8d9da", - "zh:ffe2e1e7224ea5f4dd65b9eef45464e3124cfc824473b04e7af429f9177f2375", - ] -} From 86afca60975640f6cabe5e89246c9ef6675db7fc Mon Sep 17 00:00:00 2001 From: FrostyApeOne Date: Thu, 22 Oct 2026 15:40:40 +0100 Subject: [PATCH 2/2] Updated corelibs security package reference --- .../GovUK.Dfe.FlexForms.Api.Client.csproj | 2 +- .../GovUK.Dfe.FlexForms.Application.csproj | 2 +- .../GovUK.Dfe.FlexForms.Infrastructure.csproj | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/GovUK.Dfe.FlexForms.Api.Client/GovUK.Dfe.FlexForms.Api.Client.csproj b/src/GovUK.Dfe.FlexForms.Api.Client/GovUK.Dfe.FlexForms.Api.Client.csproj index 6de4d6be..bc581cf6 100644 --- a/src/GovUK.Dfe.FlexForms.Api.Client/GovUK.Dfe.FlexForms.Api.Client.csproj +++ b/src/GovUK.Dfe.FlexForms.Api.Client/GovUK.Dfe.FlexForms.Api.Client.csproj @@ -24,7 +24,7 @@ - + diff --git a/src/GovUK.Dfe.FlexForms.Application/GovUK.Dfe.FlexForms.Application.csproj b/src/GovUK.Dfe.FlexForms.Application/GovUK.Dfe.FlexForms.Application.csproj index 62317db2..eddccb57 100644 --- a/src/GovUK.Dfe.FlexForms.Application/GovUK.Dfe.FlexForms.Application.csproj +++ b/src/GovUK.Dfe.FlexForms.Application/GovUK.Dfe.FlexForms.Application.csproj @@ -21,7 +21,7 @@ - + diff --git a/src/GovUK.Dfe.FlexForms.Infrastructure/GovUK.Dfe.FlexForms.Infrastructure.csproj b/src/GovUK.Dfe.FlexForms.Infrastructure/GovUK.Dfe.FlexForms.Infrastructure.csproj index 0106c442..bd6d90dc 100644 --- a/src/GovUK.Dfe.FlexForms.Infrastructure/GovUK.Dfe.FlexForms.Infrastructure.csproj +++ b/src/GovUK.Dfe.FlexForms.Infrastructure/GovUK.Dfe.FlexForms.Infrastructure.csproj @@ -11,7 +11,7 @@ - +