mirror of
https://github.com/bitwarden/server
synced 2025-12-15 07:43:54 +00:00
[PM-12492] Create ResendOrganizationInviteCommand (#6182)
* Add IResendOrganizationInviteCommand and ResendOrganizationInviteCommand implementation * Add unit tests for ResendOrganizationInviteCommand to validate invite resend functionality * Refactor Organizations, OrganizationUsers, and Members controllers to use IResendInviteCommand for invite resending functionality * Fix Organizations, OrganizationUsers, and Members controllers to replace IResendInviteCommand with IResendOrganizationInviteCommand * Remove ResendInviteAsync method from IOrganizationService and its implementation in OrganizationService to streamline invite management functionality. * Add IResendOrganizationInviteCommand registration in OrganizationServiceCollectionExtensions
This commit is contained in:
@@ -9,6 +9,7 @@ using Bit.Admin.Utilities;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;
|
||||
using Bit.Core.AdminConsole.Providers.Interfaces;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing.Enums;
|
||||
@@ -32,7 +33,6 @@ namespace Bit.Admin.AdminConsole.Controllers;
|
||||
[Authorize]
|
||||
public class OrganizationsController : Controller
|
||||
{
|
||||
private readonly IOrganizationService _organizationService;
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
private readonly IOrganizationConnectionRepository _organizationConnectionRepository;
|
||||
@@ -55,9 +55,9 @@ public class OrganizationsController : Controller
|
||||
private readonly IProviderBillingService _providerBillingService;
|
||||
private readonly IOrganizationInitiateDeleteCommand _organizationInitiateDeleteCommand;
|
||||
private readonly IPricingClient _pricingClient;
|
||||
private readonly IResendOrganizationInviteCommand _resendOrganizationInviteCommand;
|
||||
|
||||
public OrganizationsController(
|
||||
IOrganizationService organizationService,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IOrganizationConnectionRepository organizationConnectionRepository,
|
||||
@@ -79,9 +79,9 @@ public class OrganizationsController : Controller
|
||||
IRemoveOrganizationFromProviderCommand removeOrganizationFromProviderCommand,
|
||||
IProviderBillingService providerBillingService,
|
||||
IOrganizationInitiateDeleteCommand organizationInitiateDeleteCommand,
|
||||
IPricingClient pricingClient)
|
||||
IPricingClient pricingClient,
|
||||
IResendOrganizationInviteCommand resendOrganizationInviteCommand)
|
||||
{
|
||||
_organizationService = organizationService;
|
||||
_organizationRepository = organizationRepository;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
_organizationConnectionRepository = organizationConnectionRepository;
|
||||
@@ -104,6 +104,7 @@ public class OrganizationsController : Controller
|
||||
_providerBillingService = providerBillingService;
|
||||
_organizationInitiateDeleteCommand = organizationInitiateDeleteCommand;
|
||||
_pricingClient = pricingClient;
|
||||
_resendOrganizationInviteCommand = resendOrganizationInviteCommand;
|
||||
}
|
||||
|
||||
[RequirePermission(Permission.Org_List_View)]
|
||||
@@ -395,7 +396,7 @@ public class OrganizationsController : Controller
|
||||
var organizationUsers = await _organizationUserRepository.GetManyByOrganizationAsync(id, OrganizationUserType.Owner);
|
||||
foreach (var organizationUser in organizationUsers)
|
||||
{
|
||||
await _organizationService.ResendInviteAsync(id, null, organizationUser.Id, true);
|
||||
await _resendOrganizationInviteCommand.ResendInviteAsync(id, null, organizationUser.Id, true);
|
||||
}
|
||||
|
||||
return Json(null);
|
||||
|
||||
@@ -12,6 +12,7 @@ using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RestoreUser.v1;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
@@ -62,6 +63,7 @@ public class OrganizationUsersController : Controller
|
||||
private readonly IPolicyRequirementQuery _policyRequirementQuery;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly IPricingClient _pricingClient;
|
||||
private readonly IResendOrganizationInviteCommand _resendOrganizationInviteCommand;
|
||||
private readonly IConfirmOrganizationUserCommand _confirmOrganizationUserCommand;
|
||||
private readonly IRestoreOrganizationUserCommand _restoreOrganizationUserCommand;
|
||||
private readonly IInitPendingOrganizationCommand _initPendingOrganizationCommand;
|
||||
@@ -92,7 +94,8 @@ public class OrganizationUsersController : Controller
|
||||
IConfirmOrganizationUserCommand confirmOrganizationUserCommand,
|
||||
IRestoreOrganizationUserCommand restoreOrganizationUserCommand,
|
||||
IInitPendingOrganizationCommand initPendingOrganizationCommand,
|
||||
IRevokeOrganizationUserCommand revokeOrganizationUserCommand)
|
||||
IRevokeOrganizationUserCommand revokeOrganizationUserCommand,
|
||||
IResendOrganizationInviteCommand resendOrganizationInviteCommand)
|
||||
{
|
||||
_organizationRepository = organizationRepository;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
@@ -116,6 +119,7 @@ public class OrganizationUsersController : Controller
|
||||
_policyRequirementQuery = policyRequirementQuery;
|
||||
_featureService = featureService;
|
||||
_pricingClient = pricingClient;
|
||||
_resendOrganizationInviteCommand = resendOrganizationInviteCommand;
|
||||
_confirmOrganizationUserCommand = confirmOrganizationUserCommand;
|
||||
_restoreOrganizationUserCommand = restoreOrganizationUserCommand;
|
||||
_initPendingOrganizationCommand = initPendingOrganizationCommand;
|
||||
@@ -266,7 +270,7 @@ public class OrganizationUsersController : Controller
|
||||
public async Task Reinvite(Guid orgId, Guid id)
|
||||
{
|
||||
var userId = _userService.GetProperUserId(User);
|
||||
await _organizationService.ResendInviteAsync(orgId, userId.Value, id);
|
||||
await _resendOrganizationInviteCommand.ResendInviteAsync(orgId, userId.Value, id);
|
||||
}
|
||||
|
||||
[HttpPost("{organizationUserId}/accept-init")]
|
||||
|
||||
@@ -6,6 +6,7 @@ using Bit.Api.AdminConsole.Public.Models.Request;
|
||||
using Bit.Api.AdminConsole.Public.Models.Response;
|
||||
using Bit.Api.Models.Public.Response;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||
using Bit.Core.Context;
|
||||
@@ -32,6 +33,7 @@ public class MembersController : Controller
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
||||
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
|
||||
private readonly IResendOrganizationInviteCommand _resendOrganizationInviteCommand;
|
||||
|
||||
public MembersController(
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
@@ -45,7 +47,8 @@ public class MembersController : Controller
|
||||
IPaymentService paymentService,
|
||||
IOrganizationRepository organizationRepository,
|
||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
||||
IRemoveOrganizationUserCommand removeOrganizationUserCommand)
|
||||
IRemoveOrganizationUserCommand removeOrganizationUserCommand,
|
||||
IResendOrganizationInviteCommand resendOrganizationInviteCommand)
|
||||
{
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
_groupRepository = groupRepository;
|
||||
@@ -59,6 +62,7 @@ public class MembersController : Controller
|
||||
_organizationRepository = organizationRepository;
|
||||
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
||||
_removeOrganizationUserCommand = removeOrganizationUserCommand;
|
||||
_resendOrganizationInviteCommand = resendOrganizationInviteCommand;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -260,7 +264,7 @@ public class MembersController : Controller
|
||||
{
|
||||
return new NotFoundResult();
|
||||
}
|
||||
await _organizationService.ResendInviteAsync(_currentContext.OrganizationId.Value, null, id);
|
||||
await _resendOrganizationInviteCommand.ResendInviteAsync(_currentContext.OrganizationId.Value, null, id);
|
||||
return new OkResult();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;
|
||||
|
||||
public interface IResendOrganizationInviteCommand
|
||||
{
|
||||
/// <summary>
|
||||
/// Resend an invite to an organization user.
|
||||
/// </summary>
|
||||
/// <param name="organizationId">The ID of the organization.</param>
|
||||
/// <param name="invitingUserId">The ID of the user who is inviting the organization user.</param>
|
||||
/// <param name="organizationUserId">The ID of the organization user to resend the invite to.</param>
|
||||
/// <param name="initOrganization">Whether to initialize the organization.
|
||||
/// This is should only be true when inviting the owner of a new organization.</param>
|
||||
Task ResendInviteAsync(Guid organizationId, Guid? invitingUserId, Guid organizationUserId, bool initOrganization = false);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
|
||||
using Bit.Core.AdminConsole.Utilities.DebuggingInstruments;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;
|
||||
|
||||
public class ResendOrganizationInviteCommand : IResendOrganizationInviteCommand
|
||||
{
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly ISendOrganizationInvitesCommand _sendOrganizationInvitesCommand;
|
||||
private readonly ILogger<ResendOrganizationInviteCommand> _logger;
|
||||
|
||||
public ResendOrganizationInviteCommand(
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IOrganizationRepository organizationRepository,
|
||||
ISendOrganizationInvitesCommand sendOrganizationInvitesCommand,
|
||||
ILogger<ResendOrganizationInviteCommand> logger)
|
||||
{
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
_organizationRepository = organizationRepository;
|
||||
_sendOrganizationInvitesCommand = sendOrganizationInvitesCommand;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task ResendInviteAsync(Guid organizationId, Guid? invitingUserId, Guid organizationUserId,
|
||||
bool initOrganization = false)
|
||||
{
|
||||
var organizationUser = await _organizationUserRepository.GetByIdAsync(organizationUserId);
|
||||
if (organizationUser == null || organizationUser.OrganizationId != organizationId ||
|
||||
organizationUser.Status != OrganizationUserStatusType.Invited)
|
||||
{
|
||||
throw new BadRequestException("User invalid.");
|
||||
}
|
||||
|
||||
_logger.LogUserInviteStateDiagnostics(organizationUser);
|
||||
|
||||
var organization = await _organizationRepository.GetByIdAsync(organizationUser.OrganizationId);
|
||||
if (organization == null)
|
||||
{
|
||||
throw new BadRequestException("Organization invalid.");
|
||||
}
|
||||
await SendInviteAsync(organizationUser, organization, initOrganization);
|
||||
}
|
||||
|
||||
private async Task SendInviteAsync(OrganizationUser organizationUser, Organization organization, bool initOrganization) =>
|
||||
await _sendOrganizationInvitesCommand.SendInvitesAsync(new SendInvitesRequest(
|
||||
users: [organizationUser],
|
||||
organization: organization,
|
||||
initOrganization: initOrganization));
|
||||
}
|
||||
@@ -29,7 +29,6 @@ public interface IOrganizationService
|
||||
Task<List<OrganizationUser>> InviteUsersAsync(Guid organizationId, Guid? invitingUserId, EventSystemUser? systemUser,
|
||||
IEnumerable<(OrganizationUserInvite invite, string externalId)> invites);
|
||||
Task<IEnumerable<Tuple<OrganizationUser, string>>> ResendInvitesAsync(Guid organizationId, Guid? invitingUserId, IEnumerable<Guid> organizationUsersId);
|
||||
Task ResendInviteAsync(Guid organizationId, Guid? invitingUserId, Guid organizationUserId, bool initOrganization = false);
|
||||
Task UpdateUserResetPasswordEnrollmentAsync(Guid organizationId, Guid userId, string resetPasswordKey, Guid? callingUserId);
|
||||
Task ImportAsync(Guid organizationId, IEnumerable<ImportedGroup> groups,
|
||||
IEnumerable<ImportedOrganizationUser> newUsers, IEnumerable<string> removeUserExternalIds,
|
||||
|
||||
@@ -766,21 +766,6 @@ public class OrganizationService : IOrganizationService
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task ResendInviteAsync(Guid organizationId, Guid? invitingUserId, Guid organizationUserId,
|
||||
bool initOrganization = false)
|
||||
{
|
||||
var orgUser = await _organizationUserRepository.GetByIdAsync(organizationUserId);
|
||||
if (orgUser == null || orgUser.OrganizationId != organizationId ||
|
||||
orgUser.Status != OrganizationUserStatusType.Invited)
|
||||
{
|
||||
throw new BadRequestException("User invalid.");
|
||||
}
|
||||
|
||||
_logger.LogUserInviteStateDiagnostics(orgUser);
|
||||
|
||||
var org = await GetOrgById(orgUser.OrganizationId);
|
||||
await SendInviteAsync(orgUser, org, initOrganization);
|
||||
}
|
||||
|
||||
private async Task SendInvitesAsync(IEnumerable<OrganizationUser> orgUsers, Organization organization) =>
|
||||
await _sendOrganizationInvitesCommand.SendInvitesAsync(new SendInvitesRequest(orgUsers, organization));
|
||||
|
||||
@@ -186,6 +186,7 @@ public static class OrganizationServiceCollectionExtensions
|
||||
|
||||
services.AddScoped<IInviteOrganizationUsersCommand, InviteOrganizationUsersCommand>();
|
||||
services.AddScoped<ISendOrganizationInvitesCommand, SendOrganizationInvitesCommand>();
|
||||
services.AddScoped<IResendOrganizationInviteCommand, ResendOrganizationInviteCommand>();
|
||||
|
||||
services.AddScoped<IInviteUsersValidator, InviteOrganizationUsersValidator>();
|
||||
services.AddScoped<IInviteUsersOrganizationValidator, InviteUsersOrganizationValidator>();
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class ResendOrganizationInviteCommandTests
|
||||
{
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task ResendInviteAsync_WhenValidUserAndOrganization_SendsInvite(
|
||||
Organization organization,
|
||||
OrganizationUser organizationUser,
|
||||
SutProvider<ResendOrganizationInviteCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.OrganizationId = organization.Id;
|
||||
organizationUser.Status = OrganizationUserStatusType.Invited;
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetByIdAsync(organizationUser.Id)
|
||||
.Returns(organizationUser);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetByIdAsync(organization.Id)
|
||||
.Returns(organization);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.ResendInviteAsync(organization.Id, invitingUserId: null, organizationUser.Id);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<ISendOrganizationInvitesCommand>()
|
||||
.Received(1)
|
||||
.SendInvitesAsync(Arg.Is<SendInvitesRequest>(req =>
|
||||
req.Organization == organization &&
|
||||
req.Users.Length == 1 &&
|
||||
req.Users[0] == organizationUser &&
|
||||
req.InitOrganization == false));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task ResendInviteAsync_WhenInitOrganizationTrue_SendsInviteWithInitFlag(
|
||||
Organization organization,
|
||||
OrganizationUser organizationUser,
|
||||
SutProvider<ResendOrganizationInviteCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.OrganizationId = organization.Id;
|
||||
organizationUser.Status = OrganizationUserStatusType.Invited;
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetByIdAsync(organizationUser.Id)
|
||||
.Returns(organizationUser);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetByIdAsync(organization.Id)
|
||||
.Returns(organization);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.ResendInviteAsync(organization.Id, invitingUserId: null, organizationUser.Id, initOrganization: true);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<ISendOrganizationInvitesCommand>()
|
||||
.Received(1)
|
||||
.SendInvitesAsync(Arg.Is<SendInvitesRequest>(req =>
|
||||
req.Organization == organization &&
|
||||
req.Users.Length == 1 &&
|
||||
req.Users[0] == organizationUser &&
|
||||
req.InitOrganization == true));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task ResendInviteAsync_WhenOrganizationUserInvalid_ThrowsBadRequest(
|
||||
Organization organization,
|
||||
OrganizationUser organizationUser,
|
||||
SutProvider<ResendOrganizationInviteCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.OrganizationId = organization.Id;
|
||||
organizationUser.Status = OrganizationUserStatusType.Accepted;
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetByIdAsync(organizationUser.Id)
|
||||
.Returns(organizationUser);
|
||||
|
||||
// Act + Assert
|
||||
var ex = await Assert.ThrowsAsync<BadRequestException>(() =>
|
||||
sutProvider.Sut.ResendInviteAsync(organization.Id, invitingUserId: null, organizationUser.Id));
|
||||
|
||||
Assert.Equal("User invalid.", ex.Message);
|
||||
|
||||
await sutProvider.GetDependency<ISendOrganizationInvitesCommand>()
|
||||
.DidNotReceive()
|
||||
.SendInvitesAsync(Arg.Any<SendInvitesRequest>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task ResendInviteAsync_WhenOrganizationNotFound_ThrowsBadRequest(
|
||||
Organization organization,
|
||||
OrganizationUser organizationUser,
|
||||
SutProvider<ResendOrganizationInviteCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.OrganizationId = organization.Id;
|
||||
organizationUser.Status = OrganizationUserStatusType.Invited;
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetByIdAsync(organizationUser.Id)
|
||||
.Returns(organizationUser);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetByIdAsync(organization.Id)
|
||||
.Returns((Organization?)null);
|
||||
|
||||
// Act + Assert
|
||||
var ex = await Assert.ThrowsAsync<BadRequestException>(() =>
|
||||
sutProvider.Sut.ResendInviteAsync(organization.Id, invitingUserId: null, organizationUser.Id));
|
||||
|
||||
Assert.Equal("Organization invalid.", ex.Message);
|
||||
|
||||
await sutProvider.GetDependency<ISendOrganizationInvitesCommand>()
|
||||
.DidNotReceive()
|
||||
.SendInvitesAsync(Arg.Any<SendInvitesRequest>());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user