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:
@@ -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)
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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 admin’s 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>
|
||||
@@ -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
|
||||
admin’s 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}}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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 admin’s 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>
|
||||
@@ -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)
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user