Files
server/test/Api.Test/Auth/Controllers/AccountsControllerTests.cs
Rui Tomé a56946fd13 [PM-37486] Remove IPolicyService and associated dead code (#7672)
* Refactor InitPendingOrganizationValidator to remove IPolicyService dependency and replace with IPolicyRequirementQuery for policy checks. Update related tests to reflect changes in policy validation logic.

* Refactor AccountsController and related validators to replace IPolicyService with IPolicyRequirementQuery for policy checks. Update tests accordingly to reflect changes in policy validation logic.

* Remove IPolicyService and related implementations from the codebase, updating PolicyServiceCollectionExtensions and deleting associated tests. This change streamlines policy management by relying on IPolicyRequirementQuery for policy checks.

* Refactor OrganizationUserRepository to remove GetByUserIdWithPolicyDetailsAsync method and associated tests.

* Remove unused stored procedures: OrganizationUser_ReadByUserIdWithPolicyDetails and PolicyDetails_ReadByUserId, as they are no longer called in the codebase.

* Remove OrganizationUserPolicyDetails class and associated test fixtures, as they are no longer needed in the codebase.

* Refactor BaseRequestValidatorTests to replace IPolicyService with IPolicyRequirementQuery for SSO validation checks. Update related test logic to ensure accurate policy validation outcomes. Clean up unused test fixtures in PolicyFixtures.cs to streamline the codebase.

* Refactor BaseRequestValidator and SsoRequestValidator to improve readability by storing policy requirement results in local variables before returning values. This change enhances code clarity while maintaining existing functionality.

* Refactor AccountsController to improve clarity by storing the result of the policy requirement query in a local variable before returning the enforced options. This change enhances code readability while preserving existing functionality.

* Revert "Remove unused stored procedures: OrganizationUser_ReadByUserIdWithPolicyDetails and PolicyDetails_ReadByUserId, as they are no longer called in the codebase."

This reverts commit 0f4fdca6e7.
2026-05-25 14:27:47 +01:00

1086 lines
42 KiB
C#

using System.Security.Claims;
using Bit.Api.Auth.Controllers;
using Bit.Api.Auth.Models.Request.Accounts;
using Bit.Core;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Auth.Models.Api.Request.Accounts;
using Bit.Core.Auth.Models.Data;
using Bit.Core.Auth.Services;
using Bit.Core.Auth.UserFeatures.TdeOffboardingPassword.Interfaces;
using Bit.Core.Auth.UserFeatures.TempPassword.Interfaces;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
using Bit.Core.Auth.UserFeatures.UserApiKey.Interfaces;
using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.KeyManagement.Kdf;
using Bit.Core.KeyManagement.Models.Api.Request;
using Bit.Core.KeyManagement.Models.Data;
using Bit.Core.KeyManagement.Queries.Interfaces;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Test.Common.AutoFixture.Attributes;
using Microsoft.AspNetCore.Identity;
using NSubstitute;
using Xunit;
namespace Bit.Api.Test.Auth.Controllers;
public class AccountsControllerTests : IDisposable
{
private readonly AccountsController _sut;
private readonly IOrganizationService _organizationService;
private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly IUserService _userService;
private readonly IProviderUserRepository _providerUserRepository;
private readonly ISelfServicePasswordChangeCommand _selfServicePasswordChangeCommand;
private readonly IPolicyRequirementQuery _policyRequirementQuery;
private readonly IFinishSsoJitProvisionMasterPasswordCommand _finishSsoJitProvisionMasterPasswordCommand;
private readonly ISetInitialMasterPasswordCommandV1 _setInitialMasterPasswordCommandV1;
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
private readonly ITdeSetPasswordCommand _tdeSetPasswordCommand;
private readonly ITdeOffboardingPasswordCommand _tdeOffboardingPasswordCommand;
private readonly IReplaceAdminSetTemporaryPasswordCommand _replaceAdminSetTemporaryPasswordCommand;
private readonly IFeatureService _featureService;
private readonly IUserAccountKeysQuery _userAccountKeysQuery;
private readonly ITwoFactorEmailService _twoFactorEmailService;
private readonly IChangeKdfCommand _changeKdfCommand;
private readonly IUserRepository _userRepository;
private readonly IRotateUserApiKeyCommand _rotateUserApiKeyCommand;
public AccountsControllerTests()
{
_userService = Substitute.For<IUserService>();
_organizationService = Substitute.For<IOrganizationService>();
_organizationUserRepository = Substitute.For<IOrganizationUserRepository>();
_providerUserRepository = Substitute.For<IProviderUserRepository>();
_selfServicePasswordChangeCommand = Substitute.For<ISelfServicePasswordChangeCommand>();
_policyRequirementQuery = Substitute.For<IPolicyRequirementQuery>();
_finishSsoJitProvisionMasterPasswordCommand = Substitute.For<IFinishSsoJitProvisionMasterPasswordCommand>();
_setInitialMasterPasswordCommandV1 = Substitute.For<ISetInitialMasterPasswordCommandV1>();
_twoFactorIsEnabledQuery = Substitute.For<ITwoFactorIsEnabledQuery>();
_tdeSetPasswordCommand = Substitute.For<ITdeSetPasswordCommand>();
_tdeOffboardingPasswordCommand = Substitute.For<ITdeOffboardingPasswordCommand>();
_replaceAdminSetTemporaryPasswordCommand = Substitute.For<IReplaceAdminSetTemporaryPasswordCommand>();
_featureService = Substitute.For<IFeatureService>();
_userAccountKeysQuery = Substitute.For<IUserAccountKeysQuery>();
_twoFactorEmailService = Substitute.For<ITwoFactorEmailService>();
_changeKdfCommand = Substitute.For<IChangeKdfCommand>();
_userRepository = Substitute.For<IUserRepository>();
_rotateUserApiKeyCommand = Substitute.For<IRotateUserApiKeyCommand>();
_sut = new AccountsController(
_organizationService,
_organizationUserRepository,
_providerUserRepository,
_userService,
_selfServicePasswordChangeCommand,
_policyRequirementQuery,
_finishSsoJitProvisionMasterPasswordCommand,
_setInitialMasterPasswordCommandV1,
_tdeSetPasswordCommand,
_tdeOffboardingPasswordCommand,
_replaceAdminSetTemporaryPasswordCommand,
_twoFactorIsEnabledQuery,
_featureService,
_userAccountKeysQuery,
_twoFactorEmailService,
_changeKdfCommand,
_userRepository,
_rotateUserApiKeyCommand
);
}
public void Dispose()
{
_sut?.Dispose();
}
[Fact]
public async Task PostPasswordHint_ShouldNotifyUserService()
{
var email = "user@example.com";
await _sut.PostPasswordHint(new PasswordHintRequestModel { Email = email });
await _userService.Received(1).SendMasterPasswordHintAsync(email);
}
[Fact]
public async Task PostEmailToken_ShouldInitiateEmailChange()
{
// Arrange
var user = GenerateExampleUser();
ConfigureUserServiceToReturnValidPrincipalFor(user);
ConfigureUserServiceToAcceptPasswordFor(user);
const string newEmail = "example@user.com";
_userService.ValidateClaimedUserDomainAsync(user, newEmail).Returns(IdentityResult.Success);
// Act
await _sut.PostEmailToken(new EmailTokenRequestModel { NewEmail = newEmail });
// Assert
await _userService.Received(1).InitiateEmailChangeAsync(user, newEmail);
}
[Fact]
public async Task PostEmailToken_WhenValidateClaimedUserDomainAsyncFails_ShouldReturnError()
{
// Arrange
var user = GenerateExampleUser();
ConfigureUserServiceToReturnValidPrincipalFor(user);
ConfigureUserServiceToAcceptPasswordFor(user);
const string newEmail = "example@user.com";
_userService.ValidateClaimedUserDomainAsync(user, newEmail)
.Returns(IdentityResult.Failed(new IdentityError
{
Code = "TestFailure",
Description = "This is a test."
}));
// Act
// Assert
await Assert.ThrowsAsync<BadRequestException>(
() => _sut.PostEmailToken(new EmailTokenRequestModel { NewEmail = newEmail })
);
}
[Fact]
public async Task PostEmailToken_WhenNotAuthorized_ShouldThrowUnauthorizedAccessException()
{
ConfigureUserServiceToReturnNullPrincipal();
await Assert.ThrowsAsync<UnauthorizedAccessException>(
() => _sut.PostEmailToken(new EmailTokenRequestModel())
);
}
[Fact]
public async Task PostEmailToken_WhenInvalidPasssword_ShouldThrowBadRequestException()
{
var user = GenerateExampleUser();
ConfigureUserServiceToReturnValidPrincipalFor(user);
ConfigureUserServiceToRejectPasswordFor(user);
await Assert.ThrowsAsync<BadRequestException>(
() => _sut.PostEmailToken(new EmailTokenRequestModel())
);
}
[Fact]
public async Task PostEmail_ShouldChangeUserEmail()
{
var user = GenerateExampleUser();
ConfigureUserServiceToReturnValidPrincipalFor(user);
_userService.ChangeEmailAsync(user, default, default, default, default, default)
.Returns(Task.FromResult(IdentityResult.Success));
await _sut.PostEmail(new EmailRequestModel());
await _userService.Received(1).ChangeEmailAsync(user, default, default, default, default, default);
}
[Fact]
public async Task PostEmail_WhenNotAuthorized_ShouldThrownUnauthorizedAccessException()
{
ConfigureUserServiceToReturnNullPrincipal();
await Assert.ThrowsAsync<UnauthorizedAccessException>(
() => _sut.PostEmail(new EmailRequestModel())
);
}
[Fact]
public async Task PostEmail_WhenEmailCannotBeChanged_ShouldThrowBadRequestException()
{
var user = GenerateExampleUser();
ConfigureUserServiceToReturnValidPrincipalFor(user);
_userService.ChangeEmailAsync(user, default, default, default, default, default)
.Returns(Task.FromResult(IdentityResult.Failed()));
await Assert.ThrowsAsync<BadRequestException>(
() => _sut.PostEmail(new EmailRequestModel())
);
}
[Fact]
public async Task PostVerifyEmail_ShouldSendEmailVerification()
{
var user = GenerateExampleUser();
ConfigureUserServiceToReturnValidPrincipalFor(user);
await _sut.PostVerifyEmail();
await _userService.Received(1).SendEmailVerificationAsync(user);
}
[Fact]
public async Task PostVerifyEmail_WhenNotAuthorized_ShouldThrownUnauthorizedAccessException()
{
ConfigureUserServiceToReturnNullPrincipal();
await Assert.ThrowsAsync<UnauthorizedAccessException>(
() => _sut.PostVerifyEmail()
);
}
[Fact]
public async Task PostVerifyEmailToken_ShouldConfirmEmail()
{
var user = GenerateExampleUser();
ConfigureUserServiceToReturnValidIdFor(user);
_userService.ConfirmEmailAsync(user, Arg.Any<string>())
.Returns(Task.FromResult(IdentityResult.Success));
await _sut.PostVerifyEmailToken(new VerifyEmailRequestModel { UserId = "12345678-1234-1234-1234-123456789012" });
await _userService.Received(1).ConfirmEmailAsync(user, Arg.Any<string>());
}
[Fact]
public async Task PostVerifyEmailToken_WhenUserDoesNotExist_ShouldThrowUnauthorizedAccessException()
{
var user = GenerateExampleUser();
ConfigureUserServiceToReturnNullUserId();
await Assert.ThrowsAsync<UnauthorizedAccessException>(
() => _sut.PostVerifyEmailToken(new VerifyEmailRequestModel { UserId = "12345678-1234-1234-1234-123456789012" })
);
}
[Fact]
public async Task PostVerifyEmailToken_WhenEmailConfirmationFails_ShouldThrowBadRequestException()
{
var user = GenerateExampleUser();
ConfigureUserServiceToReturnValidIdFor(user);
_userService.ConfirmEmailAsync(user, Arg.Any<string>())
.Returns(Task.FromResult(IdentityResult.Failed()));
await Assert.ThrowsAsync<BadRequestException>(
() => _sut.PostVerifyEmailToken(new VerifyEmailRequestModel { UserId = "12345678-1234-1234-1234-123456789012" })
);
}
[Fact]
public async Task PostPassword_ShouldChangePassword()
{
var user = GenerateExampleUser();
ConfigureUserServiceToReturnValidPrincipalFor(user);
_userService.ChangePasswordAsync(user, Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>())
.Returns(Task.FromResult(IdentityResult.Success));
await _sut.PostPassword(new PasswordRequestModel
{
MasterPasswordHash = "masterPasswordHash",
NewMasterPasswordHash = "newMasterPasswordHash",
MasterPasswordHint = "masterPasswordHint",
Key = "key"
});
await _userService.Received(1).ChangePasswordAsync(user, Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>());
}
[Fact]
public async Task PostPassword_WhenNotAuthorized_ShouldThrowUnauthorizedAccessException()
{
ConfigureUserServiceToReturnNullPrincipal();
await Assert.ThrowsAsync<UnauthorizedAccessException>(
() => _sut.PostPassword(new PasswordRequestModel
{
MasterPasswordHash = "masterPasswordHash",
NewMasterPasswordHash = "newMasterPasswordHash",
MasterPasswordHint = "masterPasswordHint",
Key = "key"
})
);
}
[Fact]
public async Task PostPassword_WhenPasswordChangeFails_ShouldBadRequestException()
{
var user = GenerateExampleUser();
ConfigureUserServiceToReturnValidPrincipalFor(user);
_userService.ChangePasswordAsync(user, Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>())
.Returns(Task.FromResult(IdentityResult.Failed()));
await Assert.ThrowsAsync<BadRequestException>(
() => _sut.PostPassword(new PasswordRequestModel
{
MasterPasswordHash = "masterPasswordHash",
NewMasterPasswordHash = "newMasterPasswordHash",
MasterPasswordHint = "masterPasswordHint",
Key = "key"
})
);
}
[Fact]
public async Task GetApiKey_ShouldReturnApiKeyResponse()
{
var user = GenerateExampleUser();
ConfigureUserServiceToReturnValidPrincipalFor(user);
ConfigureUserServiceToAcceptPasswordFor(user);
await _sut.ApiKey(new SecretVerificationRequestModel());
}
[Fact]
public async Task GetApiKey_WhenUserDoesNotExist_ShouldThrowUnauthorizedAccessException()
{
ConfigureUserServiceToReturnNullPrincipal();
await Assert.ThrowsAsync<UnauthorizedAccessException>(
() => _sut.ApiKey(new SecretVerificationRequestModel())
);
}
[Fact]
public async Task GetApiKey_WhenPasswordCheckFails_ShouldThrowBadRequestException()
{
var user = GenerateExampleUser();
ConfigureUserServiceToReturnValidPrincipalFor(user);
ConfigureUserServiceToRejectPasswordFor(user);
await Assert.ThrowsAsync<BadRequestException>(
() => _sut.ApiKey(new SecretVerificationRequestModel())
);
}
// TODO: Delete this test when the PM37165_RotateUserApiKeyCommand flag is cleaned up.
[Fact]
public async Task PostRotateApiKey_FlagOff_CallsLegacyUserService()
{
var user = GenerateExampleUser();
ConfigureUserServiceToReturnValidPrincipalFor(user);
ConfigureUserServiceToAcceptPasswordFor(user);
_featureService.IsEnabled(FeatureFlagKeys.PM37165_RotateUserApiKeyCommand).Returns(false);
await _sut.RotateApiKey(new SecretVerificationRequestModel());
#pragma warning disable CS0618 // asserting the legacy path while it still exists
await _userService.Received(1).RotateApiKeyAsync(user);
#pragma warning restore CS0618
await _rotateUserApiKeyCommand.DidNotReceive().RotateApiKeyAsync(Arg.Any<User>());
}
// TODO: When the PM37165_RotateUserApiKeyCommand flag is cleaned up, rename this to
// PostRotateApiKey_ShouldRotateApiKey (and drop the flag setup) — it becomes the canonical happy-path test.
[Fact]
public async Task PostRotateApiKey_FlagOn_CallsRotateUserApiKeyCommand()
{
var user = GenerateExampleUser();
ConfigureUserServiceToReturnValidPrincipalFor(user);
ConfigureUserServiceToAcceptPasswordFor(user);
_featureService.IsEnabled(FeatureFlagKeys.PM37165_RotateUserApiKeyCommand).Returns(true);
await _sut.RotateApiKey(new SecretVerificationRequestModel());
await _rotateUserApiKeyCommand.Received(1).RotateApiKeyAsync(user);
#pragma warning disable CS0618 // asserting the legacy path was NOT called
await _userService.DidNotReceive().RotateApiKeyAsync(Arg.Any<User>());
#pragma warning restore CS0618
}
// Auth/secret guards run before the flag branch, but cover both flag states for parity. When the
// PM37165_RotateUserApiKeyCommand flag is cleaned up, drop the [InlineData] rows and convert back to [Fact].
[Theory]
[InlineData(false)]
[InlineData(true)]
public async Task PostRotateApiKey_WhenUserDoesNotExist_ShouldThrowUnauthorizedAccessException(bool flagOn)
{
_featureService.IsEnabled(FeatureFlagKeys.PM37165_RotateUserApiKeyCommand).Returns(flagOn);
ConfigureUserServiceToReturnNullPrincipal();
await Assert.ThrowsAsync<UnauthorizedAccessException>(
() => _sut.RotateApiKey(new SecretVerificationRequestModel())
);
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public async Task PostRotateApiKey_WhenPasswordCheckFails_ShouldThrowBadRequestException(bool flagOn)
{
_featureService.IsEnabled(FeatureFlagKeys.PM37165_RotateUserApiKeyCommand).Returns(flagOn);
var user = GenerateExampleUser();
ConfigureUserServiceToReturnValidPrincipalFor(user);
ConfigureUserServiceToRejectPasswordFor(user);
await Assert.ThrowsAsync<BadRequestException>(
() => _sut.RotateApiKey(new SecretVerificationRequestModel())
);
}
[Theory]
[BitAutoData(true, "existingPrivateKey", "existingPublicKey", true)] // allow providing existing keys in the request
[BitAutoData(true, null, null, true)] // allow not setting the public key when the user already has a key
[BitAutoData(false, "newPrivateKey", "newPublicKey", true)] // allow setting new keys when the user has no keys
[BitAutoData(false, null, null, true)] // allow not setting the public key when the user has no keys
// do not allow single key
[BitAutoData(false, "existingPrivateKey", null, false)]
[BitAutoData(false, null, "existingPublicKey", false)]
[BitAutoData(false, "newPrivateKey", null, false)]
[BitAutoData(false, null, "newPublicKey", false)]
[BitAutoData(true, "existingPrivateKey", null, false)]
[BitAutoData(true, null, "existingPublicKey", false)]
[BitAutoData(true, "newPrivateKey", null, false)]
[BitAutoData(true, null, "newPublicKey", false)]
// reject overwriting existing keys
[BitAutoData(true, "newPrivateKey", "newPublicKey", false)]
public async Task PostSetPasswordAsync_V1_WhenUserExistsAndSettingPasswordSucceeds_ShouldHandleKeysCorrectlyAndReturn(
bool hasExistingKeys,
string requestPrivateKey,
string requestPublicKey,
bool shouldSucceed,
User user,
SetInitialPasswordRequestModel setInitialPasswordRequestModel)
{
// Arrange
const string existingPublicKey = "existingPublicKey";
const string existingEncryptedPrivateKey = "existingPrivateKey";
if (hasExistingKeys)
{
user.PublicKey = existingPublicKey;
user.PrivateKey = existingEncryptedPrivateKey;
}
else
{
user.PublicKey = null;
user.PrivateKey = null;
}
UpdateSetInitialPasswordRequestModelToV1(setInitialPasswordRequestModel);
if (requestPrivateKey == null && requestPublicKey == null)
{
setInitialPasswordRequestModel.Keys = null;
}
else
{
setInitialPasswordRequestModel.Keys = new KeysRequestModel
{
EncryptedPrivateKey = requestPrivateKey,
PublicKey = requestPublicKey
};
}
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(Task.FromResult(user));
_setInitialMasterPasswordCommandV1.SetInitialMasterPasswordAsync(
user,
setInitialPasswordRequestModel.MasterPasswordHash,
setInitialPasswordRequestModel.Key,
setInitialPasswordRequestModel.OrgIdentifier)
.Returns(Task.FromResult(IdentityResult.Success));
// Act
if (shouldSucceed)
{
await _sut.PostSetPasswordAsync(setInitialPasswordRequestModel);
// Assert
await _setInitialMasterPasswordCommandV1.Received(1)
.SetInitialMasterPasswordAsync(
Arg.Is<User>(u => u == user),
Arg.Is<string>(s => s == setInitialPasswordRequestModel.MasterPasswordHash),
Arg.Is<string>(s => s == setInitialPasswordRequestModel.Key),
Arg.Is<string>(s => s == setInitialPasswordRequestModel.OrgIdentifier));
// Additional Assertions for User object modifications
Assert.Equal(setInitialPasswordRequestModel.MasterPasswordHint, user.MasterPasswordHint);
Assert.Equal(setInitialPasswordRequestModel.Kdf, user.Kdf);
Assert.Equal(setInitialPasswordRequestModel.KdfIterations, user.KdfIterations);
Assert.Equal(setInitialPasswordRequestModel.KdfMemory, user.KdfMemory);
Assert.Equal(setInitialPasswordRequestModel.KdfParallelism, user.KdfParallelism);
Assert.Equal(setInitialPasswordRequestModel.Key, user.Key);
}
else
{
await Assert.ThrowsAsync<BadRequestException>(() => _sut.PostSetPasswordAsync(setInitialPasswordRequestModel));
}
}
[Theory]
[BitAutoData]
public async Task PostSetPasswordAsync_V1_WhenUserExistsAndHasKeysAndKeysAreUpdated_ShouldThrowAsync(
User user,
SetInitialPasswordRequestModel setInitialPasswordRequestModel)
{
// Arrange
const string existingPublicKey = "existingPublicKey";
const string existingEncryptedPrivateKey = "existingEncryptedPrivateKey";
const string newPublicKey = "newPublicKey";
const string newEncryptedPrivateKey = "newEncryptedPrivateKey";
user.PublicKey = existingPublicKey;
user.PrivateKey = existingEncryptedPrivateKey;
UpdateSetInitialPasswordRequestModelToV1(setInitialPasswordRequestModel);
setInitialPasswordRequestModel.Keys = new KeysRequestModel()
{
PublicKey = newPublicKey,
EncryptedPrivateKey = newEncryptedPrivateKey
};
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(Task.FromResult(user));
_setInitialMasterPasswordCommandV1.SetInitialMasterPasswordAsync(
user,
setInitialPasswordRequestModel.MasterPasswordHash,
setInitialPasswordRequestModel.Key,
setInitialPasswordRequestModel.OrgIdentifier)
.Returns(Task.FromResult(IdentityResult.Success));
// Act & Assert
await Assert.ThrowsAsync<BadRequestException>(() => _sut.PostSetPasswordAsync(setInitialPasswordRequestModel));
}
[Theory]
[BitAutoData]
public async Task PostSetPasswordAsync_V1_WhenUserDoesNotExist_ShouldThrowUnauthorizedAccessException(
SetInitialPasswordRequestModel setInitialPasswordRequestModel)
{
UpdateSetInitialPasswordRequestModelToV1(setInitialPasswordRequestModel);
// Arrange
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(Task.FromResult((User)null));
// Act & Assert
await Assert.ThrowsAsync<UnauthorizedAccessException>(() => _sut.PostSetPasswordAsync(setInitialPasswordRequestModel));
}
[Theory]
[BitAutoData]
public async Task PostSetPasswordAsync_V1_WhenSettingPasswordFails_ShouldThrowBadRequestException(
User user,
SetInitialPasswordRequestModel model)
{
UpdateSetInitialPasswordRequestModelToV1(model);
model.Keys = null;
// Arrange
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(Task.FromResult(user));
_setInitialMasterPasswordCommandV1.SetInitialMasterPasswordAsync(Arg.Any<User>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>())
.Returns(Task.FromResult(IdentityResult.Failed(new IdentityError { Description = "Some Error" })));
// Act & Assert
await Assert.ThrowsAsync<BadRequestException>(() => _sut.PostSetPasswordAsync(model));
}
[Fact]
public async Task Delete_WithUserManagedByAnOrganization_ThrowsBadRequestException()
{
var user = GenerateExampleUser();
ConfigureUserServiceToReturnValidPrincipalFor(user);
ConfigureUserServiceToAcceptPasswordFor(user);
_userService.IsClaimedByAnyOrganizationAsync(user.Id).Returns(true);
var result = await Assert.ThrowsAsync<BadRequestException>(() => _sut.Delete(new SecretVerificationRequestModel()));
Assert.Equal("Cannot delete accounts owned by an organization. Contact your organization administrator for additional details.", result.Message);
}
[Fact]
public async Task Delete_WithUserNotManagedByAnOrganization_ShouldSucceed()
{
var user = GenerateExampleUser();
ConfigureUserServiceToReturnValidPrincipalFor(user);
ConfigureUserServiceToAcceptPasswordFor(user);
_userService.IsClaimedByAnyOrganizationAsync(user.Id).Returns(false);
_userService.DeleteAsync(user).Returns(IdentityResult.Success);
await _sut.Delete(new SecretVerificationRequestModel());
await _userService.Received(1).DeleteAsync(user);
}
[Theory]
[BitAutoData]
public async Task SetVerifyDevices_WhenUserDoesNotExist_ShouldThrowUnauthorizedAccessException(
SetVerifyDevicesRequestModel model)
{
// Arrange
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(Task.FromResult((User)null));
// Act & Assert
await Assert.ThrowsAsync<UnauthorizedAccessException>(() => _sut.SetUserVerifyDevicesAsync(model));
}
[Theory]
[BitAutoData]
public async Task SetVerifyDevices_WhenInvalidSecret_ShouldFail(
User user, SetVerifyDevicesRequestModel model)
{
// Arrange
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(Task.FromResult((user)));
_userService.VerifySecretAsync(user, Arg.Any<string>()).Returns(Task.FromResult(false));
// Act & Assert
await Assert.ThrowsAsync<BadRequestException>(() => _sut.SetUserVerifyDevicesAsync(model));
}
[Theory]
[BitAutoData]
public async Task SetVerifyDevices_WhenRequestValid_ShouldSucceed(
User user, SetVerifyDevicesRequestModel model)
{
// Arrange
user.VerifyDevices = false;
model.VerifyDevices = true;
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(Task.FromResult((user)));
_userService.VerifySecretAsync(user, Arg.Any<string>()).Returns(Task.FromResult(true));
// Act
await _sut.SetUserVerifyDevicesAsync(model);
await _userService.Received(1).SaveUserAsync(user);
Assert.Equal(model.VerifyDevices, user.VerifyDevices);
}
[Theory]
[BitAutoData]
public async Task ResendNewDeviceVerificationEmail_WhenUserNotFound_ShouldFail(
UnauthenticatedSecretVerificationRequestModel model)
{
// Arrange
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(Task.FromResult((User)null));
// Act & Assert
await Assert.ThrowsAsync<UnauthorizedAccessException>(() => _sut.ResendNewDeviceOtpAsync(model));
}
[Theory, BitAutoData]
public async Task ResendNewDeviceVerificationEmail_WhenSecretNotValid_ShouldFail(
User user,
UnauthenticatedSecretVerificationRequestModel model)
{
// Arrange
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(Task.FromResult(user));
_userService.VerifySecretAsync(user, Arg.Any<string>()).Returns(Task.FromResult(false));
// Act & Assert
await Assert.ThrowsAsync<BadRequestException>(() => _sut.ResendNewDeviceOtpAsync(model));
}
[Theory, BitAutoData]
public async Task ResendNewDeviceVerificationEmail_WhenTokenValid_SendsEmail(User user,
UnauthenticatedSecretVerificationRequestModel model)
{
// Arrange
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(Task.FromResult(user));
_userService.VerifySecretAsync(user, Arg.Any<string>()).Returns(Task.FromResult(true));
// Act
await _sut.ResendNewDeviceOtpAsync(model);
// Assert
await _twoFactorEmailService.Received(1).SendNewDeviceVerificationEmailAsync(user);
}
[Theory]
[BitAutoData]
public async Task PostKdf_UserNotFound_ShouldFail(ChangeKdfRequestModel model)
{
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(Task.FromResult<User>(null));
// Act
await Assert.ThrowsAsync<UnauthorizedAccessException>(() => _sut.PostKdf(model));
}
[Theory]
[BitAutoData]
public async Task PostKdf_WithNullAuthenticationData_ShouldFail(
User user, ChangeKdfRequestModel model)
{
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(Task.FromResult(user));
model.AuthenticationData = null;
// Act
var exception = await Assert.ThrowsAsync<BadRequestException>(() => _sut.PostKdf(model));
Assert.Contains("AuthenticationData and UnlockData must be provided.", exception.Message);
}
[Theory]
[BitAutoData]
public async Task PostKdf_WithNullUnlockData_ShouldFail(
User user, ChangeKdfRequestModel model)
{
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(Task.FromResult(user));
model.UnlockData = null;
// Act
var exception = await Assert.ThrowsAsync<BadRequestException>(() => _sut.PostKdf(model));
Assert.Contains("AuthenticationData and UnlockData must be provided.", exception.Message);
}
[Theory]
[BitAutoData]
public async Task PostKdf_ChangeKdfFailed_ShouldFail(
User user, ChangeKdfRequestModel model)
{
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(Task.FromResult(user));
_changeKdfCommand.ChangeKdfAsync(Arg.Any<User>(), Arg.Any<string>(),
Arg.Any<MasterPasswordAuthenticationData>(), Arg.Any<MasterPasswordUnlockData>())
.Returns(Task.FromResult(IdentityResult.Failed(new IdentityError { Description = "Change KDF failed" })));
// Act
var exception = await Assert.ThrowsAsync<BadRequestException>(() => _sut.PostKdf(model));
Assert.NotNull(exception.ModelState);
Assert.Contains("Change KDF failed",
exception.ModelState.Values.SelectMany(x => x.Errors).Select(x => x.ErrorMessage));
}
[Theory]
[BitAutoData]
public async Task PostKdf_ChangeKdfSuccess_NoError(
User user, ChangeKdfRequestModel model)
{
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(Task.FromResult(user));
_changeKdfCommand.ChangeKdfAsync(Arg.Any<User>(), Arg.Any<string>(),
Arg.Any<MasterPasswordAuthenticationData>(), Arg.Any<MasterPasswordUnlockData>())
.Returns(Task.FromResult(IdentityResult.Success));
// Act
await _sut.PostKdf(model);
}
[Theory]
[BitAutoData]
public async Task PostKeys_NoUser_Errors(KeysRequestModel model)
{
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(Task.FromResult<User>(null));
await Assert.ThrowsAsync<UnauthorizedAccessException>(() => _sut.PostKeys(model));
}
[Theory]
[BitAutoData("existing", "existing")]
[BitAutoData((string)null, "existing")]
[BitAutoData("", "existing")]
[BitAutoData(" ", "existing")]
[BitAutoData("existing", null)]
[BitAutoData("existing", "")]
[BitAutoData("existing", " ")]
public async Task PostKeys_UserAlreadyHasKeys_Errors(string? existingPrivateKey, string? existingPublicKey,
KeysRequestModel model)
{
var user = GenerateExampleUser();
user.PrivateKey = existingPrivateKey;
user.PublicKey = existingPublicKey;
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(Task.FromResult(user));
var exception = await Assert.ThrowsAsync<BadRequestException>(() => _sut.PostKeys(model));
Assert.NotNull(exception.Message);
Assert.Contains("User has existing keypair", exception.Message);
}
[Fact]
public async Task GetProfile_PoliciesInAcceptedState_FlagEnabled_PopulatesOrganizationsNew()
{
var user = GenerateExampleUser();
ConfigureUserServiceToReturnValidPrincipalFor(user);
_userService.GetOrganizationsClaimingUserAsync(user.Id).Returns(new List<Organization>());
_featureService.IsEnabled(FeatureFlagKeys.PoliciesInAcceptedState).Returns(true);
var acceptedOrganizationId = Guid.NewGuid();
var organizationsNew = new List<OrganizationUserOrganizationDetails>
{
new() { OrganizationId = acceptedOrganizationId }
};
_organizationUserRepository.GetManyConfirmedAcceptedDetailsByUserAsync(user.Id)
.Returns(organizationsNew);
var result = await _sut.GetProfile();
await _organizationUserRepository.Received(1).GetManyConfirmedAcceptedDetailsByUserAsync(user.Id);
Assert.NotNull(result.OrganizationsNew);
var returnedOrganization = Assert.Single(result.OrganizationsNew);
Assert.Equal(acceptedOrganizationId, returnedOrganization.Id);
}
[Fact]
public async Task GetProfile_PoliciesInAcceptedState_FlagDisabled_OrganizationsNewIsNull()
{
var user = GenerateExampleUser();
ConfigureUserServiceToReturnValidPrincipalFor(user);
_userService.GetOrganizationsClaimingUserAsync(user.Id).Returns(new List<Organization>());
_featureService.IsEnabled(FeatureFlagKeys.PoliciesInAcceptedState).Returns(false);
var result = await _sut.GetProfile();
await _organizationUserRepository.DidNotReceive().GetManyConfirmedAcceptedDetailsByUserAsync(Arg.Any<Guid>());
Assert.Null(result.OrganizationsNew);
}
// Below are helper functions that currently belong to this
// test class, but ultimately may need to be split out into
// something greater in order to share common test steps with
// other test suites. They are included here for the time being
// until that day comes.
private User GenerateExampleUser()
{
return new User
{
Email = "user@example.com"
};
}
private void ConfigureUserServiceToReturnNullPrincipal()
{
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())
.Returns(Task.FromResult((User)null));
}
private void ConfigureUserServiceToReturnValidPrincipalFor(User user)
{
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())
.Returns(Task.FromResult(user));
}
private void ConfigureUserServiceToRejectPasswordFor(User user)
{
_userService.CheckPasswordAsync(user, Arg.Any<string>())
.Returns(Task.FromResult(false));
}
private void ConfigureUserServiceToAcceptPasswordFor(User user)
{
_userService.CheckPasswordAsync(user, Arg.Any<string>())
.Returns(Task.FromResult(true));
_userService.VerifySecretAsync(user, Arg.Any<string>())
.Returns(Task.FromResult(true));
}
private void ConfigureUserServiceToReturnValidIdFor(User user)
{
_userService.GetUserByIdAsync(Arg.Any<Guid>())
.Returns(Task.FromResult(user));
}
private void ConfigureUserServiceToReturnNullUserId()
{
_userService.GetUserByIdAsync(Arg.Any<Guid>())
.Returns(Task.FromResult((User)null));
}
[Theory, BitAutoData]
public async Task PostKeys_WithAccountKeys_CallsSetV2AccountCryptographicState(
User user,
KeysRequestModel model)
{
// Arrange
user.PublicKey = null;
user.PrivateKey = null;
model.AccountKeys = new AccountKeysRequestModel
{
UserKeyEncryptedAccountPrivateKey = "wrapped-private-key",
AccountPublicKey = "public-key",
PublicKeyEncryptionKeyPair = new PublicKeyEncryptionKeyPairRequestModel
{
PublicKey = "public-key",
WrappedPrivateKey = "wrapped-private-key",
SignedPublicKey = "signed-public-key"
},
SignatureKeyPair = new SignatureKeyPairRequestModel
{
VerifyingKey = "verifying-key",
SignatureAlgorithm = "ed25519",
WrappedSigningKey = "wrapped-signing-key"
},
SecurityState = new SecurityStateModel
{
SecurityState = "security-state",
SecurityVersion = 2
}
};
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
// Act
var result = await _sut.PostKeys(model);
// Assert
await _userRepository.Received(1).SetV2AccountCryptographicStateAsync(
user.Id,
Arg.Any<UserAccountKeysData>());
await _userService.DidNotReceiveWithAnyArgs().SaveUserAsync(Arg.Any<User>());
Assert.NotNull(result);
Assert.Equal("keys", result.Object);
}
[Theory, BitAutoData]
public async Task PostKeys_WithoutAccountKeys_CallsSaveUser(
User user,
KeysRequestModel model)
{
// Arrange
user.PublicKey = null;
user.PrivateKey = null;
model.AccountKeys = null;
model.PublicKey = "public-key";
model.EncryptedPrivateKey = "encrypted-private-key";
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
// Act
var result = await _sut.PostKeys(model);
// Assert
await _userService.Received(1).SaveUserAsync(Arg.Is<User>(u =>
u.PublicKey == model.PublicKey &&
u.PrivateKey == model.EncryptedPrivateKey));
await _userRepository.DidNotReceiveWithAnyArgs()
.SetV2AccountCryptographicStateAsync(Arg.Any<Guid>(), Arg.Any<UserAccountKeysData>());
Assert.NotNull(result);
Assert.Equal("keys", result.Object);
}
[Theory]
[BitAutoData]
public async Task PostSetPasswordAsync_V2_WhenUserExistsAndSettingPasswordSucceeds_ShouldSetInitialMasterPassword(
User user,
SetInitialPasswordRequestModel setInitialPasswordRequestModel)
{
// Arrange
UpdateSetInitialPasswordRequestModelToV2(setInitialPasswordRequestModel);
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(Task.FromResult(user));
_finishSsoJitProvisionMasterPasswordCommand.FinishProvisionAsync(user, Arg.Any<SetInitialMasterPasswordDataModel>())
.Returns(Task.CompletedTask);
// Act
await _sut.PostSetPasswordAsync(setInitialPasswordRequestModel);
// Assert
await _finishSsoJitProvisionMasterPasswordCommand.Received(1)
.FinishProvisionAsync(
Arg.Is<User>(u => u == user),
Arg.Is<SetInitialMasterPasswordDataModel>(d =>
d.MasterPasswordAuthentication != null &&
d.MasterPasswordUnlock != null &&
d.AccountKeys != null &&
d.OrgSsoIdentifier == setInitialPasswordRequestModel.OrgIdentifier));
}
[Theory]
[BitAutoData]
public async Task PostSetPasswordAsync_V2_WithTdeSetPassword_ShouldCallTdeSetPasswordCommand(
User user,
SetInitialPasswordRequestModel setInitialPasswordRequestModel)
{
// Arrange
UpdateSetInitialPasswordRequestModelToV2(setInitialPasswordRequestModel, includeTdeSetPassword: true);
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(Task.FromResult(user));
_tdeSetPasswordCommand.SetMasterPasswordAsync(user, Arg.Any<SetInitialMasterPasswordDataModel>())
.Returns(Task.CompletedTask);
// Act
await _sut.PostSetPasswordAsync(setInitialPasswordRequestModel);
// Assert
await _tdeSetPasswordCommand.Received(1)
.SetMasterPasswordAsync(
Arg.Is<User>(u => u == user),
Arg.Is<SetInitialMasterPasswordDataModel>(d =>
d.MasterPasswordAuthentication != null &&
d.MasterPasswordUnlock != null &&
d.AccountKeys == null &&
d.OrgSsoIdentifier == setInitialPasswordRequestModel.OrgIdentifier));
}
[Theory]
[BitAutoData]
public async Task PostSetPasswordAsync_V2_WhenUserDoesNotExist_ShouldThrowUnauthorizedAccessException(
SetInitialPasswordRequestModel setInitialPasswordRequestModel)
{
// Arrange
UpdateSetInitialPasswordRequestModelToV2(setInitialPasswordRequestModel);
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(Task.FromResult((User)null));
// Act & Assert
await Assert.ThrowsAsync<UnauthorizedAccessException>(() => _sut.PostSetPasswordAsync(setInitialPasswordRequestModel));
}
[Theory]
[BitAutoData]
public async Task PostSetPasswordAsync_V2_WhenSettingPasswordFails_ShouldThrowException(
User user,
SetInitialPasswordRequestModel setInitialPasswordRequestModel)
{
// Arrange
UpdateSetInitialPasswordRequestModelToV2(setInitialPasswordRequestModel);
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(Task.FromResult(user));
_finishSsoJitProvisionMasterPasswordCommand.FinishProvisionAsync(user, Arg.Any<SetInitialMasterPasswordDataModel>())
.Returns(Task.FromException(new Exception("Setting password failed")));
// Act & Assert
await Assert.ThrowsAsync<Exception>(() => _sut.PostSetPasswordAsync(setInitialPasswordRequestModel));
}
private void UpdateSetInitialPasswordRequestModelToV1(SetInitialPasswordRequestModel model)
{
model.MasterPasswordAuthentication = null;
model.MasterPasswordUnlock = null;
model.AccountKeys = null;
}
private void UpdateSetInitialPasswordRequestModelToV2(SetInitialPasswordRequestModel model, bool includeTdeSetPassword = false)
{
var kdf = new KdfRequestModel
{
KdfType = KdfType.PBKDF2_SHA256,
Iterations = 600000
};
model.MasterPasswordAuthentication = new MasterPasswordAuthenticationDataRequestModel
{
Kdf = kdf,
MasterPasswordAuthenticationHash = "authHash",
Salt = "salt"
};
model.MasterPasswordUnlock = new MasterPasswordUnlockDataRequestModel
{
Kdf = kdf,
MasterKeyWrappedUserKey = "wrappedKey",
Salt = "salt"
};
if (includeTdeSetPassword)
{
// TDE set password does not include AccountKeys
model.AccountKeys = null;
}
else
{
model.AccountKeys = new AccountKeysRequestModel
{
UserKeyEncryptedAccountPrivateKey = "privateKey",
AccountPublicKey = "publicKey"
};
}
// Clear V1 properties
model.MasterPasswordHash = null;
model.Key = null;
model.Keys = null;
model.Kdf = null;
model.KdfIterations = null;
model.KdfMemory = null;
model.KdfParallelism = null;
}
}