mirror of
https://github.com/bitwarden/server
synced 2025-12-10 13:23:27 +00:00
[PM-18718] Refactor Bulk Revoke Users (#6601)
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RestoreUser.v1;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RevokeUser.v1;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RestoreUser.v1;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RestoreUser.v1;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RevokeUser.v1;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Text.Json;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RestoreUser.v1;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RevokeUser.v1;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
|
||||
@@ -41,6 +41,8 @@ using Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using V1_RevokeOrganizationUserCommand = Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RevokeUser.v1.IRevokeOrganizationUserCommand;
|
||||
using V2_RevokeOrganizationUserCommand = Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RevokeUser.v2;
|
||||
|
||||
namespace Bit.Api.AdminConsole.Controllers;
|
||||
|
||||
@@ -73,10 +75,11 @@ public class OrganizationUsersController : BaseAdminConsoleController
|
||||
private readonly IResendOrganizationInviteCommand _resendOrganizationInviteCommand;
|
||||
private readonly IBulkResendOrganizationInvitesCommand _bulkResendOrganizationInvitesCommand;
|
||||
private readonly IAutomaticallyConfirmOrganizationUserCommand _automaticallyConfirmOrganizationUserCommand;
|
||||
private readonly V2_RevokeOrganizationUserCommand.IRevokeOrganizationUserCommand _revokeOrganizationUserCommandVNext;
|
||||
private readonly IConfirmOrganizationUserCommand _confirmOrganizationUserCommand;
|
||||
private readonly IRestoreOrganizationUserCommand _restoreOrganizationUserCommand;
|
||||
private readonly IInitPendingOrganizationCommand _initPendingOrganizationCommand;
|
||||
private readonly IRevokeOrganizationUserCommand _revokeOrganizationUserCommand;
|
||||
private readonly V1_RevokeOrganizationUserCommand _revokeOrganizationUserCommand;
|
||||
private readonly IAdminRecoverAccountCommand _adminRecoverAccountCommand;
|
||||
|
||||
public OrganizationUsersController(IOrganizationRepository organizationRepository,
|
||||
@@ -104,11 +107,12 @@ public class OrganizationUsersController : BaseAdminConsoleController
|
||||
IConfirmOrganizationUserCommand confirmOrganizationUserCommand,
|
||||
IRestoreOrganizationUserCommand restoreOrganizationUserCommand,
|
||||
IInitPendingOrganizationCommand initPendingOrganizationCommand,
|
||||
IRevokeOrganizationUserCommand revokeOrganizationUserCommand,
|
||||
V1_RevokeOrganizationUserCommand revokeOrganizationUserCommand,
|
||||
IResendOrganizationInviteCommand resendOrganizationInviteCommand,
|
||||
IBulkResendOrganizationInvitesCommand bulkResendOrganizationInvitesCommand,
|
||||
IAdminRecoverAccountCommand adminRecoverAccountCommand,
|
||||
IAutomaticallyConfirmOrganizationUserCommand automaticallyConfirmOrganizationUserCommand)
|
||||
IAutomaticallyConfirmOrganizationUserCommand automaticallyConfirmOrganizationUserCommand,
|
||||
V2_RevokeOrganizationUserCommand.IRevokeOrganizationUserCommand revokeOrganizationUserCommandVNext)
|
||||
{
|
||||
_organizationRepository = organizationRepository;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
@@ -135,6 +139,7 @@ public class OrganizationUsersController : BaseAdminConsoleController
|
||||
_resendOrganizationInviteCommand = resendOrganizationInviteCommand;
|
||||
_bulkResendOrganizationInvitesCommand = bulkResendOrganizationInvitesCommand;
|
||||
_automaticallyConfirmOrganizationUserCommand = automaticallyConfirmOrganizationUserCommand;
|
||||
_revokeOrganizationUserCommandVNext = revokeOrganizationUserCommandVNext;
|
||||
_confirmOrganizationUserCommand = confirmOrganizationUserCommand;
|
||||
_restoreOrganizationUserCommand = restoreOrganizationUserCommand;
|
||||
_initPendingOrganizationCommand = initPendingOrganizationCommand;
|
||||
@@ -642,7 +647,29 @@ public class OrganizationUsersController : BaseAdminConsoleController
|
||||
[Authorize<ManageUsersRequirement>]
|
||||
public async Task<ListResponseModel<OrganizationUserBulkResponseModel>> BulkRevokeAsync(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model)
|
||||
{
|
||||
return await RestoreOrRevokeUsersAsync(orgId, model, _revokeOrganizationUserCommand.RevokeUsersAsync);
|
||||
if (!_featureService.IsEnabled(FeatureFlagKeys.BulkRevokeUsersV2))
|
||||
{
|
||||
return await RestoreOrRevokeUsersAsync(orgId, model, _revokeOrganizationUserCommand.RevokeUsersAsync);
|
||||
}
|
||||
|
||||
var currentUserId = _userService.GetProperUserId(User);
|
||||
if (currentUserId == null)
|
||||
{
|
||||
throw new UnauthorizedAccessException();
|
||||
}
|
||||
|
||||
var results = await _revokeOrganizationUserCommandVNext.RevokeUsersAsync(
|
||||
new V2_RevokeOrganizationUserCommand.RevokeOrganizationUsersRequest(
|
||||
orgId,
|
||||
model.Ids.ToArray(),
|
||||
new StandardUser(currentUserId.Value, await _currentContext.OrganizationOwner(orgId))));
|
||||
|
||||
return new ListResponseModel<OrganizationUserBulkResponseModel>(results
|
||||
.Select(result => new OrganizationUserBulkResponseModel(result.Id,
|
||||
result.Result.Match(
|
||||
error => error.Message,
|
||||
_ => string.Empty
|
||||
))));
|
||||
}
|
||||
|
||||
[HttpPatch("revoke")]
|
||||
|
||||
@@ -119,7 +119,7 @@ public class OrganizationUserResetPasswordEnrollmentRequestModel
|
||||
|
||||
public class OrganizationUserBulkRequestModel
|
||||
{
|
||||
[Required]
|
||||
[Required, MinLength(1)]
|
||||
public IEnumerable<Guid> Ids { get; set; }
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RevokeUser.v1;
|
||||
|
||||
public interface IRevokeOrganizationUserCommand
|
||||
{
|
||||
@@ -7,7 +7,7 @@ using Bit.Core.Platform.Push;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers;
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RevokeUser.v1;
|
||||
|
||||
public class RevokeOrganizationUserCommand(
|
||||
IEventService eventService,
|
||||
@@ -0,0 +1,8 @@
|
||||
using Bit.Core.AdminConsole.Utilities.v2;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RevokeUser.v2;
|
||||
|
||||
public record UserAlreadyRevoked() : BadRequestError("Already revoked.");
|
||||
public record CannotRevokeYourself() : BadRequestError("You cannot revoke yourself.");
|
||||
public record OnlyOwnersCanRevokeOwners() : BadRequestError("Only owners can revoke other owners.");
|
||||
public record MustHaveConfirmedOwner() : BadRequestError("Organization must have at least one confirmed owner.");
|
||||
@@ -0,0 +1,8 @@
|
||||
using Bit.Core.AdminConsole.Utilities.v2.Results;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RevokeUser.v2;
|
||||
|
||||
public interface IRevokeOrganizationUserCommand
|
||||
{
|
||||
Task<IEnumerable<BulkCommandResult>> RevokeUsersAsync(RevokeOrganizationUsersRequest request);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using Bit.Core.AdminConsole.Utilities.v2.Validation;
|
||||
using Bit.Core.Entities;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RevokeUser.v2;
|
||||
|
||||
public interface IRevokeOrganizationUserValidator
|
||||
{
|
||||
Task<ICollection<ValidationResult<OrganizationUser>>> ValidateAsync(RevokeOrganizationUsersValidationRequest request);
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
using Bit.Core.AdminConsole.Models.Data;
|
||||
using Bit.Core.AdminConsole.Utilities.v2.Results;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Platform.Push;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using OneOf.Types;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RevokeUser.v2;
|
||||
|
||||
public class RevokeOrganizationUserCommand(
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IEventService eventService,
|
||||
IPushNotificationService pushNotificationService,
|
||||
IRevokeOrganizationUserValidator validator,
|
||||
TimeProvider timeProvider,
|
||||
ILogger<RevokeOrganizationUserCommand> logger)
|
||||
: IRevokeOrganizationUserCommand
|
||||
{
|
||||
public async Task<IEnumerable<BulkCommandResult>> RevokeUsersAsync(RevokeOrganizationUsersRequest request)
|
||||
{
|
||||
var validationRequest = await CreateValidationRequestsAsync(request);
|
||||
|
||||
var results = await validator.ValidateAsync(validationRequest);
|
||||
|
||||
var validUsers = results.Where(r => r.IsValid).Select(r => r.Request).ToList();
|
||||
|
||||
await RevokeValidUsersAsync(validUsers);
|
||||
|
||||
await Task.WhenAll(
|
||||
LogRevokedOrganizationUsersAsync(validUsers, request.PerformedBy),
|
||||
SendPushNotificationsAsync(validUsers)
|
||||
);
|
||||
|
||||
return results.Select(r => r.Match(
|
||||
error => new BulkCommandResult(r.Request.Id, error),
|
||||
_ => new BulkCommandResult(r.Request.Id, new None())
|
||||
));
|
||||
}
|
||||
|
||||
private async Task<RevokeOrganizationUsersValidationRequest> CreateValidationRequestsAsync(
|
||||
RevokeOrganizationUsersRequest request)
|
||||
{
|
||||
var organizationUserToRevoke = await organizationUserRepository
|
||||
.GetManyAsync(request.OrganizationUserIdsToRevoke);
|
||||
|
||||
return new RevokeOrganizationUsersValidationRequest(
|
||||
request.OrganizationId,
|
||||
request.OrganizationUserIdsToRevoke,
|
||||
request.PerformedBy,
|
||||
organizationUserToRevoke);
|
||||
}
|
||||
|
||||
private async Task RevokeValidUsersAsync(ICollection<OrganizationUser> validUsers)
|
||||
{
|
||||
if (validUsers.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await organizationUserRepository.RevokeManyByIdAsync(validUsers.Select(u => u.Id));
|
||||
}
|
||||
|
||||
private async Task LogRevokedOrganizationUsersAsync(
|
||||
ICollection<OrganizationUser> revokedUsers,
|
||||
IActingUser actingUser)
|
||||
{
|
||||
if (revokedUsers.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var eventDate = timeProvider.GetUtcNow().UtcDateTime;
|
||||
|
||||
if (actingUser is SystemUser { SystemUserType: not null })
|
||||
{
|
||||
var revokeEventsWithSystem = revokedUsers
|
||||
.Select(user => (user, EventType.OrganizationUser_Revoked, actingUser.SystemUserType!.Value,
|
||||
(DateTime?)eventDate))
|
||||
.ToList();
|
||||
await eventService.LogOrganizationUserEventsAsync(revokeEventsWithSystem);
|
||||
}
|
||||
else
|
||||
{
|
||||
var revokeEvents = revokedUsers
|
||||
.Select(user => (user, EventType.OrganizationUser_Revoked, (DateTime?)eventDate))
|
||||
.ToList();
|
||||
await eventService.LogOrganizationUserEventsAsync(revokeEvents);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SendPushNotificationsAsync(ICollection<OrganizationUser> revokedUsers)
|
||||
{
|
||||
var userIdsToNotify = revokedUsers
|
||||
.Where(user => user.UserId.HasValue)
|
||||
.Select(user => user.UserId!.Value)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
foreach (var userId in userIdsToNotify)
|
||||
{
|
||||
try
|
||||
{
|
||||
await pushNotificationService.PushSyncOrgKeysAsync(userId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogWarning(ex, "Failed to send push notification for user {UserId}.", userId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using Bit.Core.AdminConsole.Models.Data;
|
||||
using Bit.Core.Entities;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RevokeUser.v2;
|
||||
|
||||
public record RevokeOrganizationUsersRequest(
|
||||
Guid OrganizationId,
|
||||
ICollection<Guid> OrganizationUserIdsToRevoke,
|
||||
IActingUser PerformedBy
|
||||
);
|
||||
|
||||
public record RevokeOrganizationUsersValidationRequest(
|
||||
Guid OrganizationId,
|
||||
ICollection<Guid> OrganizationUserIdsToRevoke,
|
||||
IActingUser PerformedBy,
|
||||
ICollection<OrganizationUser> OrganizationUsersToRevoke
|
||||
) : RevokeOrganizationUsersRequest(OrganizationId, OrganizationUserIdsToRevoke, PerformedBy);
|
||||
@@ -0,0 +1,39 @@
|
||||
using Bit.Core.AdminConsole.Models.Data;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.AdminConsole.Utilities.v2.Validation;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using static Bit.Core.AdminConsole.Utilities.v2.Validation.ValidationResultHelpers;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RevokeUser.v2;
|
||||
|
||||
public class RevokeOrganizationUsersValidator(IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery)
|
||||
: IRevokeOrganizationUserValidator
|
||||
{
|
||||
public async Task<ICollection<ValidationResult<OrganizationUser>>> ValidateAsync(
|
||||
RevokeOrganizationUsersValidationRequest request)
|
||||
{
|
||||
var hasRemainingOwner = await hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync(request.OrganizationId,
|
||||
request.OrganizationUsersToRevoke.Select(x => x.Id) // users excluded because they are going to be revoked
|
||||
);
|
||||
|
||||
return request.OrganizationUsersToRevoke.Select(x =>
|
||||
{
|
||||
return x switch
|
||||
{
|
||||
_ when request.PerformedBy is not SystemUser
|
||||
&& x.UserId is not null
|
||||
&& x.UserId == request.PerformedBy.UserId =>
|
||||
Invalid(x, new CannotRevokeYourself()),
|
||||
{ Status: OrganizationUserStatusType.Revoked } =>
|
||||
Invalid(x, new UserAlreadyRevoked()),
|
||||
{ Type: OrganizationUserType.Owner } when !hasRemainingOwner =>
|
||||
Invalid(x, new MustHaveConfirmedOwner()),
|
||||
{ Type: OrganizationUserType.Owner } when !request.PerformedBy.IsOrganizationOwnerOrProvider =>
|
||||
Invalid(x, new OnlyOwnersCanRevokeOwners()),
|
||||
|
||||
_ => Valid(x)
|
||||
};
|
||||
}).ToList();
|
||||
}
|
||||
}
|
||||
@@ -142,6 +142,7 @@ public static class FeatureFlagKeys
|
||||
public const string PM23845_VNextApplicationCache = "pm-24957-refactor-memory-application-cache";
|
||||
public const string BlockClaimedDomainAccountCreation = "pm-28297-block-uninvited-claimed-domain-registration";
|
||||
public const string IncreaseBulkReinviteLimitForCloud = "pm-28251-increase-bulk-reinvite-limit-for-cloud";
|
||||
public const string BulkRevokeUsersV2 = "pm-28456-bulk-revoke-users-v2";
|
||||
|
||||
/* Architecture */
|
||||
public const string DesktopMigrationMilestone1 = "desktop-ui-migration-milestone-1";
|
||||
|
||||
@@ -45,6 +45,9 @@ using Microsoft.AspNetCore.DataProtection;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
using V1_RevokeUsersCommand = Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RevokeUser.v1;
|
||||
using V2_RevokeUsersCommand = Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RevokeUser.v2;
|
||||
|
||||
namespace Bit.Core.OrganizationFeatures;
|
||||
|
||||
public static class OrganizationServiceCollectionExtensions
|
||||
@@ -133,7 +136,6 @@ public static class OrganizationServiceCollectionExtensions
|
||||
{
|
||||
services.AddScoped<IRemoveOrganizationUserCommand, RemoveOrganizationUserCommand>();
|
||||
services.AddScoped<IRevokeNonCompliantOrganizationUserCommand, RevokeNonCompliantOrganizationUserCommand>();
|
||||
services.AddScoped<IRevokeOrganizationUserCommand, RevokeOrganizationUserCommand>();
|
||||
services.AddScoped<IUpdateOrganizationUserCommand, UpdateOrganizationUserCommand>();
|
||||
services.AddScoped<IUpdateOrganizationUserGroupsCommand, UpdateOrganizationUserGroupsCommand>();
|
||||
services.AddScoped<IConfirmOrganizationUserCommand, ConfirmOrganizationUserCommand>();
|
||||
@@ -143,6 +145,11 @@ public static class OrganizationServiceCollectionExtensions
|
||||
|
||||
services.AddScoped<IDeleteClaimedOrganizationUserAccountCommand, DeleteClaimedOrganizationUserAccountCommand>();
|
||||
services.AddScoped<IDeleteClaimedOrganizationUserAccountValidator, DeleteClaimedOrganizationUserAccountValidator>();
|
||||
|
||||
services.AddScoped<V1_RevokeUsersCommand.IRevokeOrganizationUserCommand, V1_RevokeUsersCommand.RevokeOrganizationUserCommand>();
|
||||
|
||||
services.AddScoped<V2_RevokeUsersCommand.IRevokeOrganizationUserCommand, V2_RevokeUsersCommand.RevokeOrganizationUserCommand>();
|
||||
services.AddScoped<V2_RevokeUsersCommand.IRevokeOrganizationUserValidator, V2_RevokeUsersCommand.RevokeOrganizationUsersValidator>();
|
||||
}
|
||||
|
||||
private static void AddOrganizationApiKeyCommandsQueries(this IServiceCollection services)
|
||||
|
||||
@@ -625,7 +625,11 @@ public class OrganizationUserRepository : Repository<OrganizationUser, Guid>, IO
|
||||
|
||||
await connection.ExecuteAsync(
|
||||
"[dbo].[OrganizationUser_SetStatusForUsersByGuidIdArray]",
|
||||
new { OrganizationUserIds = organizationUserIds.ToGuidIdArrayTVP(), Status = OrganizationUserStatusType.Revoked },
|
||||
new
|
||||
{
|
||||
OrganizationUserIds = organizationUserIds.ToGuidIdArrayTVP(),
|
||||
Status = OrganizationUserStatusType.Revoked
|
||||
},
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,347 @@
|
||||
using System.Net;
|
||||
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
||||
using Bit.Api.AdminConsole.Models.Response.Organizations;
|
||||
using Bit.Api.IntegrationTest.Factories;
|
||||
using Bit.Api.IntegrationTest.Helpers;
|
||||
using Bit.Api.Models.Response;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.AdminConsole.Providers.Interfaces;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Api.IntegrationTest.AdminConsole.Controllers;
|
||||
|
||||
public class OrganizationUserControllerBulkRevokeTests : IClassFixture<ApiApplicationFactory>, IAsyncLifetime
|
||||
{
|
||||
private readonly HttpClient _client;
|
||||
private readonly ApiApplicationFactory _factory;
|
||||
private readonly LoginHelper _loginHelper;
|
||||
|
||||
private Organization _organization = null!;
|
||||
private string _ownerEmail = null!;
|
||||
|
||||
public OrganizationUserControllerBulkRevokeTests(ApiApplicationFactory apiFactory)
|
||||
{
|
||||
_factory = apiFactory;
|
||||
_factory.SubstituteService<IFeatureService>(featureService =>
|
||||
{
|
||||
featureService
|
||||
.IsEnabled(FeatureFlagKeys.BulkRevokeUsersV2)
|
||||
.Returns(true);
|
||||
});
|
||||
_client = _factory.CreateClient();
|
||||
_loginHelper = new LoginHelper(_factory, _client);
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
_ownerEmail = $"org-user-bulk-revoke-test-{Guid.NewGuid()}@bitwarden.com";
|
||||
await _factory.LoginWithNewAccount(_ownerEmail);
|
||||
|
||||
(_organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, plan: PlanType.EnterpriseMonthly,
|
||||
ownerEmail: _ownerEmail, passwordManagerSeats: 10, paymentMethod: PaymentMethodType.Card);
|
||||
}
|
||||
|
||||
public Task DisposeAsync()
|
||||
{
|
||||
_client.Dispose();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BulkRevoke_Success()
|
||||
{
|
||||
var (ownerEmail, _) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory,
|
||||
_organization.Id, OrganizationUserType.Owner);
|
||||
|
||||
await _loginHelper.LoginAsync(ownerEmail);
|
||||
|
||||
var (_, orgUser1) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, _organization.Id, OrganizationUserType.User);
|
||||
var (_, orgUser2) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, _organization.Id, OrganizationUserType.User);
|
||||
|
||||
var organizationUserRepository = _factory.GetService<IOrganizationUserRepository>();
|
||||
|
||||
var request = new OrganizationUserBulkRequestModel
|
||||
{
|
||||
Ids = [orgUser1.Id, orgUser2.Id]
|
||||
};
|
||||
|
||||
var httpResponse = await _client.PutAsJsonAsync($"organizations/{_organization.Id}/users/revoke", request);
|
||||
var content = await httpResponse.Content.ReadFromJsonAsync<ListResponseModel<OrganizationUserBulkResponseModel>>();
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, httpResponse.StatusCode);
|
||||
Assert.NotNull(content);
|
||||
Assert.Equal(2, content.Data.Count());
|
||||
Assert.All(content.Data, r => Assert.Empty(r.Error));
|
||||
|
||||
var actualUsers = await organizationUserRepository.GetManyAsync([orgUser1.Id, orgUser2.Id]);
|
||||
Assert.All(actualUsers, u => Assert.Equal(OrganizationUserStatusType.Revoked, u.Status));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BulkRevoke_AsAdmin_Success()
|
||||
{
|
||||
var (adminEmail, _) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory,
|
||||
_organization.Id, OrganizationUserType.Admin);
|
||||
|
||||
await _loginHelper.LoginAsync(adminEmail);
|
||||
|
||||
var (_, orgUser) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, _organization.Id, OrganizationUserType.User);
|
||||
|
||||
var request = new OrganizationUserBulkRequestModel
|
||||
{
|
||||
Ids = [orgUser.Id]
|
||||
};
|
||||
|
||||
var httpResponse = await _client.PutAsJsonAsync($"organizations/{_organization.Id}/users/revoke", request);
|
||||
var content = await httpResponse.Content.ReadFromJsonAsync<ListResponseModel<OrganizationUserBulkResponseModel>>();
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, httpResponse.StatusCode);
|
||||
Assert.NotNull(content);
|
||||
Assert.Single(content.Data);
|
||||
Assert.All(content.Data, r => Assert.Empty(r.Error));
|
||||
|
||||
var actualUser = await _factory.GetService<IOrganizationUserRepository>().GetByIdAsync(orgUser.Id);
|
||||
Assert.NotNull(actualUser);
|
||||
Assert.Equal(OrganizationUserStatusType.Revoked, actualUser.Status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BulkRevoke_CannotRevokeSelf_ReturnsError()
|
||||
{
|
||||
var (userEmail, orgUser) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory,
|
||||
_organization.Id, OrganizationUserType.Admin);
|
||||
|
||||
await _loginHelper.LoginAsync(userEmail);
|
||||
|
||||
var request = new OrganizationUserBulkRequestModel
|
||||
{
|
||||
Ids = [orgUser.Id]
|
||||
};
|
||||
|
||||
var httpResponse = await _client.PutAsJsonAsync($"organizations/{_organization.Id}/users/revoke", request);
|
||||
var content = await httpResponse.Content.ReadFromJsonAsync<ListResponseModel<OrganizationUserBulkResponseModel>>();
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, httpResponse.StatusCode);
|
||||
Assert.NotNull(content);
|
||||
Assert.Single(content.Data);
|
||||
Assert.Contains(content.Data, r => r.Id == orgUser.Id && r.Error == "You cannot revoke yourself.");
|
||||
|
||||
var actualUser = await _factory.GetService<IOrganizationUserRepository>().GetByIdAsync(orgUser.Id);
|
||||
Assert.NotNull(actualUser);
|
||||
Assert.Equal(OrganizationUserStatusType.Confirmed, actualUser.Status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BulkRevoke_AlreadyRevoked_ReturnsError()
|
||||
{
|
||||
var (ownerEmail, _) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory,
|
||||
_organization.Id, OrganizationUserType.Owner);
|
||||
|
||||
await _loginHelper.LoginAsync(ownerEmail);
|
||||
|
||||
var (_, orgUser) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, _organization.Id, OrganizationUserType.User);
|
||||
|
||||
var organizationUserRepository = _factory.GetService<IOrganizationUserRepository>();
|
||||
|
||||
await organizationUserRepository.RevokeAsync(orgUser.Id);
|
||||
|
||||
var request = new OrganizationUserBulkRequestModel
|
||||
{
|
||||
Ids = [orgUser.Id]
|
||||
};
|
||||
|
||||
var httpResponse = await _client.PutAsJsonAsync($"organizations/{_organization.Id}/users/revoke", request);
|
||||
var content = await httpResponse.Content.ReadFromJsonAsync<ListResponseModel<OrganizationUserBulkResponseModel>>();
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, httpResponse.StatusCode);
|
||||
Assert.NotNull(content);
|
||||
Assert.Single(content.Data);
|
||||
Assert.Contains(content.Data, r => r.Id == orgUser.Id && r.Error == "Already revoked.");
|
||||
|
||||
var actualUser = await organizationUserRepository.GetByIdAsync(orgUser.Id);
|
||||
Assert.NotNull(actualUser);
|
||||
Assert.Equal(OrganizationUserStatusType.Revoked, actualUser.Status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BulkRevoke_AdminCannotRevokeOwner_ReturnsError()
|
||||
{
|
||||
var (adminEmail, _) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory,
|
||||
_organization.Id, OrganizationUserType.Admin);
|
||||
|
||||
await _loginHelper.LoginAsync(adminEmail);
|
||||
|
||||
var (_, ownerOrgUser) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, _organization.Id, OrganizationUserType.Owner);
|
||||
|
||||
var request = new OrganizationUserBulkRequestModel
|
||||
{
|
||||
Ids = [ownerOrgUser.Id]
|
||||
};
|
||||
|
||||
var httpResponse = await _client.PutAsJsonAsync($"organizations/{_organization.Id}/users/revoke", request);
|
||||
var content = await httpResponse.Content.ReadFromJsonAsync<ListResponseModel<OrganizationUserBulkResponseModel>>();
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, httpResponse.StatusCode);
|
||||
Assert.NotNull(content);
|
||||
Assert.Single(content.Data);
|
||||
Assert.Contains(content.Data, r => r.Id == ownerOrgUser.Id && r.Error == "Only owners can revoke other owners.");
|
||||
|
||||
var actualUser = await _factory.GetService<IOrganizationUserRepository>().GetByIdAsync(ownerOrgUser.Id);
|
||||
Assert.NotNull(actualUser);
|
||||
Assert.Equal(OrganizationUserStatusType.Confirmed, actualUser.Status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BulkRevoke_MixedResults()
|
||||
{
|
||||
var (ownerEmail, requestingOwner) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory,
|
||||
_organization.Id, OrganizationUserType.Owner);
|
||||
|
||||
await _loginHelper.LoginAsync(ownerEmail);
|
||||
|
||||
var (_, validOrgUser) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, _organization.Id, OrganizationUserType.User);
|
||||
var (_, alreadyRevokedOrgUser) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, _organization.Id, OrganizationUserType.User);
|
||||
|
||||
var organizationUserRepository = _factory.GetService<IOrganizationUserRepository>();
|
||||
|
||||
await organizationUserRepository.RevokeAsync(alreadyRevokedOrgUser.Id);
|
||||
|
||||
var request = new OrganizationUserBulkRequestModel
|
||||
{
|
||||
Ids = [validOrgUser.Id, alreadyRevokedOrgUser.Id, requestingOwner.Id]
|
||||
};
|
||||
|
||||
var httpResponse = await _client.PutAsJsonAsync($"organizations/{_organization.Id}/users/revoke", request);
|
||||
var content = await httpResponse.Content.ReadFromJsonAsync<ListResponseModel<OrganizationUserBulkResponseModel>>();
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, httpResponse.StatusCode);
|
||||
Assert.NotNull(content);
|
||||
Assert.Equal(3, content.Data.Count());
|
||||
|
||||
Assert.Contains(content.Data, r => r.Id == validOrgUser.Id && r.Error == string.Empty);
|
||||
Assert.Contains(content.Data, r => r.Id == alreadyRevokedOrgUser.Id && r.Error == "Already revoked.");
|
||||
Assert.Contains(content.Data, r => r.Id == requestingOwner.Id && r.Error == "You cannot revoke yourself.");
|
||||
|
||||
var actualUsers = await organizationUserRepository.GetManyAsync([validOrgUser.Id, alreadyRevokedOrgUser.Id, requestingOwner.Id]);
|
||||
Assert.Equal(OrganizationUserStatusType.Revoked, actualUsers.First(u => u.Id == validOrgUser.Id).Status);
|
||||
Assert.Equal(OrganizationUserStatusType.Revoked, actualUsers.First(u => u.Id == alreadyRevokedOrgUser.Id).Status);
|
||||
Assert.Equal(OrganizationUserStatusType.Confirmed, actualUsers.First(u => u.Id == requestingOwner.Id).Status);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(OrganizationUserType.User)]
|
||||
[InlineData(OrganizationUserType.Custom)]
|
||||
public async Task BulkRevoke_WithoutManageUsersPermission_ReturnsForbidden(OrganizationUserType organizationUserType)
|
||||
{
|
||||
var (userEmail, _) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory,
|
||||
_organization.Id, organizationUserType, new Permissions { ManageUsers = false });
|
||||
|
||||
await _loginHelper.LoginAsync(userEmail);
|
||||
|
||||
var request = new OrganizationUserBulkRequestModel
|
||||
{
|
||||
Ids = [Guid.NewGuid()]
|
||||
};
|
||||
|
||||
var httpResponse = await _client.PutAsJsonAsync($"organizations/{_organization.Id}/users/revoke", request);
|
||||
|
||||
Assert.Equal(HttpStatusCode.Forbidden, httpResponse.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BulkRevoke_WithEmptyIds_ReturnsBadRequest()
|
||||
{
|
||||
await _loginHelper.LoginAsync(_ownerEmail);
|
||||
|
||||
var request = new OrganizationUserBulkRequestModel
|
||||
{
|
||||
Ids = []
|
||||
};
|
||||
|
||||
var httpResponse = await _client.PutAsJsonAsync($"organizations/{_organization.Id}/users/revoke", request);
|
||||
|
||||
Assert.Equal(HttpStatusCode.BadRequest, httpResponse.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BulkRevoke_WithInvalidOrganizationId_ReturnsForbidden()
|
||||
{
|
||||
var (ownerEmail, _) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory,
|
||||
_organization.Id, OrganizationUserType.Owner);
|
||||
|
||||
await _loginHelper.LoginAsync(ownerEmail);
|
||||
|
||||
var (_, orgUser) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, _organization.Id, OrganizationUserType.User);
|
||||
|
||||
var invalidOrgId = Guid.NewGuid();
|
||||
|
||||
var request = new OrganizationUserBulkRequestModel
|
||||
{
|
||||
Ids = [orgUser.Id]
|
||||
};
|
||||
|
||||
var httpResponse = await _client.PutAsJsonAsync($"organizations/{invalidOrgId}/users/revoke", request);
|
||||
|
||||
Assert.Equal(HttpStatusCode.Forbidden, httpResponse.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BulkRevoke_ProviderRevokesOwner_ReturnsOk()
|
||||
{
|
||||
var providerEmail = $"provider-user{Guid.NewGuid()}@example.com";
|
||||
|
||||
// create user for provider
|
||||
await _factory.LoginWithNewAccount(providerEmail);
|
||||
|
||||
// create provider and provider user
|
||||
await _factory.GetService<ICreateProviderCommand>()
|
||||
.CreateBusinessUnitAsync(
|
||||
new Provider
|
||||
{
|
||||
Name = "provider",
|
||||
Type = ProviderType.BusinessUnit
|
||||
},
|
||||
providerEmail,
|
||||
PlanType.EnterpriseAnnually2023,
|
||||
10);
|
||||
|
||||
await _loginHelper.LoginAsync(providerEmail);
|
||||
|
||||
var providerUserUser = await _factory.GetService<IUserRepository>().GetByEmailAsync(providerEmail);
|
||||
|
||||
var providerUserCollection = await _factory.GetService<IProviderUserRepository>()
|
||||
.GetManyByUserAsync(providerUserUser!.Id);
|
||||
|
||||
var providerUser = providerUserCollection.First();
|
||||
|
||||
await _factory.GetService<IProviderOrganizationRepository>().CreateAsync(new ProviderOrganization
|
||||
{
|
||||
ProviderId = providerUser.ProviderId,
|
||||
OrganizationId = _organization.Id,
|
||||
Key = null,
|
||||
Settings = null
|
||||
});
|
||||
|
||||
var (_, ownerOrgUser) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory,
|
||||
_organization.Id, OrganizationUserType.Owner);
|
||||
|
||||
var request = new OrganizationUserBulkRequestModel
|
||||
{
|
||||
Ids = [ownerOrgUser.Id]
|
||||
};
|
||||
|
||||
var httpResponse = await _client.PutAsJsonAsync($"organizations/{_organization.Id}/users/revoke", request);
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, httpResponse.StatusCode);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RevokeUser.v1;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
using Bit.Core.AdminConsole.Models.Data;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RevokeUser.v2;
|
||||
using Bit.Core.AdminConsole.Utilities.v2.Validation;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Platform.Push;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Test.AutoFixture.OrganizationUserFixtures;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers.RevokeUser.v2;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class RevokeOrganizationUserCommandTests
|
||||
{
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task RevokeUsersAsync_WithValidUsers_RevokesUsersAndLogsEvents(
|
||||
SutProvider<RevokeOrganizationUserCommand> sutProvider,
|
||||
Guid organizationId,
|
||||
Guid actingUserId,
|
||||
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.User)] OrganizationUser orgUser1,
|
||||
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.User)] OrganizationUser orgUser2)
|
||||
{
|
||||
// Arrange
|
||||
orgUser1.OrganizationId = orgUser2.OrganizationId = organizationId;
|
||||
orgUser1.UserId = Guid.NewGuid();
|
||||
orgUser2.UserId = Guid.NewGuid();
|
||||
|
||||
var actingUser = CreateActingUser(actingUserId, false, null);
|
||||
var request = new RevokeOrganizationUsersRequest(
|
||||
organizationId,
|
||||
[orgUser1.Id, orgUser2.Id],
|
||||
actingUser);
|
||||
|
||||
SetupRepositoryMocks(sutProvider, [orgUser1, orgUser2]);
|
||||
SetupValidatorMock(sutProvider, [
|
||||
ValidationResultHelpers.Valid(orgUser1),
|
||||
ValidationResultHelpers.Valid(orgUser2)
|
||||
]);
|
||||
|
||||
// Act
|
||||
var results = (await sutProvider.Sut.RevokeUsersAsync(request)).ToList();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, results.Count);
|
||||
Assert.All(results, r => Assert.True(r.Result.IsSuccess));
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.Received(1)
|
||||
.RevokeManyByIdAsync(Arg.Is<IEnumerable<Guid>>(ids =>
|
||||
ids.Contains(orgUser1.Id) && ids.Contains(orgUser2.Id)));
|
||||
|
||||
await sutProvider.GetDependency<IEventService>()
|
||||
.Received(1)
|
||||
.LogOrganizationUserEventsAsync(Arg.Is<IEnumerable<(OrganizationUser, EventType, DateTime?)>>(
|
||||
events => events.Count() == 2));
|
||||
|
||||
await sutProvider.GetDependency<IPushNotificationService>()
|
||||
.Received(1)
|
||||
.PushSyncOrgKeysAsync(orgUser1.UserId!.Value);
|
||||
|
||||
await sutProvider.GetDependency<IPushNotificationService>()
|
||||
.Received(1)
|
||||
.PushSyncOrgKeysAsync(orgUser2.UserId!.Value);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task RevokeUsersAsync_WithSystemUser_LogsEventsWithSystemUserType(
|
||||
SutProvider<RevokeOrganizationUserCommand> sutProvider,
|
||||
Guid organizationId,
|
||||
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.User)] OrganizationUser orgUser)
|
||||
{
|
||||
// Arrange
|
||||
orgUser.OrganizationId = organizationId;
|
||||
orgUser.UserId = Guid.NewGuid();
|
||||
|
||||
var actingUser = CreateActingUser(null, false, EventSystemUser.SCIM);
|
||||
|
||||
var request = new RevokeOrganizationUsersRequest(
|
||||
organizationId,
|
||||
[orgUser.Id],
|
||||
actingUser);
|
||||
|
||||
SetupRepositoryMocks(sutProvider, [orgUser]);
|
||||
SetupValidatorMock(sutProvider, [ValidationResultHelpers.Valid(orgUser)]);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.RevokeUsersAsync(request);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IEventService>()
|
||||
.Received(1)
|
||||
.LogOrganizationUserEventsAsync(Arg.Is<IEnumerable<(OrganizationUser, EventType, EventSystemUser, DateTime?)>>(
|
||||
events => events.All(e => e.Item3 == EventSystemUser.SCIM)));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task RevokeUsersAsync_WithValidationErrors_ReturnsErrorResults(
|
||||
SutProvider<RevokeOrganizationUserCommand> sutProvider,
|
||||
Guid organizationId,
|
||||
Guid actingUserId,
|
||||
[OrganizationUser(OrganizationUserStatusType.Revoked, OrganizationUserType.User)] OrganizationUser orgUser1,
|
||||
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.User)] OrganizationUser orgUser2)
|
||||
{
|
||||
// Arrange
|
||||
orgUser1.OrganizationId = orgUser2.OrganizationId = organizationId;
|
||||
|
||||
var actingUser = CreateActingUser(actingUserId, false, null);
|
||||
|
||||
var request = new RevokeOrganizationUsersRequest(
|
||||
organizationId,
|
||||
[orgUser1.Id, orgUser2.Id],
|
||||
actingUser);
|
||||
|
||||
SetupRepositoryMocks(sutProvider, [orgUser1, orgUser2]);
|
||||
SetupValidatorMock(sutProvider, [
|
||||
ValidationResultHelpers.Invalid(orgUser1, new UserAlreadyRevoked()),
|
||||
ValidationResultHelpers.Valid(orgUser2)
|
||||
]);
|
||||
|
||||
// Act
|
||||
var results = (await sutProvider.Sut.RevokeUsersAsync(request)).ToList();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, results.Count);
|
||||
var result1 = results.Single(r => r.Id == orgUser1.Id);
|
||||
var result2 = results.Single(r => r.Id == orgUser2.Id);
|
||||
|
||||
Assert.True(result1.Result.IsError);
|
||||
Assert.True(result2.Result.IsSuccess);
|
||||
|
||||
// Only the valid user should be revoked
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.Received(1)
|
||||
.RevokeManyByIdAsync(Arg.Is<IEnumerable<Guid>>(ids =>
|
||||
ids.Count() == 1 && ids.Contains(orgUser2.Id)));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task RevokeUsersAsync_WhenPushNotificationFails_ContinuesProcessing(
|
||||
SutProvider<RevokeOrganizationUserCommand> sutProvider,
|
||||
Guid organizationId,
|
||||
Guid actingUserId,
|
||||
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.User)] OrganizationUser orgUser)
|
||||
{
|
||||
// Arrange
|
||||
orgUser.OrganizationId = organizationId;
|
||||
orgUser.UserId = Guid.NewGuid();
|
||||
|
||||
var actingUser = CreateActingUser(actingUserId, false, null);
|
||||
|
||||
var request = new RevokeOrganizationUsersRequest(
|
||||
organizationId,
|
||||
[orgUser.Id],
|
||||
actingUser);
|
||||
|
||||
SetupRepositoryMocks(sutProvider, [orgUser]);
|
||||
SetupValidatorMock(sutProvider, [ValidationResultHelpers.Valid(orgUser)]);
|
||||
|
||||
sutProvider.GetDependency<IPushNotificationService>()
|
||||
.PushSyncOrgKeysAsync(orgUser.UserId!.Value)
|
||||
.Returns(Task.FromException(new Exception("Push notification failed")));
|
||||
|
||||
// Act
|
||||
var results = (await sutProvider.Sut.RevokeUsersAsync(request)).ToList();
|
||||
|
||||
// Assert
|
||||
Assert.Single(results);
|
||||
Assert.True(results[0].Result.IsSuccess);
|
||||
|
||||
// Should log warning but continue
|
||||
sutProvider.GetDependency<ILogger<RevokeOrganizationUserCommand>>()
|
||||
.Received()
|
||||
.Log(
|
||||
LogLevel.Warning,
|
||||
Arg.Any<EventId>(),
|
||||
Arg.Any<object>(),
|
||||
Arg.Any<Exception>(),
|
||||
Arg.Any<Func<object, Exception?, string>>());
|
||||
}
|
||||
|
||||
private static IActingUser CreateActingUser(Guid? userId, bool isOwnerOrProvider, EventSystemUser? systemUserType) =>
|
||||
(userId, systemUserType) switch
|
||||
{
|
||||
({ } id, _) => new StandardUser(id, isOwnerOrProvider),
|
||||
(null, { } type) => new SystemUser(type)
|
||||
};
|
||||
|
||||
private static void SetupRepositoryMocks(
|
||||
SutProvider<RevokeOrganizationUserCommand> sutProvider,
|
||||
ICollection<OrganizationUser> organizationUsers)
|
||||
{
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyAsync(Arg.Any<IEnumerable<Guid>>())
|
||||
.Returns(organizationUsers);
|
||||
}
|
||||
|
||||
private static void SetupValidatorMock(
|
||||
SutProvider<RevokeOrganizationUserCommand> sutProvider,
|
||||
ICollection<ValidationResult<OrganizationUser>> validationResults)
|
||||
{
|
||||
sutProvider.GetDependency<IRevokeOrganizationUserValidator>()
|
||||
.ValidateAsync(Arg.Any<RevokeOrganizationUsersValidationRequest>())
|
||||
.Returns(validationResults);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,325 @@
|
||||
using Bit.Core.AdminConsole.Models.Data;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RevokeUser.v2;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Test.AutoFixture.OrganizationUserFixtures;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers.RevokeUser.v2;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class RevokeOrganizationUsersValidatorTests
|
||||
{
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task ValidateAsync_WithValidUsers_ReturnsSuccess(
|
||||
SutProvider<RevokeOrganizationUsersValidator> sutProvider,
|
||||
Guid organizationId,
|
||||
Guid actingUserId,
|
||||
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.User)] OrganizationUser orgUser1,
|
||||
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.User)] OrganizationUser orgUser2)
|
||||
{
|
||||
// Arrange
|
||||
orgUser1.OrganizationId = orgUser2.OrganizationId = organizationId;
|
||||
orgUser1.UserId = Guid.NewGuid();
|
||||
orgUser2.UserId = Guid.NewGuid();
|
||||
|
||||
var actingUser = CreateActingUser(actingUserId, false, null);
|
||||
var request = CreateValidationRequest(
|
||||
organizationId,
|
||||
[orgUser1, orgUser2],
|
||||
actingUser);
|
||||
|
||||
sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()
|
||||
.HasConfirmedOwnersExceptAsync(organizationId, Arg.Any<IEnumerable<Guid>>())
|
||||
.Returns(true);
|
||||
|
||||
// Act
|
||||
var results = (await sutProvider.Sut.ValidateAsync(request)).ToList();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, results.Count);
|
||||
Assert.All(results, r => Assert.True(r.IsValid));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task ValidateAsync_WithRevokedUser_ReturnsErrorForThatUser(
|
||||
SutProvider<RevokeOrganizationUsersValidator> sutProvider,
|
||||
Guid organizationId,
|
||||
Guid actingUserId,
|
||||
[OrganizationUser(OrganizationUserStatusType.Revoked, OrganizationUserType.User)] OrganizationUser revokedUser)
|
||||
{
|
||||
// Arrange
|
||||
revokedUser.OrganizationId = organizationId;
|
||||
revokedUser.UserId = Guid.NewGuid();
|
||||
|
||||
var actingUser = CreateActingUser(actingUserId, false, null);
|
||||
var request = CreateValidationRequest(
|
||||
organizationId,
|
||||
[revokedUser],
|
||||
actingUser);
|
||||
|
||||
sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()
|
||||
.HasConfirmedOwnersExceptAsync(organizationId, Arg.Any<IEnumerable<Guid>>())
|
||||
.Returns(true);
|
||||
|
||||
// Act
|
||||
var results = (await sutProvider.Sut.ValidateAsync(request)).ToList();
|
||||
|
||||
// Assert
|
||||
Assert.Single(results);
|
||||
Assert.True(results.First().IsError);
|
||||
Assert.IsType<UserAlreadyRevoked>(results.First().AsError);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task ValidateAsync_WhenRevokingSelf_ReturnsErrorForThatUser(
|
||||
SutProvider<RevokeOrganizationUsersValidator> sutProvider,
|
||||
Guid organizationId,
|
||||
Guid actingUserId,
|
||||
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.User)] OrganizationUser orgUser)
|
||||
{
|
||||
// Arrange
|
||||
orgUser.OrganizationId = organizationId;
|
||||
orgUser.UserId = actingUserId;
|
||||
|
||||
var actingUser = CreateActingUser(actingUserId, false, null);
|
||||
var request = CreateValidationRequest(
|
||||
organizationId,
|
||||
[orgUser],
|
||||
actingUser);
|
||||
|
||||
sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()
|
||||
.HasConfirmedOwnersExceptAsync(organizationId, Arg.Any<IEnumerable<Guid>>())
|
||||
.Returns(true);
|
||||
|
||||
// Act
|
||||
var results = (await sutProvider.Sut.ValidateAsync(request)).ToList();
|
||||
|
||||
// Assert
|
||||
Assert.Single(results);
|
||||
Assert.True(results.First().IsError);
|
||||
Assert.IsType<CannotRevokeYourself>(results.First().AsError);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task ValidateAsync_WhenNonOwnerRevokesOwner_ReturnsErrorForThatUser(
|
||||
SutProvider<RevokeOrganizationUsersValidator> sutProvider,
|
||||
Guid organizationId,
|
||||
Guid actingUserId,
|
||||
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser ownerUser)
|
||||
{
|
||||
// Arrange
|
||||
ownerUser.OrganizationId = organizationId;
|
||||
ownerUser.UserId = Guid.NewGuid();
|
||||
|
||||
var actingUser = CreateActingUser(actingUserId, false, null);
|
||||
var request = CreateValidationRequest(
|
||||
organizationId,
|
||||
[ownerUser],
|
||||
actingUser);
|
||||
|
||||
sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()
|
||||
.HasConfirmedOwnersExceptAsync(organizationId, Arg.Any<IEnumerable<Guid>>())
|
||||
.Returns(true);
|
||||
|
||||
// Act
|
||||
var results = (await sutProvider.Sut.ValidateAsync(request)).ToList();
|
||||
|
||||
// Assert
|
||||
Assert.Single(results);
|
||||
Assert.True(results.First().IsError);
|
||||
Assert.IsType<OnlyOwnersCanRevokeOwners>(results.First().AsError);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task ValidateAsync_WhenOwnerRevokesOwner_ReturnsSuccess(
|
||||
SutProvider<RevokeOrganizationUsersValidator> sutProvider,
|
||||
Guid organizationId,
|
||||
Guid actingUserId,
|
||||
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser ownerUser)
|
||||
{
|
||||
// Arrange
|
||||
ownerUser.OrganizationId = organizationId;
|
||||
ownerUser.UserId = Guid.NewGuid();
|
||||
|
||||
var actingUser = CreateActingUser(actingUserId, true, null);
|
||||
var request = CreateValidationRequest(
|
||||
organizationId,
|
||||
[ownerUser],
|
||||
actingUser);
|
||||
|
||||
sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()
|
||||
.HasConfirmedOwnersExceptAsync(organizationId, Arg.Any<IEnumerable<Guid>>())
|
||||
.Returns(true);
|
||||
|
||||
// Act
|
||||
var results = (await sutProvider.Sut.ValidateAsync(request)).ToList();
|
||||
|
||||
// Assert
|
||||
Assert.Single(results);
|
||||
Assert.True(results.First().IsValid);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task ValidateAsync_WithMultipleUsers_SomeValid_ReturnsMixedResults(
|
||||
SutProvider<RevokeOrganizationUsersValidator> sutProvider,
|
||||
Guid organizationId,
|
||||
Guid actingUserId,
|
||||
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.User)] OrganizationUser validUser,
|
||||
[OrganizationUser(OrganizationUserStatusType.Revoked, OrganizationUserType.User)] OrganizationUser revokedUser)
|
||||
{
|
||||
// Arrange
|
||||
validUser.OrganizationId = revokedUser.OrganizationId = organizationId;
|
||||
validUser.UserId = Guid.NewGuid();
|
||||
revokedUser.UserId = Guid.NewGuid();
|
||||
|
||||
var actingUser = CreateActingUser(actingUserId, false, null);
|
||||
var request = CreateValidationRequest(
|
||||
organizationId,
|
||||
[validUser, revokedUser],
|
||||
actingUser);
|
||||
|
||||
sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()
|
||||
.HasConfirmedOwnersExceptAsync(organizationId, Arg.Any<IEnumerable<Guid>>())
|
||||
.Returns(true);
|
||||
|
||||
// Act
|
||||
var results = (await sutProvider.Sut.ValidateAsync(request)).ToList();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, results.Count);
|
||||
|
||||
var validResult = results.Single(r => r.Request.Id == validUser.Id);
|
||||
var errorResult = results.Single(r => r.Request.Id == revokedUser.Id);
|
||||
|
||||
Assert.True(validResult.IsValid);
|
||||
Assert.True(errorResult.IsError);
|
||||
Assert.IsType<UserAlreadyRevoked>(errorResult.AsError);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task ValidateAsync_WithSystemUser_DoesNotRequireActingUserId(
|
||||
SutProvider<RevokeOrganizationUsersValidator> sutProvider,
|
||||
Guid organizationId,
|
||||
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.User)] OrganizationUser orgUser)
|
||||
{
|
||||
// Arrange
|
||||
orgUser.OrganizationId = organizationId;
|
||||
orgUser.UserId = Guid.NewGuid();
|
||||
|
||||
var actingUser = CreateActingUser(null, false, EventSystemUser.SCIM);
|
||||
var request = CreateValidationRequest(
|
||||
organizationId,
|
||||
[orgUser],
|
||||
actingUser);
|
||||
|
||||
sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()
|
||||
.HasConfirmedOwnersExceptAsync(organizationId, Arg.Any<IEnumerable<Guid>>())
|
||||
.Returns(true);
|
||||
|
||||
// Act
|
||||
var results = (await sutProvider.Sut.ValidateAsync(request)).ToList();
|
||||
|
||||
// Assert
|
||||
Assert.Single(results);
|
||||
Assert.True(results.First().IsValid);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task ValidateAsync_WhenRevokingLastOwner_ReturnsErrorForThatUser(
|
||||
SutProvider<RevokeOrganizationUsersValidator> sutProvider,
|
||||
Guid organizationId,
|
||||
Guid actingUserId,
|
||||
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser lastOwner)
|
||||
{
|
||||
// Arrange
|
||||
lastOwner.OrganizationId = organizationId;
|
||||
lastOwner.UserId = Guid.NewGuid();
|
||||
|
||||
var actingUser = CreateActingUser(actingUserId, true, null); // Is an owner
|
||||
var request = CreateValidationRequest(
|
||||
organizationId,
|
||||
[lastOwner],
|
||||
actingUser);
|
||||
|
||||
sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()
|
||||
.HasConfirmedOwnersExceptAsync(organizationId, Arg.Any<IEnumerable<Guid>>())
|
||||
.Returns(false);
|
||||
|
||||
// Act
|
||||
var results = (await sutProvider.Sut.ValidateAsync(request)).ToList();
|
||||
|
||||
// Assert
|
||||
Assert.Single(results);
|
||||
Assert.True(results.First().IsError);
|
||||
Assert.IsType<MustHaveConfirmedOwner>(results.First().AsError);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task ValidateAsync_WithMultipleValidationErrors_ReturnsAllErrors(
|
||||
SutProvider<RevokeOrganizationUsersValidator> sutProvider,
|
||||
Guid organizationId,
|
||||
Guid actingUserId,
|
||||
[OrganizationUser(OrganizationUserStatusType.Revoked, OrganizationUserType.User)] OrganizationUser revokedUser,
|
||||
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser ownerUser)
|
||||
{
|
||||
// Arrange
|
||||
revokedUser.OrganizationId = ownerUser.OrganizationId = organizationId;
|
||||
revokedUser.UserId = Guid.NewGuid();
|
||||
ownerUser.UserId = Guid.NewGuid();
|
||||
|
||||
var actingUser = CreateActingUser(actingUserId, false, null); // Not an owner
|
||||
var request = CreateValidationRequest(
|
||||
organizationId,
|
||||
[revokedUser, ownerUser],
|
||||
actingUser);
|
||||
|
||||
sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()
|
||||
.HasConfirmedOwnersExceptAsync(organizationId, Arg.Any<IEnumerable<Guid>>())
|
||||
.Returns(true);
|
||||
|
||||
// Act
|
||||
var results = (await sutProvider.Sut.ValidateAsync(request)).ToList();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, results.Count);
|
||||
Assert.All(results, r => Assert.True(r.IsError));
|
||||
|
||||
Assert.Contains(results, r => r.AsError is UserAlreadyRevoked);
|
||||
Assert.Contains(results, r => r.AsError is OnlyOwnersCanRevokeOwners);
|
||||
}
|
||||
|
||||
private static IActingUser CreateActingUser(Guid? userId, bool isOwnerOrProvider, EventSystemUser? systemUserType) =>
|
||||
(userId, systemUserType) switch
|
||||
{
|
||||
({ } id, _) => new StandardUser(id, isOwnerOrProvider),
|
||||
(null, { } type) => new SystemUser(type)
|
||||
};
|
||||
|
||||
|
||||
private static RevokeOrganizationUsersValidationRequest CreateValidationRequest(
|
||||
Guid organizationId,
|
||||
ICollection<OrganizationUser> organizationUsers,
|
||||
IActingUser actingUser)
|
||||
{
|
||||
return new RevokeOrganizationUsersValidationRequest(
|
||||
organizationId,
|
||||
organizationUsers.Select(u => u.Id).ToList(),
|
||||
actingUser,
|
||||
organizationUsers
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user