mirror of
https://github.com/bitwarden/server.git
synced 2025-06-28 06:36:15 -05:00
re-add old ImportAsync impl
This commit is contained in:
parent
b7aac6b4da
commit
3e850b7c3a
@ -3,6 +3,7 @@ using System.Text.Json;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.AdminConsole.Models.Business;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;
|
||||
@ -23,6 +24,7 @@ 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.Platform.Push;
|
||||
using Bit.Core.Repositories;
|
||||
@ -1079,6 +1081,210 @@ public class OrganizationService : IOrganizationService
|
||||
EventType.OrganizationUser_ResetPassword_Enroll : EventType.OrganizationUser_ResetPassword_Withdraw);
|
||||
}
|
||||
|
||||
public async Task ImportAsync(Guid organizationId,
|
||||
IEnumerable<ImportedGroup> groups,
|
||||
IEnumerable<ImportedOrganizationUser> newUsers,
|
||||
IEnumerable<string> removeUserExternalIds,
|
||||
bool overwriteExisting,
|
||||
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<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.TryGetValue(u, out var existingUser) && existingUser.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)));
|
||||
}
|
||||
|
||||
public async Task DeleteSsoUserAsync(Guid userId, Guid? organizationId)
|
||||
{
|
||||
await _ssoUserRepository.DeleteAsync(userId, organizationId);
|
||||
|
@ -13,6 +13,7 @@ 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.Platform.Push;
|
||||
using Bit.Core.Repositories;
|
||||
@ -40,6 +41,131 @@ public class OrganizationServiceTests
|
||||
{
|
||||
private readonly IDataProtectorTokenFactory<OrgUserInviteTokenable> _orgUserInviteTokenDataFactory = new FakeDataProtectorTokenFactory<OrgUserInviteTokenable>();
|
||||
|
||||
[Theory, PaidOrganizationCustomize, BitAutoData]
|
||||
public async Task OrgImportCreateNewUsers(SutProvider<OrganizationService> sutProvider, Organization org, List<OrganizationUserUserDetails> existingUsers, List<ImportedOrganizationUser> newUsers)
|
||||
{
|
||||
// Setup FakeDataProtectorTokenFactory for creating new tokens - this must come first in order to avoid resetting mocks
|
||||
sutProvider.SetDependency(_orgUserInviteTokenDataFactory, "orgUserInviteTokenDataFactory");
|
||||
sutProvider.Create();
|
||||
|
||||
org.UseDirectory = true;
|
||||
org.Seats = 10;
|
||||
newUsers.Add(new ImportedOrganizationUser
|
||||
{
|
||||
Email = existingUsers.First().Email,
|
||||
ExternalId = existingUsers.First().ExternalId
|
||||
});
|
||||
var expectedNewUsersCount = newUsers.Count - 1;
|
||||
|
||||
existingUsers.First().Type = OrganizationUserType.Owner;
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(org.Id).Returns(org);
|
||||
|
||||
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||
SetupOrgUserRepositoryCreateManyAsyncMock(organizationUserRepository);
|
||||
|
||||
organizationUserRepository.GetManyDetailsByOrganizationAsync(org.Id)
|
||||
.Returns(existingUsers);
|
||||
organizationUserRepository.GetCountByOrganizationIdAsync(org.Id)
|
||||
.Returns(existingUsers.Count);
|
||||
sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()
|
||||
.HasConfirmedOwnersExceptAsync(org.Id, Arg.Any<IEnumerable<Guid>>())
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<ICurrentContext>().ManageUsers(org.Id).Returns(true);
|
||||
|
||||
|
||||
await sutProvider.Sut.ImportAsync(org.Id, null, newUsers, null, false, EventSystemUser.PublicApi);
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>().DidNotReceiveWithAnyArgs()
|
||||
.UpsertAsync(default);
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1)
|
||||
.UpsertManyAsync(Arg.Is<IEnumerable<OrganizationUser>>(users => !users.Any()));
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>().DidNotReceiveWithAnyArgs()
|
||||
.CreateAsync(default);
|
||||
|
||||
// Create new users
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1)
|
||||
.CreateManyAsync(Arg.Is<IEnumerable<OrganizationUser>>(users => users.Count() == expectedNewUsersCount));
|
||||
|
||||
await sutProvider.GetDependency<ISendOrganizationInvitesCommand>().Received(1)
|
||||
.SendInvitesAsync(
|
||||
Arg.Is<SendInvitesRequest>(
|
||||
info => info.Users.Length == expectedNewUsersCount &&
|
||||
info.Organization == org));
|
||||
|
||||
// Send events
|
||||
await sutProvider.GetDependency<IEventService>().Received(1)
|
||||
.LogOrganizationUserEventsAsync(Arg.Is<IEnumerable<(OrganizationUser, EventType, EventSystemUser, DateTime?)>>(events =>
|
||||
events.Count() == expectedNewUsersCount));
|
||||
}
|
||||
|
||||
[Theory, PaidOrganizationCustomize, BitAutoData]
|
||||
public async Task OrgImportCreateNewUsersAndMarryExistingUser(SutProvider<OrganizationService> sutProvider, Organization org, List<OrganizationUserUserDetails> existingUsers,
|
||||
List<ImportedOrganizationUser> newUsers)
|
||||
{
|
||||
// Setup FakeDataProtectorTokenFactory for creating new tokens - this must come first in order to avoid resetting mocks
|
||||
sutProvider.SetDependency(_orgUserInviteTokenDataFactory, "orgUserInviteTokenDataFactory");
|
||||
sutProvider.Create();
|
||||
|
||||
org.UseDirectory = true;
|
||||
org.Seats = newUsers.Count + existingUsers.Count + 1;
|
||||
var reInvitedUser = existingUsers.First();
|
||||
reInvitedUser.ExternalId = null;
|
||||
newUsers.Add(new ImportedOrganizationUser
|
||||
{
|
||||
Email = reInvitedUser.Email,
|
||||
ExternalId = reInvitedUser.Email,
|
||||
});
|
||||
var expectedNewUsersCount = newUsers.Count - 1;
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(org.Id).Returns(org);
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>().GetManyDetailsByOrganizationAsync(org.Id)
|
||||
.Returns(existingUsers);
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>().GetCountByOrganizationIdAsync(org.Id)
|
||||
.Returns(existingUsers.Count);
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>().GetByIdAsync(reInvitedUser.Id)
|
||||
.Returns(new OrganizationUser { Id = reInvitedUser.Id });
|
||||
|
||||
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||
|
||||
sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()
|
||||
.HasConfirmedOwnersExceptAsync(org.Id, Arg.Any<IEnumerable<Guid>>())
|
||||
.Returns(true);
|
||||
|
||||
SetupOrgUserRepositoryCreateManyAsyncMock(organizationUserRepository);
|
||||
|
||||
var currentContext = sutProvider.GetDependency<ICurrentContext>();
|
||||
currentContext.ManageUsers(org.Id).Returns(true);
|
||||
|
||||
await sutProvider.Sut.ImportAsync(org.Id, null, newUsers, null, false, EventSystemUser.PublicApi);
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>().DidNotReceiveWithAnyArgs()
|
||||
.UpsertAsync(default);
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>().DidNotReceiveWithAnyArgs()
|
||||
.CreateAsync(default);
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>().DidNotReceiveWithAnyArgs()
|
||||
.CreateAsync(default, default);
|
||||
|
||||
// Upserted existing user
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1)
|
||||
.UpsertManyAsync(Arg.Is<IEnumerable<OrganizationUser>>(users => users.Count() == 1));
|
||||
|
||||
// Created and invited new users
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1)
|
||||
.CreateManyAsync(Arg.Is<IEnumerable<OrganizationUser>>(users => users.Count() == expectedNewUsersCount));
|
||||
|
||||
await sutProvider.GetDependency<ISendOrganizationInvitesCommand>().Received(1)
|
||||
.SendInvitesAsync(Arg.Is<SendInvitesRequest>(request =>
|
||||
request.Users.Length == expectedNewUsersCount &&
|
||||
request.Organization == org));
|
||||
|
||||
// Sent events
|
||||
await sutProvider.GetDependency<IEventService>().Received(1)
|
||||
.LogOrganizationUserEventsAsync(Arg.Is<IEnumerable<(OrganizationUser, EventType, EventSystemUser, DateTime?)>>(events =>
|
||||
events.Count(e => e.Item2 == EventType.OrganizationUser_Invited) == expectedNewUsersCount));
|
||||
}
|
||||
|
||||
|
||||
[Theory]
|
||||
[OrganizationInviteCustomize(InviteeUserType = OrganizationUserType.User,
|
||||
InvitorUserType = OrganizationUserType.Owner), OrganizationCustomize, BitAutoData]
|
||||
|
Loading…
x
Reference in New Issue
Block a user