Browse Source
* Add new RegenerateUserAsymmetricKeysCommand * add new command tests * Add regen controller * Add regen controller tests * add feature flag * Add push notification to sync new asymmetric keys to other devicespull/5159/head
11 changed files with 641 additions and 0 deletions
@ -0,0 +1,50 @@
@@ -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); |
||||
} |
||||
} |
||||
@ -0,0 +1,23 @@
@@ -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, |
||||
}; |
||||
} |
||||
} |
||||
@ -0,0 +1,13 @@
@@ -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<OrganizationUser> usersOrganizationAccounts, |
||||
ICollection<EmergencyAccessDetails> designatedEmergencyAccess); |
||||
} |
||||
@ -0,0 +1,71 @@
@@ -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<RegenerateUserAsymmetricKeysCommand> _logger; |
||||
private readonly IUserAsymmetricKeysRepository _userAsymmetricKeysRepository; |
||||
private readonly IPushNotificationService _pushService; |
||||
|
||||
public RegenerateUserAsymmetricKeysCommand( |
||||
ICurrentContext currentContext, |
||||
IUserAsymmetricKeysRepository userAsymmetricKeysRepository, |
||||
IPushNotificationService pushService, |
||||
ILogger<RegenerateUserAsymmetricKeysCommand> logger) |
||||
{ |
||||
_currentContext = currentContext; |
||||
_logger = logger; |
||||
_userAsymmetricKeysRepository = userAsymmetricKeysRepository; |
||||
_pushService = pushService; |
||||
} |
||||
|
||||
public async Task RegenerateKeysAsync(UserAsymmetricKeys userAsymmetricKeys, |
||||
ICollection<OrganizationUser> usersOrganizationAccounts, |
||||
ICollection<EmergencyAccessDetails> 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); |
||||
} |
||||
} |
||||
@ -0,0 +1,18 @@
@@ -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<IRegenerateUserAsymmetricKeysCommand, RegenerateUserAsymmetricKeysCommand>(); |
||||
} |
||||
} |
||||
@ -0,0 +1,164 @@
@@ -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<ApiApplicationFactory>, 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<IUserRepository>(); |
||||
_emergencyAccessRepository = _factory.GetService<IEmergencyAccessRepository>(); |
||||
_organizationUserRepository = _factory.GetService<IOrganizationUserRepository>(); |
||||
} |
||||
|
||||
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); |
||||
} |
||||
} |
||||
@ -0,0 +1,96 @@
@@ -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<AccountsKeyManagementController> sutProvider, |
||||
KeyRegenerationRequestModel data) |
||||
{ |
||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(Arg.Is(FeatureFlagKeys.PrivateKeyRegeneration)) |
||||
.Returns(false); |
||||
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).ReturnsNull(); |
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.RegenerateKeysAsync(data)); |
||||
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>().ReceivedWithAnyArgs(0) |
||||
.GetManyByUserAsync(Arg.Any<Guid>()); |
||||
await sutProvider.GetDependency<IEmergencyAccessRepository>().ReceivedWithAnyArgs(0) |
||||
.GetManyDetailsByGranteeIdAsync(Arg.Any<Guid>()); |
||||
await sutProvider.GetDependency<IRegenerateUserAsymmetricKeysCommand>().ReceivedWithAnyArgs(0) |
||||
.RegenerateKeysAsync(Arg.Any<UserAsymmetricKeys>(), |
||||
Arg.Any<ICollection<OrganizationUser>>(), |
||||
Arg.Any<ICollection<EmergencyAccessDetails>>()); |
||||
} |
||||
|
||||
[Theory] |
||||
[BitAutoData] |
||||
public async Task RegenerateKeysAsync_UserNull_Throws(SutProvider<AccountsKeyManagementController> sutProvider, |
||||
KeyRegenerationRequestModel data) |
||||
{ |
||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(Arg.Is(FeatureFlagKeys.PrivateKeyRegeneration)) |
||||
.Returns(true); |
||||
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).ReturnsNull(); |
||||
|
||||
await Assert.ThrowsAsync<UnauthorizedAccessException>(() => sutProvider.Sut.RegenerateKeysAsync(data)); |
||||
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>().ReceivedWithAnyArgs(0) |
||||
.GetManyByUserAsync(Arg.Any<Guid>()); |
||||
await sutProvider.GetDependency<IEmergencyAccessRepository>().ReceivedWithAnyArgs(0) |
||||
.GetManyDetailsByGranteeIdAsync(Arg.Any<Guid>()); |
||||
await sutProvider.GetDependency<IRegenerateUserAsymmetricKeysCommand>().ReceivedWithAnyArgs(0) |
||||
.RegenerateKeysAsync(Arg.Any<UserAsymmetricKeys>(), |
||||
Arg.Any<ICollection<OrganizationUser>>(), |
||||
Arg.Any<ICollection<EmergencyAccessDetails>>()); |
||||
} |
||||
|
||||
[Theory] |
||||
[BitAutoData] |
||||
public async Task RegenerateKeysAsync_Success(SutProvider<AccountsKeyManagementController> sutProvider, |
||||
KeyRegenerationRequestModel data, User user, ICollection<OrganizationUser> orgUsers, |
||||
ICollection<EmergencyAccessDetails> accessDetails) |
||||
{ |
||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(Arg.Is(FeatureFlagKeys.PrivateKeyRegeneration)) |
||||
.Returns(true); |
||||
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user); |
||||
sutProvider.GetDependency<IOrganizationUserRepository>().GetManyByUserAsync(Arg.Is(user.Id)).Returns(orgUsers); |
||||
sutProvider.GetDependency<IEmergencyAccessRepository>().GetManyDetailsByGranteeIdAsync(Arg.Is(user.Id)) |
||||
.Returns(accessDetails); |
||||
|
||||
await sutProvider.Sut.RegenerateKeysAsync(data); |
||||
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1) |
||||
.GetManyByUserAsync(Arg.Is(user.Id)); |
||||
await sutProvider.GetDependency<IEmergencyAccessRepository>().Received(1) |
||||
.GetManyDetailsByGranteeIdAsync(Arg.Is(user.Id)); |
||||
await sutProvider.GetDependency<IRegenerateUserAsymmetricKeysCommand>().Received(1) |
||||
.RegenerateKeysAsync( |
||||
Arg.Is<UserAsymmetricKeys>(u => |
||||
u.UserId == user.Id && u.PublicKey == data.UserPublicKey && |
||||
u.UserKeyEncryptedPrivateKey == data.UserKeyEncryptedUserPrivateKey), |
||||
Arg.Is(orgUsers), |
||||
Arg.Is(accessDetails)); |
||||
} |
||||
} |
||||
@ -0,0 +1,197 @@
@@ -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<RegenerateUserAsymmetricKeysCommand> sutProvider, |
||||
UserAsymmetricKeys userAsymmetricKeys) |
||||
{ |
||||
sutProvider.GetDependency<ICurrentContext>().UserId.ReturnsNullForAnyArgs(); |
||||
var usersOrganizationAccounts = new List<OrganizationUser>(); |
||||
var designatedEmergencyAccess = new List<EmergencyAccessDetails>(); |
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.RegenerateKeysAsync(userAsymmetricKeys, |
||||
usersOrganizationAccounts, designatedEmergencyAccess)); |
||||
} |
||||
|
||||
[Theory] |
||||
[BitAutoData] |
||||
public async Task RegenerateKeysAsync_UserHasNoSharedAccess_Success( |
||||
SutProvider<RegenerateUserAsymmetricKeysCommand> sutProvider, |
||||
UserAsymmetricKeys userAsymmetricKeys) |
||||
{ |
||||
sutProvider.GetDependency<ICurrentContext>().UserId.ReturnsForAnyArgs(userAsymmetricKeys.UserId); |
||||
var usersOrganizationAccounts = new List<OrganizationUser>(); |
||||
var designatedEmergencyAccess = new List<EmergencyAccessDetails>(); |
||||
|
||||
await sutProvider.Sut.RegenerateKeysAsync(userAsymmetricKeys, |
||||
usersOrganizationAccounts, designatedEmergencyAccess); |
||||
|
||||
await sutProvider.GetDependency<IUserAsymmetricKeysRepository>() |
||||
.Received(1) |
||||
.RegenerateUserAsymmetricKeysAsync(Arg.Is(userAsymmetricKeys)); |
||||
await sutProvider.GetDependency<IPushNotificationService>() |
||||
.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<RegenerateUserAsymmetricKeysCommand> sutProvider, |
||||
UserAsymmetricKeys userAsymmetricKeys, |
||||
ICollection<OrganizationUser> usersOrganizationAccounts, |
||||
ICollection<EmergencyAccessDetails> designatedEmergencyAccess) |
||||
{ |
||||
sutProvider.GetDependency<ICurrentContext>().UserId |
||||
.ReturnsForAnyArgs(userAsymmetricKeysMismatch ? new Guid() : userAsymmetricKeys.UserId); |
||||
|
||||
if (!orgMismatch) |
||||
{ |
||||
usersOrganizationAccounts = |
||||
SetupOrganizationUserAccounts(userAsymmetricKeys.UserId, usersOrganizationAccounts); |
||||
} |
||||
|
||||
if (!emergencyAccessMismatch) |
||||
{ |
||||
designatedEmergencyAccess = SetupEmergencyAccess(userAsymmetricKeys.UserId, designatedEmergencyAccess); |
||||
} |
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.RegenerateKeysAsync(userAsymmetricKeys, |
||||
usersOrganizationAccounts, designatedEmergencyAccess)); |
||||
|
||||
await sutProvider.GetDependency<IUserAsymmetricKeysRepository>() |
||||
.ReceivedWithAnyArgs(0) |
||||
.RegenerateUserAsymmetricKeysAsync(Arg.Any<UserAsymmetricKeys>()); |
||||
await sutProvider.GetDependency<IPushNotificationService>() |
||||
.ReceivedWithAnyArgs(0) |
||||
.PushSyncSettingsAsync(Arg.Any<Guid>()); |
||||
} |
||||
|
||||
[Theory] |
||||
[BitAutoData(OrganizationUserStatusType.Confirmed)] |
||||
[BitAutoData(OrganizationUserStatusType.Revoked)] |
||||
public async Task RegenerateKeysAsync_UserInOrganizations_BadRequestException( |
||||
OrganizationUserStatusType organizationUserStatus, |
||||
SutProvider<RegenerateUserAsymmetricKeysCommand> sutProvider, |
||||
UserAsymmetricKeys userAsymmetricKeys, |
||||
ICollection<OrganizationUser> usersOrganizationAccounts) |
||||
{ |
||||
sutProvider.GetDependency<ICurrentContext>().UserId.ReturnsForAnyArgs(userAsymmetricKeys.UserId); |
||||
usersOrganizationAccounts = CreateInOrganizationAccounts(userAsymmetricKeys.UserId, organizationUserStatus, |
||||
usersOrganizationAccounts); |
||||
var designatedEmergencyAccess = new List<EmergencyAccessDetails>(); |
||||
|
||||
await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.RegenerateKeysAsync(userAsymmetricKeys, |
||||
usersOrganizationAccounts, designatedEmergencyAccess)); |
||||
|
||||
await sutProvider.GetDependency<IUserAsymmetricKeysRepository>() |
||||
.ReceivedWithAnyArgs(0) |
||||
.RegenerateUserAsymmetricKeysAsync(Arg.Any<UserAsymmetricKeys>()); |
||||
await sutProvider.GetDependency<IPushNotificationService>() |
||||
.ReceivedWithAnyArgs(0) |
||||
.PushSyncSettingsAsync(Arg.Any<Guid>()); |
||||
} |
||||
|
||||
[Theory] |
||||
[BitAutoData(EmergencyAccessStatusType.Confirmed)] |
||||
[BitAutoData(EmergencyAccessStatusType.RecoveryApproved)] |
||||
[BitAutoData(EmergencyAccessStatusType.RecoveryInitiated)] |
||||
public async Task RegenerateKeysAsync_UserHasDesignatedEmergencyAccess_BadRequestException( |
||||
EmergencyAccessStatusType statusType, |
||||
SutProvider<RegenerateUserAsymmetricKeysCommand> sutProvider, |
||||
UserAsymmetricKeys userAsymmetricKeys, |
||||
ICollection<EmergencyAccessDetails> designatedEmergencyAccess) |
||||
{ |
||||
sutProvider.GetDependency<ICurrentContext>().UserId.ReturnsForAnyArgs(userAsymmetricKeys.UserId); |
||||
designatedEmergencyAccess = |
||||
CreateDesignatedEmergencyAccess(userAsymmetricKeys.UserId, statusType, designatedEmergencyAccess); |
||||
var usersOrganizationAccounts = new List<OrganizationUser>(); |
||||
|
||||
|
||||
await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.RegenerateKeysAsync(userAsymmetricKeys, |
||||
usersOrganizationAccounts, designatedEmergencyAccess)); |
||||
|
||||
await sutProvider.GetDependency<IUserAsymmetricKeysRepository>() |
||||
.ReceivedWithAnyArgs(0) |
||||
.RegenerateUserAsymmetricKeysAsync(Arg.Any<UserAsymmetricKeys>()); |
||||
await sutProvider.GetDependency<IPushNotificationService>() |
||||
.ReceivedWithAnyArgs(0) |
||||
.PushSyncSettingsAsync(Arg.Any<Guid>()); |
||||
} |
||||
|
||||
private static ICollection<OrganizationUser> CreateInOrganizationAccounts(Guid userId, |
||||
OrganizationUserStatusType organizationUserStatus, ICollection<OrganizationUser> organizationUserAccounts) |
||||
{ |
||||
foreach (var organizationUserAccount in organizationUserAccounts) |
||||
{ |
||||
organizationUserAccount.UserId = userId; |
||||
organizationUserAccount.Status = organizationUserStatus; |
||||
} |
||||
|
||||
return organizationUserAccounts; |
||||
} |
||||
|
||||
private static ICollection<EmergencyAccessDetails> CreateDesignatedEmergencyAccess(Guid userId, |
||||
EmergencyAccessStatusType status, ICollection<EmergencyAccessDetails> designatedEmergencyAccess) |
||||
{ |
||||
foreach (var designated in designatedEmergencyAccess) |
||||
{ |
||||
designated.GranteeId = userId; |
||||
designated.Status = status; |
||||
} |
||||
|
||||
return designatedEmergencyAccess; |
||||
} |
||||
|
||||
private static ICollection<OrganizationUser> SetupOrganizationUserAccounts(Guid userId, |
||||
ICollection<OrganizationUser> organizationUserAccounts) |
||||
{ |
||||
foreach (var organizationUserAccount in organizationUserAccounts) |
||||
{ |
||||
organizationUserAccount.UserId = userId; |
||||
} |
||||
|
||||
return organizationUserAccounts; |
||||
} |
||||
|
||||
private static ICollection<EmergencyAccessDetails> SetupEmergencyAccess(Guid userId, |
||||
ICollection<EmergencyAccessDetails> emergencyAccessDetails) |
||||
{ |
||||
foreach (var emergencyAccessDetail in emergencyAccessDetails) |
||||
{ |
||||
emergencyAccessDetail.GranteeId = userId; |
||||
} |
||||
|
||||
return emergencyAccessDetails; |
||||
} |
||||
} |
||||
Loading…
Reference in new issue