1
0
mirror of https://github.com/bitwarden/server.git synced 2025-05-10 06:02:24 -05:00

refactor groups logic

This commit is contained in:
Brandon 2025-05-09 14:19:18 -04:00
parent f76d6fde62
commit 15cff6c507
No known key found for this signature in database
GPG Key ID: A0E0EF0B207BA40D
5 changed files with 78 additions and 55 deletions

View File

@ -0,0 +1,12 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Models.Business;
namespace Bit.Core.Models.Data.Organizations;
public class OrganizationGroupImportData
{
public IEnumerable<ImportedGroup> Groups { get; set; }
public ICollection<Group> ExistingGroups { get; set; }
public IEnumerable<Group> ExistingExternalGroups { get; set; }
public IDictionary<string, ImportedGroup> GroupsDict { get; set; }
}

View File

@ -10,6 +10,7 @@ using Bit.Core.Enums;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Models.Business; using Bit.Core.Models.Business;
using Bit.Core.Models.Commands; using Bit.Core.Models.Commands;
using Bit.Core.Models.Data.Organizations;
using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.Repositories; using Bit.Core.Repositories;
@ -60,8 +61,7 @@ public class ImportOrganizationUserCommand : IImportOrganizationUserCommand
IEnumerable<ImportedOrganizationUser> newUsers, IEnumerable<ImportedOrganizationUser> newUsers,
IEnumerable<string> removeUserExternalIds, IEnumerable<string> removeUserExternalIds,
bool overwriteExisting, bool overwriteExisting,
EventSystemUser eventSystemUser EventSystemUser eventSystemUser)
)
{ {
var organization = await GetOrgById(organizationId); var organization = await GetOrgById(organizationId);
if (organization == null) if (organization == null)
@ -76,27 +76,27 @@ public class ImportOrganizationUserCommand : IImportOrganizationUserCommand
var existingUsers = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(organizationId); var existingUsers = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(organizationId);
var importData = new OrganizationUserImportData var importUserData = new OrganizationUserImportData
{ {
NewUsersSet = new HashSet<string>(newUsers?.Select(u => u.ExternalId) ?? new List<string>()), NewUsersSet = new HashSet<string>(newUsers?.Select(u => u.ExternalId) ?? new List<string>()),
ExistingUsers = existingUsers, ExistingUsers = existingUsers,
ExistingExternalUsers = GetExistingExternalUsers(existingUsers), ExistingExternalUsers = GetExistingExternalUsers(existingUsers),
ExistingExternalUsersIdDict = GetExistingExternalUsersIdDict(existingUsers) ExistingExternalUsersIdDict = GetExistingExternalUsers(existingUsers).ToDictionary(u => u.ExternalId, u => u.Id)
}; };
var events = new List<(OrganizationUserUserDetails ou, EventType e, DateTime? d)>(); var events = new List<(OrganizationUserUserDetails ou, EventType e, DateTime? d)>();
// Users // Users
await RemoveExistingExternalUsers(removeUserExternalIds, events, importData); await RemoveExistingExternalUsers(removeUserExternalIds, events, importUserData);
if (overwriteExisting) if (overwriteExisting)
{ {
await OverwriteExisting(events, importData); await OverwriteExisting(events, importUserData);
} }
await RemoveExistingUsers(existingUsers, newUsers, organization, importData); await RemoveExistingUsers(existingUsers, newUsers, organization, importUserData);
await AddNewUsers(organization, newUsers, eventSystemUser, importData); await AddNewUsers(organization, newUsers, eventSystemUser, importUserData);
// Groups // Groups
await ImportGroups(organization, groups, eventSystemUser, importData); await ImportGroups(organization, groups, eventSystemUser, importUserData);
await _eventService.LogOrganizationUserEventsAsync(events.Select(e => (e.ou, e.e, eventSystemUser, e.d))); await _eventService.LogOrganizationUserEventsAsync(events.Select(e => (e.ou, e.e, eventSystemUser, e.d)));
await _referenceEventService.RaiseEventAsync( await _referenceEventService.RaiseEventAsync(
@ -116,20 +116,18 @@ public class ImportOrganizationUserCommand : IImportOrganizationUserCommand
await _groupRepository.UpdateUsersAsync(group.Id, users); await _groupRepository.UpdateUsersAsync(group.Id, users);
} }
private async Task RemoveExistingExternalUsers( private async Task RemoveExistingExternalUsers(IEnumerable<string> removeUserExternalIds,
IEnumerable<string> removeUserExternalIds,
List<(OrganizationUserUserDetails ou, EventType e, DateTime? d)> events, List<(OrganizationUserUserDetails ou, EventType e, DateTime? d)> events,
OrganizationUserImportData importData OrganizationUserImportData importUserData)
)
{ {
if (!removeUserExternalIds.Any()) if (!removeUserExternalIds.Any())
{ {
return; return;
} }
var existingUsersDict = importData.ExistingExternalUsers.ToDictionary(u => u.ExternalId); var existingUsersDict = importUserData.ExistingExternalUsers.ToDictionary(u => u.ExternalId);
var removeUsersSet = new HashSet<string>(removeUserExternalIds) var removeUsersSet = new HashSet<string>(removeUserExternalIds)
.Except(importData.NewUsersSet) .Except(importUserData.NewUsersSet)
.Where(u => existingUsersDict.ContainsKey(u) && existingUsersDict[u].Type != OrganizationUserType.Owner) .Where(u => existingUsersDict.ContainsKey(u) && existingUsersDict[u].Type != OrganizationUserType.Owner)
.Select(u => existingUsersDict[u]); .Select(u => existingUsersDict[u]);
@ -142,12 +140,10 @@ public class ImportOrganizationUserCommand : IImportOrganizationUserCommand
); );
} }
private async Task RemoveExistingUsers( private async Task RemoveExistingUsers(IEnumerable<OrganizationUserUserDetails> existingUsers,
IEnumerable<OrganizationUserUserDetails> existingUsers,
IEnumerable<ImportedOrganizationUser> newUsers, IEnumerable<ImportedOrganizationUser> newUsers,
Organization organization, Organization organization,
OrganizationUserImportData importData OrganizationUserImportData importUserData)
)
{ {
if (!newUsers.Any()) if (!newUsers.Any())
{ {
@ -169,7 +165,7 @@ public class ImportOrganizationUserCommand : IImportOrganizationUserCommand
{ {
orgUser.ExternalId = newUsersEmailsDict[user].ExternalId; orgUser.ExternalId = newUsersEmailsDict[user].ExternalId;
usersToUpsert.Add(orgUser); usersToUpsert.Add(orgUser);
importData.ExistingExternalUsersIdDict.Add(orgUser.ExternalId, orgUser.Id); importUserData.ExistingExternalUsersIdDict.Add(orgUser.ExternalId, orgUser.Id);
} }
} }
await _organizationUserRepository.UpsertManyAsync(usersToUpsert); await _organizationUserRepository.UpsertManyAsync(usersToUpsert);
@ -178,11 +174,11 @@ public class ImportOrganizationUserCommand : IImportOrganizationUserCommand
private async Task AddNewUsers(Organization organization, private async Task AddNewUsers(Organization organization,
IEnumerable<ImportedOrganizationUser> newUsers, IEnumerable<ImportedOrganizationUser> newUsers,
EventSystemUser eventSystemUser, EventSystemUser eventSystemUser,
OrganizationUserImportData importData) OrganizationUserImportData importUserData)
{ {
var existingUsersSet = new HashSet<string>(importData.ExistingExternalUsersIdDict.Keys); var existingUsersSet = new HashSet<string>(importUserData.ExistingExternalUsersIdDict.Keys);
var usersToAdd = importData.NewUsersSet.Except(existingUsersSet).ToList(); var usersToAdd = importUserData.NewUsersSet.Except(existingUsersSet).ToList();
var seatsAvailable = int.MaxValue; var seatsAvailable = int.MaxValue;
var enoughSeatsAvailable = true; var enoughSeatsAvailable = true;
@ -219,11 +215,10 @@ public class ImportOrganizationUserCommand : IImportOrganizationUserCommand
switch (commandResult) switch (commandResult)
{ {
case Success<InviteOrganizationUsersResponse> success: case Success<InviteOrganizationUsersResponse> result:
var result = success.Value; foreach (var u in result.Value.InvitedUsers)
foreach (var u in result.InvitedUsers)
{ {
importData.ExistingExternalUsersIdDict.Add(u.ExternalId, u.Id); importUserData.ExistingExternalUsersIdDict.Add(u.ExternalId, u.Id);
} }
break; break;
case Failure<InviteOrganizationUsersResponse> failure: case Failure<InviteOrganizationUsersResponse> failure:
@ -244,14 +239,13 @@ public class ImportOrganizationUserCommand : IImportOrganizationUserCommand
private async Task OverwriteExisting( private async Task OverwriteExisting(
List<(OrganizationUserUserDetails ou, EventType e, DateTime? d)> events, List<(OrganizationUserUserDetails ou, EventType e, DateTime? d)> events,
OrganizationUserImportData importData OrganizationUserImportData importUserData)
)
{ {
// Remove existing external users that are not in new user set // Remove existing external users that are not in new user set
var usersToDelete = importData.ExistingExternalUsers.Where(u => var usersToDelete = importUserData.ExistingExternalUsers.Where(u =>
u.Type != OrganizationUserType.Owner && u.Type != OrganizationUserType.Owner &&
!importData.NewUsersSet.Contains(u.ExternalId) && !importUserData.NewUsersSet.Contains(u.ExternalId) &&
importData.ExistingExternalUsersIdDict.ContainsKey(u.ExternalId)); importUserData.ExistingExternalUsersIdDict.ContainsKey(u.ExternalId));
await _organizationUserRepository.DeleteManyAsync(usersToDelete.Select(u => u.Id)); await _organizationUserRepository.DeleteManyAsync(usersToDelete.Select(u => u.Id));
events.AddRange(usersToDelete.Select(u => ( events.AddRange(usersToDelete.Select(u => (
u, u,
@ -261,15 +255,14 @@ public class ImportOrganizationUserCommand : IImportOrganizationUserCommand
); );
foreach (var deletedUser in usersToDelete) foreach (var deletedUser in usersToDelete)
{ {
importData.ExistingExternalUsersIdDict.Remove(deletedUser.ExternalId); importUserData.ExistingExternalUsersIdDict.Remove(deletedUser.ExternalId);
} }
} }
private async Task ImportGroups(Organization organization, private async Task ImportGroups(Organization organization,
IEnumerable<ImportedGroup> groups, IEnumerable<ImportedGroup> groups,
EventSystemUser eventSystemUser, EventSystemUser eventSystemUser,
OrganizationUserImportData importData OrganizationUserImportData importUserData)
)
{ {
if (!groups.Any()) if (!groups.Any())
@ -282,14 +275,25 @@ public class ImportOrganizationUserCommand : IImportOrganizationUserCommand
throw new BadRequestException("Organization cannot use groups."); throw new BadRequestException("Organization cannot use groups.");
} }
var groupsDict = groups.ToDictionary(g => g.Group.ExternalId);
var existingGroups = await _groupRepository.GetManyByOrganizationIdAsync(organization.Id); var existingGroups = await _groupRepository.GetManyByOrganizationIdAsync(organization.Id);
var existingExternalGroups = existingGroups var importGroupData = new OrganizationGroupImportData
.Where(u => !string.IsNullOrWhiteSpace(u.ExternalId)).ToList(); {
var existingExternalGroupsDict = existingExternalGroups.ToDictionary(g => g.ExternalId); Groups = groups,
GroupsDict = groups.ToDictionary(g => g.Group.ExternalId),
ExistingGroups = existingGroups,
ExistingExternalGroups = GetExistingExternalGroups(existingGroups)
};
var newGroups = groups await SaveNewGroups(importGroupData, importUserData, eventSystemUser);
.Where(g => !existingExternalGroupsDict.ContainsKey(g.Group.ExternalId)) await UpdateExistingGroups(importGroupData, importUserData, organization, eventSystemUser);
}
private async Task SaveNewGroups(OrganizationGroupImportData importGroupData,
OrganizationUserImportData importUserData,
EventSystemUser eventSystemUser)
{
var newGroups = importGroupData.Groups
.Where(g => !importGroupData.ExistingExternalGroups.ToDictionary(g => g.ExternalId).ContainsKey(g.Group.ExternalId))
.Select(g => g.Group).ToList(); .Select(g => g.Group).ToList();
var savedGroups = new List<Group>(); var savedGroups = new List<Group>();
@ -298,15 +302,21 @@ public class ImportOrganizationUserCommand : IImportOrganizationUserCommand
group.CreationDate = group.RevisionDate = DateTime.UtcNow; group.CreationDate = group.RevisionDate = DateTime.UtcNow;
savedGroups.Add(await _groupRepository.CreateAsync(group)); savedGroups.Add(await _groupRepository.CreateAsync(group));
await UpdateUsersAsync(group, groupsDict[group.ExternalId].ExternalUserIds, await UpdateUsersAsync(group, importGroupData.GroupsDict[group.ExternalId].ExternalUserIds,
importData.ExistingExternalUsersIdDict); importUserData.ExistingExternalUsersIdDict);
} }
await _eventService.LogGroupEventsAsync( await _eventService.LogGroupEventsAsync(
savedGroups.Select(g => (g, EventType.Group_Created, (EventSystemUser?)eventSystemUser, (DateTime?)DateTime.UtcNow))); savedGroups.Select(g => (g, EventType.Group_Created, (EventSystemUser?)eventSystemUser, (DateTime?)DateTime.UtcNow)));
}
var updateGroups = existingExternalGroups private async Task UpdateExistingGroups(OrganizationGroupImportData importGroupData,
.Where(g => groupsDict.ContainsKey(g.ExternalId)) OrganizationUserImportData importUserData,
Organization organization,
EventSystemUser eventSystemUser)
{
var updateGroups = importGroupData.ExistingExternalGroups
.Where(g => importGroupData.GroupsDict.ContainsKey(g.ExternalId))
.ToList(); .ToList();
if (updateGroups.Any()) if (updateGroups.Any())
@ -318,7 +328,7 @@ public class ImportOrganizationUserCommand : IImportOrganizationUserCommand
foreach (var group in updateGroups) foreach (var group in updateGroups)
{ {
var updatedGroup = groupsDict[group.ExternalId].Group; var updatedGroup = importGroupData.GroupsDict[group.ExternalId].Group;
if (group.Name != updatedGroup.Name) if (group.Name != updatedGroup.Name)
{ {
group.RevisionDate = DateTime.UtcNow; group.RevisionDate = DateTime.UtcNow;
@ -327,8 +337,8 @@ public class ImportOrganizationUserCommand : IImportOrganizationUserCommand
await _groupRepository.ReplaceAsync(group); await _groupRepository.ReplaceAsync(group);
} }
await UpdateUsersAsync(group, groupsDict[group.ExternalId].ExternalUserIds, await UpdateUsersAsync(group, importGroupData.GroupsDict[group.ExternalId].ExternalUserIds,
importData.ExistingExternalUsersIdDict, importUserData.ExistingExternalUsersIdDict,
existingGroupUsers.ContainsKey(group.Id) ? existingGroupUsers[group.Id] : null); existingGroupUsers.ContainsKey(group.Id) ? existingGroupUsers[group.Id] : null);
} }
@ -336,6 +346,7 @@ public class ImportOrganizationUserCommand : IImportOrganizationUserCommand
await _eventService.LogGroupEventsAsync( await _eventService.LogGroupEventsAsync(
updateGroups.Select(g => (g, EventType.Group_Updated, (EventSystemUser?)eventSystemUser, (DateTime?)DateTime.UtcNow))); updateGroups.Select(g => (g, EventType.Group_Updated, (EventSystemUser?)eventSystemUser, (DateTime?)DateTime.UtcNow)));
} }
} }
private IEnumerable<OrganizationUserUserDetails> GetExistingExternalUsers(ICollection<OrganizationUserUserDetails> existingUsers) private IEnumerable<OrganizationUserUserDetails> GetExistingExternalUsers(ICollection<OrganizationUserUserDetails> existingUsers)
@ -345,11 +356,11 @@ public class ImportOrganizationUserCommand : IImportOrganizationUserCommand
.ToList(); .ToList();
} }
private Dictionary<string, Guid> GetExistingExternalUsersIdDict(ICollection<OrganizationUserUserDetails> existingUsers) private IEnumerable<Group> GetExistingExternalGroups(ICollection<Group> existingGroups)
{ {
return existingUsers return existingGroups
.Where(u => !string.IsNullOrWhiteSpace(u.ExternalId)) .Where(u => !string.IsNullOrWhiteSpace(u.ExternalId))
.ToDictionary(u => u.ExternalId, u => u.Id); .ToList();
} }
private async Task<Organization> GetOrgById(Guid id) private async Task<Organization> GetOrgById(Guid id)

View File

@ -20,7 +20,7 @@ public interface IInviteOrganizationUsersCommand
/// <returns>Response from InviteScimOrganiation<see cref="ScimInviteOrganizationUsersResponse"/></returns> /// <returns>Response from InviteScimOrganiation<see cref="ScimInviteOrganizationUsersResponse"/></returns>
Task<CommandResult<ScimInviteOrganizationUsersResponse>> InviteScimOrganizationUserAsync(InviteOrganizationUsersRequest request); Task<CommandResult<ScimInviteOrganizationUsersResponse>> InviteScimOrganizationUserAsync(InviteOrganizationUsersRequest request);
/// <summary> /// <summary>
/// Sends invitations to add imported organization users via directory connector or public API. /// 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. /// 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. /// Success will be the successful return object.
/// </summary> /// </summary>

View File

@ -16,7 +16,7 @@ public class InviteOrganizationUsersRequestTests
public void Constructor_WhenPassedInvalidEmail_ThrowsException(string email, OrganizationUserType type, Permissions permissions, string externalId) public void Constructor_WhenPassedInvalidEmail_ThrowsException(string email, OrganizationUserType type, Permissions permissions, string externalId)
{ {
var exception = Assert.Throws<BadRequestException>(() => var exception = Assert.Throws<BadRequestException>(() =>
new OrganizationUserInvite(email, [], [], type, permissions, externalId, false)); new OrganizationUserInviteCommandModel(email, [], [], type, permissions, externalId, false));
Assert.Contains(InvalidEmailErrorMessage, exception.Message); Assert.Contains(InvalidEmailErrorMessage, exception.Message);
} }
@ -33,7 +33,7 @@ public class InviteOrganizationUsersRequestTests
}; };
var exception = Assert.Throws<BadRequestException>(() => var exception = Assert.Throws<BadRequestException>(() =>
new OrganizationUserInvite( new OrganizationUserInviteCommandModel(
email: validEmail, email: validEmail,
assignedCollections: [invalidCollectionConfiguration], assignedCollections: [invalidCollectionConfiguration],
groups: [], groups: [],
@ -51,7 +51,7 @@ public class InviteOrganizationUsersRequestTests
const string validEmail = "test@email.com"; const string validEmail = "test@email.com";
var validCollectionConfiguration = new CollectionAccessSelection { Id = Guid.NewGuid(), Manage = true }; var validCollectionConfiguration = new CollectionAccessSelection { Id = Guid.NewGuid(), Manage = true };
var invite = new OrganizationUserInvite( var invite = new OrganizationUserInviteCommandModel(
email: validEmail, email: validEmail,
assignedCollections: [validCollectionConfiguration], assignedCollections: [validCollectionConfiguration],
groups: [], groups: [],