1
0
mirror of https://github.com/bitwarden/server synced 2026-01-03 09:03:44 +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:
Brandon Treston
2025-07-22 17:30:25 -04:00
committed by GitHub
parent 6278fe7bc5
commit 947ae8db51
21 changed files with 1137 additions and 49 deletions

View File

@@ -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
);
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -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),

View File

@@ -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()

View File

@@ -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)

View File

@@ -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; }

View File

@@ -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,

View File

@@ -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()
};
}