diff --git a/src/Api/KeyManagement/Controllers/AccountsKeyManagementController.cs b/src/Api/KeyManagement/Controllers/AccountsKeyManagementController.cs new file mode 100644 index 0000000000..b8d5e30949 --- /dev/null +++ b/src/Api/KeyManagement/Controllers/AccountsKeyManagementController.cs @@ -0,0 +1,50 @@ +#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; +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 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; + } + + [HttpPost("regenerate-keys")] + public async Task RegenerateKeysAsync([FromBody] KeyRegenerationRequestModel request) + { + 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); + 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 0000000000..495d13cccd --- /dev/null +++ b/src/Api/KeyManagement/Models/Requests/KeyRegenerationRequestModel.cs @@ -0,0 +1,23 @@ +#nullable enable +using Bit.Core.KeyManagement.Models.Data; +using Bit.Core.Utilities; + +namespace Bit.Api.KeyManagement.Models.Requests; + +public class KeyRegenerationRequestModel +{ + public required string UserPublicKey { get; set; } + + [EncryptedString] + public required string UserKeyEncryptedUserPrivateKey { get; set; } + + public UserAsymmetricKeys ToUserAsymmetricKeys(Guid userId) + { + return new UserAsymmetricKeys + { + UserId = userId, + PublicKey = UserPublicKey, + UserKeyEncryptedPrivateKey = UserKeyEncryptedUserPrivateKey, + }; + } +} diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index df0abfb4b9..2c315b2578 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -160,6 +160,7 @@ public static class FeatureFlagKeys public const string PM12443RemovePagingLogic = "pm-12443-remove-paging-logic"; public const string SelfHostLicenseRefactor = "pm-11516-self-host-license-refactor"; public const string PromoteProviderServiceUserTool = "pm-15128-promote-provider-service-user-tool"; + public const string PrivateKeyRegeneration = "pm-12241-private-key-regeneration"; public static List GetAllKeys() { diff --git a/src/Core/KeyManagement/Commands/Interfaces/IRegenerateUserAsymmetricKeysCommand.cs b/src/Core/KeyManagement/Commands/Interfaces/IRegenerateUserAsymmetricKeysCommand.cs new file mode 100644 index 0000000000..d7ad7e3959 --- /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 0000000000..a54223f685 --- /dev/null +++ b/src/Core/KeyManagement/Commands/RegenerateUserAsymmetricKeysCommand.cs @@ -0,0 +1,71 @@ +#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 Bit.Core.Services; +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; + 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, + 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); + + _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) + { + 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); + + await _pushService.PushSyncSettingsAsync(userId.Value); + } +} diff --git a/src/Core/KeyManagement/KeyManagementServiceCollectionExtensions.cs b/src/Core/KeyManagement/KeyManagementServiceCollectionExtensions.cs new file mode 100644 index 0000000000..102630c7e6 --- /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 7585739d82..c757f163e9 100644 --- a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs +++ b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs @@ -26,6 +26,7 @@ using Bit.Core.Enums; using Bit.Core.HostedServices; using Bit.Core.Identity; using Bit.Core.IdentityServer; +using Bit.Core.KeyManagement; using Bit.Core.NotificationHub; using Bit.Core.OrganizationFeatures; using Bit.Core.Repositories; @@ -120,6 +121,7 @@ public static class ServiceCollectionExtensions services.AddScoped(); services.AddVaultServices(); services.AddReportingServices(); + services.AddKeyManagementServices(); } public static void AddTokenizers(this IServiceCollection services) diff --git a/test/Api.IntegrationTest/Helpers/LoginHelper.cs b/test/Api.IntegrationTest/Helpers/LoginHelper.cs index d6ce911bd0..1f5eb725d9 100644 --- a/test/Api.IntegrationTest/Helpers/LoginHelper.cs +++ b/test/Api.IntegrationTest/Helpers/LoginHelper.cs @@ -16,6 +16,12 @@ public class LoginHelper _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 0000000000..ec7ca37460 --- /dev/null +++ b/test/Api.IntegrationTest/KeyManagement/Controllers/AccountsKeyManagementControllerTests.cs @@ -0,0 +1,164 @@ +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 IOrganizationUserRepository _organizationUserRepository; + private readonly ApiApplicationFactory _factory; + private readonly LoginHelper _loginHelper; + private readonly IUserRepository _userRepository; + private string _ownerEmail = null!; + + public AccountsKeyManagementControllerTests(ApiApplicationFactory factory) + { + _factory = factory; + _factory.UpdateConfiguration("globalSettings:launchDarkly:flagValues:pm-12241-private-key-regeneration", + "true"); + _client = factory.CreateClient(); + _loginHelper = new LoginHelper(_factory, _client); + _userRepository = _factory.GetService(); + _emergencyAccessRepository = _factory.GetService(); + _organizationUserRepository = _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_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) + { + request.UserKeyEncryptedUserPrivateKey = _mockEncryptedString; + + var response = await _client.PostAsJsonAsync("/accounts/key-management/regenerate-keys", request); + + Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); + } + + [Theory] + [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( + OrganizationUserStatusType organizationUserStatus, + EmergencyAccessStatusType? emergencyAccessStatus, + KeyRegenerationRequestModel request) + { + if (organizationUserStatus is OrganizationUserStatusType.Confirmed or OrganizationUserStatusType.Revoked) + { + await CreateOrganizationUserAsync(organizationUserStatus); + } + + if (emergencyAccessStatus != null) + { + await CreateDesignatedEmergencyAccessAsync(emergencyAccessStatus.Value); + } + + 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 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); + + 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 = emergencyAccessStatus, + 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 0000000000..2615697ad3 --- /dev/null +++ b/test/Api.Test/KeyManagement/Controllers/AccountsKeyManagementControllerTests.cs @@ -0,0 +1,96 @@ +#nullable enable +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; +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_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)); + + 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, 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); + + await sutProvider.GetDependency().Received(1) + .GetManyByUserAsync(Arg.Is(user.Id)); + await sutProvider.GetDependency().Received(1) + .GetManyDetailsByGranteeIdAsync(Arg.Is(user.Id)); + 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 new file mode 100644 index 0000000000..3388956156 --- /dev/null +++ b/test/Core.Test/KeyManagement/Commands/RegenerateUserAsymmetricKeysCommandTests.cs @@ -0,0 +1,197 @@ +#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.Core.Services; +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)); + await sutProvider.GetDependency() + .Received(1) + .PushSyncSettingsAsync(Arg.Is(userAsymmetricKeys.UserId)); + } + + [Theory] + [BitAutoData(false, false, true)] + [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( + 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()); + await sutProvider.GetDependency() + .ReceivedWithAnyArgs(0) + .PushSyncSettingsAsync(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()); + await sutProvider.GetDependency() + .ReceivedWithAnyArgs(0) + .PushSyncSettingsAsync(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()); + await sutProvider.GetDependency() + .ReceivedWithAnyArgs(0) + .PushSyncSettingsAsync(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; + } +}