1
0
mirror of https://github.com/bitwarden/server.git synced 2025-04-12 16:48:12 -05:00

[pm-15621] Refactor DeleteManagedOrganizationUserAccountCommand

This commit is contained in:
Jimmy Vo 2025-01-29 17:13:56 -05:00
parent 2f2ef20c74
commit bf5f8f7d13
No known key found for this signature in database
GPG Key ID: 7CB834D6F4FFCA11
2 changed files with 29 additions and 269 deletions

View File

@ -26,7 +26,6 @@ public class DeleteManagedOrganizationUserAccountCommand : IDeleteManagedOrganiz
private readonly IHasConfirmedOwnersExceptQuery _hasConfirmedOwnersExceptQuery;
private readonly IReferenceEventService _referenceEventService;
private readonly IPushNotificationService _pushService;
private readonly IOrganizationRepository _organizationRepository;
private readonly IProviderUserRepository _providerUserRepository;
public DeleteManagedOrganizationUserAccountCommand(
IUserService userService,
@ -38,8 +37,8 @@ public class DeleteManagedOrganizationUserAccountCommand : IDeleteManagedOrganiz
IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery,
IReferenceEventService referenceEventService,
IPushNotificationService pushService,
IOrganizationRepository organizationRepository,
IProviderUserRepository providerUserRepository)
IProviderUserRepository providerUserRepository
)
{
_userService = userService;
_eventService = eventService;
@ -50,31 +49,12 @@ public class DeleteManagedOrganizationUserAccountCommand : IDeleteManagedOrganiz
_hasConfirmedOwnersExceptQuery = hasConfirmedOwnersExceptQuery;
_referenceEventService = referenceEventService;
_pushService = pushService;
_organizationRepository = organizationRepository;
_providerUserRepository = providerUserRepository;
}
public async Task DeleteUserAsync(Guid organizationId, Guid organizationUserId, Guid? deletingUserId)
{
var organizationUser = await _organizationUserRepository.GetByIdAsync(organizationUserId);
if (organizationUser == null || organizationUser.OrganizationId != organizationId)
{
throw new NotFoundException("Member not found.");
}
var managementStatus = await _getOrganizationUsersManagementStatusQuery.GetUsersOrganizationManagementStatusAsync(organizationId, new[] { organizationUserId });
var hasOtherConfirmedOwners = await _hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync(organizationId, new[] { organizationUserId }, includeProvider: true);
await ValidateDeleteUserAsync(organizationId, organizationUser, deletingUserId, managementStatus, hasOtherConfirmedOwners);
var user = await _userRepository.GetByIdAsync(organizationUser.UserId!.Value);
if (user == null)
{
throw new NotFoundException("Member not found.");
}
await _userService.DeleteAsync(user);
await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Deleted);
await DeleteManyUsersAsync(organizationId, new[] { organizationUserId }, deletingUserId);
}
public async Task<IEnumerable<(Guid OrganizationUserId, string? ErrorMessage)>> DeleteManyUsersAsync(Guid organizationId, IEnumerable<Guid> orgUserIds, Guid? deletingUserId)
@ -105,7 +85,8 @@ public class DeleteManagedOrganizationUserAccountCommand : IDeleteManagedOrganiz
throw new NotFoundException("Member not found.");
}
await ValidateUserMembershipAndPremiumAsync(user);
await ValidateUserAsync(user);
await CancelPremiumAsync(user);
results.Add((orgUserId, string.Empty));
}
@ -197,46 +178,42 @@ public class DeleteManagedOrganizationUserAccountCommand : IDeleteManagedOrganiz
}
private async Task ValidateUserMembershipAndPremiumAsync(User user)
private async Task ValidateUserAsync(User user)
{
await EnsureUserIsNotSoleOrganizationOwnerAsync(user);
await EnsureUserIsNotSoleProviderOwnerAsync(user);
}
private async Task CancelPremiumAsync(User user)
{
if (string.IsNullOrWhiteSpace(user.GatewaySubscriptionId))
{
return;
}
try
{
await _userService.CancelPremiumAsync(user);
}
catch (GatewayException) { }
}
private async Task EnsureUserIsNotSoleOrganizationOwnerAsync(User user)
{
// Check if user is the only owner of any organizations.
var onlyOwnerCount = await _organizationUserRepository.GetCountByOnlyOwnerAsync(user.Id);
if (onlyOwnerCount > 0)
{
throw new BadRequestException("Cannot delete this user because it is the sole owner of at least one organization. Please delete these organizations or upgrade another user.");
}
var orgs = await _organizationUserRepository.GetManyDetailsByUserAsync(user.Id, OrganizationUserStatusType.Confirmed);
if (orgs.Count == 1)
{
var org = await _organizationRepository.GetByIdAsync(orgs.First().OrganizationId);
if (org != null && (!org.Enabled || string.IsNullOrWhiteSpace(org.GatewaySubscriptionId)))
{
var orgCount = await _organizationUserRepository.GetCountByOrganizationIdAsync(org.Id);
if (orgCount <= 1)
{
await _organizationRepository.DeleteAsync(org);
}
else
{
throw new BadRequestException("Cannot delete this user because it is the sole owner of at least one organization. Please delete these organizations or upgrade another user.");
}
}
}
}
private async Task EnsureUserIsNotSoleProviderOwnerAsync(User user)
{
var onlyOwnerProviderCount = await _providerUserRepository.GetCountByOnlyOwnerAsync(user.Id);
if (onlyOwnerProviderCount > 0)
{
throw new BadRequestException("Cannot delete this user because it is the sole owner of at least one provider. Please delete these providers or upgrade another user.");
}
if (!string.IsNullOrWhiteSpace(user.GatewaySubscriptionId))
{
try
{
await _userService.CancelPremiumAsync(user);
}
catch (GatewayException) { }
}
}
}

View File

@ -3,7 +3,6 @@ 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;
@ -17,222 +16,6 @@ namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers;
[SutProviderCustomize]
public class DeleteManagedOrganizationUserAccountCommandTests
{
[Theory]
[BitAutoData]
public async Task DeleteUserAsync_WithValidUser_DeletesUserAndLogsEvent(
SutProvider<DeleteManagedOrganizationUserAccountCommand> 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<IGetOrganizationUsersManagementStatusQuery>()
.GetUsersOrganizationManagementStatusAsync(
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<DeleteManagedOrganizationUserAccountCommand> 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<DeleteManagedOrganizationUserAccountCommand> 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<DeleteManagedOrganizationUserAccountCommand> 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_DeletingOwnerWhenNotOwner_ThrowsException(
SutProvider<DeleteManagedOrganizationUserAccountCommand> 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<DeleteManagedOrganizationUserAccountCommand> 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<DeleteManagedOrganizationUserAccountCommand> 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<IGetOrganizationUsersManagementStatusQuery>()
.GetUsersOrganizationManagementStatusAsync(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 managed 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(