1
0
mirror of https://github.com/bitwarden/server.git synced 2025-06-30 07:36:14 -05:00

Allow for bulk processing new login device requests (#4064)

* Define a model for updating many auth requests

In order to facilitate a command method that can update many auth
requests at one time a new model must be defined that accepts valid
input for the command's needs. To achieve this a new file has been
created at
`Core/AdminConsole/OrganizationAuth/Models/OrganizationAuthRequestUpdateCommandModel.cs`
that contains a class of the same name. It's properties match those that
need to come from any calling API request models to fulfill the request.

* Declare a new command interface method

Calling API functions of the `UpdateOrganizationAuthRequestCommand` need
a function that can accept many auth request response objects and
process them as approved or denied. To achieve this a new function has
been added to `IUpdateOrganizationAuthRequestCommand` called
`UpdateManyAsync()` that accepts an
`IEnumberable<OrganizationAuthRequest>` and returns a `Task`.
Implementations of this interface method will be used to bulk process
auth requests as approved or denied.

* Stub out method implementation for unit testing

To facilitate a bulk device login request approval workflow in the admin
console `UpdateOrganizationAuthRequestCommand` needs to be updated to
include an `UpdateMany()` method. It should accept a list of
`OrganizationAuthRequestUpdateCommandModel` objects, perform some simple
data validation checks, and then pass those along to
`AuthRequestRepository` for updating in the database.

This commit stubs out this method for the purpose of writing unit tests.
At this stage the method throws a `NotImplementedException()`. It will
be expand after writing assertions.

* Inject `IAuthRequestRepository` into `UpdateOrganizationAuthCommand`

The updates to `UpdateOrganizationAuthRequestCommand` require a new
direct dependency on `IAuthRequestRepository`. This commit simply
registers this dependency in the `UpdateOrganizationAuthRequest`
constructor for use in unit tests and the `UpdateManyAsync()`
implementation.

* Write tests

* Rename `UpdateManyAsync()` to `UpdateAsync`

* Drop the `CommandModel` suffix

* Invert business logic update filters

* Rework everything to be more model-centric

* Bulk send push notifications

* Write tests that validate the command as a whole

* Fix a test that I broke by mistake

* Swap to using await instead of chained methods for processing

* Seperate a function arguement into a variable declaration

* Ungeneric-ify the processor

* Adjust ternary formatting

* Adjust naming of methods regarding logging organization events

* Throw an exception if Process is called with no auth request loaded

* Rename `_updates` -> `_update`

* Rename email methods

* Stop returning `this`

* Allow callbacks to be null

* Make some assertions about the state of a processed auth request

* Be more terse about arguements in happy path test

* Remove unneeded null check

* Expose an endpoint for bulk processing of organization auth requests  (#4077)

---------

Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com>
This commit is contained in:
Addison Beck
2024-05-26 20:56:52 -05:00
committed by GitHub
parent 0d2e953459
commit 98a191a5e8
15 changed files with 1054 additions and 21 deletions

View File

@ -0,0 +1,258 @@
using Bit.Core.AdminConsole.OrganizationAuth.Models;
using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Models.Data;
using Bit.Core.Enums;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.AdminConsole.OrganizationAuth.Models;
[SutProviderCustomize]
public class AuthRequestUpdateProcessorTests
{
[Theory]
[BitAutoData]
public void Process_NoAuthRequestLoaded_Throws(
OrganizationAuthRequestUpdate update,
AuthRequestUpdateProcessorConfiguration processorConfiguration
)
{
var sut = new AuthRequestUpdateProcessor(null, update, processorConfiguration);
Assert.ThrowsAny<AuthRequestUpdateCouldNotBeProcessedException>(() => sut.Process());
}
[Theory]
[BitAutoData]
public void Process_RequestIsAlreadyApproved_Throws(
OrganizationAdminAuthRequest authRequest,
OrganizationAuthRequestUpdate update,
AuthRequestUpdateProcessorConfiguration processorConfiguration
)
{
(authRequest, processorConfiguration) = UnrespondAndEnsureValid(authRequest, update, processorConfiguration);
authRequest = Approve(authRequest);
var sut = new AuthRequestUpdateProcessor(authRequest, update, processorConfiguration);
Assert.ThrowsAny<AuthRequestUpdateCouldNotBeProcessedException>(() => sut.Process());
}
[Theory]
[BitAutoData]
public void Process_RequestIsAlreadyDenied_Throws(
OrganizationAdminAuthRequest authRequest,
OrganizationAuthRequestUpdate update,
AuthRequestUpdateProcessorConfiguration processorConfiguration
)
{
(authRequest, processorConfiguration) = UnrespondAndEnsureValid(authRequest, update, processorConfiguration);
authRequest = Deny(authRequest);
var sut = new AuthRequestUpdateProcessor(authRequest, update, processorConfiguration);
Assert.ThrowsAny<AuthRequestUpdateCouldNotBeProcessedException>(() => sut.Process());
}
[Theory]
[BitAutoData]
public void Process_RequestIsExpired_Throws(
OrganizationAdminAuthRequest authRequest,
OrganizationAuthRequestUpdate update,
AuthRequestUpdateProcessorConfiguration processorConfiguration
)
{
(authRequest, processorConfiguration) = UnrespondAndEnsureValid(authRequest, update, processorConfiguration);
processorConfiguration.AuthRequestExpiresAfter = new TimeSpan(0, 10, 0);
authRequest.CreationDate = DateTime.UtcNow.AddMinutes(-60);
var sut = new AuthRequestUpdateProcessor(authRequest, update, processorConfiguration);
Assert.ThrowsAny<AuthRequestUpdateCouldNotBeProcessedException>(() => sut.Process());
}
[Theory]
[BitAutoData]
public void Process_UpdateDoesNotMatch_Throws(
OrganizationAdminAuthRequest authRequest,
OrganizationAuthRequestUpdate update,
AuthRequestUpdateProcessorConfiguration processorConfiguration
)
{
(authRequest, processorConfiguration) = UnrespondAndEnsureValid(authRequest, update, processorConfiguration);
while (authRequest.Id == update.Id)
{
authRequest.Id = new Guid();
}
var sut = new AuthRequestUpdateProcessor(authRequest, update, processorConfiguration);
Assert.ThrowsAny<AuthRequestUpdateCouldNotBeProcessedException>(() => sut.Process());
}
[Theory]
[BitAutoData]
public void Process_AuthRequestAndOrganizationIdMismatch_Throws(
OrganizationAdminAuthRequest authRequest,
OrganizationAuthRequestUpdate update,
AuthRequestUpdateProcessorConfiguration processorConfiguration
)
{
(authRequest, processorConfiguration) = UnrespondAndEnsureValid(authRequest, update, processorConfiguration);
while (authRequest.OrganizationId == processorConfiguration.OrganizationId)
{
authRequest.OrganizationId = new Guid();
}
var sut = new AuthRequestUpdateProcessor(authRequest, update, processorConfiguration);
Assert.ThrowsAny<AuthRequestUpdateCouldNotBeProcessedException>(() => sut.Process());
}
[Theory]
[BitAutoData]
public void Process_RequestApproved_NoKey_Throws(
OrganizationAdminAuthRequest authRequest,
OrganizationAuthRequestUpdate update,
AuthRequestUpdateProcessorConfiguration processorConfiguration
)
{
(authRequest, processorConfiguration) = UnrespondAndEnsureValid(authRequest, update, processorConfiguration);
update.Approved = true;
update.Key = null;
var sut = new AuthRequestUpdateProcessor(authRequest, update, processorConfiguration);
Assert.ThrowsAny<ApprovedAuthRequestIsMissingKeyException>(() => sut.Process());
}
[Theory]
[BitAutoData]
public void Process_RequestApproved_ValidInput_Works(
OrganizationAdminAuthRequest authRequest,
OrganizationAuthRequestUpdate update,
AuthRequestUpdateProcessorConfiguration processorConfiguration
)
{
(authRequest, processorConfiguration) = UnrespondAndEnsureValid(authRequest, update, processorConfiguration);
update.Approved = true;
update.Key = "key";
var sut = new AuthRequestUpdateProcessor(authRequest, update, processorConfiguration);
sut.Process();
Assert.True(sut.ProcessedAuthRequest.Approved);
Assert.Equal(sut.ProcessedAuthRequest.Key, update.Key);
Assert.NotNull(sut.ProcessedAuthRequest.ResponseDate);
}
[Theory]
[BitAutoData]
public void Process_RequestDenied_ValidInput_Works(
OrganizationAdminAuthRequest authRequest,
OrganizationAuthRequestUpdate update,
AuthRequestUpdateProcessorConfiguration processorConfiguration
)
{
(authRequest, processorConfiguration) = UnrespondAndEnsureValid(authRequest, update, processorConfiguration);
update.Approved = false;
var sut = new AuthRequestUpdateProcessor(authRequest, update, processorConfiguration);
sut.Process();
Assert.False(sut.ProcessedAuthRequest.Approved);
Assert.Null(sut.ProcessedAuthRequest.Key);
Assert.NotNull(sut.ProcessedAuthRequest.ResponseDate);
}
[Theory]
[BitAutoData]
public async Task SendPushNotification_RequestIsDenied_DoesNotSend(
OrganizationAdminAuthRequest authRequest,
OrganizationAuthRequestUpdate update,
AuthRequestUpdateProcessorConfiguration processorConfiguration
)
{
(authRequest, processorConfiguration) = UnrespondAndEnsureValid(authRequest, update, processorConfiguration);
update.Approved = false;
var sut = new AuthRequestUpdateProcessor(authRequest, update, processorConfiguration);
var callback = Substitute.For<Func<OrganizationAdminAuthRequest, Task>>();
sut.Process();
await sut.SendPushNotification(callback);
await callback.DidNotReceiveWithAnyArgs()(sut.ProcessedAuthRequest);
}
[Theory]
[BitAutoData]
public async Task SendPushNotification_RequestIsApproved_DoesSend(
OrganizationAdminAuthRequest authRequest,
OrganizationAuthRequestUpdate update,
AuthRequestUpdateProcessorConfiguration processorConfiguration
)
{
(authRequest, processorConfiguration) = UnrespondAndEnsureValid(authRequest, update, processorConfiguration);
update.Approved = true;
update.Key = "key";
var sut = new AuthRequestUpdateProcessor(authRequest, update, processorConfiguration);
var callback = Substitute.For<Func<OrganizationAdminAuthRequest, Task>>();
sut.Process();
await sut.SendPushNotification(callback);
await callback.Received()(sut.ProcessedAuthRequest);
}
[Theory]
[BitAutoData]
public async Task SendApprovalEmail_RequestIsDenied_DoesNotSend(
OrganizationAdminAuthRequest authRequest,
OrganizationAuthRequestUpdate update,
AuthRequestUpdateProcessorConfiguration processorConfiguration
)
{
(authRequest, processorConfiguration) = UnrespondAndEnsureValid(authRequest, update, processorConfiguration);
update.Approved = false;
var sut = new AuthRequestUpdateProcessor(authRequest, update, processorConfiguration);
var callback = Substitute.For<Func<OrganizationAdminAuthRequest, string, Task>>();
sut.Process();
await sut.SendApprovalEmail(callback);
await callback.DidNotReceiveWithAnyArgs()(sut.ProcessedAuthRequest, "string");
}
[Theory]
[BitAutoData]
public async Task SendApprovalEmail_RequestIsApproved_DoesSend(
OrganizationAdminAuthRequest authRequest,
OrganizationAuthRequestUpdate update,
AuthRequestUpdateProcessorConfiguration processorConfiguration
)
{
(authRequest, processorConfiguration) = UnrespondAndEnsureValid(authRequest, update, processorConfiguration);
authRequest.RequestDeviceType = DeviceType.iOS;
authRequest.RequestDeviceIdentifier = "device-id";
update.Approved = true;
update.Key = "key";
var sut = new AuthRequestUpdateProcessor(authRequest, update, processorConfiguration);
var callback = Substitute.For<Func<OrganizationAdminAuthRequest, string, Task>>();
sut.Process();
await sut.SendApprovalEmail(callback);
await callback.Received()(sut.ProcessedAuthRequest, "iOS - device-id");
}
private static T Approve<T>(T authRequest) where T : AuthRequest
{
authRequest.Key = "key";
authRequest.Approved = true;
authRequest.ResponseDate = DateTime.UtcNow;
return authRequest;
}
private static T Deny<T>(T authRequest) where T : AuthRequest
{
authRequest.Approved = false;
authRequest.ResponseDate = DateTime.UtcNow;
return authRequest;
}
private (
T AuthRequest,
AuthRequestUpdateProcessorConfiguration ProcessorConfiguration
) UnrespondAndEnsureValid<T>(
T authRequest,
OrganizationAuthRequestUpdate update,
AuthRequestUpdateProcessorConfiguration processorConfiguration
) where T : AuthRequest
{
authRequest.Id = update.Id;
authRequest.OrganizationId = processorConfiguration.OrganizationId;
authRequest.Key = null;
authRequest.Approved = null;
authRequest.ResponseDate = null;
authRequest.AuthenticationDate = null;
authRequest.CreationDate = DateTime.UtcNow.AddMinutes(-1);
processorConfiguration.AuthRequestExpiresAfter = new TimeSpan(1, 0, 0);
return (authRequest, processorConfiguration);
}
}

View File

@ -0,0 +1,205 @@
using Bit.Core.AdminConsole.OrganizationAuth.Models;
using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Models.Data;
using Bit.Core.Enums;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.AdminConsole.OrganizationAuth.Models;
[SutProviderCustomize]
public class BatchAuthRequestUpdateProcessorTests
{
[Theory]
[BitAutoData]
public void Process_NoProcessors_Handled(
IEnumerable<OrganizationAuthRequestUpdate> updates,
AuthRequestUpdateProcessorConfiguration configuration,
Action<Exception> errorHandler
)
{
var sut = new BatchAuthRequestUpdateProcessor(null, updates, configuration);
sut.Process(errorHandler);
}
[Theory]
[BitAutoData]
public void Process_BadInput_CallsHandler(
List<OrganizationAdminAuthRequest> authRequests,
IEnumerable<OrganizationAuthRequestUpdate> updates,
AuthRequestUpdateProcessorConfiguration configuration
)
{
// An already approved auth request should break the processor
// immediately.
authRequests[0].Approved = true;
var sut = new BatchAuthRequestUpdateProcessor(authRequests, updates, configuration);
var errorHandler = Substitute.For<Action<Exception>>();
sut.Process(errorHandler);
errorHandler.ReceivedWithAnyArgs()(new AuthRequestUpdateProcessingException());
}
[Theory]
[BitAutoData]
public void Process_ValidInput_Works(
List<OrganizationAdminAuthRequest> authRequests,
List<OrganizationAuthRequestUpdate> updates,
AuthRequestUpdateProcessorConfiguration configuration,
Action<Exception> errorHandler
)
{
(authRequests[0], updates[0], configuration) = UnrespondAndEnsureValid(authRequests[0], updates[0], configuration);
var sut = new BatchAuthRequestUpdateProcessor(authRequests, updates, configuration);
Assert.NotEmpty(sut.Processors);
sut.Process(errorHandler);
Assert.NotEmpty(sut.Processors.Where(p => p.ProcessedAuthRequest != null));
}
[Theory]
[BitAutoData]
public async Task Save_NoProcessedAuthRequests_IsHandled(
List<OrganizationAuthRequestUpdate> updates,
AuthRequestUpdateProcessorConfiguration configuration,
Func<IEnumerable<AuthRequest>, Task> saveCallback
)
{
var sut = new BatchAuthRequestUpdateProcessor(null, updates, configuration);
Assert.Empty(sut.Processors);
await sut.Save(saveCallback);
}
[Theory]
[BitAutoData]
public async Task Save_ProcessedAuthRequests_IsHandled(
List<OrganizationAdminAuthRequest> authRequests,
List<OrganizationAuthRequestUpdate> updates,
AuthRequestUpdateProcessorConfiguration configuration,
Action<Exception> errorHandler
)
{
(authRequests[0], updates[0], configuration) = UnrespondAndEnsureValid(authRequests[0], updates[0], configuration);
var sut = new BatchAuthRequestUpdateProcessor(authRequests, updates, configuration);
var saveCallback = Substitute.For<Func<IEnumerable<OrganizationAdminAuthRequest>, Task>>();
await sut.Process(errorHandler).Save(saveCallback);
await saveCallback.ReceivedWithAnyArgs()(Arg.Any<IEnumerable<OrganizationAdminAuthRequest>>());
}
[Theory]
[BitAutoData]
public async Task SendPushNotifications_NoProcessors_IsHandled
(
List<OrganizationAuthRequestUpdate> updates,
AuthRequestUpdateProcessorConfiguration configuration,
Func<AuthRequest, Task> callback
)
{
var sut = new BatchAuthRequestUpdateProcessor(null, updates, configuration);
Assert.Empty(sut.Processors);
await sut.SendPushNotifications(callback);
}
[Theory]
[BitAutoData]
public async Task SendPushNotifications_HasProcessors_Sends
(
List<OrganizationAdminAuthRequest> authRequests,
List<OrganizationAuthRequestUpdate> updates,
AuthRequestUpdateProcessorConfiguration configuration,
Action<Exception> errorHandler
)
{
(authRequests[0], updates[0], configuration) = UnrespondAndEnsureValid(authRequests[0], updates[0], configuration);
var sut = new BatchAuthRequestUpdateProcessor(authRequests, updates, configuration);
var callback = Substitute.For<Func<OrganizationAdminAuthRequest, Task>>();
await sut.Process(errorHandler).SendPushNotifications(callback);
await callback.ReceivedWithAnyArgs()(Arg.Any<OrganizationAdminAuthRequest>());
}
[Theory]
[BitAutoData]
public async Task SendApprovalEmailsForProcessedRequests_NoProcessors_IsHandled
(
List<OrganizationAuthRequestUpdate> updates,
AuthRequestUpdateProcessorConfiguration configuration,
Func<AuthRequest, string, Task> callback
)
{
var sut = new BatchAuthRequestUpdateProcessor(null, updates, configuration);
Assert.Empty(sut.Processors);
await sut.SendApprovalEmailsForProcessedRequests(callback);
}
[Theory]
[BitAutoData]
public async Task SendApprovalEmailsForProcessedRequests_HasProcessors_Sends
(
List<OrganizationAdminAuthRequest> authRequests,
List<OrganizationAuthRequestUpdate> updates,
AuthRequestUpdateProcessorConfiguration configuration,
Action<Exception> errorHandler
)
{
(authRequests[0], updates[0], configuration) = UnrespondAndEnsureValid(authRequests[0], updates[0], configuration);
var sut = new BatchAuthRequestUpdateProcessor(authRequests, updates, configuration);
var callback = Substitute.For<Func<OrganizationAdminAuthRequest, string, Task>>();
await sut.Process(errorHandler).SendApprovalEmailsForProcessedRequests(callback);
await callback.ReceivedWithAnyArgs()(Arg.Any<OrganizationAdminAuthRequest>(), Arg.Any<string>());
}
[Theory]
[BitAutoData]
public async Task LogOrganizationEventsForProcessedRequests_NoProcessedAuthRequests_IsHandled
(
List<OrganizationAuthRequestUpdate> updates,
AuthRequestUpdateProcessorConfiguration configuration
)
{
var sut = new BatchAuthRequestUpdateProcessor(null, updates, configuration);
var callback = Substitute.For<Func<IEnumerable<(OrganizationAdminAuthRequest, EventType)>, Task>>();
Assert.Empty(sut.Processors);
await sut.LogOrganizationEventsForProcessedRequests(callback);
await callback.DidNotReceiveWithAnyArgs()(Arg.Any<IEnumerable<(OrganizationAdminAuthRequest, EventType)>>());
}
[Theory]
[BitAutoData]
public async Task LogOrganizationEventsForProcessedRequests_HasProcessedAuthRequests_IsHandled
(
List<OrganizationAdminAuthRequest> authRequests,
List<OrganizationAuthRequestUpdate> updates,
AuthRequestUpdateProcessorConfiguration configuration,
Action<Exception> errorHandler
)
{
(authRequests[0], updates[0], configuration) = UnrespondAndEnsureValid(authRequests[0], updates[0], configuration);
var sut = new BatchAuthRequestUpdateProcessor(authRequests, updates, configuration);
var callback = Substitute.For<Func<IEnumerable<(OrganizationAdminAuthRequest, EventType)>, Task>>();
await sut.Process(errorHandler).LogOrganizationEventsForProcessedRequests(callback);
await callback.ReceivedWithAnyArgs()(Arg.Any<IEnumerable<(OrganizationAdminAuthRequest, EventType)>>());
}
private (
T authRequest,
OrganizationAuthRequestUpdate update,
AuthRequestUpdateProcessorConfiguration ProcessorConfiguration
) UnrespondAndEnsureValid<T>(
T authRequest,
OrganizationAuthRequestUpdate update,
AuthRequestUpdateProcessorConfiguration processorConfiguration
) where T : AuthRequest
{
authRequest.Id = update.Id;
authRequest.OrganizationId = processorConfiguration.OrganizationId;
authRequest.Key = null;
authRequest.Approved = null;
authRequest.ResponseDate = null;
authRequest.AuthenticationDate = null;
authRequest.CreationDate = DateTime.UtcNow.AddMinutes(-1);
processorConfiguration.AuthRequestExpiresAfter = new TimeSpan(1, 0, 0);
update.Approved = true;
update.Key = "key";
return (authRequest, update, processorConfiguration);
}
}