1
0
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:
Daniel James Smith
2025-09-26 12:16:17 +02:00
committed by GitHub
112 changed files with 3404 additions and 1730 deletions

View File

@@ -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);

View File

@@ -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]

View File

@@ -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

View File

@@ -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>()

View File

@@ -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);

View File

@@ -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;
}
}

View 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());
}
}

View File

@@ -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);

View File

@@ -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);

View File

@@ -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)

View File

@@ -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));
}
}

View File

@@ -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);
}
}

View 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);
}
}

View 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
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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>
{

View 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();
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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]

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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;

View File

@@ -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);
}
}