From a6f21181eb879095f14519db5b2221dfcfe0548b Mon Sep 17 00:00:00 2001 From: Thomas Avery Date: Tue, 15 Oct 2024 17:01:42 -0500 Subject: [PATCH 01/17] Add stored procedure --- .../UserAsymmetricKeys_Regenerate.sql | 17 +++++++++++++++++ ...10-15_01_AddUserAsymmetricKeysRegenerate.sql | 13 +++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 src/Sql/KeyManagement/dbo/Stored Procedures/UserAsymmetricKeys_Regenerate.sql create mode 100644 util/Migrator/DbScripts/2024-10-15_01_AddUserAsymmetricKeysRegenerate.sql diff --git a/src/Sql/KeyManagement/dbo/Stored Procedures/UserAsymmetricKeys_Regenerate.sql b/src/Sql/KeyManagement/dbo/Stored Procedures/UserAsymmetricKeys_Regenerate.sql new file mode 100644 index 000000000000..17273fc33e84 --- /dev/null +++ b/src/Sql/KeyManagement/dbo/Stored Procedures/UserAsymmetricKeys_Regenerate.sql @@ -0,0 +1,17 @@ +CREATE PROCEDURE [dbo].[UserAsymmetricKeys_Regenerate] + @UserId UNIQUEIDENTIFIER OUTPUT, + @PublicKey VARCHAR(MAX), + @PrivateKey VARCHAR(MAX) +AS +BEGIN + SET NOCOUNT ON + + BEGIN TRANSACTION UserAsymmetricKeys_Regenerate + + UPDATE [dbo].[User] + SET [PublicKey] = @PublicKey, + [PrivateKey] = @PrivateKey + WHERE [Id] = @UserId + + COMMIT TRANSACTION UserAsymmetricKeys_Regenerate +END diff --git a/util/Migrator/DbScripts/2024-10-15_01_AddUserAsymmetricKeysRegenerate.sql b/util/Migrator/DbScripts/2024-10-15_01_AddUserAsymmetricKeysRegenerate.sql new file mode 100644 index 000000000000..34eb1df975f5 --- /dev/null +++ b/util/Migrator/DbScripts/2024-10-15_01_AddUserAsymmetricKeysRegenerate.sql @@ -0,0 +1,13 @@ +CREATE OR ALTER PROCEDURE [dbo].[UserAsymmetricKeys_Regenerate] + @UserId UNIQUEIDENTIFIER OUTPUT, + @PublicKey VARCHAR(MAX), + @PrivateKey VARCHAR(MAX) +AS +BEGIN + SET NOCOUNT ON + + UPDATE [dbo].[User] + SET [PublicKey] = @PublicKey, + [PrivateKey] = @PrivateKey + WHERE [Id] = @UserId +END From 4f49c0b3fcaef8a588467ec2ed9524e57aa90f7a Mon Sep 17 00:00:00 2001 From: Thomas Avery Date: Tue, 15 Oct 2024 17:02:06 -0500 Subject: [PATCH 02/17] Add repository --- .../Models/Data/UserAsymmetricKeys.cs | 9 ++++++ .../IUserAsymmetricKeysRepository.cs | 9 ++++++ .../DapperServiceCollectionExtensions.cs | 3 ++ .../UserAsymmetricKeysRepository.cs | 31 +++++++++++++++++++ ...ityFrameworkServiceCollectionExtensions.cs | 3 ++ .../UserAsymmetricKeysRepository.cs | 31 +++++++++++++++++++ 6 files changed, 86 insertions(+) create mode 100644 src/Core/KeyManagement/Models/Data/UserAsymmetricKeys.cs create mode 100644 src/Core/KeyManagement/Repositories/IUserAsymmetricKeysRepository.cs create mode 100644 src/Infrastructure.Dapper/KeyManagement/Repositories/UserAsymmetricKeysRepository.cs create mode 100644 src/Infrastructure.EntityFramework/KeyManagement/Repositories/UserAsymmetricKeysRepository.cs diff --git a/src/Core/KeyManagement/Models/Data/UserAsymmetricKeys.cs b/src/Core/KeyManagement/Models/Data/UserAsymmetricKeys.cs new file mode 100644 index 000000000000..3c1362909260 --- /dev/null +++ b/src/Core/KeyManagement/Models/Data/UserAsymmetricKeys.cs @@ -0,0 +1,9 @@ +#nullable enable +namespace Bit.Core.KeyManagement.Models.Data; + +public class UserAsymmetricKeys +{ + public Guid UserId { get; set; } + public required string PublicKey { get; set; } + public required string UserKeyEncryptedPrivateKey { get; set; } +} diff --git a/src/Core/KeyManagement/Repositories/IUserAsymmetricKeysRepository.cs b/src/Core/KeyManagement/Repositories/IUserAsymmetricKeysRepository.cs new file mode 100644 index 000000000000..fee9aee3bbb3 --- /dev/null +++ b/src/Core/KeyManagement/Repositories/IUserAsymmetricKeysRepository.cs @@ -0,0 +1,9 @@ +#nullable enable +using Bit.Core.KeyManagement.Models.Data; + +namespace Bit.Core.KeyManagement.Repositories; + +public interface IUserAsymmetricKeysRepository +{ + Task RegenerateUserAsymmetricKeysAsync(UserAsymmetricKeys userAsymmetricKeys); +} diff --git a/src/Infrastructure.Dapper/DapperServiceCollectionExtensions.cs b/src/Infrastructure.Dapper/DapperServiceCollectionExtensions.cs index 6cfa1ef8b34a..48e730e98de4 100644 --- a/src/Infrastructure.Dapper/DapperServiceCollectionExtensions.cs +++ b/src/Infrastructure.Dapper/DapperServiceCollectionExtensions.cs @@ -1,6 +1,7 @@ using Bit.Core.AdminConsole.Repositories; using Bit.Core.Auth.Repositories; using Bit.Core.Billing.Repositories; +using Bit.Core.KeyManagement.Repositories; using Bit.Core.NotificationCenter.Repositories; using Bit.Core.Repositories; using Bit.Core.SecretsManager.Repositories; @@ -9,6 +10,7 @@ using Bit.Infrastructure.Dapper.AdminConsole.Repositories; using Bit.Infrastructure.Dapper.Auth.Repositories; using Bit.Infrastructure.Dapper.Billing.Repositories; +using Bit.Infrastructure.Dapper.KeyManagement.Repositories; using Bit.Infrastructure.Dapper.NotificationCenter.Repositories; using Bit.Infrastructure.Dapper.Repositories; using Bit.Infrastructure.Dapper.SecretsManager.Repositories; @@ -58,6 +60,7 @@ public static void AddDapperRepositories(this IServiceCollection services, bool services.AddSingleton(); services .AddSingleton(); + services.AddSingleton(); if (selfHosted) { diff --git a/src/Infrastructure.Dapper/KeyManagement/Repositories/UserAsymmetricKeysRepository.cs b/src/Infrastructure.Dapper/KeyManagement/Repositories/UserAsymmetricKeysRepository.cs new file mode 100644 index 000000000000..b232b0cf8cc2 --- /dev/null +++ b/src/Infrastructure.Dapper/KeyManagement/Repositories/UserAsymmetricKeysRepository.cs @@ -0,0 +1,31 @@ +#nullable enable +using System.Data; +using Bit.Core.KeyManagement.Models.Data; +using Bit.Core.KeyManagement.Repositories; +using Bit.Core.Settings; +using Bit.Infrastructure.Dapper.Repositories; +using Dapper; +using Microsoft.Data.SqlClient; + +namespace Bit.Infrastructure.Dapper.KeyManagement.Repositories; + +public class UserAsymmetricKeysRepository : BaseRepository, IUserAsymmetricKeysRepository +{ + public UserAsymmetricKeysRepository(GlobalSettings globalSettings) + : this(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString) + { + } + + public UserAsymmetricKeysRepository(string connectionString, string readOnlyConnectionString) : base( + connectionString, readOnlyConnectionString) + { + } + + public async Task RegenerateUserAsymmetricKeysAsync(UserAsymmetricKeys userAsymmetricKeys) + { + await using var connection = new SqlConnection(ConnectionString); + + await connection.ExecuteAsync("[dbo].[UserAsymmetricKeys_Regenerate]", + userAsymmetricKeys, commandType: CommandType.StoredProcedure); + } +} diff --git a/src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs b/src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs index ad0b46277b78..6b6c515e0fe3 100644 --- a/src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs +++ b/src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs @@ -2,6 +2,7 @@ using Bit.Core.Auth.Repositories; using Bit.Core.Billing.Repositories; using Bit.Core.Enums; +using Bit.Core.KeyManagement.Repositories; using Bit.Core.NotificationCenter.Repositories; using Bit.Core.Repositories; using Bit.Core.SecretsManager.Repositories; @@ -10,6 +11,7 @@ using Bit.Infrastructure.EntityFramework.AdminConsole.Repositories; using Bit.Infrastructure.EntityFramework.Auth.Repositories; using Bit.Infrastructure.EntityFramework.Billing.Repositories; +using Bit.Infrastructure.EntityFramework.KeyManagement.Repositories; using Bit.Infrastructure.EntityFramework.NotificationCenter.Repositories; using Bit.Infrastructure.EntityFramework.Repositories; using Bit.Infrastructure.EntityFramework.SecretsManager.Repositories; @@ -95,6 +97,7 @@ public static void AddPasswordManagerEFRepositories(this IServiceCollection serv services.AddSingleton(); services .AddSingleton(); + services.AddSingleton(); if (selfHosted) { diff --git a/src/Infrastructure.EntityFramework/KeyManagement/Repositories/UserAsymmetricKeysRepository.cs b/src/Infrastructure.EntityFramework/KeyManagement/Repositories/UserAsymmetricKeysRepository.cs new file mode 100644 index 000000000000..a2bbe6bbc729 --- /dev/null +++ b/src/Infrastructure.EntityFramework/KeyManagement/Repositories/UserAsymmetricKeysRepository.cs @@ -0,0 +1,31 @@ +#nullable enable +using AutoMapper; +using Bit.Core.KeyManagement.Models.Data; +using Bit.Core.KeyManagement.Repositories; +using Bit.Infrastructure.EntityFramework.Repositories; +using Microsoft.Extensions.DependencyInjection; + +namespace Bit.Infrastructure.EntityFramework.KeyManagement.Repositories; + +public class UserAsymmetricKeysRepository : BaseEntityFrameworkRepository, IUserAsymmetricKeysRepository +{ + public UserAsymmetricKeysRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper) : base( + serviceScopeFactory, + mapper) + { + } + + public async Task RegenerateUserAsymmetricKeysAsync(UserAsymmetricKeys userAsymmetricKeys) + { + await using var scope = ServiceScopeFactory.CreateAsyncScope(); + var dbContext = GetDatabaseContext(scope); + + var entity = await dbContext.Users.FindAsync(userAsymmetricKeys.UserId); + if (entity != null) + { + entity.PublicKey = userAsymmetricKeys.PublicKey; + entity.PrivateKey = userAsymmetricKeys.UserKeyEncryptedPrivateKey; + await dbContext.SaveChangesAsync(); + } + } +} From 2cb60452e8a6de4c46321abf32cdac3172baaa0c Mon Sep 17 00:00:00 2001 From: Thomas Avery Date: Tue, 15 Oct 2024 17:12:37 -0500 Subject: [PATCH 03/17] Update SQL stored procedure --- .../dbo/Stored Procedures/UserAsymmetricKeys_Regenerate.sql | 6 +----- .../2024-10-15_01_AddUserAsymmetricKeysRegenerate.sql | 2 +- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/Sql/KeyManagement/dbo/Stored Procedures/UserAsymmetricKeys_Regenerate.sql b/src/Sql/KeyManagement/dbo/Stored Procedures/UserAsymmetricKeys_Regenerate.sql index 17273fc33e84..31508d983245 100644 --- a/src/Sql/KeyManagement/dbo/Stored Procedures/UserAsymmetricKeys_Regenerate.sql +++ b/src/Sql/KeyManagement/dbo/Stored Procedures/UserAsymmetricKeys_Regenerate.sql @@ -1,17 +1,13 @@ CREATE PROCEDURE [dbo].[UserAsymmetricKeys_Regenerate] - @UserId UNIQUEIDENTIFIER OUTPUT, + @UserId UNIQUEIDENTIFIER, @PublicKey VARCHAR(MAX), @PrivateKey VARCHAR(MAX) AS BEGIN SET NOCOUNT ON - BEGIN TRANSACTION UserAsymmetricKeys_Regenerate - UPDATE [dbo].[User] SET [PublicKey] = @PublicKey, [PrivateKey] = @PrivateKey WHERE [Id] = @UserId - - COMMIT TRANSACTION UserAsymmetricKeys_Regenerate END diff --git a/util/Migrator/DbScripts/2024-10-15_01_AddUserAsymmetricKeysRegenerate.sql b/util/Migrator/DbScripts/2024-10-15_01_AddUserAsymmetricKeysRegenerate.sql index 34eb1df975f5..926af154338e 100644 --- a/util/Migrator/DbScripts/2024-10-15_01_AddUserAsymmetricKeysRegenerate.sql +++ b/util/Migrator/DbScripts/2024-10-15_01_AddUserAsymmetricKeysRegenerate.sql @@ -1,5 +1,5 @@ CREATE OR ALTER PROCEDURE [dbo].[UserAsymmetricKeys_Regenerate] - @UserId UNIQUEIDENTIFIER OUTPUT, + @UserId UNIQUEIDENTIFIER, @PublicKey VARCHAR(MAX), @PrivateKey VARCHAR(MAX) AS From 8a1c761d8ba865c408d98bb4b3f47728d00a9004 Mon Sep 17 00:00:00 2001 From: Thomas Avery Date: Wed, 16 Oct 2024 14:29:12 -0500 Subject: [PATCH 04/17] Bump revision dates --- .../Repositories/UserAsymmetricKeysRepository.cs | 3 +++ .../dbo/Stored Procedures/UserAsymmetricKeys_Regenerate.sql | 5 ++++- .../2024-10-15_01_AddUserAsymmetricKeysRegenerate.sql | 5 ++++- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/Infrastructure.EntityFramework/KeyManagement/Repositories/UserAsymmetricKeysRepository.cs b/src/Infrastructure.EntityFramework/KeyManagement/Repositories/UserAsymmetricKeysRepository.cs index a2bbe6bbc729..c680424f56a2 100644 --- a/src/Infrastructure.EntityFramework/KeyManagement/Repositories/UserAsymmetricKeysRepository.cs +++ b/src/Infrastructure.EntityFramework/KeyManagement/Repositories/UserAsymmetricKeysRepository.cs @@ -23,8 +23,11 @@ public async Task RegenerateUserAsymmetricKeysAsync(UserAsymmetricKeys userAsymm var entity = await dbContext.Users.FindAsync(userAsymmetricKeys.UserId); if (entity != null) { + var utcNow = DateTime.UtcNow; entity.PublicKey = userAsymmetricKeys.PublicKey; entity.PrivateKey = userAsymmetricKeys.UserKeyEncryptedPrivateKey; + entity.RevisionDate = utcNow; + entity.AccountRevisionDate = utcNow; await dbContext.SaveChangesAsync(); } } diff --git a/src/Sql/KeyManagement/dbo/Stored Procedures/UserAsymmetricKeys_Regenerate.sql b/src/Sql/KeyManagement/dbo/Stored Procedures/UserAsymmetricKeys_Regenerate.sql index 31508d983245..26d0c40183bb 100644 --- a/src/Sql/KeyManagement/dbo/Stored Procedures/UserAsymmetricKeys_Regenerate.sql +++ b/src/Sql/KeyManagement/dbo/Stored Procedures/UserAsymmetricKeys_Regenerate.sql @@ -5,9 +5,12 @@ CREATE PROCEDURE [dbo].[UserAsymmetricKeys_Regenerate] AS BEGIN SET NOCOUNT ON + DECLARE @UtcNow DATETIME2(7) = GETUTCDATE(); UPDATE [dbo].[User] SET [PublicKey] = @PublicKey, - [PrivateKey] = @PrivateKey + [PrivateKey] = @PrivateKey, + [RevisionDate] = @UtcNow, + [AccountRevisionDate] = @UtcNow WHERE [Id] = @UserId END diff --git a/util/Migrator/DbScripts/2024-10-15_01_AddUserAsymmetricKeysRegenerate.sql b/util/Migrator/DbScripts/2024-10-15_01_AddUserAsymmetricKeysRegenerate.sql index 926af154338e..e1f5431145eb 100644 --- a/util/Migrator/DbScripts/2024-10-15_01_AddUserAsymmetricKeysRegenerate.sql +++ b/util/Migrator/DbScripts/2024-10-15_01_AddUserAsymmetricKeysRegenerate.sql @@ -5,9 +5,12 @@ CREATE OR ALTER PROCEDURE [dbo].[UserAsymmetricKeys_Regenerate] AS BEGIN SET NOCOUNT ON + DECLARE @UtcNow DATETIME2(7) = GETUTCDATE(); UPDATE [dbo].[User] SET [PublicKey] = @PublicKey, - [PrivateKey] = @PrivateKey + [PrivateKey] = @PrivateKey, + [RevisionDate] = @UtcNow, + [AccountRevisionDate] = @UtcNow WHERE [Id] = @UserId END From 9278677d3065435027ea97c22ba81266fd33e99a Mon Sep 17 00:00:00 2001 From: Thomas Avery Date: Wed, 16 Oct 2024 14:29:27 -0500 Subject: [PATCH 05/17] fix dapper call --- .../Repositories/UserAsymmetricKeysRepository.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Infrastructure.Dapper/KeyManagement/Repositories/UserAsymmetricKeysRepository.cs b/src/Infrastructure.Dapper/KeyManagement/Repositories/UserAsymmetricKeysRepository.cs index b232b0cf8cc2..f176327f4ff8 100644 --- a/src/Infrastructure.Dapper/KeyManagement/Repositories/UserAsymmetricKeysRepository.cs +++ b/src/Infrastructure.Dapper/KeyManagement/Repositories/UserAsymmetricKeysRepository.cs @@ -26,6 +26,11 @@ public async Task RegenerateUserAsymmetricKeysAsync(UserAsymmetricKeys userAsymm await using var connection = new SqlConnection(ConnectionString); await connection.ExecuteAsync("[dbo].[UserAsymmetricKeys_Regenerate]", - userAsymmetricKeys, commandType: CommandType.StoredProcedure); + new + { + userAsymmetricKeys.UserId, + userAsymmetricKeys.PublicKey, + PrivateKey = userAsymmetricKeys.UserKeyEncryptedPrivateKey + }, commandType: CommandType.StoredProcedure); } } From 34bb84c14b5f08a1872063cb55bdf5d23d88c03c Mon Sep 17 00:00:00 2001 From: Thomas Avery Date: Tue, 22 Oct 2024 17:29:39 -0500 Subject: [PATCH 06/17] Add new RegenerateUserAsymmetricKeysCommand --- .../IRegenerateUserAsymmetricKeysCommand.cs | 13 ++++ .../RegenerateUserAsymmetricKeysCommand.cs | 61 +++++++++++++++++++ ...eyManagementServiceCollectionExtensions.cs | 18 ++++++ .../Utilities/ServiceCollectionExtensions.cs | 2 + 4 files changed, 94 insertions(+) create mode 100644 src/Core/KeyManagement/Commands/Interfaces/IRegenerateUserAsymmetricKeysCommand.cs create mode 100644 src/Core/KeyManagement/Commands/RegenerateUserAsymmetricKeysCommand.cs create mode 100644 src/Core/KeyManagement/KeyManagementServiceCollectionExtensions.cs diff --git a/src/Core/KeyManagement/Commands/Interfaces/IRegenerateUserAsymmetricKeysCommand.cs b/src/Core/KeyManagement/Commands/Interfaces/IRegenerateUserAsymmetricKeysCommand.cs new file mode 100644 index 000000000000..d7ad7e3959eb --- /dev/null +++ b/src/Core/KeyManagement/Commands/Interfaces/IRegenerateUserAsymmetricKeysCommand.cs @@ -0,0 +1,13 @@ +#nullable enable +using Bit.Core.Auth.Models.Data; +using Bit.Core.Entities; +using Bit.Core.KeyManagement.Models.Data; + +namespace Bit.Core.KeyManagement.Commands.Interfaces; + +public interface IRegenerateUserAsymmetricKeysCommand +{ + Task RegenerateKeysAsync(UserAsymmetricKeys userAsymmetricKeys, + ICollection usersOrganizationAccounts, + ICollection designatedEmergencyAccess); +} diff --git a/src/Core/KeyManagement/Commands/RegenerateUserAsymmetricKeysCommand.cs b/src/Core/KeyManagement/Commands/RegenerateUserAsymmetricKeysCommand.cs new file mode 100644 index 000000000000..4a76198d6efd --- /dev/null +++ b/src/Core/KeyManagement/Commands/RegenerateUserAsymmetricKeysCommand.cs @@ -0,0 +1,61 @@ +#nullable enable +using Bit.Core.Auth.Enums; +using Bit.Core.Auth.Models.Data; +using Bit.Core.Context; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.KeyManagement.Commands.Interfaces; +using Bit.Core.KeyManagement.Models.Data; +using Bit.Core.KeyManagement.Repositories; +using Microsoft.Extensions.Logging; + +namespace Bit.Core.KeyManagement.Commands; + +public class RegenerateUserAsymmetricKeysCommand : IRegenerateUserAsymmetricKeysCommand +{ + private readonly ICurrentContext _currentContext; + private readonly ILogger _logger; + private readonly IUserAsymmetricKeysRepository _userAsymmetricKeysRepository; + + public RegenerateUserAsymmetricKeysCommand( + ICurrentContext currentContext, + IUserAsymmetricKeysRepository userAsymmetricKeysRepository, + ILogger logger) + { + _currentContext = currentContext; + _logger = logger; + _userAsymmetricKeysRepository = userAsymmetricKeysRepository; + } + + public async Task RegenerateKeysAsync(UserAsymmetricKeys userAsymmetricKeys, + ICollection usersOrganizationAccounts, + ICollection designatedEmergencyAccess) + { + var userId = _currentContext.UserId; + if (!userId.HasValue || + userAsymmetricKeys.UserId != userId.Value || + usersOrganizationAccounts.Any(ou => ou.UserId != userId) || + designatedEmergencyAccess.Any(dea => dea.GranteeId != userId)) + { + throw new NotFoundException(); + } + + var inOrganizations = usersOrganizationAccounts.Any(ou => + ou.Status is OrganizationUserStatusType.Confirmed or OrganizationUserStatusType.Revoked); + var hasDesignatedEmergencyAccess = designatedEmergencyAccess.Any(x => + x.Status is EmergencyAccessStatusType.Confirmed or EmergencyAccessStatusType.RecoveryApproved + or EmergencyAccessStatusType.RecoveryInitiated); + + // For now, don't regenerate asymmetric keys for user's with organization membership and designated emergency access. + if (inOrganizations || hasDesignatedEmergencyAccess) + { + throw new BadRequestException("Key regeneration not supported for this user."); + } + + await _userAsymmetricKeysRepository.RegenerateUserAsymmetricKeysAsync(userAsymmetricKeys); + _logger.LogInformation( + "User's asymmetric keys regenerated. UserId: {userId} OrganizationMembership: {inOrganizations} DesignatedEmergencyAccess: {hasDesignatedEmergencyAccess} DeviceType: {deviceType}", + userAsymmetricKeys.UserId, inOrganizations, hasDesignatedEmergencyAccess, _currentContext.DeviceType); + } +} diff --git a/src/Core/KeyManagement/KeyManagementServiceCollectionExtensions.cs b/src/Core/KeyManagement/KeyManagementServiceCollectionExtensions.cs new file mode 100644 index 000000000000..102630c7e6b7 --- /dev/null +++ b/src/Core/KeyManagement/KeyManagementServiceCollectionExtensions.cs @@ -0,0 +1,18 @@ +using Bit.Core.KeyManagement.Commands; +using Bit.Core.KeyManagement.Commands.Interfaces; +using Microsoft.Extensions.DependencyInjection; + +namespace Bit.Core.KeyManagement; + +public static class KeyManagementServiceCollectionExtensions +{ + public static void AddKeyManagementServices(this IServiceCollection services) + { + services.AddKeyManagementCommands(); + } + + private static void AddKeyManagementCommands(this IServiceCollection services) + { + services.AddScoped(); + } +} diff --git a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs index bd3aecf2f5f7..39c163c01052 100644 --- a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs +++ b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs @@ -24,6 +24,7 @@ using Bit.Core.HostedServices; using Bit.Core.Identity; using Bit.Core.IdentityServer; +using Bit.Core.KeyManagement; using Bit.Core.OrganizationFeatures; using Bit.Core.Repositories; using Bit.Core.Resources; @@ -114,6 +115,7 @@ public static void AddBaseServices(this IServiceCollection services, IGlobalSett services.AddLoginServices(); services.AddScoped(); services.AddVaultServices(); + services.AddKeyManagementServices(); } public static void AddTokenizers(this IServiceCollection services) From 57849032dadafa351e99eb8354abac0aeb6c5de7 Mon Sep 17 00:00:00 2001 From: Thomas Avery Date: Tue, 22 Oct 2024 17:30:05 -0500 Subject: [PATCH 07/17] add new command tests --- ...egenerateUserAsymmetricKeysCommandTests.cs | 183 ++++++++++++++++++ 1 file changed, 183 insertions(+) create mode 100644 test/Core.Test/KeyManagement/Commands/RegenerateUserAsymmetricKeysCommandTests.cs diff --git a/test/Core.Test/KeyManagement/Commands/RegenerateUserAsymmetricKeysCommandTests.cs b/test/Core.Test/KeyManagement/Commands/RegenerateUserAsymmetricKeysCommandTests.cs new file mode 100644 index 000000000000..f35673e0eb86 --- /dev/null +++ b/test/Core.Test/KeyManagement/Commands/RegenerateUserAsymmetricKeysCommandTests.cs @@ -0,0 +1,183 @@ +#nullable enable +using Bit.Core.Auth.Enums; +using Bit.Core.Auth.Models.Data; +using Bit.Core.Context; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.KeyManagement.Commands; +using Bit.Core.KeyManagement.Models.Data; +using Bit.Core.KeyManagement.Repositories; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using NSubstitute.ReturnsExtensions; +using Xunit; + +namespace Bit.Core.Test.KeyManagement.Commands; + +[SutProviderCustomize] +public class RegenerateUserAsymmetricKeysCommandTests +{ + [Theory] + [BitAutoData] + public async Task RegenerateKeysAsync_NoCurrentContext_NotFoundException( + SutProvider sutProvider, + UserAsymmetricKeys userAsymmetricKeys) + { + sutProvider.GetDependency().UserId.ReturnsNullForAnyArgs(); + var usersOrganizationAccounts = new List(); + var designatedEmergencyAccess = new List(); + + await Assert.ThrowsAsync(() => sutProvider.Sut.RegenerateKeysAsync(userAsymmetricKeys, + usersOrganizationAccounts, designatedEmergencyAccess)); + } + + [Theory] + [BitAutoData] + public async Task RegenerateKeysAsync_UserHasNoSharedAccess_Success( + SutProvider sutProvider, + UserAsymmetricKeys userAsymmetricKeys) + { + sutProvider.GetDependency().UserId.ReturnsForAnyArgs(userAsymmetricKeys.UserId); + var usersOrganizationAccounts = new List(); + var designatedEmergencyAccess = new List(); + + await sutProvider.Sut.RegenerateKeysAsync(userAsymmetricKeys, + usersOrganizationAccounts, designatedEmergencyAccess); + + await sutProvider.GetDependency() + .Received(1) + .RegenerateUserAsymmetricKeysAsync(Arg.Is(userAsymmetricKeys)); + } + + [Theory] + [BitAutoData(false, false, true)] + [BitAutoData(false, true, false)] + [BitAutoData(false, true, true)] + [BitAutoData(true, false, false)] + [BitAutoData(true, true, false)] + [BitAutoData(true, true, true)] + public async Task RegenerateKeysAsync_UserIdMisMatch_NotFoundException( + bool userAsymmetricKeysMismatch, + bool orgMismatch, + bool emergencyAccessMismatch, + SutProvider sutProvider, + UserAsymmetricKeys userAsymmetricKeys, + ICollection usersOrganizationAccounts, + ICollection designatedEmergencyAccess) + { + sutProvider.GetDependency().UserId + .ReturnsForAnyArgs(userAsymmetricKeysMismatch ? new Guid() : userAsymmetricKeys.UserId); + + if (!orgMismatch) + { + usersOrganizationAccounts = + SetupOrganizationUserAccounts(userAsymmetricKeys.UserId, usersOrganizationAccounts); + } + + if (!emergencyAccessMismatch) + { + designatedEmergencyAccess = SetupEmergencyAccess(userAsymmetricKeys.UserId, designatedEmergencyAccess); + } + + await Assert.ThrowsAsync(() => sutProvider.Sut.RegenerateKeysAsync(userAsymmetricKeys, + usersOrganizationAccounts, designatedEmergencyAccess)); + + await sutProvider.GetDependency() + .ReceivedWithAnyArgs(0) + .RegenerateUserAsymmetricKeysAsync(Arg.Any()); + } + + [Theory] + [BitAutoData(OrganizationUserStatusType.Confirmed)] + [BitAutoData(OrganizationUserStatusType.Revoked)] + public async Task RegenerateKeysAsync_UserInOrganizations_BadRequestException( + OrganizationUserStatusType organizationUserStatus, + SutProvider sutProvider, + UserAsymmetricKeys userAsymmetricKeys, + ICollection usersOrganizationAccounts) + { + sutProvider.GetDependency().UserId.ReturnsForAnyArgs(userAsymmetricKeys.UserId); + usersOrganizationAccounts = CreateInOrganizationAccounts(userAsymmetricKeys.UserId, organizationUserStatus, + usersOrganizationAccounts); + var designatedEmergencyAccess = new List(); + + await Assert.ThrowsAsync(() => sutProvider.Sut.RegenerateKeysAsync(userAsymmetricKeys, + usersOrganizationAccounts, designatedEmergencyAccess)); + + await sutProvider.GetDependency() + .ReceivedWithAnyArgs(0) + .RegenerateUserAsymmetricKeysAsync(Arg.Any()); + } + + [Theory] + [BitAutoData(EmergencyAccessStatusType.Confirmed)] + [BitAutoData(EmergencyAccessStatusType.RecoveryApproved)] + [BitAutoData(EmergencyAccessStatusType.RecoveryInitiated)] + public async Task RegenerateKeysAsync_UserHasDesignatedEmergencyAccess_BadRequestException( + EmergencyAccessStatusType statusType, + SutProvider sutProvider, + UserAsymmetricKeys userAsymmetricKeys, + ICollection designatedEmergencyAccess) + { + sutProvider.GetDependency().UserId.ReturnsForAnyArgs(userAsymmetricKeys.UserId); + designatedEmergencyAccess = + CreateDesignatedEmergencyAccess(userAsymmetricKeys.UserId, statusType, designatedEmergencyAccess); + var usersOrganizationAccounts = new List(); + + + await Assert.ThrowsAsync(() => sutProvider.Sut.RegenerateKeysAsync(userAsymmetricKeys, + usersOrganizationAccounts, designatedEmergencyAccess)); + + await sutProvider.GetDependency() + .ReceivedWithAnyArgs(0) + .RegenerateUserAsymmetricKeysAsync(Arg.Any()); + } + + private static ICollection CreateInOrganizationAccounts(Guid userId, + OrganizationUserStatusType organizationUserStatus, ICollection organizationUserAccounts) + { + foreach (var organizationUserAccount in organizationUserAccounts) + { + organizationUserAccount.UserId = userId; + organizationUserAccount.Status = organizationUserStatus; + } + + return organizationUserAccounts; + } + + private static ICollection CreateDesignatedEmergencyAccess(Guid userId, + EmergencyAccessStatusType status, ICollection designatedEmergencyAccess) + { + foreach (var designated in designatedEmergencyAccess) + { + designated.GranteeId = userId; + designated.Status = status; + } + + return designatedEmergencyAccess; + } + + private static ICollection SetupOrganizationUserAccounts(Guid userId, + ICollection organizationUserAccounts) + { + foreach (var organizationUserAccount in organizationUserAccounts) + { + organizationUserAccount.UserId = userId; + } + + return organizationUserAccounts; + } + + private static ICollection SetupEmergencyAccess(Guid userId, + ICollection emergencyAccessDetails) + { + foreach (var emergencyAccessDetail in emergencyAccessDetails) + { + emergencyAccessDetail.GranteeId = userId; + } + + return emergencyAccessDetails; + } +} From 3840b5c230dea88912013ff6a057d3a45037dcdf Mon Sep 17 00:00:00 2001 From: Thomas Avery Date: Tue, 22 Oct 2024 17:30:41 -0500 Subject: [PATCH 08/17] Add regen controller --- .../AccountsKeyManagementController.cs | 41 +++++++++++++++++++ .../Requests/KeyRegenerationRequestModel.cs | 26 ++++++++++++ 2 files changed, 67 insertions(+) create mode 100644 src/Api/KeyManagement/Controllers/AccountsKeyManagementController.cs create mode 100644 src/Api/KeyManagement/Models/Requests/KeyRegenerationRequestModel.cs diff --git a/src/Api/KeyManagement/Controllers/AccountsKeyManagementController.cs b/src/Api/KeyManagement/Controllers/AccountsKeyManagementController.cs new file mode 100644 index 000000000000..ece967a55f2d --- /dev/null +++ b/src/Api/KeyManagement/Controllers/AccountsKeyManagementController.cs @@ -0,0 +1,41 @@ +#nullable enable +using Bit.Api.KeyManagement.Models.Requests; +using Bit.Core.KeyManagement.Commands.Interfaces; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Bit.Api.KeyManagement.Controllers; + +[Route("accounts/key-management")] +[Authorize("Application")] +public class AccountsKeyManagementController : Controller +{ + private readonly IEmergencyAccessRepository _emergencyAccessRepository; + private readonly IOrganizationUserRepository _organizationUserRepository; + private readonly IRegenerateUserAsymmetricKeysCommand _regenerateUserAsymmetricKeysCommand; + private readonly IUserService _userService; + + public AccountsKeyManagementController(IUserService userService, + IOrganizationUserRepository organizationUserRepository, + IEmergencyAccessRepository emergencyAccessRepository, + IRegenerateUserAsymmetricKeysCommand regenerateUserAsymmetricKeysCommand) + { + _userService = userService; + _regenerateUserAsymmetricKeysCommand = regenerateUserAsymmetricKeysCommand; + _organizationUserRepository = organizationUserRepository; + _emergencyAccessRepository = emergencyAccessRepository; + } + + [HttpPost("regenerate-keys")] + public async Task RegenerateKeysAsync([FromBody] KeyRegenerationRequestModel request) + { + // FIXME add feature flag check. + var user = await _userService.GetUserByPrincipalAsync(User) ?? throw new UnauthorizedAccessException(); + var usersOrganizationAccounts = await _organizationUserRepository.GetManyByUserAsync(user.Id); + var designatedEmergencyAccess = await _emergencyAccessRepository.GetManyDetailsByGranteeIdAsync(user.Id); + await _regenerateUserAsymmetricKeysCommand.RegenerateKeysAsync(request.ToUserAsymmetricKeys(user.Id), + usersOrganizationAccounts, designatedEmergencyAccess); + } +} diff --git a/src/Api/KeyManagement/Models/Requests/KeyRegenerationRequestModel.cs b/src/Api/KeyManagement/Models/Requests/KeyRegenerationRequestModel.cs new file mode 100644 index 000000000000..9d148b5175bd --- /dev/null +++ b/src/Api/KeyManagement/Models/Requests/KeyRegenerationRequestModel.cs @@ -0,0 +1,26 @@ +#nullable enable +using System.ComponentModel.DataAnnotations; +using Bit.Core.KeyManagement.Models.Data; +using Bit.Core.Utilities; + +namespace Bit.Api.KeyManagement.Models.Requests; + +public class KeyRegenerationRequestModel +{ + [Required] + public required string UserPublicKey { get; set; } + + [Required] + [EncryptedString] + public required string UserKeyEncryptedUserPrivateKey { get; set; } + + public UserAsymmetricKeys ToUserAsymmetricKeys(Guid userId) + { + return new UserAsymmetricKeys + { + UserId = userId, + PublicKey = UserPublicKey, + UserKeyEncryptedPrivateKey = UserKeyEncryptedUserPrivateKey, + }; + } +} From 7d68d7085d2b4ad98f294e5b270ea9f28edb326b Mon Sep 17 00:00:00 2001 From: Thomas Avery Date: Tue, 22 Oct 2024 17:30:56 -0500 Subject: [PATCH 09/17] Add regen controller tests --- .../Helpers/LoginHelper.cs | 6 + .../AccountsKeyManagementControllerTests.cs | 124 ++++++++++++++++++ .../AccountsKeyManagementControllerTests.cs | 61 +++++++++ 3 files changed, 191 insertions(+) create mode 100644 test/Api.IntegrationTest/KeyManagement/Controllers/AccountsKeyManagementControllerTests.cs create mode 100644 test/Api.Test/KeyManagement/Controllers/AccountsKeyManagementControllerTests.cs diff --git a/test/Api.IntegrationTest/Helpers/LoginHelper.cs b/test/Api.IntegrationTest/Helpers/LoginHelper.cs index d6ce911bd0fe..1f5eb725d956 100644 --- a/test/Api.IntegrationTest/Helpers/LoginHelper.cs +++ b/test/Api.IntegrationTest/Helpers/LoginHelper.cs @@ -16,6 +16,12 @@ public LoginHelper(ApiApplicationFactory factory, HttpClient client) _client = client; } + public async Task LoginAsync(string email) + { + var tokens = await _factory.LoginAsync(email); + _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokens.Token); + } + public async Task LoginWithOrganizationApiKeyAsync(Guid organizationId) { var (clientId, apiKey) = await GetOrganizationApiKey(_factory, organizationId); diff --git a/test/Api.IntegrationTest/KeyManagement/Controllers/AccountsKeyManagementControllerTests.cs b/test/Api.IntegrationTest/KeyManagement/Controllers/AccountsKeyManagementControllerTests.cs new file mode 100644 index 000000000000..77734d0b7474 --- /dev/null +++ b/test/Api.IntegrationTest/KeyManagement/Controllers/AccountsKeyManagementControllerTests.cs @@ -0,0 +1,124 @@ +using System.Net; +using Bit.Api.IntegrationTest.Factories; +using Bit.Api.IntegrationTest.Helpers; +using Bit.Api.KeyManagement.Models.Requests; +using Bit.Core.Auth.Entities; +using Bit.Core.Auth.Enums; +using Bit.Core.Billing.Enums; +using Bit.Core.Enums; +using Bit.Core.Repositories; +using Bit.Test.Common.AutoFixture.Attributes; +using Xunit; + +namespace Bit.Api.IntegrationTest.KeyManagement.Controllers; + +public class AccountsKeyManagementControllerTests : IClassFixture, IAsyncLifetime +{ + private static readonly string _mockEncryptedString = + "2.AOs41Hd8OQiCPXjyJKCiDA==|O6OHgt2U2hJGBSNGnimJmg==|iD33s8B69C8JhYYhSa4V1tArjvLr8eEaGqOV7BRo5Jk="; + + private readonly HttpClient _client; + private readonly IEmergencyAccessRepository _emergencyAccessRepository; + private readonly ApiApplicationFactory _factory; + private readonly LoginHelper _loginHelper; + private readonly IUserRepository _userRepository; + private string _ownerEmail = null!; + + public AccountsKeyManagementControllerTests(ApiApplicationFactory factory) + { + _factory = factory; + _client = factory.CreateClient(); + _loginHelper = new LoginHelper(_factory, _client); + _userRepository = _factory.GetService(); + _emergencyAccessRepository = _factory.GetService(); + } + + public async Task InitializeAsync() + { + _ownerEmail = $"integration-test{Guid.NewGuid()}@bitwarden.com"; + await _factory.LoginWithNewAccount(_ownerEmail); + } + + public Task DisposeAsync() + { + _client.Dispose(); + return Task.CompletedTask; + } + + [Theory] + [BitAutoData] + public async Task RegenerateKeysAsync_NotLoggedIn_Unauthorized(KeyRegenerationRequestModel request) + { + request.UserKeyEncryptedUserPrivateKey = _mockEncryptedString; + + var response = await _client.PostAsJsonAsync("/accounts/key-management/regenerate-keys", request); + + Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); + } + + [Theory] + [BitAutoData(true, true)] + [BitAutoData(true, false)] + [BitAutoData(false, true)] + public async Task RegenerateKeysAsync_UserInOrgOrHasDesignatedEmergencyAccess_ThrowsBadRequest( + bool inOrganization, + bool hasDesignatedEmergencyAccess, + KeyRegenerationRequestModel request) + { + if (inOrganization) + { + await OrganizationTestHelpers.SignUpAsync(_factory, + PlanType.EnterpriseAnnually, _ownerEmail, passwordManagerSeats: 10, + paymentMethod: PaymentMethodType.Card); + } + + if (hasDesignatedEmergencyAccess) + { + await CreateDesignatedEmergencyAccessAsync(); + } + + await _loginHelper.LoginAsync(_ownerEmail); + request.UserKeyEncryptedUserPrivateKey = _mockEncryptedString; + + var response = await _client.PostAsJsonAsync("/accounts/key-management/regenerate-keys", request); + + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + } + + [Theory] + [BitAutoData] + public async Task RegenerateKeysAsync_Success(KeyRegenerationRequestModel request) + { + await _loginHelper.LoginAsync(_ownerEmail); + request.UserKeyEncryptedUserPrivateKey = _mockEncryptedString; + + var response = await _client.PostAsJsonAsync("/accounts/key-management/regenerate-keys", request); + response.EnsureSuccessStatusCode(); + + var user = await _userRepository.GetByEmailAsync(_ownerEmail); + Assert.NotNull(user); + Assert.Equal(request.UserPublicKey, user.PublicKey); + Assert.Equal(request.UserKeyEncryptedUserPrivateKey, user.PrivateKey); + } + + private async Task CreateDesignatedEmergencyAccessAsync() + { + var tempEmail = $"integration-test{Guid.NewGuid()}@bitwarden.com"; + await _factory.LoginWithNewAccount(tempEmail); + + var tempUser = await _userRepository.GetByEmailAsync(tempEmail); + var user = await _userRepository.GetByEmailAsync(_ownerEmail); + var emergencyAccess = new EmergencyAccess + { + GrantorId = tempUser!.Id, + GranteeId = user!.Id, + KeyEncrypted = _mockEncryptedString, + Status = EmergencyAccessStatusType.Confirmed, + Type = EmergencyAccessType.View, + WaitTimeDays = 10, + CreationDate = DateTime.UtcNow, + RevisionDate = DateTime.UtcNow + }; + await _emergencyAccessRepository.CreateAsync(emergencyAccess); + } +} diff --git a/test/Api.Test/KeyManagement/Controllers/AccountsKeyManagementControllerTests.cs b/test/Api.Test/KeyManagement/Controllers/AccountsKeyManagementControllerTests.cs new file mode 100644 index 000000000000..bc0a6c64ddab --- /dev/null +++ b/test/Api.Test/KeyManagement/Controllers/AccountsKeyManagementControllerTests.cs @@ -0,0 +1,61 @@ +#nullable enable +using System.Security.Claims; +using Bit.Api.KeyManagement.Controllers; +using Bit.Api.KeyManagement.Models.Requests; +using Bit.Core.Auth.Models.Data; +using Bit.Core.Entities; +using Bit.Core.KeyManagement.Commands.Interfaces; +using Bit.Core.KeyManagement.Models.Data; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using NSubstitute.ReturnsExtensions; +using Xunit; + +namespace Bit.Api.Test.KeyManagement.Controllers; + +[ControllerCustomize(typeof(AccountsKeyManagementController))] +[SutProviderCustomize] +[JsonDocumentCustomize] +public class AccountsKeyManagementControllerTests +{ + [Theory] + [BitAutoData] + public async Task RegenerateKeysAsync_UserNull_Throws(SutProvider sutProvider, + KeyRegenerationRequestModel data) + { + sutProvider.GetDependency().GetUserByPrincipalAsync(Arg.Any()).ReturnsNull(); + + await Assert.ThrowsAsync(() => sutProvider.Sut.RegenerateKeysAsync(data)); + + await sutProvider.GetDependency().ReceivedWithAnyArgs(0) + .GetManyByUserAsync(Arg.Any()); + await sutProvider.GetDependency().ReceivedWithAnyArgs(0) + .GetManyDetailsByGranteeIdAsync(Arg.Any()); + await sutProvider.GetDependency().ReceivedWithAnyArgs(0) + .RegenerateKeysAsync(Arg.Any(), + Arg.Any>(), + Arg.Any>()); + } + + [Theory] + [BitAutoData] + public async Task RegenerateKeysAsync_Success(SutProvider sutProvider, + KeyRegenerationRequestModel data, User user) + { + sutProvider.GetDependency().GetUserByPrincipalAsync(Arg.Any()).Returns(user); + + await sutProvider.Sut.RegenerateKeysAsync(data); + + await sutProvider.GetDependency().Received(1) + .GetManyByUserAsync(Arg.Is(user.Id)); + await sutProvider.GetDependency().Received(1) + .GetManyDetailsByGranteeIdAsync(Arg.Is(user.Id)); + await sutProvider.GetDependency().ReceivedWithAnyArgs(1) + .RegenerateKeysAsync(Arg.Any(), + Arg.Any>(), + Arg.Any>()); + } +} From bcd4e8818cc6749e04b67ee0cd5ba36619078a60 Mon Sep 17 00:00:00 2001 From: Thomas Avery Date: Tue, 22 Oct 2024 17:41:08 -0500 Subject: [PATCH 10/17] bump migration date --- ...rate.sql => 2024-10-22_00_AddUserAsymmetricKeysRegenerate.sql} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename util/Migrator/DbScripts/{2024-10-15_01_AddUserAsymmetricKeysRegenerate.sql => 2024-10-22_00_AddUserAsymmetricKeysRegenerate.sql} (100%) diff --git a/util/Migrator/DbScripts/2024-10-15_01_AddUserAsymmetricKeysRegenerate.sql b/util/Migrator/DbScripts/2024-10-22_00_AddUserAsymmetricKeysRegenerate.sql similarity index 100% rename from util/Migrator/DbScripts/2024-10-15_01_AddUserAsymmetricKeysRegenerate.sql rename to util/Migrator/DbScripts/2024-10-22_00_AddUserAsymmetricKeysRegenerate.sql From 0b60bb93d8b40907985f092e81ac7dff12d0941a Mon Sep 17 00:00:00 2001 From: Thomas Avery Date: Wed, 23 Oct 2024 16:02:38 -0500 Subject: [PATCH 11/17] add feature flag --- .../AccountsKeyManagementController.cs | 11 +++++++- src/Core/Constants.cs | 1 + .../AccountsKeyManagementControllerTests.cs | 23 +++++++++++++++ .../AccountsKeyManagementControllerTests.cs | 28 +++++++++++++++++++ 4 files changed, 62 insertions(+), 1 deletion(-) diff --git a/src/Api/KeyManagement/Controllers/AccountsKeyManagementController.cs b/src/Api/KeyManagement/Controllers/AccountsKeyManagementController.cs index ece967a55f2d..b8d5e3094931 100644 --- a/src/Api/KeyManagement/Controllers/AccountsKeyManagementController.cs +++ b/src/Api/KeyManagement/Controllers/AccountsKeyManagementController.cs @@ -1,5 +1,7 @@ #nullable enable using Bit.Api.KeyManagement.Models.Requests; +using Bit.Core; +using Bit.Core.Exceptions; using Bit.Core.KeyManagement.Commands.Interfaces; using Bit.Core.Repositories; using Bit.Core.Services; @@ -13,16 +15,19 @@ namespace Bit.Api.KeyManagement.Controllers; public class AccountsKeyManagementController : Controller { private readonly IEmergencyAccessRepository _emergencyAccessRepository; + private readonly IFeatureService _featureService; private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IRegenerateUserAsymmetricKeysCommand _regenerateUserAsymmetricKeysCommand; private readonly IUserService _userService; public AccountsKeyManagementController(IUserService userService, + IFeatureService featureService, IOrganizationUserRepository organizationUserRepository, IEmergencyAccessRepository emergencyAccessRepository, IRegenerateUserAsymmetricKeysCommand regenerateUserAsymmetricKeysCommand) { _userService = userService; + _featureService = featureService; _regenerateUserAsymmetricKeysCommand = regenerateUserAsymmetricKeysCommand; _organizationUserRepository = organizationUserRepository; _emergencyAccessRepository = emergencyAccessRepository; @@ -31,7 +36,11 @@ public AccountsKeyManagementController(IUserService userService, [HttpPost("regenerate-keys")] public async Task RegenerateKeysAsync([FromBody] KeyRegenerationRequestModel request) { - // FIXME add feature flag check. + if (!_featureService.IsEnabled(FeatureFlagKeys.PrivateKeyRegeneration)) + { + throw new NotFoundException(); + } + var user = await _userService.GetUserByPrincipalAsync(User) ?? throw new UnauthorizedAccessException(); var usersOrganizationAccounts = await _organizationUserRepository.GetManyByUserAsync(user.Id); var designatedEmergencyAccess = await _emergencyAccessRepository.GetManyDetailsByGranteeIdAsync(user.Id); diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index ecbe190ccd2a..f17528d4f1ab 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -148,6 +148,7 @@ public static class FeatureFlagKeys public const string VerifiedSsoDomainEndpoint = "pm-12337-refactor-sso-details-endpoint"; public const string Pm13322AddPolicyDefinitions = "pm-13322-add-policy-definitions"; public const string LimitCollectionCreationDeletionSplit = "pm-10863-limit-collection-creation-deletion-split"; + public const string PrivateKeyRegeneration = "pm-12241-private-key-regeneration"; public static List GetAllKeys() { diff --git a/test/Api.IntegrationTest/KeyManagement/Controllers/AccountsKeyManagementControllerTests.cs b/test/Api.IntegrationTest/KeyManagement/Controllers/AccountsKeyManagementControllerTests.cs index 77734d0b7474..0895bb1d916b 100644 --- a/test/Api.IntegrationTest/KeyManagement/Controllers/AccountsKeyManagementControllerTests.cs +++ b/test/Api.IntegrationTest/KeyManagement/Controllers/AccountsKeyManagementControllerTests.cs @@ -27,6 +27,8 @@ public class AccountsKeyManagementControllerTests : IClassFixture(); @@ -45,6 +47,27 @@ public Task DisposeAsync() return Task.CompletedTask; } + [Theory] + [BitAutoData] + public async Task RegenerateKeysAsync_FeatureFlagTurnedOff_NotFound(KeyRegenerationRequestModel request) + { + // Localize factory to inject a false value for the feature flag. + var localFactory = new ApiApplicationFactory(); + localFactory.UpdateConfiguration("globalSettings:launchDarkly:flagValues:pm-12241-private-key-regeneration", + "false"); + var localClient = localFactory.CreateClient(); + var localEmail = $"integration-test{Guid.NewGuid()}@bitwarden.com"; + var localLoginHelper = new LoginHelper(localFactory, localClient); + await localFactory.LoginWithNewAccount(localEmail); + await localLoginHelper.LoginAsync(localEmail); + + request.UserKeyEncryptedUserPrivateKey = _mockEncryptedString; + + var response = await localClient.PostAsJsonAsync("/accounts/key-management/regenerate-keys", request); + + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + [Theory] [BitAutoData] public async Task RegenerateKeysAsync_NotLoggedIn_Unauthorized(KeyRegenerationRequestModel request) diff --git a/test/Api.Test/KeyManagement/Controllers/AccountsKeyManagementControllerTests.cs b/test/Api.Test/KeyManagement/Controllers/AccountsKeyManagementControllerTests.cs index bc0a6c64ddab..d01215b32af0 100644 --- a/test/Api.Test/KeyManagement/Controllers/AccountsKeyManagementControllerTests.cs +++ b/test/Api.Test/KeyManagement/Controllers/AccountsKeyManagementControllerTests.cs @@ -2,8 +2,10 @@ using System.Security.Claims; using Bit.Api.KeyManagement.Controllers; using Bit.Api.KeyManagement.Models.Requests; +using Bit.Core; using Bit.Core.Auth.Models.Data; using Bit.Core.Entities; +using Bit.Core.Exceptions; using Bit.Core.KeyManagement.Commands.Interfaces; using Bit.Core.KeyManagement.Models.Data; using Bit.Core.Repositories; @@ -21,11 +23,35 @@ namespace Bit.Api.Test.KeyManagement.Controllers; [JsonDocumentCustomize] public class AccountsKeyManagementControllerTests { + [Theory] + [BitAutoData] + public async Task RegenerateKeysAsync_FeatureFlagOff_Throws( + SutProvider sutProvider, + KeyRegenerationRequestModel data) + { + sutProvider.GetDependency().IsEnabled(Arg.Is(FeatureFlagKeys.PrivateKeyRegeneration)) + .Returns(false); + sutProvider.GetDependency().GetUserByPrincipalAsync(Arg.Any()).ReturnsNull(); + + await Assert.ThrowsAsync(() => sutProvider.Sut.RegenerateKeysAsync(data)); + + await sutProvider.GetDependency().ReceivedWithAnyArgs(0) + .GetManyByUserAsync(Arg.Any()); + await sutProvider.GetDependency().ReceivedWithAnyArgs(0) + .GetManyDetailsByGranteeIdAsync(Arg.Any()); + await sutProvider.GetDependency().ReceivedWithAnyArgs(0) + .RegenerateKeysAsync(Arg.Any(), + Arg.Any>(), + Arg.Any>()); + } + [Theory] [BitAutoData] public async Task RegenerateKeysAsync_UserNull_Throws(SutProvider sutProvider, KeyRegenerationRequestModel data) { + sutProvider.GetDependency().IsEnabled(Arg.Is(FeatureFlagKeys.PrivateKeyRegeneration)) + .Returns(true); sutProvider.GetDependency().GetUserByPrincipalAsync(Arg.Any()).ReturnsNull(); await Assert.ThrowsAsync(() => sutProvider.Sut.RegenerateKeysAsync(data)); @@ -45,6 +71,8 @@ await sutProvider.GetDependency().Received public async Task RegenerateKeysAsync_Success(SutProvider sutProvider, KeyRegenerationRequestModel data, User user) { + sutProvider.GetDependency().IsEnabled(Arg.Is(FeatureFlagKeys.PrivateKeyRegeneration)) + .Returns(true); sutProvider.GetDependency().GetUserByPrincipalAsync(Arg.Any()).Returns(user); await sutProvider.Sut.RegenerateKeysAsync(data); From 292fa3efc2034b5c27b069acc50a1ece834fac09 Mon Sep 17 00:00:00 2001 From: Thomas Avery Date: Thu, 24 Oct 2024 14:16:45 -0500 Subject: [PATCH 12/17] Add more logging --- .../Commands/RegenerateUserAsymmetricKeysCommand.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Core/KeyManagement/Commands/RegenerateUserAsymmetricKeysCommand.cs b/src/Core/KeyManagement/Commands/RegenerateUserAsymmetricKeysCommand.cs index 4a76198d6efd..ee6d70a95eb5 100644 --- a/src/Core/KeyManagement/Commands/RegenerateUserAsymmetricKeysCommand.cs +++ b/src/Core/KeyManagement/Commands/RegenerateUserAsymmetricKeysCommand.cs @@ -47,6 +47,10 @@ public async Task RegenerateKeysAsync(UserAsymmetricKeys userAsymmetricKeys, x.Status is EmergencyAccessStatusType.Confirmed or EmergencyAccessStatusType.RecoveryApproved or EmergencyAccessStatusType.RecoveryInitiated); + _logger.LogInformation( + "User asymmetric keys regeneration requested. UserId: {userId} OrganizationMembership: {inOrganizations} DesignatedEmergencyAccess: {hasDesignatedEmergencyAccess} DeviceType: {deviceType}", + userAsymmetricKeys.UserId, inOrganizations, hasDesignatedEmergencyAccess, _currentContext.DeviceType); + // For now, don't regenerate asymmetric keys for user's with organization membership and designated emergency access. if (inOrganizations || hasDesignatedEmergencyAccess) { From 4d6ae5741fdea0a5ebbf78306e78940eb5f30384 Mon Sep 17 00:00:00 2001 From: Thomas Avery Date: Fri, 25 Oct 2024 11:02:51 -0500 Subject: [PATCH 13/17] update request model --- .../Models/Requests/KeyRegenerationRequestModel.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Api/KeyManagement/Models/Requests/KeyRegenerationRequestModel.cs b/src/Api/KeyManagement/Models/Requests/KeyRegenerationRequestModel.cs index 9d148b5175bd..495d13cccd95 100644 --- a/src/Api/KeyManagement/Models/Requests/KeyRegenerationRequestModel.cs +++ b/src/Api/KeyManagement/Models/Requests/KeyRegenerationRequestModel.cs @@ -1,5 +1,4 @@ #nullable enable -using System.ComponentModel.DataAnnotations; using Bit.Core.KeyManagement.Models.Data; using Bit.Core.Utilities; @@ -7,10 +6,8 @@ namespace Bit.Api.KeyManagement.Models.Requests; public class KeyRegenerationRequestModel { - [Required] public required string UserPublicKey { get; set; } - [Required] [EncryptedString] public required string UserKeyEncryptedUserPrivateKey { get; set; } From 23d9e3f4e581874353c65bc587cb87806312ae51 Mon Sep 17 00:00:00 2001 From: Thomas Avery Date: Fri, 25 Oct 2024 11:03:12 -0500 Subject: [PATCH 14/17] update tests --- .../AccountsKeyManagementControllerTests.cs | 43 +++++++++++++------ .../AccountsKeyManagementControllerTests.cs | 17 +++++--- ...egenerateUserAsymmetricKeysCommandTests.cs | 1 + 3 files changed, 43 insertions(+), 18 deletions(-) diff --git a/test/Api.IntegrationTest/KeyManagement/Controllers/AccountsKeyManagementControllerTests.cs b/test/Api.IntegrationTest/KeyManagement/Controllers/AccountsKeyManagementControllerTests.cs index 0895bb1d916b..ec7ca3746010 100644 --- a/test/Api.IntegrationTest/KeyManagement/Controllers/AccountsKeyManagementControllerTests.cs +++ b/test/Api.IntegrationTest/KeyManagement/Controllers/AccountsKeyManagementControllerTests.cs @@ -19,6 +19,7 @@ public class AccountsKeyManagementControllerTests : IClassFixture(); _emergencyAccessRepository = _factory.GetService(); + _organizationUserRepository = _factory.GetService(); } public async Task InitializeAsync() @@ -80,24 +82,30 @@ public async Task RegenerateKeysAsync_NotLoggedIn_Unauthorized(KeyRegenerationRe } [Theory] - [BitAutoData(true, true)] - [BitAutoData(true, false)] - [BitAutoData(false, true)] + [BitAutoData(OrganizationUserStatusType.Confirmed, EmergencyAccessStatusType.Confirmed)] + [BitAutoData(OrganizationUserStatusType.Confirmed, EmergencyAccessStatusType.RecoveryApproved)] + [BitAutoData(OrganizationUserStatusType.Confirmed, EmergencyAccessStatusType.RecoveryInitiated)] + [BitAutoData(OrganizationUserStatusType.Revoked, EmergencyAccessStatusType.Confirmed)] + [BitAutoData(OrganizationUserStatusType.Revoked, EmergencyAccessStatusType.RecoveryApproved)] + [BitAutoData(OrganizationUserStatusType.Revoked, EmergencyAccessStatusType.RecoveryInitiated)] + [BitAutoData(OrganizationUserStatusType.Confirmed, null)] + [BitAutoData(OrganizationUserStatusType.Revoked, null)] + [BitAutoData(OrganizationUserStatusType.Invited, EmergencyAccessStatusType.Confirmed)] + [BitAutoData(OrganizationUserStatusType.Invited, EmergencyAccessStatusType.RecoveryApproved)] + [BitAutoData(OrganizationUserStatusType.Invited, EmergencyAccessStatusType.RecoveryInitiated)] public async Task RegenerateKeysAsync_UserInOrgOrHasDesignatedEmergencyAccess_ThrowsBadRequest( - bool inOrganization, - bool hasDesignatedEmergencyAccess, + OrganizationUserStatusType organizationUserStatus, + EmergencyAccessStatusType? emergencyAccessStatus, KeyRegenerationRequestModel request) { - if (inOrganization) + if (organizationUserStatus is OrganizationUserStatusType.Confirmed or OrganizationUserStatusType.Revoked) { - await OrganizationTestHelpers.SignUpAsync(_factory, - PlanType.EnterpriseAnnually, _ownerEmail, passwordManagerSeats: 10, - paymentMethod: PaymentMethodType.Card); + await CreateOrganizationUserAsync(organizationUserStatus); } - if (hasDesignatedEmergencyAccess) + if (emergencyAccessStatus != null) { - await CreateDesignatedEmergencyAccessAsync(); + await CreateDesignatedEmergencyAccessAsync(emergencyAccessStatus.Value); } await _loginHelper.LoginAsync(_ownerEmail); @@ -124,7 +132,16 @@ public async Task RegenerateKeysAsync_Success(KeyRegenerationRequestModel reques Assert.Equal(request.UserKeyEncryptedUserPrivateKey, user.PrivateKey); } - private async Task CreateDesignatedEmergencyAccessAsync() + private async Task CreateOrganizationUserAsync(OrganizationUserStatusType organizationUserStatus) + { + var (_, organizationUser) = await OrganizationTestHelpers.SignUpAsync(_factory, + PlanType.EnterpriseAnnually, _ownerEmail, passwordManagerSeats: 10, + paymentMethod: PaymentMethodType.Card); + organizationUser.Status = organizationUserStatus; + await _organizationUserRepository.ReplaceAsync(organizationUser); + } + + private async Task CreateDesignatedEmergencyAccessAsync(EmergencyAccessStatusType emergencyAccessStatus) { var tempEmail = $"integration-test{Guid.NewGuid()}@bitwarden.com"; await _factory.LoginWithNewAccount(tempEmail); @@ -136,7 +153,7 @@ private async Task CreateDesignatedEmergencyAccessAsync() GrantorId = tempUser!.Id, GranteeId = user!.Id, KeyEncrypted = _mockEncryptedString, - Status = EmergencyAccessStatusType.Confirmed, + Status = emergencyAccessStatus, Type = EmergencyAccessType.View, WaitTimeDays = 10, CreationDate = DateTime.UtcNow, diff --git a/test/Api.Test/KeyManagement/Controllers/AccountsKeyManagementControllerTests.cs b/test/Api.Test/KeyManagement/Controllers/AccountsKeyManagementControllerTests.cs index d01215b32af0..2615697ad374 100644 --- a/test/Api.Test/KeyManagement/Controllers/AccountsKeyManagementControllerTests.cs +++ b/test/Api.Test/KeyManagement/Controllers/AccountsKeyManagementControllerTests.cs @@ -69,11 +69,15 @@ await sutProvider.GetDependency().Received [Theory] [BitAutoData] public async Task RegenerateKeysAsync_Success(SutProvider sutProvider, - KeyRegenerationRequestModel data, User user) + KeyRegenerationRequestModel data, User user, ICollection orgUsers, + ICollection accessDetails) { sutProvider.GetDependency().IsEnabled(Arg.Is(FeatureFlagKeys.PrivateKeyRegeneration)) .Returns(true); sutProvider.GetDependency().GetUserByPrincipalAsync(Arg.Any()).Returns(user); + sutProvider.GetDependency().GetManyByUserAsync(Arg.Is(user.Id)).Returns(orgUsers); + sutProvider.GetDependency().GetManyDetailsByGranteeIdAsync(Arg.Is(user.Id)) + .Returns(accessDetails); await sutProvider.Sut.RegenerateKeysAsync(data); @@ -81,9 +85,12 @@ await sutProvider.GetDependency().Received(1) .GetManyByUserAsync(Arg.Is(user.Id)); await sutProvider.GetDependency().Received(1) .GetManyDetailsByGranteeIdAsync(Arg.Is(user.Id)); - await sutProvider.GetDependency().ReceivedWithAnyArgs(1) - .RegenerateKeysAsync(Arg.Any(), - Arg.Any>(), - Arg.Any>()); + await sutProvider.GetDependency().Received(1) + .RegenerateKeysAsync( + Arg.Is(u => + u.UserId == user.Id && u.PublicKey == data.UserPublicKey && + u.UserKeyEncryptedPrivateKey == data.UserKeyEncryptedUserPrivateKey), + Arg.Is(orgUsers), + Arg.Is(accessDetails)); } } diff --git a/test/Core.Test/KeyManagement/Commands/RegenerateUserAsymmetricKeysCommandTests.cs b/test/Core.Test/KeyManagement/Commands/RegenerateUserAsymmetricKeysCommandTests.cs index f35673e0eb86..ce2b9afefb2c 100644 --- a/test/Core.Test/KeyManagement/Commands/RegenerateUserAsymmetricKeysCommandTests.cs +++ b/test/Core.Test/KeyManagement/Commands/RegenerateUserAsymmetricKeysCommandTests.cs @@ -56,6 +56,7 @@ await sutProvider.GetDependency() [BitAutoData(false, true, false)] [BitAutoData(false, true, true)] [BitAutoData(true, false, false)] + [BitAutoData(true, false, true)] [BitAutoData(true, true, false)] [BitAutoData(true, true, true)] public async Task RegenerateKeysAsync_UserIdMisMatch_NotFoundException( From ff8e01eb31a86f6e4741ed2b54992eba785dcac0 Mon Sep 17 00:00:00 2001 From: Thomas Avery Date: Wed, 13 Nov 2024 17:08:21 -0600 Subject: [PATCH 15/17] bump migration date --- ...rate.sql => 2024-11-13_00_AddUserAsymmetricKeysRegenerate.sql} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename util/Migrator/DbScripts/{2024-10-22_00_AddUserAsymmetricKeysRegenerate.sql => 2024-11-13_00_AddUserAsymmetricKeysRegenerate.sql} (100%) diff --git a/util/Migrator/DbScripts/2024-10-22_00_AddUserAsymmetricKeysRegenerate.sql b/util/Migrator/DbScripts/2024-11-13_00_AddUserAsymmetricKeysRegenerate.sql similarity index 100% rename from util/Migrator/DbScripts/2024-10-22_00_AddUserAsymmetricKeysRegenerate.sql rename to util/Migrator/DbScripts/2024-11-13_00_AddUserAsymmetricKeysRegenerate.sql From 6899e49bb5752c280e97342263200c052eabb02f Mon Sep 17 00:00:00 2001 From: Thomas Avery Date: Thu, 14 Nov 2024 17:00:44 -0600 Subject: [PATCH 16/17] Add push notification to sync new asymmetric keys to other devices --- .../Commands/RegenerateUserAsymmetricKeysCommand.cs | 6 ++++++ .../RegenerateUserAsymmetricKeysCommandTests.cs | 13 +++++++++++++ 2 files changed, 19 insertions(+) diff --git a/src/Core/KeyManagement/Commands/RegenerateUserAsymmetricKeysCommand.cs b/src/Core/KeyManagement/Commands/RegenerateUserAsymmetricKeysCommand.cs index ee6d70a95eb5..a54223f685b2 100644 --- a/src/Core/KeyManagement/Commands/RegenerateUserAsymmetricKeysCommand.cs +++ b/src/Core/KeyManagement/Commands/RegenerateUserAsymmetricKeysCommand.cs @@ -8,6 +8,7 @@ using Bit.Core.KeyManagement.Commands.Interfaces; using Bit.Core.KeyManagement.Models.Data; using Bit.Core.KeyManagement.Repositories; +using Bit.Core.Services; using Microsoft.Extensions.Logging; namespace Bit.Core.KeyManagement.Commands; @@ -17,15 +18,18 @@ public class RegenerateUserAsymmetricKeysCommand : IRegenerateUserAsymmetricKeys private readonly ICurrentContext _currentContext; private readonly ILogger _logger; private readonly IUserAsymmetricKeysRepository _userAsymmetricKeysRepository; + private readonly IPushNotificationService _pushService; public RegenerateUserAsymmetricKeysCommand( ICurrentContext currentContext, IUserAsymmetricKeysRepository userAsymmetricKeysRepository, + IPushNotificationService pushService, ILogger logger) { _currentContext = currentContext; _logger = logger; _userAsymmetricKeysRepository = userAsymmetricKeysRepository; + _pushService = pushService; } public async Task RegenerateKeysAsync(UserAsymmetricKeys userAsymmetricKeys, @@ -61,5 +65,7 @@ x.Status is EmergencyAccessStatusType.Confirmed or EmergencyAccessStatusType.Rec _logger.LogInformation( "User's asymmetric keys regenerated. UserId: {userId} OrganizationMembership: {inOrganizations} DesignatedEmergencyAccess: {hasDesignatedEmergencyAccess} DeviceType: {deviceType}", userAsymmetricKeys.UserId, inOrganizations, hasDesignatedEmergencyAccess, _currentContext.DeviceType); + + await _pushService.PushSyncSettingsAsync(userId.Value); } } diff --git a/test/Core.Test/KeyManagement/Commands/RegenerateUserAsymmetricKeysCommandTests.cs b/test/Core.Test/KeyManagement/Commands/RegenerateUserAsymmetricKeysCommandTests.cs index ce2b9afefb2c..3388956156e7 100644 --- a/test/Core.Test/KeyManagement/Commands/RegenerateUserAsymmetricKeysCommandTests.cs +++ b/test/Core.Test/KeyManagement/Commands/RegenerateUserAsymmetricKeysCommandTests.cs @@ -8,6 +8,7 @@ using Bit.Core.KeyManagement.Commands; using Bit.Core.KeyManagement.Models.Data; using Bit.Core.KeyManagement.Repositories; +using Bit.Core.Services; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; @@ -49,6 +50,9 @@ await sutProvider.Sut.RegenerateKeysAsync(userAsymmetricKeys, await sutProvider.GetDependency() .Received(1) .RegenerateUserAsymmetricKeysAsync(Arg.Is(userAsymmetricKeys)); + await sutProvider.GetDependency() + .Received(1) + .PushSyncSettingsAsync(Arg.Is(userAsymmetricKeys.UserId)); } [Theory] @@ -88,6 +92,9 @@ await Assert.ThrowsAsync(() => sutProvider.Sut.RegenerateKeys await sutProvider.GetDependency() .ReceivedWithAnyArgs(0) .RegenerateUserAsymmetricKeysAsync(Arg.Any()); + await sutProvider.GetDependency() + .ReceivedWithAnyArgs(0) + .PushSyncSettingsAsync(Arg.Any()); } [Theory] @@ -110,6 +117,9 @@ await Assert.ThrowsAsync(() => sutProvider.Sut.RegenerateKe await sutProvider.GetDependency() .ReceivedWithAnyArgs(0) .RegenerateUserAsymmetricKeysAsync(Arg.Any()); + await sutProvider.GetDependency() + .ReceivedWithAnyArgs(0) + .PushSyncSettingsAsync(Arg.Any()); } [Theory] @@ -134,6 +144,9 @@ await Assert.ThrowsAsync(() => sutProvider.Sut.RegenerateKe await sutProvider.GetDependency() .ReceivedWithAnyArgs(0) .RegenerateUserAsymmetricKeysAsync(Arg.Any()); + await sutProvider.GetDependency() + .ReceivedWithAnyArgs(0) + .PushSyncSettingsAsync(Arg.Any()); } private static ICollection CreateInOrganizationAccounts(Guid userId, From e38fb9000043ddf03dd2e64fe0306508381f6dab Mon Sep 17 00:00:00 2001 From: Thomas Avery Date: Thu, 21 Nov 2024 15:54:44 -0600 Subject: [PATCH 17/17] remove old migration --- ...-11-13_00_AddUserAsymmetricKeysRegenerate.sql | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 util/Migrator/DbScripts/2024-11-13_00_AddUserAsymmetricKeysRegenerate.sql diff --git a/util/Migrator/DbScripts/2024-11-13_00_AddUserAsymmetricKeysRegenerate.sql b/util/Migrator/DbScripts/2024-11-13_00_AddUserAsymmetricKeysRegenerate.sql deleted file mode 100644 index e1f5431145eb..000000000000 --- a/util/Migrator/DbScripts/2024-11-13_00_AddUserAsymmetricKeysRegenerate.sql +++ /dev/null @@ -1,16 +0,0 @@ -CREATE OR ALTER PROCEDURE [dbo].[UserAsymmetricKeys_Regenerate] - @UserId UNIQUEIDENTIFIER, - @PublicKey VARCHAR(MAX), - @PrivateKey VARCHAR(MAX) -AS -BEGIN - SET NOCOUNT ON - DECLARE @UtcNow DATETIME2(7) = GETUTCDATE(); - - UPDATE [dbo].[User] - SET [PublicKey] = @PublicKey, - [PrivateKey] = @PrivateKey, - [RevisionDate] = @UtcNow, - [AccountRevisionDate] = @UtcNow - WHERE [Id] = @UserId -END