Skip to content

Commit

Permalink
Rework everything to be more model-centric
Browse files Browse the repository at this point in the history
  • Loading branch information
addisonbeck committed May 20, 2024
1 parent 0676885 commit ac451cb
Show file tree
Hide file tree
Showing 11 changed files with 755 additions and 803 deletions.
10 changes: 10 additions & 0 deletions src/Core/AdminConsole/Extensions/TaskExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace Bit.Core.AdminConsole.Extensions;

public static class TaskExtensions
{
public async static Task<TResult> Then<T, TResult>(this Task<T> source, Func<T, Task<TResult>> selector)
{
T x = await source;
return await selector(x);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace Bit.Core.AdminConsole.OrganizationAuth.Models;

public class ApprovedAuthRequestIsMissingKeyException : AuthRequestUpdateProcessingException
{
public ApprovedAuthRequestIsMissingKeyException(Guid id)
: base($"An auth request with id {id} was approved, but no key was provided. This auth request can not be approved.")
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace Bit.Core.AdminConsole.OrganizationAuth.Models;

public class AuthRequestUpdateCouldNotBeProcessedException : AuthRequestUpdateProcessingException
{
public AuthRequestUpdateCouldNotBeProcessedException(Guid id)
: base($"An auth request with id {id} could not be processed.")
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace Bit.Core.AdminConsole.OrganizationAuth.Models;

public class AuthRequestUpdateProcessingException : Exception
{
public AuthRequestUpdateProcessingException() { }

public AuthRequestUpdateProcessingException(string message)
: base(message) { }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
using System.ComponentModel.DataAnnotations;
using System.Reflection;
using Bit.Core.Auth.Entities;
using Bit.Core.Enums;

namespace Bit.Core.AdminConsole.OrganizationAuth.Models;

public class AuthRequestUpdateProcessor<T> where T : AuthRequest
{
public T ProcessedAuthRequest { get; private set; }

private T _unprocessedAuthRequest { get; }
private OrganizationAuthRequestUpdate _updates { get; }
private AuthRequestUpdateProcessorConfiguration _configuration { get; }

public AuthRequestUpdateProcessor(
T authRequest,
OrganizationAuthRequestUpdate updates,
AuthRequestUpdateProcessorConfiguration configuration
)
{
_unprocessedAuthRequest = authRequest;
_updates = updates;
_configuration = configuration;
}

public AuthRequestUpdateProcessor<T> Process()
{
var isExpired = DateTime.UtcNow >
_unprocessedAuthRequest.CreationDate
.Add(_configuration.AuthRequestExpiresAfter);
var isSpent = _unprocessedAuthRequest == null ||
_unprocessedAuthRequest.Approved != null ||
_unprocessedAuthRequest.ResponseDate.HasValue ||
_unprocessedAuthRequest.AuthenticationDate.HasValue;
var canBeProcessed = !isExpired &&
!isSpent &&
_unprocessedAuthRequest.Id == _updates.Id &&
_unprocessedAuthRequest.OrganizationId == _configuration.OrganizationId;
if (!canBeProcessed)
{
throw new AuthRequestUpdateCouldNotBeProcessedException(_unprocessedAuthRequest.Id);
}
return _updates.Approved ?
Approve() :
Deny();
}

public async Task<AuthRequestUpdateProcessor<T>> SendPushNotification(Func<T, Task> callback)
{
if (!ProcessedAuthRequest?.Approved ?? false || callback == null)
{
return this;
}
await callback(ProcessedAuthRequest);
return this;
}

public async Task<AuthRequestUpdateProcessor<T>> SendNewDeviceEmail(Func<T, string, Task> callback)
{
if (!ProcessedAuthRequest?.Approved ?? false || callback == null)
{
return this;
}
var deviceTypeDisplayName = _unprocessedAuthRequest.RequestDeviceType.GetType()
.GetMember(_unprocessedAuthRequest.RequestDeviceType.ToString())
.FirstOrDefault()?
// This unknown case can't be unit tested without adding an enum
// with no display attribute. Faith and trust are required!
.GetCustomAttribute<DisplayAttribute>()?.Name ?? "Unknown Device Type";
var deviceTypeAndIdentifierDisplayString =
string.IsNullOrWhiteSpace(_unprocessedAuthRequest.RequestDeviceIdentifier) ?
deviceTypeDisplayName :
$"{deviceTypeDisplayName} - {_unprocessedAuthRequest.RequestDeviceIdentifier}";
await callback(ProcessedAuthRequest, deviceTypeAndIdentifierDisplayString);
return this;
}

public async Task<AuthRequestUpdateProcessor<T>> SendEventLog(Func<T, EventType, Task> callback)
{
if (!ProcessedAuthRequest?.Approved == null || callback == null)
{
return this;
}
var eventType = _updates.Approved ?
EventType.OrganizationUser_ApprovedAuthRequest :
EventType.OrganizationUser_RejectedAuthRequest;
await callback(ProcessedAuthRequest, eventType);
return this;
}

private AuthRequestUpdateProcessor<T> Approve()
{
if (string.IsNullOrWhiteSpace(_updates.Key))
{
throw new ApprovedAuthRequestIsMissingKeyException(_updates.Id);
}
ProcessedAuthRequest = _unprocessedAuthRequest;
ProcessedAuthRequest.Key = _updates.Key;
ProcessedAuthRequest.Approved = true;
ProcessedAuthRequest.ResponseDate = DateTime.UtcNow;
return this;
}

private AuthRequestUpdateProcessor<T> Deny()
{
ProcessedAuthRequest = _unprocessedAuthRequest;
ProcessedAuthRequest.Approved = false;
ProcessedAuthRequest.ResponseDate = DateTime.UtcNow;
return this;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Bit.Core.AdminConsole.OrganizationAuth.Models;

public class AuthRequestUpdateProcessorConfiguration
{
public Guid OrganizationId { get; set; }
public TimeSpan AuthRequestExpiresAfter { get; set; }
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
using Bit.Core.Auth.Entities;
using Bit.Core.Enums;

namespace Bit.Core.AdminConsole.OrganizationAuth.Models;

public class BatchAuthRequestUpdateProcessor<T> where T : AuthRequest
{
public List<AuthRequestUpdateProcessor<T>> Processors { get; } = new List<AuthRequestUpdateProcessor<T>>();
private List<AuthRequestUpdateProcessor<T>> _processed => Processors
.Where(p => p.ProcessedAuthRequest != null)
.ToList();

public BatchAuthRequestUpdateProcessor(
ICollection<T> authRequests,
IEnumerable<OrganizationAuthRequestUpdate> updates,
AuthRequestUpdateProcessorConfiguration configuration
)
{
Processors = authRequests?.Select(ar =>
{
return new AuthRequestUpdateProcessor<T>(
ar,
updates.FirstOrDefault(u => u.Id == ar.Id),
configuration
);
}).ToList() ?? Processors;
}

public BatchAuthRequestUpdateProcessor<T> Process(Action<Exception> errorHandlerCallback)
{
foreach (var processor in Processors)
{
try
{
processor.Process();
}
catch (AuthRequestUpdateProcessingException e)
{
errorHandlerCallback(e);
}
}
return this;
}

public async Task<BatchAuthRequestUpdateProcessor<T>> Save(Func<IEnumerable<T>, Task> callback)
{
if (_processed.Any())
{
await callback(_processed.Select(p => p.ProcessedAuthRequest));
}
return this;
}

// Currently events like notifications, emails, and event logs are still
// done per-request in a loop, which is different than saving updates to
// the database. Saving can be done in bulk all the way through to the
// repository.
//
// Perhaps these operations should be extended to be more batch-friendly
// as well.
public async Task<BatchAuthRequestUpdateProcessor<T>> SendPushNotifications(Func<T, Task> callback)
{
foreach (var processor in _processed)
{
await processor.SendPushNotification(callback);
}
return this;
}

public async Task<BatchAuthRequestUpdateProcessor<T>> SendNewDeviceEmails(Func<T, string, Task> callback)
{
foreach (var processor in _processed)
{
await processor.SendNewDeviceEmail(callback);
}
return this;
}

public async Task<BatchAuthRequestUpdateProcessor<T>> SendEventLogs(Func<T, EventType, Task> callback)
{
foreach (var processor in _processed)
{
await processor.SendEventLog(callback);
}
return this;
}
}

0 comments on commit ac451cb

Please sign in to comment.