1
0
mirror of https://github.com/bitwarden/server.git synced 2025-05-10 06:02:24 -05:00

extract function RemoveExistingExternalUsers

This commit is contained in:
Brandon 2025-05-06 16:48:35 -04:00
parent ad1b78d9ea
commit eec8b76c62
No known key found for this signature in database
GPG Key ID: A0E0EF0B207BA40D
4 changed files with 54 additions and 213 deletions

View File

@ -8,6 +8,7 @@ using Bit.Core.Exceptions;
using Bit.Core.Models.Business; using Bit.Core.Models.Business;
using Bit.Core.Models.Data; using Bit.Core.Models.Data;
using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Tools.Enums; using Bit.Core.Tools.Enums;
@ -15,7 +16,7 @@ using Bit.Core.Tools.Models.Business;
using Bit.Core.Tools.Services; using Bit.Core.Tools.Services;
using OrganizationUserInvite = Bit.Core.Models.Business.OrganizationUserInvite; using OrganizationUserInvite = Bit.Core.Models.Business.OrganizationUserInvite;
public class ImportOrganizationUserCommand public class ImportOrganizationUserCommand : IImportOrganizationUserCommand
{ {
private readonly IOrganizationRepository _organizationRepository; private readonly IOrganizationRepository _organizationRepository;
private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IOrganizationUserRepository _organizationUserRepository;
@ -74,22 +75,9 @@ public class ImportOrganizationUserCommand
var events = new List<(OrganizationUserUserDetails ou, EventType e, DateTime? d)>(); var events = new List<(OrganizationUserUserDetails ou, EventType e, DateTime? d)>();
// Remove Users
if (removeUserExternalIds?.Any() ?? false) if (removeUserExternalIds?.Any() ?? false)
{ {
var existingUsersDict = existingExternalUsers.ToDictionary(u => u.ExternalId); await RemoveExistingExternalUsers(removeUserExternalIds, events, existingExternalUsers, newUsersSet);
var removeUsersSet = new HashSet<string>(removeUserExternalIds)
.Except(newUsersSet)
.Where(u => existingUsersDict.ContainsKey(u) && existingUsersDict[u].Type != OrganizationUserType.Owner)
.Select(u => existingUsersDict[u]);
await _organizationUserRepository.DeleteManyAsync(removeUsersSet.Select(u => u.Id));
events.AddRange(removeUsersSet.Select(u => (
u,
EventType.OrganizationUser_Removed,
(DateTime?)DateTime.UtcNow
))
);
} }
if (overwriteExisting) if (overwriteExisting)
@ -270,4 +258,26 @@ public class ImportOrganizationUserCommand
await _groupRepository.UpdateUsersAsync(group.Id, users); await _groupRepository.UpdateUsersAsync(group.Id, users);
} }
private async Task RemoveExistingExternalUsers(
IEnumerable<string> removeUserExternalIds,
List<(OrganizationUserUserDetails ou, EventType e, DateTime? d)> events,
IEnumerable<OrganizationUserUserDetails> existingExternalUsers,
HashSet<string> newUsersSet
)
{
var existingUsersDict = existingExternalUsers.ToDictionary(u => u.ExternalId);
var removeUsersSet = new HashSet<string>(removeUserExternalIds)
.Except(newUsersSet)
.Where(u => existingUsersDict.ContainsKey(u) && existingUsersDict[u].Type != OrganizationUserType.Owner)
.Select(u => existingUsersDict[u]);
await _organizationUserRepository.DeleteManyAsync(removeUsersSet.Select(u => u.Id));
events.AddRange(removeUsersSet.Select(u => (
u,
EventType.OrganizationUser_Removed,
(DateTime?)DateTime.UtcNow
))
);
}
} }

View File

@ -0,0 +1,16 @@
using Bit.Core.AdminConsole.Models.Business;
using Bit.Core.Enums;
using Bit.Core.Models.Business;
namespace Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;
public interface IImportOrganizationUserCommand
{
Task ImportAsync(Guid organizationId,
IEnumerable<ImportedGroup> groups,
IEnumerable<ImportedOrganizationUser> newUsers,
IEnumerable<string> removeUserExternalIds,
bool overwriteExisting,
EventSystemUser eventSystemUser
);
}

View File

@ -25,8 +25,8 @@ using Bit.Core.Enums;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Models.Business; using Bit.Core.Models.Business;
using Bit.Core.Models.Data; using Bit.Core.Models.Data;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface; using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
using Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.Platform.Push; using Bit.Core.Platform.Push;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Settings; using Bit.Core.Settings;
@ -74,6 +74,7 @@ public class OrganizationService : IOrganizationService
private readonly IPricingClient _pricingClient; private readonly IPricingClient _pricingClient;
private readonly IPolicyRequirementQuery _policyRequirementQuery; private readonly IPolicyRequirementQuery _policyRequirementQuery;
private readonly ISendOrganizationInvitesCommand _sendOrganizationInvitesCommand; private readonly ISendOrganizationInvitesCommand _sendOrganizationInvitesCommand;
private readonly IImportOrganizationUserCommand _importOrganizationUserCommand;
public OrganizationService( public OrganizationService(
IOrganizationRepository organizationRepository, IOrganizationRepository organizationRepository,
@ -107,7 +108,8 @@ public class OrganizationService : IOrganizationService
IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery, IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery,
IPricingClient pricingClient, IPricingClient pricingClient,
IPolicyRequirementQuery policyRequirementQuery, IPolicyRequirementQuery policyRequirementQuery,
ISendOrganizationInvitesCommand sendOrganizationInvitesCommand ISendOrganizationInvitesCommand sendOrganizationInvitesCommand,
IImportOrganizationUserCommand importOrganizationUserCommand
) )
{ {
_organizationRepository = organizationRepository; _organizationRepository = organizationRepository;
@ -142,6 +144,7 @@ public class OrganizationService : IOrganizationService
_pricingClient = pricingClient; _pricingClient = pricingClient;
_policyRequirementQuery = policyRequirementQuery; _policyRequirementQuery = policyRequirementQuery;
_sendOrganizationInvitesCommand = sendOrganizationInvitesCommand; _sendOrganizationInvitesCommand = sendOrganizationInvitesCommand;
_importOrganizationUserCommand = importOrganizationUserCommand;
} }
public async Task ReplacePaymentMethodAsync(Guid organizationId, string paymentToken, public async Task ReplacePaymentMethodAsync(Guid organizationId, string paymentToken,
@ -1208,202 +1211,13 @@ public class OrganizationService : IOrganizationService
EventSystemUser eventSystemUser EventSystemUser eventSystemUser
) )
{ {
var organization = await GetOrgById(organizationId); // @TODO DEVELOPMENT FLAG FOR TESTING ---- REVERT THIS LATER
if (organization == null) await _importOrganizationUserCommand.ImportAsync(organizationId,
{ groups,
throw new NotFoundException(); newUsers,
} removeUserExternalIds,
overwriteExisting,
if (!organization.UseDirectory) eventSystemUser);
{
throw new BadRequestException("Organization cannot use directory syncing.");
}
var newUsersSet = new HashSet<string>(newUsers?.Select(u => u.ExternalId) ?? new List<string>());
var existingUsers = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(organizationId);
var existingExternalUsers = existingUsers.Where(u => !string.IsNullOrWhiteSpace(u.ExternalId)).ToList();
var existingExternalUsersIdDict = existingExternalUsers.ToDictionary(u => u.ExternalId, u => u.Id);
// Users
var events = new List<(OrganizationUserUserDetails ou, EventType e, DateTime? d)>();
// Remove Users
if (removeUserExternalIds?.Any() ?? false)
{
var existingUsersDict = existingExternalUsers.ToDictionary(u => u.ExternalId);
var removeUsersSet = new HashSet<string>(removeUserExternalIds)
.Except(newUsersSet)
.Where(u => existingUsersDict.ContainsKey(u) && existingUsersDict[u].Type != OrganizationUserType.Owner)
.Select(u => existingUsersDict[u]);
await _organizationUserRepository.DeleteManyAsync(removeUsersSet.Select(u => u.Id));
events.AddRange(removeUsersSet.Select(u => (
u,
EventType.OrganizationUser_Removed,
(DateTime?)DateTime.UtcNow
))
);
}
if (overwriteExisting)
{
// Remove existing external users that are not in new user set
var usersToDelete = existingExternalUsers.Where(u =>
u.Type != OrganizationUserType.Owner &&
!newUsersSet.Contains(u.ExternalId) &&
existingExternalUsersIdDict.ContainsKey(u.ExternalId));
await _organizationUserRepository.DeleteManyAsync(usersToDelete.Select(u => u.Id));
events.AddRange(usersToDelete.Select(u => (
u,
EventType.OrganizationUser_Removed,
(DateTime?)DateTime.UtcNow
))
);
foreach (var deletedUser in usersToDelete)
{
existingExternalUsersIdDict.Remove(deletedUser.ExternalId);
}
}
if (newUsers?.Any() ?? false)
{
// Marry existing users
var existingUsersEmailsDict = existingUsers
.Where(u => string.IsNullOrWhiteSpace(u.ExternalId))
.ToDictionary(u => u.Email);
var newUsersEmailsDict = newUsers.ToDictionary(u => u.Email);
var usersToAttach = existingUsersEmailsDict.Keys.Intersect(newUsersEmailsDict.Keys).ToList();
var usersToUpsert = new List<OrganizationUser>();
foreach (var user in usersToAttach)
{
var orgUserDetails = existingUsersEmailsDict[user];
var orgUser = await _organizationUserRepository.GetByIdAsync(orgUserDetails.Id);
if (orgUser != null)
{
orgUser.ExternalId = newUsersEmailsDict[user].ExternalId;
usersToUpsert.Add(orgUser);
existingExternalUsersIdDict.Add(orgUser.ExternalId, orgUser.Id);
}
}
await _organizationUserRepository.UpsertManyAsync(usersToUpsert);
// Add new users
var existingUsersSet = new HashSet<string>(existingExternalUsersIdDict.Keys);
var usersToAdd = newUsersSet.Except(existingUsersSet).ToList();
var seatsAvailable = int.MaxValue;
var enoughSeatsAvailable = true;
if (organization.Seats.HasValue)
{
var occupiedSeats = await _organizationUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
seatsAvailable = organization.Seats.Value - occupiedSeats;
enoughSeatsAvailable = seatsAvailable >= usersToAdd.Count;
}
var hasStandaloneSecretsManager = await _paymentService.HasSecretsManagerStandalone(organization);
var userInvites = new List<(OrganizationUserInvite, string)>();
foreach (var user in newUsers)
{
if (!usersToAdd.Contains(user.ExternalId) || string.IsNullOrWhiteSpace(user.Email))
{
continue;
}
try
{
var invite = new OrganizationUserInvite
{
Emails = new List<string> { user.Email },
Type = OrganizationUserType.User,
Collections = new List<CollectionAccessSelection>(),
AccessSecretsManager = hasStandaloneSecretsManager
};
userInvites.Add((invite, user.ExternalId));
}
catch (BadRequestException)
{
// Thrown when the user is already invited to the organization
continue;
}
}
var invitedUsers = await InviteUsersAsync(organizationId, invitingUserId: null, systemUser: eventSystemUser, userInvites);
foreach (var invitedUser in invitedUsers)
{
existingExternalUsersIdDict.Add(invitedUser.ExternalId, invitedUser.Id);
}
}
// Groups
if (groups?.Any() ?? false)
{
if (!organization.UseGroups)
{
throw new BadRequestException("Organization cannot use groups.");
}
var groupsDict = groups.ToDictionary(g => g.Group.ExternalId);
var existingGroups = await _groupRepository.GetManyByOrganizationIdAsync(organizationId);
var existingExternalGroups = existingGroups
.Where(u => !string.IsNullOrWhiteSpace(u.ExternalId)).ToList();
var existingExternalGroupsDict = existingExternalGroups.ToDictionary(g => g.ExternalId);
var newGroups = groups
.Where(g => !existingExternalGroupsDict.ContainsKey(g.Group.ExternalId))
.Select(g => g.Group).ToList();
var savedGroups = new List<Group>();
foreach (var group in newGroups)
{
group.CreationDate = group.RevisionDate = DateTime.UtcNow;
savedGroups.Add(await _groupRepository.CreateAsync(group));
await UpdateUsersAsync(group, groupsDict[group.ExternalId].ExternalUserIds,
existingExternalUsersIdDict);
}
await _eventService.LogGroupEventsAsync(
savedGroups.Select(g => (g, EventType.Group_Created, (EventSystemUser?)eventSystemUser, (DateTime?)DateTime.UtcNow)));
var updateGroups = existingExternalGroups
.Where(g => groupsDict.ContainsKey(g.ExternalId))
.ToList();
if (updateGroups.Any())
{
var groupUsers = await _groupRepository.GetManyGroupUsersByOrganizationIdAsync(organizationId);
var existingGroupUsers = groupUsers
.GroupBy(gu => gu.GroupId)
.ToDictionary(g => g.Key, g => new HashSet<Guid>(g.Select(gr => gr.OrganizationUserId)));
foreach (var group in updateGroups)
{
var updatedGroup = groupsDict[group.ExternalId].Group;
if (group.Name != updatedGroup.Name)
{
group.RevisionDate = DateTime.UtcNow;
group.Name = updatedGroup.Name;
await _groupRepository.ReplaceAsync(group);
}
await UpdateUsersAsync(group, groupsDict[group.ExternalId].ExternalUserIds,
existingExternalUsersIdDict,
existingGroupUsers.ContainsKey(group.Id) ? existingGroupUsers[group.Id] : null);
}
await _eventService.LogGroupEventsAsync(
updateGroups.Select(g => (g, EventType.Group_Updated, (EventSystemUser?)eventSystemUser, (DateTime?)DateTime.UtcNow)));
}
}
await _eventService.LogOrganizationUserEventsAsync(events.Select(e => (e.ou, e.e, eventSystemUser, e.d)));
await _referenceEventService.RaiseEventAsync(
new ReferenceEvent(ReferenceEventType.DirectorySynced, organization, _currentContext));
} }
public async Task DeleteSsoUserAsync(Guid userId, Guid? organizationId) public async Task DeleteSsoUserAsync(Guid userId, Guid? organizationId)

View File

@ -194,6 +194,7 @@ public static class OrganizationServiceCollectionExtensions
services.AddScoped<IInviteUsersPasswordManagerValidator, InviteUsersPasswordManagerValidator>(); services.AddScoped<IInviteUsersPasswordManagerValidator, InviteUsersPasswordManagerValidator>();
services.AddScoped<IInviteUsersEnvironmentValidator, InviteUsersEnvironmentValidator>(); services.AddScoped<IInviteUsersEnvironmentValidator, InviteUsersEnvironmentValidator>();
services.AddScoped<IInitPendingOrganizationCommand, InitPendingOrganizationCommand>(); services.AddScoped<IInitPendingOrganizationCommand, InitPendingOrganizationCommand>();
services.AddScoped<IImportOrganizationUserCommand, ImportOrganizationUserCommand>();
} }
// TODO: move to OrganizationSubscriptionServiceCollectionExtensions when OrganizationUser methods are moved out of // TODO: move to OrganizationSubscriptionServiceCollectionExtensions when OrganizationUser methods are moved out of