mirror of
https://github.com/bitwarden/server
synced 2026-02-25 00:52:57 +00:00
[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.
This commit is contained in:
@@ -28,6 +28,7 @@ using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Repositories;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Api;
|
||||
@@ -334,7 +335,7 @@ public class OrganizationUsersController : BaseAdminConsoleController
|
||||
? (await _policyRequirementQuery.GetAsync<ResetPasswordPolicyRequirement>(user.Id)).AutoEnrollEnabled(orgId)
|
||||
: await ShouldHandleResetPasswordAsync(orgId);
|
||||
|
||||
if (useMasterPasswordPolicy && string.IsNullOrWhiteSpace(model.ResetPasswordKey))
|
||||
if (useMasterPasswordPolicy && !OrganizationUser.IsValidResetPasswordKey(model.ResetPasswordKey))
|
||||
{
|
||||
throw new BadRequestException("Master Password reset is required, but not provided.");
|
||||
}
|
||||
@@ -487,7 +488,7 @@ public class OrganizationUsersController : BaseAdminConsoleController
|
||||
|
||||
var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(orgId);
|
||||
var isTdeEnrollment = ssoConfig != null && ssoConfig.Enabled && ssoConfig.GetData().MemberDecryptionType == MemberDecryptionType.TrustedDeviceEncryption;
|
||||
if (!isTdeEnrollment && !string.IsNullOrWhiteSpace(model.ResetPasswordKey) && !await _userService.VerifySecretAsync(user, model.MasterPasswordHash))
|
||||
if (!isTdeEnrollment && OrganizationUser.IsValidResetPasswordKey(model.ResetPasswordKey) && !await _userService.VerifySecretAsync(user, model.MasterPasswordHash))
|
||||
{
|
||||
throw new BadRequestException("Incorrect password");
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Models.Data;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Api;
|
||||
using Bit.Core.Models.Data;
|
||||
@@ -57,7 +58,7 @@ public abstract class BaseProfileOrganizationResponseModel : ResponseModel
|
||||
Key = organizationDetails.Key;
|
||||
HasPublicAndPrivateKeys = organizationDetails.PublicKey != null && organizationDetails.PrivateKey != null;
|
||||
SsoBound = !string.IsNullOrWhiteSpace(organizationDetails.SsoExternalId);
|
||||
ResetPasswordEnrolled = !string.IsNullOrWhiteSpace(organizationDetails.ResetPasswordKey);
|
||||
ResetPasswordEnrolled = OrganizationUser.IsValidResetPasswordKey(organizationDetails.ResetPasswordKey);
|
||||
ProviderId = organizationDetails.ProviderId;
|
||||
ProviderName = organizationDetails.ProviderName;
|
||||
ProviderType = organizationDetails.ProviderType;
|
||||
|
||||
@@ -29,7 +29,7 @@ public class OrganizationUserResponseModel : ResponseModel
|
||||
ExternalId = organizationUser.ExternalId;
|
||||
AccessSecretsManager = organizationUser.AccessSecretsManager;
|
||||
Permissions = CoreHelpers.LoadClassFromJsonData<Permissions>(organizationUser.Permissions);
|
||||
ResetPasswordEnrolled = !string.IsNullOrEmpty(organizationUser.ResetPasswordKey);
|
||||
ResetPasswordEnrolled = OrganizationUser.IsValidResetPasswordKey(organizationUser.ResetPasswordKey);
|
||||
}
|
||||
|
||||
public OrganizationUserResponseModel(OrganizationUserUserDetails organizationUser,
|
||||
@@ -48,7 +48,7 @@ public class OrganizationUserResponseModel : ResponseModel
|
||||
ExternalId = organizationUser.ExternalId;
|
||||
AccessSecretsManager = organizationUser.AccessSecretsManager;
|
||||
Permissions = CoreHelpers.LoadClassFromJsonData<Permissions>(organizationUser.Permissions);
|
||||
ResetPasswordEnrolled = !string.IsNullOrEmpty(organizationUser.ResetPasswordKey);
|
||||
ResetPasswordEnrolled = OrganizationUser.IsValidResetPasswordKey(organizationUser.ResetPasswordKey);
|
||||
UsesKeyConnector = organizationUser.UsesKeyConnector;
|
||||
HasMasterPassword = organizationUser.HasMasterPassword;
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ public class MemberResponseModel : MemberBaseModel, IResponseModel
|
||||
Email = user.Email;
|
||||
Status = user.Status;
|
||||
Collections = collections?.Select(c => new AssociationWithPermissionsResponseModel(c));
|
||||
ResetPasswordEnrolled = !string.IsNullOrWhiteSpace(user.ResetPasswordKey);
|
||||
ResetPasswordEnrolled = OrganizationUser.IsValidResetPasswordKey(user.ResetPasswordKey);
|
||||
}
|
||||
|
||||
[SetsRequiredMembers]
|
||||
@@ -52,7 +52,7 @@ public class MemberResponseModel : MemberBaseModel, IResponseModel
|
||||
TwoFactorEnabled = twoFactorEnabled;
|
||||
Status = user.Status;
|
||||
Collections = collections?.Select(c => new AssociationWithPermissionsResponseModel(c));
|
||||
ResetPasswordEnrolled = !string.IsNullOrWhiteSpace(user.ResetPasswordKey);
|
||||
ResetPasswordEnrolled = OrganizationUser.IsValidResetPasswordKey(user.ResetPasswordKey);
|
||||
SsoExternalId = user.SsoExternalId;
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ public class OrganizationUserRotationValidator : IRotationValidator<IEnumerable<
|
||||
}
|
||||
|
||||
// Exclude any account recovery that do not have a key.
|
||||
existing = existing.Where(o => !string.IsNullOrEmpty(o.ResetPasswordKey)).ToList();
|
||||
existing = existing.Where(o => OrganizationUser.IsValidResetPasswordKey(o.ResetPasswordKey)).ToList();
|
||||
|
||||
foreach (var ou in existing)
|
||||
{
|
||||
@@ -44,6 +44,8 @@ public class OrganizationUserRotationValidator : IRotationValidator<IEnumerable<
|
||||
throw new BadRequestException("All existing reset password keys must be included in the rotation.");
|
||||
}
|
||||
|
||||
// Should be migrated to: if (!OrganizationUser.IsValidResetPasswordKey(organizationUser.ResetPasswordKey))
|
||||
// after https://bitwarden.atlassian.net/browse/PM-31001 is resolved
|
||||
if (organizationUser.ResetPasswordKey == null)
|
||||
{
|
||||
throw new BadRequestException("Reset Password keys cannot be set to null during rotation.");
|
||||
|
||||
@@ -81,6 +81,17 @@ public class OrganizationUser : ITableObject<Guid>, IExternal, IOrganizationUser
|
||||
/// </summary>
|
||||
public bool AccessSecretsManager { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether the given reset password key is non-null and non-whitespace.
|
||||
/// </summary>
|
||||
public static bool IsValidResetPasswordKey(string? resetPasswordKey)
|
||||
=> !string.IsNullOrWhiteSpace(resetPasswordKey);
|
||||
|
||||
/// <summary>
|
||||
/// Whether this organization user is enrolled in account recovery.
|
||||
/// </summary>
|
||||
public bool IsEnrolledInAccountRecovery() => IsValidResetPasswordKey(ResetPasswordKey);
|
||||
|
||||
public void SetNewId()
|
||||
{
|
||||
Id = CoreHelpers.GenerateComb();
|
||||
|
||||
@@ -40,7 +40,7 @@ public class AdminRecoverAccountCommand(IOrganizationRepository organizationRepo
|
||||
if (organizationUser == null ||
|
||||
organizationUser.Status != OrganizationUserStatusType.Confirmed ||
|
||||
organizationUser.OrganizationId != orgId ||
|
||||
string.IsNullOrEmpty(organizationUser.ResetPasswordKey) ||
|
||||
!organizationUser.IsEnrolledInAccountRecovery() ||
|
||||
!organizationUser.UserId.HasValue)
|
||||
{
|
||||
throw new BadRequestException("Organization User not valid");
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Core.Repositories;
|
||||
@@ -96,7 +97,8 @@ public class OrganizationUserUserDetailsQuery : IOrganizationUserUserDetailsQuer
|
||||
{
|
||||
var organizationUsers = (await _organizationUserRepository
|
||||
.GetManyDetailsByOrganizationAsync_vNext(request.OrganizationId, request.IncludeGroups, request.IncludeCollections))
|
||||
.Where(o => o.Status.Equals(OrganizationUserStatusType.Confirmed) && o.UsesKeyConnector == false && !String.IsNullOrEmpty(o.ResetPasswordKey))
|
||||
.Where(o => o.Status.Equals(OrganizationUserStatusType.Confirmed) && o.UsesKeyConnector == false &&
|
||||
OrganizationUser.IsValidResetPasswordKey(o.ResetPasswordKey))
|
||||
.ToArray();
|
||||
|
||||
var twoFactorTask = _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(organizationUsers);
|
||||
|
||||
@@ -6,6 +6,7 @@ using Bit.Core.Dirt.Reports.Models.Data;
|
||||
using Bit.Core.Dirt.Reports.ReportFeatures.OrganizationReportMembers.Interfaces;
|
||||
using Bit.Core.Dirt.Reports.ReportFeatures.Requests;
|
||||
using Bit.Core.Dirt.Reports.Repositories;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Services;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
@@ -65,7 +66,7 @@ public class MemberAccessReportQuery(
|
||||
UserName = g.Key.UserName,
|
||||
Email = g.Key.Email,
|
||||
TwoFactorEnabled = orgUsersTwoFactorEnabled.FirstOrDefault(x => x.userId == g.Key.UserGuid).twoFactorIsEnabled,
|
||||
AccountRecoveryEnabled = !string.IsNullOrWhiteSpace(g.Key.ResetPasswordKey) && orgAbility.UseResetPassword,
|
||||
AccountRecoveryEnabled = OrganizationUser.IsValidResetPasswordKey(g.Key.ResetPasswordKey) && orgAbility.UseResetPassword,
|
||||
UsesKeyConnector = g.Key.UsesKeyConnector,
|
||||
GroupId = g.Key.GroupId,
|
||||
GroupName = g.Key.GroupName,
|
||||
|
||||
@@ -581,7 +581,8 @@ public class UserService : UserManager<User>, IUserService
|
||||
// Org User must be confirmed and have a ResetPasswordKey
|
||||
var orgUser = await _organizationUserRepository.GetByIdAsync(id);
|
||||
if (orgUser == null || orgUser.Status != OrganizationUserStatusType.Confirmed ||
|
||||
orgUser.OrganizationId != orgId || string.IsNullOrEmpty(orgUser.ResetPasswordKey) ||
|
||||
orgUser.OrganizationId != orgId ||
|
||||
!orgUser.IsEnrolledInAccountRecovery() ||
|
||||
!orgUser.UserId.HasValue)
|
||||
{
|
||||
throw new BadRequestException("Organization User not valid");
|
||||
|
||||
@@ -145,8 +145,8 @@ public class UserDecryptionOptionsBuilder : IUserDecryptionOptionsBuilder
|
||||
var organizationUser = await _organizationUserRepository.GetByOrganizationAsync(_ssoConfig.OrganizationId, _user.Id);
|
||||
var hasManageResetPasswordPermission = await EvaluateHasManageResetPasswordPermission();
|
||||
|
||||
// They are only able to be approved by an admin if they have enrolled is reset password
|
||||
var hasAdminApproval = organizationUser != null && !string.IsNullOrEmpty(organizationUser.ResetPasswordKey);
|
||||
// They are only able to be approved by an admin if they have enrolled in account recovery
|
||||
var hasAdminApproval = organizationUser != null && organizationUser.IsEnrolledInAccountRecovery();
|
||||
|
||||
_options.TrustedDeviceOption = new TrustedDeviceUserDecryptionOption(
|
||||
hasAdminApproval,
|
||||
|
||||
@@ -10,14 +10,13 @@ namespace Bit.Api.Test.AdminConsole.Public.Models.Response;
|
||||
public class MemberResponseModelTests
|
||||
{
|
||||
[Fact]
|
||||
public void ResetPasswordEnrolled_ShouldBeTrue_WhenUserHasResetPasswordKey()
|
||||
public void ResetPasswordEnrolled_ShouldBeTrue_WhenUserIsResetPasswordEnrolled()
|
||||
{
|
||||
// Arrange
|
||||
var user = Substitute.For<OrganizationUser>();
|
||||
var collections = Substitute.For<IEnumerable<CollectionAccessSelection>>();
|
||||
user.ResetPasswordKey = "none-empty";
|
||||
|
||||
|
||||
// Act
|
||||
var sut = new MemberResponseModel(user, collections);
|
||||
|
||||
|
||||
@@ -199,4 +199,77 @@ public class OrganizationUserRotationValidatorTests
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
using Bit.Core.Entities;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.AdminConsole.Entities;
|
||||
|
||||
public class OrganizationUserTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(null)]
|
||||
[InlineData("")]
|
||||
[InlineData(" ")]
|
||||
public void IsValidResetPasswordKey_InvalidKeys_ReturnsFalse(string? resetPasswordKey)
|
||||
{
|
||||
Assert.False(OrganizationUser.IsValidResetPasswordKey(resetPasswordKey));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsValidResetPasswordKey_ValidKey_ReturnsTrue()
|
||||
{
|
||||
Assert.True(OrganizationUser.IsValidResetPasswordKey("validKey"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsEnrolledInAccountRecovery_NullKey_ReturnsFalse()
|
||||
{
|
||||
var orgUser = new OrganizationUser { ResetPasswordKey = null };
|
||||
|
||||
Assert.False(orgUser.IsEnrolledInAccountRecovery());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsEnrolledInAccountRecovery_ValidKey_ReturnsTrue()
|
||||
{
|
||||
var orgUser = new OrganizationUser { ResetPasswordKey = "validKey" };
|
||||
|
||||
Assert.True(orgUser.IsEnrolledInAccountRecovery());
|
||||
}
|
||||
}
|
||||
@@ -150,6 +150,15 @@ public class AdminRecoverAccountCommandTests
|
||||
};
|
||||
yield return [emptyResetPasswordKey, organization];
|
||||
|
||||
var whitespaceResetPasswordKey = new OrganizationUser
|
||||
{
|
||||
Status = OrganizationUserStatusType.Confirmed,
|
||||
OrganizationId = organization.Id,
|
||||
ResetPasswordKey = " ",
|
||||
UserId = Guid.NewGuid(),
|
||||
};
|
||||
yield return [whitespaceResetPasswordKey, organization];
|
||||
|
||||
var nullUserId = new OrganizationUser
|
||||
{
|
||||
Status = OrganizationUserStatusType.Confirmed,
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Core.AdminConsole.OrganizationFeatures.OrganizationUsers;
|
||||
using Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class OrganizationUserUserDetailsQueryTests
|
||||
{
|
||||
[Theory]
|
||||
[BitAutoData(" ")]
|
||||
public async Task GetAccountRecoveryEnrolledUsers_InvalidKey_FiltersOut(
|
||||
string invalidKey,
|
||||
Guid orgId,
|
||||
SutProvider<OrganizationUserUserDetailsQuery> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var request = new OrganizationUserUserDetailsQueryRequest { OrganizationId = orgId };
|
||||
|
||||
var validUser = CreateOrgUserDetails(orgId, "valid-key");
|
||||
var invalidUser = CreateOrgUserDetails(orgId, invalidKey);
|
||||
var allUsers = new[] { validUser, invalidUser };
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyDetailsByOrganizationAsync_vNext(orgId, false, false)
|
||||
.Returns(allUsers);
|
||||
|
||||
SetupTwoFactorAndClaimedStatus(sutProvider, orgId);
|
||||
|
||||
// Act
|
||||
var result = (await sutProvider.Sut.GetAccountRecoveryEnrolledUsers(request)).ToList();
|
||||
|
||||
// Assert - invalid key user should be filtered out
|
||||
Assert.Single(result);
|
||||
Assert.Equal(validUser.Id, result[0].OrgUser.Id);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetAccountRecoveryEnrolledUsers_NullKey_FiltersOut(
|
||||
Guid orgId,
|
||||
SutProvider<OrganizationUserUserDetailsQuery> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var request = new OrganizationUserUserDetailsQueryRequest { OrganizationId = orgId };
|
||||
|
||||
var validUser = CreateOrgUserDetails(orgId, "valid-key");
|
||||
var nullUser = CreateOrgUserDetails(orgId, null!);
|
||||
var allUsers = new[] { validUser, nullUser };
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyDetailsByOrganizationAsync_vNext(orgId, false, false)
|
||||
.Returns(allUsers);
|
||||
|
||||
SetupTwoFactorAndClaimedStatus(sutProvider, orgId);
|
||||
|
||||
// Act
|
||||
var result = (await sutProvider.Sut.GetAccountRecoveryEnrolledUsers(request)).ToList();
|
||||
|
||||
// Assert
|
||||
Assert.Single(result);
|
||||
Assert.Equal(validUser.Id, result[0].OrgUser.Id);
|
||||
}
|
||||
|
||||
private static OrganizationUserUserDetails CreateOrgUserDetails(Guid orgId, string resetPasswordKey)
|
||||
{
|
||||
return new OrganizationUserUserDetails
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
OrganizationId = orgId,
|
||||
UserId = Guid.NewGuid(),
|
||||
Status = OrganizationUserStatusType.Confirmed,
|
||||
Type = OrganizationUserType.User,
|
||||
UsesKeyConnector = false,
|
||||
ResetPasswordKey = resetPasswordKey,
|
||||
Email = "test@example.com"
|
||||
};
|
||||
}
|
||||
|
||||
private static void SetupTwoFactorAndClaimedStatus(
|
||||
SutProvider<OrganizationUserUserDetailsQuery> sutProvider, Guid orgId)
|
||||
{
|
||||
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
|
||||
.TwoFactorIsEnabledAsync(Arg.Any<IEnumerable<OrganizationUserUserDetails>>())
|
||||
.Returns(callInfo =>
|
||||
{
|
||||
var users = callInfo.Arg<IEnumerable<OrganizationUserUserDetails>>();
|
||||
return users.Select(u => (user: u, twoFactorIsEnabled: false)).ToList();
|
||||
});
|
||||
|
||||
sutProvider.GetDependency<IGetOrganizationUsersClaimedStatusQuery>()
|
||||
.GetUsersOrganizationClaimedStatusAsync(Arg.Any<Guid>(), Arg.Any<IEnumerable<Guid>>())
|
||||
.Returns(callInfo =>
|
||||
{
|
||||
var userIds = callInfo.Arg<IEnumerable<Guid>>();
|
||||
return userIds.ToDictionary(id => id, _ => false);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Test.AdminConsole.AutoFixture;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
@@ -595,6 +596,41 @@ public class UserServiceTests
|
||||
user.MasterPassword = null;
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData("")]
|
||||
[BitAutoData(" ")]
|
||||
[BitAutoData("\t")]
|
||||
public async Task AdminResetPasswordAsync_EmptyOrWhitespaceResetPasswordKey_ThrowsBadRequest(
|
||||
string resetPasswordKey,
|
||||
SutProvider<UserService> sutProvider,
|
||||
Organization organization,
|
||||
OrganizationUser orgUser,
|
||||
[Policy(PolicyType.ResetPassword, true)] PolicyStatus policy)
|
||||
{
|
||||
// Arrange
|
||||
organization.UseResetPassword = true;
|
||||
orgUser.Status = OrganizationUserStatusType.Confirmed;
|
||||
orgUser.OrganizationId = organization.Id;
|
||||
orgUser.ResetPasswordKey = resetPasswordKey;
|
||||
orgUser.UserId = Guid.NewGuid();
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetByIdAsync(organization.Id)
|
||||
.Returns(organization);
|
||||
sutProvider.GetDependency<IPolicyQuery>()
|
||||
.RunAsync(organization.Id, PolicyType.ResetPassword)
|
||||
.Returns(policy);
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetByIdAsync(orgUser.Id)
|
||||
.Returns(orgUser);
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
|
||||
sutProvider.Sut.AdminResetPasswordAsync(
|
||||
OrganizationUserType.Owner, organization.Id, orgUser.Id, "newPassword", "key"));
|
||||
Assert.Equal("Organization User not valid", exception.Message);
|
||||
}
|
||||
}
|
||||
|
||||
public static class UserServiceSutProviderExtensions
|
||||
|
||||
@@ -299,6 +299,27 @@ public class UserDecryptionOptionsBuilderTests
|
||||
Assert.True(result.TrustedDeviceOption?.HasAdminApproval);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData("")]
|
||||
[BitAutoData(" ")]
|
||||
[BitAutoData((string)null)]
|
||||
public async Task Build_EmptyOrWhitespaceResetPasswordKey_ShouldReturnHasAdminApprovalFalse(
|
||||
string resetPasswordKey,
|
||||
SsoConfig ssoConfig,
|
||||
SsoConfigurationData configurationData,
|
||||
[OrganizationUserWithDefaultPermissions] OrganizationUser organizationUser,
|
||||
User user)
|
||||
{
|
||||
configurationData.MemberDecryptionType = MemberDecryptionType.TrustedDeviceEncryption;
|
||||
ssoConfig.Data = configurationData.Serialize();
|
||||
organizationUser.ResetPasswordKey = resetPasswordKey;
|
||||
_organizationUserRepository.GetByOrganizationAsync(ssoConfig.OrganizationId, user.Id).Returns(organizationUser);
|
||||
|
||||
var result = await _builder.ForUser(user).WithSso(ssoConfig).BuildAsync();
|
||||
|
||||
Assert.False(result.TrustedDeviceOption?.HasAdminApproval);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Build_WhenUserHasNoMasterPassword_ShouldReturnNoMasterPasswordUnlock(User user)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user