1
0
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:
Rui Tomé
2025-01-27 10:59:46 +00:00
committed by GitHub
parent 3908edd08f
commit 9e718d7336
11 changed files with 249 additions and 1 deletions

View File

@ -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);
}

View File

@ -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);
}
}

View File

@ -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";

View File

@ -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}}

View File

@ -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}}

View File

@ -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}}

View File

@ -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}}

View File

@ -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);
}

View File

@ -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);

View File

@ -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);
}
}