mirror of
https://github.com/bitwarden/server
synced 2025-12-06 00:03:34 +00:00
[PM-19145] refactor organization service.import async (#5800)
* initial lift and shift * extract function RemoveExistingExternalUsers * Extract function RemoveExistingUsers() * extract function OverwriteExisting() * create new model for sync data * extract add users to function, rename * rename OrganizatinUserInvite for command, implement command * implement command * refactor groups logic * fix imports * remove old tests, fix imports * fix namespace * fix CommandResult useage * tests wip * wip * wip * remove redundant code, remove looping db call, refactor tests * clean up * remove looping db call with bulk method * clean up * remove orgId param to use id already in request * change param * cleanup params * remove IReferenceEventService * fix test * fix tests * cr feedback * remove _timeProvider * add xmldoc, refactor to make InviteOrganizationUsersCommand vNext instead of default * switch back to command * re-add old ImportAsync impl * fix test * add feature flag * cleanup * clean up * fix tests * wip * wip * add api integration tests for users WIP * groups integration tests * cleanup * fix error from merging main * fix tests * cr feedback * fix test * fix test
This commit is contained in:
@@ -6,9 +6,9 @@ using Bit.Core.AdminConsole.Models.Business;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Business;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Utilities;
|
||||
using OrganizationUserInvite = Bit.Core.Models.Business.OrganizationUserInvite;
|
||||
|
||||
namespace Bit.Scim.Models;
|
||||
|
||||
@@ -47,7 +47,7 @@ public class ScimUserRequestModel : BaseScimUserModel
|
||||
return new InviteOrganizationUsersRequest(
|
||||
invites:
|
||||
[
|
||||
new Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models.OrganizationUserInvite(
|
||||
new OrganizationUserInviteCommandModel(
|
||||
email: email,
|
||||
externalId: ExternalIdForInvite()
|
||||
)
|
||||
|
||||
@@ -4,8 +4,9 @@
|
||||
using System.Net;
|
||||
using Bit.Api.AdminConsole.Public.Models.Request;
|
||||
using Bit.Api.Models.Public.Response;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
@@ -21,15 +22,21 @@ public class OrganizationController : Controller
|
||||
private readonly IOrganizationService _organizationService;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private readonly IImportOrganizationUsersAndGroupsCommand _importOrganizationUsersAndGroupsCommand;
|
||||
private readonly IFeatureService _featureService;
|
||||
|
||||
public OrganizationController(
|
||||
IOrganizationService organizationService,
|
||||
ICurrentContext currentContext,
|
||||
GlobalSettings globalSettings)
|
||||
GlobalSettings globalSettings,
|
||||
IImportOrganizationUsersAndGroupsCommand importOrganizationUsersAndGroupsCommand,
|
||||
IFeatureService featureService)
|
||||
{
|
||||
_organizationService = organizationService;
|
||||
_currentContext = currentContext;
|
||||
_globalSettings = globalSettings;
|
||||
_importOrganizationUsersAndGroupsCommand = importOrganizationUsersAndGroupsCommand;
|
||||
_featureService = featureService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -50,13 +57,26 @@ public class OrganizationController : Controller
|
||||
throw new BadRequestException("You cannot import this much data at once.");
|
||||
}
|
||||
|
||||
await _organizationService.ImportAsync(
|
||||
_currentContext.OrganizationId.Value,
|
||||
model.Groups.Select(g => g.ToImportedGroup(_currentContext.OrganizationId.Value)),
|
||||
model.Members.Where(u => !u.Deleted).Select(u => u.ToImportedOrganizationUser()),
|
||||
model.Members.Where(u => u.Deleted).Select(u => u.ExternalId),
|
||||
model.OverwriteExisting.GetValueOrDefault(),
|
||||
EventSystemUser.PublicApi);
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.ImportAsyncRefactor))
|
||||
{
|
||||
await _importOrganizationUsersAndGroupsCommand.ImportAsync(
|
||||
_currentContext.OrganizationId.Value,
|
||||
model.Groups.Select(g => g.ToImportedGroup(_currentContext.OrganizationId.Value)),
|
||||
model.Members.Where(u => !u.Deleted).Select(u => u.ToImportedOrganizationUser()),
|
||||
model.Members.Where(u => u.Deleted).Select(u => u.ExternalId),
|
||||
model.OverwriteExisting.GetValueOrDefault());
|
||||
}
|
||||
else
|
||||
{
|
||||
await _organizationService.ImportAsync(
|
||||
_currentContext.OrganizationId.Value,
|
||||
model.Groups.Select(g => g.ToImportedGroup(_currentContext.OrganizationId.Value)),
|
||||
model.Members.Where(u => !u.Deleted).Select(u => u.ToImportedOrganizationUser()),
|
||||
model.Members.Where(u => u.Deleted).Select(u => u.ExternalId),
|
||||
model.OverwriteExisting.GetValueOrDefault(),
|
||||
Core.Enums.EventSystemUser.PublicApi);
|
||||
}
|
||||
|
||||
return new OkResult();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
#nullable enable
|
||||
|
||||
using Bit.Core.AdminConsole.Models.Business;
|
||||
using Bit.Core.Models.Business;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
|
||||
public interface IImportOrganizationUsersAndGroupsCommand
|
||||
{
|
||||
Task ImportAsync(Guid organizationId,
|
||||
IEnumerable<ImportedGroup> groups,
|
||||
IEnumerable<ImportedOrganizationUser> newUsers,
|
||||
IEnumerable<string> removeUserExternalIds,
|
||||
bool overwriteExisting
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,391 @@
|
||||
#nullable enable
|
||||
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Models.Business;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Business;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Models.Data.Organizations;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers;
|
||||
|
||||
public class ImportOrganizationUsersAndGroupsCommand : IImportOrganizationUsersAndGroupsCommand
|
||||
{
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
private readonly IPaymentService _paymentService;
|
||||
private readonly IGroupRepository _groupRepository;
|
||||
private readonly IEventService _eventService;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly IOrganizationService _organizationService;
|
||||
private readonly IInviteOrganizationUsersCommand _inviteOrganizationUsersCommand;
|
||||
private readonly IPricingClient _pricingClient;
|
||||
|
||||
private readonly EventSystemUser _EventSystemUser = EventSystemUser.PublicApi;
|
||||
|
||||
public ImportOrganizationUsersAndGroupsCommand(IOrganizationRepository organizationRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IPaymentService paymentService,
|
||||
IGroupRepository groupRepository,
|
||||
IEventService eventService,
|
||||
ICurrentContext currentContext,
|
||||
IOrganizationService organizationService,
|
||||
IInviteOrganizationUsersCommand inviteOrganizationUsersCommand,
|
||||
IPricingClient pricingClient
|
||||
)
|
||||
{
|
||||
_organizationRepository = organizationRepository;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
_paymentService = paymentService;
|
||||
_groupRepository = groupRepository;
|
||||
_eventService = eventService;
|
||||
_currentContext = currentContext;
|
||||
_organizationService = organizationService;
|
||||
_inviteOrganizationUsersCommand = inviteOrganizationUsersCommand;
|
||||
_pricingClient = pricingClient;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Imports and synchronizes organization users and groups.
|
||||
/// </summary>
|
||||
/// <param name="organizationId">The unique identifier of the organization.</param>
|
||||
/// <param name="importedGroups">List of groups to import.</param>
|
||||
/// <param name="importedUsers">List of users to import.</param>
|
||||
/// <param name="removeUserExternalIds">A collection of ExternalUserIds to be removed from the organization.</param>
|
||||
/// <param name="overwriteExisting">Indicates whether to delete existing external users from the organization
|
||||
/// who are not included in the current import.</param>
|
||||
/// <exception cref="NotFoundException">Thrown if the organization does not exist.</exception>
|
||||
/// <exception cref="BadRequestException">Thrown if the organization is not configured to use directory syncing.</exception>
|
||||
public async Task ImportAsync(Guid organizationId,
|
||||
IEnumerable<ImportedGroup> importedGroups,
|
||||
IEnumerable<ImportedOrganizationUser> importedUsers,
|
||||
IEnumerable<string> removeUserExternalIds,
|
||||
bool overwriteExisting)
|
||||
{
|
||||
var organization = await GetOrgById(organizationId);
|
||||
if (organization is null)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
if (!organization.UseDirectory)
|
||||
{
|
||||
throw new BadRequestException("Organization cannot use directory syncing.");
|
||||
}
|
||||
|
||||
var existingUsers = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(organizationId);
|
||||
var importUserData = new OrganizationUserImportData(existingUsers, importedUsers);
|
||||
var events = new List<(OrganizationUserUserDetails ou, EventType e, DateTime? d)>();
|
||||
|
||||
await RemoveExistingExternalUsers(removeUserExternalIds, events, importUserData);
|
||||
|
||||
if (overwriteExisting)
|
||||
{
|
||||
await OverwriteExisting(events, importUserData);
|
||||
}
|
||||
|
||||
await UpdateExistingUsers(importedUsers, importUserData);
|
||||
|
||||
await AddNewUsers(organization, importedUsers, importUserData);
|
||||
|
||||
await ImportGroups(organization, importedGroups, importUserData);
|
||||
|
||||
await _eventService.LogOrganizationUserEventsAsync(events.Select(e => (e.ou, e.e, _EventSystemUser, e.d)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes external users based on provided set of ExternalIds.
|
||||
/// </summary>
|
||||
/// <param name="removeUserExternalIds">A collection of external user IDs to be deleted.</param>
|
||||
/// <param name="events">A list to which user removal events will be added.</param>
|
||||
/// <param name="importUserData">Data containing imported and existing external users.</param>
|
||||
|
||||
private async Task RemoveExistingExternalUsers(IEnumerable<string> removeUserExternalIds,
|
||||
List<(OrganizationUserUserDetails ou, EventType e, DateTime? d)> events,
|
||||
OrganizationUserImportData importUserData)
|
||||
{
|
||||
if (!removeUserExternalIds.Any())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var existingUsersDict = importUserData.ExistingExternalUsers.ToDictionary(u => u.ExternalId);
|
||||
// Determine which ids in removeUserExternalIds to delete based on:
|
||||
// They are not in ImportedExternalIds, they are in existingUsersDict, and they are not an owner.
|
||||
var removeUsersSet = new HashSet<string>(removeUserExternalIds)
|
||||
.Except(importUserData.ImportedExternalIds)
|
||||
.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
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates existing organization users by assigning each an ExternalId from the imported user data
|
||||
/// where a match is found by email and the existing user lacks an ExternalId. Saves the updated
|
||||
/// users and updates the ExistingExternalUsersIdDict mapping.
|
||||
/// </summary>
|
||||
/// <param name="importedUsers">List of imported organization users.</param>
|
||||
/// <param name="importUserData">Data containing existing and imported users, along with mapping dictionaries.</param>
|
||||
private async Task UpdateExistingUsers(IEnumerable<ImportedOrganizationUser> importedUsers, OrganizationUserImportData importUserData)
|
||||
{
|
||||
if (!importedUsers.Any())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var updateUsers = new List<OrganizationUser>();
|
||||
|
||||
// Map existing and imported users to dicts keyed by Email
|
||||
var existingUsersEmailsDict = importUserData.ExistingUsers
|
||||
.Where(u => string.IsNullOrWhiteSpace(u.ExternalId))
|
||||
.ToDictionary(u => u.Email);
|
||||
var importedUsersEmailsDict = importedUsers.ToDictionary(u => u.Email);
|
||||
|
||||
// Determine which users to update.
|
||||
var userEmailsToUpdate = existingUsersEmailsDict.Keys.Intersect(importedUsersEmailsDict.Keys).ToList();
|
||||
var userIdsToUpdate = userEmailsToUpdate.Select(e => existingUsersEmailsDict[e].Id).ToList();
|
||||
|
||||
var organizationUsers = (await _organizationUserRepository.GetManyAsync(userIdsToUpdate)).ToDictionary(u => u.Id);
|
||||
|
||||
foreach (var userEmail in userEmailsToUpdate)
|
||||
{
|
||||
// verify userEmail has an associated OrganizationUser
|
||||
existingUsersEmailsDict.TryGetValue(userEmail, out var existingUser);
|
||||
organizationUsers.TryGetValue(existingUser!.Id, out var organizationUser);
|
||||
importedUsersEmailsDict.TryGetValue(userEmail, out var importedUser);
|
||||
|
||||
if (organizationUser is null || importedUser is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
organizationUser.ExternalId = importedUser.ExternalId;
|
||||
updateUsers.Add(organizationUser);
|
||||
importUserData.ExistingExternalUsersIdDict.Add(organizationUser.ExternalId, organizationUser.Id);
|
||||
}
|
||||
await _organizationUserRepository.UpsertManyAsync(updateUsers);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds new external users to the organization by inviting users who are present in the imported data
|
||||
/// but not already part of the organization. Sends invitations, updates the user Id mapping on success,
|
||||
/// and throws exceptions on failure.
|
||||
/// </summary>
|
||||
/// <param name="organization">The target organization to which users are being added.</param>
|
||||
/// <param name="importedUsers">A collection of imported users to consider for addition.</param>
|
||||
/// <param name="importUserData">Data containing imported user info and existing user mappings.</param>
|
||||
private async Task AddNewUsers(Organization organization,
|
||||
IEnumerable<ImportedOrganizationUser> importedUsers,
|
||||
OrganizationUserImportData importUserData)
|
||||
{
|
||||
// Determine which users are already in the organization
|
||||
var existingUsersSet = new HashSet<string>(importUserData.ExistingExternalUsersIdDict.Keys);
|
||||
var usersToAdd = importUserData.ImportedExternalIds.Except(existingUsersSet).ToList();
|
||||
var userInvites = new List<(OrganizationUserInvite, string)>();
|
||||
var hasStandaloneSecretsManager = await _paymentService.HasSecretsManagerStandalone(organization);
|
||||
|
||||
foreach (var user in importedUsers)
|
||||
{
|
||||
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 _organizationService.InviteUsersAsync(organization.Id, Guid.Empty, _EventSystemUser, userInvites);
|
||||
foreach (var invitedUser in invitedUsers)
|
||||
{
|
||||
importUserData.ExistingExternalUsersIdDict.TryAdd(invitedUser.ExternalId!, invitedUser.Id);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes existing external users from the organization who are not included in the current import and are not owners.
|
||||
/// Records corresponding removal events and updates the internal mapping by removing deleted users.
|
||||
/// </summary>
|
||||
/// <param name="events">A list to which user removal events will be added.</param>
|
||||
/// <param name="importUserData">Data containing existing and imported external users along with their Id mappings.</param>
|
||||
private async Task OverwriteExisting(
|
||||
List<(OrganizationUserUserDetails ou, EventType e, DateTime? d)> events,
|
||||
OrganizationUserImportData importUserData)
|
||||
{
|
||||
var usersToDelete = importUserData.ExistingExternalUsers.Where(u =>
|
||||
u.Type != OrganizationUserType.Owner &&
|
||||
!importUserData.ImportedExternalIds.Contains(u.ExternalId) &&
|
||||
importUserData.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)
|
||||
{
|
||||
importUserData.ExistingExternalUsersIdDict.Remove(deletedUser.ExternalId);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Imports group data into the organization by saving new groups and updating existing ones.
|
||||
/// </summary>
|
||||
/// <param name="organization">The organization into which groups are being imported.</param>
|
||||
/// <param name="importedGroups">A collection of groups to be imported.</param>
|
||||
/// <param name="importUserData">Data containing information about existing and imported users.</param>
|
||||
private async Task ImportGroups(Organization organization, IEnumerable<ImportedGroup> importedGroups, OrganizationUserImportData importUserData)
|
||||
{
|
||||
if (!importedGroups.Any())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!organization.UseGroups)
|
||||
{
|
||||
throw new BadRequestException("Organization cannot use groups.");
|
||||
}
|
||||
|
||||
var existingGroups = await _groupRepository.GetManyByOrganizationIdAsync(organization.Id);
|
||||
var importGroupData = new OrganizationGroupImportData(importedGroups, existingGroups);
|
||||
|
||||
await SaveNewGroups(importGroupData, importUserData);
|
||||
await UpdateExistingGroups(importGroupData, importUserData, organization);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves newly imported groups that do not already exist in the organization.
|
||||
/// Sets their creation and revision dates, associates users with each group.
|
||||
/// </summary>
|
||||
/// <param name="importGroupData">Data containing both imported and existing groups.</param>
|
||||
/// <param name="importUserData">Data containing information about existing and imported users.</param>
|
||||
private async Task SaveNewGroups(OrganizationGroupImportData importGroupData, OrganizationUserImportData importUserData)
|
||||
{
|
||||
var existingExternalGroupsDict = importGroupData.ExistingExternalGroups.ToDictionary(g => g.ExternalId!);
|
||||
var newGroups = importGroupData.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, importGroupData.GroupsDict[group.ExternalId!].ExternalUserIds,
|
||||
importUserData.ExistingExternalUsersIdDict);
|
||||
}
|
||||
|
||||
await _eventService.LogGroupEventsAsync(
|
||||
savedGroups.Select(g => (g, EventType.Group_Created, (EventSystemUser?)_EventSystemUser, (DateTime?)DateTime.UtcNow)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates existing groups in the organization based on imported group data.
|
||||
/// If a group's name has changed, it updates the name and revision date in the repository.
|
||||
/// Also updates group-user associations.
|
||||
/// </summary>
|
||||
/// <param name="importGroupData">Data containing imported groups and their user associations.</param>
|
||||
/// <param name="importUserData">Data containing imported and existing organization users.</param>
|
||||
/// <param name="organization">The organization to which the groups belong.</param>
|
||||
private async Task UpdateExistingGroups(OrganizationGroupImportData importGroupData,
|
||||
OrganizationUserImportData importUserData,
|
||||
Organization organization)
|
||||
{
|
||||
var updateGroups = importGroupData.ExistingExternalGroups
|
||||
.Where(g => importGroupData.GroupsDict.ContainsKey(g.ExternalId!))
|
||||
.ToList();
|
||||
|
||||
if (updateGroups.Any())
|
||||
{
|
||||
// get existing group users
|
||||
var groupUsers = await _groupRepository.GetManyGroupUsersByOrganizationIdAsync(organization.Id);
|
||||
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)
|
||||
{
|
||||
// Check for changes to the group, update if changed.
|
||||
var updatedGroup = importGroupData.GroupsDict[group.ExternalId!].Group;
|
||||
if (group.Name != updatedGroup.Name)
|
||||
{
|
||||
group.RevisionDate = DateTime.UtcNow;
|
||||
group.Name = updatedGroup.Name;
|
||||
|
||||
await _groupRepository.ReplaceAsync(group);
|
||||
}
|
||||
|
||||
// compare and update user group associations
|
||||
await UpdateUsersAsync(group, importGroupData.GroupsDict[group.ExternalId!].ExternalUserIds,
|
||||
importUserData.ExistingExternalUsersIdDict,
|
||||
existingGroupUsers.ContainsKey(group.Id) ? existingGroupUsers[group.Id] : null);
|
||||
|
||||
}
|
||||
|
||||
await _eventService.LogGroupEventsAsync(
|
||||
updateGroups.Select(g => (g, EventType.Group_Updated, (EventSystemUser?)_EventSystemUser, (DateTime?)DateTime.UtcNow)));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the user associations for a given group.
|
||||
/// Only updates if the set of associated users differs from the current group membership.
|
||||
/// Filters users based on those present in the existing user Id dictionary.
|
||||
/// </summary>
|
||||
/// <param name="group">The group whose user associations are being updated.</param>
|
||||
/// <param name="groupUsers">A set of ExternalUserIds to be associated with the group.</param>
|
||||
/// <param name="existingUsersIdDict">A dictionary mapping ExternalUserIds to internal user Ids.</param>
|
||||
/// <param name="existingUsers">Optional set of currently associated user Ids for comparison.</param>
|
||||
private async Task UpdateUsersAsync(Group group, HashSet<string> groupUsers,
|
||||
Dictionary<string, Guid> existingUsersIdDict, HashSet<Guid>? existingUsers = null)
|
||||
{
|
||||
var availableUsers = groupUsers.Intersect(existingUsersIdDict.Keys);
|
||||
var users = new HashSet<Guid>(availableUsers.Select(u => existingUsersIdDict[u]));
|
||||
if (existingUsers is not null && existingUsers.Count == users.Count && users.SetEquals(existingUsers))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await _groupRepository.UpdateUsersAsync(group.Id, users);
|
||||
}
|
||||
|
||||
private async Task<Organization?> GetOrgById(Guid id)
|
||||
{
|
||||
return await _organizationRepository.GetByIdAsync(id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
#nullable enable
|
||||
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Models.Business;
|
||||
|
||||
namespace Bit.Core.Models.Data.Organizations;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the data required to import organization groups,
|
||||
/// including newly imported groups and existing groups within the organization.
|
||||
/// </summary>
|
||||
public class OrganizationGroupImportData
|
||||
{
|
||||
/// <summary>
|
||||
/// The collection of groups that are being imported.
|
||||
/// </summary>
|
||||
public readonly IEnumerable<ImportedGroup> Groups;
|
||||
|
||||
/// <summary>
|
||||
/// Collection of groups that already exist in the organization.
|
||||
/// </summary>
|
||||
public readonly ICollection<Group> ExistingGroups;
|
||||
|
||||
/// <summary>
|
||||
/// Existing groups with ExternalId set.
|
||||
/// </summary>
|
||||
public readonly IEnumerable<Group> ExistingExternalGroups;
|
||||
|
||||
/// <summary>
|
||||
/// Mapping of imported groups keyed by their ExternalId.
|
||||
/// </summary>
|
||||
public readonly IDictionary<string, ImportedGroup> GroupsDict;
|
||||
|
||||
public OrganizationGroupImportData(IEnumerable<ImportedGroup> groups, ICollection<Group> existingGroups)
|
||||
{
|
||||
Groups = groups;
|
||||
GroupsDict = groups.ToDictionary(g => g.Group.ExternalId!);
|
||||
ExistingGroups = existingGroups;
|
||||
ExistingExternalGroups = existingGroups.Where(u => !string.IsNullOrWhiteSpace(u.ExternalId)).ToList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
#nullable enable
|
||||
|
||||
using Bit.Core.Models.Business;
|
||||
namespace Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
|
||||
public class OrganizationUserImportData
|
||||
{
|
||||
/// <summary>
|
||||
/// Set of user ExternalIds that are being imported
|
||||
/// </summary>
|
||||
public readonly HashSet<string> ImportedExternalIds;
|
||||
/// <summary>
|
||||
/// All existing OrganizationUsers for the organization
|
||||
/// </summary>
|
||||
public readonly ICollection<OrganizationUserUserDetails> ExistingUsers;
|
||||
/// <summary>
|
||||
/// Existing OrganizationUsers with ExternalIds set.
|
||||
/// </summary>
|
||||
public readonly IEnumerable<OrganizationUserUserDetails> ExistingExternalUsers;
|
||||
/// <summary>
|
||||
/// Mapping of an existing users's ExternalId to their Id
|
||||
/// </summary>
|
||||
public readonly Dictionary<string, Guid> ExistingExternalUsersIdDict;
|
||||
|
||||
public OrganizationUserImportData(ICollection<OrganizationUserUserDetails> existingUsers, IEnumerable<ImportedOrganizationUser> importedUsers)
|
||||
{
|
||||
ImportedExternalIds = new HashSet<string>(importedUsers?.Select(u => u.ExternalId) ?? new List<string>());
|
||||
ExistingUsers = existingUsers;
|
||||
ExistingExternalUsers = ExistingUsers.Where(u => !string.IsNullOrWhiteSpace(u.ExternalId)).ToList();
|
||||
ExistingExternalUsersIdDict = ExistingExternalUsers.ToDictionary(u => u.ExternalId, u => u.Id);
|
||||
}
|
||||
}
|
||||
@@ -19,4 +19,14 @@ public interface IInviteOrganizationUsersCommand
|
||||
/// </param>
|
||||
/// <returns>Response from InviteScimOrganiation<see cref="ScimInviteOrganizationUsersResponse"/></returns>
|
||||
Task<CommandResult<ScimInviteOrganizationUsersResponse>> InviteScimOrganizationUserAsync(InviteOrganizationUsersRequest request);
|
||||
/// <summary>
|
||||
/// Sends invitations to add imported organization users via the public API.
|
||||
/// This can be a Success or a Failure. Failure will contain the Error along with a representation of the errored value.
|
||||
/// Success will be the successful return object.
|
||||
/// </summary>
|
||||
/// <param name="request">
|
||||
/// Contains the details for inviting the imported organization users.
|
||||
/// </param>
|
||||
/// <returns>Response from InviteOrganiationUsersAsync<see cref="InviteOrganizationUsersResponse"/></returns>
|
||||
Task<CommandResult<InviteOrganizationUsersResponse>> InviteImportedOrganizationUsersAsync(InviteOrganizationUsersRequest request);
|
||||
}
|
||||
|
||||
@@ -12,13 +12,13 @@ using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.AdminConsole.Utilities.Commands;
|
||||
using Bit.Core.AdminConsole.Utilities.Errors;
|
||||
using Bit.Core.AdminConsole.Utilities.Validation;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Business;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using OrganizationUserInvite = Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models.OrganizationUserInvite;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;
|
||||
|
||||
@@ -74,6 +74,40 @@ public class InviteOrganizationUsersCommand(IEventService eventService,
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<CommandResult<InviteOrganizationUsersResponse>> InviteImportedOrganizationUsersAsync(InviteOrganizationUsersRequest request)
|
||||
{
|
||||
var result = await InviteOrganizationUsersAsync(request);
|
||||
|
||||
switch (result)
|
||||
{
|
||||
case Failure<InviteOrganizationUsersResponse> failure:
|
||||
return new Failure<InviteOrganizationUsersResponse>(
|
||||
new Error<InviteOrganizationUsersResponse>(
|
||||
failure.Error.Message,
|
||||
new InviteOrganizationUsersResponse(failure.Error.ErroredValue.InvitedUsers, request.InviteOrganization.OrganizationId)
|
||||
)
|
||||
);
|
||||
|
||||
case Success<InviteOrganizationUsersResponse> success when success.Value.InvitedUsers.Any():
|
||||
|
||||
List<(OrganizationUser, EventType, EventSystemUser, DateTime?)> events = new List<(OrganizationUser, EventType, EventSystemUser, DateTime?)>();
|
||||
foreach (var user in success.Value.InvitedUsers)
|
||||
{
|
||||
events.Add((user, EventType.OrganizationUser_Invited, EventSystemUser.PublicApi, request.PerformedAt.UtcDateTime));
|
||||
}
|
||||
|
||||
await eventService.LogOrganizationUserEventsAsync(events);
|
||||
|
||||
return new Success<InviteOrganizationUsersResponse>(new InviteOrganizationUsersResponse(success.Value.InvitedUsers, request.InviteOrganization.OrganizationId)
|
||||
);
|
||||
|
||||
default:
|
||||
return new Failure<InviteOrganizationUsersResponse>(
|
||||
new InvalidResultTypeError<InviteOrganizationUsersResponse>(
|
||||
new InviteOrganizationUsersResponse(request.InviteOrganization.OrganizationId)));
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<CommandResult<InviteOrganizationUsersResponse>> InviteOrganizationUsersAsync(InviteOrganizationUsersRequest request)
|
||||
{
|
||||
var invitesToSend = (await FilterExistingUsersAsync(request)).ToArray();
|
||||
@@ -141,7 +175,7 @@ public class InviteOrganizationUsersCommand(IEventService eventService,
|
||||
organizationId: organization!.Id));
|
||||
}
|
||||
|
||||
private async Task<IEnumerable<OrganizationUserInvite>> FilterExistingUsersAsync(InviteOrganizationUsersRequest request)
|
||||
private async Task<IEnumerable<OrganizationUserInviteCommandModel>> FilterExistingUsersAsync(InviteOrganizationUsersRequest request)
|
||||
{
|
||||
var existingEmails = new HashSet<string>(await organizationUserRepository.SelectKnownEmailsAsync(
|
||||
request.InviteOrganization.OrganizationId, request.Invites.Select(i => i.Email), false),
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUse
|
||||
|
||||
public static class CreateOrganizationUserExtensions
|
||||
{
|
||||
public static CreateOrganizationUser MapToDataModel(this OrganizationUserInvite organizationUserInvite,
|
||||
public static CreateOrganizationUser MapToDataModel(this OrganizationUserInviteCommandModel organizationUserInvite,
|
||||
DateTimeOffset performedAt,
|
||||
InviteOrganization organization) =>
|
||||
new()
|
||||
|
||||
@@ -4,12 +4,12 @@ namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUse
|
||||
|
||||
public class InviteOrganizationUsersRequest
|
||||
{
|
||||
public OrganizationUserInvite[] Invites { get; } = [];
|
||||
public OrganizationUserInviteCommandModel[] Invites { get; } = [];
|
||||
public InviteOrganization InviteOrganization { get; }
|
||||
public Guid PerformedBy { get; }
|
||||
public DateTimeOffset PerformedAt { get; }
|
||||
|
||||
public InviteOrganizationUsersRequest(OrganizationUserInvite[] invites,
|
||||
public InviteOrganizationUsersRequest(OrganizationUserInviteCommandModel[] invites,
|
||||
InviteOrganization inviteOrganization,
|
||||
Guid performedBy,
|
||||
DateTimeOffset performedAt)
|
||||
|
||||
@@ -32,7 +32,7 @@ public class InviteOrganizationUsersValidationRequest
|
||||
SecretsManagerSubscriptionUpdate = smSubscriptionUpdate;
|
||||
}
|
||||
|
||||
public OrganizationUserInvite[] Invites { get; init; } = [];
|
||||
public OrganizationUserInviteCommandModel[] Invites { get; init; } = [];
|
||||
public InviteOrganization InviteOrganization { get; init; }
|
||||
public Guid PerformedBy { get; init; }
|
||||
public DateTimeOffset PerformedAt { get; init; }
|
||||
|
||||
@@ -7,7 +7,7 @@ using static Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Invite
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
|
||||
|
||||
public class OrganizationUserInvite
|
||||
public class OrganizationUserInviteCommandModel
|
||||
{
|
||||
public string Email { get; private init; }
|
||||
public CollectionAccessSelection[] AssignedCollections { get; private init; }
|
||||
@@ -17,7 +17,7 @@ public class OrganizationUserInvite
|
||||
public bool AccessSecretsManager { get; private init; }
|
||||
public Guid[] Groups { get; private init; }
|
||||
|
||||
public OrganizationUserInvite(string email, string externalId) :
|
||||
public OrganizationUserInviteCommandModel(string email, string externalId) :
|
||||
this(
|
||||
email: email,
|
||||
assignedCollections: [],
|
||||
@@ -29,7 +29,7 @@ public class OrganizationUserInvite
|
||||
{
|
||||
}
|
||||
|
||||
public OrganizationUserInvite(OrganizationUserInvite invite, bool accessSecretsManager) :
|
||||
public OrganizationUserInviteCommandModel(OrganizationUserInviteCommandModel invite, bool accessSecretsManager) :
|
||||
this(invite.Email,
|
||||
invite.AssignedCollections,
|
||||
invite.Groups,
|
||||
@@ -41,7 +41,7 @@ public class OrganizationUserInvite
|
||||
|
||||
}
|
||||
|
||||
public OrganizationUserInvite(string email,
|
||||
public OrganizationUserInviteCommandModel(string email,
|
||||
IEnumerable<CollectionAccessSelection> assignedCollections,
|
||||
IEnumerable<Guid> groups,
|
||||
OrganizationUserType type,
|
||||
@@ -9,7 +9,6 @@ using Bit.Core.Models.Business;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using OrganizationUserInvite = Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models.OrganizationUserInvite;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation;
|
||||
|
||||
@@ -41,7 +40,7 @@ public class InviteOrganizationUsersValidator(
|
||||
request = new InviteOrganizationUsersValidationRequest(request)
|
||||
{
|
||||
Invites = request.Invites
|
||||
.Select(x => new OrganizationUserInvite(x, accessSecretsManager: true))
|
||||
.Select(x => new OrganizationUserInviteCommandModel(x, accessSecretsManager: true))
|
||||
.ToArray()
|
||||
};
|
||||
}
|
||||
|
||||
@@ -113,6 +113,7 @@ public static class FeatureFlagKeys
|
||||
public const string ScimInviteUserOptimization = "pm-16811-optimize-invite-user-flow-to-fail-fast";
|
||||
public const string EventBasedOrganizationIntegrations = "event-based-organization-integrations";
|
||||
public const string SeparateCustomRolePermissions = "pm-19917-separate-custom-role-permissions";
|
||||
public const string ImportAsyncRefactor = "pm-22583-refactor-import-async";
|
||||
public const string CreateDefaultLocation = "pm-19467-create-default-location";
|
||||
|
||||
/* Auth Team */
|
||||
|
||||
@@ -190,6 +190,7 @@ public static class OrganizationServiceCollectionExtensions
|
||||
services.AddScoped<IInviteUsersPasswordManagerValidator, InviteUsersPasswordManagerValidator>();
|
||||
services.AddScoped<IInviteUsersEnvironmentValidator, InviteUsersEnvironmentValidator>();
|
||||
services.AddScoped<IInitPendingOrganizationCommand, InitPendingOrganizationCommand>();
|
||||
services.AddScoped<IImportOrganizationUsersAndGroupsCommand, ImportOrganizationUsersAndGroupsCommand>();
|
||||
}
|
||||
|
||||
// TODO: move to OrganizationSubscriptionServiceCollectionExtensions when OrganizationUser methods are moved out of
|
||||
|
||||
@@ -0,0 +1,312 @@
|
||||
using System.Net;
|
||||
using Bit.Api.AdminConsole.Public.Models.Request;
|
||||
using Bit.Api.IntegrationTest.Factories;
|
||||
using Bit.Api.IntegrationTest.Helpers;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Api.IntegrationTest.AdminConsole.Import;
|
||||
|
||||
public class ImportOrganizationUsersAndGroupsCommandTests : IClassFixture<ApiApplicationFactory>, IAsyncLifetime
|
||||
{
|
||||
private readonly HttpClient _client;
|
||||
private readonly ApiApplicationFactory _factory;
|
||||
private readonly LoginHelper _loginHelper;
|
||||
private Organization _organization = null!;
|
||||
private string _ownerEmail = null!;
|
||||
|
||||
public ImportOrganizationUsersAndGroupsCommandTests(ApiApplicationFactory factory)
|
||||
{
|
||||
_factory = factory;
|
||||
_factory.SubstituteService((IFeatureService featureService)
|
||||
=> featureService.IsEnabled(FeatureFlagKeys.ImportAsyncRefactor)
|
||||
.Returns(true));
|
||||
_client = _factory.CreateClient();
|
||||
_loginHelper = new LoginHelper(_factory, _client);
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
// Create the owner account
|
||||
_ownerEmail = $"integration-test{Guid.NewGuid()}@bitwarden.com";
|
||||
await _factory.LoginWithNewAccount(_ownerEmail);
|
||||
|
||||
// Create the organization
|
||||
(_organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, plan: PlanType.EnterpriseAnnually2023,
|
||||
ownerEmail: _ownerEmail, passwordManagerSeats: 10, paymentMethod: PaymentMethodType.Card);
|
||||
|
||||
// Authorize with the organization api key
|
||||
await _loginHelper.LoginWithOrganizationApiKeyAsync(_organization.Id);
|
||||
}
|
||||
|
||||
public Task DisposeAsync()
|
||||
{
|
||||
_client.Dispose();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Import_Existing_Organization_User_Succeeds()
|
||||
{
|
||||
var (email, ou) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, _organization.Id,
|
||||
OrganizationUserType.User);
|
||||
|
||||
var externalId = Guid.NewGuid().ToString();
|
||||
var request = new OrganizationImportRequestModel();
|
||||
request.LargeImport = false;
|
||||
request.OverwriteExisting = false;
|
||||
request.Groups = [];
|
||||
request.Members = [
|
||||
new OrganizationImportRequestModel.OrganizationImportMemberRequestModel
|
||||
{
|
||||
Email = email,
|
||||
ExternalId = externalId,
|
||||
Deleted = false
|
||||
}
|
||||
];
|
||||
|
||||
var response = await _client.PostAsync($"/public/organization/import", JsonContent.Create(request));
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
|
||||
// Assert against the database values
|
||||
var organizationUserRepository = _factory.GetService<IOrganizationUserRepository>();
|
||||
var orgUser = await organizationUserRepository.GetByIdAsync(ou.Id);
|
||||
|
||||
Assert.NotNull(orgUser);
|
||||
Assert.Equal(ou.Id, orgUser.Id);
|
||||
Assert.Equal(email, orgUser.Email);
|
||||
Assert.Equal(OrganizationUserType.User, orgUser.Type);
|
||||
Assert.Equal(externalId, orgUser.ExternalId);
|
||||
Assert.Equal(OrganizationUserStatusType.Confirmed, orgUser.Status);
|
||||
Assert.Equal(_organization.Id, orgUser.OrganizationId);
|
||||
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Import_New_Organization_User_Succeeds()
|
||||
{
|
||||
var email = $"integration-test{Guid.NewGuid()}@bitwarden.com";
|
||||
await _factory.LoginWithNewAccount(email);
|
||||
|
||||
var externalId = Guid.NewGuid().ToString();
|
||||
var request = new OrganizationImportRequestModel();
|
||||
request.LargeImport = false;
|
||||
request.OverwriteExisting = false;
|
||||
request.Groups = [];
|
||||
request.Members = [
|
||||
new OrganizationImportRequestModel.OrganizationImportMemberRequestModel
|
||||
{
|
||||
Email = email,
|
||||
ExternalId = externalId,
|
||||
Deleted = false
|
||||
}
|
||||
];
|
||||
|
||||
var response = await _client.PostAsync($"/public/organization/import", JsonContent.Create(request));
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
|
||||
// Assert against the database values
|
||||
var organizationUserRepository = _factory.GetService<IOrganizationUserRepository>();
|
||||
var orgUser = await organizationUserRepository.GetByOrganizationEmailAsync(_organization.Id, email);
|
||||
|
||||
Assert.NotNull(orgUser);
|
||||
Assert.Equal(email, orgUser.Email);
|
||||
Assert.Equal(OrganizationUserType.User, orgUser.Type);
|
||||
Assert.Equal(externalId, orgUser.ExternalId);
|
||||
Assert.Equal(OrganizationUserStatusType.Invited, orgUser.Status);
|
||||
Assert.Equal(_organization.Id, orgUser.OrganizationId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Import_New_And_Existing_Organization_Users_Succeeds()
|
||||
{
|
||||
// Existing organization user
|
||||
var (existingEmail, ou) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, _organization.Id,
|
||||
OrganizationUserType.User);
|
||||
var existingExternalId = Guid.NewGuid().ToString();
|
||||
|
||||
// New organization user
|
||||
var newEmail = $"integration-test{Guid.NewGuid()}@bitwarden.com";
|
||||
await _factory.LoginWithNewAccount(newEmail);
|
||||
var newExternalId = Guid.NewGuid().ToString();
|
||||
|
||||
var request = new OrganizationImportRequestModel();
|
||||
request.LargeImport = false;
|
||||
request.OverwriteExisting = false;
|
||||
request.Groups = [];
|
||||
request.Members = [
|
||||
new OrganizationImportRequestModel.OrganizationImportMemberRequestModel
|
||||
{
|
||||
Email = existingEmail,
|
||||
ExternalId = existingExternalId,
|
||||
Deleted = false
|
||||
},
|
||||
new OrganizationImportRequestModel.OrganizationImportMemberRequestModel
|
||||
{
|
||||
Email = newEmail,
|
||||
ExternalId = newExternalId,
|
||||
Deleted = false
|
||||
}
|
||||
];
|
||||
|
||||
var response = await _client.PostAsync($"/public/organization/import", JsonContent.Create(request));
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
|
||||
// Assert against the database values
|
||||
var organizationUserRepository = _factory.GetService<IOrganizationUserRepository>();
|
||||
|
||||
// Existing user
|
||||
var existingOrgUser = await organizationUserRepository.GetByIdAsync(ou.Id);
|
||||
Assert.NotNull(existingOrgUser);
|
||||
Assert.Equal(existingEmail, existingOrgUser.Email);
|
||||
Assert.Equal(OrganizationUserType.User, existingOrgUser.Type);
|
||||
Assert.Equal(existingExternalId, existingOrgUser.ExternalId);
|
||||
Assert.Equal(OrganizationUserStatusType.Confirmed, existingOrgUser.Status);
|
||||
Assert.Equal(_organization.Id, existingOrgUser.OrganizationId);
|
||||
|
||||
// New User
|
||||
var newOrgUser = await organizationUserRepository.GetByOrganizationEmailAsync(_organization.Id, newEmail);
|
||||
Assert.NotNull(newOrgUser);
|
||||
Assert.Equal(newEmail, newOrgUser.Email);
|
||||
Assert.Equal(OrganizationUserType.User, newOrgUser.Type);
|
||||
Assert.Equal(newExternalId, newOrgUser.ExternalId);
|
||||
Assert.Equal(OrganizationUserStatusType.Invited, newOrgUser.Status);
|
||||
Assert.Equal(_organization.Id, newOrgUser.OrganizationId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Import_Existing_Groups_Succeeds()
|
||||
{
|
||||
var organizationUserRepository = _factory.GetService<IOrganizationUserRepository>();
|
||||
var group = await OrganizationTestHelpers.CreateGroup(_factory, _organization.Id);
|
||||
var request = new OrganizationImportRequestModel();
|
||||
var addedMember = new OrganizationImportRequestModel.OrganizationImportMemberRequestModel
|
||||
{
|
||||
Email = "test@test.com",
|
||||
ExternalId = "bwtest-externalId",
|
||||
Deleted = false
|
||||
};
|
||||
|
||||
request.LargeImport = false;
|
||||
request.OverwriteExisting = false;
|
||||
request.Groups = [
|
||||
new OrganizationImportRequestModel.OrganizationImportGroupRequestModel
|
||||
{
|
||||
Name = "new-name",
|
||||
ExternalId = "bwtest-externalId",
|
||||
MemberExternalIds = []
|
||||
}
|
||||
];
|
||||
request.Members = [addedMember];
|
||||
|
||||
var response = await _client.PostAsync($"/public/organization/import", JsonContent.Create(request));
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
|
||||
// Assert against the database values
|
||||
var groupRepository = _factory.GetService<IGroupRepository>();
|
||||
var existingGroups = (await groupRepository.GetManyByOrganizationIdAsync(_organization.Id)).ToArray();
|
||||
|
||||
// Assert that we are actually updating the existing group, not adding a new one.
|
||||
Assert.Single(existingGroups);
|
||||
Assert.NotNull(existingGroups[0]);
|
||||
Assert.Equal(group.Id, existingGroups[0].Id);
|
||||
Assert.Equal("new-name", existingGroups[0].Name);
|
||||
Assert.Equal(group.ExternalId, existingGroups[0].ExternalId);
|
||||
|
||||
var addedOrgUser = await organizationUserRepository.GetByOrganizationEmailAsync(_organization.Id, addedMember.Email);
|
||||
Assert.NotNull(addedOrgUser);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Import_New_Groups_Succeeds()
|
||||
{
|
||||
var group = new Group
|
||||
{
|
||||
OrganizationId = _organization.Id,
|
||||
ExternalId = new Guid().ToString(),
|
||||
Name = "bwtest1"
|
||||
};
|
||||
|
||||
var request = new OrganizationImportRequestModel();
|
||||
request.LargeImport = false;
|
||||
request.OverwriteExisting = false;
|
||||
request.Groups = [
|
||||
new OrganizationImportRequestModel.OrganizationImportGroupRequestModel
|
||||
{
|
||||
Name = group.Name,
|
||||
ExternalId = group.ExternalId,
|
||||
MemberExternalIds = []
|
||||
}
|
||||
];
|
||||
request.Members = [];
|
||||
|
||||
var response = await _client.PostAsync($"/public/organization/import", JsonContent.Create(request));
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
|
||||
// Assert against the database values
|
||||
var groupRepository = _factory.GetService<IGroupRepository>();
|
||||
var existingGroups = await groupRepository.GetManyByOrganizationIdAsync(_organization.Id);
|
||||
var existingGroup = existingGroups.Where(g => g.ExternalId == group.ExternalId).FirstOrDefault();
|
||||
|
||||
Assert.NotNull(existingGroup);
|
||||
Assert.Equal(existingGroup.Name, group.Name);
|
||||
Assert.Equal(existingGroup.ExternalId, group.ExternalId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Import_New_And_Existing_Groups_Succeeds()
|
||||
{
|
||||
var existingGroup = await OrganizationTestHelpers.CreateGroup(_factory, _organization.Id);
|
||||
|
||||
var newGroup = new Group
|
||||
{
|
||||
OrganizationId = _organization.Id,
|
||||
ExternalId = "test",
|
||||
Name = "bwtest1"
|
||||
};
|
||||
|
||||
var request = new OrganizationImportRequestModel();
|
||||
request.LargeImport = false;
|
||||
request.OverwriteExisting = false;
|
||||
request.Groups = [
|
||||
new OrganizationImportRequestModel.OrganizationImportGroupRequestModel
|
||||
{
|
||||
Name = "new-name",
|
||||
ExternalId = existingGroup.ExternalId,
|
||||
MemberExternalIds = []
|
||||
},
|
||||
new OrganizationImportRequestModel.OrganizationImportGroupRequestModel
|
||||
{
|
||||
Name = newGroup.Name,
|
||||
ExternalId = newGroup.ExternalId,
|
||||
MemberExternalIds = []
|
||||
}
|
||||
];
|
||||
request.Members = [];
|
||||
|
||||
var response = await _client.PostAsync($"/public/organization/import", JsonContent.Create(request));
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
|
||||
// Assert against the database values
|
||||
var groupRepository = _factory.GetService<IGroupRepository>();
|
||||
var groups = await groupRepository.GetManyByOrganizationIdAsync(_organization.Id);
|
||||
|
||||
var newGroupInDb = groups.Where(g => g.ExternalId == newGroup.ExternalId).FirstOrDefault();
|
||||
Assert.NotNull(newGroupInDb);
|
||||
Assert.Equal(newGroupInDb.Name, newGroup.Name);
|
||||
Assert.Equal(newGroupInDb.ExternalId, newGroup.ExternalId);
|
||||
|
||||
var existingGroupInDb = groups.Where(g => g.ExternalId == existingGroup.ExternalId).FirstOrDefault();
|
||||
Assert.NotNull(existingGroupInDb);
|
||||
Assert.Equal(existingGroup.Id, existingGroupInDb.Id);
|
||||
Assert.Equal("new-name", existingGroupInDb.Name);
|
||||
Assert.Equal(existingGroup.ExternalId, existingGroupInDb.ExternalId);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
using Bit.Api.IntegrationTest.Factories;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
@@ -78,6 +79,7 @@ public static class OrganizationTestHelpers
|
||||
Status = userStatusType,
|
||||
ExternalId = null,
|
||||
AccessSecretsManager = accessSecretsManager,
|
||||
Email = userEmail
|
||||
};
|
||||
|
||||
if (permissions != null)
|
||||
@@ -130,4 +132,20 @@ public static class OrganizationTestHelpers
|
||||
|
||||
await organizationDomainRepository.CreateAsync(verifiedDomain);
|
||||
}
|
||||
|
||||
public static async Task<Group> CreateGroup(ApiApplicationFactory factory, Guid organizationId)
|
||||
{
|
||||
|
||||
var groupRepository = factory.GetService<IGroupRepository>();
|
||||
var group = new Group
|
||||
{
|
||||
OrganizationId = organizationId,
|
||||
Id = new Guid(),
|
||||
ExternalId = "bwtest-externalId",
|
||||
Name = "bwtest"
|
||||
};
|
||||
|
||||
await groupRepository.CreateAsync(group, new List<CollectionAccessSelection>());
|
||||
return group;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ public class InviteOrganizationUsersRequestTests
|
||||
public void Constructor_WhenPassedInvalidEmail_ThrowsException(string email, OrganizationUserType type, Permissions permissions, string externalId)
|
||||
{
|
||||
var exception = Assert.Throws<BadRequestException>(() =>
|
||||
new OrganizationUserInvite(email, [], [], type, permissions, externalId, false));
|
||||
new OrganizationUserInviteCommandModel(email, [], [], type, permissions, externalId, false));
|
||||
|
||||
Assert.Contains(InvalidEmailErrorMessage, exception.Message);
|
||||
}
|
||||
@@ -33,7 +33,7 @@ public class InviteOrganizationUsersRequestTests
|
||||
};
|
||||
|
||||
var exception = Assert.Throws<BadRequestException>(() =>
|
||||
new OrganizationUserInvite(
|
||||
new OrganizationUserInviteCommandModel(
|
||||
email: validEmail,
|
||||
assignedCollections: [invalidCollectionConfiguration],
|
||||
groups: [],
|
||||
@@ -51,7 +51,7 @@ public class InviteOrganizationUsersRequestTests
|
||||
const string validEmail = "test@email.com";
|
||||
var validCollectionConfiguration = new CollectionAccessSelection { Id = Guid.NewGuid(), Manage = true };
|
||||
|
||||
var invite = new OrganizationUserInvite(
|
||||
var invite = new OrganizationUserInviteCommandModel(
|
||||
email: validEmail,
|
||||
assignedCollections: [validCollectionConfiguration],
|
||||
groups: [],
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
using Bit.Core.AdminConsole.Models.Business;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers;
|
||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Business;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Test.AutoFixture.OrganizationFixtures;
|
||||
using Bit.Core.Tokens;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Bit.Test.Common.Fakes;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
using Organization = Bit.Core.AdminConsole.Entities.Organization;
|
||||
|
||||
namespace Bit.Core.Test.OrganizationFeatures.OrganizationUsers;
|
||||
|
||||
public class ImportOrganizationUsersAndGroupsCommandTests
|
||||
{
|
||||
|
||||
private readonly IDataProtectorTokenFactory<OrgUserInviteTokenable> _orgUserInviteTokenDataFactory = new FakeDataProtectorTokenFactory<OrgUserInviteTokenable>();
|
||||
|
||||
[Theory, PaidOrganizationCustomize, BitAutoData]
|
||||
public async Task OrgImportCallsInviteOrgUserCommand(
|
||||
SutProvider<ImportOrganizationUsersAndGroupsCommand> sutProvider,
|
||||
Organization org,
|
||||
List<OrganizationUserUserDetails> existingUsers,
|
||||
List<ImportedOrganizationUser> importedUsers,
|
||||
List<ImportedGroup> newGroups)
|
||||
{
|
||||
SetupOrganizationConfigForImport(sutProvider, org, existingUsers, importedUsers);
|
||||
|
||||
var orgUsers = new List<OrganizationUser>();
|
||||
|
||||
// fix mocked email format, mock OrganizationUsers.
|
||||
foreach (var u in importedUsers)
|
||||
{
|
||||
u.Email += "@bitwardentest.com";
|
||||
orgUsers.Add(new OrganizationUser { Email = u.Email, ExternalId = u.ExternalId });
|
||||
}
|
||||
|
||||
importedUsers.Add(new ImportedOrganizationUser
|
||||
{
|
||||
Email = existingUsers.First().Email,
|
||||
ExternalId = existingUsers.First().ExternalId
|
||||
});
|
||||
|
||||
|
||||
existingUsers.First().Type = OrganizationUserType.Owner;
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(org.Id).Returns(org);
|
||||
|
||||
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||
SetupOrgUserRepositoryCreateManyAsyncMock(organizationUserRepository);
|
||||
|
||||
sutProvider.GetDependency<IPaymentService>().HasSecretsManagerStandalone(org).Returns(true);
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>().GetManyDetailsByOrganizationAsync(org.Id).Returns(existingUsers);
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetOccupiedSeatCountByOrganizationIdAsync(org.Id).Returns(
|
||||
new OrganizationSeatCounts
|
||||
{
|
||||
Users = existingUsers.Count,
|
||||
Sponsored = 0
|
||||
});
|
||||
sutProvider.GetDependency<ICurrentContext>().ManageUsers(org.Id).Returns(true);
|
||||
sutProvider.GetDependency<IOrganizationService>().InviteUsersAsync(org.Id, Guid.Empty, EventSystemUser.PublicApi,
|
||||
Arg.Any<IEnumerable<(OrganizationUserInvite, string)>>())
|
||||
.Returns(orgUsers);
|
||||
|
||||
await sutProvider.Sut.ImportAsync(org.Id, newGroups, importedUsers, new List<string>(), false);
|
||||
|
||||
var expectedNewUsersCount = importedUsers.Count - 1;
|
||||
|
||||
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);
|
||||
|
||||
// Send Invites
|
||||
await sutProvider.GetDependency<IOrganizationService>().Received(1).
|
||||
InviteUsersAsync(org.Id, Guid.Empty, EventSystemUser.PublicApi,
|
||||
Arg.Is<IEnumerable<(OrganizationUserInvite, string)>>(invites => invites.Count() == expectedNewUsersCount));
|
||||
|
||||
// Send events
|
||||
await sutProvider.GetDependency<IEventService>().Received(1)
|
||||
.LogOrganizationUserEventsAsync(Arg.Any<IEnumerable<(OrganizationUserUserDetails, EventType, EventSystemUser, DateTime?)>>());
|
||||
}
|
||||
|
||||
[Theory, PaidOrganizationCustomize, BitAutoData]
|
||||
public async Task OrgImportCreateNewUsersAndMarryExistingUser(
|
||||
SutProvider<ImportOrganizationUsersAndGroupsCommand> sutProvider,
|
||||
Organization org,
|
||||
List<OrganizationUserUserDetails> existingUsers,
|
||||
List<ImportedOrganizationUser> importedUsers,
|
||||
List<ImportedGroup> newGroups)
|
||||
{
|
||||
SetupOrganizationConfigForImport(sutProvider, org, existingUsers, importedUsers);
|
||||
|
||||
var orgUsers = new List<OrganizationUser>();
|
||||
var reInvitedUser = existingUsers.First();
|
||||
// Existing user has no external ID. This will make the SUT call UpsertManyAsync
|
||||
reInvitedUser.ExternalId = "";
|
||||
|
||||
// Mock an existing org user for this "existing" user
|
||||
var reInvitedOrgUser = new OrganizationUser { Email = reInvitedUser.Email, Id = reInvitedUser.Id };
|
||||
|
||||
// fix email formatting, mock orgUsers to be returned
|
||||
foreach (var u in existingUsers)
|
||||
{
|
||||
u.Email += "@bitwardentest.com";
|
||||
orgUsers.Add(new OrganizationUser { Email = u.Email, ExternalId = u.ExternalId });
|
||||
}
|
||||
foreach (var u in importedUsers)
|
||||
{
|
||||
u.Email += "@bitwardentest.com";
|
||||
orgUsers.Add(new OrganizationUser { Email = u.Email, ExternalId = u.ExternalId });
|
||||
}
|
||||
|
||||
// add the existing user to be re-imported
|
||||
importedUsers.Add(new ImportedOrganizationUser
|
||||
{
|
||||
Email = reInvitedUser.Email,
|
||||
ExternalId = reInvitedUser.Email,
|
||||
});
|
||||
|
||||
var expectedNewUsersCount = importedUsers.Count - 1;
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(org.Id).Returns(org);
|
||||
|
||||
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||
SetupOrgUserRepositoryCreateManyAsyncMock(organizationUserRepository);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>().GetManyAsync(Arg.Any<IEnumerable<Guid>>())
|
||||
.Returns(new List<OrganizationUser>([reInvitedOrgUser]));
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>().GetManyDetailsByOrganizationAsync(org.Id).Returns(existingUsers);
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetOccupiedSeatCountByOrganizationIdAsync(org.Id).Returns(
|
||||
new OrganizationSeatCounts
|
||||
{
|
||||
Users = existingUsers.Count,
|
||||
Sponsored = 0
|
||||
});
|
||||
|
||||
sutProvider.GetDependency<IOrganizationService>().InviteUsersAsync(org.Id, Guid.Empty, EventSystemUser.PublicApi,
|
||||
Arg.Any<IEnumerable<(OrganizationUserInvite, string)>>())
|
||||
.Returns(orgUsers);
|
||||
|
||||
await sutProvider.Sut.ImportAsync(org.Id, newGroups, importedUsers, new List<string>(), false);
|
||||
|
||||
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 && users.First() == reInvitedOrgUser));
|
||||
|
||||
// Send Invites
|
||||
await sutProvider.GetDependency<IOrganizationService>().Received(1).
|
||||
InviteUsersAsync(org.Id, Guid.Empty, EventSystemUser.PublicApi,
|
||||
Arg.Is<IEnumerable<(OrganizationUserInvite, string)>>(invites => invites.Count() == expectedNewUsersCount));
|
||||
|
||||
// Send events
|
||||
await sutProvider.GetDependency<IEventService>().Received(1)
|
||||
.LogOrganizationUserEventsAsync(Arg.Any<IEnumerable<(OrganizationUserUserDetails, EventType, EventSystemUser, DateTime?)>>());
|
||||
}
|
||||
|
||||
private void SetupOrganizationConfigForImport(
|
||||
SutProvider<ImportOrganizationUsersAndGroupsCommand> sutProvider,
|
||||
Organization org,
|
||||
List<OrganizationUserUserDetails> existingUsers,
|
||||
List<ImportedOrganizationUser> importedUsers)
|
||||
{
|
||||
// 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 = importedUsers.Count + existingUsers.Count + 1;
|
||||
}
|
||||
|
||||
// Must set real guids in order for dictionary of guids to not throw aggregate exceptions
|
||||
private void SetupOrgUserRepositoryCreateManyAsyncMock(IOrganizationUserRepository organizationUserRepository)
|
||||
{
|
||||
organizationUserRepository.CreateManyAsync(Arg.Any<IEnumerable<OrganizationUser>>()).Returns(
|
||||
info =>
|
||||
{
|
||||
var orgUsers = info.Arg<IEnumerable<OrganizationUser>>();
|
||||
foreach (var orgUser in orgUsers)
|
||||
{
|
||||
orgUser.Id = Guid.NewGuid();
|
||||
}
|
||||
|
||||
return Task.FromResult<ICollection<Guid>>(orgUsers.Select(u => u.Id).ToList());
|
||||
}
|
||||
);
|
||||
|
||||
organizationUserRepository.CreateAsync(Arg.Any<OrganizationUser>(), Arg.Any<IEnumerable<CollectionAccessSelection>>()).Returns(
|
||||
info =>
|
||||
{
|
||||
var orgUser = info.Arg<OrganizationUser>();
|
||||
orgUser.Id = Guid.NewGuid();
|
||||
return Task.FromResult<Guid>(orgUser.Id);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,6 @@ using NSubstitute;
|
||||
using NSubstitute.ExceptionExtensions;
|
||||
using Xunit;
|
||||
using static Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Helpers.InviteUserOrganizationValidationRequestHelpers;
|
||||
using OrganizationUserInvite = Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models.OrganizationUserInvite;
|
||||
|
||||
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;
|
||||
|
||||
@@ -54,7 +53,7 @@ public class InviteOrganizationUserCommandTests
|
||||
|
||||
var request = new InviteOrganizationUsersRequest(
|
||||
invites: [
|
||||
new OrganizationUserInvite(
|
||||
new OrganizationUserInviteCommandModel(
|
||||
email: user.Email,
|
||||
assignedCollections: [],
|
||||
groups: [],
|
||||
@@ -112,7 +111,7 @@ public class InviteOrganizationUserCommandTests
|
||||
|
||||
var request = new InviteOrganizationUsersRequest(
|
||||
invites: [
|
||||
new OrganizationUserInvite(
|
||||
new OrganizationUserInviteCommandModel(
|
||||
email: orgUser.Email,
|
||||
assignedCollections: [],
|
||||
groups: [],
|
||||
@@ -182,7 +181,7 @@ public class InviteOrganizationUserCommandTests
|
||||
|
||||
var request = new InviteOrganizationUsersRequest(
|
||||
invites: [
|
||||
new OrganizationUserInvite(
|
||||
new OrganizationUserInviteCommandModel(
|
||||
email: user.Email,
|
||||
assignedCollections: [],
|
||||
groups: [],
|
||||
@@ -257,7 +256,7 @@ public class InviteOrganizationUserCommandTests
|
||||
|
||||
var request = new InviteOrganizationUsersRequest(
|
||||
invites: [
|
||||
new OrganizationUserInvite(
|
||||
new OrganizationUserInviteCommandModel(
|
||||
email: user.Email,
|
||||
assignedCollections: [],
|
||||
groups: [],
|
||||
@@ -334,7 +333,7 @@ public class InviteOrganizationUserCommandTests
|
||||
var request = new InviteOrganizationUsersRequest(
|
||||
invites:
|
||||
[
|
||||
new OrganizationUserInvite(
|
||||
new OrganizationUserInviteCommandModel(
|
||||
email: user.Email,
|
||||
assignedCollections: [],
|
||||
groups: [],
|
||||
@@ -411,7 +410,7 @@ public class InviteOrganizationUserCommandTests
|
||||
|
||||
var request = new InviteOrganizationUsersRequest(
|
||||
invites: [
|
||||
new OrganizationUserInvite(
|
||||
new OrganizationUserInviteCommandModel(
|
||||
email: user.Email,
|
||||
assignedCollections: [],
|
||||
groups: [],
|
||||
@@ -492,7 +491,7 @@ public class InviteOrganizationUserCommandTests
|
||||
|
||||
var request = new InviteOrganizationUsersRequest(
|
||||
invites: [
|
||||
new OrganizationUserInvite(
|
||||
new OrganizationUserInviteCommandModel(
|
||||
email: user.Email,
|
||||
assignedCollections: [],
|
||||
groups: [],
|
||||
@@ -566,7 +565,7 @@ public class InviteOrganizationUserCommandTests
|
||||
|
||||
var request = new InviteOrganizationUsersRequest(
|
||||
invites: [
|
||||
new OrganizationUserInvite(
|
||||
new OrganizationUserInviteCommandModel(
|
||||
email: user.Email,
|
||||
assignedCollections: [],
|
||||
groups: [],
|
||||
@@ -669,7 +668,7 @@ public class InviteOrganizationUserCommandTests
|
||||
|
||||
var request = new InviteOrganizationUsersRequest(
|
||||
invites: [
|
||||
new OrganizationUserInvite(
|
||||
new OrganizationUserInviteCommandModel(
|
||||
email: user.Email,
|
||||
assignedCollections: [],
|
||||
groups: [],
|
||||
@@ -768,7 +767,7 @@ public class InviteOrganizationUserCommandTests
|
||||
|
||||
var request = new InviteOrganizationUsersRequest(
|
||||
invites: [
|
||||
new OrganizationUserInvite(
|
||||
new OrganizationUserInviteCommandModel(
|
||||
email: user.Email,
|
||||
assignedCollections: [],
|
||||
groups: [],
|
||||
@@ -863,7 +862,7 @@ public class InviteOrganizationUserCommandTests
|
||||
var request = new InviteOrganizationUsersRequest(
|
||||
invites:
|
||||
[
|
||||
new OrganizationUserInvite(
|
||||
new OrganizationUserInviteCommandModel(
|
||||
email: user.Email,
|
||||
assignedCollections: [],
|
||||
groups: [],
|
||||
@@ -942,7 +941,7 @@ public class InviteOrganizationUserCommandTests
|
||||
var request = new InviteOrganizationUsersRequest(
|
||||
invites:
|
||||
[
|
||||
new OrganizationUserInvite(
|
||||
new OrganizationUserInviteCommandModel(
|
||||
email: user.Email,
|
||||
assignedCollections: [],
|
||||
groups: [],
|
||||
|
||||
@@ -14,7 +14,6 @@ using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using NSubstitute.ExceptionExtensions;
|
||||
using Xunit;
|
||||
using OrganizationUserInvite = Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models.OrganizationUserInvite;
|
||||
|
||||
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation;
|
||||
|
||||
@@ -36,13 +35,13 @@ public class InviteOrganizationUsersValidatorTests
|
||||
{
|
||||
Invites =
|
||||
[
|
||||
new OrganizationUserInvite(
|
||||
new OrganizationUserInviteCommandModel(
|
||||
email: "test@email.com",
|
||||
externalId: "test-external-id"),
|
||||
new OrganizationUserInvite(
|
||||
new OrganizationUserInviteCommandModel(
|
||||
email: "test2@email.com",
|
||||
externalId: "test-external-id2"),
|
||||
new OrganizationUserInvite(
|
||||
new OrganizationUserInviteCommandModel(
|
||||
email: "test3@email.com",
|
||||
externalId: "test-external-id3")
|
||||
],
|
||||
@@ -82,13 +81,13 @@ public class InviteOrganizationUsersValidatorTests
|
||||
{
|
||||
Invites =
|
||||
[
|
||||
new OrganizationUserInvite(
|
||||
new OrganizationUserInviteCommandModel(
|
||||
email: "test@email.com",
|
||||
externalId: "test-external-id"),
|
||||
new OrganizationUserInvite(
|
||||
new OrganizationUserInviteCommandModel(
|
||||
email: "test2@email.com",
|
||||
externalId: "test-external-id2"),
|
||||
new OrganizationUserInvite(
|
||||
new OrganizationUserInviteCommandModel(
|
||||
email: "test3@email.com",
|
||||
externalId: "test-external-id3")
|
||||
],
|
||||
@@ -126,13 +125,13 @@ public class InviteOrganizationUsersValidatorTests
|
||||
{
|
||||
Invites =
|
||||
[
|
||||
new OrganizationUserInvite(
|
||||
new OrganizationUserInviteCommandModel(
|
||||
email: "test@email.com",
|
||||
externalId: "test-external-id"),
|
||||
new OrganizationUserInvite(
|
||||
new OrganizationUserInviteCommandModel(
|
||||
email: "test2@email.com",
|
||||
externalId: "test-external-id2"),
|
||||
new OrganizationUserInvite(
|
||||
new OrganizationUserInviteCommandModel(
|
||||
email: "test3@email.com",
|
||||
externalId: "test-external-id3")
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user