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:
parent
ad1b78d9ea
commit
eec8b76c62
@ -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
|
||||||
|
))
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
);
|
||||||
|
}
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user