Files
server/test/Api.Test/AdminConsole/Controllers/OrganizationUsersControllerTests.cs
Dave 8b2cb89390 [PM-35394] MasterPasswordService Admin Console Integration (#7629)
* test(org-user-request-model): Add model validation tests.

* feat(request-models): Add Authentication and Unlock Data fields with annotations.

* test(recover-command): Add tests for Authentication and Unlock Data payload signature.

* feat(recover-command): Add overload for Authentication and Unlock Data payload signature.

* test(recover-command): Add tests for behavior with authentication and unlock data.

* feat(recover-command): Add impl for hash and key, authentication and unlock data inputs.

* test(org-users-controller): Add controller tests for dispatch.

* feat(org-users-controller): Add controller impl for dispatch for both request payload variants.

* chore: lint.

* fix(request-model): Validation method drifted in base; rename.

* test(request-model): Update validation tests.

* feat(request-model): Support 2FA-only validation at the boundary.

* test(request-model): Express handling of v1 vs v2 requests.

* PM-35394 - Per reviewer's request, mark  AdminRecoverAccountCommand.RecoverAccountAsync that doesn't accept new models obselete

* PM-35394 - Fix using directive after model namespace move

Merge from main moved OrganizationUserResetPasswordRequestModel to the
AdminConsole namespace; update the test's using directive to match,
restoring both the build and dotnet format checks.

---------

Co-authored-by: Jared Snider <jsnider@bitwarden.com>
2026-05-28 16:37:43 -04:00

934 lines
44 KiB
C#

using System.Security.Claims;
using Bit.Api.AdminConsole.Authorization;
using Bit.Api.AdminConsole.Authorization.Collections;
using Bit.Api.AdminConsole.Controllers;
using Bit.Api.AdminConsole.Models.Request.Organizations;
using Bit.Core;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.AccountRecovery;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUser;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.UpdateUserResetPasswordEnrollment;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
using Bit.Core.AdminConsole.Utilities.v2.Results;
using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Repositories;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.KeyManagement.Models.Api.Request;
using Bit.Core.Models.Api;
using Bit.Core.Models.Business;
using Bit.Core.Models.Data;
using Bit.Core.Models.Data.Organizations;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Utilities;
using Bit.Test.Common.AutoFixture;
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;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using NSubstitute;
using OneOf.Types;
using Xunit;
namespace Bit.Api.Test.AdminConsole.Controllers;
[ControllerCustomize(typeof(OrganizationUsersController))]
[SutProviderCustomize]
public class OrganizationUsersControllerTests
{
[Theory]
[BitAutoData]
public async Task PutResetPasswordEnrollment_InvitedUser_AcceptsInvite(Guid orgId, Guid userId, OrganizationUserResetPasswordEnrollmentRequestModel model,
User user, OrganizationUser orgUser, SutProvider<OrganizationUsersController> sutProvider)
{
orgUser.Status = Core.Enums.OrganizationUserStatusType.Invited;
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(user);
sutProvider.GetDependency<IUserService>().VerifySecretAsync(default, default).ReturnsForAnyArgs(true);
sutProvider.GetDependency<IOrganizationUserRepository>().GetByOrganizationAsync(default, default).ReturnsForAnyArgs(orgUser);
await sutProvider.Sut.PutResetPasswordEnrollment(orgId, userId, model);
await sutProvider.GetDependency<IAcceptOrgUserCommand>().Received(1).AcceptOrgUserByOrgIdAsync(orgId, user, sutProvider.GetDependency<IUserService>());
}
[Theory]
[BitAutoData]
public async Task PutResetPasswordEnrollment_ConfirmedUser_AcceptsInvite(Guid orgId, Guid userId, OrganizationUserResetPasswordEnrollmentRequestModel model,
User user, OrganizationUser orgUser, SutProvider<OrganizationUsersController> sutProvider)
{
orgUser.Status = Core.Enums.OrganizationUserStatusType.Confirmed;
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(user);
sutProvider.GetDependency<IUserService>().VerifySecretAsync(default, default).ReturnsForAnyArgs(true);
sutProvider.GetDependency<IOrganizationUserRepository>().GetByOrganizationAsync(default, default).ReturnsForAnyArgs(orgUser);
await sutProvider.Sut.PutResetPasswordEnrollment(orgId, userId, model);
await sutProvider.GetDependency<IAcceptOrgUserCommand>().Received(0).AcceptOrgUserByOrgIdAsync(orgId, user, sutProvider.GetDependency<IUserService>());
}
[Theory]
[BitAutoData]
public async Task PutResetPasswordEnrollment_PasswordValidationFails_Throws(Guid orgId, Guid userId, OrganizationUserResetPasswordEnrollmentRequestModel model,
User user, SutProvider<OrganizationUsersController> sutProvider, OrganizationUser orgUser)
{
orgUser.Status = OrganizationUserStatusType.Confirmed;
model.MasterPasswordHash = "NotThePassword";
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(user);
sutProvider.GetDependency<ISsoConfigRepository>().GetByOrganizationIdAsync(default).ReturnsForAnyArgs((SsoConfig)null);
await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.PutResetPasswordEnrollment(orgId, userId, model));
}
[Theory]
[BitAutoData]
public async Task PutResetPasswordEnrollment_PasswordValidationPasses_Continues(Guid orgId, Guid userId, OrganizationUserResetPasswordEnrollmentRequestModel model,
User user, OrganizationUser orgUser, SutProvider<OrganizationUsersController> sutProvider)
{
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(user);
sutProvider.GetDependency<IUserService>().VerifySecretAsync(user, model.MasterPasswordHash).Returns(true);
sutProvider.GetDependency<ISsoConfigRepository>().GetByOrganizationIdAsync(default).ReturnsForAnyArgs((SsoConfig)null);
sutProvider.GetDependency<IOrganizationUserRepository>().GetByOrganizationAsync(default, default).ReturnsForAnyArgs(orgUser);
await sutProvider.Sut.PutResetPasswordEnrollment(orgId, userId, model);
await sutProvider.GetDependency<IUpdateUserResetPasswordEnrollmentCommand>().Received(1).UpdateUserResetPasswordEnrollmentAsync(
orgId,
userId,
model.ResetPasswordKey,
user.Id
);
}
[Theory]
[BitAutoData]
public async Task Accept_RequiresKnownUser(Guid orgId, Guid orgUserId, OrganizationUserAcceptRequestModel model,
SutProvider<OrganizationUsersController> sutProvider)
{
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs((User)null);
await Assert.ThrowsAsync<UnauthorizedAccessException>(() => sutProvider.Sut.Accept(orgId, orgUserId, model));
}
[Theory]
[BitAutoData]
public async Task Accept_WhenOrganizationUserNotFound_ThrowsNotFoundException(
Guid orgId, Guid orgUserId, OrganizationUserAcceptRequestModel model, User user,
SutProvider<OrganizationUsersController> sutProvider)
{
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(user);
sutProvider.GetDependency<IOrganizationUserRepository>().GetByIdAsync(orgUserId).Returns((OrganizationUser)null);
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.Accept(orgId, orgUserId, model));
}
[Theory]
[BitAutoData]
public async Task Accept_WhenOrganizationIdMismatch_ThrowsNotFoundException(
Guid orgId, Guid orgUserId, OrganizationUserAcceptRequestModel model, User user, OrganizationUser organizationUser,
SutProvider<OrganizationUsersController> sutProvider)
{
organizationUser.OrganizationId = Guid.NewGuid(); // Different org ID
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(user);
sutProvider.GetDependency<IOrganizationUserRepository>().GetByIdAsync(orgUserId).Returns(organizationUser);
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.Accept(orgId, orgUserId, model));
}
[Theory]
[BitAutoData]
public async Task Accept_NoMasterPasswordReset(Guid orgId, Guid orgUserId,
OrganizationUserAcceptRequestModel model, User user, OrganizationUser organizationUser, SutProvider<OrganizationUsersController> sutProvider)
{
organizationUser.OrganizationId = orgId;
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(user);
sutProvider.GetDependency<IOrganizationUserRepository>().GetByIdAsync(orgUserId).Returns(organizationUser);
var policyRequirement = new ResetPasswordPolicyRequirement([
new PolicyDetails
{
OrganizationId = orgId,
PolicyData = CoreHelpers.ClassToJsonData(new ResetPasswordDataModel { AutoEnrollEnabled = false })
}
]);
sutProvider.GetDependency<IPolicyRequirementQuery>().GetAsync<ResetPasswordPolicyRequirement>(user.Id)
.Returns(policyRequirement);
await sutProvider.Sut.Accept(orgId, orgUserId, model);
await sutProvider.GetDependency<IAcceptOrgUserCommand>().Received(1)
.AcceptOrgUserByEmailTokenAsync(orgUserId, user, model.Token, sutProvider.GetDependency<IUserService>());
await sutProvider.GetDependency<IUpdateUserResetPasswordEnrollmentCommand>().DidNotReceiveWithAnyArgs()
.UpdateUserResetPasswordEnrollmentAsync(default, default, default, default);
}
[Theory]
[BitAutoData]
public async Task Accept_WhenOrganizationUsePoliciesIsEnabledAndResetPolicyIsEnabled_ShouldHandleResetPassword(Guid orgId, Guid orgUserId,
OrganizationUserAcceptRequestModel model, User user, OrganizationUser organizationUser,
SutProvider<OrganizationUsersController> sutProvider)
{
// Arrange
organizationUser.OrganizationId = orgId;
var userService = sutProvider.GetDependency<IUserService>();
userService.GetUserByPrincipalAsync(default).ReturnsForAnyArgs(user);
sutProvider.GetDependency<IOrganizationUserRepository>().GetByIdAsync(orgUserId).Returns(organizationUser);
var policyRequirement = new ResetPasswordPolicyRequirement([
new PolicyDetails
{
OrganizationId = orgId,
PolicyData = CoreHelpers.ClassToJsonData(new ResetPasswordDataModel { AutoEnrollEnabled = true })
}
]);
sutProvider.GetDependency<IPolicyRequirementQuery>().GetAsync<ResetPasswordPolicyRequirement>(user.Id)
.Returns(policyRequirement);
// Act
await sutProvider.Sut.Accept(orgId, orgUserId, model);
// Assert
await sutProvider.GetDependency<IAcceptOrgUserCommand>().Received(1)
.AcceptOrgUserByEmailTokenAsync(orgUserId, user, model.Token, userService);
await sutProvider.GetDependency<IUpdateUserResetPasswordEnrollmentCommand>().Received(1)
.UpdateUserResetPasswordEnrollmentAsync(orgId, user.Id, model.ResetPasswordKey, user.Id);
await userService.Received(1).GetUserByPrincipalAsync(default);
}
[Theory]
[BitAutoData]
public async Task Accept_WhenResetPolicyIsEnabled_WithAutoEnrollDisabled_ShouldNotResetPassword(Guid orgId, Guid orgUserId,
OrganizationUserAcceptRequestModel model, User user, OrganizationUser organizationUser,
SutProvider<OrganizationUsersController> sutProvider)
{
// Arrange
organizationUser.OrganizationId = orgId;
var userService = sutProvider.GetDependency<IUserService>();
userService.GetUserByPrincipalAsync(default).ReturnsForAnyArgs(user);
sutProvider.GetDependency<IOrganizationUserRepository>().GetByIdAsync(orgUserId).Returns(organizationUser);
var policyRequirement = new ResetPasswordPolicyRequirement([
new PolicyDetails
{
OrganizationId = orgId,
PolicyData = CoreHelpers.ClassToJsonData(new ResetPasswordDataModel { AutoEnrollEnabled = false })
}
]);
sutProvider.GetDependency<IPolicyRequirementQuery>().GetAsync<ResetPasswordPolicyRequirement>(user.Id)
.Returns(policyRequirement);
// Act
await sutProvider.Sut.Accept(orgId, orgUserId, model);
// Assert
await userService.Received(1).GetUserByPrincipalAsync(default);
await sutProvider.GetDependency<IAcceptOrgUserCommand>().Received(1)
.AcceptOrgUserByEmailTokenAsync(orgUserId, user, model.Token, userService);
await sutProvider.GetDependency<IUpdateUserResetPasswordEnrollmentCommand>().Received(0)
.UpdateUserResetPasswordEnrollmentAsync(orgId, user.Id, model.ResetPasswordKey, user.Id);
}
[Theory]
[BitAutoData]
public async Task Invite_Success(OrganizationAbility organizationAbility, OrganizationUserInviteRequestModel model,
Guid userId, SutProvider<OrganizationUsersController> sutProvider)
{
sutProvider.GetDependency<ICurrentContext>().ManageUsers(organizationAbility.Id).Returns(true);
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilityAsync(organizationAbility.Id)
.Returns(organizationAbility);
sutProvider.GetDependency<IAuthorizationService>()
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(),
Arg.Any<IEnumerable<Collection>>(),
Arg.Is<IEnumerable<IAuthorizationRequirement>>(reqs => reqs.Contains(BulkCollectionOperations.ModifyUserAccess)))
.Returns(AuthorizationResult.Success());
sutProvider.GetDependency<IUserService>().GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);
await sutProvider.Sut.Invite(organizationAbility.Id, model);
await sutProvider.GetDependency<IOrganizationService>().Received(1).InviteUsersAsync(organizationAbility.Id,
userId, systemUser: null, Arg.Is<IEnumerable<(OrganizationUserInvite, string)>>(invites =>
invites.Count() == 1 &&
invites.First().Item1.Emails.SequenceEqual(model.Emails) &&
invites.First().Item1.Type == model.Type &&
invites.First().Item1.AccessSecretsManager == model.AccessSecretsManager));
}
[Theory]
[BitAutoData]
public async Task Invite_NotAuthorizedToGiveAccessToCollections_Throws(OrganizationAbility organizationAbility, OrganizationUserInviteRequestModel model,
Guid userId, SutProvider<OrganizationUsersController> sutProvider)
{
sutProvider.GetDependency<ICurrentContext>().ManageUsers(organizationAbility.Id).Returns(true);
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilityAsync(organizationAbility.Id)
.Returns(organizationAbility);
sutProvider.GetDependency<IAuthorizationService>()
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(),
Arg.Any<IEnumerable<Collection>>(),
Arg.Is<IEnumerable<IAuthorizationRequirement>>(reqs => reqs.Contains(BulkCollectionOperations.ModifyUserAccess)))
.Returns(AuthorizationResult.Failed());
sutProvider.GetDependency<IUserService>().GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.Invite(organizationAbility.Id, model));
}
[Theory, BitAutoData]
public async Task Get_ReturnsUser(
OrganizationUserUserDetails organizationUser, ICollection<CollectionAccessSelection> collections,
SutProvider<OrganizationUsersController> sutProvider)
{
organizationUser.Permissions = null;
sutProvider.GetDependency<ICurrentContext>()
.ManageUsers(organizationUser.OrganizationId)
.Returns(true);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetDetailsByIdWithSharedCollectionsAsync(organizationUser.Id)
.Returns((organizationUser, collections));
sutProvider.GetDependency<IGetOrganizationUsersClaimedStatusQuery>()
.GetUsersOrganizationClaimedStatusAsync(organizationUser.OrganizationId, Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(organizationUser.Id)))
.Returns(new Dictionary<Guid, bool> { { organizationUser.Id, true } });
var response = await sutProvider.Sut.Get(organizationUser.OrganizationId, organizationUser.Id, false);
Assert.Equal(organizationUser.Id, response.Id);
Assert.True(response.ManagedByOrganization);
Assert.True(response.ClaimedByOrganization);
}
[Theory]
[BitAutoData]
public async Task GetMany_ReturnsUsers(
ICollection<OrganizationUserUserDetails> organizationUsers, OrganizationAbility organizationAbility,
SutProvider<OrganizationUsersController> sutProvider)
{
GetMany_Setup(organizationAbility, organizationUsers, sutProvider);
var response = await sutProvider.Sut.GetAll(organizationAbility.Id, false, false);
Assert.True(response.Data.All(r => organizationUsers.Any(ou => ou.Id == r.Id)));
}
[Theory]
[BitAutoData]
public async Task GetAccountRecoveryDetails_ReturnsDetails(
Guid organizationId,
OrganizationUserBulkRequestModel bulkRequestModel,
ICollection<OrganizationUserResetPasswordDetails> resetPasswordDetails,
SutProvider<OrganizationUsersController> sutProvider)
{
sutProvider.GetDependency<ICurrentContext>().ManageResetPassword(organizationId).Returns(true);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyAccountRecoveryDetailsByOrganizationUserAsync(organizationId, bulkRequestModel.Ids)
.Returns(resetPasswordDetails);
var response = await sutProvider.Sut.GetAccountRecoveryDetails(organizationId, bulkRequestModel);
Assert.Equal(resetPasswordDetails.Count, response.Data.Count());
Assert.True(response.Data.All(r =>
resetPasswordDetails.Any(ou =>
ou.OrganizationUserId == r.OrganizationUserId &&
ou.Kdf == r.Kdf &&
ou.KdfIterations == r.KdfIterations &&
ou.KdfMemory == r.KdfMemory &&
ou.KdfParallelism == r.KdfParallelism &&
ou.ResetPasswordKey == r.ResetPasswordKey &&
ou.EncryptedPrivateKey == r.EncryptedPrivateKey &&
ou.MasterPasswordSalt == r.MasterPasswordSalt)));
}
[Theory]
[BitAutoData]
public async Task GetResetPasswordDetails_WhenOrganizationUserNotFound_ThrowsNotFound(
Guid orgId, Guid orgUserId,
SutProvider<OrganizationUsersController> sutProvider)
{
// Arrange
sutProvider.GetDependency<IOrganizationUserRepository>().GetByIdAsync(orgUserId).Returns((OrganizationUser)null);
// Act & Assert
await Assert.ThrowsAsync<NotFoundException>(() =>
sutProvider.Sut.GetResetPasswordDetails(orgUserId, new Organization { Id = orgId }));
}
[Theory]
[BitAutoData]
public async Task GetResetPasswordDetails_WhenOrganizationIdMismatch_ThrowsNotFound(
Guid orgId, Guid orgUserId, OrganizationUser organizationUser,
SutProvider<OrganizationUsersController> sutProvider)
{
// Arrange
organizationUser.OrganizationId = Guid.NewGuid(); // Different from the bound org
organizationUser.UserId = Guid.NewGuid();
sutProvider.GetDependency<IOrganizationUserRepository>().GetByIdAsync(orgUserId).Returns(organizationUser);
// Act & Assert
await Assert.ThrowsAsync<NotFoundException>(() =>
sutProvider.Sut.GetResetPasswordDetails(orgUserId, new Organization { Id = orgId }));
}
[Theory]
[BitAutoData]
public async Task GetResetPasswordDetails_WhenUserIdIsNull_ThrowsNotFound(
Guid orgId, Guid orgUserId, OrganizationUser organizationUser,
SutProvider<OrganizationUsersController> sutProvider)
{
// Arrange
organizationUser.OrganizationId = orgId;
organizationUser.UserId = null;
sutProvider.GetDependency<IOrganizationUserRepository>().GetByIdAsync(orgUserId).Returns(organizationUser);
// Act & Assert
await Assert.ThrowsAsync<NotFoundException>(() =>
sutProvider.Sut.GetResetPasswordDetails(orgUserId, new Organization { Id = orgId }));
}
[Theory]
[BitAutoData]
public async Task GetResetPasswordDetails_WhenValid_ReturnsDetails(
Guid orgId, Guid orgUserId, OrganizationUser organizationUser, User user, Organization org,
SutProvider<OrganizationUsersController> sutProvider)
{
// Arrange
org.Id = orgId;
organizationUser.OrganizationId = org.Id;
organizationUser.UserId = user.Id;
sutProvider.GetDependency<IOrganizationUserRepository>().GetByIdAsync(orgUserId).Returns(organizationUser);
sutProvider.GetDependency<IUserService>().GetUserByIdAsync(user.Id).Returns(user);
// Act — org is passed directly via [BindOrganization]; the repository is no longer called
var response = await sutProvider.Sut.GetResetPasswordDetails(orgUserId, org);
// Assert
Assert.Equal(organizationUser.Id, response.OrganizationUserId);
Assert.Equal(user.Kdf, response.Kdf);
Assert.Equal(user.KdfIterations, response.KdfIterations);
Assert.Equal(org.PrivateKey, response.EncryptedPrivateKey);
Assert.Equal(user.MasterPasswordSalt, response.MasterPasswordSalt);
}
[Theory]
[BitAutoData]
public async Task DeleteAccount_WhenCurrentUserNotFound_ReturnsUnauthorizedResult(
Guid orgId, Guid id, SutProvider<OrganizationUsersController> sutProvider)
{
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs((Guid?)null);
var result = await sutProvider.Sut.DeleteAccount(orgId, id);
Assert.IsType<UnauthorizedHttpResult>(result);
}
[Theory]
[BitAutoData]
public async Task BulkDeleteAccount_WhenCurrentUserNotFound_ThrowsUnauthorizedAccessException(
Guid orgId, OrganizationUserBulkRequestModel model, SutProvider<OrganizationUsersController> sutProvider)
{
sutProvider.GetDependency<ICurrentContext>().ManageUsers(orgId).Returns(true);
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs((User)null);
await Assert.ThrowsAsync<UnauthorizedAccessException>(() =>
sutProvider.Sut.BulkDeleteAccount(orgId, model));
}
private void GetMany_Setup(OrganizationAbility organizationAbility,
ICollection<OrganizationUserUserDetails> organizationUsers,
SutProvider<OrganizationUsersController> sutProvider)
{
foreach (var orgUser in organizationUsers)
{
orgUser.Permissions = null;
}
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilityAsync(organizationAbility.Id)
.Returns(organizationAbility);
sutProvider.GetDependency<IOrganizationUserUserDetailsQuery>().GetOrganizationUserUserDetails(Arg.Any<OrganizationUserUserDetailsQueryRequest>()).Returns(organizationUsers);
sutProvider.GetDependency<IAuthorizationService>().AuthorizeAsync(
user: Arg.Any<ClaimsPrincipal>(),
resource: Arg.Any<Object>(),
requirements: Arg.Any<IEnumerable<IAuthorizationRequirement>>())
.Returns(AuthorizationResult.Success());
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyDetailsByOrganizationAsync(organizationAbility.Id, Arg.Any<bool>(), Arg.Any<bool>())
.Returns(organizationUsers);
}
[Theory]
[BitAutoData]
public async Task Accept_WithInvalidModelResetPasswordKey_ThrowsBadRequestException(Guid orgId, Guid orgUserId,
OrganizationUserAcceptRequestModel model, User user, OrganizationUser organizationUser, SutProvider<OrganizationUsersController> sutProvider)
{
// Arrange
model.ResetPasswordKey = " ";
organizationUser.OrganizationId = orgId;
sutProvider.GetDependency<IOrganizationUserRepository>().GetByIdAsync(orgUserId).Returns(organizationUser);
var userService = sutProvider.GetDependency<IUserService>();
userService.GetUserByPrincipalAsync(default).ReturnsForAnyArgs(user);
var policyRequirementQuery = sutProvider.GetDependency<IPolicyRequirementQuery>();
var policyRequirement = new ResetPasswordPolicyRequirement([
new PolicyDetails
{
OrganizationId = orgId,
PolicyData = CoreHelpers.ClassToJsonData(new ResetPasswordDataModel { AutoEnrollEnabled = true })
}
]);
policyRequirementQuery.GetAsync<ResetPasswordPolicyRequirement>(user.Id).Returns(policyRequirement);
// Act
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.Accept(orgId, orgUserId, model));
// Assert
await sutProvider.GetDependency<IAcceptOrgUserCommand>().Received(0)
.AcceptOrgUserByEmailTokenAsync(orgUserId, user, model.Token, userService);
await sutProvider.GetDependency<IUpdateUserResetPasswordEnrollmentCommand>().Received(0)
.UpdateUserResetPasswordEnrollmentAsync(orgId, user.Id, model.ResetPasswordKey, user.Id);
await userService.Received(1).GetUserByPrincipalAsync(default);
Assert.Equal("Master Password reset is required, but not provided.", exception.Message);
}
[Theory]
[BitAutoData]
public async Task PutRecoverAccount_WhenAuthorizationFails_ReturnsBadRequest(
Guid orgId, Guid orgUserId, OrganizationUserResetPasswordRequestModel model, OrganizationUser organizationUser,
SutProvider<OrganizationUsersController> sutProvider)
{
organizationUser.OrganizationId = orgId;
sutProvider.GetDependency<IAuthorizationService>()
.AuthorizeAsync(
Arg.Any<ClaimsPrincipal>(),
organizationUser,
Arg.Is<IEnumerable<IAuthorizationRequirement>>(x => x.SingleOrDefault() is RecoverAccountAuthorizationRequirement))
.Returns(AuthorizationResult.Failed());
var result = await sutProvider.Sut.PutRecoverAccount(orgId, orgUserId, model, organizationUser);
Assert.IsType<BadRequest<ErrorResponseModel>>(result);
}
[Theory]
[BitAutoData]
public async Task PutRecoverAccount_FlagOff_WithHashAndKey_WhenSucceeds_RoutesToLegacyOverloadAndReturnsOk(
Guid orgId, Guid orgUserId, OrganizationUserResetPasswordRequestModel model, OrganizationUser organizationUser,
SutProvider<OrganizationUsersController> sutProvider)
{
organizationUser.OrganizationId = orgId;
model.UnlockData = null;
model.AuthenticationData = null;
sutProvider.GetDependency<IAuthorizationService>()
.AuthorizeAsync(
Arg.Any<ClaimsPrincipal>(),
organizationUser,
Arg.Is<IEnumerable<IAuthorizationRequirement>>(x => x.SingleOrDefault() is RecoverAccountAuthorizationRequirement))
.Returns(AuthorizationResult.Success());
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.AdminResetTwoFactor)
.Returns(false);
sutProvider.GetDependency<IAdminRecoverAccountCommand>()
.RecoverAccountAsync(Arg.Any<Guid>(), Arg.Any<OrganizationUser>(), Arg.Any<string>(), Arg.Any<string>())
.Returns(Microsoft.AspNetCore.Identity.IdentityResult.Success);
var result = await sutProvider.Sut.PutRecoverAccount(orgId, orgUserId, model, organizationUser);
Assert.IsType<Ok>(result);
await sutProvider.GetDependency<IAdminRecoverAccountCommand>().Received(1)
.RecoverAccountAsync(Arg.Any<Guid>(), Arg.Any<OrganizationUser>(), Arg.Any<string>(), Arg.Any<string>());
await sutProvider.GetDependency<IAdminRecoverAccountCommand>().DidNotReceive()
.RecoverAccountAsync(
Arg.Any<Guid>(),
Arg.Any<OrganizationUser>(),
Arg.Any<Bit.Core.KeyManagement.Models.Data.MasterPasswordUnlockData>(),
Arg.Any<Bit.Core.KeyManagement.Models.Data.MasterPasswordAuthenticationData>());
}
[Theory]
[BitAutoData]
public async Task PutRecoverAccount_FlagOff_WithHashAndKey_WhenFails_ReturnsBadRequest(
Guid orgId, Guid orgUserId, OrganizationUserResetPasswordRequestModel model, OrganizationUser organizationUser,
SutProvider<OrganizationUsersController> sutProvider)
{
organizationUser.OrganizationId = orgId;
model.UnlockData = null;
model.AuthenticationData = null;
sutProvider.GetDependency<IAuthorizationService>()
.AuthorizeAsync(
Arg.Any<ClaimsPrincipal>(),
organizationUser,
Arg.Is<IEnumerable<IAuthorizationRequirement>>(x => x.SingleOrDefault() is RecoverAccountAuthorizationRequirement))
.Returns(AuthorizationResult.Success());
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.AdminResetTwoFactor)
.Returns(false);
sutProvider.GetDependency<IAdminRecoverAccountCommand>()
.RecoverAccountAsync(Arg.Any<Guid>(), Arg.Any<OrganizationUser>(), Arg.Any<string>(), Arg.Any<string>())
.Returns(Microsoft.AspNetCore.Identity.IdentityResult.Failed(new Microsoft.AspNetCore.Identity.IdentityError { Description = "Error message" }));
var result = await sutProvider.Sut.PutRecoverAccount(orgId, orgUserId, model, organizationUser);
Assert.IsType<BadRequest<ModelStateDictionary>>(result);
}
// Flag-off dispatch to data-typed overload (UnlockData + AuthenticationData present)
[Theory]
[BitAutoData]
public async Task PutRecoverAccount_FlagOff_WithUnlockAndAuthenticationData_WhenSucceeds_RoutesToDataTypedOverloadAndReturnsOk(
Guid orgId, Guid orgUserId, OrganizationUserResetPasswordRequestModel model, OrganizationUser organizationUser,
KdfRequestModel kdf, string masterKeyWrappedUserKey, string salt, string masterPasswordAuthenticationHash,
SutProvider<OrganizationUsersController> sutProvider)
{
organizationUser.OrganizationId = orgId;
model.NewMasterPasswordHash = null;
model.Key = null;
model.UnlockData = new MasterPasswordUnlockDataRequestModel
{
Kdf = kdf,
MasterKeyWrappedUserKey = masterKeyWrappedUserKey,
Salt = salt
};
model.AuthenticationData = new MasterPasswordAuthenticationDataRequestModel
{
Kdf = kdf,
MasterPasswordAuthenticationHash = masterPasswordAuthenticationHash,
Salt = salt
};
sutProvider.GetDependency<IAuthorizationService>()
.AuthorizeAsync(
Arg.Any<ClaimsPrincipal>(),
organizationUser,
Arg.Is<IEnumerable<IAuthorizationRequirement>>(x => x.SingleOrDefault() is RecoverAccountAuthorizationRequirement))
.Returns(AuthorizationResult.Success());
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.AdminResetTwoFactor)
.Returns(false);
sutProvider.GetDependency<IAdminRecoverAccountCommand>()
.RecoverAccountAsync(
Arg.Any<Guid>(),
Arg.Any<OrganizationUser>(),
Arg.Any<Bit.Core.KeyManagement.Models.Data.MasterPasswordUnlockData>(),
Arg.Any<Bit.Core.KeyManagement.Models.Data.MasterPasswordAuthenticationData>())
.Returns(Microsoft.AspNetCore.Identity.IdentityResult.Success);
var result = await sutProvider.Sut.PutRecoverAccount(orgId, orgUserId, model, organizationUser);
Assert.IsType<Ok>(result);
await sutProvider.GetDependency<IAdminRecoverAccountCommand>().Received(1)
.RecoverAccountAsync(
Arg.Any<Guid>(),
Arg.Any<OrganizationUser>(),
Arg.Any<Bit.Core.KeyManagement.Models.Data.MasterPasswordUnlockData>(),
Arg.Any<Bit.Core.KeyManagement.Models.Data.MasterPasswordAuthenticationData>());
await sutProvider.GetDependency<IAdminRecoverAccountCommand>().DidNotReceive()
.RecoverAccountAsync(Arg.Any<Guid>(), Arg.Any<OrganizationUser>(), Arg.Any<string>(), Arg.Any<string>());
}
[Theory]
[BitAutoData]
public async Task PutRecoverAccount_FlagOff_WithUnlockAndAuthenticationData_WhenFails_ReturnsBadRequest(
Guid orgId, Guid orgUserId, OrganizationUserResetPasswordRequestModel model, OrganizationUser organizationUser,
KdfRequestModel kdf, string masterKeyWrappedUserKey, string salt, string masterPasswordAuthenticationHash,
SutProvider<OrganizationUsersController> sutProvider)
{
organizationUser.OrganizationId = orgId;
model.NewMasterPasswordHash = null;
model.Key = null;
model.UnlockData = new MasterPasswordUnlockDataRequestModel
{
Kdf = kdf,
MasterKeyWrappedUserKey = masterKeyWrappedUserKey,
Salt = salt
};
model.AuthenticationData = new MasterPasswordAuthenticationDataRequestModel
{
Kdf = kdf,
MasterPasswordAuthenticationHash = masterPasswordAuthenticationHash,
Salt = salt
};
sutProvider.GetDependency<IAuthorizationService>()
.AuthorizeAsync(
Arg.Any<ClaimsPrincipal>(),
organizationUser,
Arg.Is<IEnumerable<IAuthorizationRequirement>>(x => x.SingleOrDefault() is RecoverAccountAuthorizationRequirement))
.Returns(AuthorizationResult.Success());
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.AdminResetTwoFactor)
.Returns(false);
sutProvider.GetDependency<IAdminRecoverAccountCommand>()
.RecoverAccountAsync(
Arg.Any<Guid>(),
Arg.Any<OrganizationUser>(),
Arg.Any<Bit.Core.KeyManagement.Models.Data.MasterPasswordUnlockData>(),
Arg.Any<Bit.Core.KeyManagement.Models.Data.MasterPasswordAuthenticationData>())
.Returns(Microsoft.AspNetCore.Identity.IdentityResult.Failed(
new Microsoft.AspNetCore.Identity.IdentityError { Description = "Error message" }));
var result = await sutProvider.Sut.PutRecoverAccount(orgId, orgUserId, model, organizationUser);
Assert.IsType<BadRequest<ModelStateDictionary>>(result);
}
[Theory]
[BitAutoData]
public async Task PutRecoverAccount_FlagOn_WhenRecoverAccountSucceeds_ReturnsNoContent(
Guid orgId, Guid orgUserId, OrganizationUserResetPasswordRequestModel model, OrganizationUser organizationUser,
SutProvider<OrganizationUsersController> sutProvider)
{
organizationUser.OrganizationId = orgId;
sutProvider.GetDependency<IAuthorizationService>()
.AuthorizeAsync(
Arg.Any<ClaimsPrincipal>(),
organizationUser,
Arg.Is<IEnumerable<IAuthorizationRequirement>>(x => x.SingleOrDefault() is RecoverAccountAuthorizationRequirement))
.Returns(AuthorizationResult.Success());
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.AdminResetTwoFactor)
.Returns(true);
sutProvider.GetDependency<Bit.Core.AdminConsole.OrganizationFeatures.AccountRecovery.v2.IAdminRecoverAccountCommand>()
.RecoverAccountAsync(Arg.Any<Bit.Core.AdminConsole.OrganizationFeatures.AccountRecovery.v2.RecoverAccountRequest>())
.Returns(new Bit.Core.AdminConsole.Utilities.v2.Results.CommandResult(new OneOf.Types.None()));
var result = await sutProvider.Sut.PutRecoverAccount(orgId, orgUserId, model, organizationUser);
Assert.IsType<NoContent>(result);
}
[Theory]
[BitAutoData]
public async Task PutRecoverAccount_FlagOn_WhenRecoverAccountFails_ReturnsBadRequest(
Guid orgId, Guid orgUserId, OrganizationUserResetPasswordRequestModel model, OrganizationUser organizationUser,
SutProvider<OrganizationUsersController> sutProvider)
{
organizationUser.OrganizationId = orgId;
sutProvider.GetDependency<IAuthorizationService>()
.AuthorizeAsync(
Arg.Any<ClaimsPrincipal>(),
organizationUser,
Arg.Is<IEnumerable<IAuthorizationRequirement>>(x => x.SingleOrDefault() is RecoverAccountAuthorizationRequirement))
.Returns(AuthorizationResult.Success());
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.AdminResetTwoFactor)
.Returns(true);
sutProvider.GetDependency<Bit.Core.AdminConsole.OrganizationFeatures.AccountRecovery.v2.IAdminRecoverAccountCommand>()
.RecoverAccountAsync(Arg.Any<Bit.Core.AdminConsole.OrganizationFeatures.AccountRecovery.v2.RecoverAccountRequest>())
.Returns(new Bit.Core.AdminConsole.Utilities.v2.Results.CommandResult(
new Bit.Core.AdminConsole.OrganizationFeatures.AccountRecovery.v2.PasswordUpdateFailedError("Error message")));
var result = await sutProvider.Sut.PutRecoverAccount(orgId, orgUserId, model, organizationUser);
Assert.IsType<BadRequest<ErrorResponseModel>>(result);
}
[Theory]
[BitAutoData]
public async Task AutomaticallyConfirmOrganizationUserAsync_UserIdNull_ReturnsUnauthorized(
Guid orgId,
Guid orgUserId,
OrganizationUserConfirmRequestModel model,
SutProvider<OrganizationUsersController> sutProvider)
{
// Arrange
sutProvider.GetDependency<IUserService>()
.GetProperUserId(Arg.Any<ClaimsPrincipal>())
.Returns((Guid?)null);
// Act
var result = await sutProvider.Sut.AutomaticallyConfirmOrganizationUserAsync(orgId, orgUserId, model);
// Assert
Assert.IsType<UnauthorizedHttpResult>(result);
}
[Theory]
[BitAutoData]
public async Task AutomaticallyConfirmOrganizationUserAsync_UserIdEmpty_ReturnsUnauthorized(
Guid orgId,
Guid orgUserId,
OrganizationUserConfirmRequestModel model,
SutProvider<OrganizationUsersController> sutProvider)
{
// Arrange
sutProvider.GetDependency<IUserService>()
.GetProperUserId(Arg.Any<ClaimsPrincipal>())
.Returns(Guid.Empty);
// Act
var result = await sutProvider.Sut.AutomaticallyConfirmOrganizationUserAsync(orgId, orgUserId, model);
// Assert
Assert.IsType<UnauthorizedHttpResult>(result);
}
[Theory]
[BitAutoData]
public async Task AutomaticallyConfirmOrganizationUserAsync_Success_ReturnsOk(
Guid orgId,
Guid orgUserId,
Guid userId,
OrganizationUserConfirmRequestModel model,
SutProvider<OrganizationUsersController> sutProvider)
{
// Arrange
sutProvider.GetDependency<IUserService>()
.GetProperUserId(Arg.Any<ClaimsPrincipal>())
.Returns(userId);
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(orgId)
.Returns(true);
sutProvider.GetDependency<IAutomaticallyConfirmOrganizationUserCommand>()
.AutomaticallyConfirmOrganizationUserAsync(Arg.Any<AutomaticallyConfirmOrganizationUserRequest>())
.Returns(new CommandResult(new None()));
// Act
var result = await sutProvider.Sut.AutomaticallyConfirmOrganizationUserAsync(orgId, orgUserId, model);
// Assert
Assert.IsType<NoContent>(result);
}
[Theory]
[BitAutoData]
public async Task AutomaticallyConfirmOrganizationUserAsync_NotFoundError_ReturnsNotFound(
Guid orgId,
Guid orgUserId,
Guid userId,
OrganizationUserConfirmRequestModel model,
SutProvider<OrganizationUsersController> sutProvider)
{
// Arrange
sutProvider.GetDependency<IUserService>()
.GetProperUserId(Arg.Any<ClaimsPrincipal>())
.Returns(userId);
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(orgId)
.Returns(false);
var notFoundError = new OrganizationNotFound();
sutProvider.GetDependency<IAutomaticallyConfirmOrganizationUserCommand>()
.AutomaticallyConfirmOrganizationUserAsync(Arg.Any<AutomaticallyConfirmOrganizationUserRequest>())
.Returns(new CommandResult(notFoundError));
// Act
var result = await sutProvider.Sut.AutomaticallyConfirmOrganizationUserAsync(orgId, orgUserId, model);
// Assert
var notFoundResult = Assert.IsType<NotFound<ErrorResponseModel>>(result);
Assert.Equal(notFoundError.Message, notFoundResult.Value.Message);
}
[Theory]
[BitAutoData]
public async Task AutomaticallyConfirmOrganizationUserAsync_BadRequestError_ReturnsBadRequest(
Guid orgId,
Guid orgUserId,
Guid userId,
OrganizationUserConfirmRequestModel model,
SutProvider<OrganizationUsersController> sutProvider)
{
// Arrange
sutProvider.GetDependency<IUserService>()
.GetProperUserId(Arg.Any<ClaimsPrincipal>())
.Returns(userId);
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(orgId)
.Returns(true);
var badRequestError = new UserIsNotAccepted();
sutProvider.GetDependency<IAutomaticallyConfirmOrganizationUserCommand>()
.AutomaticallyConfirmOrganizationUserAsync(Arg.Any<AutomaticallyConfirmOrganizationUserRequest>())
.Returns(new CommandResult(badRequestError));
// Act
var result = await sutProvider.Sut.AutomaticallyConfirmOrganizationUserAsync(orgId, orgUserId, model);
// Assert
var badRequestResult = Assert.IsType<BadRequest<ErrorResponseModel>>(result);
Assert.Equal(badRequestError.Message, badRequestResult.Value.Message);
}
[Theory]
[BitAutoData]
public async Task AutomaticallyConfirmOrganizationUserAsync_InternalError_ReturnsProblem(
Guid orgId,
Guid orgUserId,
Guid userId,
OrganizationUserConfirmRequestModel model,
SutProvider<OrganizationUsersController> sutProvider)
{
// Arrange
sutProvider.GetDependency<IUserService>()
.GetProperUserId(Arg.Any<ClaimsPrincipal>())
.Returns(userId);
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(orgId)
.Returns(true);
var internalError = new FailedToWriteToEventLog();
sutProvider.GetDependency<IAutomaticallyConfirmOrganizationUserCommand>()
.AutomaticallyConfirmOrganizationUserAsync(Arg.Any<AutomaticallyConfirmOrganizationUserRequest>())
.Returns(new CommandResult(internalError));
// Act
var result = await sutProvider.Sut.AutomaticallyConfirmOrganizationUserAsync(orgId, orgUserId, model);
// Assert
var problemResult = Assert.IsType<JsonHttpResult<ErrorResponseModel>>(result);
Assert.Equal(StatusCodes.Status500InternalServerError, problemResult.StatusCode);
}
[Theory]
[BitAutoData]
public async Task BulkReinvite_UsesBulkResendOrganizationInvitesCommand(
Guid organizationId,
OrganizationUserBulkRequestModel bulkRequestModel,
List<OrganizationUser> organizationUsers,
Guid userId,
SutProvider<OrganizationUsersController> sutProvider)
{
// Arrange
sutProvider.GetDependency<ICurrentContext>().ManageUsers(organizationId).Returns(true);
sutProvider.GetDependency<IUserService>().GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);
var expectedResults = organizationUsers.Select(u => Tuple.Create(u, "")).ToList();
sutProvider.GetDependency<IBulkResendOrganizationInvitesCommand>()
.BulkResendInvitesAsync(organizationId, userId, bulkRequestModel.Ids)
.Returns(expectedResults);
// Act
var response = await sutProvider.Sut.BulkReinvite(organizationId, bulkRequestModel);
// Assert
Assert.Equal(organizationUsers.Count, response.Data.Count());
await sutProvider.GetDependency<IBulkResendOrganizationInvitesCommand>()
.Received(1)
.BulkResendInvitesAsync(organizationId, userId, bulkRequestModel.Ids);
}
}