mirror of
https://github.com/bitwarden/server.git
synced 2025-04-04 20:50:21 -05:00
[PM-15621] WIP – this is a broken build.
This commit is contained in:
parent
67df34e784
commit
c08b3d99ab
@ -594,7 +594,7 @@ public class OrganizationUsersController : Controller
|
|||||||
|
|
||||||
var deletionResult = await _deleteManagedOrganizationUserAccountCommand.DeleteUserAsync(orgId, id, currentUser.Id);
|
var deletionResult = await _deleteManagedOrganizationUserAccountCommand.DeleteUserAsync(orgId, id, currentUser.Id);
|
||||||
|
|
||||||
return deletionResult.result.MapToActionResult();
|
return deletionResult.MapToActionResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
[RequireFeature(FeatureFlagKeys.AccountDeprovisioning)]
|
[RequireFeature(FeatureFlagKeys.AccountDeprovisioning)]
|
||||||
|
7
src/Core/AdminConsole/Errors/BadRequestError.cs
Normal file
7
src/Core/AdminConsole/Errors/BadRequestError.cs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
namespace Bit.Core.AdminConsole.Errors;
|
||||||
|
|
||||||
|
public record BadRequestError<T> : Error<T>
|
||||||
|
{
|
||||||
|
public BadRequestError(string code, T invalidRequest)
|
||||||
|
: base(code, invalidRequest) { }
|
||||||
|
}
|
@ -1,3 +1,8 @@
|
|||||||
namespace Bit.Core.AdminConsole.Errors;
|
namespace Bit.Core.AdminConsole.Errors;
|
||||||
|
|
||||||
public record Error<T>(string Message, T ErroredValue);
|
public record Error<T>(string Message, T ErroredValue);
|
||||||
|
|
||||||
|
public static class ErrorMappers
|
||||||
|
{
|
||||||
|
public static Error<B> ToError<A, B>(this Error<A> errorA, B erroredValue) => new(errorA.Message, erroredValue);
|
||||||
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Shared.Validation;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
@ -11,103 +11,108 @@ using Bit.Core.Services;
|
|||||||
using Bit.Core.Tools.Enums;
|
using Bit.Core.Tools.Enums;
|
||||||
using Bit.Core.Tools.Models.Business;
|
using Bit.Core.Tools.Models.Business;
|
||||||
using Bit.Core.Tools.Services;
|
using Bit.Core.Tools.Services;
|
||||||
using Bit.Core.Validators;
|
|
||||||
|
|
||||||
|
|
||||||
#nullable enable
|
|
||||||
|
|
||||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers;
|
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers;
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
public class DeleteManagedOrganizationUserAccountCommand : IDeleteManagedOrganizationUserAccountCommand
|
public class DeleteManagedOrganizationUserAccountCommand : IDeleteManagedOrganizationUserAccountCommand
|
||||||
{
|
{
|
||||||
private readonly IUserService _userService;
|
private readonly IUserService _userService;
|
||||||
private readonly IEventService _eventService;
|
private readonly IEventService _eventService;
|
||||||
|
private readonly IDeleteManagedOrganizationUserAccountValidator _deleteManagedOrganizationUserAccountValidator;
|
||||||
private readonly IGetOrganizationUsersManagementStatusQuery _getOrganizationUsersManagementStatusQuery;
|
private readonly IGetOrganizationUsersManagementStatusQuery _getOrganizationUsersManagementStatusQuery;
|
||||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||||
private readonly IUserRepository _userRepository;
|
private readonly IUserRepository _userRepository;
|
||||||
private readonly ICurrentContext _currentContext;
|
private readonly ICurrentContext _currentContext;
|
||||||
private readonly IReferenceEventService _referenceEventService;
|
private readonly IReferenceEventService _referenceEventService;
|
||||||
private readonly IPushNotificationService _pushService;
|
private readonly IPushNotificationService _pushService;
|
||||||
private readonly IProviderUserRepository _providerUserRepository;
|
|
||||||
public DeleteManagedOrganizationUserAccountCommand(
|
public DeleteManagedOrganizationUserAccountCommand(
|
||||||
IUserService userService,
|
IUserService userService,
|
||||||
IEventService eventService,
|
IEventService eventService,
|
||||||
|
IDeleteManagedOrganizationUserAccountValidator deleteManagedOrganizationUserAccountValidator,
|
||||||
IGetOrganizationUsersManagementStatusQuery getOrganizationUsersManagementStatusQuery,
|
IGetOrganizationUsersManagementStatusQuery getOrganizationUsersManagementStatusQuery,
|
||||||
IOrganizationUserRepository organizationUserRepository,
|
IOrganizationUserRepository organizationUserRepository,
|
||||||
IUserRepository userRepository,
|
IUserRepository userRepository,
|
||||||
ICurrentContext currentContext,
|
ICurrentContext currentContext,
|
||||||
IReferenceEventService referenceEventService,
|
IReferenceEventService referenceEventService,
|
||||||
IPushNotificationService pushService,
|
IPushNotificationService pushService)
|
||||||
IProviderUserRepository providerUserRepository
|
|
||||||
)
|
|
||||||
{
|
{
|
||||||
_userService = userService;
|
_userService = userService;
|
||||||
_eventService = eventService;
|
_eventService = eventService;
|
||||||
|
_deleteManagedOrganizationUserAccountValidator = deleteManagedOrganizationUserAccountValidator;
|
||||||
_getOrganizationUsersManagementStatusQuery = getOrganizationUsersManagementStatusQuery;
|
_getOrganizationUsersManagementStatusQuery = getOrganizationUsersManagementStatusQuery;
|
||||||
_organizationUserRepository = organizationUserRepository;
|
_organizationUserRepository = organizationUserRepository;
|
||||||
_userRepository = userRepository;
|
_userRepository = userRepository;
|
||||||
_currentContext = currentContext;
|
_currentContext = currentContext;
|
||||||
_referenceEventService = referenceEventService;
|
_referenceEventService = referenceEventService;
|
||||||
_pushService = pushService;
|
_pushService = pushService;
|
||||||
_providerUserRepository = providerUserRepository;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<(Guid OrganizationUserId, CommandResult result)> DeleteUserAsync(Guid organizationId, Guid organizationUserId, Guid? deletingUserId)
|
public async Task<CommandResult> DeleteUserAsync(Guid organizationId, Guid organizationUserId, Guid? deletingUserId)
|
||||||
{
|
{
|
||||||
var result = await InternalDeleteManyUsersAsync(organizationId, new[] { organizationUserId }, deletingUserId);
|
var result = await InternalDeleteManyUsersAsync(organizationId, new[] { organizationUserId }, deletingUserId);
|
||||||
|
|
||||||
return result.FirstOrDefault();
|
|
||||||
|
if (result.InvalidResults.Count > 0)
|
||||||
|
{
|
||||||
|
|
||||||
|
var error = result.InvalidResults.FirstOrDefault()?.Errors.FirstOrDefault();
|
||||||
|
|
||||||
|
return new Failure();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Success();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<(Guid OrganizationUserId, CommandResult result)>> DeleteManyUsersAsync(Guid organizationId, IEnumerable<Guid> orgUserIds, Guid? deletingUserId)
|
public async Task<IEnumerable<(Guid OrganizationUserId, CommandResult result)>> DeleteManyUsersAsync(Guid organizationId, IEnumerable<Guid> orgUserIds, Guid? deletingUserId)
|
||||||
{
|
{
|
||||||
return await InternalDeleteManyUsersAsync(organizationId, orgUserIds, deletingUserId);
|
var results = await InternalDeleteManyUsersAsync(organizationId, orgUserIds, deletingUserId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<IEnumerable<(Guid OrganizationUserId, CommandResult result)>> InternalDeleteManyUsersAsync(Guid organizationId, IEnumerable<Guid> orgUserIds, Guid? deletingUserId)
|
private async Task<PartialValidationResult<DeleteUserValidationRequest>> InternalDeleteManyUsersAsync(Guid organizationId, IEnumerable<Guid> orgUserIds, Guid? deletingUserId)
|
||||||
{
|
{
|
||||||
var orgUsers = await _organizationUserRepository.GetManyAsync(orgUserIds);
|
var orgUsers = await _organizationUserRepository.GetManyAsync(orgUserIds);
|
||||||
var users = await GetUsersAsync(orgUsers);
|
var users = await GetUsersAsync(orgUsers);
|
||||||
var managementStatus = await _getOrganizationUsersManagementStatusQuery.GetUsersOrganizationManagementStatusAsync(organizationId, orgUserIds);
|
var managementStatuses = await _getOrganizationUsersManagementStatusQuery.GetUsersOrganizationManagementStatusAsync(organizationId, orgUserIds);
|
||||||
|
|
||||||
var userDeletionRequests = new List<(Guid OrganizationUserId, CommandResult result, OrganizationUser? orgUser, User? user)>();
|
var requests = CreateRequests(organizationId, deletingUserId, orgUserIds, orgUsers, users, managementStatuses);
|
||||||
|
var validationResults = await _deleteManagedOrganizationUserAccountValidator.ValidateAsync(requests);
|
||||||
|
|
||||||
|
await CancelPremiumsAsync(validationResults.ValidResults);
|
||||||
|
await HandleUserDeletionsAsync(validationResults.ValidResults);
|
||||||
|
await LogDeletedOrganizationUsersAsync(validationResults.ValidResults);
|
||||||
|
|
||||||
|
return validationResults;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<DeleteUserValidationRequest> CreateRequests(
|
||||||
|
Guid organizationId,
|
||||||
|
Guid? deletingUserId,
|
||||||
|
IEnumerable<Guid> orgUserIds,
|
||||||
|
ICollection<OrganizationUser> orgUsers,
|
||||||
|
IEnumerable<User> users,
|
||||||
|
IDictionary<Guid, bool> managementStatuses)
|
||||||
|
{
|
||||||
|
var requests = new List<DeleteUserValidationRequest>();
|
||||||
foreach (var orgUserId in orgUserIds)
|
foreach (var orgUserId in orgUserIds)
|
||||||
{
|
{
|
||||||
var orgUser = orgUsers.FirstOrDefault(ou => ou.Id == orgUserId);
|
var orgUser = orgUsers.FirstOrDefault(orgUser => orgUser.Id == orgUserId);
|
||||||
if (orgUser == null || orgUser.OrganizationId != organizationId)
|
var user = users.FirstOrDefault(user => user.Id == orgUser?.UserId);
|
||||||
|
managementStatuses.TryGetValue(orgUserId, out var isManaged);
|
||||||
|
|
||||||
|
requests.Add(new DeleteUserValidationRequest
|
||||||
{
|
{
|
||||||
userDeletionRequests.Add((orgUserId, new BadRequestFailure("Member not found."), null, null));
|
User = user,
|
||||||
continue;
|
OrganizationUser = orgUser,
|
||||||
}
|
IsManaged = isManaged,
|
||||||
|
OrganizationId = organizationId,
|
||||||
var user = users.FirstOrDefault(u => u.Id == orgUser.UserId);
|
DeletingUserId = deletingUserId,
|
||||||
|
});
|
||||||
if (user == null)
|
|
||||||
{
|
|
||||||
userDeletionRequests.Add((orgUserId, new BadRequestFailure("Member not found."), orgUser, null));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var result = await ValidateAsync(organizationId, orgUser, user, deletingUserId, managementStatus);
|
|
||||||
if (result is not Success)
|
|
||||||
{
|
|
||||||
userDeletionRequests.Add((orgUserId, result, orgUser, user));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
await CancelPremiumAsync(user);
|
|
||||||
|
|
||||||
userDeletionRequests.Add((orgUserId, new Success(), orgUser, user));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await HandleUserDeletionsAsync(userDeletionRequests);
|
return requests;
|
||||||
|
|
||||||
await LogDeletedOrganizationUsersAsync(userDeletionRequests);
|
|
||||||
|
|
||||||
return userDeletionRequests
|
|
||||||
.Select(request => (request.OrganizationUserId, request.result))
|
|
||||||
.ToList();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<IEnumerable<User>> GetUsersAsync(ICollection<OrganizationUser> orgUsers)
|
private async Task<IEnumerable<User>> GetUsersAsync(ICollection<OrganizationUser> orgUsers)
|
||||||
@ -120,112 +125,12 @@ public class DeleteManagedOrganizationUserAccountCommand : IDeleteManagedOrganiz
|
|||||||
return await _userRepository.GetManyAsync(userIds);
|
return await _userRepository.GetManyAsync(userIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<CommandResult> ValidateAsync(Guid organizationId, OrganizationUser orgUser, User user, Guid? deletingUserId, IDictionary<Guid, bool> managementStatus)
|
private async Task LogDeletedOrganizationUsersAsync(List<Valid<DeleteUserValidationRequest>> requests)
|
||||||
{
|
|
||||||
var validators = new[]
|
|
||||||
{
|
|
||||||
() => EnsureUserStatusIsNotInvited(orgUser),
|
|
||||||
() => PreventSelfDeletion(orgUser, deletingUserId),
|
|
||||||
() => EnsureUserIsManagedByOrganization(orgUser, managementStatus),
|
|
||||||
};
|
|
||||||
var result = CommandResultValidator.ExecuteValidators(validators);
|
|
||||||
|
|
||||||
if (result is not Success)
|
|
||||||
{
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
var asyncValidators = new[]
|
|
||||||
{
|
|
||||||
async () => await EnsureOnlyOwnersCanDeleteOwnersAsync(organizationId, orgUser, deletingUserId),
|
|
||||||
async () => await EnsureUserIsNotSoleOrganizationOwnerAsync(user),
|
|
||||||
async () => await EnsureUserIsNotSoleProviderOwnerAsync(user)
|
|
||||||
};
|
|
||||||
var asyncResult = await CommandResultValidator.ExecuteValidatorAsync(asyncValidators);
|
|
||||||
|
|
||||||
if (asyncResult is not Success)
|
|
||||||
{
|
|
||||||
return asyncResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Success();
|
|
||||||
}
|
|
||||||
private static CommandResult EnsureUserStatusIsNotInvited(OrganizationUser orgUser)
|
|
||||||
{
|
|
||||||
if (!orgUser.UserId.HasValue || orgUser.Status == OrganizationUserStatusType.Invited)
|
|
||||||
{
|
|
||||||
return new BadRequestFailure("You cannot delete a member with Invited status.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Success();
|
|
||||||
}
|
|
||||||
private static CommandResult PreventSelfDeletion(OrganizationUser orgUser, Guid? deletingUserId)
|
|
||||||
{
|
|
||||||
if (!orgUser.UserId.HasValue || !deletingUserId.HasValue)
|
|
||||||
{
|
|
||||||
return new Success();
|
|
||||||
}
|
|
||||||
if (orgUser.UserId.Value == deletingUserId.Value)
|
|
||||||
{
|
|
||||||
return new BadRequestFailure("You cannot delete yourself.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Success();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<CommandResult> EnsureOnlyOwnersCanDeleteOwnersAsync(Guid organizationId, OrganizationUser orgUser, Guid? deletingUserId)
|
|
||||||
{
|
|
||||||
if (orgUser.Type != OrganizationUserType.Owner)
|
|
||||||
{
|
|
||||||
return new Success();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (deletingUserId.HasValue && !await _currentContext.OrganizationOwner(organizationId))
|
|
||||||
{
|
|
||||||
return new BadRequestFailure("Only owners can delete other owners.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Success();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static CommandResult EnsureUserIsManagedByOrganization(OrganizationUser orgUser, IDictionary<Guid, bool> managementStatus)
|
|
||||||
{
|
|
||||||
if (!managementStatus.TryGetValue(orgUser.Id, out var isManaged) || !isManaged)
|
|
||||||
{
|
|
||||||
return new BadRequestFailure("Member is not managed by the organization.");
|
|
||||||
}
|
|
||||||
return new Success();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<CommandResult> EnsureUserIsNotSoleOrganizationOwnerAsync(User user)
|
|
||||||
{
|
|
||||||
var onlyOwnerCount = await _organizationUserRepository.GetCountByOnlyOwnerAsync(user.Id);
|
|
||||||
if (onlyOwnerCount > 0)
|
|
||||||
{
|
|
||||||
return new BadRequestFailure("Cannot delete this user because it is the sole owner of at least one organization. Please delete these organizations or upgrade another user.");
|
|
||||||
}
|
|
||||||
return new Success();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<CommandResult> EnsureUserIsNotSoleProviderOwnerAsync(User user)
|
|
||||||
{
|
|
||||||
var onlyOwnerProviderCount = await _providerUserRepository.GetCountByOnlyOwnerAsync(user.Id);
|
|
||||||
if (onlyOwnerProviderCount > 0)
|
|
||||||
{
|
|
||||||
return new BadRequestFailure("Cannot delete this user because it is the sole owner of at least one provider. Please delete these providers or upgrade another user.");
|
|
||||||
}
|
|
||||||
return new Success();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task LogDeletedOrganizationUsersAsync(
|
|
||||||
List<(Guid OrganizationUserId, CommandResult result, OrganizationUser? orgUser, User? user)> userDeletionRequests)
|
|
||||||
{
|
{
|
||||||
var eventDate = DateTime.UtcNow;
|
var eventDate = DateTime.UtcNow;
|
||||||
|
|
||||||
var events = userDeletionRequests
|
var events = requests
|
||||||
.Where(request =>
|
.Select(request => (request.Value.OrganizationUser!, (EventType)EventType.OrganizationUser_Deleted, (DateTime?)eventDate))
|
||||||
request.result is Success)
|
|
||||||
.Select(request => (request.orgUser!, (EventType)EventType.OrganizationUser_Deleted, (DateTime?)eventDate))
|
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
if (events.Any())
|
if (events.Any())
|
||||||
@ -234,16 +139,15 @@ public class DeleteManagedOrganizationUserAccountCommand : IDeleteManagedOrganiz
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task HandleUserDeletionsAsync(List<(Guid OrganizationUserId, CommandResult result, OrganizationUser? orgUser, User? user)> userDeletionRequests)
|
|
||||||
{
|
|
||||||
var usersToDelete = userDeletionRequests
|
|
||||||
.Where(request =>
|
|
||||||
request.result is Success)
|
|
||||||
.Select(request => request.user!);
|
|
||||||
|
|
||||||
if (usersToDelete.Any())
|
private async Task HandleUserDeletionsAsync(List<Valid<DeleteUserValidationRequest>> requests)
|
||||||
|
{
|
||||||
|
var users = requests
|
||||||
|
.Select(request => request.Value.User!);
|
||||||
|
|
||||||
|
if (users.Any())
|
||||||
{
|
{
|
||||||
await DeleteManyAsync(usersToDelete);
|
await DeleteManyAsync(users);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -259,16 +163,23 @@ public class DeleteManagedOrganizationUserAccountCommand : IDeleteManagedOrganiz
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task CancelPremiumAsync(User user)
|
private async Task CancelPremiumsAsync(List<Valid<DeleteUserValidationRequest>> requests)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(user.GatewaySubscriptionId))
|
var users = requests
|
||||||
|
.Select(request => request.Value.User!);
|
||||||
|
|
||||||
|
foreach (var user in users)
|
||||||
{
|
{
|
||||||
return;
|
try
|
||||||
|
{
|
||||||
|
await _userService.CancelPremiumAsync(user);
|
||||||
|
}
|
||||||
|
catch (GatewayException)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
try
|
|
||||||
{
|
|
||||||
await _userService.CancelPremiumAsync(user);
|
|
||||||
}
|
|
||||||
catch (GatewayException) { }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,15 @@
|
|||||||
|
using Bit.Core.Entities;
|
||||||
|
|
||||||
|
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers;
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
public class DeleteUserValidationRequest
|
||||||
|
{
|
||||||
|
public Guid OrganizationId { get; init; }
|
||||||
|
public OrganizationUser? OrganizationUser { get; init; }
|
||||||
|
public User? User { get; init; }
|
||||||
|
public Guid? DeletingUserId { get; init; }
|
||||||
|
|
||||||
|
public IDictionary<Guid, bool>? ManagementStatus { get; init; }
|
||||||
|
public bool? IsManaged { get; init; }
|
||||||
|
}
|
@ -9,7 +9,8 @@ public interface IDeleteManagedOrganizationUserAccountCommand
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Removes a user from an organization and deletes all of their associated user data.
|
/// Removes a user from an organization and deletes all of their associated user data.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Task<(Guid OrganizationUserId, CommandResult result)> DeleteUserAsync(Guid organizationId, Guid organizationUserId, Guid? deletingUserId);
|
/// Jimmy temporary comment: consider removing the nullable from deletingUserId.
|
||||||
|
Task<CommandResult> DeleteUserAsync(Guid organizationId, Guid organizationUserId, Guid? deletingUserId);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Removes multiple users from an organization and deletes all of their associated user data.
|
/// Removes multiple users from an organization and deletes all of their associated user data.
|
||||||
@ -17,5 +18,6 @@ public interface IDeleteManagedOrganizationUserAccountCommand
|
|||||||
/// <returns>
|
/// <returns>
|
||||||
/// An error message for each user that could not be removed, otherwise null.
|
/// An error message for each user that could not be removed, otherwise null.
|
||||||
/// </returns>
|
/// </returns>
|
||||||
|
/// Jimmy temporary comment: consider removing the nullable from deletingUserId.
|
||||||
Task<IEnumerable<(Guid OrganizationUserId, CommandResult result)>> DeleteManyUsersAsync(Guid organizationId, IEnumerable<Guid> orgUserIds, Guid? deletingUserId);
|
Task<IEnumerable<(Guid OrganizationUserId, CommandResult result)>> DeleteManyUsersAsync(Guid organizationId, IEnumerable<Guid> orgUserIds, Guid? deletingUserId);
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,8 @@
|
|||||||
|
using Bit.Core.AdminConsole.Shared.Validation;
|
||||||
|
|
||||||
|
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||||
|
|
||||||
|
public interface IDeleteManagedOrganizationUserAccountValidator
|
||||||
|
{
|
||||||
|
Task<PartialValidationResult<DeleteUserValidationRequest>> ValidateAsync(List<DeleteUserValidationRequest> requests);
|
||||||
|
}
|
@ -0,0 +1,160 @@
|
|||||||
|
using Bit.Core.AdminConsole.Errors;
|
||||||
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||||
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
|
using Bit.Core.AdminConsole.Shared.Validation;
|
||||||
|
using Bit.Core.Context;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
|
||||||
|
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RestoreUser;
|
||||||
|
|
||||||
|
public class DeleteManagedOrganizationUserAccountValidator(
|
||||||
|
ICurrentContext currentContext,
|
||||||
|
IOrganizationUserRepository organizationUserRepository,
|
||||||
|
IProviderUserRepository providerUserRepository) : IDeleteManagedOrganizationUserAccountValidator
|
||||||
|
{
|
||||||
|
public async Task<PartialValidationResult<DeleteUserValidationRequest>> ValidateAsync(List<DeleteUserValidationRequest> requests)
|
||||||
|
{
|
||||||
|
var invalidResults = new List<Invalid<DeleteUserValidationRequest>>();
|
||||||
|
var validResults = new List<Valid<DeleteUserValidationRequest>>();
|
||||||
|
foreach (var request in requests)
|
||||||
|
{
|
||||||
|
// The order of the validators matters.
|
||||||
|
// Earlier validators assert nullable properties so that later validators don’t have to.
|
||||||
|
var validators = new[]
|
||||||
|
{
|
||||||
|
EnsureUserBelongsToOrganization,
|
||||||
|
EnsureUserStatusIsNotInvited,
|
||||||
|
PreventSelfDeletion,
|
||||||
|
EnsureUserIsManagedByOrganization
|
||||||
|
};
|
||||||
|
|
||||||
|
var asyncValidators = new[]
|
||||||
|
{
|
||||||
|
EnsureOnlyOwnersCanDeleteOwnersAsync,
|
||||||
|
EnsureUserIsNotSoleOrganizationOwnerAsync,
|
||||||
|
EnsureUserIsNotSoleProviderOwnerAsync
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await ExecuteValidatorsAsync(validators, asyncValidators, request);
|
||||||
|
|
||||||
|
if (result is Valid<DeleteUserValidationRequest> valid)
|
||||||
|
{
|
||||||
|
validResults.Add(valid);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
invalidResults.Add((Invalid<DeleteUserValidationRequest>)result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new PartialValidationResult<DeleteUserValidationRequest>()
|
||||||
|
{
|
||||||
|
InvalidResults = invalidResults,
|
||||||
|
ValidResults = validResults
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<ValidationResult<DeleteUserValidationRequest>> ExecuteValidatorsAsync(
|
||||||
|
Func<DeleteUserValidationRequest, ValidationResult<DeleteUserValidationRequest>>[] validators,
|
||||||
|
Func<DeleteUserValidationRequest, Task<ValidationResult<DeleteUserValidationRequest>>>[] asyncValidators,
|
||||||
|
DeleteUserValidationRequest request)
|
||||||
|
{
|
||||||
|
foreach (var validator in validators)
|
||||||
|
{
|
||||||
|
var result = validator(request);
|
||||||
|
|
||||||
|
if (result is Invalid<DeleteUserValidationRequest>)
|
||||||
|
{
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var asyncValidator in asyncValidators)
|
||||||
|
{
|
||||||
|
var result = await asyncValidator(request);
|
||||||
|
|
||||||
|
if (result is Invalid<DeleteUserValidationRequest>)
|
||||||
|
{
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Valid<DeleteUserValidationRequest>(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ValidationResult<DeleteUserValidationRequest> EnsureUserBelongsToOrganization(DeleteUserValidationRequest request)
|
||||||
|
{
|
||||||
|
if (request.User == null || request.OrganizationUser == null)
|
||||||
|
{
|
||||||
|
return new Invalid<DeleteUserValidationRequest>(new BadRequestError<DeleteUserValidationRequest>("You cannot delete a member with Invited status.", request));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Valid<DeleteUserValidationRequest>();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ValidationResult<DeleteUserValidationRequest> EnsureUserIsManagedByOrganization(DeleteUserValidationRequest request)
|
||||||
|
{
|
||||||
|
if (request.IsManaged == true)
|
||||||
|
{
|
||||||
|
return new Valid<DeleteUserValidationRequest>();
|
||||||
|
}
|
||||||
|
return new Invalid<DeleteUserValidationRequest>(new BadRequestError<DeleteUserValidationRequest>("Member is not managed by the organization.", request));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ValidationResult<DeleteUserValidationRequest> EnsureUserStatusIsNotInvited(DeleteUserValidationRequest request)
|
||||||
|
{
|
||||||
|
if (request.OrganizationUser!.Status == OrganizationUserStatusType.Invited)
|
||||||
|
{
|
||||||
|
return new Invalid<DeleteUserValidationRequest>(new BadRequestError<DeleteUserValidationRequest>("You cannot delete a member with Invited status.", request));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Valid<DeleteUserValidationRequest>();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ValidationResult<DeleteUserValidationRequest> PreventSelfDeletion(DeleteUserValidationRequest request)
|
||||||
|
{
|
||||||
|
if (request.OrganizationUser?.UserId == request.DeletingUserId)
|
||||||
|
{
|
||||||
|
return new Invalid<DeleteUserValidationRequest>(new BadRequestError<DeleteUserValidationRequest>("You cannot delete yourself.", request));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Valid<DeleteUserValidationRequest>(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<ValidationResult<DeleteUserValidationRequest>> EnsureOnlyOwnersCanDeleteOwnersAsync(DeleteUserValidationRequest request)
|
||||||
|
{
|
||||||
|
if (request.OrganizationUser?.Type != OrganizationUserType.Owner)
|
||||||
|
{
|
||||||
|
return new Valid<DeleteUserValidationRequest>(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.DeletingUserId.HasValue && !await currentContext.OrganizationOwner(request.OrganizationId))
|
||||||
|
{
|
||||||
|
return new Invalid<DeleteUserValidationRequest>(new BadRequestError<DeleteUserValidationRequest>("Only owners can delete other owners.", request));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Valid<DeleteUserValidationRequest>(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<ValidationResult<DeleteUserValidationRequest>> EnsureUserIsNotSoleOrganizationOwnerAsync(DeleteUserValidationRequest request)
|
||||||
|
{
|
||||||
|
var onlyOwnerCount = await organizationUserRepository.GetCountByOnlyOwnerAsync(request.User!.Id);
|
||||||
|
if (onlyOwnerCount > 0)
|
||||||
|
{
|
||||||
|
return new Invalid<DeleteUserValidationRequest>(new BadRequestError<DeleteUserValidationRequest>("Cannot delete this user because it is the sole owner of at least one organization. Please delete these organizations or upgrade another user.", request));
|
||||||
|
}
|
||||||
|
return new Valid<DeleteUserValidationRequest>(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<ValidationResult<DeleteUserValidationRequest>> EnsureUserIsNotSoleProviderOwnerAsync(DeleteUserValidationRequest request)
|
||||||
|
{
|
||||||
|
var onlyOwnerProviderCount = await providerUserRepository.GetCountByOnlyOwnerAsync(request.User!.Id);
|
||||||
|
if (onlyOwnerProviderCount > 0)
|
||||||
|
{
|
||||||
|
return new Invalid<DeleteUserValidationRequest>(new BadRequestError<DeleteUserValidationRequest>("Cannot delete this user because it is the sole owner of at least one provider. Please delete these providers or upgrade another user.", request));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Valid<DeleteUserValidationRequest>(request);
|
||||||
|
}
|
||||||
|
}
|
@ -6,10 +6,35 @@ public abstract record ValidationResult<T>;
|
|||||||
|
|
||||||
public record Valid<T> : ValidationResult<T>
|
public record Valid<T> : ValidationResult<T>
|
||||||
{
|
{
|
||||||
|
public Valid() { }
|
||||||
|
|
||||||
|
public Valid(T Value)
|
||||||
|
{
|
||||||
|
this.Value = Value;
|
||||||
|
}
|
||||||
|
|
||||||
public T Value { get; init; }
|
public T Value { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public record PartialValidationResult<T>
|
||||||
|
{
|
||||||
|
public List<Invalid<T>> InvalidResults { get; init; }
|
||||||
|
|
||||||
|
public List<Valid<T>> ValidResults { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
public record Invalid<T> : ValidationResult<T>
|
public record Invalid<T> : ValidationResult<T>
|
||||||
{
|
{
|
||||||
public IEnumerable<Error<T>> Errors { get; init; }
|
public IEnumerable<Error<T>> Errors { get; init; } = [];
|
||||||
|
|
||||||
|
public string ErrorMessageString => string.Join(" ", Errors.Select(e => e.Message));
|
||||||
|
|
||||||
|
public Invalid() { }
|
||||||
|
|
||||||
|
public Invalid(Error<T> error) : this([error]) { }
|
||||||
|
|
||||||
|
public Invalid(IEnumerable<Error<T>> errors)
|
||||||
|
{
|
||||||
|
Errors = errors;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,22 @@
|
|||||||
|
namespace Bit.Core.AdminConsole.Shared.Validation;
|
||||||
|
|
||||||
|
public static class ValidationResultMappers
|
||||||
|
{
|
||||||
|
// public static Failure MapToFailure<T>(this Error<T> error)
|
||||||
|
// {
|
||||||
|
//
|
||||||
|
// return error switch
|
||||||
|
// {
|
||||||
|
// BadRequestError<T> badRequestError => new Failure(badRequestError.Message),
|
||||||
|
// _ => throw new InvalidOperationException($"Unhandled commandResult type: {error.GetType().Name}")
|
||||||
|
// }
|
||||||
|
// // return commandResult switch
|
||||||
|
// // {
|
||||||
|
// // NoRecordFoundFailure<T> failure => new ObjectResult(failure.ErrorMessages) { StatusCode = StatusCodes.Status404NotFound },
|
||||||
|
// // BadRequestFailure<T> failure => new ObjectResult(failure.ErrorMessages) { StatusCode = StatusCodes.Status400BadRequest },
|
||||||
|
// // Failure<T> failure => new ObjectResult(failure.ErrorMessages) { StatusCode = StatusCodes.Status400BadRequest },
|
||||||
|
// // Success<T> success => new ObjectResult(success.Value) { StatusCode = StatusCodes.Status200OK },
|
||||||
|
// // _ => throw new InvalidOperationException($"Unhandled commandResult type: {commandResult.GetType().Name}")
|
||||||
|
// // };
|
||||||
|
// }
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user