1
0
mirror of https://github.com/bitwarden/server synced 2025-12-29 06:33:43 +00:00

[PM-18718] Refactor Bulk Revoke Users (#6601)

This commit is contained in:
Jared McCannon
2025-12-05 11:19:26 -06:00
committed by GitHub
parent d5f39eac91
commit 2f893768f5
20 changed files with 1135 additions and 13 deletions

View File

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

View File

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

View File

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