1
0
mirror of https://github.com/bitwarden/server synced 2026-02-25 17:03:22 +00:00
Files
server/test/Api.Test/KeyManagement/Validators/OrganizationUserRotationValidatorTests.cs
Rui Tomé ef4f4e352f [PM-21179] Add interface to check if user is enrolled in account recovery (#6993)
* Add validation for reset password key and account recovery enrollment in OrganizationUser

* Update admin approval logic to check account recovery enrollment and add tests for reset password key validation

* Enhance UserService validation to include account recovery enrollment and add unit test for empty or whitespace reset password key handling

* Refactor OrganizationUserUserDetailsQuery to validate reset password keys and add unit tests for filtering out invalid keys

* Update AdminRecoverAccountCommand to validate account recovery enrollment and adjust tests for whitespace reset password keys

* Enhance OrganizationUserRotationValidator to validate reset password keys, including filtering out whitespace-only keys, and add corresponding unit tests for validation logic.

* Refactor OrganizationUserUserDetailsQueryTests to remove unnecessary whitespace-only test cases for account recovery key validation.

* Refactor MemberResponseModel to use OrganizationUser's validation method for ResetPasswordEnrolled status and update corresponding unit test for clarity.

* Refactor OrganizationUsersController and response models to utilize OrganizationUser's validation method for ResetPasswordKey, ensuring consistent validation across the application. Add unit tests for OrganizationUser to verify key validation logic.

* Update OrganizationUserRotationValidator to handle null reset password keys and adjust tests for client-side bug. Add comments for future migration after resolving PM-31001.

* Fix whitespace issue in UserServiceTests.cs by removing BOM character from the file header.
2026-02-24 14:16:54 +00:00

276 lines
10 KiB
C#

using Bit.Api.AdminConsole.Models.Request.Organizations;
using Bit.Api.KeyManagement.Validators;
using Bit.Core.Entities;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
namespace Bit.Api.Test.KeyManagement.Validators;
[SutProviderCustomize]
public class OrganizationUserRotationValidatorTests
{
[Theory]
[BitAutoData]
public async Task ValidateAsync_Success_ReturnsValid(
SutProvider<OrganizationUserRotationValidator> sutProvider, User user,
IEnumerable<ResetPasswordWithOrgIdRequestModel> resetPasswordKeys)
{
var existingUserResetPassword = resetPasswordKeys
.Select(a =>
new OrganizationUser
{
Id = new Guid(),
ResetPasswordKey = a.ResetPasswordKey,
OrganizationId = a.OrganizationId
}).ToList();
sutProvider.GetDependency<IOrganizationUserRepository>().GetManyByUserAsync(user.Id)
.Returns(existingUserResetPassword);
var result = await sutProvider.Sut.ValidateAsync(user, resetPasswordKeys);
Assert.Equal(result.Select(r => r.OrganizationId), resetPasswordKeys.Select(a => a.OrganizationId));
}
[Theory]
[BitAutoData]
public async Task ValidateAsync_NullResetPasswordKeys_ReturnsEmptyList(
SutProvider<OrganizationUserRotationValidator> sutProvider, User user)
{
// Arrange
IEnumerable<ResetPasswordWithOrgIdRequestModel> resetPasswordKeys = null;
// Act
var result = await sutProvider.Sut.ValidateAsync(user, resetPasswordKeys);
// Assert
Assert.NotNull(result);
Assert.Empty(result);
}
[Theory]
[BitAutoData]
public async Task ValidateAsync_NoOrgUsers_ReturnsEmptyList(
SutProvider<OrganizationUserRotationValidator> sutProvider, User user,
IEnumerable<ResetPasswordWithOrgIdRequestModel> resetPasswordKeys)
{
// Arrange
sutProvider.GetDependency<IOrganizationUserRepository>().GetManyByUserAsync(user.Id)
.Returns(new List<OrganizationUser>()); // Return an empty list
// Act
var result = await sutProvider.Sut.ValidateAsync(user, resetPasswordKeys);
// Assert
Assert.NotNull(result);
Assert.Empty(result);
}
[Theory]
[BitAutoData([null])]
[BitAutoData("")]
public async Task ValidateAsync_OrgUsersWithNullOrEmptyResetPasswordKey_FiltersOutInvalidKeys(
string? invalidResetPasswordKey,
SutProvider<OrganizationUserRotationValidator> sutProvider, User user,
ResetPasswordWithOrgIdRequestModel validResetPasswordKey)
{
// Arrange
var existingUserResetPassword = new List<OrganizationUser>
{
// Valid org user with reset password key
new OrganizationUser
{
Id = Guid.NewGuid(),
OrganizationId = validResetPasswordKey.OrganizationId,
ResetPasswordKey = validResetPasswordKey.ResetPasswordKey
},
// Invalid org user with null or empty reset password key - should be filtered out
new OrganizationUser
{
Id = Guid.NewGuid(),
OrganizationId = Guid.NewGuid(),
ResetPasswordKey = invalidResetPasswordKey
}
};
sutProvider.GetDependency<IOrganizationUserRepository>().GetManyByUserAsync(user.Id)
.Returns(existingUserResetPassword);
// Act
var result = await sutProvider.Sut.ValidateAsync(user, new[] { validResetPasswordKey });
// Assert
Assert.NotNull(result);
Assert.Single(result);
Assert.Equal(validResetPasswordKey.OrganizationId, result[0].OrganizationId);
}
[Theory]
[BitAutoData]
public async Task ValidateAsync_MissingResetPassword_Throws(
SutProvider<OrganizationUserRotationValidator> sutProvider, User user,
IEnumerable<ResetPasswordWithOrgIdRequestModel> resetPasswordKeys)
{
var existingUserResetPassword = resetPasswordKeys
.Select(a =>
new OrganizationUser
{
Id = new Guid(),
ResetPasswordKey = a.ResetPasswordKey,
OrganizationId = a.OrganizationId
}).ToList();
existingUserResetPassword.Add(new OrganizationUser
{
Id = Guid.NewGuid(),
ResetPasswordKey = "Missing ResetPasswordKey"
});
sutProvider.GetDependency<IOrganizationUserRepository>().GetManyByUserAsync(user.Id)
.Returns(existingUserResetPassword);
await Assert.ThrowsAsync<BadRequestException>(async () =>
await sutProvider.Sut.ValidateAsync(user, resetPasswordKeys));
}
[Theory]
[BitAutoData]
public async Task ValidateAsync_ResetPasswordDoesNotBelongToUser_NotReturned(
SutProvider<OrganizationUserRotationValidator> sutProvider, User user,
IEnumerable<ResetPasswordWithOrgIdRequestModel> resetPasswordKeys)
{
var existingUserResetPassword = resetPasswordKeys
.Select(a =>
new OrganizationUser
{
Id = new Guid(),
ResetPasswordKey = a.ResetPasswordKey,
OrganizationId = a.OrganizationId
}).ToList();
existingUserResetPassword.RemoveAt(0);
sutProvider.GetDependency<IOrganizationUserRepository>().GetManyByUserAsync(user.Id)
.Returns(existingUserResetPassword);
var result = await sutProvider.Sut.ValidateAsync(user, resetPasswordKeys);
Assert.DoesNotContain(result, c => c.Id == resetPasswordKeys.First().OrganizationId);
}
[Theory]
[BitAutoData]
public async Task ValidateAsync_AttemptToSetKeyToNull_Throws(
SutProvider<OrganizationUserRotationValidator> sutProvider, User user,
IEnumerable<ResetPasswordWithOrgIdRequestModel> resetPasswordKeys)
{
var existingUserResetPassword = resetPasswordKeys
.Select(a =>
new OrganizationUser
{
Id = new Guid(),
ResetPasswordKey = a.ResetPasswordKey,
OrganizationId = a.OrganizationId
}).ToList();
sutProvider.GetDependency<IOrganizationUserRepository>().GetManyByUserAsync(user.Id)
.Returns(existingUserResetPassword);
resetPasswordKeys.First().ResetPasswordKey = null;
await Assert.ThrowsAsync<BadRequestException>(async () =>
await sutProvider.Sut.ValidateAsync(user, resetPasswordKeys));
}
[Theory]
[BitAutoData]
public async Task ValidateAsync_NoOrganizationsInRequestButInDatabase_Throws(
SutProvider<OrganizationUserRotationValidator> sutProvider, User user,
IEnumerable<ResetPasswordWithOrgIdRequestModel> resetPasswordKeys)
{
var existingUserResetPassword = resetPasswordKeys
.Select(a =>
new OrganizationUser
{
Id = new Guid(),
ResetPasswordKey = a.ResetPasswordKey,
OrganizationId = a.OrganizationId
}).ToList();
sutProvider.GetDependency<IOrganizationUserRepository>().GetManyByUserAsync(user.Id)
.Returns(existingUserResetPassword);
await Assert.ThrowsAsync<BadRequestException>(async () =>
await sutProvider.Sut.ValidateAsync(user, Enumerable.Empty<ResetPasswordWithOrgIdRequestModel>()));
}
// TODO: Remove this test after https://bitwarden.atlassian.net/browse/PM-31001 is resolved.
// Clients currently send "" as a reset password key value during rotation due to a client-side bug.
// The server must accept "" to avoid blocking key rotation for affected users.
// After PM-31001 is fixed, this should be replaced with a test asserting that "" throws BadRequestException.
[Theory]
[BitAutoData("")]
[BitAutoData(" ")]
public async Task ValidateAsync_EmptyOrWhitespaceKey_AcceptedDueToClientBug(
string emptyKey,
SutProvider<OrganizationUserRotationValidator> sutProvider, User user,
ResetPasswordWithOrgIdRequestModel validResetPasswordKey)
{
// Arrange
var existingUserResetPassword = new List<OrganizationUser>
{
new OrganizationUser
{
Id = Guid.NewGuid(),
OrganizationId = validResetPasswordKey.OrganizationId,
ResetPasswordKey = "existing-valid-key"
}
};
sutProvider.GetDependency<IOrganizationUserRepository>().GetManyByUserAsync(user.Id)
.Returns(existingUserResetPassword);
// Set the incoming key to empty/whitespace (simulating client bug)
validResetPasswordKey.ResetPasswordKey = emptyKey;
// Act — rotation should succeed (not throw) to preserve backward compatibility
var result = await sutProvider.Sut.ValidateAsync(user, new[] { validResetPasswordKey });
// Assert
Assert.NotNull(result);
Assert.Single(result);
Assert.Equal(emptyKey, result[0].ResetPasswordKey);
}
[Theory]
[BitAutoData(" ")]
public async Task ValidateAsync_WhitespaceOnlyExistingKey_FiltersOut(
string whitespaceKey,
SutProvider<OrganizationUserRotationValidator> sutProvider, User user,
ResetPasswordWithOrgIdRequestModel validResetPasswordKey)
{
// Arrange
var existingUserResetPassword = new List<OrganizationUser>
{
new OrganizationUser
{
Id = Guid.NewGuid(),
OrganizationId = validResetPasswordKey.OrganizationId,
ResetPasswordKey = validResetPasswordKey.ResetPasswordKey
},
// Whitespace-only key should be filtered out
new OrganizationUser
{
Id = Guid.NewGuid(),
OrganizationId = Guid.NewGuid(),
ResetPasswordKey = whitespaceKey
}
};
sutProvider.GetDependency<IOrganizationUserRepository>().GetManyByUserAsync(user.Id)
.Returns(existingUserResetPassword);
// Act
var result = await sutProvider.Sut.ValidateAsync(user, new[] { validResetPasswordKey });
// Assert
Assert.NotNull(result);
Assert.Single(result);
Assert.Equal(validResetPasswordKey.OrganizationId, result[0].OrganizationId);
}
}