1
0
mirror of https://github.com/bitwarden/server synced 2025-12-30 23:23:37 +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

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