mirror of
https://github.com/bitwarden/server
synced 2026-01-20 09:23:28 +00:00
Merge branch 'main' into tools/pm-21918/send-authentication-commands
This commit is contained in:
@@ -29,6 +29,7 @@ using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http.HttpResults;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
@@ -305,29 +306,14 @@ public class OrganizationUsersControllerTests
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task DeleteAccount_WhenUserCanManageUsers_Success(
|
||||
Guid orgId, Guid id, User currentUser, SutProvider<OrganizationUsersController> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<ICurrentContext>().ManageUsers(orgId).Returns(true);
|
||||
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(currentUser);
|
||||
|
||||
await sutProvider.Sut.DeleteAccount(orgId, id);
|
||||
|
||||
await sutProvider.GetDependency<IDeleteClaimedOrganizationUserAccountCommand>()
|
||||
.Received(1)
|
||||
.DeleteUserAsync(orgId, id, currentUser.Id);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task DeleteAccount_WhenCurrentUserNotFound_ThrowsUnauthorizedAccessException(
|
||||
public async Task DeleteAccount_WhenCurrentUserNotFound_ReturnsUnauthorizedResult(
|
||||
Guid orgId, Guid id, SutProvider<OrganizationUsersController> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<ICurrentContext>().ManageUsers(orgId).Returns(true);
|
||||
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs((User)null);
|
||||
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs((Guid?)null);
|
||||
|
||||
await Assert.ThrowsAsync<UnauthorizedAccessException>(() =>
|
||||
sutProvider.Sut.DeleteAccount(orgId, id));
|
||||
var result = await sutProvider.Sut.DeleteAccount(orgId, id);
|
||||
|
||||
Assert.IsType<UnauthorizedHttpResult>(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
|
||||
@@ -10,6 +10,7 @@ using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||
using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.KeyManagement.Kdf;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
@@ -33,6 +34,7 @@ public class AccountsControllerTests : IDisposable
|
||||
private readonly ITdeOffboardingPasswordCommand _tdeOffboardingPasswordCommand;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly ITwoFactorEmailService _twoFactorEmailService;
|
||||
private readonly IChangeKdfCommand _changeKdfCommand;
|
||||
|
||||
|
||||
public AccountsControllerTests()
|
||||
@@ -47,7 +49,7 @@ public class AccountsControllerTests : IDisposable
|
||||
_tdeOffboardingPasswordCommand = Substitute.For<ITdeOffboardingPasswordCommand>();
|
||||
_featureService = Substitute.For<IFeatureService>();
|
||||
_twoFactorEmailService = Substitute.For<ITwoFactorEmailService>();
|
||||
|
||||
_changeKdfCommand = Substitute.For<IChangeKdfCommand>();
|
||||
|
||||
_sut = new AccountsController(
|
||||
_organizationService,
|
||||
@@ -59,7 +61,8 @@ public class AccountsControllerTests : IDisposable
|
||||
_tdeOffboardingPasswordCommand,
|
||||
_twoFactorIsEnabledQuery,
|
||||
_featureService,
|
||||
_twoFactorEmailService
|
||||
_twoFactorEmailService,
|
||||
_changeKdfCommand
|
||||
);
|
||||
}
|
||||
|
||||
@@ -242,12 +245,18 @@ public class AccountsControllerTests : IDisposable
|
||||
{
|
||||
var user = GenerateExampleUser();
|
||||
ConfigureUserServiceToReturnValidPrincipalFor(user);
|
||||
_userService.ChangePasswordAsync(user, default, default, default, default)
|
||||
_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());
|
||||
await _sut.PostPassword(new PasswordRequestModel
|
||||
{
|
||||
MasterPasswordHash = "masterPasswordHash",
|
||||
NewMasterPasswordHash = "newMasterPasswordHash",
|
||||
MasterPasswordHint = "masterPasswordHint",
|
||||
Key = "key"
|
||||
});
|
||||
|
||||
await _userService.Received(1).ChangePasswordAsync(user, default, default, default, default);
|
||||
await _userService.Received(1).ChangePasswordAsync(user, Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -256,7 +265,13 @@ public class AccountsControllerTests : IDisposable
|
||||
ConfigureUserServiceToReturnNullPrincipal();
|
||||
|
||||
await Assert.ThrowsAsync<UnauthorizedAccessException>(
|
||||
() => _sut.PostPassword(new PasswordRequestModel())
|
||||
() => _sut.PostPassword(new PasswordRequestModel
|
||||
{
|
||||
MasterPasswordHash = "masterPasswordHash",
|
||||
NewMasterPasswordHash = "newMasterPasswordHash",
|
||||
MasterPasswordHint = "masterPasswordHint",
|
||||
Key = "key"
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -265,11 +280,17 @@ public class AccountsControllerTests : IDisposable
|
||||
{
|
||||
var user = GenerateExampleUser();
|
||||
ConfigureUserServiceToReturnValidPrincipalFor(user);
|
||||
_userService.ChangePasswordAsync(user, default, default, default, default)
|
||||
_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())
|
||||
() => _sut.PostPassword(new PasswordRequestModel
|
||||
{
|
||||
MasterPasswordHash = "masterPasswordHash",
|
||||
NewMasterPasswordHash = "newMasterPasswordHash",
|
||||
MasterPasswordHint = "masterPasswordHint",
|
||||
Key = "key"
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -593,6 +614,30 @@ public class AccountsControllerTests : IDisposable
|
||||
await _twoFactorEmailService.Received(1).SendNewDeviceVerificationEmailAsync(user);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PostKdf_WithNullAuthenticationData_ShouldFail(
|
||||
User user, PasswordRequestModel model)
|
||||
{
|
||||
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(Task.FromResult(user));
|
||||
model.AuthenticationData = null;
|
||||
|
||||
// Act
|
||||
await Assert.ThrowsAsync<BadRequestException>(() => _sut.PostKdf(model));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PostKdf_WithNullUnlockData_ShouldFail(
|
||||
User user, PasswordRequestModel model)
|
||||
{
|
||||
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(Task.FromResult(user));
|
||||
model.UnlockData = null;
|
||||
|
||||
// Act
|
||||
await Assert.ThrowsAsync<BadRequestException>(() => _sut.PostKdf(model));
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
@@ -222,7 +222,7 @@ public class AuthRequestsControllerTests
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Put_ReturnsAuthRequest(
|
||||
public async Task Put_WithRequestNotApproved_ReturnsAuthRequest(
|
||||
SutProvider<AuthRequestsController> sutProvider,
|
||||
User user,
|
||||
AuthRequestUpdateRequestModel requestModel,
|
||||
@@ -230,6 +230,7 @@ public class AuthRequestsControllerTests
|
||||
{
|
||||
// Arrange
|
||||
SetBaseServiceUri(sutProvider);
|
||||
requestModel.RequestApproved = false; // Not an approval, so validation should be skipped
|
||||
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.GetProperUserId(Arg.Any<ClaimsPrincipal>())
|
||||
@@ -248,6 +249,117 @@ public class AuthRequestsControllerTests
|
||||
Assert.IsType<AuthRequestResponseModel>(result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Put_WithApprovedRequest_ValidatesAndReturnsAuthRequest(
|
||||
SutProvider<AuthRequestsController> sutProvider,
|
||||
User user,
|
||||
AuthRequestUpdateRequestModel requestModel,
|
||||
AuthRequest currentAuthRequest,
|
||||
AuthRequest updatedAuthRequest,
|
||||
List<PendingAuthRequestDetails> pendingRequests)
|
||||
{
|
||||
// Arrange
|
||||
SetBaseServiceUri(sutProvider);
|
||||
requestModel.RequestApproved = true; // Approval triggers validation
|
||||
currentAuthRequest.RequestDeviceIdentifier = "device-identifier-123";
|
||||
|
||||
// Setup pending requests - make the current request the most recent for its device
|
||||
var mostRecentForDevice = new PendingAuthRequestDetails(currentAuthRequest, Guid.NewGuid());
|
||||
pendingRequests.Add(mostRecentForDevice);
|
||||
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.GetProperUserId(Arg.Any<ClaimsPrincipal>())
|
||||
.Returns(user.Id);
|
||||
|
||||
// Setup validation dependencies
|
||||
sutProvider.GetDependency<IAuthRequestService>()
|
||||
.GetAuthRequestAsync(currentAuthRequest.Id, user.Id)
|
||||
.Returns(currentAuthRequest);
|
||||
|
||||
sutProvider.GetDependency<IAuthRequestRepository>()
|
||||
.GetManyPendingAuthRequestByUserId(user.Id)
|
||||
.Returns(pendingRequests);
|
||||
|
||||
sutProvider.GetDependency<IAuthRequestService>()
|
||||
.UpdateAuthRequestAsync(currentAuthRequest.Id, user.Id, requestModel)
|
||||
.Returns(updatedAuthRequest);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut
|
||||
.Put(currentAuthRequest.Id, requestModel);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.IsType<AuthRequestResponseModel>(result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Put_WithApprovedRequest_CurrentAuthRequestNotFound_ThrowsNotFoundException(
|
||||
SutProvider<AuthRequestsController> sutProvider,
|
||||
User user,
|
||||
AuthRequestUpdateRequestModel requestModel,
|
||||
Guid authRequestId)
|
||||
{
|
||||
// Arrange
|
||||
requestModel.RequestApproved = true; // Approval triggers validation
|
||||
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.GetProperUserId(Arg.Any<ClaimsPrincipal>())
|
||||
.Returns(user.Id);
|
||||
|
||||
// Current auth request not found
|
||||
sutProvider.GetDependency<IAuthRequestService>()
|
||||
.GetAuthRequestAsync(authRequestId, user.Id)
|
||||
.Returns((AuthRequest)null);
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<NotFoundException>(
|
||||
() => sutProvider.Sut.Put(authRequestId, requestModel));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Put_WithApprovedRequest_NotMostRecentForDevice_ThrowsBadRequestException(
|
||||
SutProvider<AuthRequestsController> sutProvider,
|
||||
User user,
|
||||
AuthRequestUpdateRequestModel requestModel,
|
||||
AuthRequest currentAuthRequest,
|
||||
List<PendingAuthRequestDetails> pendingRequests)
|
||||
{
|
||||
// Arrange
|
||||
requestModel.RequestApproved = true; // Approval triggers validation
|
||||
currentAuthRequest.RequestDeviceIdentifier = "device-identifier-123";
|
||||
|
||||
// Setup pending requests - make a different request the most recent for the same device
|
||||
var differentAuthRequest = new AuthRequest
|
||||
{
|
||||
Id = Guid.NewGuid(), // Different ID than current request
|
||||
RequestDeviceIdentifier = currentAuthRequest.RequestDeviceIdentifier,
|
||||
UserId = user.Id,
|
||||
Type = AuthRequestType.AuthenticateAndUnlock,
|
||||
CreationDate = DateTime.UtcNow
|
||||
};
|
||||
var mostRecentForDevice = new PendingAuthRequestDetails(differentAuthRequest, Guid.NewGuid());
|
||||
pendingRequests.Add(mostRecentForDevice);
|
||||
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.GetProperUserId(Arg.Any<ClaimsPrincipal>())
|
||||
.Returns(user.Id);
|
||||
|
||||
sutProvider.GetDependency<IAuthRequestService>()
|
||||
.GetAuthRequestAsync(currentAuthRequest.Id, user.Id)
|
||||
.Returns(currentAuthRequest);
|
||||
|
||||
sutProvider.GetDependency<IAuthRequestRepository>()
|
||||
.GetManyPendingAuthRequestByUserId(user.Id)
|
||||
.Returns(pendingRequests);
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.Put(currentAuthRequest.Id, requestModel));
|
||||
|
||||
Assert.Equal("This request is no longer valid. Make sure to approve the most recent request.", exception.Message);
|
||||
}
|
||||
|
||||
private void SetBaseServiceUri(SutProvider<AuthRequestsController> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<IGlobalSettings>()
|
||||
|
||||
@@ -18,7 +18,7 @@ public class MasterPasswordUnlockDataModelTests
|
||||
[InlineData(KdfType.Argon2id, 3, 64, 4)]
|
||||
public void Validate_Success(KdfType kdfType, int kdfIterations, int? kdfMemory, int? kdfParallelism)
|
||||
{
|
||||
var model = new MasterPasswordUnlockDataModel
|
||||
var model = new MasterPasswordUnlockAndAuthenticationDataModel
|
||||
{
|
||||
KdfType = kdfType,
|
||||
KdfIterations = kdfIterations,
|
||||
@@ -43,7 +43,7 @@ public class MasterPasswordUnlockDataModelTests
|
||||
[InlineData((KdfType)2, 2, 64, 4)]
|
||||
public void Validate_Failure(KdfType kdfType, int kdfIterations, int? kdfMemory, int? kdfParallelism)
|
||||
{
|
||||
var model = new MasterPasswordUnlockDataModel
|
||||
var model = new MasterPasswordUnlockAndAuthenticationDataModel
|
||||
{
|
||||
KdfType = kdfType,
|
||||
KdfIterations = kdfIterations,
|
||||
@@ -59,7 +59,7 @@ public class MasterPasswordUnlockDataModelTests
|
||||
Assert.NotNull(result.First().ErrorMessage);
|
||||
}
|
||||
|
||||
private static List<ValidationResult> Validate(MasterPasswordUnlockDataModel model)
|
||||
private static List<ValidationResult> Validate(MasterPasswordUnlockAndAuthenticationDataModel model)
|
||||
{
|
||||
var results = new List<ValidationResult>();
|
||||
Validator.TryValidateObject(model, new ValidationContext(model), results, true);
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Bit.Api.Auth.Models.Request.Accounts;
|
||||
using Bit.Core.Enums;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Api.Test.Models.Request.Accounts;
|
||||
|
||||
public class KdfRequestModelTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(KdfType.PBKDF2_SHA256, 1_000_000, null, null)] // Somewhere in the middle
|
||||
[InlineData(KdfType.PBKDF2_SHA256, 600_000, null, null)] // Right on the lower boundary
|
||||
[InlineData(KdfType.PBKDF2_SHA256, 2_000_000, null, null)] // Right on the upper boundary
|
||||
[InlineData(KdfType.Argon2id, 5, 500, 8)] // Somewhere in the middle
|
||||
[InlineData(KdfType.Argon2id, 2, 15, 1)] // Right on the lower boundary
|
||||
[InlineData(KdfType.Argon2id, 10, 1024, 16)] // Right on the upper boundary
|
||||
public void Validate_IsValid(KdfType kdfType, int? kdfIterations, int? kdfMemory, int? kdfParallelism)
|
||||
{
|
||||
var model = new KdfRequestModel
|
||||
{
|
||||
Kdf = kdfType,
|
||||
KdfIterations = kdfIterations,
|
||||
KdfMemory = kdfMemory,
|
||||
KdfParallelism = kdfParallelism,
|
||||
Key = "TEST",
|
||||
NewMasterPasswordHash = "TEST",
|
||||
};
|
||||
|
||||
var results = Validate(model);
|
||||
Assert.Empty(results);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(null, 350_000, null, null, 1)] // Although KdfType is nullable, it's marked as [Required]
|
||||
[InlineData(KdfType.PBKDF2_SHA256, 500_000, null, null, 1)] // Too few iterations
|
||||
[InlineData(KdfType.PBKDF2_SHA256, 2_000_001, null, null, 1)] // Too many iterations
|
||||
[InlineData(KdfType.Argon2id, 0, 30, 8, 1)] // Iterations must be greater than 0
|
||||
[InlineData(KdfType.Argon2id, 10, 14, 8, 1)] // Too little memory
|
||||
[InlineData(KdfType.Argon2id, 10, 14, 0, 1)] // Too small of a parallelism value
|
||||
[InlineData(KdfType.Argon2id, 10, 1025, 8, 1)] // Too much memory
|
||||
[InlineData(KdfType.Argon2id, 10, 512, 17, 1)] // Too big of a parallelism value
|
||||
public void Validate_Fails(KdfType? kdfType, int? kdfIterations, int? kdfMemory, int? kdfParallelism, int expectedFailures)
|
||||
{
|
||||
var model = new KdfRequestModel
|
||||
{
|
||||
Kdf = kdfType,
|
||||
KdfIterations = kdfIterations,
|
||||
KdfMemory = kdfMemory,
|
||||
KdfParallelism = kdfParallelism,
|
||||
Key = "TEST",
|
||||
NewMasterPasswordHash = "TEST",
|
||||
};
|
||||
|
||||
var results = Validate(model);
|
||||
Assert.NotEmpty(results);
|
||||
Assert.Equal(expectedFailures, results.Count);
|
||||
}
|
||||
|
||||
public static List<ValidationResult> Validate(KdfRequestModel model)
|
||||
{
|
||||
var results = new List<ValidationResult>();
|
||||
Validator.TryValidateObject(model, new ValidationContext(model), results);
|
||||
return results;
|
||||
}
|
||||
}
|
||||
36
test/Api.Test/Utilities/KdfSettingsValidatorTests.cs
Normal file
36
test/Api.Test/Utilities/KdfSettingsValidatorTests.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Utilities;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Api.Test.Utilities;
|
||||
|
||||
public class KdfSettingsValidatorTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(KdfType.PBKDF2_SHA256, 1_000_000, null, null)] // Somewhere in the middle
|
||||
[InlineData(KdfType.PBKDF2_SHA256, 600_000, null, null)] // Right on the lower boundary
|
||||
[InlineData(KdfType.PBKDF2_SHA256, 2_000_000, null, null)] // Right on the upper boundary
|
||||
[InlineData(KdfType.Argon2id, 5, 500, 8)] // Somewhere in the middle
|
||||
[InlineData(KdfType.Argon2id, 2, 15, 1)] // Right on the lower boundary
|
||||
[InlineData(KdfType.Argon2id, 10, 1024, 16)] // Right on the upper boundary
|
||||
public void Validate_IsValid(KdfType kdfType, int kdfIterations, int? kdfMemory, int? kdfParallelism)
|
||||
{
|
||||
var results = KdfSettingsValidator.Validate(kdfType, kdfIterations, kdfMemory, kdfParallelism);
|
||||
Assert.Empty(results);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(KdfType.PBKDF2_SHA256, 500_000, null, null, 1)] // Too few iterations
|
||||
[InlineData(KdfType.PBKDF2_SHA256, 2_000_001, null, null, 1)] // Too many iterations
|
||||
[InlineData(KdfType.Argon2id, 0, 30, 8, 1)] // Iterations must be greater than 0
|
||||
[InlineData(KdfType.Argon2id, 10, 14, 8, 1)] // Too little memory
|
||||
[InlineData(KdfType.Argon2id, 10, 14, 0, 1)] // Too small of a parallelism value
|
||||
[InlineData(KdfType.Argon2id, 10, 1025, 8, 1)] // Too much memory
|
||||
[InlineData(KdfType.Argon2id, 10, 512, 17, 1)] // Too big of a parallelism value
|
||||
public void Validate_Fails(KdfType kdfType, int kdfIterations, int? kdfMemory, int? kdfParallelism, int expectedFailures)
|
||||
{
|
||||
var results = KdfSettingsValidator.Validate(kdfType, kdfIterations, kdfMemory, kdfParallelism);
|
||||
Assert.NotEmpty(results);
|
||||
Assert.Equal(expectedFailures, results.Count());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user