1
0
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:
Jimmy Vo 2025-04-03 17:41:04 -04:00
parent 67df34e784
commit c08b3d99ab
No known key found for this signature in database
GPG Key ID: 7CB834D6F4FFCA11
10 changed files with 323 additions and 168 deletions

View File

@ -594,7 +594,7 @@ public class OrganizationUsersController : Controller
var deletionResult = await _deleteManagedOrganizationUserAccountCommand.DeleteUserAsync(orgId, id, currentUser.Id);
return deletionResult.result.MapToActionResult();
return deletionResult.MapToActionResult();
}
[RequireFeature(FeatureFlagKeys.AccountDeprovisioning)]

View 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) { }
}

View File

@ -1,3 +1,8 @@
namespace Bit.Core.AdminConsole.Errors;
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);
}

View File

@ -1,5 +1,5 @@
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.Entities;
using Bit.Core.Enums;
@ -11,103 +11,108 @@ using Bit.Core.Services;
using Bit.Core.Tools.Enums;
using Bit.Core.Tools.Models.Business;
using Bit.Core.Tools.Services;
using Bit.Core.Validators;
#nullable enable
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers;
#nullable enable
public class DeleteManagedOrganizationUserAccountCommand : IDeleteManagedOrganizationUserAccountCommand
{
private readonly IUserService _userService;
private readonly IEventService _eventService;
private readonly IDeleteManagedOrganizationUserAccountValidator _deleteManagedOrganizationUserAccountValidator;
private readonly IGetOrganizationUsersManagementStatusQuery _getOrganizationUsersManagementStatusQuery;
private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly IUserRepository _userRepository;
private readonly ICurrentContext _currentContext;
private readonly IReferenceEventService _referenceEventService;
private readonly IPushNotificationService _pushService;
private readonly IProviderUserRepository _providerUserRepository;
public DeleteManagedOrganizationUserAccountCommand(
IUserService userService,
IEventService eventService,
IDeleteManagedOrganizationUserAccountValidator deleteManagedOrganizationUserAccountValidator,
IGetOrganizationUsersManagementStatusQuery getOrganizationUsersManagementStatusQuery,
IOrganizationUserRepository organizationUserRepository,
IUserRepository userRepository,
ICurrentContext currentContext,
IReferenceEventService referenceEventService,
IPushNotificationService pushService,
IProviderUserRepository providerUserRepository
)
IPushNotificationService pushService)
{
_userService = userService;
_eventService = eventService;
_deleteManagedOrganizationUserAccountValidator = deleteManagedOrganizationUserAccountValidator;
_getOrganizationUsersManagementStatusQuery = getOrganizationUsersManagementStatusQuery;
_organizationUserRepository = organizationUserRepository;
_userRepository = userRepository;
_currentContext = currentContext;
_referenceEventService = referenceEventService;
_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);
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)
{
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 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)
{
var orgUser = orgUsers.FirstOrDefault(ou => ou.Id == orgUserId);
if (orgUser == null || orgUser.OrganizationId != organizationId)
var orgUser = orgUsers.FirstOrDefault(orgUser => orgUser.Id == orgUserId);
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));
continue;
User = user,
OrganizationUser = orgUser,
IsManaged = isManaged,
OrganizationId = organizationId,
DeletingUserId = deletingUserId,
});
}
var user = users.FirstOrDefault(u => u.Id == orgUser.UserId);
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);
await LogDeletedOrganizationUsersAsync(userDeletionRequests);
return userDeletionRequests
.Select(request => (request.OrganizationUserId, request.result))
.ToList();
return requests;
}
private async Task<IEnumerable<User>> GetUsersAsync(ICollection<OrganizationUser> orgUsers)
@ -120,112 +125,12 @@ public class DeleteManagedOrganizationUserAccountCommand : IDeleteManagedOrganiz
return await _userRepository.GetManyAsync(userIds);
}
private async Task<CommandResult> ValidateAsync(Guid organizationId, OrganizationUser orgUser, User user, Guid? deletingUserId, IDictionary<Guid, bool> managementStatus)
{
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)
private async Task LogDeletedOrganizationUsersAsync(List<Valid<DeleteUserValidationRequest>> requests)
{
var eventDate = DateTime.UtcNow;
var events = userDeletionRequests
.Where(request =>
request.result is Success)
.Select(request => (request.orgUser!, (EventType)EventType.OrganizationUser_Deleted, (DateTime?)eventDate))
var events = requests
.Select(request => (request.Value.OrganizationUser!, (EventType)EventType.OrganizationUser_Deleted, (DateTime?)eventDate))
.ToList();
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)
{
await DeleteManyAsync(usersToDelete);
var users = requests
.Select(request => request.Value.User!);
if (users.Any())
{
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) { }
catch (GatewayException)
{
}
}
}
}

View File

@ -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; }
}

View File

@ -9,7 +9,8 @@ public interface IDeleteManagedOrganizationUserAccountCommand
/// <summary>
/// Removes a user from an organization and deletes all of their associated user data.
/// </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>
/// Removes multiple users from an organization and deletes all of their associated user data.
@ -17,5 +18,6 @@ public interface IDeleteManagedOrganizationUserAccountCommand
/// <returns>
/// An error message for each user that could not be removed, otherwise null.
/// </returns>
/// Jimmy temporary comment: consider removing the nullable from deletingUserId.
Task<IEnumerable<(Guid OrganizationUserId, CommandResult result)>> DeleteManyUsersAsync(Guid organizationId, IEnumerable<Guid> orgUserIds, Guid? deletingUserId);
}

View File

@ -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);
}

View File

@ -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 dont 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);
}
}

View File

@ -6,10 +6,35 @@ public abstract record ValidationResult<T>;
public record Valid<T> : ValidationResult<T>
{
public Valid() { }
public Valid(T Value)
{
this.Value = Value;
}
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 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;
}
}

View File

@ -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}")
// // };
// }
}