1
0
mirror of https://github.com/bitwarden/server.git synced 2025-06-30 15:42:48 -05:00

[PM-15614] Allow Users to opt out of new device verification (#5176)

feat(NewDeviceVerification) : 
* Created database migration scripts for VerifyDevices column in [dbo].[User].
* Updated DeviceValidator to check if user has opted out of device verification.
* Added endpoint to AccountsController.cs to allow editing of new User.VerifyDevices property.
* Added tests for new methods and endpoint.
* Updating queries to track [dbo].[User].[VerifyDevices].
* Updated DeviceValidator to set `User.EmailVerified` property during the New Device Verification flow.
This commit is contained in:
Ike
2025-01-08 07:31:24 -08:00
committed by GitHub
parent 481a766cd2
commit a84ef0724c
21 changed files with 9459 additions and 9 deletions

View File

@ -563,6 +563,49 @@ public class AccountsControllerTests : IDisposable
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);
}
// 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

View File

@ -36,6 +36,7 @@ public class CloudICloudOrganizationSignUpCommandTests
signup.PremiumAccessAddon = false;
signup.UseSecretsManager = false;
signup.IsFromSecretsManagerTrial = false;
signup.IsFromProvider = false;
var result = await sutProvider.Sut.SignUpOrganizationAsync(signup);
@ -85,6 +86,7 @@ public class CloudICloudOrganizationSignUpCommandTests
signup.PaymentMethodType = PaymentMethodType.Card;
signup.PremiumAccessAddon = false;
signup.UseSecretsManager = false;
signup.IsFromProvider = false;
// Extract orgUserId when created
Guid? orgUserId = null;
@ -128,6 +130,8 @@ public class CloudICloudOrganizationSignUpCommandTests
signup.PaymentMethodType = PaymentMethodType.Card;
signup.PremiumAccessAddon = false;
signup.IsFromSecretsManagerTrial = false;
signup.IsFromProvider = false;
var result = await sutProvider.Sut.SignUpOrganizationAsync(signup);
@ -196,6 +200,7 @@ public class CloudICloudOrganizationSignUpCommandTests
signup.PremiumAccessAddon = false;
signup.AdditionalServiceAccounts = 10;
signup.AdditionalStorageGb = 0;
signup.IsFromProvider = false;
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SignUpOrganizationAsync(signup));
@ -213,6 +218,7 @@ public class CloudICloudOrganizationSignUpCommandTests
signup.PaymentMethodType = PaymentMethodType.Card;
signup.PremiumAccessAddon = false;
signup.AdditionalServiceAccounts = 10;
signup.IsFromProvider = false;
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SignUpOrganizationAsync(signup));
@ -230,6 +236,7 @@ public class CloudICloudOrganizationSignUpCommandTests
signup.PaymentMethodType = PaymentMethodType.Card;
signup.PremiumAccessAddon = false;
signup.AdditionalServiceAccounts = -10;
signup.IsFromProvider = false;
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SignUpOrganizationAsync(signup));

View File

@ -429,6 +429,30 @@ public class DeviceValidatorTests
Assert.Equal(expectedErrorMessage, actualResponse.Message);
}
[Theory, BitAutoData]
public async void HandleNewDeviceVerificationAsync_VerifyDevicesFalse_ReturnsSuccess(
CustomValidatorRequestContext context,
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request)
{
// Arrange
ArrangeForHandleNewDeviceVerificationTest(context, request);
_featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification).Returns(true);
_globalSettings.EnableNewDeviceVerification = true;
context.User.VerifyDevices = false;
// Act
var result = await _sut.ValidateRequestDeviceAsync(request, context);
// Assert
await _userService.Received(0).SendOTPAsync(context.User);
await _deviceService.Received(1).SaveAsync(Arg.Any<Device>());
Assert.True(result);
Assert.False(context.CustomResponse.ContainsKey("ErrorModel"));
Assert.Equal(context.User.Id, context.Device.UserId);
Assert.NotNull(context.Device);
}
[Theory, BitAutoData]
public async void HandleNewDeviceVerificationAsync_UserHasCacheValue_ReturnsSuccess(
CustomValidatorRequestContext context,