1
0
mirror of https://github.com/bitwarden/server synced 2026-02-21 20:03:40 +00:00

[PM-27705] - Notify Admins/Owners/Managers Auto Confirm Enabled (#6938)

* Adding email for sending to owners, admins, and managers to notify that auto confirm feature has been enabled from admin portal
This commit is contained in:
Jared McCannon
2026-02-20 08:26:31 -06:00
committed by GitHub
parent c7785cd491
commit 708ea66393
8 changed files with 804 additions and 5 deletions

View File

@@ -9,6 +9,7 @@ using Bit.Admin.Utilities;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUser;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Enforcement.AutoConfirm;
using Bit.Core.AdminConsole.Providers.Interfaces;
@@ -63,6 +64,7 @@ public class OrganizationsController : Controller
private readonly IOrganizationBillingService _organizationBillingService;
private readonly IEventService _eventService;
private readonly IAutomaticUserConfirmationOrganizationPolicyComplianceValidator _automaticUserConfirmationOrganizationPolicyComplianceValidator;
private readonly IOrganizationAutoConfirmEnabledNotificationCommand _organizationAutoConfirmEnabledNotificationCommand;
public OrganizationsController(
IOrganizationRepository organizationRepository,
@@ -90,7 +92,8 @@ public class OrganizationsController : Controller
IResendOrganizationInviteCommand resendOrganizationInviteCommand,
IOrganizationBillingService organizationBillingService,
IEventService eventService,
IAutomaticUserConfirmationOrganizationPolicyComplianceValidator automaticUserConfirmationOrganizationPolicyComplianceValidator)
IAutomaticUserConfirmationOrganizationPolicyComplianceValidator automaticUserConfirmationOrganizationPolicyComplianceValidator,
IOrganizationAutoConfirmEnabledNotificationCommand organizationAutoConfirmEnabledNotificationCommand)
{
_organizationRepository = organizationRepository;
_organizationUserRepository = organizationUserRepository;
@@ -118,6 +121,7 @@ public class OrganizationsController : Controller
_organizationBillingService = organizationBillingService;
_eventService = eventService;
_automaticUserConfirmationOrganizationPolicyComplianceValidator = automaticUserConfirmationOrganizationPolicyComplianceValidator;
_organizationAutoConfirmEnabledNotificationCommand = organizationAutoConfirmEnabledNotificationCommand;
}
[RequirePermission(Permission.Org_List_View)]
@@ -286,8 +290,6 @@ public class OrganizationsController : Controller
}
}
var previousUseAutomaticUserConfirmation = organization.UseAutomaticUserConfirmation;
UpdateOrganization(organization, model);
var plan = await _pricingClient.GetPlanOrThrow(organization.PlanType);
if (organization.UseSecretsManager && !plan.SupportsSecretsManager)
@@ -309,15 +311,40 @@ public class OrganizationsController : Controller
await _organizationRepository.ReplaceAsync(organization);
if (previousUseAutomaticUserConfirmation != organization.UseAutomaticUserConfirmation)
await _applicationCacheService.UpsertOrganizationAbilityAsync(organization);
if (existingOrganizationData.UseAutomaticUserConfirmation != organization.UseAutomaticUserConfirmation)
{
var eventType = organization.UseAutomaticUserConfirmation
? EventType.Organization_AutoConfirmEnabled_Portal
: EventType.Organization_AutoConfirmDisabled_Portal;
await _eventService.LogOrganizationEventAsync(organization, eventType, EventSystemUser.BitwardenPortal);
}
await _applicationCacheService.UpsertOrganizationAbilityAsync(organization);
if (!existingOrganizationData.UseAutomaticUserConfirmation && organization.UseAutomaticUserConfirmation)
{
try
{
var emailsToNotify =
(await _organizationUserRepository.GetManyDetailsByOrganizationAsync_vNext(organization.Id))
.Where(x =>
(x.Type == OrganizationUserType.Admin
|| x.Type == OrganizationUserType.Owner
|| x.GetPermissions()?.ManageUsers == true)
&& !string.IsNullOrWhiteSpace(x.Email))
.Select(x => x.Email)
.ToList();
await _organizationAutoConfirmEnabledNotificationCommand.SendEmailAsync(
new OrganizationAutoConfirmEnabledNotificationRequest(organization, emailsToNotify));
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to send email notification to admins when organization auto-confirm was enabled.");
TempData["Warning"] = "Organization updated successfully, but email notification to admins failed.";
}
}
// Sync name/email changes to Stripe
if (existingOrganizationData.Name != organization.Name || existingOrganizationData.BillingEmail != organization.BillingEmail)

View File

@@ -0,0 +1,13 @@
using Bit.Core.Platform.Mail.Mailer;
namespace Bit.Core.AdminConsole.Models.Mail.Mailer.OrganizationUserAutoConfirmation;
public class OrganizationAutoConfirmationEnabledView : BaseMailView
{
public required string WebVaultUrl { get; set; }
}
public class OrganizationAutoConfirmationEnabled : BaseMail<OrganizationAutoConfirmationEnabledView>
{
public override required string Subject { get; set; }
}

View File

@@ -0,0 +1,513 @@
<!doctype html>
<html lang="und" dir="auto" xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
<head>
<title></title>
<!--[if !mso]><!-->
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<!--<![endif]-->
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style type="text/css">
#outlook a { padding:0; }
body { margin:0;padding:0;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%; }
table, td { border-collapse:collapse;mso-table-lspace:0pt;mso-table-rspace:0pt; }
img { border:0;height:auto;line-height:100%; outline:none;text-decoration:none;-ms-interpolation-mode:bicubic; }
p { display:block;margin:13px 0; }
</style>
<!--[if mso]>
<noscript>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG/>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
</noscript>
<![endif]-->
<!--[if lte mso 11]>
<style type="text/css">
.mj-outlook-group-fix { width:100% !important; }
</style>
<![endif]-->
<!--[if !mso]><!-->
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700" rel="stylesheet" type="text/css">
<style type="text/css">
@import url(https://fonts.googleapis.com/css?family=Roboto:300,400,500,700);
</style>
<!--<![endif]-->
<style type="text/css">
@media only screen and (min-width:480px) {
.mj-column-per-100 { width:100% !important; max-width: 100%; }
}
</style>
<style media="screen and (min-width:480px)">
.moz-text-html .mj-column-per-100 { width:100% !important; max-width: 100%; }
</style>
<style type="text/css">
@media only screen and (max-width:479px) {
table.mj-full-width-mobile { width: 100% !important; }
td.mj-full-width-mobile { width: auto !important; }
}
</style>
<style type="text/css">
.border-fix > table {
border-collapse: separate !important;
}
.border-fix > table > tbody > tr > td {
border-radius: 3px;
}
</style>
</head>
<body style="word-spacing:normal;background-color:#e6e9ef;">
<div style="background-color:#e6e9ef;" lang="und" dir="auto">
<!-- Blue Header Section -->
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="border-fix-outlook" role="presentation" style="width:660px;" width="660" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div class="border-fix" style="margin:0px auto;max-width:660px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:20px 24px 0px 24px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" width="660px" ><![endif]-->
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#175ddc;background-color:#175ddc;width:100%;border-radius:4px 4px 0px 0px;">
<tbody>
<tr>
<td>
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:612px;" width="612" bgcolor="#175ddc" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="margin:0px auto;border-radius:4px 4px 0px 0px;max-width:612px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;border-radius:4px 4px 0px 0px;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:20px 20px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:572px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tbody>
<tr>
<td align="left" style="font-size:0px;padding:10px 5px;word-break:break-word;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;">
<tbody>
<tr>
<td style="width:150px;">
<img alt src="https://bitwarden.com/images/logo-horizontal-white.png" style="border:0;display:block;outline:none;text-decoration:none;height:30px;width:100%;font-size:16px;" width="150" height="30">
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
<!-- Main Content -->
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:660px;" width="660" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="margin:0px auto;max-width:660px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:0px 25px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" width="660px" ><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:610px;" width="610" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:610px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#ffffff;background-color:#ffffff;width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:24px 0px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:610px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" width="100%">
<tbody>
<tr>
<td style="vertical-align:top;padding:0px 25px;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style width="100%">
<tbody>
<tr>
<td align="left" style="font-size:0px;padding:0px 0px 24px 0px;word-break:break-word;">
<div style="font-family:roboto;font-size:16px;font-weight:700;line-height:24px;text-align:left;color:#1B2029;">Automatic user confirmation is now available!</div>
</td>
</tr>
<tr>
<td align="left" style="font-size:0px;padding:0px 0px 24px 0px;word-break:break-word;">
<div style="font-family:roboto;font-size:16px;font-weight:400;line-height:24px;text-align:left;color:#1B2029;">A new policy is available for your organization. It allows new users
to be automatically confirmed while an admins device is unlocked.
Log in to the web app to turn on the policy.</div>
</td>
</tr>
<tr>
<td align="left" style="font-size:0px;padding:0px 0px 24px 0px;word-break:break-word;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;">
<tbody>
<tr>
<td align="center" bgcolor="#175ddc" role="presentation" style="border:none;border-radius:20px;cursor:auto;mso-padding-alt:12px 24px;background:#175ddc;" valign="middle">
<a href="{{WebVaultUrl}}" style="display:inline-block;background:#175ddc;color:#ffffff;font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;font-weight:600;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:12px 24px;mso-padding-alt:0px;border-radius:20px;" target="_blank">
Log in
</a>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
<tr>
<td align="left" style="font-size:0px;padding:0px;word-break:break-word;">
<div style="font-family:roboto;font-size:13px;font-weight:700;line-height:16px;text-align:left;color:#1B2029;"><a class="link" href="https://bitwarden.com/help/automatic-confirmation/" style="text-decoration: none; color: #175ddc; font-weight: 600;">Learn more about this policy</a></div>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
<!-- Footer -->
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:660px;" width="660" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="margin:0px auto;max-width:660px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:20px 0;padding-top:10px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" width="660px" ><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:660px;" width="660" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="margin:0px auto;max-width:660px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:5px 20px 10px 20px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:620px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tbody>
<tr>
<td align="center" style="font-size:0px;padding:0;word-break:break-word;">
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" ><tr><td><![endif]-->
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
<tbody>
<tr>
<td style="padding:8px;vertical-align:middle;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
<tbody>
<tr>
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
<a href="https://x.com/bitwarden" target="_blank">
<img alt height="24" src="https://assets.bitwarden.com/email/v1/social-icons-x-twitter.png" style="border-radius:3px;display:block;" width="24">
</a>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<!--[if mso | IE]></td><td><![endif]-->
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
<tbody>
<tr>
<td style="padding:8px;vertical-align:middle;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
<tbody>
<tr>
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
<a href="https://www.reddit.com/r/Bitwarden/" target="_blank">
<img alt height="24" src="https://assets.bitwarden.com/email/v1/social-icons-reddit.png" style="border-radius:3px;display:block;" width="24">
</a>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<!--[if mso | IE]></td><td><![endif]-->
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
<tbody>
<tr>
<td style="padding:8px;vertical-align:middle;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
<tbody>
<tr>
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
<a href="https://community.bitwarden.com/" target="_blank">
<img alt height="24" src="https://assets.bitwarden.com/email/v1/social-icons-discourse.png" style="border-radius:3px;display:block;" width="24">
</a>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<!--[if mso | IE]></td><td><![endif]-->
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
<tbody>
<tr>
<td style="padding:8px;vertical-align:middle;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
<tbody>
<tr>
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
<a href="https://github.com/bitwarden" target="_blank">
<img alt height="24" src="https://assets.bitwarden.com/email/v1/social-icons-github.png" style="border-radius:3px;display:block;" width="24">
</a>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<!--[if mso | IE]></td><td><![endif]-->
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
<tbody>
<tr>
<td style="padding:8px;vertical-align:middle;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
<tbody>
<tr>
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
<a href="https://www.youtube.com/channel/UCId9a_jQqvJre0_dE2lE_Rw" target="_blank">
<img alt height="24" src="https://assets.bitwarden.com/email/v1/social-icons-youtube.png" style="border-radius:3px;display:block;" width="24">
</a>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<!--[if mso | IE]></td><td><![endif]-->
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
<tbody>
<tr>
<td style="padding:8px;vertical-align:middle;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
<tbody>
<tr>
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
<a href="https://www.linkedin.com/company/bitwarden1/" target="_blank">
<img alt height="24" src="https://assets.bitwarden.com/email/v1/social-icons-linkedin.png" style="border-radius:3px;display:block;" width="24">
</a>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<!--[if mso | IE]></td><td><![endif]-->
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
<tbody>
<tr>
<td style="padding:8px;vertical-align:middle;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
<tbody>
<tr>
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
<a href="https://www.facebook.com/bitwarden/" target="_blank">
<img alt height="24" src="https://assets.bitwarden.com/email/v1/social-icons-facebook.png" style="border-radius:3px;display:block;" width="24">
</a>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
<tr>
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:12px;line-height:16px;text-align:center;color:#5A6D91;"><p style="margin-bottom: 5px; margin-top: 5px">
© {{ CurrentYear }} Bitwarden Inc. 1 N. Calle Cesar Chavez, Suite 102, Santa
Barbara, CA, USA
</p>
<p style="margin-top: 5px">
Always confirm you are on a trusted Bitwarden domain before logging
in:<br>
<a href="https://bitwarden.com/" style="text-decoration:none;color:#175ddc; font-weight:400">bitwarden.com</a> |
<a href="https://bitwarden.com/help/emails-from-bitwarden/" style="text-decoration:none; color:#175ddc; font-weight:400">Learn why we include this</a>
</p></div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</div>
</body>
</html>

View File

@@ -0,0 +1,8 @@
{{#>BasicTextLayout}}
Automatic user confirmation is now available!
A new policy is available for your organization. It allows new users to be automatically confirmed while an
admins device is unlocked. Log in to the web app to turn on the policy.
Learn more about this policy here: https://bitwarden.com/help/automatic-confirmation/
{{/BasicTextLayout}}

View File

@@ -0,0 +1,61 @@
using System.Net;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Models.Mail.Mailer.OrganizationUserAutoConfirmation;
using Bit.Core.Platform.Mail.Mailer;
using Bit.Core.Settings;
using Microsoft.Extensions.Logging;
using OneOf.Types;
using CommandResult = Bit.Core.AdminConsole.Utilities.v2.Results.CommandResult;
using Error = Bit.Core.AdminConsole.Utilities.v2.Error;
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUser;
public record OrganizationAutoConfirmEnabledNotificationRequest(Organization Organization, ICollection<string> Emails);
public record NoEmailsWereProvided() : Error("No emails were provided");
public record EmailSendingFailed() : Error("Failed to send email to organization admins");
public interface IOrganizationAutoConfirmEnabledNotificationCommand
{
Task<CommandResult> SendEmailAsync(OrganizationAutoConfirmEnabledNotificationRequest request);
}
public class OrganizationAutoConfirmEnabledNotificationCommand(
IMailer mailer,
ILogger<OrganizationAutoConfirmEnabledNotificationCommand> logger,
GlobalSettings globalSettings) : IOrganizationAutoConfirmEnabledNotificationCommand
{
public async Task<CommandResult> SendEmailAsync(OrganizationAutoConfirmEnabledNotificationRequest request)
{
if (request.Emails.Count == 0)
{
return new NoEmailsWereProvided();
}
var mail = new OrganizationAutoConfirmationEnabled
{
ToEmails = request.Emails,
View = new OrganizationAutoConfirmationEnabledView
{
WebVaultUrl = globalSettings.BaseServiceUri.Vault + "#/organizations/" + request.Organization.Id + "/settings/policies"
},
Subject = $"Automatic user confirmation is available for {WebUtility.HtmlEncode(request.Organization.Name)}"
};
try
{
await mailer.SendEmail(mail);
}
catch (Exception ex)
{
logger.LogError(ex,
"Failed to send email to organization admins for Auto Confirm feature enablement. Organization: {OrganizationId}",
request.Organization.Id);
return new EmailSendingFailed();
}
return new None();
}
}

View File

@@ -0,0 +1,54 @@
<mjml>
<mj-head>
<mj-include path="../../../components/head.mjml" />
</mj-head>
<mj-body>
<!-- Blue Header Section -->
<mj-wrapper css-class="border-fix" padding="20px 24px 0px 24px">
<mj-bw-simple-hero />
</mj-wrapper>
<!-- Main Content -->
<mj-wrapper padding="0px 25px">
<mj-section background-color="#fff" padding="24px 0px">
<mj-column padding="0px 25px">
<mj-text padding="0px 0px 24px 0px" font-family="roboto" font-weight="700" line-height="24px">
Automatic user confirmation is now available!
</mj-text>
<mj-text padding="0px 0px 24px 0px" font-family="roboto" font-weight="400" line-height="24px">
A new policy is available for your organization. It allows new users
to be automatically confirmed while an admins device is unlocked.
Log in to the web app to turn on the policy.
</mj-text>
<mj-button
padding="0px 0px 24px 0px"
inner-padding="12px 24px"
border-radius="20px"
font-weight="600"
align="left"
href="{{WebVaultUrl}}"
>Log in</mj-button
>
<mj-text
padding="0px"
font-family="roboto"
line-height="16px"
font-weight="700"
font-size="13px"
>
<a
class="link"
href="https://bitwarden.com/help/automatic-confirmation/"
>Learn more about this policy</a
>
</mj-text>
</mj-column>
</mj-section>
</mj-wrapper>
<!-- Footer -->
<mj-wrapper padding-top="15px">
<mj-include path="../../../components/footer.mjml" />
</mj-wrapper>
</mj-body>
</mjml>

View File

@@ -73,6 +73,12 @@ public static class OrganizationServiceCollectionExtensions
services.AddOrganizationUserCommands();
services.AddOrganizationUserCommandsQueries();
services.AddBaseOrganizationSubscriptionCommandsQueries();
services.AddOrganizationFeatureCommands();
}
private static void AddOrganizationFeatureCommands(this IServiceCollection services)
{
services.AddScoped<IOrganizationAutoConfirmEnabledNotificationCommand, OrganizationAutoConfirmEnabledNotificationCommand>();
}
private static void AddOrganizationSignUpCommands(this IServiceCollection services)

View File

@@ -0,0 +1,117 @@
using System.Net;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Models.Mail.Mailer.OrganizationUserAutoConfirmation;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUser;
using Bit.Core.Platform.Mail.Mailer;
using Bit.Core.Test.AutoFixture.OrganizationFixtures;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using NSubstitute.ExceptionExtensions;
using Xunit;
using GlobalSettings = Bit.Core.Settings.GlobalSettings;
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUser;
[SutProviderCustomize]
public class OrganizationAutoConfirmEnabledNotificationCommandTests
{
[Theory]
[OrganizationCustomize, BitAutoData]
public async Task SendEmailAsync_NoEmailsProvided_ReturnsNoEmailsWereProvidedError(
Organization organization,
SutProvider<OrganizationAutoConfirmEnabledNotificationCommand> sutProvider)
{
// Arrange
SetupGlobalSettings(sutProvider);
var request = new OrganizationAutoConfirmEnabledNotificationRequest(organization, []);
// Act
var result = await sutProvider.Sut.SendEmailAsync(request);
// Assert
Assert.True(result.IsError);
Assert.IsType<NoEmailsWereProvided>(result.AsError);
await sutProvider.GetDependency<IMailer>()
.DidNotReceive()
.SendEmail(Arg.Any<OrganizationAutoConfirmationEnabled>());
}
[Theory]
[OrganizationCustomize, BitAutoData]
public async Task SendEmailAsync_WithValidEmails_SendsEmailWithCorrectProperties(
Organization organization,
List<string> emails,
SutProvider<OrganizationAutoConfirmEnabledNotificationCommand> sutProvider)
{
// Arrange
const string vaultUrl = "https://vault.bitwarden.com/";
SetupGlobalSettings(sutProvider, vaultUrl);
var request = new OrganizationAutoConfirmEnabledNotificationRequest(organization, emails);
var expectedUrl = $"{vaultUrl}#/organizations/{organization.Id}/settings/policies";
// Act
var result = await sutProvider.Sut.SendEmailAsync(request);
// Assert
Assert.True(result.IsSuccess);
await sutProvider.GetDependency<IMailer>()
.Received(1)
.SendEmail(Arg.Is<OrganizationAutoConfirmationEnabled>(mail =>
mail.ToEmails.SequenceEqual(emails) &&
mail.View.WebVaultUrl == expectedUrl &&
mail.Subject == $"Automatic user confirmation is available for {WebUtility.HtmlEncode(organization.Name)}"));
}
[Theory]
[OrganizationCustomize, BitAutoData]
public async Task SendEmailAsync_MailerThrowsException_ReturnsEmailSendingFailedError(
Organization organization,
List<string> emails,
SutProvider<OrganizationAutoConfirmEnabledNotificationCommand> sutProvider)
{
// Arrange
SetupGlobalSettings(sutProvider);
sutProvider.GetDependency<IMailer>()
.SendEmail(Arg.Any<OrganizationAutoConfirmationEnabled>())
.ThrowsAsync(new Exception("SMTP failure"));
var request = new OrganizationAutoConfirmEnabledNotificationRequest(organization, emails);
// Act
var result = await sutProvider.Sut.SendEmailAsync(request);
// Assert
Assert.True(result.IsError);
Assert.IsType<EmailSendingFailed>(result.AsError);
}
[Theory]
[OrganizationCustomize, BitAutoData]
public async Task SendEmailAsync_OrganizationNameWithSpecialCharacters_HtmlEncodesSubject(
Organization organization,
List<string> emails,
SutProvider<OrganizationAutoConfirmEnabledNotificationCommand> sutProvider)
{
// Arrange
SetupGlobalSettings(sutProvider);
organization.Name = "Test & Company <script>";
var request = new OrganizationAutoConfirmEnabledNotificationRequest(organization, emails);
// Act
await sutProvider.Sut.SendEmailAsync(request);
// Assert
await sutProvider.GetDependency<IMailer>()
.Received(1)
.SendEmail(Arg.Is<OrganizationAutoConfirmationEnabled>(mail =>
mail.Subject == $"Automatic user confirmation is available for {WebUtility.HtmlEncode(organization.Name)}"));
}
private static void SetupGlobalSettings(
SutProvider<OrganizationAutoConfirmEnabledNotificationCommand> sutProvider,
string vaultUrl = "https://vault.bitwarden.com/")
{
var globalSettings = sutProvider.GetDependency<GlobalSettings>();
globalSettings.BaseServiceUri = new GlobalSettings.BaseServiceUriSettings(globalSettings) { Vault = vaultUrl };
}
}