mirror of
https://github.com/bitwarden/server
synced 2025-12-19 09:43:25 +00: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:
@@ -1,10 +1,15 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Reflection;
|
||||
using Bit.Core.AdminConsole.OrganizationAuth.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationAuth.Models;
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Auth.Models.Api.Request.AuthRequest;
|
||||
using Bit.Core.Auth.Models.Data;
|
||||
using Bit.Core.Auth.Services;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationAuth;
|
||||
@@ -15,19 +20,38 @@ public class UpdateOrganizationAuthRequestCommand : IUpdateOrganizationAuthReque
|
||||
private readonly IMailService _mailService;
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly ILogger<UpdateOrganizationAuthRequestCommand> _logger;
|
||||
private readonly IAuthRequestRepository _authRequestRepository;
|
||||
private readonly IGlobalSettings _globalSettings;
|
||||
private readonly IPushNotificationService _pushNotificationService;
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
private readonly IEventService _eventService;
|
||||
|
||||
public UpdateOrganizationAuthRequestCommand(
|
||||
IAuthRequestService authRequestService,
|
||||
IMailService mailService,
|
||||
IUserRepository userRepository,
|
||||
ILogger<UpdateOrganizationAuthRequestCommand> logger)
|
||||
ILogger<UpdateOrganizationAuthRequestCommand> logger,
|
||||
IAuthRequestRepository authRequestRepository,
|
||||
IGlobalSettings globalSettings,
|
||||
IPushNotificationService pushNotificationService,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IEventService eventService)
|
||||
{
|
||||
_authRequestService = authRequestService;
|
||||
_mailService = mailService;
|
||||
_userRepository = userRepository;
|
||||
_logger = logger;
|
||||
_authRequestRepository = authRequestRepository;
|
||||
_globalSettings = globalSettings;
|
||||
_pushNotificationService = pushNotificationService;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
_eventService = eventService;
|
||||
}
|
||||
|
||||
// TODO: When refactoring this method as a part of Bulk Device Approval
|
||||
// post-release cleanup we should be able to construct a single
|
||||
// AuthRequestProcessor and run its Process() Save() methods, and the
|
||||
// various calls to send notifications.
|
||||
public async Task UpdateAsync(Guid requestId, Guid userId, bool requestApproved, string encryptedUserKey)
|
||||
{
|
||||
var updatedAuthRequest = await _authRequestService.UpdateAuthRequestAsync(requestId, userId,
|
||||
@@ -51,5 +75,65 @@ public class UpdateOrganizationAuthRequestCommand : IUpdateOrganizationAuthReque
|
||||
updatedAuthRequest.RequestIpAddress, deviceTypeAndIdentifier);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task UpdateAsync(Guid organizationId, IEnumerable<OrganizationAuthRequestUpdate> authRequestUpdates)
|
||||
{
|
||||
var authRequestEntities = await FetchManyOrganizationAuthRequestsFromTheDatabase(organizationId, authRequestUpdates.Select(aru => aru.Id));
|
||||
var processor = new BatchAuthRequestUpdateProcessor(
|
||||
authRequestEntities,
|
||||
authRequestUpdates,
|
||||
new AuthRequestUpdateProcessorConfiguration()
|
||||
{
|
||||
OrganizationId = organizationId,
|
||||
AuthRequestExpiresAfter = _globalSettings.PasswordlessAuth.AdminRequestExpiration
|
||||
}
|
||||
);
|
||||
processor.Process((Exception e) => _logger.LogError(e.Message));
|
||||
await processor.Save((IEnumerable<OrganizationAdminAuthRequest> authRequests) => _authRequestRepository.UpdateManyAsync(authRequests));
|
||||
await processor.SendPushNotifications((ar) => _pushNotificationService.PushAuthRequestResponseAsync(ar));
|
||||
await processor.SendApprovalEmailsForProcessedRequests(SendApprovalEmail);
|
||||
await processor.LogOrganizationEventsForProcessedRequests(LogOrganizationEvents);
|
||||
}
|
||||
|
||||
async Task<ICollection<OrganizationAdminAuthRequest>> FetchManyOrganizationAuthRequestsFromTheDatabase(Guid organizationId, IEnumerable<Guid> authRequestIds)
|
||||
{
|
||||
return authRequestIds != null && authRequestIds.Any()
|
||||
? await _authRequestRepository
|
||||
.GetManyAdminApprovalRequestsByManyIdsAsync(
|
||||
organizationId,
|
||||
authRequestIds
|
||||
)
|
||||
: new List<OrganizationAdminAuthRequest>();
|
||||
}
|
||||
|
||||
async Task SendApprovalEmail<T>(T authRequest, string identifier) where T : AuthRequest
|
||||
{
|
||||
var user = await _userRepository.GetByIdAsync(authRequest.UserId);
|
||||
|
||||
// This should be impossible
|
||||
if (user == null)
|
||||
{
|
||||
_logger.LogError($"User {authRequest.UserId} not found. Trusted device admin approval email not sent.");
|
||||
return;
|
||||
}
|
||||
|
||||
await _mailService.SendTrustedDeviceAdminApprovalEmailAsync(
|
||||
user.Email,
|
||||
authRequest.ResponseDate ?? DateTime.UtcNow,
|
||||
authRequest.RequestIpAddress,
|
||||
identifier
|
||||
);
|
||||
}
|
||||
|
||||
async Task LogOrganizationEvents(IEnumerable<(OrganizationAdminAuthRequest AuthRequest, EventType EventType)> events)
|
||||
{
|
||||
var organizationUsers = await _organizationUserRepository.GetManyAsync(events.Select(e => e.AuthRequest.OrganizationUserId));
|
||||
await _eventService.LogOrganizationUserEventsAsync(
|
||||
organizationUsers.Select(ou =>
|
||||
{
|
||||
var e = events.FirstOrDefault(e => e.AuthRequest.OrganizationUserId == ou.Id);
|
||||
return (ou, e.EventType, e.AuthRequest.ResponseDate);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user