1
0
mirror of https://github.com/bitwarden/server.git synced 2025-06-27 22:26:13 -05:00
This commit is contained in:
Brandon 2025-06-11 10:34:47 -04:00
parent 3a1ed0d81b
commit e309c3cc2f
No known key found for this signature in database
GPG Key ID: A0E0EF0B207BA40D
6 changed files with 55 additions and 35 deletions

View File

@ -20,20 +20,20 @@ public class OrganizationController : Controller
private readonly IOrganizationService _organizationService; private readonly IOrganizationService _organizationService;
private readonly ICurrentContext _currentContext; private readonly ICurrentContext _currentContext;
private readonly GlobalSettings _globalSettings; private readonly GlobalSettings _globalSettings;
private readonly IImportOrganizationUserCommand _importOrganizationUserCommand; private readonly IImportOrganizationUsersAndGroupsCommand _importOrganizationUsersAndGroupsCommand;
private readonly IFeatureService _featureService; private readonly IFeatureService _featureService;
public OrganizationController( public OrganizationController(
IOrganizationService organizationService, IOrganizationService organizationService,
ICurrentContext currentContext, ICurrentContext currentContext,
GlobalSettings globalSettings, GlobalSettings globalSettings,
IImportOrganizationUserCommand importOrganizationUserCommand, IImportOrganizationUsersAndGroupsCommand importOrganizationUsersAndGroupsCommand,
IFeatureService featureService) IFeatureService featureService)
{ {
_organizationService = organizationService; _organizationService = organizationService;
_currentContext = currentContext; _currentContext = currentContext;
_globalSettings = globalSettings; _globalSettings = globalSettings;
_importOrganizationUserCommand = importOrganizationUserCommand; _importOrganizationUsersAndGroupsCommand = importOrganizationUsersAndGroupsCommand;
_featureService = featureService; _featureService = featureService;
} }
@ -57,7 +57,7 @@ public class OrganizationController : Controller
if (_featureService.IsEnabled(FeatureFlagKeys.ScimInviteUserOptimization)) if (_featureService.IsEnabled(FeatureFlagKeys.ScimInviteUserOptimization))
{ {
await _importOrganizationUserCommand.ImportAsync( await _importOrganizationUsersAndGroupsCommand.ImportAsync(
_currentContext.OrganizationId.Value, _currentContext.OrganizationId.Value,
model.Groups.Select(g => g.ToImportedGroup(_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.ToImportedOrganizationUser()),

View File

@ -1,9 +1,11 @@
using Bit.Core.AdminConsole.Models.Business; #nullable enable
using Bit.Core.AdminConsole.Models.Business;
using Bit.Core.Models.Business; using Bit.Core.Models.Business;
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
public interface IImportOrganizationUserCommand public interface IImportOrganizationUsersAndGroupsCommand
{ {
Task ImportAsync(Guid organizationId, Task ImportAsync(Guid organizationId,
IEnumerable<ImportedGroup> groups, IEnumerable<ImportedGroup> groups,

View File

@ -1,4 +1,6 @@
using Bit.Core.AdminConsole.Entities; #nullable enable
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Models.Business; using Bit.Core.AdminConsole.Models.Business;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;
@ -16,11 +18,9 @@ using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
#nullable enable
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers; namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers;
public class ImportOrganizationUserCommand : IImportOrganizationUserCommand public class ImportOrganizationUsersAndGroupsCommand : IImportOrganizationUsersAndGroupsCommand
{ {
private readonly IOrganizationRepository _organizationRepository; private readonly IOrganizationRepository _organizationRepository;
private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IOrganizationUserRepository _organizationUserRepository;
@ -34,7 +34,7 @@ public class ImportOrganizationUserCommand : IImportOrganizationUserCommand
private readonly EventSystemUser _EventSystemUser = EventSystemUser.PublicApi; private readonly EventSystemUser _EventSystemUser = EventSystemUser.PublicApi;
public ImportOrganizationUserCommand(IOrganizationRepository organizationRepository, public ImportOrganizationUsersAndGroupsCommand(IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository, IOrganizationUserRepository organizationUserRepository,
IPaymentService paymentService, IPaymentService paymentService,
IGroupRepository groupRepository, IGroupRepository groupRepository,
@ -95,7 +95,7 @@ public class ImportOrganizationUserCommand : IImportOrganizationUserCommand
await OverwriteExisting(events, importUserData); await OverwriteExisting(events, importUserData);
} }
await Update(importedUsers, importUserData); await UpdateExistingUsers(importedUsers, importUserData);
await AddNewUsers(organization, importedUsers, importUserData); await AddNewUsers(organization, importedUsers, importUserData);
@ -144,7 +144,7 @@ public class ImportOrganizationUserCommand : IImportOrganizationUserCommand
/// </summary> /// </summary>
/// <param name="importedUsers">List of imported organization users.</param> /// <param name="importedUsers">List of imported organization users.</param>
/// <param name="importUserData">Data containing existing and imported users, along with mapping dictionaries.</param> /// <param name="importUserData">Data containing existing and imported users, along with mapping dictionaries.</param>
private async Task Update(IEnumerable<ImportedOrganizationUser> importedUsers, OrganizationUserImportData importUserData) private async Task UpdateExistingUsers(IEnumerable<ImportedOrganizationUser> importedUsers, OrganizationUserImportData importUserData)
{ {
if (!importedUsers.Any()) if (!importedUsers.Any())
{ {
@ -160,27 +160,24 @@ public class ImportOrganizationUserCommand : IImportOrganizationUserCommand
var importedUsersEmailsDict = importedUsers.ToDictionary(u => u.Email); var importedUsersEmailsDict = importedUsers.ToDictionary(u => u.Email);
// Determine which users to update. // Determine which users to update.
var userDetailsToUpdate = existingUsersEmailsDict.Keys.Intersect(importedUsersEmailsDict.Keys).ToList(); var userEmailsToUpdate = existingUsersEmailsDict.Keys.Intersect(importedUsersEmailsDict.Keys).ToList();
var userIdsToUpdate = importUserData.ExistingUsers var userIdsToUpdate = userEmailsToUpdate.Select(e => existingUsersEmailsDict[e].Id).ToList();
.Where(u => userDetailsToUpdate.Contains(u.Email))
.Select(u => u.Id)
.ToList();
var organizationUsers = (await _organizationUserRepository.GetManyAsync(userIdsToUpdate)).ToDictionary(u => u.Id); var organizationUsers = (await _organizationUserRepository.GetManyAsync(userIdsToUpdate)).ToDictionary(u => u.Id);
foreach (var userEmail in userDetailsToUpdate) foreach (var userEmail in userEmailsToUpdate)
{ {
// verify userEmail has an associated OrganizationUser // verify userEmail has an associated OrganizationUser
existingUsersEmailsDict.TryGetValue(userEmail, out var existingUser); existingUsersEmailsDict.TryGetValue(userEmail, out var existingUser);
organizationUsers.TryGetValue(existingUser!.Id, out var organizationUser); organizationUsers.TryGetValue(existingUser!.Id, out var organizationUser);
importedUsersEmailsDict.TryGetValue(userEmail, out var user); importedUsersEmailsDict.TryGetValue(userEmail, out var importedUser);
if (organizationUser is null || user is null) if (organizationUser is null || importedUser is null)
{ {
continue; continue;
} }
organizationUser.ExternalId = user.ExternalId; organizationUser.ExternalId = importedUser.ExternalId;
updateUsers.Add(organizationUser); updateUsers.Add(organizationUser);
importUserData.ExistingExternalUsersIdDict.Add(organizationUser.ExternalId, organizationUser.Id); importUserData.ExistingExternalUsersIdDict.Add(organizationUser.ExternalId, organizationUser.Id);
} }
@ -302,11 +299,8 @@ public class ImportOrganizationUserCommand : IImportOrganizationUserCommand
/// <param name="organization">The organization into which groups are being imported.</param> /// <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="importedGroups">A collection of groups to be imported.</param>
/// <param name="importUserData">Data containing information about existing and imported users.</param> /// <param name="importUserData">Data containing information about existing and imported users.</param>
private async Task ImportGroups(Organization organization, private async Task ImportGroups(Organization organization, IEnumerable<ImportedGroup> importedGroups, OrganizationUserImportData importUserData)
IEnumerable<ImportedGroup> importedGroups,
OrganizationUserImportData importUserData)
{ {
if (!importedGroups.Any()) if (!importedGroups.Any())
{ {
return; return;
@ -314,7 +308,7 @@ public class ImportOrganizationUserCommand : IImportOrganizationUserCommand
if (!organization.UseGroups) if (!organization.UseGroups)
{ {
throw new BadRequestException("Organization cannot use importedGroups."); throw new BadRequestException("Organization cannot use groups.");
} }
var existingGroups = await _groupRepository.GetManyByOrganizationIdAsync(organization.Id); var existingGroups = await _groupRepository.GetManyByOrganizationIdAsync(organization.Id);
@ -332,9 +326,11 @@ public class ImportOrganizationUserCommand : IImportOrganizationUserCommand
/// <param name="importUserData">Data containing information about existing and imported users.</param> /// <param name="importUserData">Data containing information about existing and imported users.</param>
private async Task SaveNewGroups(OrganizationGroupImportData importGroupData, OrganizationUserImportData importUserData) private async Task SaveNewGroups(OrganizationGroupImportData importGroupData, OrganizationUserImportData importUserData)
{ {
var existingExternalGroupsDict = importGroupData.ExistingExternalGroups.ToDictionary(g => g.ExternalId!);
var newGroups = importGroupData.Groups var newGroups = importGroupData.Groups
.Where(g => !importGroupData.ExistingExternalGroups.ToDictionary(g => g.ExternalId!).ContainsKey(g.Group.ExternalId!)) .Where(g => !existingExternalGroupsDict.ContainsKey(g.Group.ExternalId!))
.Select(g => g.Group).ToList()!; .Select(g => g.Group)
.ToList()!;
var savedGroups = new List<Group>(); var savedGroups = new List<Group>();
foreach (var group in newGroups) foreach (var group in newGroups)

View File

@ -1,19 +1,40 @@
using Bit.Core.AdminConsole.Entities; #nullable enable
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Models.Business; using Bit.Core.AdminConsole.Models.Business;
namespace Bit.Core.Models.Data.Organizations; 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 public class OrganizationGroupImportData
{ {
/// <summary>
/// The collection of groups that are being imported.
/// </summary>
public readonly IEnumerable<ImportedGroup> Groups; public readonly IEnumerable<ImportedGroup> Groups;
/// <summary>
/// Collection of groups that already exist in the organization.
/// </summary>
public readonly ICollection<Group> ExistingGroups; public readonly ICollection<Group> ExistingGroups;
/// <summary>
/// Existing groups with ExternalId set.
/// </summary>
public readonly IEnumerable<Group> ExistingExternalGroups; public readonly IEnumerable<Group> ExistingExternalGroups;
/// <summary>
/// Mapping of imported groups keyed by their ExternalId.
/// </summary>
public readonly IDictionary<string, ImportedGroup> GroupsDict; public readonly IDictionary<string, ImportedGroup> GroupsDict;
public OrganizationGroupImportData(IEnumerable<ImportedGroup> groups, ICollection<Group> existingGroups) public OrganizationGroupImportData(IEnumerable<ImportedGroup> groups, ICollection<Group> existingGroups)
{ {
Groups = groups; Groups = groups;
GroupsDict = groups.ToDictionary(g => g.Group.ExternalId); GroupsDict = groups.ToDictionary(g => g.Group.ExternalId!);
ExistingGroups = existingGroups; ExistingGroups = existingGroups;
ExistingExternalGroups = existingGroups.Where(u => !string.IsNullOrWhiteSpace(u.ExternalId)).ToList(); ExistingExternalGroups = existingGroups.Where(u => !string.IsNullOrWhiteSpace(u.ExternalId)).ToList();
} }

View File

@ -1,4 +1,6 @@
using Bit.Core.Models.Business; #nullable enable
using Bit.Core.Models.Business;
namespace Bit.Core.Models.Data.Organizations.OrganizationUsers; namespace Bit.Core.Models.Data.Organizations.OrganizationUsers;
public class OrganizationUserImportData public class OrganizationUserImportData
@ -8,11 +10,11 @@ public class OrganizationUserImportData
/// </summary> /// </summary>
public readonly HashSet<string> ImportedExternalIds; public readonly HashSet<string> ImportedExternalIds;
/// <summary> /// <summary>
/// Exising organization users details /// All existing OrganizationUsers for the organization
/// </summary> /// </summary>
public readonly ICollection<OrganizationUserUserDetails> ExistingUsers; public readonly ICollection<OrganizationUserUserDetails> ExistingUsers;
/// <summary> /// <summary>
/// List of ExternalIds belonging to existing organization Users /// Existing OrganizationUsers with ExternalIds set.
/// </summary> /// </summary>
public readonly IEnumerable<OrganizationUserUserDetails> ExistingExternalUsers; public readonly IEnumerable<OrganizationUserUserDetails> ExistingExternalUsers;
/// <summary> /// <summary>

View File

@ -1,5 +1,4 @@
using Bit.Core.AdminConsole.Models.Business; using Bit.Core.AdminConsole.Models.Business;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
using Bit.Core.AdminConsole.Utilities.Commands; using Bit.Core.AdminConsole.Utilities.Commands;