1
0
mirror of https://github.com/bitwarden/server.git synced 2025-07-03 00:52:49 -05:00

[AC-1191] TDE admin approval email (#3044)

* feat: add new command for updating request and emailing user, refs AC-1191

* feat: inject service with organization service collection extensions, refs AC-1191

* feat: add function to send admin approval email to mail services (interface/noop/handlebars), refs AC-1191

* feat: add html/text mail templates and add view model for email data, refs AC-1191

* feat: update org auth request controller to use new command during auth request update, refs AC-1191

* fix: dotnet format, refs AC-1191

* refactor: update user not found error, FirstOrDefault for enum type display name, refs AC-1191

* refactor: update user not found to log error instead of throws, refs AC-1191

* fix: remove whitespace lint errors, refs AC-1191

* refactor: update hardcoded UTC timezone string, refs AC-1191

* refactor: add unit test for new command, refs AC-1191

* refactor: improve enum name fallback and identifier string creation, refs AC-1191

* refactor: add addtional unit tests, refs AC-1191

* refactor: update success test to use more generated params, refs AC-1191

* fix: dotnet format...again, refs AC-1191

* refactor: make UTC display a constant for handlebars mail service, refs AC-1191

* refactor: update displayTypeIdentifer to displayTypeAndIdentifier for clarity, refs AC-1191
This commit is contained in:
Vincent Salucci
2023-07-06 10:03:49 -05:00
committed by GitHub
parent 62beb7d1e8
commit 3b4c8afea0
11 changed files with 243 additions and 8 deletions

View File

@ -0,0 +1,6 @@
namespace Bit.Core.AdminConsole.OrganizationAuth.Interfaces;
public interface IUpdateOrganizationAuthRequestCommand
{
Task UpdateAsync(Guid requestId, Guid userId, bool requestApproved, string encryptedUserKey);
}

View File

@ -0,0 +1,55 @@
using System.ComponentModel.DataAnnotations;
using System.Reflection;
using Bit.Core.AdminConsole.OrganizationAuth.Interfaces;
using Bit.Core.Auth.Models.Api.Request.AuthRequest;
using Bit.Core.Auth.Services;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Microsoft.Extensions.Logging;
namespace Bit.Core.AdminConsole.OrganizationAuth;
public class UpdateOrganizationAuthRequestCommand : IUpdateOrganizationAuthRequestCommand
{
private readonly IAuthRequestService _authRequestService;
private readonly IMailService _mailService;
private readonly IUserRepository _userRepository;
private readonly ILogger<UpdateOrganizationAuthRequestCommand> _logger;
public UpdateOrganizationAuthRequestCommand(
IAuthRequestService authRequestService,
IMailService mailService,
IUserRepository userRepository,
ILogger<UpdateOrganizationAuthRequestCommand> logger)
{
_authRequestService = authRequestService;
_mailService = mailService;
_userRepository = userRepository;
_logger = logger;
}
public async Task UpdateAsync(Guid requestId, Guid userId, bool requestApproved, string encryptedUserKey)
{
var updatedAuthRequest = await _authRequestService.UpdateAuthRequestAsync(requestId, userId,
new AuthRequestUpdateRequestModel { RequestApproved = requestApproved, Key = encryptedUserKey });
if (updatedAuthRequest.Approved is true)
{
var user = await _userRepository.GetByIdAsync(userId);
if (user == null)
{
_logger.LogError("User ({id}) not found. Trusted device admin approval email not sent.", userId);
return;
}
var approvalDateTime = updatedAuthRequest.ResponseDate ?? DateTime.UtcNow;
var deviceTypeDisplayName = updatedAuthRequest.RequestDeviceType.GetType()
.GetMember(updatedAuthRequest.RequestDeviceType.ToString())
.FirstOrDefault()?
.GetCustomAttribute<DisplayAttribute>()?.Name ?? "Unknown";
var deviceTypeAndIdentifier = $"{deviceTypeDisplayName} - {updatedAuthRequest.RequestDeviceIdentifier}";
await _mailService.SendTrustedDeviceAdminApprovalEmailAsync(user.Email, approvalDateTime,
updatedAuthRequest.RequestIpAddress, deviceTypeAndIdentifier);
}
}
}

View File

@ -0,0 +1,22 @@
{{#>FullHtmlLayout}}
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top">
You must log in on the device below within 12 hours or approval will expire.
</td>
</tr>
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top">
<b style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">Device:</b> {{DeviceType}}<br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;" />
<b style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">IP Address:</b> {{IpAddress}}<br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;" />
<b style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">Date:</b> {{TheDate}} at {{TheTime}} {{TimeZone}}<br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;" />
</td>
</tr>
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<td class="content-block last" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;" valign="top">
If you do not recognize this device, contact your organization administrator.
</td>
</tr>
</table>
{{/FullHtmlLayout}}

View File

@ -0,0 +1,9 @@
{{#>BasicTextLayout}}
You must log in on the device below within 12 hours or approval will expire.
Device Type: {{DeviceType}}
IP Address: {{IpAddress}}
Date: {{TheDate}} at {{TheTime}} {{TimeZone}}
If you do not recognize this device, contact your organization administrator.
{{/BasicTextLayout}}

View File

@ -0,0 +1,3 @@
namespace Bit.Core.Models.Mail;
public class TrustedDeviceAdminApprovalViewModel : NewDeviceLoggedInModel { }

View File

@ -1,4 +1,6 @@
using Bit.Core.Models.Business.Tokenables;
using Bit.Core.AdminConsole.OrganizationAuth;
using Bit.Core.AdminConsole.OrganizationAuth.Interfaces;
using Bit.Core.Models.Business.Tokenables;
using Bit.Core.OrganizationFeatures.Groups;
using Bit.Core.OrganizationFeatures.Groups.Interfaces;
using Bit.Core.OrganizationFeatures.OrganizationApiKeys;
@ -41,6 +43,7 @@ public static class OrganizationServiceCollectionExtensions
services.AddOrganizationGroupCommands();
services.AddOrganizationLicenseCommandsQueries();
services.AddOrganizationDomainCommandsQueries();
services.AddOrganizationAuthCommands();
}
private static void AddOrganizationConnectionCommands(this IServiceCollection services)
@ -110,6 +113,11 @@ public static class OrganizationServiceCollectionExtensions
services.AddScoped<IDeleteOrganizationDomainCommand, DeleteOrganizationDomainCommand>();
}
private static void AddOrganizationAuthCommands(this IServiceCollection services)
{
services.AddScoped<IUpdateOrganizationAuthRequestCommand, UpdateOrganizationAuthRequestCommand>();
}
private static void AddTokenizers(this IServiceCollection services)
{
services.AddSingleton<IDataProtectorTokenFactory<OrganizationSponsorshipOfferTokenable>>(serviceProvider =>
@ -121,3 +129,4 @@ public static class OrganizationServiceCollectionExtensions
);
}
}

View File

@ -55,4 +55,6 @@ public interface IMailService
Task SendFailedLoginAttemptsEmailAsync(string email, DateTime utcNow, string ip);
Task SendFailedTwoFactorAttemptsEmailAsync(string email, DateTime utcNow, string ip);
Task SendUnverifiedOrganizationDomainEmailAsync(IEnumerable<string> adminEmails, string organizationId, string domainName);
Task SendTrustedDeviceAdminApprovalEmailAsync(string email, DateTime utcNow, string ip, string deviceTypeAndIdentifier);
}

View File

@ -17,6 +17,7 @@ namespace Bit.Core.Services;
public class HandlebarsMailService : IMailService
{
private const string Namespace = "Bit.Core.MailTemplates.Handlebars";
private const string _utcTimeZoneDisplay = "UTC";
private readonly GlobalSettings _globalSettings;
private readonly IMailDeliveryService _mailDeliveryService;
@ -353,7 +354,7 @@ public class HandlebarsMailService : IMailService
DeviceType = deviceType,
TheDate = timestamp.ToLongDateString(),
TheTime = timestamp.ToShortTimeString(),
TimeZone = "UTC",
TimeZone = _utcTimeZoneDisplay,
IpAddress = ip
};
await AddMessageContentAsync(message, "NewDeviceLoggedIn", model);
@ -370,7 +371,7 @@ public class HandlebarsMailService : IMailService
SiteName = _globalSettings.SiteName,
TheDate = timestamp.ToLongDateString(),
TheTime = timestamp.ToShortTimeString(),
TimeZone = "UTC",
TimeZone = _utcTimeZoneDisplay,
IpAddress = ip
};
await AddMessageContentAsync(message, "Auth.RecoverTwoFactor", model);
@ -856,7 +857,7 @@ public class HandlebarsMailService : IMailService
{
TheDate = utcNow.ToLongDateString(),
TheTime = utcNow.ToShortTimeString(),
TimeZone = "UTC",
TimeZone = _utcTimeZoneDisplay,
IpAddress = ip,
AffectedEmail = email
@ -873,7 +874,7 @@ public class HandlebarsMailService : IMailService
{
TheDate = utcNow.ToLongDateString(),
TheTime = utcNow.ToShortTimeString(),
TimeZone = "UTC",
TimeZone = _utcTimeZoneDisplay,
IpAddress = ip,
AffectedEmail = email
@ -896,8 +897,26 @@ public class HandlebarsMailService : IMailService
await _mailDeliveryService.SendEmailAsync(message);
}
public async Task SendTrustedDeviceAdminApprovalEmailAsync(string email, DateTime utcNow, string ip,
string deviceTypeAndIdentifier)
{
var message = CreateDefaultMessage("Login request approved", email);
var model = new TrustedDeviceAdminApprovalViewModel
{
TheDate = utcNow.ToLongDateString(),
TheTime = utcNow.ToShortTimeString(),
TimeZone = _utcTimeZoneDisplay,
IpAddress = ip,
DeviceType = deviceTypeAndIdentifier,
};
await AddMessageContentAsync(message, "Auth.TrustedDeviceAdminApproval", model);
message.Category = "TrustedDeviceAdminApproval";
await _mailDeliveryService.SendEmailAsync(message);
}
private static string GetUserIdentifier(string email, string userName)
{
return string.IsNullOrEmpty(userName) ? email : CoreHelpers.SanitizeForEmail(userName, false);
}
}

View File

@ -238,4 +238,10 @@ public class NoopMailService : IMailService
{
return Task.FromResult(0);
}
public Task SendTrustedDeviceAdminApprovalEmailAsync(string email, DateTime utcNow, string ip, string deviceTypeAndIdentifier)
{
return Task.FromResult(0);
}
}