From 708ea66393aaf01afc9713c3893b498429566c78 Mon Sep 17 00:00:00 2001 From: Jared McCannon Date: Fri, 20 Feb 2026 08:26:31 -0600 Subject: [PATCH] [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 --- .../Controllers/OrganizationsController.cs | 37 +- ...OrganizationAutoConfirmationEnabledView.cs | 13 + ...zationAutoConfirmationEnabledView.html.hbs | 513 ++++++++++++++++++ ...zationAutoConfirmationEnabledView.text.hbs | 8 + ...onAutoConfirmEnabledNotificationCommand.cs | 61 +++ .../organization-auto-confirm-enabled.mjml | 54 ++ ...OrganizationServiceCollectionExtensions.cs | 6 + ...oConfirmEnabledNotificationCommandTests.cs | 117 ++++ 8 files changed, 804 insertions(+), 5 deletions(-) create mode 100644 src/Core/AdminConsole/Models/Mail/Mailer/OrganizationUserAutoConfirmation/OrganizationAutoConfirmationEnabledView.cs create mode 100644 src/Core/AdminConsole/Models/Mail/Mailer/OrganizationUserAutoConfirmation/OrganizationAutoConfirmationEnabledView.html.hbs create mode 100644 src/Core/AdminConsole/Models/Mail/Mailer/OrganizationUserAutoConfirmation/OrganizationAutoConfirmationEnabledView.text.hbs create mode 100644 src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AutoConfirmUser/OrganizationAutoConfirmEnabledNotificationCommand.cs create mode 100644 src/Core/MailTemplates/Mjml/emails/AdminConsole/OrganizationConfirmation/organization-auto-confirm-enabled.mjml create mode 100644 test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/AutoConfirmUser/OrganizationAutoConfirmEnabledNotificationCommandTests.cs diff --git a/src/Admin/AdminConsole/Controllers/OrganizationsController.cs b/src/Admin/AdminConsole/Controllers/OrganizationsController.cs index ac5514b838..5151c12eb2 100644 --- a/src/Admin/AdminConsole/Controllers/OrganizationsController.cs +++ b/src/Admin/AdminConsole/Controllers/OrganizationsController.cs @@ -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) diff --git a/src/Core/AdminConsole/Models/Mail/Mailer/OrganizationUserAutoConfirmation/OrganizationAutoConfirmationEnabledView.cs b/src/Core/AdminConsole/Models/Mail/Mailer/OrganizationUserAutoConfirmation/OrganizationAutoConfirmationEnabledView.cs new file mode 100644 index 0000000000..3c87792ff7 --- /dev/null +++ b/src/Core/AdminConsole/Models/Mail/Mailer/OrganizationUserAutoConfirmation/OrganizationAutoConfirmationEnabledView.cs @@ -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 +{ + public override required string Subject { get; set; } +} diff --git a/src/Core/AdminConsole/Models/Mail/Mailer/OrganizationUserAutoConfirmation/OrganizationAutoConfirmationEnabledView.html.hbs b/src/Core/AdminConsole/Models/Mail/Mailer/OrganizationUserAutoConfirmation/OrganizationAutoConfirmationEnabledView.html.hbs new file mode 100644 index 0000000000..f79f367442 --- /dev/null +++ b/src/Core/AdminConsole/Models/Mail/Mailer/OrganizationUserAutoConfirmation/OrganizationAutoConfirmationEnabledView.html.hbs @@ -0,0 +1,513 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + +
+ + + + + + + +
+ + + + + + + + +
+ + + + + +
+ + + + + + + +
+ + +
+ + + + + + + + + +
+ + + + + + + +
+ + + +
+ +
+ +
+ + +
+ +
+ + + + + +
+ + +
+ +
+ + + + + + + + + +
+ + + + + + + +
+ + + +
+ + + + + + + +
+ + +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +
+ +
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.
+ +
+ + + + + + + +
+ + Log in + +
+ +
+ + + +
+ +
+ +
+ + +
+ +
+ + + +
+ +
+ + + + + + + + + +
+ + + + + + + +
+ + + +
+ + + + + + + +
+ + +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + +
+ +

+ © {{ CurrentYear }} Bitwarden Inc. 1 N. Calle Cesar Chavez, Suite 102, Santa + Barbara, CA, USA +

+

+ Always confirm you are on a trusted Bitwarden domain before logging + in:
+ bitwarden.com | + Learn why we include this +

+ +
+ +
+ + +
+ +
+ + + +
+ +
+ + + + + +
+ + + diff --git a/src/Core/AdminConsole/Models/Mail/Mailer/OrganizationUserAutoConfirmation/OrganizationAutoConfirmationEnabledView.text.hbs b/src/Core/AdminConsole/Models/Mail/Mailer/OrganizationUserAutoConfirmation/OrganizationAutoConfirmationEnabledView.text.hbs new file mode 100644 index 0000000000..86f0804944 --- /dev/null +++ b/src/Core/AdminConsole/Models/Mail/Mailer/OrganizationUserAutoConfirmation/OrganizationAutoConfirmationEnabledView.text.hbs @@ -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}} diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AutoConfirmUser/OrganizationAutoConfirmEnabledNotificationCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AutoConfirmUser/OrganizationAutoConfirmEnabledNotificationCommand.cs new file mode 100644 index 0000000000..aece8c930c --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AutoConfirmUser/OrganizationAutoConfirmEnabledNotificationCommand.cs @@ -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 Emails); + +public record NoEmailsWereProvided() : Error("No emails were provided"); + +public record EmailSendingFailed() : Error("Failed to send email to organization admins"); + +public interface IOrganizationAutoConfirmEnabledNotificationCommand +{ + Task SendEmailAsync(OrganizationAutoConfirmEnabledNotificationRequest request); +} + +public class OrganizationAutoConfirmEnabledNotificationCommand( + IMailer mailer, + ILogger logger, + GlobalSettings globalSettings) : IOrganizationAutoConfirmEnabledNotificationCommand +{ + public async Task 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(); + } +} diff --git a/src/Core/MailTemplates/Mjml/emails/AdminConsole/OrganizationConfirmation/organization-auto-confirm-enabled.mjml b/src/Core/MailTemplates/Mjml/emails/AdminConsole/OrganizationConfirmation/organization-auto-confirm-enabled.mjml new file mode 100644 index 0000000000..c9eab86813 --- /dev/null +++ b/src/Core/MailTemplates/Mjml/emails/AdminConsole/OrganizationConfirmation/organization-auto-confirm-enabled.mjml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + 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. + + Log in + + Learn more about this policy + + + + + + + + + + + diff --git a/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs index 6fb1145e7c..317a9366ae 100644 --- a/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs +++ b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs @@ -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(); } private static void AddOrganizationSignUpCommands(this IServiceCollection services) diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/AutoConfirmUser/OrganizationAutoConfirmEnabledNotificationCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/AutoConfirmUser/OrganizationAutoConfirmEnabledNotificationCommandTests.cs new file mode 100644 index 0000000000..c5a2522aae --- /dev/null +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/AutoConfirmUser/OrganizationAutoConfirmEnabledNotificationCommandTests.cs @@ -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 sutProvider) + { + // Arrange + SetupGlobalSettings(sutProvider); + var request = new OrganizationAutoConfirmEnabledNotificationRequest(organization, []); + + // Act + var result = await sutProvider.Sut.SendEmailAsync(request); + + // Assert + Assert.True(result.IsError); + Assert.IsType(result.AsError); + await sutProvider.GetDependency() + .DidNotReceive() + .SendEmail(Arg.Any()); + } + + [Theory] + [OrganizationCustomize, BitAutoData] + public async Task SendEmailAsync_WithValidEmails_SendsEmailWithCorrectProperties( + Organization organization, + List emails, + SutProvider 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() + .Received(1) + .SendEmail(Arg.Is(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 emails, + SutProvider sutProvider) + { + // Arrange + SetupGlobalSettings(sutProvider); + sutProvider.GetDependency() + .SendEmail(Arg.Any()) + .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(result.AsError); + } + + [Theory] + [OrganizationCustomize, BitAutoData] + public async Task SendEmailAsync_OrganizationNameWithSpecialCharacters_HtmlEncodesSubject( + Organization organization, + List emails, + SutProvider sutProvider) + { + // Arrange + SetupGlobalSettings(sutProvider); + organization.Name = "Test & Company