mirror of
https://github.com/bitwarden/server.git
synced 2025-07-15 22:57:44 -05:00
Merge branch 'main' into PM-17830
This commit is contained in:
@ -2,12 +2,18 @@
|
||||
using Bit.Api.IntegrationTest.Factories;
|
||||
using Bit.Api.IntegrationTest.Helpers;
|
||||
using Bit.Api.KeyManagement.Models.Requests;
|
||||
using Bit.Api.Tools.Models.Request;
|
||||
using Bit.Api.Vault.Models;
|
||||
using Bit.Api.Vault.Models.Request;
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Vault.Enums;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Api.IntegrationTest.KeyManagement.Controllers;
|
||||
@ -23,6 +29,7 @@ public class AccountsKeyManagementControllerTests : IClassFixture<ApiApplication
|
||||
private readonly ApiApplicationFactory _factory;
|
||||
private readonly LoginHelper _loginHelper;
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly IPasswordHasher<User> _passwordHasher;
|
||||
private string _ownerEmail = null!;
|
||||
|
||||
public AccountsKeyManagementControllerTests(ApiApplicationFactory factory)
|
||||
@ -35,6 +42,7 @@ public class AccountsKeyManagementControllerTests : IClassFixture<ApiApplication
|
||||
_userRepository = _factory.GetService<IUserRepository>();
|
||||
_emergencyAccessRepository = _factory.GetService<IEmergencyAccessRepository>();
|
||||
_organizationUserRepository = _factory.GetService<IOrganizationUserRepository>();
|
||||
_passwordHasher = _factory.GetService<IPasswordHasher<User>>();
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
@ -161,4 +169,87 @@ public class AccountsKeyManagementControllerTests : IClassFixture<ApiApplication
|
||||
};
|
||||
await _emergencyAccessRepository.CreateAsync(emergencyAccess);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task RotateUserAccountKeysAsync_NotLoggedIn_Unauthorized(RotateUserAccountKeysAndDataRequestModel request)
|
||||
{
|
||||
var response = await _client.PostAsJsonAsync("/accounts/key-management/rotate-user-account-keys", request);
|
||||
|
||||
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task RotateUserAccountKeysAsync_Success(RotateUserAccountKeysAndDataRequestModel request)
|
||||
{
|
||||
await _loginHelper.LoginAsync(_ownerEmail);
|
||||
var user = await _userRepository.GetByEmailAsync(_ownerEmail);
|
||||
if (user == null)
|
||||
{
|
||||
throw new InvalidOperationException("User not found.");
|
||||
}
|
||||
|
||||
var password = _passwordHasher.HashPassword(user, "newMasterPassword");
|
||||
user.MasterPassword = password;
|
||||
user.PublicKey = "publicKey";
|
||||
await _userRepository.ReplaceAsync(user);
|
||||
|
||||
request.AccountUnlockData.MasterPasswordUnlockData.KdfType = user.Kdf;
|
||||
request.AccountUnlockData.MasterPasswordUnlockData.KdfIterations = user.KdfIterations;
|
||||
request.AccountUnlockData.MasterPasswordUnlockData.KdfMemory = user.KdfMemory;
|
||||
request.AccountUnlockData.MasterPasswordUnlockData.KdfParallelism = user.KdfParallelism;
|
||||
request.AccountUnlockData.MasterPasswordUnlockData.Email = user.Email;
|
||||
request.AccountKeys.AccountPublicKey = "publicKey";
|
||||
request.AccountKeys.UserKeyEncryptedAccountPrivateKey = _mockEncryptedString;
|
||||
|
||||
request.OldMasterKeyAuthenticationHash = "newMasterPassword";
|
||||
|
||||
request.AccountData.Ciphers =
|
||||
[
|
||||
new CipherWithIdRequestModel
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Type = CipherType.Login,
|
||||
Name = _mockEncryptedString,
|
||||
Login = new CipherLoginModel
|
||||
{
|
||||
Username = _mockEncryptedString,
|
||||
Password = _mockEncryptedString,
|
||||
},
|
||||
},
|
||||
];
|
||||
request.AccountData.Folders = [
|
||||
new FolderWithIdRequestModel
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Name = _mockEncryptedString,
|
||||
},
|
||||
];
|
||||
request.AccountData.Sends = [
|
||||
new SendWithIdRequestModel
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Name = _mockEncryptedString,
|
||||
Key = _mockEncryptedString,
|
||||
Disabled = false,
|
||||
DeletionDate = DateTime.UtcNow.AddDays(1),
|
||||
},
|
||||
];
|
||||
request.AccountUnlockData.MasterPasswordUnlockData.MasterKeyEncryptedUserKey = _mockEncryptedString;
|
||||
request.AccountUnlockData.PasskeyUnlockData = [];
|
||||
request.AccountUnlockData.EmergencyAccessUnlockData = [];
|
||||
request.AccountUnlockData.OrganizationAccountRecoveryUnlockData = [];
|
||||
|
||||
var response = await _client.PostAsJsonAsync("/accounts/key-management/rotate-user-account-keys", request);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var userNewState = await _userRepository.GetByEmailAsync(_ownerEmail);
|
||||
Assert.NotNull(userNewState);
|
||||
Assert.Equal(request.AccountUnlockData.MasterPasswordUnlockData.Email, userNewState.Email);
|
||||
Assert.Equal(request.AccountUnlockData.MasterPasswordUnlockData.KdfType, userNewState.Kdf);
|
||||
Assert.Equal(request.AccountUnlockData.MasterPasswordUnlockData.KdfIterations, userNewState.KdfIterations);
|
||||
Assert.Equal(request.AccountUnlockData.MasterPasswordUnlockData.KdfMemory, userNewState.KdfMemory);
|
||||
Assert.Equal(request.AccountUnlockData.MasterPasswordUnlockData.KdfParallelism, userNewState.KdfParallelism);
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,8 @@ using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Auth.Repositories;
|
||||
@ -424,4 +426,93 @@ public class OrganizationUsersControllerTests
|
||||
.GetManyDetailsByOrganizationAsync(organizationAbility.Id, Arg.Any<bool>(), Arg.Any<bool>())
|
||||
.Returns(organizationUsers);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task Accept_WhenOrganizationUsePoliciesIsEnabledAndResetPolicyIsEnabled_WithPolicyRequirementsEnabled_ShouldHandleResetPassword(Guid orgId, Guid orgUserId,
|
||||
OrganizationUserAcceptRequestModel model, User user, SutProvider<OrganizationUsersController> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var applicationCacheService = sutProvider.GetDependency<IApplicationCacheService>();
|
||||
applicationCacheService.GetOrganizationAbilityAsync(orgId).Returns(new OrganizationAbility { UsePolicies = true });
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true);
|
||||
|
||||
var policy = new Policy
|
||||
{
|
||||
Enabled = true,
|
||||
Data = CoreHelpers.ClassToJsonData(new ResetPasswordDataModel { AutoEnrollEnabled = true, }),
|
||||
};
|
||||
var userService = sutProvider.GetDependency<IUserService>();
|
||||
userService.GetUserByPrincipalAsync(default).ReturnsForAnyArgs(user);
|
||||
|
||||
var policyRequirementQuery = sutProvider.GetDependency<IPolicyRequirementQuery>();
|
||||
|
||||
var policyRepository = sutProvider.GetDependency<IPolicyRepository>();
|
||||
|
||||
var policyRequirement = new ResetPasswordPolicyRequirement { AutoEnrollOrganizations = [orgId] };
|
||||
|
||||
policyRequirementQuery.GetAsync<ResetPasswordPolicyRequirement>(user.Id).Returns(policyRequirement);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.Accept(orgId, orgUserId, model);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IAcceptOrgUserCommand>().Received(1)
|
||||
.AcceptOrgUserByEmailTokenAsync(orgUserId, user, model.Token, userService);
|
||||
await sutProvider.GetDependency<IOrganizationService>().Received(1)
|
||||
.UpdateUserResetPasswordEnrollmentAsync(orgId, user.Id, model.ResetPasswordKey, user.Id);
|
||||
|
||||
await userService.Received(1).GetUserByPrincipalAsync(default);
|
||||
await applicationCacheService.Received(0).GetOrganizationAbilityAsync(orgId);
|
||||
await policyRepository.Received(0).GetByOrganizationIdTypeAsync(orgId, PolicyType.ResetPassword);
|
||||
await policyRequirementQuery.Received(1).GetAsync<ResetPasswordPolicyRequirement>(user.Id);
|
||||
Assert.True(policyRequirement.AutoEnrollEnabled(orgId));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task Accept_WithInvalidModelResetPasswordKey_WithPolicyRequirementsEnabled_ThrowsBadRequestException(Guid orgId, Guid orgUserId,
|
||||
OrganizationUserAcceptRequestModel model, User user, SutProvider<OrganizationUsersController> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
model.ResetPasswordKey = " ";
|
||||
var applicationCacheService = sutProvider.GetDependency<IApplicationCacheService>();
|
||||
applicationCacheService.GetOrganizationAbilityAsync(orgId).Returns(new OrganizationAbility { UsePolicies = true });
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true);
|
||||
|
||||
var policy = new Policy
|
||||
{
|
||||
Enabled = true,
|
||||
Data = CoreHelpers.ClassToJsonData(new ResetPasswordDataModel { AutoEnrollEnabled = true, }),
|
||||
};
|
||||
var userService = sutProvider.GetDependency<IUserService>();
|
||||
userService.GetUserByPrincipalAsync(default).ReturnsForAnyArgs(user);
|
||||
|
||||
var policyRepository = sutProvider.GetDependency<IPolicyRepository>();
|
||||
|
||||
var policyRequirementQuery = sutProvider.GetDependency<IPolicyRequirementQuery>();
|
||||
|
||||
var policyRequirement = new ResetPasswordPolicyRequirement { AutoEnrollOrganizations = [orgId] };
|
||||
|
||||
policyRequirementQuery.GetAsync<ResetPasswordPolicyRequirement>(user.Id).Returns(policyRequirement);
|
||||
|
||||
// Act
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
|
||||
sutProvider.Sut.Accept(orgId, orgUserId, model));
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IAcceptOrgUserCommand>().Received(0)
|
||||
.AcceptOrgUserByEmailTokenAsync(orgUserId, user, model.Token, userService);
|
||||
await sutProvider.GetDependency<IOrganizationService>().Received(0)
|
||||
.UpdateUserResetPasswordEnrollmentAsync(orgId, user.Id, model.ResetPasswordKey, user.Id);
|
||||
|
||||
await userService.Received(1).GetUserByPrincipalAsync(default);
|
||||
await applicationCacheService.Received(0).GetOrganizationAbilityAsync(orgId);
|
||||
await policyRepository.Received(0).GetByOrganizationIdTypeAsync(orgId, PolicyType.ResetPassword);
|
||||
await policyRequirementQuery.Received(1).GetAsync<ResetPasswordPolicyRequirement>(user.Id);
|
||||
|
||||
Assert.Equal("Master Password reset is required, but not provided.", exception.Message);
|
||||
}
|
||||
}
|
||||
|
@ -4,12 +4,15 @@ using Bit.Api.AdminConsole.Controllers;
|
||||
using Bit.Api.Auth.Models.Request.Accounts;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.AdminConsole.Models.Business.Tokenables;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationApiKeys.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Auth.Enums;
|
||||
@ -55,6 +58,7 @@ public class OrganizationsControllerTests : IDisposable
|
||||
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
|
||||
private readonly ICloudOrganizationSignUpCommand _cloudOrganizationSignUpCommand;
|
||||
private readonly IOrganizationDeleteCommand _organizationDeleteCommand;
|
||||
private readonly IPolicyRequirementQuery _policyRequirementQuery;
|
||||
private readonly IPricingClient _pricingClient;
|
||||
private readonly OrganizationsController _sut;
|
||||
|
||||
@ -80,6 +84,7 @@ public class OrganizationsControllerTests : IDisposable
|
||||
_removeOrganizationUserCommand = Substitute.For<IRemoveOrganizationUserCommand>();
|
||||
_cloudOrganizationSignUpCommand = Substitute.For<ICloudOrganizationSignUpCommand>();
|
||||
_organizationDeleteCommand = Substitute.For<IOrganizationDeleteCommand>();
|
||||
_policyRequirementQuery = Substitute.For<IPolicyRequirementQuery>();
|
||||
_pricingClient = Substitute.For<IPricingClient>();
|
||||
|
||||
_sut = new OrganizationsController(
|
||||
@ -103,6 +108,7 @@ public class OrganizationsControllerTests : IDisposable
|
||||
_removeOrganizationUserCommand,
|
||||
_cloudOrganizationSignUpCommand,
|
||||
_organizationDeleteCommand,
|
||||
_policyRequirementQuery,
|
||||
_pricingClient);
|
||||
}
|
||||
|
||||
@ -236,4 +242,55 @@ public class OrganizationsControllerTests : IDisposable
|
||||
|
||||
await _organizationDeleteCommand.Received(1).DeleteAsync(organization);
|
||||
}
|
||||
|
||||
[Theory, AutoData]
|
||||
public async Task GetAutoEnrollStatus_WithPolicyRequirementsEnabled_ReturnsOrganizationAutoEnrollStatus_WithResetPasswordEnabledTrue(
|
||||
User user,
|
||||
Organization organization,
|
||||
OrganizationUser organizationUser
|
||||
)
|
||||
{
|
||||
var policyRequirement = new ResetPasswordPolicyRequirement() { AutoEnrollOrganizations = [organization.Id] };
|
||||
|
||||
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
|
||||
_organizationRepository.GetByIdentifierAsync(organization.Id.ToString()).Returns(organization);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true);
|
||||
_organizationUserRepository.GetByOrganizationAsync(organization.Id, user.Id).Returns(organizationUser);
|
||||
_policyRequirementQuery.GetAsync<ResetPasswordPolicyRequirement>(user.Id).Returns(policyRequirement);
|
||||
|
||||
var result = await _sut.GetAutoEnrollStatus(organization.Id.ToString());
|
||||
|
||||
await _userService.Received(1).GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>());
|
||||
await _organizationRepository.Received(1).GetByIdentifierAsync(organization.Id.ToString());
|
||||
await _policyRequirementQuery.Received(1).GetAsync<ResetPasswordPolicyRequirement>(user.Id);
|
||||
|
||||
Assert.True(result.ResetPasswordEnabled);
|
||||
Assert.Equal(result.Id, organization.Id);
|
||||
}
|
||||
|
||||
[Theory, AutoData]
|
||||
public async Task GetAutoEnrollStatus_WithPolicyRequirementsDisabled_ReturnsOrganizationAutoEnrollStatus_WithResetPasswordEnabledTrue(
|
||||
User user,
|
||||
Organization organization,
|
||||
OrganizationUser organizationUser
|
||||
)
|
||||
{
|
||||
|
||||
var policy = new Policy() { Type = PolicyType.ResetPassword, Enabled = true, Data = "{\"AutoEnrollEnabled\": true}", OrganizationId = organization.Id };
|
||||
|
||||
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
|
||||
_organizationRepository.GetByIdentifierAsync(organization.Id.ToString()).Returns(organization);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(false);
|
||||
_organizationUserRepository.GetByOrganizationAsync(organization.Id, user.Id).Returns(organizationUser);
|
||||
_policyRepository.GetByOrganizationIdTypeAsync(organization.Id, PolicyType.ResetPassword).Returns(policy);
|
||||
|
||||
var result = await _sut.GetAutoEnrollStatus(organization.Id.ToString());
|
||||
|
||||
await _userService.Received(1).GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>());
|
||||
await _organizationRepository.Received(1).GetByIdentifierAsync(organization.Id.ToString());
|
||||
await _policyRequirementQuery.Received(0).GetAsync<ResetPasswordPolicyRequirement>(user.Id);
|
||||
await _policyRepository.Received(1).GetByOrganizationIdTypeAsync(organization.Id, PolicyType.ResetPassword);
|
||||
|
||||
Assert.True(result.ResetPasswordEnabled);
|
||||
}
|
||||
}
|
||||
|
@ -1,17 +1,28 @@
|
||||
#nullable enable
|
||||
using System.Security.Claims;
|
||||
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
||||
using Bit.Api.Auth.Models.Request;
|
||||
using Bit.Api.Auth.Models.Request.WebAuthn;
|
||||
using Bit.Api.KeyManagement.Controllers;
|
||||
using Bit.Api.KeyManagement.Models.Requests;
|
||||
using Bit.Api.KeyManagement.Validators;
|
||||
using Bit.Api.Tools.Models.Request;
|
||||
using Bit.Api.Vault.Models.Request;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Auth.Entities;
|
||||
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.KeyManagement.UserKey;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Tools.Entities;
|
||||
using Bit.Core.Vault.Entities;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using NSubstitute;
|
||||
using NSubstitute.ReturnsExtensions;
|
||||
using Xunit;
|
||||
@ -93,4 +104,78 @@ public class AccountsKeyManagementControllerTests
|
||||
Arg.Is(orgUsers),
|
||||
Arg.Is(accessDetails));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task RotateUserAccountKeysSuccess(SutProvider<AccountsKeyManagementController> sutProvider,
|
||||
RotateUserAccountKeysAndDataRequestModel data, User user)
|
||||
{
|
||||
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
|
||||
sutProvider.GetDependency<IRotateUserAccountKeysCommand>().RotateUserAccountKeysAsync(Arg.Any<User>(), Arg.Any<RotateUserAccountKeysData>())
|
||||
.Returns(IdentityResult.Success);
|
||||
await sutProvider.Sut.RotateUserAccountKeysAsync(data);
|
||||
|
||||
await sutProvider.GetDependency<IRotationValidator<IEnumerable<EmergencyAccessWithIdRequestModel>, IEnumerable<EmergencyAccess>>>().Received(1)
|
||||
.ValidateAsync(Arg.Any<User>(), Arg.Is(data.AccountUnlockData.EmergencyAccessUnlockData));
|
||||
await sutProvider.GetDependency<IRotationValidator<IEnumerable<ResetPasswordWithOrgIdRequestModel>, IReadOnlyList<OrganizationUser>>>().Received(1)
|
||||
.ValidateAsync(Arg.Any<User>(), Arg.Is(data.AccountUnlockData.OrganizationAccountRecoveryUnlockData));
|
||||
await sutProvider.GetDependency<IRotationValidator<IEnumerable<WebAuthnLoginRotateKeyRequestModel>, IEnumerable<WebAuthnLoginRotateKeyData>>>().Received(1)
|
||||
.ValidateAsync(Arg.Any<User>(), Arg.Is(data.AccountUnlockData.PasskeyUnlockData));
|
||||
|
||||
await sutProvider.GetDependency<IRotationValidator<IEnumerable<CipherWithIdRequestModel>, IEnumerable<Cipher>>>().Received(1)
|
||||
.ValidateAsync(Arg.Any<User>(), Arg.Is(data.AccountData.Ciphers));
|
||||
await sutProvider.GetDependency<IRotationValidator<IEnumerable<FolderWithIdRequestModel>, IEnumerable<Folder>>>().Received(1)
|
||||
.ValidateAsync(Arg.Any<User>(), Arg.Is(data.AccountData.Folders));
|
||||
await sutProvider.GetDependency<IRotationValidator<IEnumerable<SendWithIdRequestModel>, IReadOnlyList<Send>>>().Received(1)
|
||||
.ValidateAsync(Arg.Any<User>(), Arg.Is(data.AccountData.Sends));
|
||||
|
||||
await sutProvider.GetDependency<IRotateUserAccountKeysCommand>().Received(1)
|
||||
.RotateUserAccountKeysAsync(Arg.Is(user), Arg.Is<RotateUserAccountKeysData>(d =>
|
||||
d.OldMasterKeyAuthenticationHash == data.OldMasterKeyAuthenticationHash
|
||||
|
||||
&& d.MasterPasswordUnlockData.KdfType == data.AccountUnlockData.MasterPasswordUnlockData.KdfType
|
||||
&& d.MasterPasswordUnlockData.KdfIterations == data.AccountUnlockData.MasterPasswordUnlockData.KdfIterations
|
||||
&& d.MasterPasswordUnlockData.KdfMemory == data.AccountUnlockData.MasterPasswordUnlockData.KdfMemory
|
||||
&& d.MasterPasswordUnlockData.KdfParallelism == data.AccountUnlockData.MasterPasswordUnlockData.KdfParallelism
|
||||
&& d.MasterPasswordUnlockData.Email == data.AccountUnlockData.MasterPasswordUnlockData.Email
|
||||
|
||||
&& d.MasterPasswordUnlockData.MasterKeyAuthenticationHash == data.AccountUnlockData.MasterPasswordUnlockData.MasterKeyAuthenticationHash
|
||||
&& d.MasterPasswordUnlockData.MasterKeyEncryptedUserKey == data.AccountUnlockData.MasterPasswordUnlockData.MasterKeyEncryptedUserKey
|
||||
|
||||
&& d.AccountPublicKey == data.AccountKeys.AccountPublicKey
|
||||
&& d.UserKeyEncryptedAccountPrivateKey == data.AccountKeys.UserKeyEncryptedAccountPrivateKey
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task RotateUserKeyNoUser_Throws(SutProvider<AccountsKeyManagementController> sutProvider,
|
||||
RotateUserAccountKeysAndDataRequestModel data)
|
||||
{
|
||||
User? user = null;
|
||||
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
|
||||
sutProvider.GetDependency<IRotateUserAccountKeysCommand>().RotateUserAccountKeysAsync(Arg.Any<User>(), Arg.Any<RotateUserAccountKeysData>())
|
||||
.Returns(IdentityResult.Success);
|
||||
await Assert.ThrowsAsync<UnauthorizedAccessException>(() => sutProvider.Sut.RotateUserAccountKeysAsync(data));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task RotateUserKeyWrongData_Throws(SutProvider<AccountsKeyManagementController> sutProvider,
|
||||
RotateUserAccountKeysAndDataRequestModel data, User user, IdentityErrorDescriber _identityErrorDescriber)
|
||||
{
|
||||
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
|
||||
sutProvider.GetDependency<IRotateUserAccountKeysCommand>().RotateUserAccountKeysAsync(Arg.Any<User>(), Arg.Any<RotateUserAccountKeysData>())
|
||||
.Returns(IdentityResult.Failed(_identityErrorDescriber.PasswordMismatch()));
|
||||
try
|
||||
{
|
||||
await sutProvider.Sut.RotateUserAccountKeysAsync(data);
|
||||
Assert.Fail("Should have thrown");
|
||||
}
|
||||
catch (BadRequestException ex)
|
||||
{
|
||||
Assert.NotEmpty(ex.ModelState.Values);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,68 @@
|
||||
#nullable enable
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Bit.Api.Auth.Models.Request.Accounts;
|
||||
using Bit.Core.Enums;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Api.Test.KeyManagement.Models.Request;
|
||||
|
||||
public class MasterPasswordUnlockDataModelTests
|
||||
{
|
||||
|
||||
readonly string _mockEncryptedString = "2.3Uk+WNBIoU5xzmVFNcoWzz==|1MsPIYuRfdOHfu/0uY6H2Q==|/98sp4wb6pHP1VTZ9JcNCYgQjEUMFPlqJgCwRk1YXKg=";
|
||||
|
||||
[Theory]
|
||||
[InlineData(KdfType.PBKDF2_SHA256, 5000, null, null)]
|
||||
[InlineData(KdfType.PBKDF2_SHA256, 100000, null, null)]
|
||||
[InlineData(KdfType.PBKDF2_SHA256, 600000, null, null)]
|
||||
[InlineData(KdfType.Argon2id, 3, 64, 4)]
|
||||
public void Validate_Success(KdfType kdfType, int kdfIterations, int? kdfMemory, int? kdfParallelism)
|
||||
{
|
||||
var model = new MasterPasswordUnlockDataModel
|
||||
{
|
||||
KdfType = kdfType,
|
||||
KdfIterations = kdfIterations,
|
||||
KdfMemory = kdfMemory,
|
||||
KdfParallelism = kdfParallelism,
|
||||
Email = "example@example.com",
|
||||
MasterKeyAuthenticationHash = "hash",
|
||||
MasterKeyEncryptedUserKey = _mockEncryptedString,
|
||||
MasterPasswordHint = "hint"
|
||||
};
|
||||
var result = Validate(model);
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(KdfType.Argon2id, 1, null, 1)]
|
||||
[InlineData(KdfType.Argon2id, 1, 64, null)]
|
||||
[InlineData(KdfType.PBKDF2_SHA256, 5000, 0, null)]
|
||||
[InlineData(KdfType.PBKDF2_SHA256, 5000, null, 0)]
|
||||
[InlineData(KdfType.PBKDF2_SHA256, 5000, 0, 0)]
|
||||
[InlineData((KdfType)2, 100000, null, null)]
|
||||
[InlineData((KdfType)2, 2, 64, 4)]
|
||||
public void Validate_Failure(KdfType kdfType, int kdfIterations, int? kdfMemory, int? kdfParallelism)
|
||||
{
|
||||
var model = new MasterPasswordUnlockDataModel
|
||||
{
|
||||
KdfType = kdfType,
|
||||
KdfIterations = kdfIterations,
|
||||
KdfMemory = kdfMemory,
|
||||
KdfParallelism = kdfParallelism,
|
||||
Email = "example@example.com",
|
||||
MasterKeyAuthenticationHash = "hash",
|
||||
MasterKeyEncryptedUserKey = _mockEncryptedString,
|
||||
MasterPasswordHint = "hint"
|
||||
};
|
||||
var result = Validate(model);
|
||||
Assert.Single(result);
|
||||
Assert.NotNull(result.First().ErrorMessage);
|
||||
}
|
||||
|
||||
private static List<ValidationResult> Validate(MasterPasswordUnlockDataModel model)
|
||||
{
|
||||
var results = new List<ValidationResult>();
|
||||
Validator.TryValidateObject(model, new ValidationContext(model), results, true);
|
||||
return results;
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,324 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Test.AdminConsole.AutoFixture;
|
||||
using Bit.Core.Test.AutoFixture.OrganizationUserFixtures;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class ConfirmOrganizationUserCommandTests
|
||||
{
|
||||
[Theory, BitAutoData]
|
||||
public async Task ConfirmUserAsync_WithInvalidStatus_ThrowsBadRequestException(OrganizationUser confirmingUser,
|
||||
[OrganizationUser(OrganizationUserStatusType.Invited)] OrganizationUser orgUser, string key,
|
||||
SutProvider<ConfirmOrganizationUserCommand> sutProvider)
|
||||
{
|
||||
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||
|
||||
organizationUserRepository.GetByIdAsync(orgUser.Id).Returns(orgUser);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id));
|
||||
Assert.Contains("User not valid.", exception.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ConfirmUserAsync_WithWrongOrganization_ThrowsBadRequestException(OrganizationUser confirmingUser,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, string key,
|
||||
SutProvider<ConfirmOrganizationUserCommand> sutProvider)
|
||||
{
|
||||
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||
|
||||
organizationUserRepository.GetByIdAsync(orgUser.Id).Returns(orgUser);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.ConfirmUserAsync(confirmingUser.OrganizationId, orgUser.Id, key, confirmingUser.Id));
|
||||
Assert.Contains("User not valid.", exception.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(OrganizationUserType.Admin)]
|
||||
[BitAutoData(OrganizationUserType.Owner)]
|
||||
public async Task ConfirmUserAsync_ToFree_WithExistingAdminOrOwner_ThrowsBadRequestException(OrganizationUserType userType, Organization org, OrganizationUser confirmingUser,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user,
|
||||
string key, SutProvider<ConfirmOrganizationUserCommand> sutProvider)
|
||||
{
|
||||
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||
var userRepository = sutProvider.GetDependency<IUserRepository>();
|
||||
|
||||
org.PlanType = PlanType.Free;
|
||||
orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id;
|
||||
orgUser.UserId = user.Id;
|
||||
orgUser.Type = userType;
|
||||
organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser });
|
||||
organizationUserRepository.GetCountByFreeOrganizationAdminUserAsync(orgUser.UserId.Value).Returns(1);
|
||||
organizationRepository.GetByIdAsync(org.Id).Returns(org);
|
||||
userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user });
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id));
|
||||
Assert.Contains("User can only be an admin of one free organization.", exception.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(PlanType.Custom, OrganizationUserType.Admin)]
|
||||
[BitAutoData(PlanType.Custom, OrganizationUserType.Owner)]
|
||||
[BitAutoData(PlanType.EnterpriseAnnually, OrganizationUserType.Admin)]
|
||||
[BitAutoData(PlanType.EnterpriseAnnually, OrganizationUserType.Owner)]
|
||||
[BitAutoData(PlanType.EnterpriseAnnually2020, OrganizationUserType.Admin)]
|
||||
[BitAutoData(PlanType.EnterpriseAnnually2020, OrganizationUserType.Owner)]
|
||||
[BitAutoData(PlanType.EnterpriseAnnually2019, OrganizationUserType.Admin)]
|
||||
[BitAutoData(PlanType.EnterpriseAnnually2019, OrganizationUserType.Owner)]
|
||||
[BitAutoData(PlanType.EnterpriseMonthly, OrganizationUserType.Admin)]
|
||||
[BitAutoData(PlanType.EnterpriseMonthly, OrganizationUserType.Owner)]
|
||||
[BitAutoData(PlanType.EnterpriseMonthly2020, OrganizationUserType.Admin)]
|
||||
[BitAutoData(PlanType.EnterpriseMonthly2020, OrganizationUserType.Owner)]
|
||||
[BitAutoData(PlanType.EnterpriseMonthly2019, OrganizationUserType.Admin)]
|
||||
[BitAutoData(PlanType.EnterpriseMonthly2019, OrganizationUserType.Owner)]
|
||||
[BitAutoData(PlanType.FamiliesAnnually, OrganizationUserType.Admin)]
|
||||
[BitAutoData(PlanType.FamiliesAnnually, OrganizationUserType.Owner)]
|
||||
[BitAutoData(PlanType.FamiliesAnnually2019, OrganizationUserType.Admin)]
|
||||
[BitAutoData(PlanType.FamiliesAnnually2019, OrganizationUserType.Owner)]
|
||||
[BitAutoData(PlanType.TeamsAnnually, OrganizationUserType.Admin)]
|
||||
[BitAutoData(PlanType.TeamsAnnually, OrganizationUserType.Owner)]
|
||||
[BitAutoData(PlanType.TeamsAnnually2020, OrganizationUserType.Admin)]
|
||||
[BitAutoData(PlanType.TeamsAnnually2020, OrganizationUserType.Owner)]
|
||||
[BitAutoData(PlanType.TeamsAnnually2019, OrganizationUserType.Admin)]
|
||||
[BitAutoData(PlanType.TeamsAnnually2019, OrganizationUserType.Owner)]
|
||||
[BitAutoData(PlanType.TeamsMonthly, OrganizationUserType.Admin)]
|
||||
[BitAutoData(PlanType.TeamsMonthly, OrganizationUserType.Owner)]
|
||||
[BitAutoData(PlanType.TeamsMonthly2020, OrganizationUserType.Admin)]
|
||||
[BitAutoData(PlanType.TeamsMonthly2020, OrganizationUserType.Owner)]
|
||||
[BitAutoData(PlanType.TeamsMonthly2019, OrganizationUserType.Admin)]
|
||||
[BitAutoData(PlanType.TeamsMonthly2019, OrganizationUserType.Owner)]
|
||||
public async Task ConfirmUserAsync_ToNonFree_WithExistingFreeAdminOrOwner_Succeeds(PlanType planType, OrganizationUserType orgUserType, Organization org, OrganizationUser confirmingUser,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user,
|
||||
string key, SutProvider<ConfirmOrganizationUserCommand> sutProvider)
|
||||
{
|
||||
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||
var userRepository = sutProvider.GetDependency<IUserRepository>();
|
||||
|
||||
org.PlanType = planType;
|
||||
orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id;
|
||||
orgUser.UserId = user.Id;
|
||||
orgUser.Type = orgUserType;
|
||||
orgUser.AccessSecretsManager = false;
|
||||
organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser });
|
||||
organizationUserRepository.GetCountByFreeOrganizationAdminUserAsync(orgUser.UserId.Value).Returns(1);
|
||||
organizationRepository.GetByIdAsync(org.Id).Returns(org);
|
||||
userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user });
|
||||
|
||||
await sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id);
|
||||
|
||||
await sutProvider.GetDependency<IEventService>().Received(1).LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Confirmed);
|
||||
await sutProvider.GetDependency<IMailService>().Received(1).SendOrganizationConfirmedEmailAsync(org.DisplayName(), user.Email);
|
||||
await organizationUserRepository.Received(1).ReplaceManyAsync(Arg.Is<List<OrganizationUser>>(users => users.Contains(orgUser) && users.Count == 1));
|
||||
}
|
||||
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ConfirmUserAsync_AsUser_WithSingleOrgPolicyAppliedFromConfirmingOrg_ThrowsBadRequestException(Organization org, OrganizationUser confirmingUser,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user,
|
||||
OrganizationUser orgUserAnotherOrg, [OrganizationUserPolicyDetails(PolicyType.SingleOrg)] OrganizationUserPolicyDetails singleOrgPolicy,
|
||||
string key, SutProvider<ConfirmOrganizationUserCommand> sutProvider)
|
||||
{
|
||||
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||
var userRepository = sutProvider.GetDependency<IUserRepository>();
|
||||
var policyService = sutProvider.GetDependency<IPolicyService>();
|
||||
|
||||
org.PlanType = PlanType.EnterpriseAnnually;
|
||||
orgUser.Status = OrganizationUserStatusType.Accepted;
|
||||
orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id;
|
||||
orgUser.UserId = orgUserAnotherOrg.UserId = user.Id;
|
||||
organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser });
|
||||
organizationUserRepository.GetManyByManyUsersAsync(default).ReturnsForAnyArgs(new[] { orgUserAnotherOrg });
|
||||
organizationRepository.GetByIdAsync(org.Id).Returns(org);
|
||||
userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user });
|
||||
singleOrgPolicy.OrganizationId = org.Id;
|
||||
policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.SingleOrg).Returns(new[] { singleOrgPolicy });
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id));
|
||||
Assert.Contains("Cannot confirm this member to the organization until they leave or remove all other organizations.", exception.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ConfirmUserAsync_AsUser_WithSingleOrgPolicyAppliedFromOtherOrg_ThrowsBadRequestException(Organization org, OrganizationUser confirmingUser,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user,
|
||||
OrganizationUser orgUserAnotherOrg, [OrganizationUserPolicyDetails(PolicyType.SingleOrg)] OrganizationUserPolicyDetails singleOrgPolicy,
|
||||
string key, SutProvider<ConfirmOrganizationUserCommand> sutProvider)
|
||||
{
|
||||
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||
var userRepository = sutProvider.GetDependency<IUserRepository>();
|
||||
var policyService = sutProvider.GetDependency<IPolicyService>();
|
||||
|
||||
org.PlanType = PlanType.EnterpriseAnnually;
|
||||
orgUser.Status = OrganizationUserStatusType.Accepted;
|
||||
orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id;
|
||||
orgUser.UserId = orgUserAnotherOrg.UserId = user.Id;
|
||||
organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser });
|
||||
organizationUserRepository.GetManyByManyUsersAsync(default).ReturnsForAnyArgs(new[] { orgUserAnotherOrg });
|
||||
organizationRepository.GetByIdAsync(org.Id).Returns(org);
|
||||
userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user });
|
||||
singleOrgPolicy.OrganizationId = orgUserAnotherOrg.Id;
|
||||
policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.SingleOrg).Returns(new[] { singleOrgPolicy });
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id));
|
||||
Assert.Contains("Cannot confirm this member to the organization because they are in another organization which forbids it.", exception.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(OrganizationUserType.Admin)]
|
||||
[BitAutoData(OrganizationUserType.Owner)]
|
||||
public async Task ConfirmUserAsync_AsOwnerOrAdmin_WithSingleOrgPolicy_ExcludedViaUserType_Success(
|
||||
OrganizationUserType userType, Organization org, OrganizationUser confirmingUser,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user,
|
||||
OrganizationUser orgUserAnotherOrg,
|
||||
string key, SutProvider<ConfirmOrganizationUserCommand> sutProvider)
|
||||
{
|
||||
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||
var userRepository = sutProvider.GetDependency<IUserRepository>();
|
||||
|
||||
org.PlanType = PlanType.EnterpriseAnnually;
|
||||
orgUser.Type = userType;
|
||||
orgUser.Status = OrganizationUserStatusType.Accepted;
|
||||
orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id;
|
||||
orgUser.UserId = orgUserAnotherOrg.UserId = user.Id;
|
||||
orgUser.AccessSecretsManager = true;
|
||||
organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser });
|
||||
organizationUserRepository.GetManyByManyUsersAsync(default).ReturnsForAnyArgs(new[] { orgUserAnotherOrg });
|
||||
organizationRepository.GetByIdAsync(org.Id).Returns(org);
|
||||
userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user });
|
||||
|
||||
await sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id);
|
||||
|
||||
await sutProvider.GetDependency<IEventService>().Received(1).LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Confirmed);
|
||||
await sutProvider.GetDependency<IMailService>().Received(1).SendOrganizationConfirmedEmailAsync(org.DisplayName(), user.Email, true);
|
||||
await organizationUserRepository.Received(1).ReplaceManyAsync(Arg.Is<List<OrganizationUser>>(users => users.Contains(orgUser) && users.Count == 1));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ConfirmUserAsync_WithTwoFactorPolicyAndTwoFactorDisabled_ThrowsBadRequestException(Organization org, OrganizationUser confirmingUser,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user,
|
||||
OrganizationUser orgUserAnotherOrg,
|
||||
[OrganizationUserPolicyDetails(PolicyType.TwoFactorAuthentication)] OrganizationUserPolicyDetails twoFactorPolicy,
|
||||
string key, SutProvider<ConfirmOrganizationUserCommand> sutProvider)
|
||||
{
|
||||
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||
var userRepository = sutProvider.GetDependency<IUserRepository>();
|
||||
var policyService = sutProvider.GetDependency<IPolicyService>();
|
||||
var twoFactorIsEnabledQuery = sutProvider.GetDependency<ITwoFactorIsEnabledQuery>();
|
||||
|
||||
org.PlanType = PlanType.EnterpriseAnnually;
|
||||
orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id;
|
||||
orgUser.UserId = orgUserAnotherOrg.UserId = user.Id;
|
||||
organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser });
|
||||
organizationUserRepository.GetManyByManyUsersAsync(default).ReturnsForAnyArgs(new[] { orgUserAnotherOrg });
|
||||
organizationRepository.GetByIdAsync(org.Id).Returns(org);
|
||||
userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user });
|
||||
twoFactorPolicy.OrganizationId = org.Id;
|
||||
policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication).Returns(new[] { twoFactorPolicy });
|
||||
twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(user.Id)))
|
||||
.Returns(new List<(Guid userId, bool twoFactorIsEnabled)>() { (user.Id, false) });
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id));
|
||||
Assert.Contains("User does not have two-step login enabled.", exception.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ConfirmUserAsync_WithTwoFactorPolicyAndTwoFactorEnabled_Succeeds(Organization org, OrganizationUser confirmingUser,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user,
|
||||
[OrganizationUserPolicyDetails(PolicyType.TwoFactorAuthentication)] OrganizationUserPolicyDetails twoFactorPolicy,
|
||||
string key, SutProvider<ConfirmOrganizationUserCommand> sutProvider)
|
||||
{
|
||||
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||
var userRepository = sutProvider.GetDependency<IUserRepository>();
|
||||
var policyService = sutProvider.GetDependency<IPolicyService>();
|
||||
var twoFactorIsEnabledQuery = sutProvider.GetDependency<ITwoFactorIsEnabledQuery>();
|
||||
|
||||
org.PlanType = PlanType.EnterpriseAnnually;
|
||||
orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id;
|
||||
orgUser.UserId = user.Id;
|
||||
organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser });
|
||||
organizationRepository.GetByIdAsync(org.Id).Returns(org);
|
||||
userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user });
|
||||
twoFactorPolicy.OrganizationId = org.Id;
|
||||
policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication).Returns(new[] { twoFactorPolicy });
|
||||
twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(user.Id)))
|
||||
.Returns(new List<(Guid userId, bool twoFactorIsEnabled)>() { (user.Id, true) });
|
||||
|
||||
await sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ConfirmUsersAsync_WithMultipleUsers_ReturnsExpectedMixedResults(Organization org,
|
||||
OrganizationUser confirmingUser,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser1,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser2,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser3,
|
||||
OrganizationUser anotherOrgUser, User user1, User user2, User user3,
|
||||
[OrganizationUserPolicyDetails(PolicyType.TwoFactorAuthentication)] OrganizationUserPolicyDetails twoFactorPolicy,
|
||||
[OrganizationUserPolicyDetails(PolicyType.SingleOrg)] OrganizationUserPolicyDetails singleOrgPolicy,
|
||||
string key, SutProvider<ConfirmOrganizationUserCommand> sutProvider)
|
||||
{
|
||||
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||
var userRepository = sutProvider.GetDependency<IUserRepository>();
|
||||
var policyService = sutProvider.GetDependency<IPolicyService>();
|
||||
var twoFactorIsEnabledQuery = sutProvider.GetDependency<ITwoFactorIsEnabledQuery>();
|
||||
|
||||
org.PlanType = PlanType.EnterpriseAnnually;
|
||||
orgUser1.OrganizationId = orgUser2.OrganizationId = orgUser3.OrganizationId = confirmingUser.OrganizationId = org.Id;
|
||||
orgUser1.UserId = user1.Id;
|
||||
orgUser2.UserId = user2.Id;
|
||||
orgUser3.UserId = user3.Id;
|
||||
anotherOrgUser.UserId = user3.Id;
|
||||
var orgUsers = new[] { orgUser1, orgUser2, orgUser3 };
|
||||
organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(orgUsers);
|
||||
organizationRepository.GetByIdAsync(org.Id).Returns(org);
|
||||
userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user1, user2, user3 });
|
||||
twoFactorPolicy.OrganizationId = org.Id;
|
||||
policyService.GetPoliciesApplicableToUserAsync(Arg.Any<Guid>(), PolicyType.TwoFactorAuthentication).Returns(new[] { twoFactorPolicy });
|
||||
twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(user1.Id) && ids.Contains(user2.Id) && ids.Contains(user3.Id)))
|
||||
.Returns(new List<(Guid userId, bool twoFactorIsEnabled)>()
|
||||
{
|
||||
(user1.Id, true),
|
||||
(user2.Id, false),
|
||||
(user3.Id, true)
|
||||
});
|
||||
singleOrgPolicy.OrganizationId = org.Id;
|
||||
policyService.GetPoliciesApplicableToUserAsync(user3.Id, PolicyType.SingleOrg)
|
||||
.Returns(new[] { singleOrgPolicy });
|
||||
organizationUserRepository.GetManyByManyUsersAsync(default)
|
||||
.ReturnsForAnyArgs(new[] { orgUser1, orgUser2, orgUser3, anotherOrgUser });
|
||||
|
||||
var keys = orgUsers.ToDictionary(ou => ou.Id, _ => key);
|
||||
var result = await sutProvider.Sut.ConfirmUsersAsync(confirmingUser.OrganizationId, keys, confirmingUser.Id);
|
||||
Assert.Contains("", result[0].Item2);
|
||||
Assert.Contains("User does not have two-step login enabled.", result[1].Item2);
|
||||
Assert.Contains("Cannot confirm this member to the organization until they leave or remove all other organizations.", result[2].Item2);
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
using Bit.Core.Test.AdminConsole.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class PersonalOwnershipPolicyRequirementFactoryTests
|
||||
{
|
||||
[Theory, BitAutoData]
|
||||
public void DisablePersonalOwnership_WithNoPolicies_ReturnsFalse(SutProvider<PersonalOwnershipPolicyRequirementFactory> sutProvider)
|
||||
{
|
||||
var actual = sutProvider.Sut.Create([]);
|
||||
|
||||
Assert.False(actual.DisablePersonalOwnership);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void DisablePersonalOwnership_WithPersonalOwnershipPolicies_ReturnsTrue(
|
||||
[PolicyDetails(PolicyType.PersonalOwnership)] PolicyDetails[] policies,
|
||||
SutProvider<PersonalOwnershipPolicyRequirementFactory> sutProvider)
|
||||
{
|
||||
var actual = sutProvider.Sut.Create(policies);
|
||||
|
||||
Assert.True(actual.DisablePersonalOwnership);
|
||||
}
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
using Bit.Core.Test.AdminConsole.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class ResetPasswordPolicyRequirementFactoryTests
|
||||
{
|
||||
[Theory, BitAutoData]
|
||||
public void AutoEnroll_WithNoPolicies_IsEmpty(SutProvider<ResetPasswordPolicyRequirementFactory> sutProvider, Guid orgId)
|
||||
{
|
||||
var actual = sutProvider.Sut.Create([]);
|
||||
|
||||
Assert.False(actual.AutoEnrollEnabled(orgId));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void AutoEnrollAdministration_WithAnyResetPasswordPolices_ReturnsEnabledOrganizationIds(
|
||||
[PolicyDetails(PolicyType.ResetPassword)] PolicyDetails[] policies,
|
||||
SutProvider<ResetPasswordPolicyRequirementFactory> sutProvider)
|
||||
{
|
||||
policies[0].SetDataModel(new ResetPasswordDataModel { AutoEnrollEnabled = true });
|
||||
policies[1].SetDataModel(new ResetPasswordDataModel { AutoEnrollEnabled = false });
|
||||
policies[2].SetDataModel(new ResetPasswordDataModel { AutoEnrollEnabled = true });
|
||||
|
||||
var actual = sutProvider.Sut.Create(policies);
|
||||
|
||||
Assert.True(actual.AutoEnrollEnabled(policies[0].OrganizationId));
|
||||
Assert.False(actual.AutoEnrollEnabled(policies[1].OrganizationId));
|
||||
Assert.True(actual.AutoEnrollEnabled(policies[2].OrganizationId));
|
||||
}
|
||||
}
|
@ -24,7 +24,6 @@ using Bit.Core.Platform.Push;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Test.AdminConsole.AutoFixture;
|
||||
using Bit.Core.Test.AutoFixture.OrganizationFixtures;
|
||||
using Bit.Core.Test.AutoFixture.OrganizationUserFixtures;
|
||||
using Bit.Core.Tokens;
|
||||
@ -978,306 +977,6 @@ OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
|
||||
sutProvider.GetDependency<ICurrentContext>().ManageUsers(organization.Id).Returns(true);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ConfirmUser_InvalidStatus(OrganizationUser confirmingUser,
|
||||
[OrganizationUser(OrganizationUserStatusType.Invited)] OrganizationUser orgUser, string key,
|
||||
SutProvider<OrganizationService> sutProvider)
|
||||
{
|
||||
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||
|
||||
organizationUserRepository.GetByIdAsync(orgUser.Id).Returns(orgUser);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id));
|
||||
Assert.Contains("User not valid.", exception.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ConfirmUser_WrongOrganization(OrganizationUser confirmingUser,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, string key,
|
||||
SutProvider<OrganizationService> sutProvider)
|
||||
{
|
||||
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||
|
||||
organizationUserRepository.GetByIdAsync(orgUser.Id).Returns(orgUser);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.ConfirmUserAsync(confirmingUser.OrganizationId, orgUser.Id, key, confirmingUser.Id));
|
||||
Assert.Contains("User not valid.", exception.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(OrganizationUserType.Admin)]
|
||||
[BitAutoData(OrganizationUserType.Owner)]
|
||||
public async Task ConfirmUserToFree_AlreadyFreeAdminOrOwner_Throws(OrganizationUserType userType, Organization org, OrganizationUser confirmingUser,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user,
|
||||
string key, SutProvider<OrganizationService> sutProvider)
|
||||
{
|
||||
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||
var userRepository = sutProvider.GetDependency<IUserRepository>();
|
||||
|
||||
org.PlanType = PlanType.Free;
|
||||
orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id;
|
||||
orgUser.UserId = user.Id;
|
||||
orgUser.Type = userType;
|
||||
organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser });
|
||||
organizationUserRepository.GetCountByFreeOrganizationAdminUserAsync(orgUser.UserId.Value).Returns(1);
|
||||
organizationRepository.GetByIdAsync(org.Id).Returns(org);
|
||||
userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user });
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id));
|
||||
Assert.Contains("User can only be an admin of one free organization.", exception.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(PlanType.Custom, OrganizationUserType.Admin)]
|
||||
[BitAutoData(PlanType.Custom, OrganizationUserType.Owner)]
|
||||
[BitAutoData(PlanType.EnterpriseAnnually, OrganizationUserType.Admin)]
|
||||
[BitAutoData(PlanType.EnterpriseAnnually, OrganizationUserType.Owner)]
|
||||
[BitAutoData(PlanType.EnterpriseAnnually2020, OrganizationUserType.Admin)]
|
||||
[BitAutoData(PlanType.EnterpriseAnnually2020, OrganizationUserType.Owner)]
|
||||
[BitAutoData(PlanType.EnterpriseAnnually2019, OrganizationUserType.Admin)]
|
||||
[BitAutoData(PlanType.EnterpriseAnnually2019, OrganizationUserType.Owner)]
|
||||
[BitAutoData(PlanType.EnterpriseMonthly, OrganizationUserType.Admin)]
|
||||
[BitAutoData(PlanType.EnterpriseMonthly, OrganizationUserType.Owner)]
|
||||
[BitAutoData(PlanType.EnterpriseMonthly2020, OrganizationUserType.Admin)]
|
||||
[BitAutoData(PlanType.EnterpriseMonthly2020, OrganizationUserType.Owner)]
|
||||
[BitAutoData(PlanType.EnterpriseMonthly2019, OrganizationUserType.Admin)]
|
||||
[BitAutoData(PlanType.EnterpriseMonthly2019, OrganizationUserType.Owner)]
|
||||
[BitAutoData(PlanType.FamiliesAnnually, OrganizationUserType.Admin)]
|
||||
[BitAutoData(PlanType.FamiliesAnnually, OrganizationUserType.Owner)]
|
||||
[BitAutoData(PlanType.FamiliesAnnually2019, OrganizationUserType.Admin)]
|
||||
[BitAutoData(PlanType.FamiliesAnnually2019, OrganizationUserType.Owner)]
|
||||
[BitAutoData(PlanType.TeamsAnnually, OrganizationUserType.Admin)]
|
||||
[BitAutoData(PlanType.TeamsAnnually, OrganizationUserType.Owner)]
|
||||
[BitAutoData(PlanType.TeamsAnnually2020, OrganizationUserType.Admin)]
|
||||
[BitAutoData(PlanType.TeamsAnnually2020, OrganizationUserType.Owner)]
|
||||
[BitAutoData(PlanType.TeamsAnnually2019, OrganizationUserType.Admin)]
|
||||
[BitAutoData(PlanType.TeamsAnnually2019, OrganizationUserType.Owner)]
|
||||
[BitAutoData(PlanType.TeamsMonthly, OrganizationUserType.Admin)]
|
||||
[BitAutoData(PlanType.TeamsMonthly, OrganizationUserType.Owner)]
|
||||
[BitAutoData(PlanType.TeamsMonthly2020, OrganizationUserType.Admin)]
|
||||
[BitAutoData(PlanType.TeamsMonthly2020, OrganizationUserType.Owner)]
|
||||
[BitAutoData(PlanType.TeamsMonthly2019, OrganizationUserType.Admin)]
|
||||
[BitAutoData(PlanType.TeamsMonthly2019, OrganizationUserType.Owner)]
|
||||
public async Task ConfirmUserToNonFree_AlreadyFreeAdminOrOwner_DoesNotThrow(PlanType planType, OrganizationUserType orgUserType, Organization org, OrganizationUser confirmingUser,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user,
|
||||
string key, SutProvider<OrganizationService> sutProvider)
|
||||
{
|
||||
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||
var userRepository = sutProvider.GetDependency<IUserRepository>();
|
||||
|
||||
org.PlanType = planType;
|
||||
orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id;
|
||||
orgUser.UserId = user.Id;
|
||||
orgUser.Type = orgUserType;
|
||||
orgUser.AccessSecretsManager = false;
|
||||
organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser });
|
||||
organizationUserRepository.GetCountByFreeOrganizationAdminUserAsync(orgUser.UserId.Value).Returns(1);
|
||||
organizationRepository.GetByIdAsync(org.Id).Returns(org);
|
||||
userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user });
|
||||
|
||||
await sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id);
|
||||
|
||||
await sutProvider.GetDependency<IEventService>().Received(1).LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Confirmed);
|
||||
await sutProvider.GetDependency<IMailService>().Received(1).SendOrganizationConfirmedEmailAsync(org.DisplayName(), user.Email);
|
||||
await organizationUserRepository.Received(1).ReplaceManyAsync(Arg.Is<List<OrganizationUser>>(users => users.Contains(orgUser) && users.Count == 1));
|
||||
}
|
||||
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ConfirmUser_AsUser_SingleOrgPolicy_AppliedFromConfirmingOrg_Throws(Organization org, OrganizationUser confirmingUser,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user,
|
||||
OrganizationUser orgUserAnotherOrg, [OrganizationUserPolicyDetails(PolicyType.SingleOrg)] OrganizationUserPolicyDetails singleOrgPolicy,
|
||||
string key, SutProvider<OrganizationService> sutProvider)
|
||||
{
|
||||
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||
var userRepository = sutProvider.GetDependency<IUserRepository>();
|
||||
var policyService = sutProvider.GetDependency<IPolicyService>();
|
||||
|
||||
org.PlanType = PlanType.EnterpriseAnnually;
|
||||
orgUser.Status = OrganizationUserStatusType.Accepted;
|
||||
orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id;
|
||||
orgUser.UserId = orgUserAnotherOrg.UserId = user.Id;
|
||||
organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser });
|
||||
organizationUserRepository.GetManyByManyUsersAsync(default).ReturnsForAnyArgs(new[] { orgUserAnotherOrg });
|
||||
organizationRepository.GetByIdAsync(org.Id).Returns(org);
|
||||
userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user });
|
||||
singleOrgPolicy.OrganizationId = org.Id;
|
||||
policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.SingleOrg).Returns(new[] { singleOrgPolicy });
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id));
|
||||
Assert.Contains("Cannot confirm this member to the organization until they leave or remove all other organizations.", exception.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ConfirmUser_AsUser_SingleOrgPolicy_AppliedFromOtherOrg_Throws(Organization org, OrganizationUser confirmingUser,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user,
|
||||
OrganizationUser orgUserAnotherOrg, [OrganizationUserPolicyDetails(PolicyType.SingleOrg)] OrganizationUserPolicyDetails singleOrgPolicy,
|
||||
string key, SutProvider<OrganizationService> sutProvider)
|
||||
{
|
||||
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||
var userRepository = sutProvider.GetDependency<IUserRepository>();
|
||||
var policyService = sutProvider.GetDependency<IPolicyService>();
|
||||
|
||||
org.PlanType = PlanType.EnterpriseAnnually;
|
||||
orgUser.Status = OrganizationUserStatusType.Accepted;
|
||||
orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id;
|
||||
orgUser.UserId = orgUserAnotherOrg.UserId = user.Id;
|
||||
organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser });
|
||||
organizationUserRepository.GetManyByManyUsersAsync(default).ReturnsForAnyArgs(new[] { orgUserAnotherOrg });
|
||||
organizationRepository.GetByIdAsync(org.Id).Returns(org);
|
||||
userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user });
|
||||
singleOrgPolicy.OrganizationId = orgUserAnotherOrg.Id;
|
||||
policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.SingleOrg).Returns(new[] { singleOrgPolicy });
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id));
|
||||
Assert.Contains("Cannot confirm this member to the organization because they are in another organization which forbids it.", exception.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(OrganizationUserType.Admin)]
|
||||
[BitAutoData(OrganizationUserType.Owner)]
|
||||
public async Task ConfirmUser_AsOwnerOrAdmin_SingleOrgPolicy_ExcludedViaUserType_Success(
|
||||
OrganizationUserType userType, Organization org, OrganizationUser confirmingUser,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user,
|
||||
OrganizationUser orgUserAnotherOrg,
|
||||
string key, SutProvider<OrganizationService> sutProvider)
|
||||
{
|
||||
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||
var userRepository = sutProvider.GetDependency<IUserRepository>();
|
||||
|
||||
org.PlanType = PlanType.EnterpriseAnnually;
|
||||
orgUser.Type = userType;
|
||||
orgUser.Status = OrganizationUserStatusType.Accepted;
|
||||
orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id;
|
||||
orgUser.UserId = orgUserAnotherOrg.UserId = user.Id;
|
||||
orgUser.AccessSecretsManager = true;
|
||||
organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser });
|
||||
organizationUserRepository.GetManyByManyUsersAsync(default).ReturnsForAnyArgs(new[] { orgUserAnotherOrg });
|
||||
organizationRepository.GetByIdAsync(org.Id).Returns(org);
|
||||
userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user });
|
||||
|
||||
await sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id);
|
||||
|
||||
await sutProvider.GetDependency<IEventService>().Received(1).LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Confirmed);
|
||||
await sutProvider.GetDependency<IMailService>().Received(1).SendOrganizationConfirmedEmailAsync(org.DisplayName(), user.Email, true);
|
||||
await organizationUserRepository.Received(1).ReplaceManyAsync(Arg.Is<List<OrganizationUser>>(users => users.Contains(orgUser) && users.Count == 1));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ConfirmUser_TwoFactorPolicy_NotEnabled_Throws(Organization org, OrganizationUser confirmingUser,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user,
|
||||
OrganizationUser orgUserAnotherOrg,
|
||||
[OrganizationUserPolicyDetails(PolicyType.TwoFactorAuthentication)] OrganizationUserPolicyDetails twoFactorPolicy,
|
||||
string key, SutProvider<OrganizationService> sutProvider)
|
||||
{
|
||||
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||
var userRepository = sutProvider.GetDependency<IUserRepository>();
|
||||
var policyService = sutProvider.GetDependency<IPolicyService>();
|
||||
var twoFactorIsEnabledQuery = sutProvider.GetDependency<ITwoFactorIsEnabledQuery>();
|
||||
|
||||
org.PlanType = PlanType.EnterpriseAnnually;
|
||||
orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id;
|
||||
orgUser.UserId = orgUserAnotherOrg.UserId = user.Id;
|
||||
organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser });
|
||||
organizationUserRepository.GetManyByManyUsersAsync(default).ReturnsForAnyArgs(new[] { orgUserAnotherOrg });
|
||||
organizationRepository.GetByIdAsync(org.Id).Returns(org);
|
||||
userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user });
|
||||
twoFactorPolicy.OrganizationId = org.Id;
|
||||
policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication).Returns(new[] { twoFactorPolicy });
|
||||
twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(user.Id)))
|
||||
.Returns(new List<(Guid userId, bool twoFactorIsEnabled)>() { (user.Id, false) });
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id));
|
||||
Assert.Contains("User does not have two-step login enabled.", exception.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ConfirmUser_TwoFactorPolicy_Enabled_Success(Organization org, OrganizationUser confirmingUser,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user,
|
||||
[OrganizationUserPolicyDetails(PolicyType.TwoFactorAuthentication)] OrganizationUserPolicyDetails twoFactorPolicy,
|
||||
string key, SutProvider<OrganizationService> sutProvider)
|
||||
{
|
||||
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||
var userRepository = sutProvider.GetDependency<IUserRepository>();
|
||||
var policyService = sutProvider.GetDependency<IPolicyService>();
|
||||
var twoFactorIsEnabledQuery = sutProvider.GetDependency<ITwoFactorIsEnabledQuery>();
|
||||
|
||||
org.PlanType = PlanType.EnterpriseAnnually;
|
||||
orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id;
|
||||
orgUser.UserId = user.Id;
|
||||
organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser });
|
||||
organizationRepository.GetByIdAsync(org.Id).Returns(org);
|
||||
userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user });
|
||||
twoFactorPolicy.OrganizationId = org.Id;
|
||||
policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication).Returns(new[] { twoFactorPolicy });
|
||||
twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(user.Id)))
|
||||
.Returns(new List<(Guid userId, bool twoFactorIsEnabled)>() { (user.Id, true) });
|
||||
|
||||
await sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ConfirmUsers_Success(Organization org,
|
||||
OrganizationUser confirmingUser,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser1,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser2,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser3,
|
||||
OrganizationUser anotherOrgUser, User user1, User user2, User user3,
|
||||
[OrganizationUserPolicyDetails(PolicyType.TwoFactorAuthentication)] OrganizationUserPolicyDetails twoFactorPolicy,
|
||||
[OrganizationUserPolicyDetails(PolicyType.SingleOrg)] OrganizationUserPolicyDetails singleOrgPolicy,
|
||||
string key, SutProvider<OrganizationService> sutProvider)
|
||||
{
|
||||
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||
var userRepository = sutProvider.GetDependency<IUserRepository>();
|
||||
var policyService = sutProvider.GetDependency<IPolicyService>();
|
||||
var twoFactorIsEnabledQuery = sutProvider.GetDependency<ITwoFactorIsEnabledQuery>();
|
||||
|
||||
org.PlanType = PlanType.EnterpriseAnnually;
|
||||
orgUser1.OrganizationId = orgUser2.OrganizationId = orgUser3.OrganizationId = confirmingUser.OrganizationId = org.Id;
|
||||
orgUser1.UserId = user1.Id;
|
||||
orgUser2.UserId = user2.Id;
|
||||
orgUser3.UserId = user3.Id;
|
||||
anotherOrgUser.UserId = user3.Id;
|
||||
var orgUsers = new[] { orgUser1, orgUser2, orgUser3 };
|
||||
organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(orgUsers);
|
||||
organizationRepository.GetByIdAsync(org.Id).Returns(org);
|
||||
userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user1, user2, user3 });
|
||||
twoFactorPolicy.OrganizationId = org.Id;
|
||||
policyService.GetPoliciesApplicableToUserAsync(Arg.Any<Guid>(), PolicyType.TwoFactorAuthentication).Returns(new[] { twoFactorPolicy });
|
||||
twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(user1.Id) && ids.Contains(user2.Id) && ids.Contains(user3.Id)))
|
||||
.Returns(new List<(Guid userId, bool twoFactorIsEnabled)>()
|
||||
{
|
||||
(user1.Id, true),
|
||||
(user2.Id, false),
|
||||
(user3.Id, true)
|
||||
});
|
||||
singleOrgPolicy.OrganizationId = org.Id;
|
||||
policyService.GetPoliciesApplicableToUserAsync(user3.Id, PolicyType.SingleOrg)
|
||||
.Returns(new[] { singleOrgPolicy });
|
||||
organizationUserRepository.GetManyByManyUsersAsync(default)
|
||||
.ReturnsForAnyArgs(new[] { orgUser1, orgUser2, orgUser3, anotherOrgUser });
|
||||
|
||||
var keys = orgUsers.ToDictionary(ou => ou.Id, _ => key);
|
||||
var result = await sutProvider.Sut.ConfirmUsersAsync(confirmingUser.OrganizationId, keys, confirmingUser.Id);
|
||||
Assert.Contains("", result[0].Item2);
|
||||
Assert.Contains("User does not have two-step login enabled.", result[1].Item2);
|
||||
Assert.Contains("Cannot confirm this member to the organization until they leave or remove all other organizations.", result[2].Item2);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateOrganizationKeysAsync_WithoutManageResetPassword_Throws(Guid orgId, string publicKey,
|
||||
string privateKey, SutProvider<OrganizationService> sutProvider)
|
||||
|
58
test/Core.Test/AdminConsole/Shared/IValidatorTests.cs
Normal file
58
test/Core.Test/AdminConsole/Shared/IValidatorTests.cs
Normal file
@ -0,0 +1,58 @@
|
||||
using Bit.Core.AdminConsole.Errors;
|
||||
using Bit.Core.AdminConsole.Shared.Validation;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.AdminConsole.Shared;
|
||||
|
||||
public class IValidatorTests
|
||||
{
|
||||
public class TestClass
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public record InvalidRequestError<T>(T ErroredValue) : Error<T>(Code, ErroredValue)
|
||||
{
|
||||
public const string Code = "InvalidRequest";
|
||||
}
|
||||
|
||||
public class TestClassValidator : IValidator<TestClass>
|
||||
{
|
||||
public Task<ValidationResult<TestClass>> ValidateAsync(TestClass value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value.Name))
|
||||
{
|
||||
return Task.FromResult<ValidationResult<TestClass>>(new Invalid<TestClass>
|
||||
{
|
||||
Errors = [new InvalidRequestError<TestClass>(value)]
|
||||
});
|
||||
}
|
||||
|
||||
return Task.FromResult<ValidationResult<TestClass>>(new Valid<TestClass> { Value = value });
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ValidateAsync_WhenSomethingIsInvalid_ReturnsInvalidWithError()
|
||||
{
|
||||
var example = new TestClass();
|
||||
|
||||
var result = await new TestClassValidator().ValidateAsync(example);
|
||||
|
||||
Assert.IsType<Invalid<TestClass>>(result);
|
||||
var invalidResult = result as Invalid<TestClass>;
|
||||
Assert.Equal(InvalidRequestError<TestClass>.Code, invalidResult.Errors.First().Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ValidateAsync_WhenIsValid_ReturnsValid()
|
||||
{
|
||||
var example = new TestClass { Name = "Valid" };
|
||||
|
||||
var result = await new TestClassValidator().ValidateAsync(example);
|
||||
|
||||
Assert.IsType<Valid<TestClass>>(result);
|
||||
var validResult = result as Valid<TestClass>;
|
||||
Assert.Equal(example.Name, validResult.Value.Name);
|
||||
}
|
||||
}
|
@ -0,0 +1,120 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.KeyManagement.Models.Data;
|
||||
using Bit.Core.KeyManagement.UserKey.Implementations;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.KeyManagement.UserKey;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class RotateUserAccountKeysCommandTests
|
||||
{
|
||||
[Theory, BitAutoData]
|
||||
public async Task RejectsWrongOldMasterPassword(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user,
|
||||
RotateUserAccountKeysData model)
|
||||
{
|
||||
user.Email = model.MasterPasswordUnlockData.Email;
|
||||
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(user, model.OldMasterKeyAuthenticationHash)
|
||||
.Returns(false);
|
||||
|
||||
var result = await sutProvider.Sut.RotateUserAccountKeysAsync(user, model);
|
||||
|
||||
Assert.NotEqual(IdentityResult.Success, result);
|
||||
}
|
||||
[Theory, BitAutoData]
|
||||
public async Task ThrowsWhenUserIsNull(SutProvider<RotateUserAccountKeysCommand> sutProvider,
|
||||
RotateUserAccountKeysData model)
|
||||
{
|
||||
await Assert.ThrowsAsync<ArgumentNullException>(async () => await sutProvider.Sut.RotateUserAccountKeysAsync(null, model));
|
||||
}
|
||||
[Theory, BitAutoData]
|
||||
public async Task RejectsEmailChange(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user,
|
||||
RotateUserAccountKeysData model)
|
||||
{
|
||||
user.Kdf = Enums.KdfType.Argon2id;
|
||||
user.KdfIterations = 3;
|
||||
user.KdfMemory = 64;
|
||||
user.KdfParallelism = 4;
|
||||
|
||||
model.MasterPasswordUnlockData.Email = user.Email + ".different-domain";
|
||||
model.MasterPasswordUnlockData.KdfType = Enums.KdfType.Argon2id;
|
||||
model.MasterPasswordUnlockData.KdfIterations = 3;
|
||||
model.MasterPasswordUnlockData.KdfMemory = 64;
|
||||
model.MasterPasswordUnlockData.KdfParallelism = 4;
|
||||
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(user, model.OldMasterKeyAuthenticationHash)
|
||||
.Returns(true);
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(async () => await sutProvider.Sut.RotateUserAccountKeysAsync(user, model));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RejectsKdfChange(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user,
|
||||
RotateUserAccountKeysData model)
|
||||
{
|
||||
user.Kdf = Enums.KdfType.Argon2id;
|
||||
user.KdfIterations = 3;
|
||||
user.KdfMemory = 64;
|
||||
user.KdfParallelism = 4;
|
||||
|
||||
model.MasterPasswordUnlockData.Email = user.Email;
|
||||
model.MasterPasswordUnlockData.KdfType = Enums.KdfType.PBKDF2_SHA256;
|
||||
model.MasterPasswordUnlockData.KdfIterations = 600000;
|
||||
model.MasterPasswordUnlockData.KdfMemory = null;
|
||||
model.MasterPasswordUnlockData.KdfParallelism = null;
|
||||
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(user, model.OldMasterKeyAuthenticationHash)
|
||||
.Returns(true);
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(async () => await sutProvider.Sut.RotateUserAccountKeysAsync(user, model));
|
||||
}
|
||||
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RejectsPublicKeyChange(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user,
|
||||
RotateUserAccountKeysData model)
|
||||
{
|
||||
user.PublicKey = "old-public";
|
||||
user.Kdf = Enums.KdfType.Argon2id;
|
||||
user.KdfIterations = 3;
|
||||
user.KdfMemory = 64;
|
||||
user.KdfParallelism = 4;
|
||||
|
||||
model.AccountPublicKey = "new-public";
|
||||
model.MasterPasswordUnlockData.Email = user.Email;
|
||||
model.MasterPasswordUnlockData.KdfType = Enums.KdfType.Argon2id;
|
||||
model.MasterPasswordUnlockData.KdfIterations = 3;
|
||||
model.MasterPasswordUnlockData.KdfMemory = 64;
|
||||
model.MasterPasswordUnlockData.KdfParallelism = 4;
|
||||
|
||||
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(user, model.OldMasterKeyAuthenticationHash)
|
||||
.Returns(true);
|
||||
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(async () => await sutProvider.Sut.RotateUserAccountKeysAsync(user, model));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RotatesCorrectly(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user,
|
||||
RotateUserAccountKeysData model)
|
||||
{
|
||||
user.Kdf = Enums.KdfType.Argon2id;
|
||||
user.KdfIterations = 3;
|
||||
user.KdfMemory = 64;
|
||||
user.KdfParallelism = 4;
|
||||
|
||||
model.MasterPasswordUnlockData.Email = user.Email;
|
||||
model.MasterPasswordUnlockData.KdfType = Enums.KdfType.Argon2id;
|
||||
model.MasterPasswordUnlockData.KdfIterations = 3;
|
||||
model.MasterPasswordUnlockData.KdfMemory = 64;
|
||||
model.MasterPasswordUnlockData.KdfParallelism = 4;
|
||||
|
||||
model.AccountPublicKey = user.PublicKey;
|
||||
|
||||
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(user, model.OldMasterKeyAuthenticationHash)
|
||||
.Returns(true);
|
||||
|
||||
var result = await sutProvider.Sut.RotateUserAccountKeysAsync(user, model);
|
||||
|
||||
Assert.Equal(IdentityResult.Success, result);
|
||||
}
|
||||
}
|
@ -11,7 +11,7 @@ using Microsoft.AspNetCore.Identity;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.KeyManagement.UserFeatures.UserKey;
|
||||
namespace Bit.Core.Test.KeyManagement.UserKey;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class RotateUserKeyCommandTests
|
||||
|
53
test/Core.Test/Models/Commands/CommandResultTests.cs
Normal file
53
test/Core.Test/Models/Commands/CommandResultTests.cs
Normal file
@ -0,0 +1,53 @@
|
||||
using Bit.Core.AdminConsole.Errors;
|
||||
using Bit.Core.Models.Commands;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Models.Commands;
|
||||
|
||||
public class CommandResultTests
|
||||
{
|
||||
public class TestItem
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public string Value { get; set; }
|
||||
}
|
||||
|
||||
public CommandResult<TestItem> BulkAction(IEnumerable<TestItem> items)
|
||||
{
|
||||
var itemList = items.ToList();
|
||||
var successfulItems = items.Where(x => x.Value == "SuccessfulRequest").ToArray();
|
||||
|
||||
var failedItems = itemList.Except(successfulItems).ToArray();
|
||||
|
||||
var notFound = failedItems.First(x => x.Value == "Failed due to not found");
|
||||
var invalidPermissions = failedItems.First(x => x.Value == "Failed due to invalid permissions");
|
||||
|
||||
var notFoundError = new RecordNotFoundError<TestItem>(notFound);
|
||||
var insufficientPermissionsError = new InsufficientPermissionsError<TestItem>(invalidPermissions);
|
||||
|
||||
return new Partial<TestItem>(successfulItems.ToArray(), [notFoundError, insufficientPermissionsError]);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void Partial_CommandResult_BulkRequestWithSuccessAndFailures(Guid successId1, Guid failureId1, Guid failureId2)
|
||||
{
|
||||
var listOfRecords = new List<TestItem>
|
||||
{
|
||||
new TestItem() { Id = successId1, Value = "SuccessfulRequest" },
|
||||
new TestItem() { Id = failureId1, Value = "Failed due to not found" },
|
||||
new TestItem() { Id = failureId2, Value = "Failed due to invalid permissions" }
|
||||
};
|
||||
|
||||
var result = BulkAction(listOfRecords);
|
||||
|
||||
Assert.IsType<Partial<TestItem>>(result);
|
||||
|
||||
var failures = (result as Partial<TestItem>).Failures.ToArray();
|
||||
var success = (result as Partial<TestItem>).Successes.First();
|
||||
|
||||
Assert.Equal(listOfRecords.First(), success);
|
||||
Assert.Equal(2, failures.Length);
|
||||
}
|
||||
}
|
@ -1,10 +1,13 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Platform.Push;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Test.AutoFixture.CipherFixtures;
|
||||
using Bit.Core.Tools.Enums;
|
||||
using Bit.Core.Tools.ImportFeatures;
|
||||
@ -18,7 +21,6 @@ using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
|
||||
namespace Bit.Core.Test.Tools.ImportFeatures;
|
||||
|
||||
[UserCipherCustomize]
|
||||
@ -51,6 +53,34 @@ public class ImportCiphersAsyncCommandTests
|
||||
await sutProvider.GetDependency<IPushNotificationService>().Received(1).PushSyncVaultAsync(importingUserId);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ImportIntoIndividualVaultAsync_WithPolicyRequirementsEnabled_WithDisablePersonalOwnershipPolicyDisabled_Success(
|
||||
Guid importingUserId,
|
||||
List<CipherDetails> ciphers,
|
||||
SutProvider<ImportCiphersCommand> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.PolicyRequirements)
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||
.GetAsync<PersonalOwnershipPolicyRequirement>(importingUserId)
|
||||
.Returns(new PersonalOwnershipPolicyRequirement { DisablePersonalOwnership = false });
|
||||
|
||||
sutProvider.GetDependency<IFolderRepository>()
|
||||
.GetManyByUserIdAsync(importingUserId)
|
||||
.Returns(new List<Folder>());
|
||||
|
||||
var folders = new List<Folder> { new Folder { UserId = importingUserId } };
|
||||
|
||||
var folderRelationships = new List<KeyValuePair<int, int>>();
|
||||
|
||||
await sutProvider.Sut.ImportIntoIndividualVaultAsync(folders, ciphers, folderRelationships, importingUserId);
|
||||
|
||||
await sutProvider.GetDependency<ICipherRepository>().Received(1).CreateAsync(ciphers, Arg.Any<List<Folder>>());
|
||||
await sutProvider.GetDependency<IPushNotificationService>().Received(1).PushSyncVaultAsync(importingUserId);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ImportIntoIndividualVaultAsync_ThrowsBadRequestException(
|
||||
List<Folder> folders,
|
||||
@ -73,6 +103,32 @@ public class ImportCiphersAsyncCommandTests
|
||||
Assert.Equal("You cannot import items into your personal vault because you are a member of an organization which forbids it.", exception.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ImportIntoIndividualVaultAsync_WithPolicyRequirementsEnabled_WithDisablePersonalOwnershipPolicyEnabled_ThrowsBadRequestException(
|
||||
List<Folder> folders,
|
||||
List<CipherDetails> ciphers,
|
||||
SutProvider<ImportCiphersCommand> sutProvider)
|
||||
{
|
||||
var userId = Guid.NewGuid();
|
||||
folders.ForEach(f => f.UserId = userId);
|
||||
ciphers.ForEach(c => c.UserId = userId);
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.PolicyRequirements)
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||
.GetAsync<PersonalOwnershipPolicyRequirement>(userId)
|
||||
.Returns(new PersonalOwnershipPolicyRequirement { DisablePersonalOwnership = true });
|
||||
|
||||
var folderRelationships = new List<KeyValuePair<int, int>>();
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
|
||||
sutProvider.Sut.ImportIntoIndividualVaultAsync(folders, ciphers, folderRelationships, userId));
|
||||
|
||||
Assert.Equal("You cannot import items into your personal vault because you are a member of an organization which forbids it.", exception.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ImportIntoOrganizationalVaultAsync_Success(
|
||||
Organization organization,
|
||||
|
@ -1,4 +1,9 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using System.Text.Json;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
@ -9,7 +14,9 @@ using Bit.Core.Services;
|
||||
using Bit.Core.Test.AutoFixture.CipherFixtures;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Core.Vault.Entities;
|
||||
using Bit.Core.Vault.Enums;
|
||||
using Bit.Core.Vault.Models.Data;
|
||||
using Bit.Core.Vault.Queries;
|
||||
using Bit.Core.Vault.Repositories;
|
||||
using Bit.Core.Vault.Services;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
@ -104,6 +111,98 @@ public class CipherServiceTests
|
||||
await sutProvider.GetDependency<ICipherRepository>().Received(1).ReplaceAsync(cipherDetails);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task SaveDetailsAsync_PersonalVault_WithDisablePersonalOwnershipPolicyEnabled_Throws(
|
||||
SutProvider<CipherService> sutProvider,
|
||||
CipherDetails cipher,
|
||||
Guid savingUserId)
|
||||
{
|
||||
cipher.Id = default;
|
||||
cipher.UserId = savingUserId;
|
||||
cipher.OrganizationId = null;
|
||||
|
||||
sutProvider.GetDependency<IPolicyService>()
|
||||
.AnyPoliciesApplicableToUserAsync(savingUserId, PolicyType.PersonalOwnership)
|
||||
.Returns(true);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.SaveDetailsAsync(cipher, savingUserId, null));
|
||||
Assert.Contains("restricted from saving items to your personal vault", exception.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task SaveDetailsAsync_PersonalVault_WithDisablePersonalOwnershipPolicyDisabled_Succeeds(
|
||||
SutProvider<CipherService> sutProvider,
|
||||
CipherDetails cipher,
|
||||
Guid savingUserId)
|
||||
{
|
||||
cipher.Id = default;
|
||||
cipher.UserId = savingUserId;
|
||||
cipher.OrganizationId = null;
|
||||
|
||||
sutProvider.GetDependency<IPolicyService>()
|
||||
.AnyPoliciesApplicableToUserAsync(savingUserId, PolicyType.PersonalOwnership)
|
||||
.Returns(false);
|
||||
|
||||
await sutProvider.Sut.SaveDetailsAsync(cipher, savingUserId, null);
|
||||
|
||||
await sutProvider.GetDependency<ICipherRepository>()
|
||||
.Received(1)
|
||||
.CreateAsync(cipher);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task SaveDetailsAsync_PersonalVault_WithPolicyRequirementsEnabled_WithDisablePersonalOwnershipPolicyEnabled_Throws(
|
||||
SutProvider<CipherService> sutProvider,
|
||||
CipherDetails cipher,
|
||||
Guid savingUserId)
|
||||
{
|
||||
cipher.Id = default;
|
||||
cipher.UserId = savingUserId;
|
||||
cipher.OrganizationId = null;
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.PolicyRequirements)
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||
.GetAsync<PersonalOwnershipPolicyRequirement>(savingUserId)
|
||||
.Returns(new PersonalOwnershipPolicyRequirement { DisablePersonalOwnership = true });
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.SaveDetailsAsync(cipher, savingUserId, null));
|
||||
Assert.Contains("restricted from saving items to your personal vault", exception.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task SaveDetailsAsync_PersonalVault_WithPolicyRequirementsEnabled_WithDisablePersonalOwnershipPolicyDisabled_Succeeds(
|
||||
SutProvider<CipherService> sutProvider,
|
||||
CipherDetails cipher,
|
||||
Guid savingUserId)
|
||||
{
|
||||
cipher.Id = default;
|
||||
cipher.UserId = savingUserId;
|
||||
cipher.OrganizationId = null;
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.PolicyRequirements)
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||
.GetAsync<PersonalOwnershipPolicyRequirement>(savingUserId)
|
||||
.Returns(new PersonalOwnershipPolicyRequirement { DisablePersonalOwnership = false });
|
||||
|
||||
await sutProvider.Sut.SaveDetailsAsync(cipher, savingUserId, null);
|
||||
|
||||
await sutProvider.GetDependency<ICipherRepository>()
|
||||
.Received(1)
|
||||
.CreateAsync(cipher);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData("")]
|
||||
[BitAutoData("Correct Time")]
|
||||
@ -602,6 +701,78 @@ public class CipherServiceTests
|
||||
Assert.NotEqual(initialRevisionDate, cipher.RevisionDate);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task RestoreAsync_WithAlreadyRestoredCipher_SkipsOperation(
|
||||
Guid restoringUserId, Cipher cipher, SutProvider<CipherService> sutProvider)
|
||||
{
|
||||
cipher.DeletedDate = null;
|
||||
|
||||
await sutProvider.Sut.RestoreAsync(cipher, restoringUserId, true);
|
||||
|
||||
await sutProvider.GetDependency<ICipherRepository>().DidNotReceiveWithAnyArgs().UpsertAsync(default);
|
||||
await sutProvider.GetDependency<IEventService>().DidNotReceiveWithAnyArgs().LogCipherEventAsync(default, default);
|
||||
await sutProvider.GetDependency<IPushNotificationService>().DidNotReceiveWithAnyArgs().PushSyncCipherUpdateAsync(default, default);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task RestoreAsync_WithPersonalCipherBelongingToDifferentUser_ThrowsBadRequestException(
|
||||
Guid restoringUserId, Cipher cipher, SutProvider<CipherService> sutProvider)
|
||||
{
|
||||
cipher.UserId = Guid.NewGuid();
|
||||
cipher.OrganizationId = null;
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.RestoreAsync(cipher, restoringUserId));
|
||||
|
||||
Assert.Contains("do not have permissions", exception.Message);
|
||||
await sutProvider.GetDependency<ICipherRepository>().DidNotReceiveWithAnyArgs().UpsertAsync(default);
|
||||
await sutProvider.GetDependency<IEventService>().DidNotReceiveWithAnyArgs().LogCipherEventAsync(default, default);
|
||||
await sutProvider.GetDependency<IPushNotificationService>().DidNotReceiveWithAnyArgs().PushSyncCipherUpdateAsync(default, default);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[OrganizationCipherCustomize]
|
||||
[BitAutoData]
|
||||
public async Task RestoreAsync_WithOrgCipherLackingEditPermission_ThrowsBadRequestException(
|
||||
Guid restoringUserId, Cipher cipher, SutProvider<CipherService> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<ICipherRepository>()
|
||||
.GetCanEditByIdAsync(restoringUserId, cipher.Id)
|
||||
.Returns(false);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.RestoreAsync(cipher, restoringUserId));
|
||||
|
||||
Assert.Contains("do not have permissions", exception.Message);
|
||||
await sutProvider.GetDependency<ICipherRepository>().DidNotReceiveWithAnyArgs().UpsertAsync(default);
|
||||
await sutProvider.GetDependency<IEventService>().DidNotReceiveWithAnyArgs().LogCipherEventAsync(default, default);
|
||||
await sutProvider.GetDependency<IPushNotificationService>().DidNotReceiveWithAnyArgs().PushSyncCipherUpdateAsync(default, default);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task RestoreAsync_WithCipherDetailsType_RestoresCipherDetails(
|
||||
Guid restoringUserId, CipherDetails cipherDetails, SutProvider<CipherService> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<ICipherRepository>()
|
||||
.GetCanEditByIdAsync(restoringUserId, cipherDetails.Id)
|
||||
.Returns(true);
|
||||
|
||||
var initialRevisionDate = new DateTime(1970, 1, 1, 0, 0, 0);
|
||||
cipherDetails.DeletedDate = initialRevisionDate;
|
||||
cipherDetails.RevisionDate = initialRevisionDate;
|
||||
|
||||
await sutProvider.Sut.RestoreAsync(cipherDetails, restoringUserId);
|
||||
|
||||
Assert.Null(cipherDetails.DeletedDate);
|
||||
Assert.NotEqual(initialRevisionDate, cipherDetails.RevisionDate);
|
||||
await sutProvider.GetDependency<ICipherRepository>().Received(1).UpsertAsync(cipherDetails);
|
||||
await sutProvider.GetDependency<IEventService>().Received(1).LogCipherEventAsync(cipherDetails, EventType.Cipher_Restored);
|
||||
await sutProvider.GetDependency<IPushNotificationService>().Received(1).PushSyncCipherUpdateAsync(cipherDetails, null);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task RestoreManyAsync_UpdatesCiphers(ICollection<CipherDetails> ciphers,
|
||||
@ -725,6 +896,415 @@ public class CipherServiceTests
|
||||
Arg.Is<IEnumerable<Cipher>>(arg => !arg.Except(ciphers).Any()));
|
||||
}
|
||||
|
||||
private class SaveDetailsAsyncDependencies
|
||||
{
|
||||
public CipherDetails CipherDetails { get; set; }
|
||||
public SutProvider<CipherService> SutProvider { get; set; }
|
||||
}
|
||||
|
||||
private static SaveDetailsAsyncDependencies GetSaveDetailsAsyncDependencies(
|
||||
SutProvider<CipherService> sutProvider,
|
||||
string newPassword,
|
||||
bool viewPassword,
|
||||
bool editPermission,
|
||||
string? key = null,
|
||||
string? totp = null,
|
||||
CipherLoginFido2CredentialData[]? passkeys = null
|
||||
)
|
||||
{
|
||||
var cipherDetails = new CipherDetails
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
OrganizationId = Guid.NewGuid(),
|
||||
Type = CipherType.Login,
|
||||
UserId = Guid.NewGuid(),
|
||||
RevisionDate = DateTime.UtcNow,
|
||||
Key = key,
|
||||
};
|
||||
|
||||
var newLoginData = new CipherLoginData { Username = "user", Password = newPassword, Totp = totp, Fido2Credentials = passkeys };
|
||||
cipherDetails.Data = JsonSerializer.Serialize(newLoginData);
|
||||
|
||||
var existingCipher = new Cipher
|
||||
{
|
||||
Id = cipherDetails.Id,
|
||||
Data = JsonSerializer.Serialize(
|
||||
new CipherLoginData
|
||||
{
|
||||
Username = "user",
|
||||
Password = "OriginalPassword",
|
||||
Totp = "OriginalTotp",
|
||||
Fido2Credentials = []
|
||||
}
|
||||
),
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<ICipherRepository>()
|
||||
.GetByIdAsync(cipherDetails.Id)
|
||||
.Returns(existingCipher);
|
||||
|
||||
sutProvider.GetDependency<ICipherRepository>()
|
||||
.ReplaceAsync(Arg.Any<CipherDetails>())
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
var permissions = new Dictionary<Guid, OrganizationCipherPermission>
|
||||
{
|
||||
{ cipherDetails.Id, new OrganizationCipherPermission { ViewPassword = viewPassword, Edit = editPermission } }
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IGetCipherPermissionsForUserQuery>()
|
||||
.GetByOrganization(cipherDetails.OrganizationId.Value)
|
||||
.Returns(permissions);
|
||||
|
||||
return new SaveDetailsAsyncDependencies
|
||||
{
|
||||
CipherDetails = cipherDetails,
|
||||
SutProvider = sutProvider,
|
||||
};
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task SaveDetailsAsync_PasswordNotChangedWithoutViewPasswordPermission(string _, SutProvider<CipherService> sutProvider)
|
||||
{
|
||||
var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", viewPassword: false, editPermission: true);
|
||||
|
||||
await deps.SutProvider.Sut.SaveDetailsAsync(
|
||||
deps.CipherDetails,
|
||||
deps.CipherDetails.UserId.Value,
|
||||
deps.CipherDetails.RevisionDate,
|
||||
null,
|
||||
true);
|
||||
|
||||
var updatedLoginData = JsonSerializer.Deserialize<CipherLoginData>(deps.CipherDetails.Data);
|
||||
Assert.Equal("OriginalPassword", updatedLoginData.Password);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task SaveDetailsAsync_PasswordNotChangedWithoutEditPermission(string _, SutProvider<CipherService> sutProvider)
|
||||
{
|
||||
var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", viewPassword: true, editPermission: false);
|
||||
|
||||
await deps.SutProvider.Sut.SaveDetailsAsync(
|
||||
deps.CipherDetails,
|
||||
deps.CipherDetails.UserId.Value,
|
||||
deps.CipherDetails.RevisionDate,
|
||||
null,
|
||||
true);
|
||||
|
||||
var updatedLoginData = JsonSerializer.Deserialize<CipherLoginData>(deps.CipherDetails.Data);
|
||||
Assert.Equal("OriginalPassword", updatedLoginData.Password);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task SaveDetailsAsync_PasswordChangedWithPermission(string _, SutProvider<CipherService> sutProvider)
|
||||
{
|
||||
var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", viewPassword: true, editPermission: true);
|
||||
|
||||
await deps.SutProvider.Sut.SaveDetailsAsync(
|
||||
deps.CipherDetails,
|
||||
deps.CipherDetails.UserId.Value,
|
||||
deps.CipherDetails.RevisionDate,
|
||||
null,
|
||||
true);
|
||||
|
||||
var updatedLoginData = JsonSerializer.Deserialize<CipherLoginData>(deps.CipherDetails.Data);
|
||||
Assert.Equal("NewPassword", updatedLoginData.Password);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task SaveDetailsAsync_CipherKeyChangedWithPermission(string _, SutProvider<CipherService> sutProvider)
|
||||
{
|
||||
var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", viewPassword: true, editPermission: true, "NewKey");
|
||||
|
||||
await deps.SutProvider.Sut.SaveDetailsAsync(
|
||||
deps.CipherDetails,
|
||||
deps.CipherDetails.UserId.Value,
|
||||
deps.CipherDetails.RevisionDate,
|
||||
null,
|
||||
true);
|
||||
|
||||
Assert.Equal("NewKey", deps.CipherDetails.Key);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task SaveDetailsAsync_CipherKeyChangedWithoutPermission(string _, SutProvider<CipherService> sutProvider)
|
||||
{
|
||||
var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", viewPassword: true, editPermission: false, "NewKey");
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() => deps.SutProvider.Sut.SaveDetailsAsync(
|
||||
deps.CipherDetails,
|
||||
deps.CipherDetails.UserId.Value,
|
||||
deps.CipherDetails.RevisionDate,
|
||||
null,
|
||||
true));
|
||||
|
||||
Assert.Contains("do not have permission", exception.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task SaveDetailsAsync_TotpChangedWithoutPermission(string _, SutProvider<CipherService> sutProvider)
|
||||
{
|
||||
var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", viewPassword: true, editPermission: false, totp: "NewTotp");
|
||||
|
||||
await deps.SutProvider.Sut.SaveDetailsAsync(
|
||||
deps.CipherDetails,
|
||||
deps.CipherDetails.UserId.Value,
|
||||
deps.CipherDetails.RevisionDate,
|
||||
null,
|
||||
true);
|
||||
|
||||
var updatedLoginData = JsonSerializer.Deserialize<CipherLoginData>(deps.CipherDetails.Data);
|
||||
Assert.Equal("OriginalTotp", updatedLoginData.Totp);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task SaveDetailsAsync_TotpChangedWithPermission(string _, SutProvider<CipherService> sutProvider)
|
||||
{
|
||||
var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", viewPassword: true, editPermission: true, totp: "NewTotp");
|
||||
|
||||
await deps.SutProvider.Sut.SaveDetailsAsync(
|
||||
deps.CipherDetails,
|
||||
deps.CipherDetails.UserId.Value,
|
||||
deps.CipherDetails.RevisionDate,
|
||||
null,
|
||||
true);
|
||||
|
||||
var updatedLoginData = JsonSerializer.Deserialize<CipherLoginData>(deps.CipherDetails.Data);
|
||||
Assert.Equal("NewTotp", updatedLoginData.Totp);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task SaveDetailsAsync_Fido2CredentialsChangedWithoutPermission(string _, SutProvider<CipherService> sutProvider)
|
||||
{
|
||||
var passkeys = new[]
|
||||
{
|
||||
new CipherLoginFido2CredentialData
|
||||
{
|
||||
CredentialId = "CredentialId",
|
||||
UserHandle = "UserHandle",
|
||||
}
|
||||
};
|
||||
|
||||
var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", viewPassword: true, editPermission: false, passkeys: passkeys);
|
||||
|
||||
await deps.SutProvider.Sut.SaveDetailsAsync(
|
||||
deps.CipherDetails,
|
||||
deps.CipherDetails.UserId.Value,
|
||||
deps.CipherDetails.RevisionDate,
|
||||
null,
|
||||
true);
|
||||
|
||||
var updatedLoginData = JsonSerializer.Deserialize<CipherLoginData>(deps.CipherDetails.Data);
|
||||
Assert.Empty(updatedLoginData.Fido2Credentials);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task SaveDetailsAsync_Fido2CredentialsChangedWithPermission(string _, SutProvider<CipherService> sutProvider)
|
||||
{
|
||||
var passkeys = new[]
|
||||
{
|
||||
new CipherLoginFido2CredentialData
|
||||
{
|
||||
CredentialId = "CredentialId",
|
||||
UserHandle = "UserHandle",
|
||||
}
|
||||
};
|
||||
|
||||
var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", viewPassword: true, editPermission: true, passkeys: passkeys);
|
||||
|
||||
await deps.SutProvider.Sut.SaveDetailsAsync(
|
||||
deps.CipherDetails,
|
||||
deps.CipherDetails.UserId.Value,
|
||||
deps.CipherDetails.RevisionDate,
|
||||
null,
|
||||
true);
|
||||
|
||||
var updatedLoginData = JsonSerializer.Deserialize<CipherLoginData>(deps.CipherDetails.Data);
|
||||
Assert.Equal(passkeys.Length, updatedLoginData.Fido2Credentials.Length);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task DeleteAsync_WithPersonalCipherOwner_DeletesCipher(
|
||||
Guid deletingUserId, Cipher cipher, SutProvider<CipherService> sutProvider)
|
||||
{
|
||||
cipher.UserId = deletingUserId;
|
||||
cipher.OrganizationId = null;
|
||||
|
||||
await sutProvider.Sut.DeleteAsync(cipher, deletingUserId);
|
||||
|
||||
await sutProvider.GetDependency<ICipherRepository>().Received(1).DeleteAsync(cipher);
|
||||
await sutProvider.GetDependency<IAttachmentStorageService>().Received(1).DeleteAttachmentsForCipherAsync(cipher.Id);
|
||||
await sutProvider.GetDependency<IEventService>().Received(1).LogCipherEventAsync(cipher, EventType.Cipher_Deleted);
|
||||
await sutProvider.GetDependency<IPushNotificationService>().Received(1).PushSyncCipherDeleteAsync(cipher);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[OrganizationCipherCustomize]
|
||||
[BitAutoData]
|
||||
public async Task DeleteAsync_WithOrgCipherAndEditPermission_DeletesCipher(
|
||||
Guid deletingUserId, Cipher cipher, SutProvider<CipherService> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<ICipherRepository>()
|
||||
.GetCanEditByIdAsync(deletingUserId, cipher.Id)
|
||||
.Returns(true);
|
||||
|
||||
await sutProvider.Sut.DeleteAsync(cipher, deletingUserId);
|
||||
|
||||
await sutProvider.GetDependency<ICipherRepository>().Received(1).DeleteAsync(cipher);
|
||||
await sutProvider.GetDependency<IAttachmentStorageService>().Received(1).DeleteAttachmentsForCipherAsync(cipher.Id);
|
||||
await sutProvider.GetDependency<IEventService>().Received(1).LogCipherEventAsync(cipher, EventType.Cipher_Deleted);
|
||||
await sutProvider.GetDependency<IPushNotificationService>().Received(1).PushSyncCipherDeleteAsync(cipher);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task DeleteAsync_WithPersonalCipherBelongingToDifferentUser_ThrowsBadRequestException(
|
||||
Guid deletingUserId, Cipher cipher, SutProvider<CipherService> sutProvider)
|
||||
{
|
||||
cipher.UserId = Guid.NewGuid();
|
||||
cipher.OrganizationId = null;
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.DeleteAsync(cipher, deletingUserId));
|
||||
|
||||
Assert.Contains("do not have permissions", exception.Message);
|
||||
await sutProvider.GetDependency<ICipherRepository>().DidNotReceiveWithAnyArgs().DeleteAsync(default);
|
||||
await sutProvider.GetDependency<IAttachmentStorageService>().DidNotReceiveWithAnyArgs().DeleteAttachmentsForCipherAsync(default);
|
||||
await sutProvider.GetDependency<IEventService>().DidNotReceiveWithAnyArgs().LogCipherEventAsync(default, default);
|
||||
await sutProvider.GetDependency<IPushNotificationService>().DidNotReceiveWithAnyArgs().PushSyncCipherDeleteAsync(default);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[OrganizationCipherCustomize]
|
||||
[BitAutoData]
|
||||
public async Task DeleteAsync_WithOrgCipherLackingEditPermission_ThrowsBadRequestException(
|
||||
Guid deletingUserId, Cipher cipher, SutProvider<CipherService> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<ICipherRepository>()
|
||||
.GetCanEditByIdAsync(deletingUserId, cipher.Id)
|
||||
.Returns(false);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.DeleteAsync(cipher, deletingUserId));
|
||||
|
||||
Assert.Contains("do not have permissions", exception.Message);
|
||||
await sutProvider.GetDependency<ICipherRepository>().DidNotReceiveWithAnyArgs().DeleteAsync(default);
|
||||
await sutProvider.GetDependency<IAttachmentStorageService>().DidNotReceiveWithAnyArgs().DeleteAttachmentsForCipherAsync(default);
|
||||
await sutProvider.GetDependency<IEventService>().DidNotReceiveWithAnyArgs().LogCipherEventAsync(default, default);
|
||||
await sutProvider.GetDependency<IPushNotificationService>().DidNotReceiveWithAnyArgs().PushSyncCipherDeleteAsync(default);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task SoftDeleteAsync_WithPersonalCipherOwner_SoftDeletesCipher(
|
||||
Guid deletingUserId, Cipher cipher, SutProvider<CipherService> sutProvider)
|
||||
{
|
||||
cipher.UserId = deletingUserId;
|
||||
cipher.OrganizationId = null;
|
||||
cipher.DeletedDate = null;
|
||||
|
||||
sutProvider.GetDependency<ICipherRepository>()
|
||||
.GetCanEditByIdAsync(deletingUserId, cipher.Id)
|
||||
.Returns(true);
|
||||
|
||||
await sutProvider.Sut.SoftDeleteAsync(cipher, deletingUserId);
|
||||
|
||||
Assert.NotNull(cipher.DeletedDate);
|
||||
Assert.Equal(cipher.RevisionDate, cipher.DeletedDate);
|
||||
await sutProvider.GetDependency<ICipherRepository>().Received(1).UpsertAsync(cipher);
|
||||
await sutProvider.GetDependency<IEventService>().Received(1).LogCipherEventAsync(cipher, EventType.Cipher_SoftDeleted);
|
||||
await sutProvider.GetDependency<IPushNotificationService>().Received(1).PushSyncCipherUpdateAsync(cipher, null);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[OrganizationCipherCustomize]
|
||||
[BitAutoData]
|
||||
public async Task SoftDeleteAsync_WithOrgCipherAndEditPermission_SoftDeletesCipher(
|
||||
Guid deletingUserId, Cipher cipher, SutProvider<CipherService> sutProvider)
|
||||
{
|
||||
cipher.DeletedDate = null;
|
||||
|
||||
sutProvider.GetDependency<ICipherRepository>()
|
||||
.GetCanEditByIdAsync(deletingUserId, cipher.Id)
|
||||
.Returns(true);
|
||||
|
||||
await sutProvider.Sut.SoftDeleteAsync(cipher, deletingUserId);
|
||||
|
||||
Assert.NotNull(cipher.DeletedDate);
|
||||
Assert.Equal(cipher.DeletedDate, cipher.RevisionDate);
|
||||
await sutProvider.GetDependency<ICipherRepository>().Received(1).UpsertAsync(cipher);
|
||||
await sutProvider.GetDependency<IEventService>().Received(1).LogCipherEventAsync(cipher, EventType.Cipher_SoftDeleted);
|
||||
await sutProvider.GetDependency<IPushNotificationService>().Received(1).PushSyncCipherUpdateAsync(cipher, null);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task SoftDeleteAsync_WithPersonalCipherBelongingToDifferentUser_ThrowsBadRequestException(
|
||||
Guid deletingUserId, Cipher cipher, SutProvider<CipherService> sutProvider)
|
||||
{
|
||||
cipher.UserId = Guid.NewGuid();
|
||||
cipher.OrganizationId = null;
|
||||
|
||||
sutProvider.GetDependency<ICipherRepository>()
|
||||
.GetCanEditByIdAsync(deletingUserId, cipher.Id)
|
||||
.Returns(false);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.SoftDeleteAsync(cipher, deletingUserId));
|
||||
|
||||
Assert.Contains("do not have permissions", exception.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[OrganizationCipherCustomize]
|
||||
[BitAutoData]
|
||||
public async Task SoftDeleteAsync_WithOrgCipherLackingEditPermission_ThrowsBadRequestException(
|
||||
Guid deletingUserId, Cipher cipher, SutProvider<CipherService> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<ICipherRepository>()
|
||||
.GetCanEditByIdAsync(deletingUserId, cipher.Id)
|
||||
.Returns(false);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.SoftDeleteAsync(cipher, deletingUserId));
|
||||
|
||||
Assert.Contains("do not have permissions", exception.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task SoftDeleteAsync_WithCipherDetailsType_SoftDeletesCipherDetails(
|
||||
Guid deletingUserId, CipherDetails cipher, SutProvider<CipherService> sutProvider)
|
||||
{
|
||||
cipher.DeletedDate = null;
|
||||
|
||||
await sutProvider.Sut.SoftDeleteAsync(cipher, deletingUserId, true);
|
||||
|
||||
Assert.NotNull(cipher.DeletedDate);
|
||||
Assert.Equal(cipher.DeletedDate, cipher.RevisionDate);
|
||||
await sutProvider.GetDependency<ICipherRepository>().Received(1).UpsertAsync(cipher);
|
||||
await sutProvider.GetDependency<IEventService>().Received(1).LogCipherEventAsync(cipher, EventType.Cipher_SoftDeleted);
|
||||
await sutProvider.GetDependency<IPushNotificationService>().Received(1).PushSyncCipherUpdateAsync(cipher, null);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task SoftDeleteAsync_WithAlreadySoftDeletedCipher_SkipsOperation(
|
||||
Guid deletingUserId, Cipher cipher, SutProvider<CipherService> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<ICipherRepository>()
|
||||
.GetCanEditByIdAsync(deletingUserId, cipher.Id)
|
||||
.Returns(true);
|
||||
cipher.DeletedDate = DateTime.UtcNow.AddDays(-1);
|
||||
|
||||
await sutProvider.Sut.SoftDeleteAsync(cipher, deletingUserId);
|
||||
|
||||
await sutProvider.GetDependency<ICipherRepository>().DidNotReceive().UpsertAsync(Arg.Any<Cipher>());
|
||||
await sutProvider.GetDependency<IEventService>().DidNotReceive().LogCipherEventAsync(Arg.Any<Cipher>(), Arg.Any<EventType>());
|
||||
await sutProvider.GetDependency<IPushNotificationService>().DidNotReceive().PushSyncCipherUpdateAsync(Arg.Any<Cipher>(), Arg.Any<IEnumerable<Guid>>());
|
||||
}
|
||||
|
||||
private async Task AssertNoActionsAsync(SutProvider<CipherService> sutProvider)
|
||||
{
|
||||
await sutProvider.GetDependency<ICipherRepository>().DidNotReceiveWithAnyArgs().GetManyOrganizationDetailsByOrganizationIdAsync(default);
|
||||
|
@ -1,5 +1,4 @@
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Identity.TokenProviders;
|
||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||
@ -464,7 +463,6 @@ public class TwoFactorAuthenticationValidatorTests
|
||||
user.TwoFactorRecoveryCode = token;
|
||||
|
||||
_userService.RecoverTwoFactorAsync(Arg.Is(user), Arg.Is(token)).Returns(true);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.RecoveryCodeLogin).Returns(true);
|
||||
|
||||
// Act
|
||||
var result = await _sut.VerifyTwoFactorAsync(
|
||||
@ -486,7 +484,6 @@ public class TwoFactorAuthenticationValidatorTests
|
||||
user.TwoFactorRecoveryCode = token;
|
||||
|
||||
_userService.RecoverTwoFactorAsync(Arg.Is(user), Arg.Is(token)).Returns(false);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.RecoveryCodeLogin).Returns(true);
|
||||
|
||||
// Act
|
||||
var result = await _sut.VerifyTwoFactorAsync(
|
||||
|
@ -1,6 +1,7 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Test.AutoFixture.Attributes;
|
||||
using Bit.Infrastructure.EFIntegration.Test.AutoFixture;
|
||||
@ -289,4 +290,27 @@ public class UserRepositoryTests
|
||||
var distinctItems = returnedList.Distinct(equalityComparer);
|
||||
Assert.True(!distinctItems.Skip(1).Any());
|
||||
}
|
||||
|
||||
[CiSkippedTheory, EfUserAutoData]
|
||||
public async Task UpdateUserKeyAndEncryptedDataAsync_Works_DataMatches(User user, SqlRepo.UserRepository sqlUserRepo)
|
||||
{
|
||||
var sqlUser = await sqlUserRepo.CreateAsync(user);
|
||||
sqlUser.Kdf = KdfType.PBKDF2_SHA256;
|
||||
sqlUser.KdfIterations = 6_000_000;
|
||||
sqlUser.KdfMemory = 7_000_000;
|
||||
sqlUser.KdfParallelism = 8_000_000;
|
||||
sqlUser.MasterPassword = "masterPasswordHash";
|
||||
sqlUser.MasterPasswordHint = "masterPasswordHint";
|
||||
sqlUser.Email = "example@example.com";
|
||||
|
||||
await sqlUserRepo.UpdateUserKeyAndEncryptedDataV2Async(sqlUser, []);
|
||||
var updatedUser = await sqlUserRepo.GetByIdAsync(sqlUser.Id);
|
||||
Assert.Equal(sqlUser.Kdf, updatedUser.Kdf);
|
||||
Assert.Equal(sqlUser.KdfIterations, updatedUser.KdfIterations);
|
||||
Assert.Equal(sqlUser.KdfMemory, updatedUser.KdfMemory);
|
||||
Assert.Equal(sqlUser.KdfParallelism, updatedUser.KdfParallelism);
|
||||
Assert.Equal(sqlUser.MasterPassword, updatedUser.MasterPassword);
|
||||
Assert.Equal(sqlUser.MasterPasswordHint, updatedUser.MasterPasswordHint);
|
||||
Assert.Equal(sqlUser.Email, updatedUser.Email);
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user