From eec8b76c62776e0462400236a475fcc99121ef0c Mon Sep 17 00:00:00 2001 From: Brandon Date: Tue, 6 May 2025 16:48:35 -0400 Subject: [PATCH] extract function RemoveExistingExternalUsers --- .../ImportOrganizationUserCommand.cs | 40 ++-- .../IImportOrganizationUserCommand.cs | 16 ++ .../Implementations/OrganizationService.cs | 210 +----------------- ...OrganizationServiceCollectionExtensions.cs | 1 + 4 files changed, 54 insertions(+), 213 deletions(-) create mode 100644 src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IImportOrganizationUserCommand.cs diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/ImportOrganizationUserCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/ImportOrganizationUserCommand.cs index be609adb40..29a8c86c20 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/ImportOrganizationUserCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/ImportOrganizationUserCommand.cs @@ -8,6 +8,7 @@ using Bit.Core.Exceptions; using Bit.Core.Models.Business; using Bit.Core.Models.Data; using Bit.Core.Models.Data.Organizations.OrganizationUsers; +using Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Tools.Enums; @@ -15,7 +16,7 @@ using Bit.Core.Tools.Models.Business; using Bit.Core.Tools.Services; using OrganizationUserInvite = Bit.Core.Models.Business.OrganizationUserInvite; -public class ImportOrganizationUserCommand +public class ImportOrganizationUserCommand : IImportOrganizationUserCommand { private readonly IOrganizationRepository _organizationRepository; private readonly IOrganizationUserRepository _organizationUserRepository; @@ -74,22 +75,9 @@ public class ImportOrganizationUserCommand 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(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 - )) - ); + await RemoveExistingExternalUsers(removeUserExternalIds, events, existingExternalUsers, newUsersSet); } if (overwriteExisting) @@ -270,4 +258,26 @@ public class ImportOrganizationUserCommand await _groupRepository.UpdateUsersAsync(group.Id, users); } + + private async Task RemoveExistingExternalUsers( + IEnumerable removeUserExternalIds, + List<(OrganizationUserUserDetails ou, EventType e, DateTime? d)> events, + IEnumerable existingExternalUsers, + HashSet newUsersSet + ) + { + var existingUsersDict = existingExternalUsers.ToDictionary(u => u.ExternalId); + var removeUsersSet = new HashSet(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 + )) + ); + } } diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IImportOrganizationUserCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IImportOrganizationUserCommand.cs new file mode 100644 index 0000000000..1cc10ce908 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IImportOrganizationUserCommand.cs @@ -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 groups, + IEnumerable newUsers, + IEnumerable removeUserExternalIds, + bool overwriteExisting, + EventSystemUser eventSystemUser + ); +} diff --git a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs index 5c7e5e29ed..41c45ef7b9 100644 --- a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs +++ b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs @@ -25,8 +25,8 @@ using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Models.Business; using Bit.Core.Models.Data; -using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface; +using Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.Platform.Push; using Bit.Core.Repositories; using Bit.Core.Settings; @@ -74,6 +74,7 @@ public class OrganizationService : IOrganizationService private readonly IPricingClient _pricingClient; private readonly IPolicyRequirementQuery _policyRequirementQuery; private readonly ISendOrganizationInvitesCommand _sendOrganizationInvitesCommand; + private readonly IImportOrganizationUserCommand _importOrganizationUserCommand; public OrganizationService( IOrganizationRepository organizationRepository, @@ -107,7 +108,8 @@ public class OrganizationService : IOrganizationService IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery, IPricingClient pricingClient, IPolicyRequirementQuery policyRequirementQuery, - ISendOrganizationInvitesCommand sendOrganizationInvitesCommand + ISendOrganizationInvitesCommand sendOrganizationInvitesCommand, + IImportOrganizationUserCommand importOrganizationUserCommand ) { _organizationRepository = organizationRepository; @@ -142,6 +144,7 @@ public class OrganizationService : IOrganizationService _pricingClient = pricingClient; _policyRequirementQuery = policyRequirementQuery; _sendOrganizationInvitesCommand = sendOrganizationInvitesCommand; + _importOrganizationUserCommand = importOrganizationUserCommand; } public async Task ReplacePaymentMethodAsync(Guid organizationId, string paymentToken, @@ -1208,202 +1211,13 @@ public class OrganizationService : IOrganizationService EventSystemUser eventSystemUser ) { - var organization = await GetOrgById(organizationId); - if (organization == null) - { - throw new NotFoundException(); - } - - if (!organization.UseDirectory) - { - throw new BadRequestException("Organization cannot use directory syncing."); - } - - var newUsersSet = new HashSet(newUsers?.Select(u => u.ExternalId) ?? new List()); - 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(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(); - 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(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 { user.Email }, - Type = OrganizationUserType.User, - Collections = new List(), - 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(); - 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(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)); + // @TODO DEVELOPMENT FLAG FOR TESTING ---- REVERT THIS LATER + await _importOrganizationUserCommand.ImportAsync(organizationId, + groups, + newUsers, + removeUserExternalIds, + overwriteExisting, + eventSystemUser); } public async Task DeleteSsoUserAsync(Guid userId, Guid? organizationId) diff --git a/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs index b016e329bf..2d6e460593 100644 --- a/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs +++ b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs @@ -194,6 +194,7 @@ public static class OrganizationServiceCollectionExtensions services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); } // TODO: move to OrganizationSubscriptionServiceCollectionExtensions when OrganizationUser methods are moved out of