mirror of
https://github.com/bitwarden/server
synced 2026-01-16 07:23:15 +00:00
Merge branch 'main' into tools/pm-21918/send-authentication-commands
This commit is contained in:
@@ -7,7 +7,7 @@ using Bit.Api.Models.Request;
|
||||
using Bit.Api.Models.Response;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccountvNext;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Entities;
|
||||
@@ -33,10 +33,6 @@ public class OrganizationUserControllerTests : IClassFixture<ApiApplicationFacto
|
||||
featureService
|
||||
.IsEnabled(FeatureFlagKeys.CreateDefaultLocation)
|
||||
.Returns(true);
|
||||
|
||||
featureService
|
||||
.IsEnabled(FeatureFlagKeys.DeleteClaimedUserAccountRefactor)
|
||||
.Returns(true);
|
||||
});
|
||||
_client = _factory.CreateClient();
|
||||
_loginHelper = new LoginHelper(_factory, _client);
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Core.Platform.Push;
|
||||
using Bit.Core.Repositories;
|
||||
@@ -471,18 +472,32 @@ public class ConfirmOrganizationUserCommandTests
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.CreateDefaultLocation).Returns(true);
|
||||
|
||||
var policyDetails = new PolicyDetails
|
||||
{
|
||||
OrganizationId = organization.Id,
|
||||
OrganizationUserId = orgUser.Id,
|
||||
IsProvider = false,
|
||||
OrganizationUserStatus = orgUser.Status,
|
||||
OrganizationUserType = orgUser.Type,
|
||||
PolicyType = PolicyType.OrganizationDataOwnership
|
||||
};
|
||||
sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||
.GetManyByOrganizationIdAsync<OrganizationDataOwnershipPolicyRequirement>(organization.Id)
|
||||
.Returns(new List<Guid> { orgUser.Id });
|
||||
.GetAsync<OrganizationDataOwnershipPolicyRequirement>(orgUser.UserId!.Value)
|
||||
.Returns(new OrganizationDataOwnershipPolicyRequirement(OrganizationDataOwnershipState.Enabled, [policyDetails]));
|
||||
|
||||
await sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id, collectionName);
|
||||
|
||||
await sutProvider.GetDependency<ICollectionRepository>()
|
||||
.Received(1)
|
||||
.UpsertDefaultCollectionsAsync(
|
||||
organization.Id,
|
||||
Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(orgUser.Id)),
|
||||
collectionName);
|
||||
.CreateAsync(
|
||||
Arg.Is<Collection>(c =>
|
||||
c.Name == collectionName &&
|
||||
c.OrganizationId == organization.Id &&
|
||||
c.Type == CollectionType.DefaultUserCollection),
|
||||
Arg.Any<IEnumerable<CollectionAccessSelection>>(),
|
||||
Arg.Is<IEnumerable<CollectionAccessSelection>>(cu =>
|
||||
cu.Single().Id == orgUser.Id &&
|
||||
cu.Single().Manage));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
@@ -511,7 +526,7 @@ public class ConfirmOrganizationUserCommandTests
|
||||
[Theory, BitAutoData]
|
||||
public async Task ConfirmUserAsync_WithCreateDefaultLocationEnabled_WithOrganizationDataOwnershipPolicyNotApplicable_DoesNotCreateDefaultCollection(
|
||||
Organization org, OrganizationUser confirmingUser,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted, OrganizationUserType.Owner)] OrganizationUser orgUser, User user,
|
||||
string key, string collectionName, SutProvider<ConfirmOrganizationUserCommand> sutProvider)
|
||||
{
|
||||
org.PlanType = PlanType.EnterpriseAnnually;
|
||||
@@ -523,9 +538,18 @@ public class ConfirmOrganizationUserCommandTests
|
||||
sutProvider.GetDependency<IUserRepository>().GetManyAsync(default).ReturnsForAnyArgs(new[] { user });
|
||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.CreateDefaultLocation).Returns(true);
|
||||
|
||||
var policyDetails = new PolicyDetails
|
||||
{
|
||||
OrganizationId = org.Id,
|
||||
OrganizationUserId = orgUser.Id,
|
||||
IsProvider = false,
|
||||
OrganizationUserStatus = orgUser.Status,
|
||||
OrganizationUserType = orgUser.Type,
|
||||
PolicyType = PolicyType.OrganizationDataOwnership
|
||||
};
|
||||
sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||
.GetManyByOrganizationIdAsync<OrganizationDataOwnershipPolicyRequirement>(org.Id)
|
||||
.Returns(new List<Guid> { orgUser.UserId!.Value });
|
||||
.GetAsync<OrganizationDataOwnershipPolicyRequirement>(orgUser.UserId!.Value)
|
||||
.Returns(new OrganizationDataOwnershipPolicyRequirement(OrganizationDataOwnershipState.Disabled, [policyDetails]));
|
||||
|
||||
await sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id, collectionName);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccountvNext;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
@@ -17,12 +17,12 @@ using Xunit;
|
||||
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccountvNext;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class DeleteClaimedOrganizationUserAccountCommandvNextTests
|
||||
public class DeleteClaimedOrganizationUserAccountCommandTests
|
||||
{
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task DeleteUserAsync_WithValidSingleUser_CallsDeleteManyUsersAsync(
|
||||
SutProvider<DeleteClaimedOrganizationUserAccountCommandvNext> sutProvider,
|
||||
SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider,
|
||||
User user,
|
||||
Guid organizationId,
|
||||
Guid deletingUserId,
|
||||
@@ -65,7 +65,7 @@ public class DeleteClaimedOrganizationUserAccountCommandvNextTests
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task DeleteManyUsersAsync_WithEmptyUserIds_ReturnsEmptyResults(
|
||||
SutProvider<DeleteClaimedOrganizationUserAccountCommandvNext> sutProvider,
|
||||
SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider,
|
||||
Guid organizationId,
|
||||
Guid deletingUserId)
|
||||
{
|
||||
@@ -77,7 +77,7 @@ public class DeleteClaimedOrganizationUserAccountCommandvNextTests
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task DeleteManyUsersAsync_WithValidUsers_DeletesUsersAndLogsEvents(
|
||||
SutProvider<DeleteClaimedOrganizationUserAccountCommandvNext> sutProvider,
|
||||
SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider,
|
||||
User user1,
|
||||
User user2,
|
||||
Guid organizationId,
|
||||
@@ -135,7 +135,7 @@ public class DeleteClaimedOrganizationUserAccountCommandvNextTests
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task DeleteManyUsersAsync_WithValidationErrors_ReturnsErrorResults(
|
||||
SutProvider<DeleteClaimedOrganizationUserAccountCommandvNext> sutProvider,
|
||||
SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider,
|
||||
Guid organizationId,
|
||||
Guid orgUserId1,
|
||||
Guid orgUserId2,
|
||||
@@ -183,7 +183,7 @@ public class DeleteClaimedOrganizationUserAccountCommandvNextTests
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task DeleteManyUsersAsync_WithMixedValidationResults_HandlesPartialSuccessCorrectly(
|
||||
SutProvider<DeleteClaimedOrganizationUserAccountCommandvNext> sutProvider,
|
||||
SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider,
|
||||
User validUser,
|
||||
Guid organizationId,
|
||||
Guid validOrgUserId,
|
||||
@@ -243,7 +243,7 @@ public class DeleteClaimedOrganizationUserAccountCommandvNextTests
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task DeleteManyUsersAsync_CancelPremiumsAsync_HandlesGatewayExceptionAndLogsWarning(
|
||||
SutProvider<DeleteClaimedOrganizationUserAccountCommandvNext> sutProvider,
|
||||
SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider,
|
||||
User user,
|
||||
Guid organizationId,
|
||||
Guid deletingUserId,
|
||||
@@ -285,7 +285,7 @@ public class DeleteClaimedOrganizationUserAccountCommandvNextTests
|
||||
await sutProvider.GetDependency<IUserService>().Received(1).CancelPremiumAsync(user);
|
||||
await AssertSuccessfulUserOperations(sutProvider, [user], [orgUser]);
|
||||
|
||||
sutProvider.GetDependency<ILogger<DeleteClaimedOrganizationUserAccountCommandvNext>>()
|
||||
sutProvider.GetDependency<ILogger<DeleteClaimedOrganizationUserAccountCommand>>()
|
||||
.Received(1)
|
||||
.Log(
|
||||
LogLevel.Warning,
|
||||
@@ -299,7 +299,7 @@ public class DeleteClaimedOrganizationUserAccountCommandvNextTests
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task CreateInternalRequests_CreatesCorrectRequestsForAllUsers(
|
||||
SutProvider<DeleteClaimedOrganizationUserAccountCommandvNext> sutProvider,
|
||||
SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider,
|
||||
User user1,
|
||||
User user2,
|
||||
Guid organizationId,
|
||||
@@ -326,7 +326,7 @@ public class DeleteClaimedOrganizationUserAccountCommandvNextTests
|
||||
.GetUsersOrganizationClaimedStatusAsync(organizationId, Arg.Any<IEnumerable<Guid>>())
|
||||
.Returns(claimedStatuses);
|
||||
|
||||
sutProvider.GetDependency<IDeleteClaimedOrganizationUserAccountValidatorvNext>()
|
||||
sutProvider.GetDependency<IDeleteClaimedOrganizationUserAccountValidator>()
|
||||
.ValidateAsync(Arg.Any<IEnumerable<DeleteUserValidationRequest>>())
|
||||
.Returns(callInfo =>
|
||||
{
|
||||
@@ -338,7 +338,7 @@ public class DeleteClaimedOrganizationUserAccountCommandvNextTests
|
||||
await sutProvider.Sut.DeleteManyUsersAsync(organizationId, orgUserIds, deletingUserId);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IDeleteClaimedOrganizationUserAccountValidatorvNext>()
|
||||
await sutProvider.GetDependency<IDeleteClaimedOrganizationUserAccountValidator>()
|
||||
.Received(1)
|
||||
.ValidateAsync(Arg.Is<IEnumerable<DeleteUserValidationRequest>>(requests =>
|
||||
requests.Count() == 2 &&
|
||||
@@ -359,7 +359,7 @@ public class DeleteClaimedOrganizationUserAccountCommandvNextTests
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetUsersAsync_WithNullUserIds_ReturnsEmptyCollection(
|
||||
SutProvider<DeleteClaimedOrganizationUserAccountCommandvNext> sutProvider,
|
||||
SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider,
|
||||
Guid organizationId,
|
||||
Guid deletingUserId,
|
||||
[OrganizationUser] OrganizationUser orgUserWithoutUserId)
|
||||
@@ -374,7 +374,7 @@ public class DeleteClaimedOrganizationUserAccountCommandvNextTests
|
||||
.GetManyAsync(Arg.Is<IEnumerable<Guid>>(ids => !ids.Any()))
|
||||
.Returns([]);
|
||||
|
||||
sutProvider.GetDependency<IDeleteClaimedOrganizationUserAccountValidatorvNext>()
|
||||
sutProvider.GetDependency<IDeleteClaimedOrganizationUserAccountValidator>()
|
||||
.ValidateAsync(Arg.Any<IEnumerable<DeleteUserValidationRequest>>())
|
||||
.Returns(callInfo =>
|
||||
{
|
||||
@@ -386,7 +386,7 @@ public class DeleteClaimedOrganizationUserAccountCommandvNextTests
|
||||
await sutProvider.Sut.DeleteManyUsersAsync(organizationId, [orgUserWithoutUserId.Id], deletingUserId);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IDeleteClaimedOrganizationUserAccountValidatorvNext>()
|
||||
await sutProvider.GetDependency<IDeleteClaimedOrganizationUserAccountValidator>()
|
||||
.Received(1)
|
||||
.ValidateAsync(Arg.Is<IEnumerable<DeleteUserValidationRequest>>(requests =>
|
||||
requests.Count() == 1 &&
|
||||
@@ -406,7 +406,7 @@ public class DeleteClaimedOrganizationUserAccountCommandvNextTests
|
||||
ValidationResultHelpers.Invalid(request, error);
|
||||
|
||||
private static void SetupRepositoryMocks(
|
||||
SutProvider<DeleteClaimedOrganizationUserAccountCommandvNext> sutProvider,
|
||||
SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider,
|
||||
ICollection<OrganizationUser> orgUsers,
|
||||
IEnumerable<User> users,
|
||||
Guid organizationId,
|
||||
@@ -426,16 +426,16 @@ public class DeleteClaimedOrganizationUserAccountCommandvNextTests
|
||||
}
|
||||
|
||||
private static void SetupValidatorMock(
|
||||
SutProvider<DeleteClaimedOrganizationUserAccountCommandvNext> sutProvider,
|
||||
SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider,
|
||||
IEnumerable<ValidationResult<DeleteUserValidationRequest>> validationResults)
|
||||
{
|
||||
sutProvider.GetDependency<IDeleteClaimedOrganizationUserAccountValidatorvNext>()
|
||||
sutProvider.GetDependency<IDeleteClaimedOrganizationUserAccountValidator>()
|
||||
.ValidateAsync(Arg.Any<IEnumerable<DeleteUserValidationRequest>>())
|
||||
.Returns(validationResults);
|
||||
}
|
||||
|
||||
private static async Task AssertSuccessfulUserOperations(
|
||||
SutProvider<DeleteClaimedOrganizationUserAccountCommandvNext> sutProvider,
|
||||
SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider,
|
||||
IEnumerable<User> expectedUsers,
|
||||
IEnumerable<OrganizationUser> expectedOrgUsers)
|
||||
{
|
||||
@@ -457,7 +457,7 @@ public class DeleteClaimedOrganizationUserAccountCommandvNextTests
|
||||
events.Any(e => e.Item1.Id == expectedOrgUser.Id && e.Item2 == EventType.OrganizationUser_Deleted))));
|
||||
}
|
||||
|
||||
private static async Task AssertNoUserOperations(SutProvider<DeleteClaimedOrganizationUserAccountCommandvNext> sutProvider)
|
||||
private static async Task AssertNoUserOperations(SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider)
|
||||
{
|
||||
await sutProvider.GetDependency<IUserRepository>().DidNotReceiveWithAnyArgs().DeleteManyAsync(default);
|
||||
await sutProvider.GetDependency<IPushNotificationService>().DidNotReceiveWithAnyArgs().PushLogOutAsync(default);
|
||||
@@ -1,4 +1,4 @@
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccountvNext;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
@@ -13,12 +13,12 @@ using Xunit;
|
||||
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccountvNext;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class DeleteClaimedOrganizationUserAccountValidatorvNextTests
|
||||
public class DeleteClaimedOrganizationUserAccountValidatorTests
|
||||
{
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task ValidateAsync_WithValidSingleRequest_ReturnsValidResult(
|
||||
SutProvider<DeleteClaimedOrganizationUserAccountValidatorvNext> sutProvider,
|
||||
SutProvider<DeleteClaimedOrganizationUserAccountValidator> sutProvider,
|
||||
User user,
|
||||
Guid organizationId,
|
||||
Guid deletingUserId,
|
||||
@@ -50,7 +50,7 @@ public class DeleteClaimedOrganizationUserAccountValidatorvNextTests
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task ValidateAsync_WithMultipleValidRequests_ReturnsAllValidResults(
|
||||
SutProvider<DeleteClaimedOrganizationUserAccountValidatorvNext> sutProvider,
|
||||
SutProvider<DeleteClaimedOrganizationUserAccountValidator> sutProvider,
|
||||
User user1,
|
||||
User user2,
|
||||
Guid organizationId,
|
||||
@@ -97,7 +97,7 @@ public class DeleteClaimedOrganizationUserAccountValidatorvNextTests
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task ValidateAsync_WithNullUser_ReturnsUserNotFoundError(
|
||||
SutProvider<DeleteClaimedOrganizationUserAccountValidatorvNext> sutProvider,
|
||||
SutProvider<DeleteClaimedOrganizationUserAccountValidator> sutProvider,
|
||||
Guid organizationId,
|
||||
Guid deletingUserId,
|
||||
[OrganizationUser] OrganizationUser organizationUser)
|
||||
@@ -123,7 +123,7 @@ public class DeleteClaimedOrganizationUserAccountValidatorvNextTests
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task ValidateAsync_WithNullOrganizationUser_ReturnsUserNotFoundError(
|
||||
SutProvider<DeleteClaimedOrganizationUserAccountValidatorvNext> sutProvider,
|
||||
SutProvider<DeleteClaimedOrganizationUserAccountValidator> sutProvider,
|
||||
User user,
|
||||
Guid organizationId,
|
||||
Guid deletingUserId)
|
||||
@@ -149,7 +149,7 @@ public class DeleteClaimedOrganizationUserAccountValidatorvNextTests
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task ValidateAsync_WithInvitedUser_ReturnsInvalidUserStatusError(
|
||||
SutProvider<DeleteClaimedOrganizationUserAccountValidatorvNext> sutProvider,
|
||||
SutProvider<DeleteClaimedOrganizationUserAccountValidator> sutProvider,
|
||||
User user,
|
||||
Guid organizationId,
|
||||
Guid deletingUserId,
|
||||
@@ -178,7 +178,7 @@ public class DeleteClaimedOrganizationUserAccountValidatorvNextTests
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task ValidateAsync_WhenDeletingYourself_ReturnsCannotDeleteYourselfError(
|
||||
SutProvider<DeleteClaimedOrganizationUserAccountValidatorvNext> sutProvider,
|
||||
SutProvider<DeleteClaimedOrganizationUserAccountValidator> sutProvider,
|
||||
User user,
|
||||
Guid organizationId,
|
||||
[OrganizationUser] OrganizationUser organizationUser)
|
||||
@@ -206,7 +206,7 @@ public class DeleteClaimedOrganizationUserAccountValidatorvNextTests
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task ValidateAsync_WithUnclaimedUser_ReturnsUserNotClaimedError(
|
||||
SutProvider<DeleteClaimedOrganizationUserAccountValidatorvNext> sutProvider,
|
||||
SutProvider<DeleteClaimedOrganizationUserAccountValidator> sutProvider,
|
||||
User user,
|
||||
Guid organizationId,
|
||||
Guid deletingUserId,
|
||||
@@ -235,7 +235,7 @@ public class DeleteClaimedOrganizationUserAccountValidatorvNextTests
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task ValidateAsync_DeletingOwnerWhenCurrentUserIsNotOwner_ReturnsCannotDeleteOwnersError(
|
||||
SutProvider<DeleteClaimedOrganizationUserAccountValidatorvNext> sutProvider,
|
||||
SutProvider<DeleteClaimedOrganizationUserAccountValidator> sutProvider,
|
||||
User user,
|
||||
Guid organizationId,
|
||||
Guid deletingUserId,
|
||||
@@ -266,7 +266,7 @@ public class DeleteClaimedOrganizationUserAccountValidatorvNextTests
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task ValidateAsync_DeletingOwnerWhenCurrentUserIsOwner_ReturnsValidResult(
|
||||
SutProvider<DeleteClaimedOrganizationUserAccountValidatorvNext> sutProvider,
|
||||
SutProvider<DeleteClaimedOrganizationUserAccountValidator> sutProvider,
|
||||
User user,
|
||||
Guid organizationId,
|
||||
Guid deletingUserId,
|
||||
@@ -296,7 +296,7 @@ public class DeleteClaimedOrganizationUserAccountValidatorvNextTests
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task ValidateAsync_WithSoleOwnerOfOrganization_ReturnsSoleOwnerError(
|
||||
SutProvider<DeleteClaimedOrganizationUserAccountValidatorvNext> sutProvider,
|
||||
SutProvider<DeleteClaimedOrganizationUserAccountValidator> sutProvider,
|
||||
User user,
|
||||
Guid organizationId,
|
||||
Guid deletingUserId,
|
||||
@@ -331,7 +331,7 @@ public class DeleteClaimedOrganizationUserAccountValidatorvNextTests
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task ValidateAsync_WithSoleProviderOwner_ReturnsSoleProviderError(
|
||||
SutProvider<DeleteClaimedOrganizationUserAccountValidatorvNext> sutProvider,
|
||||
SutProvider<DeleteClaimedOrganizationUserAccountValidator> sutProvider,
|
||||
User user,
|
||||
Guid organizationId,
|
||||
Guid deletingUserId,
|
||||
@@ -366,7 +366,7 @@ public class DeleteClaimedOrganizationUserAccountValidatorvNextTests
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task ValidateAsync_CustomUserDeletingAdmin_ReturnsCannotDeleteAdminsError(
|
||||
SutProvider<DeleteClaimedOrganizationUserAccountValidatorvNext> sutProvider,
|
||||
SutProvider<DeleteClaimedOrganizationUserAccountValidator> sutProvider,
|
||||
User user,
|
||||
Guid organizationId,
|
||||
Guid deletingUserId,
|
||||
@@ -397,7 +397,7 @@ public class DeleteClaimedOrganizationUserAccountValidatorvNextTests
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task ValidateAsync_AdminDeletingAdmin_ReturnsValidResult(
|
||||
SutProvider<DeleteClaimedOrganizationUserAccountValidatorvNext> sutProvider,
|
||||
SutProvider<DeleteClaimedOrganizationUserAccountValidator> sutProvider,
|
||||
User user,
|
||||
Guid organizationId,
|
||||
Guid deletingUserId,
|
||||
@@ -427,7 +427,7 @@ public class DeleteClaimedOrganizationUserAccountValidatorvNextTests
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task ValidateAsync_WithMixedValidAndInvalidRequests_ReturnsCorrespondingResults(
|
||||
SutProvider<DeleteClaimedOrganizationUserAccountValidatorvNext> sutProvider,
|
||||
SutProvider<DeleteClaimedOrganizationUserAccountValidator> sutProvider,
|
||||
User validUser,
|
||||
User invalidUser,
|
||||
Guid organizationId,
|
||||
@@ -475,7 +475,7 @@ public class DeleteClaimedOrganizationUserAccountValidatorvNextTests
|
||||
}
|
||||
|
||||
private static void SetupMocks(
|
||||
SutProvider<DeleteClaimedOrganizationUserAccountValidatorvNext> sutProvider,
|
||||
SutProvider<DeleteClaimedOrganizationUserAccountValidator> sutProvider,
|
||||
Guid organizationId,
|
||||
Guid userId,
|
||||
OrganizationUserType currentUserType = OrganizationUserType.Owner)
|
||||
@@ -1,526 +0,0 @@
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
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 DeleteClaimedOrganizationUserAccountCommandTests
|
||||
{
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task DeleteUserAsync_WithValidUser_DeletesUserAndLogsEvent(
|
||||
SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider, User user, Guid deletingUserId,
|
||||
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.User)] OrganizationUser organizationUser)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.UserId = user.Id;
|
||||
|
||||
sutProvider.GetDependency<IUserRepository>()
|
||||
.GetByIdAsync(user.Id)
|
||||
.Returns(user);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetByIdAsync(organizationUser.Id)
|
||||
.Returns(organizationUser);
|
||||
|
||||
sutProvider.GetDependency<IGetOrganizationUsersClaimedStatusQuery>()
|
||||
.GetUsersOrganizationClaimedStatusAsync(
|
||||
organizationUser.OrganizationId,
|
||||
Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(organizationUser.Id)))
|
||||
.Returns(new Dictionary<Guid, bool> { { organizationUser.Id, true } });
|
||||
|
||||
sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()
|
||||
.HasConfirmedOwnersExceptAsync(
|
||||
organizationUser.OrganizationId,
|
||||
Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(organizationUser.Id)),
|
||||
includeProvider: Arg.Any<bool>())
|
||||
.Returns(true);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.DeleteUserAsync(organizationUser.OrganizationId, organizationUser.Id, deletingUserId);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IUserService>().Received(1).DeleteAsync(user);
|
||||
await sutProvider.GetDependency<IEventService>().Received(1)
|
||||
.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Deleted);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task DeleteUserAsync_WithUserNotFound_ThrowsException(
|
||||
SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider,
|
||||
Guid organizationId, Guid organizationUserId)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetByIdAsync(organizationUserId)
|
||||
.Returns((OrganizationUser?)null);
|
||||
|
||||
// Act
|
||||
var exception = await Assert.ThrowsAsync<NotFoundException>(() =>
|
||||
sutProvider.Sut.DeleteUserAsync(organizationId, organizationUserId, null));
|
||||
|
||||
// Assert
|
||||
Assert.Equal("Member not found.", exception.Message);
|
||||
await sutProvider.GetDependency<IUserService>().Received(0).DeleteAsync(Arg.Any<User>());
|
||||
await sutProvider.GetDependency<IEventService>().Received(0)
|
||||
.LogOrganizationUserEventAsync(Arg.Any<OrganizationUser>(), Arg.Any<EventType>(), Arg.Any<DateTime?>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task DeleteUserAsync_DeletingYourself_ThrowsException(
|
||||
SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider,
|
||||
User user,
|
||||
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.User)] OrganizationUser organizationUser,
|
||||
Guid deletingUserId)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.UserId = user.Id = deletingUserId;
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetByIdAsync(organizationUser.Id)
|
||||
.Returns(organizationUser);
|
||||
|
||||
sutProvider.GetDependency<IUserRepository>().GetByIdAsync(user.Id)
|
||||
.Returns(user);
|
||||
|
||||
// Act
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
|
||||
sutProvider.Sut.DeleteUserAsync(organizationUser.OrganizationId, organizationUser.Id, deletingUserId));
|
||||
|
||||
// Assert
|
||||
Assert.Equal("You cannot delete yourself.", exception.Message);
|
||||
await sutProvider.GetDependency<IUserService>().Received(0).DeleteAsync(Arg.Any<User>());
|
||||
await sutProvider.GetDependency<IEventService>().Received(0)
|
||||
.LogOrganizationUserEventAsync(Arg.Any<OrganizationUser>(), Arg.Any<EventType>(), Arg.Any<DateTime?>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task DeleteUserAsync_WhenUserIsInvited_ThrowsException(
|
||||
SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider,
|
||||
[OrganizationUser(OrganizationUserStatusType.Invited, OrganizationUserType.User)] OrganizationUser organizationUser)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.UserId = null;
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetByIdAsync(organizationUser.Id)
|
||||
.Returns(organizationUser);
|
||||
|
||||
// Act
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
|
||||
sutProvider.Sut.DeleteUserAsync(organizationUser.OrganizationId, organizationUser.Id, null));
|
||||
|
||||
// Assert
|
||||
Assert.Equal("You cannot delete a member with Invited status.", exception.Message);
|
||||
await sutProvider.GetDependency<IUserService>().Received(0).DeleteAsync(Arg.Any<User>());
|
||||
await sutProvider.GetDependency<IEventService>().Received(0)
|
||||
.LogOrganizationUserEventAsync(Arg.Any<OrganizationUser>(), Arg.Any<EventType>(), Arg.Any<DateTime?>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task DeleteUserAsync_WhenCustomUserDeletesAdmin_ThrowsException(
|
||||
SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider, User user,
|
||||
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Admin)] OrganizationUser organizationUser,
|
||||
Guid deletingUserId)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.UserId = user.Id;
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetByIdAsync(organizationUser.Id)
|
||||
.Returns(organizationUser);
|
||||
|
||||
sutProvider.GetDependency<IUserRepository>().GetByIdAsync(user.Id)
|
||||
.Returns(user);
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationCustom(organizationUser.OrganizationId)
|
||||
.Returns(true);
|
||||
|
||||
// Act
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
|
||||
sutProvider.Sut.DeleteUserAsync(organizationUser.OrganizationId, organizationUser.Id, deletingUserId));
|
||||
|
||||
// Assert
|
||||
Assert.Equal("Custom users can not delete admins.", exception.Message);
|
||||
await sutProvider.GetDependency<IUserService>().Received(0).DeleteAsync(Arg.Any<User>());
|
||||
await sutProvider.GetDependency<IEventService>().Received(0)
|
||||
.LogOrganizationUserEventAsync(Arg.Any<OrganizationUser>(), Arg.Any<EventType>(), Arg.Any<DateTime?>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task DeleteUserAsync_DeletingOwnerWhenNotOwner_ThrowsException(
|
||||
SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider, User user,
|
||||
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser organizationUser,
|
||||
Guid deletingUserId)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.UserId = user.Id;
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetByIdAsync(organizationUser.Id)
|
||||
.Returns(organizationUser);
|
||||
|
||||
sutProvider.GetDependency<IUserRepository>().GetByIdAsync(user.Id)
|
||||
.Returns(user);
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationOwner(organizationUser.OrganizationId)
|
||||
.Returns(false);
|
||||
|
||||
// Act
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
|
||||
sutProvider.Sut.DeleteUserAsync(organizationUser.OrganizationId, organizationUser.Id, deletingUserId));
|
||||
|
||||
// Assert
|
||||
Assert.Equal("Only owners can delete other owners.", exception.Message);
|
||||
await sutProvider.GetDependency<IUserService>().Received(0).DeleteAsync(Arg.Any<User>());
|
||||
await sutProvider.GetDependency<IEventService>().Received(0)
|
||||
.LogOrganizationUserEventAsync(Arg.Any<OrganizationUser>(), Arg.Any<EventType>(), Arg.Any<DateTime?>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task DeleteUserAsync_DeletingLastConfirmedOwner_ThrowsException(
|
||||
SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider, User user,
|
||||
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser organizationUser,
|
||||
Guid deletingUserId)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.UserId = user.Id;
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetByIdAsync(organizationUser.Id)
|
||||
.Returns(organizationUser);
|
||||
|
||||
sutProvider.GetDependency<IUserRepository>().GetByIdAsync(user.Id)
|
||||
.Returns(user);
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationOwner(organizationUser.OrganizationId)
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()
|
||||
.HasConfirmedOwnersExceptAsync(
|
||||
organizationUser.OrganizationId,
|
||||
Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(organizationUser.Id)),
|
||||
includeProvider: Arg.Any<bool>())
|
||||
.Returns(false);
|
||||
|
||||
// Act
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
|
||||
sutProvider.Sut.DeleteUserAsync(organizationUser.OrganizationId, organizationUser.Id, deletingUserId));
|
||||
|
||||
// Assert
|
||||
Assert.Equal("Organization must have at least one confirmed owner.", exception.Message);
|
||||
await sutProvider.GetDependency<IUserService>().Received(0).DeleteAsync(Arg.Any<User>());
|
||||
await sutProvider.GetDependency<IEventService>().Received(0)
|
||||
.LogOrganizationUserEventAsync(Arg.Any<OrganizationUser>(), Arg.Any<EventType>(), Arg.Any<DateTime?>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task DeleteUserAsync_WithUserNotManaged_ThrowsException(
|
||||
SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider, User user,
|
||||
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.User)] OrganizationUser organizationUser)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.UserId = user.Id;
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetByIdAsync(organizationUser.Id)
|
||||
.Returns(organizationUser);
|
||||
|
||||
sutProvider.GetDependency<IUserRepository>().GetByIdAsync(user.Id)
|
||||
.Returns(user);
|
||||
|
||||
sutProvider.GetDependency<IGetOrganizationUsersClaimedStatusQuery>()
|
||||
.GetUsersOrganizationClaimedStatusAsync(organizationUser.OrganizationId, Arg.Any<IEnumerable<Guid>>())
|
||||
.Returns(new Dictionary<Guid, bool> { { organizationUser.Id, false } });
|
||||
|
||||
// Act
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
|
||||
sutProvider.Sut.DeleteUserAsync(organizationUser.OrganizationId, organizationUser.Id, null));
|
||||
|
||||
// Assert
|
||||
Assert.Equal("Member is not claimed by the organization.", exception.Message);
|
||||
await sutProvider.GetDependency<IUserService>().Received(0).DeleteAsync(Arg.Any<User>());
|
||||
await sutProvider.GetDependency<IEventService>().Received(0)
|
||||
.LogOrganizationUserEventAsync(Arg.Any<OrganizationUser>(), Arg.Any<EventType>(), Arg.Any<DateTime?>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task DeleteManyUsersAsync_WithValidUsers_DeletesUsersAndLogsEvents(
|
||||
SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider, User user1, User user2, Guid organizationId,
|
||||
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.User)] OrganizationUser orgUser1,
|
||||
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.User)] OrganizationUser orgUser2)
|
||||
{
|
||||
// Arrange
|
||||
orgUser1.OrganizationId = orgUser2.OrganizationId = organizationId;
|
||||
orgUser1.UserId = user1.Id;
|
||||
orgUser2.UserId = user2.Id;
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyAsync(Arg.Any<IEnumerable<Guid>>())
|
||||
.Returns(new List<OrganizationUser> { orgUser1, orgUser2 });
|
||||
|
||||
sutProvider.GetDependency<IUserRepository>()
|
||||
.GetManyAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(user1.Id) && ids.Contains(user2.Id)))
|
||||
.Returns(new[] { user1, user2 });
|
||||
|
||||
sutProvider.GetDependency<IGetOrganizationUsersClaimedStatusQuery>()
|
||||
.GetUsersOrganizationClaimedStatusAsync(organizationId, Arg.Any<IEnumerable<Guid>>())
|
||||
.Returns(new Dictionary<Guid, bool> { { orgUser1.Id, true }, { orgUser2.Id, true } });
|
||||
|
||||
// Act
|
||||
var userIds = new[] { orgUser1.Id, orgUser2.Id };
|
||||
var results = await sutProvider.Sut.DeleteManyUsersAsync(organizationId, userIds, null);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, results.Count());
|
||||
Assert.All(results, r => Assert.Empty(r.Item2));
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1).GetManyAsync(userIds);
|
||||
await sutProvider.GetDependency<IUserRepository>().Received(1).DeleteManyAsync(Arg.Is<IEnumerable<User>>(users => users.Any(u => u.Id == user1.Id) && users.Any(u => u.Id == user2.Id)));
|
||||
await sutProvider.GetDependency<IEventService>().Received(1).LogOrganizationUserEventsAsync(
|
||||
Arg.Is<IEnumerable<(OrganizationUser, EventType, DateTime?)>>(events =>
|
||||
events.Count(e => e.Item1.Id == orgUser1.Id && e.Item2 == EventType.OrganizationUser_Deleted) == 1
|
||||
&& events.Count(e => e.Item1.Id == orgUser2.Id && e.Item2 == EventType.OrganizationUser_Deleted) == 1));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task DeleteManyUsersAsync_WhenUserNotFound_ReturnsErrorMessage(
|
||||
SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider,
|
||||
Guid organizationId,
|
||||
Guid orgUserId)
|
||||
{
|
||||
// Act
|
||||
var result = await sutProvider.Sut.DeleteManyUsersAsync(organizationId, new[] { orgUserId }, null);
|
||||
|
||||
// Assert
|
||||
Assert.Single(result);
|
||||
Assert.Equal(orgUserId, result.First().Item1);
|
||||
Assert.Contains("Member not found.", result.First().Item2);
|
||||
await sutProvider.GetDependency<IUserRepository>()
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.DeleteManyAsync(default);
|
||||
await sutProvider.GetDependency<IEventService>().Received(0)
|
||||
.LogOrganizationUserEventsAsync(Arg.Any<IEnumerable<(OrganizationUser, EventType, DateTime?)>>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task DeleteManyUsersAsync_WhenDeletingYourself_ReturnsErrorMessage(
|
||||
SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider,
|
||||
User user, [OrganizationUser] OrganizationUser orgUser, Guid deletingUserId)
|
||||
{
|
||||
// Arrange
|
||||
orgUser.UserId = user.Id = deletingUserId;
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyAsync(Arg.Any<IEnumerable<Guid>>())
|
||||
.Returns(new List<OrganizationUser> { orgUser });
|
||||
|
||||
sutProvider.GetDependency<IUserRepository>()
|
||||
.GetManyAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(user.Id)))
|
||||
.Returns(new[] { user });
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.DeleteManyUsersAsync(orgUser.OrganizationId, new[] { orgUser.Id }, deletingUserId);
|
||||
|
||||
// Assert
|
||||
Assert.Single(result);
|
||||
Assert.Equal(orgUser.Id, result.First().Item1);
|
||||
Assert.Contains("You cannot delete yourself.", result.First().Item2);
|
||||
await sutProvider.GetDependency<IUserService>().Received(0).DeleteAsync(Arg.Any<User>());
|
||||
await sutProvider.GetDependency<IEventService>().Received(0)
|
||||
.LogOrganizationUserEventsAsync(Arg.Any<IEnumerable<(OrganizationUser, EventType, DateTime?)>>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task DeleteManyUsersAsync_WhenUserIsInvited_ReturnsErrorMessage(
|
||||
SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider,
|
||||
[OrganizationUser(OrganizationUserStatusType.Invited, OrganizationUserType.User)] OrganizationUser orgUser)
|
||||
{
|
||||
// Arrange
|
||||
orgUser.UserId = null;
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyAsync(Arg.Any<IEnumerable<Guid>>())
|
||||
.Returns(new List<OrganizationUser> { orgUser });
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.DeleteManyUsersAsync(orgUser.OrganizationId, new[] { orgUser.Id }, null);
|
||||
|
||||
// Assert
|
||||
Assert.Single(result);
|
||||
Assert.Equal(orgUser.Id, result.First().Item1);
|
||||
Assert.Contains("You cannot delete a member with Invited status.", result.First().Item2);
|
||||
await sutProvider.GetDependency<IUserService>().Received(0).DeleteAsync(Arg.Any<User>());
|
||||
await sutProvider.GetDependency<IEventService>().Received(0)
|
||||
.LogOrganizationUserEventsAsync(Arg.Any<IEnumerable<(OrganizationUser, EventType, DateTime?)>>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task DeleteManyUsersAsync_WhenDeletingOwnerAsNonOwner_ReturnsErrorMessage(
|
||||
SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider, User user,
|
||||
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser orgUser,
|
||||
Guid deletingUserId)
|
||||
{
|
||||
// Arrange
|
||||
orgUser.UserId = user.Id;
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyAsync(Arg.Any<IEnumerable<Guid>>())
|
||||
.Returns(new List<OrganizationUser> { orgUser });
|
||||
|
||||
sutProvider.GetDependency<IUserRepository>()
|
||||
.GetManyAsync(Arg.Is<IEnumerable<Guid>>(i => i.Contains(user.Id)))
|
||||
.Returns(new[] { user });
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationOwner(orgUser.OrganizationId)
|
||||
.Returns(false);
|
||||
|
||||
var result = await sutProvider.Sut.DeleteManyUsersAsync(orgUser.OrganizationId, new[] { orgUser.Id }, deletingUserId);
|
||||
|
||||
Assert.Single(result);
|
||||
Assert.Equal(orgUser.Id, result.First().Item1);
|
||||
Assert.Contains("Only owners can delete other owners.", result.First().Item2);
|
||||
await sutProvider.GetDependency<IUserService>().Received(0).DeleteAsync(Arg.Any<User>());
|
||||
await sutProvider.GetDependency<IEventService>().Received(0)
|
||||
.LogOrganizationUserEventsAsync(Arg.Any<IEnumerable<(OrganizationUser, EventType, DateTime?)>>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task DeleteManyUsersAsync_WhenDeletingLastOwner_ReturnsErrorMessage(
|
||||
SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider, User user,
|
||||
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser orgUser,
|
||||
Guid deletingUserId)
|
||||
{
|
||||
// Arrange
|
||||
orgUser.UserId = user.Id;
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyAsync(Arg.Any<IEnumerable<Guid>>())
|
||||
.Returns(new List<OrganizationUser> { orgUser });
|
||||
|
||||
sutProvider.GetDependency<IUserRepository>()
|
||||
.GetManyAsync(Arg.Is<IEnumerable<Guid>>(i => i.Contains(user.Id)))
|
||||
.Returns(new[] { user });
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationOwner(orgUser.OrganizationId)
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()
|
||||
.HasConfirmedOwnersExceptAsync(orgUser.OrganizationId, Arg.Any<IEnumerable<Guid>>(), Arg.Any<bool>())
|
||||
.Returns(false);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.DeleteManyUsersAsync(orgUser.OrganizationId, new[] { orgUser.Id }, deletingUserId);
|
||||
|
||||
// Assert
|
||||
Assert.Single(result);
|
||||
Assert.Equal(orgUser.Id, result.First().Item1);
|
||||
Assert.Contains("Organization must have at least one confirmed owner.", result.First().Item2);
|
||||
await sutProvider.GetDependency<IUserService>().Received(0).DeleteAsync(Arg.Any<User>());
|
||||
await sutProvider.GetDependency<IEventService>().Received(0)
|
||||
.LogOrganizationUserEventsAsync(Arg.Any<IEnumerable<(OrganizationUser, EventType, DateTime?)>>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task DeleteManyUsersAsync_WhenUserNotManaged_ReturnsErrorMessage(
|
||||
SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider, User user,
|
||||
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.User)] OrganizationUser orgUser)
|
||||
{
|
||||
// Arrange
|
||||
orgUser.UserId = user.Id;
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyAsync(Arg.Any<IEnumerable<Guid>>())
|
||||
.Returns(new List<OrganizationUser> { orgUser });
|
||||
|
||||
sutProvider.GetDependency<IUserRepository>()
|
||||
.GetManyAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(orgUser.UserId.Value)))
|
||||
.Returns(new[] { user });
|
||||
|
||||
sutProvider.GetDependency<IGetOrganizationUsersClaimedStatusQuery>()
|
||||
.GetUsersOrganizationClaimedStatusAsync(Arg.Any<Guid>(), Arg.Any<IEnumerable<Guid>>())
|
||||
.Returns(new Dictionary<Guid, bool> { { orgUser.Id, false } });
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.DeleteManyUsersAsync(orgUser.OrganizationId, new[] { orgUser.Id }, null);
|
||||
|
||||
// Assert
|
||||
Assert.Single(result);
|
||||
Assert.Equal(orgUser.Id, result.First().Item1);
|
||||
Assert.Contains("Member is not claimed by the organization.", result.First().Item2);
|
||||
await sutProvider.GetDependency<IUserService>().Received(0).DeleteAsync(Arg.Any<User>());
|
||||
await sutProvider.GetDependency<IEventService>().Received(0)
|
||||
.LogOrganizationUserEventsAsync(Arg.Any<IEnumerable<(OrganizationUser, EventType, DateTime?)>>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task DeleteManyUsersAsync_MixedValidAndInvalidUsers_ReturnsAppropriateResults(
|
||||
SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider, User user1, User user3,
|
||||
Guid organizationId,
|
||||
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.User)] OrganizationUser orgUser1,
|
||||
[OrganizationUser(OrganizationUserStatusType.Invited, OrganizationUserType.User)] OrganizationUser orgUser2,
|
||||
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.User)] OrganizationUser orgUser3)
|
||||
{
|
||||
// Arrange
|
||||
orgUser1.UserId = user1.Id;
|
||||
orgUser2.UserId = null;
|
||||
orgUser3.UserId = user3.Id;
|
||||
orgUser1.OrganizationId = orgUser2.OrganizationId = orgUser3.OrganizationId = organizationId;
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyAsync(Arg.Any<IEnumerable<Guid>>())
|
||||
.Returns(new List<OrganizationUser> { orgUser1, orgUser2, orgUser3 });
|
||||
|
||||
sutProvider.GetDependency<IUserRepository>()
|
||||
.GetManyAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(user1.Id) && ids.Contains(user3.Id)))
|
||||
.Returns(new[] { user1, user3 });
|
||||
|
||||
sutProvider.GetDependency<IGetOrganizationUsersClaimedStatusQuery>()
|
||||
.GetUsersOrganizationClaimedStatusAsync(organizationId, Arg.Any<IEnumerable<Guid>>())
|
||||
.Returns(new Dictionary<Guid, bool> { { orgUser1.Id, true }, { orgUser3.Id, false } });
|
||||
|
||||
// Act
|
||||
var results = await sutProvider.Sut.DeleteManyUsersAsync(organizationId, new[] { orgUser1.Id, orgUser2.Id, orgUser3.Id }, null);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(3, results.Count());
|
||||
Assert.Empty(results.First(r => r.Item1 == orgUser1.Id).Item2);
|
||||
Assert.Equal("You cannot delete a member with Invited status.", results.First(r => r.Item1 == orgUser2.Id).Item2);
|
||||
Assert.Equal("Member is not claimed by the organization.", results.First(r => r.Item1 == orgUser3.Id).Item2);
|
||||
|
||||
await sutProvider.GetDependency<IEventService>().Received(1).LogOrganizationUserEventsAsync(
|
||||
Arg.Is<IEnumerable<(OrganizationUser, EventType, DateTime?)>>(events =>
|
||||
events.Count(e => e.Item1.Id == orgUser1.Id && e.Item2 == EventType.OrganizationUser_Deleted) == 1));
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing.Caches;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Organizations.Queries;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Context;
|
||||
@@ -13,11 +14,14 @@ using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using NSubstitute.ReturnsExtensions;
|
||||
using Stripe;
|
||||
using Stripe.Tax;
|
||||
using Stripe.TestHelpers;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Billing.Organizations.Queries;
|
||||
|
||||
using static StripeConstants;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class GetOrganizationWarningsQueryTests
|
||||
{
|
||||
@@ -57,7 +61,7 @@ public class GetOrganizationWarningsQueryTests
|
||||
))
|
||||
.Returns(new Subscription
|
||||
{
|
||||
Status = StripeConstants.SubscriptionStatus.Trialing,
|
||||
Status = SubscriptionStatus.Trialing,
|
||||
TrialEnd = now.AddDays(7),
|
||||
Customer = new Customer
|
||||
{
|
||||
@@ -95,7 +99,7 @@ public class GetOrganizationWarningsQueryTests
|
||||
))
|
||||
.Returns(new Subscription
|
||||
{
|
||||
Status = StripeConstants.SubscriptionStatus.Trialing,
|
||||
Status = SubscriptionStatus.Trialing,
|
||||
TrialEnd = now.AddDays(7),
|
||||
Customer = new Customer
|
||||
{
|
||||
@@ -142,7 +146,7 @@ public class GetOrganizationWarningsQueryTests
|
||||
))
|
||||
.Returns(new Subscription
|
||||
{
|
||||
Status = StripeConstants.SubscriptionStatus.Unpaid,
|
||||
Status = SubscriptionStatus.Unpaid,
|
||||
Customer = new Customer
|
||||
{
|
||||
InvoiceSettings = new CustomerInvoiceSettings(),
|
||||
@@ -170,7 +174,8 @@ public class GetOrganizationWarningsQueryTests
|
||||
))
|
||||
.Returns(new Subscription
|
||||
{
|
||||
Status = StripeConstants.SubscriptionStatus.Unpaid
|
||||
Customer = new Customer(),
|
||||
Status = SubscriptionStatus.Unpaid
|
||||
});
|
||||
|
||||
sutProvider.GetDependency<IProviderRepository>().GetByOrganizationIdAsync(organization.Id)
|
||||
@@ -197,7 +202,8 @@ public class GetOrganizationWarningsQueryTests
|
||||
))
|
||||
.Returns(new Subscription
|
||||
{
|
||||
Status = StripeConstants.SubscriptionStatus.Unpaid
|
||||
Customer = new Customer(),
|
||||
Status = SubscriptionStatus.Unpaid
|
||||
});
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>().OrganizationOwner(organization.Id).Returns(true);
|
||||
@@ -223,7 +229,8 @@ public class GetOrganizationWarningsQueryTests
|
||||
))
|
||||
.Returns(new Subscription
|
||||
{
|
||||
Status = StripeConstants.SubscriptionStatus.Canceled
|
||||
Customer = new Customer(),
|
||||
Status = SubscriptionStatus.Canceled
|
||||
});
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>().OrganizationOwner(organization.Id).Returns(true);
|
||||
@@ -249,7 +256,8 @@ public class GetOrganizationWarningsQueryTests
|
||||
))
|
||||
.Returns(new Subscription
|
||||
{
|
||||
Status = StripeConstants.SubscriptionStatus.Unpaid
|
||||
Customer = new Customer(),
|
||||
Status = SubscriptionStatus.Unpaid
|
||||
});
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>().OrganizationOwner(organization.Id).Returns(false);
|
||||
@@ -275,8 +283,9 @@ public class GetOrganizationWarningsQueryTests
|
||||
))
|
||||
.Returns(new Subscription
|
||||
{
|
||||
CollectionMethod = StripeConstants.CollectionMethod.SendInvoice,
|
||||
Status = StripeConstants.SubscriptionStatus.Active,
|
||||
CollectionMethod = CollectionMethod.SendInvoice,
|
||||
Customer = new Customer(),
|
||||
Status = SubscriptionStatus.Active,
|
||||
CurrentPeriodEnd = now.AddDays(10),
|
||||
TestClock = new TestClock
|
||||
{
|
||||
@@ -313,11 +322,12 @@ public class GetOrganizationWarningsQueryTests
|
||||
))
|
||||
.Returns(new Subscription
|
||||
{
|
||||
CollectionMethod = StripeConstants.CollectionMethod.SendInvoice,
|
||||
Status = StripeConstants.SubscriptionStatus.Active,
|
||||
CollectionMethod = CollectionMethod.SendInvoice,
|
||||
Customer = new Customer(),
|
||||
Status = SubscriptionStatus.Active,
|
||||
LatestInvoice = new Invoice
|
||||
{
|
||||
Status = StripeConstants.InvoiceStatus.Open,
|
||||
Status = InvoiceStatus.Open,
|
||||
DueDate = now.AddDays(30),
|
||||
Created = now
|
||||
},
|
||||
@@ -360,8 +370,9 @@ public class GetOrganizationWarningsQueryTests
|
||||
.Returns(new Subscription
|
||||
{
|
||||
Id = subscriptionId,
|
||||
CollectionMethod = StripeConstants.CollectionMethod.SendInvoice,
|
||||
Status = StripeConstants.SubscriptionStatus.PastDue,
|
||||
CollectionMethod = CollectionMethod.SendInvoice,
|
||||
Customer = new Customer(),
|
||||
Status = SubscriptionStatus.PastDue,
|
||||
TestClock = new TestClock
|
||||
{
|
||||
FrozenTime = now
|
||||
@@ -390,4 +401,406 @@ public class GetOrganizationWarningsQueryTests
|
||||
|
||||
Assert.Equal(dueDate.AddDays(30), response.ResellerRenewal.PastDue!.SuspensionDate);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Run_USCustomer_NoTaxIdWarning(
|
||||
Organization organization,
|
||||
SutProvider<GetOrganizationWarningsQuery> sutProvider)
|
||||
{
|
||||
var subscription = new Subscription
|
||||
{
|
||||
Customer = new Customer
|
||||
{
|
||||
Address = new Address { Country = "US" },
|
||||
TaxIds = new StripeList<TaxId> { Data = new List<TaxId>() },
|
||||
InvoiceSettings = new CustomerInvoiceSettings(),
|
||||
Metadata = new Dictionary<string, string>()
|
||||
}
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<ISubscriberService>()
|
||||
.GetSubscription(organization, Arg.Any<SubscriptionGetOptions>())
|
||||
.Returns(subscription);
|
||||
|
||||
var response = await sutProvider.Sut.Run(organization);
|
||||
|
||||
Assert.Null(response.TaxId);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Run_FreeCustomer_NoTaxIdWarning(
|
||||
Organization organization,
|
||||
SutProvider<GetOrganizationWarningsQuery> sutProvider)
|
||||
{
|
||||
organization.PlanType = PlanType.Free;
|
||||
|
||||
var subscription = new Subscription
|
||||
{
|
||||
Customer = new Customer
|
||||
{
|
||||
Address = new Address { Country = "CA" },
|
||||
TaxIds = new StripeList<TaxId> { Data = new List<TaxId>() },
|
||||
InvoiceSettings = new CustomerInvoiceSettings(),
|
||||
Metadata = new Dictionary<string, string>()
|
||||
}
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<ISubscriberService>()
|
||||
.GetSubscription(organization, Arg.Any<SubscriptionGetOptions>())
|
||||
.Returns(subscription);
|
||||
|
||||
var response = await sutProvider.Sut.Run(organization);
|
||||
|
||||
Assert.Null(response.TaxId);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Run_NotOwner_NoTaxIdWarning(
|
||||
Organization organization,
|
||||
SutProvider<GetOrganizationWarningsQuery> sutProvider)
|
||||
{
|
||||
organization.PlanType = PlanType.TeamsAnnually;
|
||||
|
||||
var subscription = new Subscription
|
||||
{
|
||||
Customer = new Customer
|
||||
{
|
||||
Address = new Address { Country = "CA" },
|
||||
TaxIds = new StripeList<TaxId> { Data = new List<TaxId>() },
|
||||
InvoiceSettings = new CustomerInvoiceSettings(),
|
||||
Metadata = new Dictionary<string, string>()
|
||||
}
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<ISubscriberService>()
|
||||
.GetSubscription(organization, Arg.Any<SubscriptionGetOptions>())
|
||||
.Returns(subscription);
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationOwner(organization.Id)
|
||||
.Returns(false);
|
||||
|
||||
var response = await sutProvider.Sut.Run(organization);
|
||||
|
||||
Assert.Null(response.TaxId);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Run_HasProvider_NoTaxIdWarning(
|
||||
Organization organization,
|
||||
SutProvider<GetOrganizationWarningsQuery> sutProvider)
|
||||
{
|
||||
organization.PlanType = PlanType.TeamsAnnually;
|
||||
|
||||
var subscription = new Subscription
|
||||
{
|
||||
Customer = new Customer
|
||||
{
|
||||
Address = new Address { Country = "CA" },
|
||||
TaxIds = new StripeList<TaxId> { Data = new List<TaxId>() },
|
||||
InvoiceSettings = new CustomerInvoiceSettings(),
|
||||
Metadata = new Dictionary<string, string>()
|
||||
}
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<ISubscriberService>()
|
||||
.GetSubscription(organization, Arg.Any<SubscriptionGetOptions>())
|
||||
.Returns(subscription);
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationOwner(organization.Id)
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IProviderRepository>()
|
||||
.GetByOrganizationIdAsync(organization.Id)
|
||||
.Returns(new Provider());
|
||||
|
||||
var response = await sutProvider.Sut.Run(organization);
|
||||
|
||||
Assert.Null(response.TaxId);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Run_NoRegistrationInCountry_NoTaxIdWarning(
|
||||
Organization organization,
|
||||
SutProvider<GetOrganizationWarningsQuery> sutProvider)
|
||||
{
|
||||
organization.PlanType = PlanType.TeamsAnnually;
|
||||
|
||||
var subscription = new Subscription
|
||||
{
|
||||
Customer = new Customer
|
||||
{
|
||||
Address = new Address { Country = "CA" },
|
||||
TaxIds = new StripeList<TaxId> { Data = new List<TaxId>() },
|
||||
InvoiceSettings = new CustomerInvoiceSettings(),
|
||||
Metadata = new Dictionary<string, string>()
|
||||
}
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<ISubscriberService>()
|
||||
.GetSubscription(organization, Arg.Any<SubscriptionGetOptions>())
|
||||
.Returns(subscription);
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationOwner(organization.Id)
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IStripeAdapter>()
|
||||
.TaxRegistrationsListAsync(Arg.Any<RegistrationListOptions>())
|
||||
.Returns(new StripeList<Registration>
|
||||
{
|
||||
Data = new List<Registration>
|
||||
{
|
||||
new() { Country = "GB" }
|
||||
}
|
||||
});
|
||||
|
||||
var response = await sutProvider.Sut.Run(organization);
|
||||
|
||||
Assert.Null(response.TaxId);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Run_Has_TaxIdWarning_Missing(
|
||||
Organization organization,
|
||||
SutProvider<GetOrganizationWarningsQuery> sutProvider)
|
||||
{
|
||||
organization.PlanType = PlanType.TeamsAnnually;
|
||||
|
||||
var subscription = new Subscription
|
||||
{
|
||||
Customer = new Customer
|
||||
{
|
||||
Address = new Address { Country = "CA" },
|
||||
TaxIds = new StripeList<TaxId> { Data = new List<TaxId>() },
|
||||
InvoiceSettings = new CustomerInvoiceSettings(),
|
||||
Metadata = new Dictionary<string, string>()
|
||||
}
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<ISubscriberService>()
|
||||
.GetSubscription(organization, Arg.Any<SubscriptionGetOptions>())
|
||||
.Returns(subscription);
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationOwner(organization.Id)
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IStripeAdapter>()
|
||||
.TaxRegistrationsListAsync(Arg.Any<RegistrationListOptions>())
|
||||
.Returns(new StripeList<Registration>
|
||||
{
|
||||
Data = new List<Registration>
|
||||
{
|
||||
new() { Country = "CA" }
|
||||
}
|
||||
});
|
||||
|
||||
var response = await sutProvider.Sut.Run(organization);
|
||||
|
||||
Assert.True(response is
|
||||
{
|
||||
TaxId.Type: "tax_id_missing"
|
||||
});
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Run_Has_TaxIdWarning_PendingVerification(
|
||||
Organization organization,
|
||||
SutProvider<GetOrganizationWarningsQuery> sutProvider)
|
||||
{
|
||||
organization.PlanType = PlanType.EnterpriseAnnually;
|
||||
|
||||
var taxId = new TaxId
|
||||
{
|
||||
Verification = new TaxIdVerification
|
||||
{
|
||||
Status = TaxIdVerificationStatus.Pending
|
||||
}
|
||||
};
|
||||
|
||||
var subscription = new Subscription
|
||||
{
|
||||
Customer = new Customer
|
||||
{
|
||||
Address = new Address { Country = "CA" },
|
||||
TaxIds = new StripeList<TaxId> { Data = new List<TaxId> { taxId } },
|
||||
InvoiceSettings = new CustomerInvoiceSettings(),
|
||||
Metadata = new Dictionary<string, string>()
|
||||
}
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<ISubscriberService>()
|
||||
.GetSubscription(organization, Arg.Any<SubscriptionGetOptions>())
|
||||
.Returns(subscription);
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationOwner(organization.Id)
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IStripeAdapter>()
|
||||
.TaxRegistrationsListAsync(Arg.Any<RegistrationListOptions>())
|
||||
.Returns(new StripeList<Registration>
|
||||
{
|
||||
Data = new List<Registration>
|
||||
{
|
||||
new() { Country = "CA" }
|
||||
}
|
||||
});
|
||||
|
||||
var response = await sutProvider.Sut.Run(organization);
|
||||
|
||||
Assert.True(response is
|
||||
{
|
||||
TaxId.Type: "tax_id_pending_verification"
|
||||
});
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Run_Has_TaxIdWarning_FailedVerification(
|
||||
Organization organization,
|
||||
SutProvider<GetOrganizationWarningsQuery> sutProvider)
|
||||
{
|
||||
organization.PlanType = PlanType.TeamsAnnually;
|
||||
|
||||
var taxId = new TaxId
|
||||
{
|
||||
Verification = new TaxIdVerification
|
||||
{
|
||||
Status = TaxIdVerificationStatus.Unverified
|
||||
}
|
||||
};
|
||||
|
||||
var subscription = new Subscription
|
||||
{
|
||||
Customer = new Customer
|
||||
{
|
||||
Address = new Address { Country = "CA" },
|
||||
TaxIds = new StripeList<TaxId> { Data = new List<TaxId> { taxId } },
|
||||
InvoiceSettings = new CustomerInvoiceSettings(),
|
||||
Metadata = new Dictionary<string, string>()
|
||||
}
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<ISubscriberService>()
|
||||
.GetSubscription(organization, Arg.Any<SubscriptionGetOptions>())
|
||||
.Returns(subscription);
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationOwner(organization.Id)
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IStripeAdapter>()
|
||||
.TaxRegistrationsListAsync(Arg.Any<RegistrationListOptions>())
|
||||
.Returns(new StripeList<Registration>
|
||||
{
|
||||
Data = new List<Registration>
|
||||
{
|
||||
new() { Country = "CA" }
|
||||
}
|
||||
});
|
||||
|
||||
var response = await sutProvider.Sut.Run(organization);
|
||||
|
||||
Assert.True(response is
|
||||
{
|
||||
TaxId.Type: "tax_id_failed_verification"
|
||||
});
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Run_VerifiedTaxId_NoTaxIdWarning(
|
||||
Organization organization,
|
||||
SutProvider<GetOrganizationWarningsQuery> sutProvider)
|
||||
{
|
||||
organization.PlanType = PlanType.TeamsAnnually;
|
||||
|
||||
var taxId = new TaxId
|
||||
{
|
||||
Verification = new TaxIdVerification
|
||||
{
|
||||
Status = TaxIdVerificationStatus.Verified
|
||||
}
|
||||
};
|
||||
|
||||
var subscription = new Subscription
|
||||
{
|
||||
Customer = new Customer
|
||||
{
|
||||
Address = new Address { Country = "CA" },
|
||||
TaxIds = new StripeList<TaxId> { Data = new List<TaxId> { taxId } },
|
||||
InvoiceSettings = new CustomerInvoiceSettings(),
|
||||
Metadata = new Dictionary<string, string>()
|
||||
}
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<ISubscriberService>()
|
||||
.GetSubscription(organization, Arg.Any<SubscriptionGetOptions>())
|
||||
.Returns(subscription);
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationOwner(organization.Id)
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IStripeAdapter>()
|
||||
.TaxRegistrationsListAsync(Arg.Any<RegistrationListOptions>())
|
||||
.Returns(new StripeList<Registration>
|
||||
{
|
||||
Data = new List<Registration>
|
||||
{
|
||||
new() { Country = "CA" }
|
||||
}
|
||||
});
|
||||
|
||||
var response = await sutProvider.Sut.Run(organization);
|
||||
|
||||
Assert.Null(response.TaxId);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Run_NullVerification_NoTaxIdWarning(
|
||||
Organization organization,
|
||||
SutProvider<GetOrganizationWarningsQuery> sutProvider)
|
||||
{
|
||||
organization.PlanType = PlanType.TeamsAnnually;
|
||||
|
||||
var taxId = new TaxId
|
||||
{
|
||||
Verification = null
|
||||
};
|
||||
|
||||
var subscription = new Subscription
|
||||
{
|
||||
Customer = new Customer
|
||||
{
|
||||
Address = new Address { Country = "CA" },
|
||||
TaxIds = new StripeList<TaxId> { Data = new List<TaxId> { taxId } },
|
||||
InvoiceSettings = new CustomerInvoiceSettings(),
|
||||
Metadata = new Dictionary<string, string>()
|
||||
}
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<ISubscriberService>()
|
||||
.GetSubscription(organization, Arg.Any<SubscriptionGetOptions>())
|
||||
.Returns(subscription);
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationOwner(organization.Id)
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IStripeAdapter>()
|
||||
.TaxRegistrationsListAsync(Arg.Any<RegistrationListOptions>())
|
||||
.Returns(new StripeList<Registration>
|
||||
{
|
||||
Data = new List<Registration>
|
||||
{
|
||||
new() { Country = "CA" }
|
||||
}
|
||||
});
|
||||
|
||||
var response = await sutProvider.Sut.Run(organization);
|
||||
|
||||
Assert.Null(response.TaxId);
|
||||
}
|
||||
}
|
||||
|
||||
322
test/Core.Test/KeyManagement/Kdf/ChangeKdfCommandTests.cs
Normal file
322
test/Core.Test/KeyManagement/Kdf/ChangeKdfCommandTests.cs
Normal file
@@ -0,0 +1,322 @@
|
||||
#nullable enable
|
||||
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.KeyManagement.Kdf.Implementations;
|
||||
using Bit.Core.KeyManagement.Models.Data;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.KeyManagement.Kdf;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class ChangeKdfCommandTests
|
||||
{
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task ChangeKdfAsync_ChangesKdfAsync(SutProvider<ChangeKdfCommand> sutProvider, User user)
|
||||
{
|
||||
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(Arg.Any<User>(), Arg.Any<string>()).Returns(Task.FromResult(true));
|
||||
sutProvider.GetDependency<IUserService>().UpdatePasswordHash(Arg.Any<User>(), Arg.Any<string>()).Returns(Task.FromResult(IdentityResult.Success));
|
||||
|
||||
var kdf = new KdfSettings
|
||||
{
|
||||
KdfType = Enums.KdfType.Argon2id,
|
||||
Iterations = 4,
|
||||
Memory = 512,
|
||||
Parallelism = 4
|
||||
};
|
||||
var authenticationData = new MasterPasswordAuthenticationData
|
||||
{
|
||||
Kdf = kdf,
|
||||
MasterPasswordAuthenticationHash = "newMasterPassword",
|
||||
Salt = user.GetMasterPasswordSalt()
|
||||
};
|
||||
var unlockData = new MasterPasswordUnlockData
|
||||
{
|
||||
Kdf = kdf,
|
||||
MasterKeyWrappedUserKey = "masterKeyWrappedUserKey",
|
||||
Salt = user.GetMasterPasswordSalt()
|
||||
};
|
||||
|
||||
await sutProvider.Sut.ChangeKdfAsync(user, "masterPassword", authenticationData, unlockData);
|
||||
|
||||
await sutProvider.GetDependency<IUserRepository>().Received(1).ReplaceAsync(Arg.Is<User>(u =>
|
||||
u.Id == user.Id
|
||||
&& u.Kdf == Enums.KdfType.Argon2id
|
||||
&& u.KdfIterations == 4
|
||||
&& u.KdfMemory == 512
|
||||
&& u.KdfParallelism == 4
|
||||
));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task ChangeKdfAsync_UserIsNull_ThrowsArgumentNullException(SutProvider<ChangeKdfCommand> sutProvider)
|
||||
{
|
||||
var kdf = new KdfSettings
|
||||
{
|
||||
KdfType = Enums.KdfType.Argon2id,
|
||||
Iterations = 4,
|
||||
Memory = 512,
|
||||
Parallelism = 4
|
||||
};
|
||||
var authenticationData = new MasterPasswordAuthenticationData
|
||||
{
|
||||
Kdf = kdf,
|
||||
MasterPasswordAuthenticationHash = "newMasterPassword",
|
||||
Salt = "salt"
|
||||
};
|
||||
var unlockData = new MasterPasswordUnlockData
|
||||
{
|
||||
Kdf = kdf,
|
||||
MasterKeyWrappedUserKey = "masterKeyWrappedUserKey",
|
||||
Salt = "salt"
|
||||
};
|
||||
|
||||
await Assert.ThrowsAsync<ArgumentNullException>(async () =>
|
||||
await sutProvider.Sut.ChangeKdfAsync(null!, "masterPassword", authenticationData, unlockData));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task ChangeKdfAsync_WrongPassword_ReturnsPasswordMismatch(SutProvider<ChangeKdfCommand> sutProvider, User user)
|
||||
{
|
||||
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(Arg.Any<User>(), Arg.Any<string>()).Returns(Task.FromResult(false));
|
||||
|
||||
var kdf = new KdfSettings
|
||||
{
|
||||
KdfType = Enums.KdfType.Argon2id,
|
||||
Iterations = 4,
|
||||
Memory = 512,
|
||||
Parallelism = 4
|
||||
};
|
||||
var authenticationData = new MasterPasswordAuthenticationData
|
||||
{
|
||||
Kdf = kdf,
|
||||
MasterPasswordAuthenticationHash = "newMasterPassword",
|
||||
Salt = user.GetMasterPasswordSalt()
|
||||
};
|
||||
var unlockData = new MasterPasswordUnlockData
|
||||
{
|
||||
Kdf = kdf,
|
||||
MasterKeyWrappedUserKey = "masterKeyWrappedUserKey",
|
||||
Salt = user.GetMasterPasswordSalt()
|
||||
};
|
||||
|
||||
var result = await sutProvider.Sut.ChangeKdfAsync(user, "masterPassword", authenticationData, unlockData);
|
||||
Assert.False(result.Succeeded);
|
||||
Assert.Contains(result.Errors, e => e.Code == "PasswordMismatch");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task ChangeKdfAsync_WithAuthenticationAndUnlockData_UpdatesUserCorrectly(SutProvider<ChangeKdfCommand> sutProvider, User user)
|
||||
{
|
||||
var constantKdf = new KdfSettings
|
||||
{
|
||||
KdfType = Enums.KdfType.Argon2id,
|
||||
Iterations = 5,
|
||||
Memory = 1024,
|
||||
Parallelism = 4
|
||||
};
|
||||
var authenticationData = new MasterPasswordAuthenticationData
|
||||
{
|
||||
Kdf = constantKdf,
|
||||
MasterPasswordAuthenticationHash = "new-auth-hash",
|
||||
Salt = user.GetMasterPasswordSalt()
|
||||
};
|
||||
var unlockData = new MasterPasswordUnlockData
|
||||
{
|
||||
Kdf = constantKdf,
|
||||
MasterKeyWrappedUserKey = "new-wrapped-key",
|
||||
Salt = user.GetMasterPasswordSalt()
|
||||
};
|
||||
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(Arg.Any<User>(), Arg.Any<string>()).Returns(Task.FromResult(true));
|
||||
sutProvider.GetDependency<IUserService>().UpdatePasswordHash(Arg.Any<User>(), Arg.Any<string>()).Returns(Task.FromResult(IdentityResult.Success));
|
||||
|
||||
await sutProvider.Sut.ChangeKdfAsync(user, "masterPassword", authenticationData, unlockData);
|
||||
|
||||
await sutProvider.GetDependency<IUserRepository>().Received(1).ReplaceAsync(Arg.Is<User>(u =>
|
||||
u.Id == user.Id
|
||||
&& u.Kdf == constantKdf.KdfType
|
||||
&& u.KdfIterations == constantKdf.Iterations
|
||||
&& u.KdfMemory == constantKdf.Memory
|
||||
&& u.KdfParallelism == constantKdf.Parallelism
|
||||
&& u.Key == "new-wrapped-key"
|
||||
));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task ChangeKdfAsync_KdfNotEqualBetweenAuthAndUnlock_ThrowsBadRequestException(SutProvider<ChangeKdfCommand> sutProvider, User user)
|
||||
{
|
||||
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(Arg.Any<User>(), Arg.Any<string>()).Returns(Task.FromResult(true));
|
||||
|
||||
var authenticationData = new MasterPasswordAuthenticationData
|
||||
{
|
||||
Kdf = new KdfSettings { KdfType = Enums.KdfType.Argon2id, Iterations = 4, Memory = 512, Parallelism = 4 },
|
||||
MasterPasswordAuthenticationHash = "new-auth-hash",
|
||||
Salt = user.GetMasterPasswordSalt()
|
||||
};
|
||||
var unlockData = new MasterPasswordUnlockData
|
||||
{
|
||||
Kdf = new KdfSettings { KdfType = Enums.KdfType.PBKDF2_SHA256, Iterations = 100000 },
|
||||
MasterKeyWrappedUserKey = "new-wrapped-key",
|
||||
Salt = user.GetMasterPasswordSalt()
|
||||
};
|
||||
await Assert.ThrowsAsync<BadRequestException>(async () =>
|
||||
await sutProvider.Sut.ChangeKdfAsync(user, "masterPassword", authenticationData, unlockData));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task ChangeKdfAsync_AuthDataSaltMismatch_Throws(SutProvider<ChangeKdfCommand> sutProvider, User user, KdfSettings kdf)
|
||||
{
|
||||
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(Arg.Any<User>(), Arg.Any<string>()).Returns(Task.FromResult(true));
|
||||
|
||||
var authenticationData = new MasterPasswordAuthenticationData
|
||||
{
|
||||
Kdf = kdf,
|
||||
MasterPasswordAuthenticationHash = "new-auth-hash",
|
||||
Salt = "different-salt"
|
||||
};
|
||||
var unlockData = new MasterPasswordUnlockData
|
||||
{
|
||||
Kdf = kdf,
|
||||
MasterKeyWrappedUserKey = "new-wrapped-key",
|
||||
Salt = user.GetMasterPasswordSalt()
|
||||
};
|
||||
await Assert.ThrowsAsync<ArgumentException>(async () =>
|
||||
await sutProvider.Sut.ChangeKdfAsync(user, "masterPassword", authenticationData, unlockData));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task ChangeKdfAsync_UnlockDataSaltMismatch_Throws(SutProvider<ChangeKdfCommand> sutProvider, User user, KdfSettings kdf)
|
||||
{
|
||||
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(Arg.Any<User>(), Arg.Any<string>()).Returns(Task.FromResult(true));
|
||||
|
||||
var authenticationData = new MasterPasswordAuthenticationData
|
||||
{
|
||||
Kdf = kdf,
|
||||
MasterPasswordAuthenticationHash = "new-auth-hash",
|
||||
Salt = user.GetMasterPasswordSalt()
|
||||
};
|
||||
var unlockData = new MasterPasswordUnlockData
|
||||
{
|
||||
Kdf = kdf,
|
||||
MasterKeyWrappedUserKey = "new-wrapped-key",
|
||||
Salt = "different-salt"
|
||||
};
|
||||
await Assert.ThrowsAsync<ArgumentException>(async () =>
|
||||
await sutProvider.Sut.ChangeKdfAsync(user, "masterPassword", authenticationData, unlockData));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task ChangeKdfAsync_UpdatePasswordHashFails_ReturnsFailure(SutProvider<ChangeKdfCommand> sutProvider, User user)
|
||||
{
|
||||
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(Arg.Any<User>(), Arg.Any<string>()).Returns(Task.FromResult(true));
|
||||
var failedResult = IdentityResult.Failed(new IdentityError { Code = "TestFail", Description = "Test fail" });
|
||||
sutProvider.GetDependency<IUserService>().UpdatePasswordHash(Arg.Any<User>(), Arg.Any<string>()).Returns(Task.FromResult(failedResult));
|
||||
|
||||
var kdf = new KdfSettings
|
||||
{
|
||||
KdfType = Enums.KdfType.Argon2id,
|
||||
Iterations = 4,
|
||||
Memory = 512,
|
||||
Parallelism = 4
|
||||
};
|
||||
var authenticationData = new MasterPasswordAuthenticationData
|
||||
{
|
||||
Kdf = kdf,
|
||||
MasterPasswordAuthenticationHash = "newMasterPassword",
|
||||
Salt = user.GetMasterPasswordSalt()
|
||||
};
|
||||
var unlockData = new MasterPasswordUnlockData
|
||||
{
|
||||
Kdf = kdf,
|
||||
MasterKeyWrappedUserKey = "masterKeyWrappedUserKey",
|
||||
Salt = user.GetMasterPasswordSalt()
|
||||
};
|
||||
|
||||
var result = await sutProvider.Sut.ChangeKdfAsync(user, "masterPassword", authenticationData, unlockData);
|
||||
|
||||
Assert.False(result.Succeeded);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task ChangeKdfAsync_InvalidKdfSettings_ThrowsBadRequestException(SutProvider<ChangeKdfCommand> sutProvider, User user)
|
||||
{
|
||||
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(Arg.Any<User>(), Arg.Any<string>()).Returns(Task.FromResult(true));
|
||||
|
||||
// Create invalid KDF settings (iterations too low for PBKDF2)
|
||||
var invalidKdf = new KdfSettings
|
||||
{
|
||||
KdfType = Enums.KdfType.PBKDF2_SHA256,
|
||||
Iterations = 1000, // This is below the minimum of 600,000
|
||||
Memory = null,
|
||||
Parallelism = null
|
||||
};
|
||||
|
||||
var authenticationData = new MasterPasswordAuthenticationData
|
||||
{
|
||||
Kdf = invalidKdf,
|
||||
MasterPasswordAuthenticationHash = "new-auth-hash",
|
||||
Salt = user.GetMasterPasswordSalt()
|
||||
};
|
||||
var unlockData = new MasterPasswordUnlockData
|
||||
{
|
||||
Kdf = invalidKdf,
|
||||
MasterKeyWrappedUserKey = "new-wrapped-key",
|
||||
Salt = user.GetMasterPasswordSalt()
|
||||
};
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>
|
||||
await sutProvider.Sut.ChangeKdfAsync(user, "masterPassword", authenticationData, unlockData));
|
||||
|
||||
Assert.Equal("KDF settings are invalid.", exception.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task ChangeKdfAsync_InvalidArgon2Settings_ThrowsBadRequestException(SutProvider<ChangeKdfCommand> sutProvider, User user)
|
||||
{
|
||||
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(Arg.Any<User>(), Arg.Any<string>()).Returns(Task.FromResult(true));
|
||||
|
||||
// Create invalid Argon2 KDF settings (memory too high)
|
||||
var invalidKdf = new KdfSettings
|
||||
{
|
||||
KdfType = Enums.KdfType.Argon2id,
|
||||
Iterations = 3, // Valid
|
||||
Memory = 2048, // This is above the maximum of 1024
|
||||
Parallelism = 4 // Valid
|
||||
};
|
||||
|
||||
var authenticationData = new MasterPasswordAuthenticationData
|
||||
{
|
||||
Kdf = invalidKdf,
|
||||
MasterPasswordAuthenticationHash = "new-auth-hash",
|
||||
Salt = user.GetMasterPasswordSalt()
|
||||
};
|
||||
var unlockData = new MasterPasswordUnlockData
|
||||
{
|
||||
Kdf = invalidKdf,
|
||||
MasterKeyWrappedUserKey = "new-wrapped-key",
|
||||
Salt = user.GetMasterPasswordSalt()
|
||||
};
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>
|
||||
await sutProvider.Sut.ChangeKdfAsync(user, "masterPassword", authenticationData, unlockData));
|
||||
|
||||
Assert.Equal("KDF settings are invalid.", exception.Message);
|
||||
}
|
||||
|
||||
}
|
||||
230
test/Core.Test/Utilities/EnumerationProtectionHelpersTests.cs
Normal file
230
test/Core.Test/Utilities/EnumerationProtectionHelpersTests.cs
Normal file
@@ -0,0 +1,230 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using Bit.Core.Utilities;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Utilities;
|
||||
|
||||
public class EnumerationProtectionHelpersTests
|
||||
{
|
||||
#region GetIndexForInputHash Tests
|
||||
|
||||
[Fact]
|
||||
public void GetIndexForInputHash_NullHmacKey_ReturnsZero()
|
||||
{
|
||||
// Arrange
|
||||
byte[] hmacKey = null;
|
||||
var salt = "test@example.com";
|
||||
var range = 10;
|
||||
|
||||
// Act
|
||||
var result = EnumerationProtectionHelpers.GetIndexForInputHash(hmacKey, salt, range);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(0, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetIndexForInputHash_ZeroRange_ReturnsZero()
|
||||
{
|
||||
// Arrange
|
||||
var hmacKey = RandomNumberGenerator.GetBytes(32);
|
||||
var salt = "test@example.com";
|
||||
var range = 0;
|
||||
|
||||
// Act
|
||||
var result = EnumerationProtectionHelpers.GetIndexForInputHash(hmacKey, salt, range);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(0, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetIndexForInputHash_NegativeRange_ReturnsZero()
|
||||
{
|
||||
// Arrange
|
||||
var hmacKey = RandomNumberGenerator.GetBytes(32);
|
||||
var salt = "test@example.com";
|
||||
var range = -5;
|
||||
|
||||
// Act
|
||||
var result = EnumerationProtectionHelpers.GetIndexForInputHash(hmacKey, salt, range);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(0, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetIndexForInputHash_ValidInputs_ReturnsConsistentResult()
|
||||
{
|
||||
// Arrange
|
||||
var hmacKey = Encoding.UTF8.GetBytes("test-key-12345678901234567890123456789012");
|
||||
var salt = "test@example.com";
|
||||
var range = 10;
|
||||
|
||||
// Act
|
||||
var result1 = EnumerationProtectionHelpers.GetIndexForInputHash(hmacKey, salt, range);
|
||||
var result2 = EnumerationProtectionHelpers.GetIndexForInputHash(hmacKey, salt, range);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(result1, result2);
|
||||
Assert.InRange(result1, 0, range - 1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetIndexForInputHash_SameInputSameKey_AlwaysReturnsSameResult()
|
||||
{
|
||||
// Arrange
|
||||
var hmacKey = RandomNumberGenerator.GetBytes(32);
|
||||
var salt = "consistent@example.com";
|
||||
var range = 100;
|
||||
|
||||
// Act - Call multiple times
|
||||
var results = new int[10];
|
||||
for (var i = 0; i < 10; i++)
|
||||
{
|
||||
results[i] = EnumerationProtectionHelpers.GetIndexForInputHash(hmacKey, salt, range);
|
||||
}
|
||||
|
||||
// Assert - All results should be identical
|
||||
Assert.All(results, result => Assert.Equal(results[0], result));
|
||||
Assert.All(results, result => Assert.InRange(result, 0, range - 1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetIndexForInputHash_DifferentInputsSameKey_ReturnsDifferentResults()
|
||||
{
|
||||
// Arrange
|
||||
var hmacKey = RandomNumberGenerator.GetBytes(32);
|
||||
var salt1 = "user1@example.com";
|
||||
var salt2 = "user2@example.com";
|
||||
var range = 100;
|
||||
|
||||
// Act
|
||||
var result1 = EnumerationProtectionHelpers.GetIndexForInputHash(hmacKey, salt1, range);
|
||||
var result2 = EnumerationProtectionHelpers.GetIndexForInputHash(hmacKey, salt2, range);
|
||||
|
||||
// Assert
|
||||
Assert.NotEqual(result1, result2);
|
||||
Assert.InRange(result1, 0, range - 1);
|
||||
Assert.InRange(result2, 0, range - 1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetIndexForInputHash_DifferentKeysSameInput_ReturnsDifferentResults()
|
||||
{
|
||||
// Arrange
|
||||
var hmacKey1 = RandomNumberGenerator.GetBytes(32);
|
||||
var hmacKey2 = RandomNumberGenerator.GetBytes(32);
|
||||
var salt = "test@example.com";
|
||||
var range = 100;
|
||||
|
||||
// Act
|
||||
var result1 = EnumerationProtectionHelpers.GetIndexForInputHash(hmacKey1, salt, range);
|
||||
var result2 = EnumerationProtectionHelpers.GetIndexForInputHash(hmacKey2, salt, range);
|
||||
|
||||
// Assert
|
||||
Assert.NotEqual(result1, result2);
|
||||
Assert.InRange(result1, 0, range - 1);
|
||||
Assert.InRange(result2, 0, range - 1);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(1)]
|
||||
[InlineData(2)]
|
||||
[InlineData(5)]
|
||||
[InlineData(10)]
|
||||
[InlineData(100)]
|
||||
[InlineData(1000)]
|
||||
public void GetIndexForInputHash_VariousRanges_ReturnsValidIndex(int range)
|
||||
{
|
||||
// Arrange
|
||||
var hmacKey = RandomNumberGenerator.GetBytes(32);
|
||||
var salt = "test@example.com";
|
||||
|
||||
// Act
|
||||
var result = EnumerationProtectionHelpers.GetIndexForInputHash(hmacKey, salt, range);
|
||||
|
||||
// Assert
|
||||
Assert.InRange(result, 0, range - 1);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("")]
|
||||
[InlineData(" ")]
|
||||
public void GetIndexForInputHash_EmptyString_HandlesGracefully(string salt)
|
||||
{
|
||||
// Arrange
|
||||
var hmacKey = RandomNumberGenerator.GetBytes(32);
|
||||
|
||||
// Act
|
||||
var result = EnumerationProtectionHelpers.GetIndexForInputHash(hmacKey, salt, 10);
|
||||
|
||||
// Assert
|
||||
Assert.InRange(result, 0, 9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetIndexForInputHash_NullInput_ThrowsException()
|
||||
{
|
||||
// Arrange
|
||||
var hmacKey = RandomNumberGenerator.GetBytes(32);
|
||||
string salt = null;
|
||||
var range = 10;
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<NullReferenceException>(() =>
|
||||
EnumerationProtectionHelpers.GetIndexForInputHash(hmacKey, salt, range));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetIndexForInputHash_SpecialCharacters_HandlesCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var hmacKey = RandomNumberGenerator.GetBytes(32);
|
||||
var salt = "test+user@example.com!@#$%^&*()";
|
||||
var range = 50;
|
||||
|
||||
// Act
|
||||
var result1 = EnumerationProtectionHelpers.GetIndexForInputHash(hmacKey, salt, range);
|
||||
var result2 = EnumerationProtectionHelpers.GetIndexForInputHash(hmacKey, salt, range);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(result1, result2);
|
||||
Assert.InRange(result1, 0, range - 1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetIndexForInputHash_UnicodeCharacters_HandlesCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var hmacKey = RandomNumberGenerator.GetBytes(32);
|
||||
var salt = "tëst@éxämplé.cöm";
|
||||
var range = 25;
|
||||
|
||||
// Act
|
||||
var result1 = EnumerationProtectionHelpers.GetIndexForInputHash(hmacKey, salt, range);
|
||||
var result2 = EnumerationProtectionHelpers.GetIndexForInputHash(hmacKey, salt, range);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(result1, result2);
|
||||
Assert.InRange(result1, 0, range - 1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetIndexForInputHash_LongInput_HandlesCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var hmacKey = RandomNumberGenerator.GetBytes(32);
|
||||
var salt = new string('a', 1000) + "@example.com";
|
||||
var range = 30;
|
||||
|
||||
// Act
|
||||
var result = EnumerationProtectionHelpers.GetIndexForInputHash(hmacKey, salt, range);
|
||||
|
||||
// Assert
|
||||
Assert.InRange(result, 0, range - 1);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -1,10 +1,8 @@
|
||||
using Bit.Core;
|
||||
using Bit.Core.Auth.IdentityServer;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Tools.Models.Data;
|
||||
using Bit.Core.Tools.SendFeatures.Queries.Interfaces;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Identity.IdentityServer.Enums;
|
||||
using Bit.Identity.IdentityServer.RequestValidators.SendAccess;
|
||||
using Bit.IntegrationTestCommon.Factories;
|
||||
@@ -13,16 +11,14 @@ using Duende.IdentityServer.Validation;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Identity.IntegrationTest.RequestValidation;
|
||||
namespace Bit.Identity.IntegrationTest.RequestValidation.SendAccess;
|
||||
|
||||
// in order to test the default case for the authentication method, we need to create a custom one so we can ensure the
|
||||
// method throws as expected.
|
||||
internal record AnUnknownAuthenticationMethod : SendAuthenticationMethod { }
|
||||
|
||||
public class SendAccessGrantValidatorIntegrationTests(IdentityApplicationFactory factory) : IClassFixture<IdentityApplicationFactory>
|
||||
public class SendAccessGrantValidatorIntegrationTests(IdentityApplicationFactory _factory) : IClassFixture<IdentityApplicationFactory>
|
||||
{
|
||||
private readonly IdentityApplicationFactory _factory = factory;
|
||||
|
||||
[Fact]
|
||||
public async Task SendAccessGrant_FeatureFlagDisabled_ReturnsUnsupportedGrantType()
|
||||
{
|
||||
@@ -39,7 +35,7 @@ public class SendAccessGrantValidatorIntegrationTests(IdentityApplicationFactory
|
||||
});
|
||||
}).CreateClient();
|
||||
|
||||
var requestBody = CreateTokenRequestBody(sendId);
|
||||
var requestBody = SendAccessTestUtilities.CreateTokenRequestBody(sendId);
|
||||
|
||||
// Act
|
||||
var response = await client.PostAsync("/connect/token", requestBody);
|
||||
@@ -70,7 +66,7 @@ public class SendAccessGrantValidatorIntegrationTests(IdentityApplicationFactory
|
||||
});
|
||||
}).CreateClient();
|
||||
|
||||
var requestBody = CreateTokenRequestBody(sendId);
|
||||
var requestBody = SendAccessTestUtilities.CreateTokenRequestBody(sendId);
|
||||
|
||||
// Act
|
||||
var response = await client.PostAsync("/connect/token", requestBody);
|
||||
@@ -125,7 +121,7 @@ public class SendAccessGrantValidatorIntegrationTests(IdentityApplicationFactory
|
||||
});
|
||||
}).CreateClient();
|
||||
|
||||
var requestBody = CreateTokenRequestBody(sendId);
|
||||
var requestBody = SendAccessTestUtilities.CreateTokenRequestBody(sendId);
|
||||
|
||||
// Act
|
||||
var response = await client.PostAsync("/connect/token", requestBody);
|
||||
@@ -154,7 +150,7 @@ public class SendAccessGrantValidatorIntegrationTests(IdentityApplicationFactory
|
||||
});
|
||||
}).CreateClient();
|
||||
|
||||
var requestBody = CreateTokenRequestBody(sendId);
|
||||
var requestBody = SendAccessTestUtilities.CreateTokenRequestBody(sendId);
|
||||
|
||||
// Act
|
||||
var response = await client.PostAsync("/connect/token", requestBody);
|
||||
@@ -183,7 +179,7 @@ public class SendAccessGrantValidatorIntegrationTests(IdentityApplicationFactory
|
||||
});
|
||||
}).CreateClient();
|
||||
|
||||
var requestBody = CreateTokenRequestBody(sendId);
|
||||
var requestBody = SendAccessTestUtilities.CreateTokenRequestBody(sendId);
|
||||
|
||||
// Act
|
||||
var error = await client.PostAsync("/connect/token", requestBody);
|
||||
@@ -225,7 +221,7 @@ public class SendAccessGrantValidatorIntegrationTests(IdentityApplicationFactory
|
||||
});
|
||||
}).CreateClient();
|
||||
|
||||
var requestBody = CreateTokenRequestBody(sendId, "password123");
|
||||
var requestBody = SendAccessTestUtilities.CreateTokenRequestBody(sendId, "password123");
|
||||
|
||||
// Act
|
||||
var response = await client.PostAsync("/connect/token", requestBody);
|
||||
@@ -236,37 +232,4 @@ public class SendAccessGrantValidatorIntegrationTests(IdentityApplicationFactory
|
||||
Assert.Contains("access_token", content);
|
||||
Assert.Contains("Bearer", content);
|
||||
}
|
||||
|
||||
private static FormUrlEncodedContent CreateTokenRequestBody(
|
||||
Guid sendId,
|
||||
string password = null,
|
||||
string sendEmail = null,
|
||||
string emailOtp = null)
|
||||
{
|
||||
var sendIdBase64 = CoreHelpers.Base64UrlEncode(sendId.ToByteArray());
|
||||
var parameters = new List<KeyValuePair<string, string>>
|
||||
{
|
||||
new(OidcConstants.TokenRequest.GrantType, CustomGrantTypes.SendAccess),
|
||||
new(OidcConstants.TokenRequest.ClientId, BitwardenClient.Send ),
|
||||
new(OidcConstants.TokenRequest.Scope, ApiScopes.ApiSendAccess),
|
||||
new("deviceType", ((int)DeviceType.FirefoxBrowser).ToString()),
|
||||
new(SendAccessConstants.TokenRequest.SendId, sendIdBase64)
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(password))
|
||||
{
|
||||
parameters.Add(new(SendAccessConstants.TokenRequest.ClientB64HashedPassword, password));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(emailOtp) && !string.IsNullOrEmpty(sendEmail))
|
||||
{
|
||||
parameters.AddRange(
|
||||
[
|
||||
new KeyValuePair<string, string>("email", sendEmail),
|
||||
new KeyValuePair<string, string>("email_otp", emailOtp)
|
||||
]);
|
||||
}
|
||||
|
||||
return new FormUrlEncodedContent(parameters);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
using Bit.Core.Auth.IdentityServer;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Identity.IdentityServer.Enums;
|
||||
using Bit.Identity.IdentityServer.RequestValidators.SendAccess;
|
||||
using Duende.IdentityModel;
|
||||
|
||||
namespace Bit.Identity.IntegrationTest.RequestValidation.SendAccess;
|
||||
|
||||
public static class SendAccessTestUtilities
|
||||
{
|
||||
public static FormUrlEncodedContent CreateTokenRequestBody(
|
||||
Guid sendId,
|
||||
string email = null,
|
||||
string emailOtp = null,
|
||||
string password = null)
|
||||
{
|
||||
var sendIdBase64 = CoreHelpers.Base64UrlEncode(sendId.ToByteArray());
|
||||
var parameters = new List<KeyValuePair<string, string>>
|
||||
{
|
||||
new(OidcConstants.TokenRequest.GrantType, CustomGrantTypes.SendAccess),
|
||||
new(OidcConstants.TokenRequest.ClientId, BitwardenClient.Send),
|
||||
new(SendAccessConstants.TokenRequest.SendId, sendIdBase64),
|
||||
new(OidcConstants.TokenRequest.Scope, ApiScopes.ApiSendAccess),
|
||||
new("device_type", "10")
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(email))
|
||||
{
|
||||
parameters.Add(new KeyValuePair<string, string>(SendAccessConstants.TokenRequest.Email, email));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(emailOtp))
|
||||
{
|
||||
parameters.Add(new KeyValuePair<string, string>(SendAccessConstants.TokenRequest.Otp, emailOtp));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(password))
|
||||
{
|
||||
parameters.Add(new KeyValuePair<string, string>(SendAccessConstants.TokenRequest.ClientB64HashedPassword, password));
|
||||
}
|
||||
|
||||
return new FormUrlEncodedContent(parameters);
|
||||
}
|
||||
}
|
||||
@@ -1,28 +1,16 @@
|
||||
using Bit.Core.Auth.Identity.TokenProviders;
|
||||
using Bit.Core.Auth.IdentityServer;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Tools.Models.Data;
|
||||
using Bit.Core.Tools.SendFeatures.Queries.Interfaces;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Identity.IdentityServer.Enums;
|
||||
using Bit.Identity.IdentityServer.RequestValidators.SendAccess;
|
||||
using Bit.IntegrationTestCommon.Factories;
|
||||
using Duende.IdentityModel;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Identity.IntegrationTest.RequestValidation;
|
||||
namespace Bit.Identity.IntegrationTest.RequestValidation.SendAccess;
|
||||
|
||||
public class SendEmailOtpRequestValidatorIntegrationTests : IClassFixture<IdentityApplicationFactory>
|
||||
public class SendEmailOtpRequestValidatorIntegrationTests(IdentityApplicationFactory _factory) : IClassFixture<IdentityApplicationFactory>
|
||||
{
|
||||
private readonly IdentityApplicationFactory _factory;
|
||||
|
||||
public SendEmailOtpRequestValidatorIntegrationTests(IdentityApplicationFactory factory)
|
||||
{
|
||||
_factory = factory;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SendAccess_EmailOtpProtectedSend_MissingEmail_ReturnsInvalidRequest()
|
||||
{
|
||||
@@ -43,7 +31,7 @@ public class SendEmailOtpRequestValidatorIntegrationTests : IClassFixture<Identi
|
||||
});
|
||||
}).CreateClient();
|
||||
|
||||
var requestBody = CreateTokenRequestBody(sendId); // No email
|
||||
var requestBody = SendAccessTestUtilities.CreateTokenRequestBody(sendId); // No email
|
||||
|
||||
// Act
|
||||
var response = await client.PostAsync("/connect/token", requestBody);
|
||||
@@ -87,7 +75,7 @@ public class SendEmailOtpRequestValidatorIntegrationTests : IClassFixture<Identi
|
||||
});
|
||||
}).CreateClient();
|
||||
|
||||
var requestBody = CreateTokenRequestBody(sendId, sendEmail: email); // Email but no OTP
|
||||
var requestBody = SendAccessTestUtilities.CreateTokenRequestBody(sendId, email: email); // Email but no OTP
|
||||
|
||||
// Act
|
||||
var response = await client.PostAsync("/connect/token", requestBody);
|
||||
@@ -130,7 +118,7 @@ public class SendEmailOtpRequestValidatorIntegrationTests : IClassFixture<Identi
|
||||
});
|
||||
}).CreateClient();
|
||||
|
||||
var requestBody = CreateTokenRequestBody(sendId, sendEmail: email, emailOtp: otp);
|
||||
var requestBody = SendAccessTestUtilities.CreateTokenRequestBody(sendId, email: email, emailOtp: otp);
|
||||
|
||||
// Act
|
||||
var response = await client.PostAsync("/connect/token", requestBody);
|
||||
@@ -174,7 +162,7 @@ public class SendEmailOtpRequestValidatorIntegrationTests : IClassFixture<Identi
|
||||
});
|
||||
}).CreateClient();
|
||||
|
||||
var requestBody = CreateTokenRequestBody(sendId, sendEmail: email, emailOtp: invalidOtp);
|
||||
var requestBody = SendAccessTestUtilities.CreateTokenRequestBody(sendId, email: email, emailOtp: invalidOtp);
|
||||
|
||||
// Act
|
||||
var response = await client.PostAsync("/connect/token", requestBody);
|
||||
@@ -216,7 +204,7 @@ public class SendEmailOtpRequestValidatorIntegrationTests : IClassFixture<Identi
|
||||
});
|
||||
}).CreateClient();
|
||||
|
||||
var requestBody = CreateTokenRequestBody(sendId, sendEmail: email); // Email but no OTP
|
||||
var requestBody = SendAccessTestUtilities.CreateTokenRequestBody(sendId, email: email); // Email but no OTP
|
||||
|
||||
// Act
|
||||
var response = await client.PostAsync("/connect/token", requestBody);
|
||||
@@ -225,32 +213,4 @@ public class SendEmailOtpRequestValidatorIntegrationTests : IClassFixture<Identi
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
Assert.Contains(OidcConstants.TokenErrors.InvalidRequest, content);
|
||||
}
|
||||
|
||||
private static FormUrlEncodedContent CreateTokenRequestBody(Guid sendId,
|
||||
string sendEmail = null, string emailOtp = null)
|
||||
{
|
||||
var sendIdBase64 = CoreHelpers.Base64UrlEncode(sendId.ToByteArray());
|
||||
var parameters = new List<KeyValuePair<string, string>>
|
||||
{
|
||||
new(OidcConstants.TokenRequest.GrantType, CustomGrantTypes.SendAccess),
|
||||
new(OidcConstants.TokenRequest.ClientId, BitwardenClient.Send ),
|
||||
new(OidcConstants.TokenRequest.Scope, ApiScopes.ApiSendAccess),
|
||||
new("deviceType", ((int)DeviceType.FirefoxBrowser).ToString()),
|
||||
new(SendAccessConstants.TokenRequest.SendId, sendIdBase64)
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(sendEmail))
|
||||
{
|
||||
parameters.Add(new KeyValuePair<string, string>(
|
||||
SendAccessConstants.TokenRequest.Email, sendEmail));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(emailOtp))
|
||||
{
|
||||
parameters.Add(new KeyValuePair<string, string>(
|
||||
SendAccessConstants.TokenRequest.Otp, emailOtp));
|
||||
}
|
||||
|
||||
return new FormUrlEncodedContent(parameters);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Tools.Models.Data;
|
||||
using Bit.Core.Tools.SendFeatures.Queries.Interfaces;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Identity.IdentityServer.RequestValidators.SendAccess;
|
||||
using Bit.IntegrationTestCommon.Factories;
|
||||
using Duende.IdentityModel;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Identity.IntegrationTest.RequestValidation.SendAccess;
|
||||
|
||||
public class SendNeverAuthenticateRequestValidatorIntegrationTests(
|
||||
IdentityApplicationFactory _factory) : IClassFixture<IdentityApplicationFactory>
|
||||
{
|
||||
/// <summary>
|
||||
/// To support the static hashing function <see cref="EnumerationProtectionHelpers.GetIndexForInputHash"/> theses GUIDs and Key must be hardcoded
|
||||
/// </summary>
|
||||
private static readonly string _testHashKey = "test-key-123456789012345678901234567890";
|
||||
// These Guids are static and ensure the correct index for each error type
|
||||
private static readonly Guid _invalidSendGuid = Guid.Parse("1b35fbf3-a14a-4d48-82b7-2adc34fdae6f");
|
||||
private static readonly Guid _emailSendGuid = Guid.Parse("bc8e2ef5-a0bd-44d2-bdb7-5902be6f5c41");
|
||||
private static readonly Guid _passwordSendGuid = Guid.Parse("da36fa27-f0e8-4701-a585-d3d8c2f67c4b");
|
||||
|
||||
[Fact]
|
||||
public async Task SendAccess_NeverAuthenticateSend_NoParameters_ReturnsInvalidSendId()
|
||||
{
|
||||
// Arrange
|
||||
var client = ConfigureTestHttpClient(_invalidSendGuid);
|
||||
var requestBody = SendAccessTestUtilities.CreateTokenRequestBody(_invalidSendGuid);
|
||||
|
||||
// Act
|
||||
var response = await client.PostAsync("/connect/token", requestBody);
|
||||
|
||||
// Assert
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
|
||||
Assert.Contains(OidcConstants.TokenErrors.InvalidGrant, content);
|
||||
|
||||
var expectedError = SendAccessConstants.SendIdGuidValidatorResults.InvalidSendId;
|
||||
Assert.Contains(expectedError, content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SendAccess_NeverAuthenticateSend_ReturnsEmailRequired()
|
||||
{
|
||||
// Arrange
|
||||
var client = ConfigureTestHttpClient(_emailSendGuid);
|
||||
var requestBody = SendAccessTestUtilities.CreateTokenRequestBody(_emailSendGuid);
|
||||
|
||||
// Act
|
||||
var response = await client.PostAsync("/connect/token", requestBody);
|
||||
|
||||
// Assert
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
|
||||
// should be invalid grant
|
||||
Assert.Contains(OidcConstants.TokenErrors.InvalidRequest, content);
|
||||
|
||||
// Try to compel the invalid email error
|
||||
var expectedError = SendAccessConstants.EmailOtpValidatorResults.EmailRequired;
|
||||
Assert.Contains(expectedError, content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SendAccess_NeverAuthenticateSend_WithEmail_ReturnsEmailInvalid()
|
||||
{
|
||||
// Arrange
|
||||
var email = "test@example.com";
|
||||
var client = ConfigureTestHttpClient(_emailSendGuid);
|
||||
var requestBody = SendAccessTestUtilities.CreateTokenRequestBody(_emailSendGuid, email: email);
|
||||
|
||||
// Act
|
||||
var response = await client.PostAsync("/connect/token", requestBody);
|
||||
|
||||
// Assert
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
|
||||
// should be invalid grant
|
||||
Assert.Contains(OidcConstants.TokenErrors.InvalidGrant, content);
|
||||
|
||||
// Try to compel the invalid email error
|
||||
var expectedError = SendAccessConstants.EmailOtpValidatorResults.EmailInvalid;
|
||||
Assert.Contains(expectedError, content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SendAccess_NeverAuthenticateSend_ReturnsPasswordRequired()
|
||||
{
|
||||
// Arrange
|
||||
var client = ConfigureTestHttpClient(_passwordSendGuid);
|
||||
|
||||
var requestBody = SendAccessTestUtilities.CreateTokenRequestBody(_passwordSendGuid);
|
||||
|
||||
// Act
|
||||
var response = await client.PostAsync("/connect/token", requestBody);
|
||||
|
||||
// Assert
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
|
||||
Assert.Contains(OidcConstants.TokenErrors.InvalidGrant, content);
|
||||
|
||||
var expectedError = SendAccessConstants.PasswordValidatorResults.RequestPasswordIsRequired;
|
||||
Assert.Contains(expectedError, content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SendAccess_NeverAuthenticateSend_WithPassword_ReturnsPasswordInvalid()
|
||||
{
|
||||
// Arrange
|
||||
var password = "test-password-hash";
|
||||
|
||||
var client = ConfigureTestHttpClient(_passwordSendGuid);
|
||||
|
||||
var requestBody = SendAccessTestUtilities.CreateTokenRequestBody(_passwordSendGuid, password: password);
|
||||
|
||||
// Act
|
||||
var response = await client.PostAsync("/connect/token", requestBody);
|
||||
|
||||
// Assert
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
|
||||
Assert.Contains(OidcConstants.TokenErrors.InvalidRequest, content);
|
||||
|
||||
var expectedError = SendAccessConstants.PasswordValidatorResults.RequestPasswordDoesNotMatch;
|
||||
Assert.Contains(expectedError, content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SendAccess_NeverAuthenticateSend_ConsistentResponse_SameSendId()
|
||||
{
|
||||
// Arrange
|
||||
var client = ConfigureTestHttpClient(_emailSendGuid);
|
||||
|
||||
var requestBody1 = SendAccessTestUtilities.CreateTokenRequestBody(_emailSendGuid);
|
||||
var requestBody2 = SendAccessTestUtilities.CreateTokenRequestBody(_emailSendGuid);
|
||||
|
||||
// Act
|
||||
var response1 = await client.PostAsync("/connect/token", requestBody1);
|
||||
var response2 = await client.PostAsync("/connect/token", requestBody2);
|
||||
|
||||
// Assert
|
||||
var content1 = await response1.Content.ReadAsStringAsync();
|
||||
var content2 = await response2.Content.ReadAsStringAsync();
|
||||
|
||||
Assert.Equal(content1, content2);
|
||||
}
|
||||
|
||||
private HttpClient ConfigureTestHttpClient(Guid sendId)
|
||||
{
|
||||
_factory.UpdateConfiguration(
|
||||
"globalSettings:sendDefaultHashKey", _testHashKey);
|
||||
return _factory.WithWebHostBuilder(builder =>
|
||||
{
|
||||
builder.ConfigureServices(services =>
|
||||
{
|
||||
var featureService = Substitute.For<IFeatureService>();
|
||||
featureService.IsEnabled(Arg.Any<string>()).Returns(true);
|
||||
services.AddSingleton(featureService);
|
||||
|
||||
var sendAuthQuery = Substitute.For<ISendAuthenticationQuery>();
|
||||
sendAuthQuery.GetAuthenticationMethod(sendId)
|
||||
.Returns(new NeverAuthenticate());
|
||||
services.AddSingleton(sendAuthQuery);
|
||||
});
|
||||
}).CreateClient();
|
||||
}
|
||||
}
|
||||
@@ -1,28 +1,17 @@
|
||||
using Bit.Core.Auth.IdentityServer;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.KeyManagement.Sends;
|
||||
using Bit.Core.KeyManagement.Sends;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Tools.Models.Data;
|
||||
using Bit.Core.Tools.SendFeatures.Queries.Interfaces;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Identity.IdentityServer.Enums;
|
||||
using Bit.Identity.IdentityServer.RequestValidators.SendAccess;
|
||||
using Bit.IntegrationTestCommon.Factories;
|
||||
using Duende.IdentityModel;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Identity.IntegrationTest.RequestValidation;
|
||||
namespace Bit.Identity.IntegrationTest.RequestValidation.SendAccess;
|
||||
|
||||
public class SendPasswordRequestValidatorIntegrationTests : IClassFixture<IdentityApplicationFactory>
|
||||
public class SendPasswordRequestValidatorIntegrationTests(IdentityApplicationFactory _factory) : IClassFixture<IdentityApplicationFactory>
|
||||
{
|
||||
private readonly IdentityApplicationFactory _factory;
|
||||
|
||||
public SendPasswordRequestValidatorIntegrationTests(IdentityApplicationFactory factory)
|
||||
{
|
||||
_factory = factory;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SendAccess_PasswordProtectedSend_ValidPassword_ReturnsAccessToken()
|
||||
{
|
||||
@@ -54,7 +43,7 @@ public class SendPasswordRequestValidatorIntegrationTests : IClassFixture<Identi
|
||||
});
|
||||
}).CreateClient();
|
||||
|
||||
var requestBody = CreateTokenRequestBody(sendId, clientPasswordHash);
|
||||
var requestBody = SendAccessTestUtilities.CreateTokenRequestBody(sendId, password: clientPasswordHash);
|
||||
|
||||
// Act
|
||||
var response = await client.PostAsync("/connect/token", requestBody);
|
||||
@@ -95,7 +84,7 @@ public class SendPasswordRequestValidatorIntegrationTests : IClassFixture<Identi
|
||||
});
|
||||
}).CreateClient();
|
||||
|
||||
var requestBody = CreateTokenRequestBody(sendId, wrongClientPasswordHash);
|
||||
var requestBody = SendAccessTestUtilities.CreateTokenRequestBody(sendId, password: wrongClientPasswordHash);
|
||||
|
||||
// Act
|
||||
var response = await client.PostAsync("/connect/token", requestBody);
|
||||
@@ -131,7 +120,7 @@ public class SendPasswordRequestValidatorIntegrationTests : IClassFixture<Identi
|
||||
});
|
||||
}).CreateClient();
|
||||
|
||||
var requestBody = CreateTokenRequestBody(sendId); // No password
|
||||
var requestBody = SendAccessTestUtilities.CreateTokenRequestBody(sendId); // No password
|
||||
|
||||
// Act
|
||||
var response = await client.PostAsync("/connect/token", requestBody);
|
||||
@@ -176,7 +165,7 @@ public class SendPasswordRequestValidatorIntegrationTests : IClassFixture<Identi
|
||||
});
|
||||
}).CreateClient();
|
||||
|
||||
var requestBody = CreateTokenRequestBody(sendId, string.Empty);
|
||||
var requestBody = SendAccessTestUtilities.CreateTokenRequestBody(sendId, string.Empty);
|
||||
|
||||
// Act
|
||||
var response = await client.PostAsync("/connect/token", requestBody);
|
||||
@@ -186,24 +175,4 @@ public class SendPasswordRequestValidatorIntegrationTests : IClassFixture<Identi
|
||||
Assert.Contains(OidcConstants.TokenErrors.InvalidRequest, content);
|
||||
Assert.Contains($"{SendAccessConstants.TokenRequest.ClientB64HashedPassword} is required", content);
|
||||
}
|
||||
|
||||
private static FormUrlEncodedContent CreateTokenRequestBody(Guid sendId, string passwordHash = null)
|
||||
{
|
||||
var sendIdBase64 = CoreHelpers.Base64UrlEncode(sendId.ToByteArray());
|
||||
var parameters = new List<KeyValuePair<string, string>>
|
||||
{
|
||||
new(OidcConstants.TokenRequest.GrantType, CustomGrantTypes.SendAccess),
|
||||
new(OidcConstants.TokenRequest.ClientId, BitwardenClient.Send),
|
||||
new(SendAccessConstants.TokenRequest.SendId, sendIdBase64),
|
||||
new(OidcConstants.TokenRequest.Scope, ApiScopes.ApiSendAccess),
|
||||
new("deviceType", "10")
|
||||
};
|
||||
|
||||
if (passwordHash != null)
|
||||
{
|
||||
parameters.Add(new KeyValuePair<string, string>(SendAccessConstants.TokenRequest.ClientB64HashedPassword, passwordHash));
|
||||
}
|
||||
|
||||
return new FormUrlEncodedContent(parameters);
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ using Bit.Test.Common.Helpers;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Identity.IntegrationTest.RequestValidation;
|
||||
namespace Bit.Identity.IntegrationTest.RequestValidation.VaultAccess;
|
||||
|
||||
public class ResourceOwnerPasswordValidatorTests : IClassFixture<IdentityApplicationFactory>
|
||||
{
|
||||
26
test/Identity.Test/AutoFixture/OrganizationUserFixtures.cs
Normal file
26
test/Identity.Test/AutoFixture/OrganizationUserFixtures.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using System.Reflection;
|
||||
using AutoFixture;
|
||||
using AutoFixture.Xunit2;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Identity.Test.AutoFixture;
|
||||
|
||||
internal class OrganizationUserWithDefaultPermissionsCustomization : ICustomization
|
||||
{
|
||||
public void Customize(IFixture fixture)
|
||||
{
|
||||
fixture.Customize<OrganizationUser>(composer => composer
|
||||
// On OrganizationUser, Permissions can be JSON data (as string) or sometimes null.
|
||||
// Entity APIs should prevent it from being anything else.
|
||||
// An un-modified fixture for OrganizationUser will return a bare string Permissions{guid}
|
||||
// in the member, throwing a JsonException on deserialization of a bare string.
|
||||
.With(organizationUser => organizationUser.Permissions, CoreHelpers.ClassToJsonData(new Permissions())));
|
||||
}
|
||||
}
|
||||
|
||||
public class OrganizationUserWithDefaultPermissionsAttribute : CustomizeAttribute
|
||||
{
|
||||
public override ICustomization GetCustomization(ParameterInfo parameter) => new OrganizationUserWithDefaultPermissionsCustomization();
|
||||
}
|
||||
@@ -1,12 +1,8 @@
|
||||
using System.Collections.Specialized;
|
||||
using Bit.Core;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Auth.Identity;
|
||||
using Bit.Core.Auth.IdentityServer;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Tools.Models.Data;
|
||||
using Bit.Core.Tools.SendFeatures.Queries.Interfaces;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Identity.IdentityServer.Enums;
|
||||
using Bit.Identity.IdentityServer.RequestValidators.SendAccess;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
@@ -81,7 +77,7 @@ public class SendAccessGrantValidatorTests
|
||||
var context = new ExtensionGrantValidationContext();
|
||||
|
||||
tokenRequest.GrantType = CustomGrantTypes.SendAccess;
|
||||
tokenRequest.Raw = CreateTokenRequestBody(Guid.Empty);
|
||||
tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(Guid.Empty);
|
||||
|
||||
// To preserve the CreateTokenRequestBody method for more general usage we over write the sendId
|
||||
tokenRequest.Raw.Set(SendAccessConstants.TokenRequest.SendId, "invalid-guid-format");
|
||||
@@ -118,7 +114,9 @@ public class SendAccessGrantValidatorTests
|
||||
public async Task ValidateAsync_NeverAuthenticateMethod_ReturnsInvalidGrant(
|
||||
[AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
|
||||
SutProvider<SendAccessGrantValidator> sutProvider,
|
||||
Guid sendId)
|
||||
NeverAuthenticate neverAuthenticate,
|
||||
Guid sendId,
|
||||
GrantValidationResult expectedResult)
|
||||
{
|
||||
// Arrange
|
||||
var context = SetupTokenRequest(
|
||||
@@ -128,14 +126,20 @@ public class SendAccessGrantValidatorTests
|
||||
|
||||
sutProvider.GetDependency<ISendAuthenticationQuery>()
|
||||
.GetAuthenticationMethod(sendId)
|
||||
.Returns(new NeverAuthenticate());
|
||||
.Returns(neverAuthenticate);
|
||||
|
||||
sutProvider.GetDependency<ISendAuthenticationMethodValidator<NeverAuthenticate>>()
|
||||
.ValidateRequestAsync(context, neverAuthenticate, sendId)
|
||||
.Returns(expectedResult);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.ValidateAsync(context);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(OidcConstants.TokenErrors.InvalidGrant, context.Result.Error);
|
||||
Assert.Equal($"{SendAccessConstants.TokenRequest.SendId} is invalid.", context.Result.ErrorDescription);
|
||||
Assert.Equal(expectedResult, context.Result);
|
||||
await sutProvider.GetDependency<ISendAuthenticationMethodValidator<NeverAuthenticate>>()
|
||||
.Received(1)
|
||||
.ValidateRequestAsync(context, neverAuthenticate, sendId);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
@@ -264,7 +268,7 @@ public class SendAccessGrantValidatorTests
|
||||
public void GrantType_ReturnsCorrectType()
|
||||
{
|
||||
// Arrange & Act
|
||||
var validator = new SendAccessGrantValidator(null!, null!, null!, null!);
|
||||
var validator = new SendAccessGrantValidator(null!, null!, null!, null!, null!);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(CustomGrantTypes.SendAccess, ((IExtensionGrantValidator)validator).GrantType);
|
||||
@@ -289,44 +293,9 @@ public class SendAccessGrantValidatorTests
|
||||
var context = new ExtensionGrantValidationContext();
|
||||
|
||||
request.GrantType = CustomGrantTypes.SendAccess;
|
||||
request.Raw = CreateTokenRequestBody(sendId);
|
||||
request.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(sendId);
|
||||
context.Request = request;
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
private static NameValueCollection CreateTokenRequestBody(
|
||||
Guid sendId,
|
||||
string passwordHash = null,
|
||||
string sendEmail = null,
|
||||
string otpCode = null)
|
||||
{
|
||||
var sendIdBase64 = CoreHelpers.Base64UrlEncode(sendId.ToByteArray());
|
||||
|
||||
var rawRequestParameters = new NameValueCollection
|
||||
{
|
||||
{ OidcConstants.TokenRequest.GrantType, CustomGrantTypes.SendAccess },
|
||||
{ OidcConstants.TokenRequest.ClientId, BitwardenClient.Send },
|
||||
{ OidcConstants.TokenRequest.Scope, ApiScopes.ApiSendAccess },
|
||||
{ "deviceType", ((int)DeviceType.FirefoxBrowser).ToString() },
|
||||
{ SendAccessConstants.TokenRequest.SendId, sendIdBase64 }
|
||||
};
|
||||
|
||||
if (passwordHash != null)
|
||||
{
|
||||
rawRequestParameters.Add(SendAccessConstants.TokenRequest.ClientB64HashedPassword, passwordHash);
|
||||
}
|
||||
|
||||
if (sendEmail != null)
|
||||
{
|
||||
rawRequestParameters.Add(SendAccessConstants.TokenRequest.Email, sendEmail);
|
||||
}
|
||||
|
||||
if (otpCode != null && sendEmail != null)
|
||||
{
|
||||
rawRequestParameters.Add(SendAccessConstants.TokenRequest.Otp, otpCode);
|
||||
}
|
||||
|
||||
return rawRequestParameters;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
using System.Collections.Specialized;
|
||||
using Bit.Core.Auth.IdentityServer;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Identity.IdentityServer.Enums;
|
||||
using Bit.Identity.IdentityServer.RequestValidators.SendAccess;
|
||||
using Duende.IdentityModel;
|
||||
|
||||
namespace Bit.Identity.Test.IdentityServer.SendAccess;
|
||||
|
||||
public static class SendAccessTestUtilities
|
||||
{
|
||||
public static NameValueCollection CreateValidatedTokenRequest(
|
||||
Guid sendId,
|
||||
string sendEmail = null,
|
||||
string otpCode = null,
|
||||
params string[] passwordHash)
|
||||
{
|
||||
var sendIdBase64 = CoreHelpers.Base64UrlEncode(sendId.ToByteArray());
|
||||
|
||||
var rawRequestParameters = new NameValueCollection
|
||||
{
|
||||
{ OidcConstants.TokenRequest.GrantType, CustomGrantTypes.SendAccess },
|
||||
{ OidcConstants.TokenRequest.ClientId, BitwardenClient.Send },
|
||||
{ OidcConstants.TokenRequest.Scope, ApiScopes.ApiSendAccess },
|
||||
{ "device_type", ((int)DeviceType.FirefoxBrowser).ToString() },
|
||||
{ SendAccessConstants.TokenRequest.SendId, sendIdBase64 }
|
||||
};
|
||||
|
||||
if (sendEmail != null)
|
||||
{
|
||||
rawRequestParameters.Add(SendAccessConstants.TokenRequest.Email, sendEmail);
|
||||
}
|
||||
|
||||
if (otpCode != null && sendEmail != null)
|
||||
{
|
||||
rawRequestParameters.Add(SendAccessConstants.TokenRequest.Otp, otpCode);
|
||||
}
|
||||
|
||||
if (passwordHash != null && passwordHash.Length > 0)
|
||||
{
|
||||
foreach (var hash in passwordHash)
|
||||
{
|
||||
rawRequestParameters.Add(SendAccessConstants.TokenRequest.ClientB64HashedPassword, hash);
|
||||
}
|
||||
}
|
||||
|
||||
return rawRequestParameters;
|
||||
}
|
||||
}
|
||||
@@ -31,9 +31,9 @@ public class SendConstantsSnapshotTests
|
||||
public void GrantValidatorResults_Constants_HaveCorrectValues()
|
||||
{
|
||||
// Assert
|
||||
Assert.Equal("valid_send_guid", SendAccessConstants.GrantValidatorResults.ValidSendGuid);
|
||||
Assert.Equal("send_id_required", SendAccessConstants.GrantValidatorResults.SendIdRequired);
|
||||
Assert.Equal("send_id_invalid", SendAccessConstants.GrantValidatorResults.InvalidSendId);
|
||||
Assert.Equal("valid_send_guid", SendAccessConstants.SendIdGuidValidatorResults.ValidSendGuid);
|
||||
Assert.Equal("send_id_required", SendAccessConstants.SendIdGuidValidatorResults.SendIdRequired);
|
||||
Assert.Equal("send_id_invalid", SendAccessConstants.SendIdGuidValidatorResults.InvalidSendId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
using System.Collections.Specialized;
|
||||
using Bit.Core.Auth.Identity;
|
||||
using Bit.Core.Auth.Identity;
|
||||
using Bit.Core.Auth.Identity.TokenProviders;
|
||||
using Bit.Core.Auth.IdentityServer;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Tools.Models.Data;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Identity.IdentityServer.Enums;
|
||||
using Bit.Identity.IdentityServer.RequestValidators.SendAccess;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
@@ -28,7 +23,7 @@ public class SendEmailOtpRequestValidatorTests
|
||||
Guid sendId)
|
||||
{
|
||||
// Arrange
|
||||
tokenRequest.Raw = CreateValidatedTokenRequest(sendId);
|
||||
tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(sendId);
|
||||
var context = new ExtensionGrantValidationContext
|
||||
{
|
||||
Request = tokenRequest
|
||||
@@ -61,8 +56,7 @@ public class SendEmailOtpRequestValidatorTests
|
||||
Guid sendId)
|
||||
{
|
||||
// Arrange
|
||||
tokenRequest.Raw = CreateValidatedTokenRequest(sendId, email);
|
||||
var emailOTP = new EmailOtp(["user@test.dev"]);
|
||||
tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(sendId, email);
|
||||
var context = new ExtensionGrantValidationContext
|
||||
{
|
||||
Request = tokenRequest
|
||||
@@ -96,7 +90,7 @@ public class SendEmailOtpRequestValidatorTests
|
||||
string generatedToken)
|
||||
{
|
||||
// Arrange
|
||||
tokenRequest.Raw = CreateValidatedTokenRequest(sendId, email);
|
||||
tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(sendId, email);
|
||||
var context = new ExtensionGrantValidationContext
|
||||
{
|
||||
Request = tokenRequest
|
||||
@@ -144,7 +138,7 @@ public class SendEmailOtpRequestValidatorTests
|
||||
string email)
|
||||
{
|
||||
// Arrange
|
||||
tokenRequest.Raw = CreateValidatedTokenRequest(sendId, email);
|
||||
tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(sendId, email);
|
||||
var context = new ExtensionGrantValidationContext
|
||||
{
|
||||
Request = tokenRequest
|
||||
@@ -179,7 +173,7 @@ public class SendEmailOtpRequestValidatorTests
|
||||
string otp)
|
||||
{
|
||||
// Arrange
|
||||
tokenRequest.Raw = CreateValidatedTokenRequest(sendId, email, otp);
|
||||
tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(sendId, email, otp);
|
||||
var context = new ExtensionGrantValidationContext
|
||||
{
|
||||
Request = tokenRequest
|
||||
@@ -231,7 +225,7 @@ public class SendEmailOtpRequestValidatorTests
|
||||
string invalidOtp)
|
||||
{
|
||||
// Arrange
|
||||
tokenRequest.Raw = CreateValidatedTokenRequest(sendId, email, invalidOtp);
|
||||
tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(sendId, email, invalidOtp);
|
||||
var context = new ExtensionGrantValidationContext
|
||||
{
|
||||
Request = tokenRequest
|
||||
@@ -278,33 +272,4 @@ public class SendEmailOtpRequestValidatorTests
|
||||
// Assert
|
||||
Assert.NotNull(validator);
|
||||
}
|
||||
|
||||
private static NameValueCollection CreateValidatedTokenRequest(
|
||||
Guid sendId,
|
||||
string sendEmail = null,
|
||||
string otpCode = null)
|
||||
{
|
||||
var sendIdBase64 = CoreHelpers.Base64UrlEncode(sendId.ToByteArray());
|
||||
|
||||
var rawRequestParameters = new NameValueCollection
|
||||
{
|
||||
{ OidcConstants.TokenRequest.GrantType, CustomGrantTypes.SendAccess },
|
||||
{ OidcConstants.TokenRequest.ClientId, BitwardenClient.Send },
|
||||
{ OidcConstants.TokenRequest.Scope, ApiScopes.ApiSendAccess },
|
||||
{ "device_type", ((int)DeviceType.FirefoxBrowser).ToString() },
|
||||
{ SendAccessConstants.TokenRequest.SendId, sendIdBase64 }
|
||||
};
|
||||
|
||||
if (sendEmail != null)
|
||||
{
|
||||
rawRequestParameters.Add(SendAccessConstants.TokenRequest.Email, sendEmail);
|
||||
}
|
||||
|
||||
if (otpCode != null && sendEmail != null)
|
||||
{
|
||||
rawRequestParameters.Add(SendAccessConstants.TokenRequest.Otp, otpCode);
|
||||
}
|
||||
|
||||
return rawRequestParameters;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,280 @@
|
||||
using Bit.Core.Tools.Models.Data;
|
||||
using Bit.Identity.IdentityServer.RequestValidators.SendAccess;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Duende.IdentityModel;
|
||||
using Duende.IdentityServer.Validation;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Identity.Test.IdentityServer.SendAccess;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class SendNeverAuthenticateRequestValidatorTests
|
||||
{
|
||||
/// <summary>
|
||||
/// To support the static hashing function <see cref="EnumerationProtectionHelpers.GetIndexForSaltHash"/> theses GUIDs and Key must be hardcoded
|
||||
/// </summary>
|
||||
private static readonly string _testHashKey = "test-key-123456789012345678901234567890";
|
||||
// These Guids are static and ensure the correct index for each error type
|
||||
private static readonly Guid _invalidSendGuid = Guid.Parse("1b35fbf3-a14a-4d48-82b7-2adc34fdae6f");
|
||||
private static readonly Guid _emailSendGuid = Guid.Parse("bc8e2ef5-a0bd-44d2-bdb7-5902be6f5c41");
|
||||
private static readonly Guid _passwordSendGuid = Guid.Parse("da36fa27-f0e8-4701-a585-d3d8c2f67c4b");
|
||||
|
||||
private static readonly NeverAuthenticate _authMethod = new();
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ValidateRequestAsync_GuidErrorSelected_ReturnsInvalidSendId(
|
||||
SutProvider<SendNeverAuthenticateRequestValidator> sutProvider,
|
||||
[AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest)
|
||||
{
|
||||
// Arrange
|
||||
tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(_invalidSendGuid);
|
||||
var context = new ExtensionGrantValidationContext
|
||||
{
|
||||
Request = tokenRequest
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<Core.Settings.GlobalSettings>().SendDefaultHashKey = _testHashKey;
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ValidateRequestAsync(context, _authMethod, _invalidSendGuid);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsError);
|
||||
Assert.Equal(OidcConstants.TokenErrors.InvalidGrant, result.Error);
|
||||
Assert.Equal(SendAccessConstants.SendIdGuidValidatorResults.InvalidSendId, result.ErrorDescription);
|
||||
|
||||
var customResponse = result.CustomResponse as Dictionary<string, object>;
|
||||
Assert.NotNull(customResponse);
|
||||
Assert.Equal(
|
||||
SendAccessConstants.SendIdGuidValidatorResults.InvalidSendId, customResponse[SendAccessConstants.SendAccessError]);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ValidateRequestAsync_EmailErrorSelected_HasEmail_ReturnsEmailInvalid(
|
||||
SutProvider<SendNeverAuthenticateRequestValidator> sutProvider,
|
||||
[AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
|
||||
string email)
|
||||
{
|
||||
// Arrange
|
||||
tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(_emailSendGuid, sendEmail: email);
|
||||
var context = new ExtensionGrantValidationContext
|
||||
{
|
||||
Request = tokenRequest
|
||||
};
|
||||
sutProvider.GetDependency<Core.Settings.GlobalSettings>().SendDefaultHashKey = _testHashKey;
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ValidateRequestAsync(context, _authMethod, _emailSendGuid);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsError);
|
||||
Assert.Equal(OidcConstants.TokenErrors.InvalidGrant, result.Error);
|
||||
Assert.Equal(SendAccessConstants.EmailOtpValidatorResults.EmailInvalid, result.ErrorDescription);
|
||||
|
||||
var customResponse = result.CustomResponse as Dictionary<string, object>;
|
||||
Assert.NotNull(customResponse);
|
||||
Assert.Equal(SendAccessConstants.EmailOtpValidatorResults.EmailInvalid, customResponse[SendAccessConstants.SendAccessError]);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ValidateRequestAsync_EmailErrorSelected_NoEmail_ReturnsEmailRequired(
|
||||
SutProvider<SendNeverAuthenticateRequestValidator> sutProvider,
|
||||
[AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest)
|
||||
{
|
||||
// Arrange
|
||||
tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(_emailSendGuid);
|
||||
var context = new ExtensionGrantValidationContext
|
||||
{
|
||||
Request = tokenRequest
|
||||
};
|
||||
sutProvider.GetDependency<Core.Settings.GlobalSettings>().SendDefaultHashKey = _testHashKey;
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ValidateRequestAsync(context, _authMethod, _emailSendGuid);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsError);
|
||||
Assert.Equal(OidcConstants.TokenErrors.InvalidRequest, result.Error);
|
||||
Assert.Equal(SendAccessConstants.EmailOtpValidatorResults.EmailRequired, result.ErrorDescription);
|
||||
|
||||
var customResponse = result.CustomResponse as Dictionary<string, object>;
|
||||
Assert.NotNull(customResponse);
|
||||
Assert.Equal(SendAccessConstants.EmailOtpValidatorResults.EmailRequired, customResponse[SendAccessConstants.SendAccessError]);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ValidateRequestAsync_PasswordErrorSelected_HasPassword_ReturnsPasswordDoesNotMatch(
|
||||
SutProvider<SendNeverAuthenticateRequestValidator> sutProvider,
|
||||
[AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
|
||||
string password)
|
||||
{
|
||||
// Arrange
|
||||
tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(_passwordSendGuid, passwordHash: password);
|
||||
var context = new ExtensionGrantValidationContext
|
||||
{
|
||||
Request = tokenRequest
|
||||
};
|
||||
sutProvider.GetDependency<Core.Settings.GlobalSettings>().SendDefaultHashKey = _testHashKey;
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ValidateRequestAsync(context, _authMethod, _passwordSendGuid);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsError);
|
||||
Assert.Equal(OidcConstants.TokenErrors.InvalidRequest, result.Error);
|
||||
Assert.Equal(SendAccessConstants.PasswordValidatorResults.RequestPasswordDoesNotMatch, result.ErrorDescription);
|
||||
|
||||
var customResponse = result.CustomResponse as Dictionary<string, object>;
|
||||
Assert.NotNull(customResponse);
|
||||
Assert.Equal(SendAccessConstants.PasswordValidatorResults.RequestPasswordDoesNotMatch, customResponse[SendAccessConstants.SendAccessError]);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ValidateRequestAsync_PasswordErrorSelected_NoPassword_ReturnsPasswordRequired(
|
||||
SutProvider<SendNeverAuthenticateRequestValidator> sutProvider,
|
||||
[AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest)
|
||||
{
|
||||
// Arrange
|
||||
|
||||
tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(_passwordSendGuid);
|
||||
var context = new ExtensionGrantValidationContext
|
||||
{
|
||||
Request = tokenRequest
|
||||
};
|
||||
sutProvider.GetDependency<Core.Settings.GlobalSettings>().SendDefaultHashKey = _testHashKey;
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ValidateRequestAsync(context, _authMethod, _passwordSendGuid);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsError);
|
||||
Assert.Equal(OidcConstants.TokenErrors.InvalidGrant, result.Error);
|
||||
Assert.Equal(SendAccessConstants.PasswordValidatorResults.RequestPasswordIsRequired, result.ErrorDescription);
|
||||
|
||||
var customResponse = result.CustomResponse as Dictionary<string, object>;
|
||||
Assert.NotNull(customResponse);
|
||||
Assert.Equal(SendAccessConstants.PasswordValidatorResults.RequestPasswordIsRequired, customResponse[SendAccessConstants.SendAccessError]);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ValidateRequestAsync_NullHashKey_UsesEmptyKey(
|
||||
SutProvider<SendNeverAuthenticateRequestValidator> sutProvider,
|
||||
[AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest)
|
||||
{
|
||||
// Arrange
|
||||
tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(_invalidSendGuid);
|
||||
var context = new ExtensionGrantValidationContext { Request = tokenRequest };
|
||||
sutProvider.GetDependency<Core.Settings.GlobalSettings>().SendDefaultHashKey = null;
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ValidateRequestAsync(context, _authMethod, _invalidSendGuid);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsError);
|
||||
Assert.Equal(OidcConstants.TokenErrors.InvalidGrant, result.Error);
|
||||
Assert.Contains(result.ErrorDescription, SendAccessConstants.SendIdGuidValidatorResults.InvalidSendId);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ValidateRequestAsync_EmptyHashKey_UsesEmptyKey(
|
||||
SutProvider<SendNeverAuthenticateRequestValidator> sutProvider,
|
||||
[AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest)
|
||||
{
|
||||
// Arrange
|
||||
tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(_invalidSendGuid);
|
||||
var context = new ExtensionGrantValidationContext
|
||||
{
|
||||
Request = tokenRequest
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<Core.Settings.GlobalSettings>().SendDefaultHashKey = "";
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ValidateRequestAsync(context, _authMethod, _invalidSendGuid);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsError);
|
||||
Assert.Equal(OidcConstants.TokenErrors.InvalidGrant, result.Error);
|
||||
Assert.Contains(result.ErrorDescription, SendAccessConstants.SendIdGuidValidatorResults.InvalidSendId);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ValidateRequestAsync_ConsistentBehavior_SameSendIdSameResult(
|
||||
SutProvider<SendNeverAuthenticateRequestValidator> sutProvider,
|
||||
[AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
|
||||
Guid sendId)
|
||||
{
|
||||
// Arrange
|
||||
tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(sendId);
|
||||
var context = new ExtensionGrantValidationContext
|
||||
{
|
||||
Request = tokenRequest
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<Core.Settings.GlobalSettings>().SendDefaultHashKey = "consistent-test-key-123456789012345678901234567890";
|
||||
|
||||
// Act
|
||||
var result1 = await sutProvider.Sut.ValidateRequestAsync(context, _authMethod, sendId);
|
||||
var result2 = await sutProvider.Sut.ValidateRequestAsync(context, _authMethod, sendId);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(result1.ErrorDescription, result2.ErrorDescription);
|
||||
Assert.Equal(result1.Error, result2.Error);
|
||||
|
||||
var customResponse1 = result1.CustomResponse as Dictionary<string, object>;
|
||||
var customResponse2 = result2.CustomResponse as Dictionary<string, object>;
|
||||
Assert.Equal(customResponse1[SendAccessConstants.SendAccessError], customResponse2[SendAccessConstants.SendAccessError]);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ValidateRequestAsync_DifferentSendIds_CanReturnDifferentResults(
|
||||
SutProvider<SendNeverAuthenticateRequestValidator> sutProvider,
|
||||
[AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
|
||||
Guid sendId1,
|
||||
Guid sendId2)
|
||||
{
|
||||
// Arrange
|
||||
tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(sendId1);
|
||||
var context = new ExtensionGrantValidationContext
|
||||
{
|
||||
Request = tokenRequest
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<Core.Settings.GlobalSettings>().SendDefaultHashKey = "different-test-key-123456789012345678901234567890";
|
||||
|
||||
// Act
|
||||
var result1 = await sutProvider.Sut.ValidateRequestAsync(context, _authMethod, sendId1);
|
||||
var result2 = await sutProvider.Sut.ValidateRequestAsync(context, _authMethod, sendId2);
|
||||
|
||||
// Assert - Both should be errors
|
||||
Assert.True(result1.IsError);
|
||||
Assert.True(result2.IsError);
|
||||
|
||||
// Both should have valid error types
|
||||
var validErrors = new[]
|
||||
{
|
||||
SendAccessConstants.SendIdGuidValidatorResults.InvalidSendId,
|
||||
SendAccessConstants.EmailOtpValidatorResults.EmailRequired,
|
||||
SendAccessConstants.PasswordValidatorResults.RequestPasswordIsRequired
|
||||
};
|
||||
Assert.Contains(result1.ErrorDescription, validErrors);
|
||||
Assert.Contains(result2.ErrorDescription, validErrors);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithValidGlobalSettings_CreatesInstance()
|
||||
{
|
||||
// Arrange
|
||||
var globalSettings = new Core.Settings.GlobalSettings
|
||||
{
|
||||
SendDefaultHashKey = "test-key-123456789012345678901234567890"
|
||||
};
|
||||
|
||||
// Act
|
||||
var validator = new SendNeverAuthenticateRequestValidator(globalSettings);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(validator);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,7 @@
|
||||
using System.Collections.Specialized;
|
||||
using Bit.Core.Auth.Identity;
|
||||
using Bit.Core.Auth.IdentityServer;
|
||||
using Bit.Core.Auth.Identity;
|
||||
using Bit.Core.Auth.UserFeatures.SendAccess;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.KeyManagement.Sends;
|
||||
using Bit.Core.Tools.Models.Data;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Identity.IdentityServer.Enums;
|
||||
using Bit.Identity.IdentityServer.RequestValidators.SendAccess;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
@@ -28,7 +23,7 @@ public class SendPasswordRequestValidatorTests
|
||||
Guid sendId)
|
||||
{
|
||||
// Arrange
|
||||
tokenRequest.Raw = CreateValidatedTokenRequest(sendId);
|
||||
tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(sendId);
|
||||
|
||||
var context = new ExtensionGrantValidationContext
|
||||
{
|
||||
@@ -58,7 +53,7 @@ public class SendPasswordRequestValidatorTests
|
||||
string clientPasswordHash)
|
||||
{
|
||||
// Arrange
|
||||
tokenRequest.Raw = CreateValidatedTokenRequest(sendId, clientPasswordHash);
|
||||
tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(sendId, passwordHash: clientPasswordHash);
|
||||
|
||||
var context = new ExtensionGrantValidationContext
|
||||
{
|
||||
@@ -92,7 +87,7 @@ public class SendPasswordRequestValidatorTests
|
||||
string clientPasswordHash)
|
||||
{
|
||||
// Arrange
|
||||
tokenRequest.Raw = CreateValidatedTokenRequest(sendId, clientPasswordHash);
|
||||
tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(sendId, passwordHash: clientPasswordHash);
|
||||
|
||||
var context = new ExtensionGrantValidationContext
|
||||
{
|
||||
@@ -130,7 +125,7 @@ public class SendPasswordRequestValidatorTests
|
||||
Guid sendId)
|
||||
{
|
||||
// Arrange
|
||||
tokenRequest.Raw = CreateValidatedTokenRequest(sendId, string.Empty);
|
||||
tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(sendId, passwordHash: string.Empty);
|
||||
|
||||
var context = new ExtensionGrantValidationContext
|
||||
{
|
||||
@@ -163,7 +158,7 @@ public class SendPasswordRequestValidatorTests
|
||||
{
|
||||
// Arrange
|
||||
var whitespacePassword = " ";
|
||||
tokenRequest.Raw = CreateValidatedTokenRequest(sendId, whitespacePassword);
|
||||
tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(sendId, passwordHash: whitespacePassword);
|
||||
|
||||
var context = new ExtensionGrantValidationContext
|
||||
{
|
||||
@@ -196,7 +191,7 @@ public class SendPasswordRequestValidatorTests
|
||||
// Arrange
|
||||
var firstPassword = "first-password";
|
||||
var secondPassword = "second-password";
|
||||
tokenRequest.Raw = CreateValidatedTokenRequest(sendId, firstPassword, secondPassword);
|
||||
tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(sendId, passwordHash: [firstPassword, secondPassword]);
|
||||
|
||||
var context = new ExtensionGrantValidationContext
|
||||
{
|
||||
@@ -229,7 +224,7 @@ public class SendPasswordRequestValidatorTests
|
||||
string clientPasswordHash)
|
||||
{
|
||||
// Arrange
|
||||
tokenRequest.Raw = CreateValidatedTokenRequest(sendId, clientPasswordHash);
|
||||
tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(sendId, passwordHash: clientPasswordHash);
|
||||
|
||||
var context = new ExtensionGrantValidationContext
|
||||
{
|
||||
@@ -268,30 +263,4 @@ public class SendPasswordRequestValidatorTests
|
||||
// Assert
|
||||
Assert.NotNull(validator);
|
||||
}
|
||||
|
||||
private static NameValueCollection CreateValidatedTokenRequest(
|
||||
Guid sendId,
|
||||
params string[] passwordHash)
|
||||
{
|
||||
var sendIdBase64 = CoreHelpers.Base64UrlEncode(sendId.ToByteArray());
|
||||
|
||||
var rawRequestParameters = new NameValueCollection
|
||||
{
|
||||
{ OidcConstants.TokenRequest.GrantType, CustomGrantTypes.SendAccess },
|
||||
{ OidcConstants.TokenRequest.ClientId, BitwardenClient.Send },
|
||||
{ OidcConstants.TokenRequest.Scope, ApiScopes.ApiSendAccess },
|
||||
{ "device_type", ((int)DeviceType.FirefoxBrowser).ToString() },
|
||||
{ SendAccessConstants.TokenRequest.SendId, sendIdBase64 }
|
||||
};
|
||||
|
||||
if (passwordHash != null && passwordHash.Length > 0)
|
||||
{
|
||||
foreach (var hash in passwordHash)
|
||||
{
|
||||
rawRequestParameters.Add(SendAccessConstants.TokenRequest.ClientB64HashedPassword, hash);
|
||||
}
|
||||
}
|
||||
|
||||
return rawRequestParameters;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Models.Data;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Identity.IdentityServer;
|
||||
using Bit.Identity.Test.AutoFixture;
|
||||
using Bit.Identity.Utilities;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
using User = Bit.Core.Entities.User;
|
||||
|
||||
namespace Bit.Identity.Test.IdentityServer;
|
||||
|
||||
@@ -20,6 +25,7 @@ public class UserDecryptionOptionsBuilderTests
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
private readonly ILoginApprovingClientTypes _loginApprovingClientTypes;
|
||||
private readonly UserDecryptionOptionsBuilder _builder;
|
||||
private readonly IFeatureService _featureService;
|
||||
|
||||
public UserDecryptionOptionsBuilderTests()
|
||||
{
|
||||
@@ -27,7 +33,8 @@ public class UserDecryptionOptionsBuilderTests
|
||||
_deviceRepository = Substitute.For<IDeviceRepository>();
|
||||
_organizationUserRepository = Substitute.For<IOrganizationUserRepository>();
|
||||
_loginApprovingClientTypes = Substitute.For<ILoginApprovingClientTypes>();
|
||||
_builder = new UserDecryptionOptionsBuilder(_currentContext, _deviceRepository, _organizationUserRepository, _loginApprovingClientTypes);
|
||||
_featureService = Substitute.For<IFeatureService>();
|
||||
_builder = new UserDecryptionOptionsBuilder(_currentContext, _deviceRepository, _organizationUserRepository, _loginApprovingClientTypes, _featureService);
|
||||
var user = new User();
|
||||
_builder.ForUser(user);
|
||||
}
|
||||
@@ -220,19 +227,65 @@ public class UserDecryptionOptionsBuilderTests
|
||||
Assert.False(result.TrustedDeviceOption?.HasLoginApprovingDevice);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
/// <summary>
|
||||
/// This logic has been flagged as part of PM-23174.
|
||||
/// When removing the server flag, please also remove this test, and remove the FeatureService
|
||||
/// dependency from this suite and the following test.
|
||||
/// </summary>
|
||||
/// <param name="organizationUserType"></param>
|
||||
/// <param name="ssoConfig"></param>
|
||||
/// <param name="configurationData"></param>
|
||||
/// <param name="organization"></param>
|
||||
/// <param name="organizationUser"></param>
|
||||
/// <param name="user"></param>
|
||||
[Theory]
|
||||
[BitAutoData(OrganizationUserType.Custom)]
|
||||
public async Task Build_WhenManageResetPasswordPermissions_ShouldReturnHasManageResetPasswordPermissionTrue(
|
||||
OrganizationUserType organizationUserType,
|
||||
SsoConfig ssoConfig,
|
||||
SsoConfigurationData configurationData,
|
||||
CurrentContextOrganization organization)
|
||||
CurrentContextOrganization organization,
|
||||
[OrganizationUserWithDefaultPermissions] OrganizationUser organizationUser,
|
||||
User user)
|
||||
{
|
||||
configurationData.MemberDecryptionType = MemberDecryptionType.TrustedDeviceEncryption;
|
||||
ssoConfig.Data = configurationData.Serialize();
|
||||
ssoConfig.OrganizationId = organization.Id;
|
||||
_currentContext.Organizations.Returns(new List<CurrentContextOrganization>(new CurrentContextOrganization[] { organization }));
|
||||
_currentContext.Organizations.Returns([organization]);
|
||||
_currentContext.ManageResetPassword(organization.Id).Returns(true);
|
||||
organizationUser.Type = organizationUserType;
|
||||
organizationUser.OrganizationId = organization.Id;
|
||||
organizationUser.UserId = user.Id;
|
||||
organizationUser.SetPermissions(new Permissions() { ManageResetPassword = true });
|
||||
_organizationUserRepository.GetByOrganizationAsync(ssoConfig.OrganizationId, user.Id).Returns(organizationUser);
|
||||
|
||||
var result = await _builder.WithSso(ssoConfig).BuildAsync();
|
||||
var result = await _builder.ForUser(user).WithSso(ssoConfig).BuildAsync();
|
||||
|
||||
Assert.True(result.TrustedDeviceOption?.HasManageResetPasswordPermission);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(OrganizationUserType.Custom)]
|
||||
public async Task Build_WhenManageResetPasswordPermissions_ShouldFetchUserFromRepositoryAndReturnHasManageResetPasswordPermissionTrue(
|
||||
OrganizationUserType organizationUserType,
|
||||
SsoConfig ssoConfig,
|
||||
SsoConfigurationData configurationData,
|
||||
CurrentContextOrganization organization,
|
||||
[OrganizationUserWithDefaultPermissions] OrganizationUser organizationUser,
|
||||
User user)
|
||||
{
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM23174ManageAccountRecoveryPermissionDrivesTheNeedToSetMasterPassword)
|
||||
.Returns(true);
|
||||
configurationData.MemberDecryptionType = MemberDecryptionType.TrustedDeviceEncryption;
|
||||
ssoConfig.Data = configurationData.Serialize();
|
||||
ssoConfig.OrganizationId = organization.Id;
|
||||
organizationUser.Type = organizationUserType;
|
||||
organizationUser.OrganizationId = organization.Id;
|
||||
organizationUser.UserId = user.Id;
|
||||
organizationUser.SetPermissions(new Permissions() { ManageResetPassword = true });
|
||||
_organizationUserRepository.GetByOrganizationAsync(ssoConfig.OrganizationId, user.Id).Returns(organizationUser);
|
||||
|
||||
var result = await _builder.ForUser(user).WithSso(ssoConfig).BuildAsync();
|
||||
|
||||
Assert.True(result.TrustedDeviceOption?.HasManageResetPasswordPermission);
|
||||
}
|
||||
@@ -241,7 +294,7 @@ public class UserDecryptionOptionsBuilderTests
|
||||
public async Task Build_WhenIsOwnerInvite_ShouldReturnHasManageResetPasswordPermissionTrue(
|
||||
SsoConfig ssoConfig,
|
||||
SsoConfigurationData configurationData,
|
||||
OrganizationUser organizationUser,
|
||||
[OrganizationUserWithDefaultPermissions] OrganizationUser organizationUser,
|
||||
User user)
|
||||
{
|
||||
configurationData.MemberDecryptionType = MemberDecryptionType.TrustedDeviceEncryption;
|
||||
@@ -258,7 +311,7 @@ public class UserDecryptionOptionsBuilderTests
|
||||
public async Task Build_WhenIsAdminInvite_ShouldReturnHasManageResetPasswordPermissionTrue(
|
||||
SsoConfig ssoConfig,
|
||||
SsoConfigurationData configurationData,
|
||||
OrganizationUser organizationUser,
|
||||
[OrganizationUserWithDefaultPermissions] OrganizationUser organizationUser,
|
||||
User user)
|
||||
{
|
||||
configurationData.MemberDecryptionType = MemberDecryptionType.TrustedDeviceEncryption;
|
||||
@@ -275,7 +328,7 @@ public class UserDecryptionOptionsBuilderTests
|
||||
public async Task Build_WhenUserHasEnrolledIntoPasswordReset_ShouldReturnHasAdminApprovalTrue(
|
||||
SsoConfig ssoConfig,
|
||||
SsoConfigurationData configurationData,
|
||||
OrganizationUser organizationUser,
|
||||
[OrganizationUserWithDefaultPermissions] OrganizationUser organizationUser,
|
||||
User user)
|
||||
{
|
||||
configurationData.MemberDecryptionType = MemberDecryptionType.TrustedDeviceEncryption;
|
||||
|
||||
@@ -1241,4 +1241,164 @@ public class CipherRepositoryTests
|
||||
Assert.NotNull(archivedCipher);
|
||||
Assert.NotNull(archivedCipher.ArchivedDate);
|
||||
}
|
||||
|
||||
[DatabaseTheory, DatabaseData]
|
||||
public async Task DeleteByOrganizationIdAsync_ExcludesDefaultCollectionCiphers(
|
||||
IOrganizationRepository organizationRepository,
|
||||
IUserRepository userRepository,
|
||||
ICipherRepository cipherRepository,
|
||||
ICollectionRepository collectionRepository,
|
||||
ICollectionCipherRepository collectionCipherRepository)
|
||||
{
|
||||
var user = await userRepository.CreateAsync(new User
|
||||
{
|
||||
Name = "Test User",
|
||||
Email = $"test+{Guid.NewGuid()}@email.com",
|
||||
ApiKey = "TEST",
|
||||
SecurityStamp = "stamp",
|
||||
});
|
||||
|
||||
var organization = await organizationRepository.CreateAsync(new Organization
|
||||
{
|
||||
Name = "Test Organization",
|
||||
BillingEmail = user.Email,
|
||||
Plan = "Test"
|
||||
});
|
||||
|
||||
var defaultCollection = await collectionRepository.CreateAsync(new Collection
|
||||
{
|
||||
Name = "Default Collection",
|
||||
OrganizationId = organization.Id,
|
||||
Type = CollectionType.DefaultUserCollection
|
||||
});
|
||||
|
||||
var sharedCollection = await collectionRepository.CreateAsync(new Collection
|
||||
{
|
||||
Name = "Shared Collection",
|
||||
OrganizationId = organization.Id,
|
||||
});
|
||||
|
||||
async Task<Cipher> CreateOrgCipherAsync() => await cipherRepository.CreateAsync(new Cipher
|
||||
{
|
||||
Type = CipherType.Login,
|
||||
OrganizationId = organization.Id,
|
||||
Data = ""
|
||||
});
|
||||
|
||||
var cipherInDefaultCollection = await CreateOrgCipherAsync();
|
||||
var cipherInSharedCollection = await CreateOrgCipherAsync();
|
||||
var cipherInBothCollections = await CreateOrgCipherAsync();
|
||||
var unassignedCipher = await CreateOrgCipherAsync();
|
||||
|
||||
await collectionCipherRepository.UpdateCollectionsForAdminAsync(cipherInDefaultCollection.Id, organization.Id,
|
||||
new List<Guid> { defaultCollection.Id });
|
||||
await collectionCipherRepository.UpdateCollectionsForAdminAsync(cipherInSharedCollection.Id, organization.Id,
|
||||
new List<Guid> { sharedCollection.Id });
|
||||
await collectionCipherRepository.UpdateCollectionsForAdminAsync(cipherInBothCollections.Id, organization.Id,
|
||||
new List<Guid> { defaultCollection.Id, sharedCollection.Id });
|
||||
|
||||
await cipherRepository.DeleteByOrganizationIdAsync(organization.Id);
|
||||
|
||||
var remainingCipherInDefault = await cipherRepository.GetByIdAsync(cipherInDefaultCollection.Id);
|
||||
var deletedCipherInShared = await cipherRepository.GetByIdAsync(cipherInSharedCollection.Id);
|
||||
var remainingCipherInBoth = await cipherRepository.GetByIdAsync(cipherInBothCollections.Id);
|
||||
var deletedUnassignedCipher = await cipherRepository.GetByIdAsync(unassignedCipher.Id);
|
||||
|
||||
Assert.Null(deletedCipherInShared);
|
||||
Assert.Null(deletedUnassignedCipher);
|
||||
|
||||
Assert.NotNull(remainingCipherInDefault);
|
||||
Assert.NotNull(remainingCipherInBoth);
|
||||
|
||||
var remainingCollectionCiphers = await collectionCipherRepository.GetManyByOrganizationIdAsync(organization.Id);
|
||||
|
||||
// Should still have the default collection cipher relationships
|
||||
Assert.Contains(remainingCollectionCiphers, cc =>
|
||||
cc.CipherId == cipherInDefaultCollection.Id && cc.CollectionId == defaultCollection.Id);
|
||||
Assert.Contains(remainingCollectionCiphers, cc =>
|
||||
cc.CipherId == cipherInBothCollections.Id && cc.CollectionId == defaultCollection.Id);
|
||||
|
||||
// Should not have the shared collection cipher relationships
|
||||
Assert.DoesNotContain(remainingCollectionCiphers, cc => cc.CollectionId == sharedCollection.Id);
|
||||
}
|
||||
|
||||
[DatabaseTheory, DatabaseData]
|
||||
public async Task DeleteByOrganizationIdAsync_DeletesAllWhenNoDefaultCollections(
|
||||
IOrganizationRepository organizationRepository,
|
||||
IUserRepository userRepository,
|
||||
ICipherRepository cipherRepository,
|
||||
ICollectionRepository collectionRepository,
|
||||
ICollectionCipherRepository collectionCipherRepository)
|
||||
{
|
||||
// Arrange
|
||||
var user = await userRepository.CreateAsync(new User
|
||||
{
|
||||
Name = "Test User",
|
||||
Email = $"test+{Guid.NewGuid()}@email.com",
|
||||
ApiKey = "TEST",
|
||||
SecurityStamp = "stamp",
|
||||
});
|
||||
|
||||
var organization = await organizationRepository.CreateAsync(new Organization
|
||||
{
|
||||
Name = "Test Organization",
|
||||
BillingEmail = user.Email,
|
||||
Plan = "Test"
|
||||
});
|
||||
|
||||
var sharedCollection1 = await collectionRepository.CreateAsync(new Collection
|
||||
{
|
||||
Name = "Shared Collection 1",
|
||||
OrganizationId = organization.Id,
|
||||
Type = CollectionType.SharedCollection
|
||||
});
|
||||
|
||||
var sharedCollection2 = await collectionRepository.CreateAsync(new Collection
|
||||
{
|
||||
Name = "Shared Collection 2",
|
||||
OrganizationId = organization.Id,
|
||||
Type = CollectionType.SharedCollection
|
||||
});
|
||||
|
||||
// Create ciphers
|
||||
var cipherInSharedCollection1 = await cipherRepository.CreateAsync(new Cipher
|
||||
{
|
||||
Type = CipherType.Login,
|
||||
OrganizationId = organization.Id,
|
||||
Data = ""
|
||||
});
|
||||
|
||||
var cipherInSharedCollection2 = await cipherRepository.CreateAsync(new Cipher
|
||||
{
|
||||
Type = CipherType.Login,
|
||||
OrganizationId = organization.Id,
|
||||
Data = ""
|
||||
});
|
||||
|
||||
var unassignedCipher = await cipherRepository.CreateAsync(new Cipher
|
||||
{
|
||||
Type = CipherType.Login,
|
||||
OrganizationId = organization.Id,
|
||||
Data = ""
|
||||
});
|
||||
|
||||
await collectionCipherRepository.UpdateCollectionsForAdminAsync(cipherInSharedCollection1.Id, organization.Id,
|
||||
new List<Guid> { sharedCollection1.Id });
|
||||
await collectionCipherRepository.UpdateCollectionsForAdminAsync(cipherInSharedCollection2.Id, organization.Id,
|
||||
new List<Guid> { sharedCollection2.Id });
|
||||
|
||||
await cipherRepository.DeleteByOrganizationIdAsync(organization.Id);
|
||||
|
||||
var deletedCipher1 = await cipherRepository.GetByIdAsync(cipherInSharedCollection1.Id);
|
||||
var deletedCipher2 = await cipherRepository.GetByIdAsync(cipherInSharedCollection2.Id);
|
||||
var deletedUnassignedCipher = await cipherRepository.GetByIdAsync(unassignedCipher.Id);
|
||||
|
||||
Assert.Null(deletedCipher1);
|
||||
Assert.Null(deletedCipher2);
|
||||
Assert.Null(deletedUnassignedCipher);
|
||||
|
||||
// All collection cipher relationships should be removed
|
||||
var remainingCollectionCiphers = await collectionCipherRepository.GetManyByOrganizationIdAsync(organization.Id);
|
||||
Assert.Empty(remainingCollectionCiphers);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user