mirror of
https://github.com/bitwarden/server.git
synced 2025-06-30 07:36:14 -05:00
[PM-15637] Add Email Notification Templates and Logic for Device Approval Requests (#5270)
* Add device approval notification email templates * Add DeviceApprovalRequestedViewModel for device approval notifications * Add method to send device approval requested notification email * Send email notification to Organization Admins when adding a new admin approval auth request * Add tests for device approval notification email sending in AuthRequestServiceTests * fix(email-templates): Remove unnecessary triple braces from user name variable in device approval notification emails * Add feature flag for admin notifications on device approval requests * Add logging for skipped admin notifications on device approval requests
This commit is contained in:
@ -0,0 +1,14 @@
|
||||
using Bit.Core.Models.Mail;
|
||||
|
||||
namespace Bit.Core.AdminConsole.Models.Mail;
|
||||
|
||||
public class DeviceApprovalRequestedViewModel : BaseMailModel
|
||||
{
|
||||
public Guid OrganizationId { get; set; }
|
||||
public string UserNameRequestingAccess { get; set; }
|
||||
|
||||
public string Url => string.Format("{0}/organizations/{1}/settings/device-approvals",
|
||||
WebVaultUrl,
|
||||
OrganizationId);
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
#nullable enable
|
||||
|
||||
@ -27,6 +28,9 @@ public class AuthRequestService : IAuthRequestService
|
||||
private readonly IPushNotificationService _pushNotificationService;
|
||||
private readonly IEventService _eventService;
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
private readonly IMailService _mailService;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly ILogger<AuthRequestService> _logger;
|
||||
|
||||
public AuthRequestService(
|
||||
IAuthRequestRepository authRequestRepository,
|
||||
@ -36,7 +40,10 @@ public class AuthRequestService : IAuthRequestService
|
||||
ICurrentContext currentContext,
|
||||
IPushNotificationService pushNotificationService,
|
||||
IEventService eventService,
|
||||
IOrganizationUserRepository organizationRepository)
|
||||
IOrganizationUserRepository organizationRepository,
|
||||
IMailService mailService,
|
||||
IFeatureService featureService,
|
||||
ILogger<AuthRequestService> logger)
|
||||
{
|
||||
_authRequestRepository = authRequestRepository;
|
||||
_userRepository = userRepository;
|
||||
@ -46,6 +53,9 @@ public class AuthRequestService : IAuthRequestService
|
||||
_pushNotificationService = pushNotificationService;
|
||||
_eventService = eventService;
|
||||
_organizationUserRepository = organizationRepository;
|
||||
_mailService = mailService;
|
||||
_featureService = featureService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<AuthRequest?> GetAuthRequestAsync(Guid id, Guid userId)
|
||||
@ -132,6 +142,8 @@ public class AuthRequestService : IAuthRequestService
|
||||
{
|
||||
var createdAuthRequest = await CreateAuthRequestAsync(model, user, organizationUser.OrganizationId);
|
||||
firstAuthRequest ??= createdAuthRequest;
|
||||
|
||||
await NotifyAdminsOfDeviceApprovalRequestAsync(organizationUser, user);
|
||||
}
|
||||
|
||||
// I know this won't be null because I have already validated that at least one organization exists
|
||||
@ -276,4 +288,19 @@ public class AuthRequestService : IAuthRequestService
|
||||
{
|
||||
return DateTime.UtcNow > savedDate.Add(allowedLifetime);
|
||||
}
|
||||
|
||||
private async Task NotifyAdminsOfDeviceApprovalRequestAsync(OrganizationUser organizationUser, User user)
|
||||
{
|
||||
if (!_featureService.IsEnabled(FeatureFlagKeys.DeviceApprovalRequestAdminNotifications))
|
||||
{
|
||||
_logger.LogWarning("Skipped sending device approval notification to admins - feature flag disabled");
|
||||
return;
|
||||
}
|
||||
|
||||
var admins = await _organizationUserRepository.GetManyByMinimumRoleAsync(
|
||||
organizationUser.OrganizationId,
|
||||
OrganizationUserType.Admin);
|
||||
var adminEmails = admins.Select(a => a.Email).Distinct().ToList();
|
||||
await _mailService.SendDeviceApprovalRequestedNotificationEmailAsync(adminEmails, organizationUser.OrganizationId, user.Email, user.Name);
|
||||
}
|
||||
}
|
||||
|
@ -106,6 +106,7 @@ public static class FeatureFlagKeys
|
||||
public const string AccountDeprovisioning = "pm-10308-account-deprovisioning";
|
||||
public const string VerifiedSsoDomainEndpoint = "pm-12337-refactor-sso-details-endpoint";
|
||||
public const string IntegrationPage = "pm-14505-admin-console-integration-page";
|
||||
public const string DeviceApprovalRequestAdminNotifications = "pm-15637-device-approval-request-admin-notifications";
|
||||
|
||||
public const string ReturnErrorOnExistingKeypair = "return-error-on-existing-keypair";
|
||||
public const string UseTreeWalkerApiForPageDetailsCollection = "use-tree-walker-api-for-page-details-collection";
|
||||
|
@ -0,0 +1,23 @@
|
||||
{{#>FullHtmlLayout}}
|
||||
<table width="100%" cellpadding="0" cellspacing="0"
|
||||
style="margin: 0; box-sizing: border-box; ">
|
||||
<tr
|
||||
style="margin: 0; box-sizing: border-box; ">
|
||||
<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 0 10px; -webkit-text-size-adjust: none;"
|
||||
valign="top">
|
||||
{{UserNameRequestingAccess}} has sent a device approval request. Review login requests to allow the member to finish logging in.
|
||||
<br class="line-break" />
|
||||
<br class="line-break" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="margin: 0; box-sizing: border-box; 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; text-align: center;" valign="top" align="center">
|
||||
<a href="{{Url}}" clicktracking=off target="_blank" rel="noopener noreferrer" style="color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background-color: #175DDC; border-color: #175DDC; border-style: solid; border-width: 10px 20px; margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
Review request
|
||||
</a>
|
||||
<br class="line-break" />
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{{/FullHtmlLayout}}
|
@ -0,0 +1,5 @@
|
||||
{{#>BasicTextLayout}}
|
||||
{{UserNameRequestingAccess}} has sent a device approval request. Review login requests to allow the member to finish logging in.
|
||||
|
||||
{{Url}}
|
||||
{{/BasicTextLayout}}
|
@ -0,0 +1,29 @@
|
||||
{{#>FullHtmlLayout}}
|
||||
<table width="100%" cellpadding="0" cellspacing="0"
|
||||
style="margin: 0; box-sizing: border-box; ">
|
||||
<tr style="margin: 0; box-sizing: border-box; ">
|
||||
<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 0 10px; -webkit-text-size-adjust: none;"
|
||||
valign="top">
|
||||
{{UserNameRequestingAccess}} has sent a device approval request. Review login requests to allow the member to finish logging in.
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="margin: 0; box-sizing: border-box; ">
|
||||
<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 0 10px; -webkit-text-size-adjust: none;"
|
||||
valign="top">
|
||||
To review requests, log in to your self-hosted instance → navigate to the Admin Console → select Device Approvals
|
||||
<br class="line-break" />
|
||||
<br class="line-break" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="margin: 0; box-sizing: border-box; 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; text-align: center;" valign="top" align="center">
|
||||
<a href="{{Url}}" clicktracking=off target="_blank" rel="noopener noreferrer" style="color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background-color: #175DDC; border-color: #175DDC; border-style: solid; border-width: 10px 20px; margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
Review request
|
||||
</a>
|
||||
<br class="line-break" />
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{{/FullHtmlLayout}}
|
@ -0,0 +1,7 @@
|
||||
{{#>BasicTextLayout}}
|
||||
{{UserNameRequestingAccess}} has sent a device approval request. Review login requests to allow the member to finish logging in.
|
||||
|
||||
To review requests, log in to your self-hosted instance -> navigate to the Admin Console -> select Device Approvals.
|
||||
|
||||
{{Url}}
|
||||
{{/BasicTextLayout}}
|
@ -96,5 +96,6 @@ public interface IMailService
|
||||
Task SendFamiliesForEnterpriseRemoveSponsorshipsEmailAsync(string email, string offerAcceptanceDate, string organizationId,
|
||||
string organizationName);
|
||||
Task SendClaimedDomainUserEmailAsync(ManagedUserDomainClaimedEmails emailList);
|
||||
Task SendDeviceApprovalRequestedNotificationEmailAsync(IEnumerable<string> adminEmails, Guid organizationId, string email, string userName);
|
||||
}
|
||||
|
||||
|
@ -2,6 +2,7 @@
|
||||
using System.Reflection;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Models.Mail;
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Auth.Models.Mail;
|
||||
using Bit.Core.Billing.Enums;
|
||||
@ -1168,6 +1169,23 @@ public class HandlebarsMailService : IMailService
|
||||
await _mailDeliveryService.SendEmailAsync(message);
|
||||
}
|
||||
|
||||
public async Task SendDeviceApprovalRequestedNotificationEmailAsync(IEnumerable<string> adminEmails, Guid organizationId, string email, string userName)
|
||||
{
|
||||
var templateName = _globalSettings.SelfHosted ?
|
||||
"AdminConsole.SelfHostNotifyAdminDeviceApprovalRequested" :
|
||||
"AdminConsole.NotifyAdminDeviceApprovalRequested";
|
||||
var message = CreateDefaultMessage("Review SSO login request for new device", adminEmails);
|
||||
var model = new DeviceApprovalRequestedViewModel
|
||||
{
|
||||
WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,
|
||||
UserNameRequestingAccess = GetUserIdentifier(email, userName),
|
||||
OrganizationId = organizationId,
|
||||
};
|
||||
await AddMessageContentAsync(message, templateName, model);
|
||||
message.Category = "DeviceApprovalRequested";
|
||||
await _mailDeliveryService.SendEmailAsync(message);
|
||||
}
|
||||
|
||||
private static string GetUserIdentifier(string email, string userName)
|
||||
{
|
||||
return string.IsNullOrEmpty(userName) ? email : CoreHelpers.SanitizeForEmail(userName, false);
|
||||
|
@ -316,5 +316,10 @@ public class NoopMailService : IMailService
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
public Task SendClaimedDomainUserEmailAsync(ManagedUserDomainClaimedEmails emailList) => Task.CompletedTask;
|
||||
|
||||
public Task SendDeviceApprovalRequestedNotificationEmailAsync(IEnumerable<string> adminEmails, Guid organizationId, string email, string userName)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user