mirror of
https://github.com/bitwarden/server
synced 2026-01-11 21:13:50 +00:00
[PM-27882] Add SendOrganizationConfirmationCommand (#6743)
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
using Bit.Core.Platform.Mail.Mailer;
|
||||
|
||||
namespace Bit.Core.AdminConsole.Models.Mail.Mailer.OrganizationConfirmation;
|
||||
|
||||
public abstract class OrganizationConfirmationBaseView : BaseMailView
|
||||
{
|
||||
public required string OrganizationName { get; set; }
|
||||
public required string TitleFirst { get; set; }
|
||||
public required string TitleSecondBold { get; set; }
|
||||
public required string TitleThird { get; set; }
|
||||
public required string WebVaultUrl { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using Bit.Core.Platform.Mail.Mailer;
|
||||
|
||||
namespace Bit.Core.AdminConsole.Models.Mail.Mailer.OrganizationConfirmation;
|
||||
|
||||
public class OrganizationConfirmationEnterpriseTeamsView : OrganizationConfirmationBaseView
|
||||
{
|
||||
}
|
||||
|
||||
public class OrganizationConfirmationEnterpriseTeams : BaseMail<OrganizationConfirmationEnterpriseTeamsView>
|
||||
{
|
||||
public override required string Subject { get; set; }
|
||||
}
|
||||
@@ -178,7 +178,7 @@
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" bgcolor="#ffffff" role="presentation" style="border:none;border-radius:20px;cursor:auto;mso-padding-alt:10px 25px;background:#ffffff;" valign="middle">
|
||||
<a href="https://vault.bitwarden.com" style="display:inline-block;background:#ffffff;color:#1A41AC;font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:20px;" target="_blank">
|
||||
<a href="{{{WebVaultUrl}}}" style="display:inline-block;background:#ffffff;color:#1A41AC;font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:20px;" target="_blank">
|
||||
Log in
|
||||
</a>
|
||||
</td>
|
||||
@@ -0,0 +1,12 @@
|
||||
using Bit.Core.Platform.Mail.Mailer;
|
||||
|
||||
namespace Bit.Core.AdminConsole.Models.Mail.Mailer.OrganizationConfirmation;
|
||||
|
||||
public class OrganizationConfirmationFamilyFreeView : OrganizationConfirmationBaseView
|
||||
{
|
||||
}
|
||||
|
||||
public class OrganizationConfirmationFamilyFree : BaseMail<OrganizationConfirmationFamilyFreeView>
|
||||
{
|
||||
public override required string Subject { get; set; }
|
||||
}
|
||||
@@ -182,7 +182,7 @@
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" bgcolor="#ffffff" role="presentation" style="border:none;border-radius:20px;cursor:auto;mso-padding-alt:10px 25px;background:#ffffff;" valign="middle">
|
||||
<a href="https://vault.bitwarden.com" style="display:inline-block;background:#ffffff;color:#1A41AC;font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:20px;" target="_blank">
|
||||
<a href="{{{WebVaultUrl}}}" style="display:inline-block;background:#ffffff;color:#1A41AC;font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:20px;" target="_blank">
|
||||
Log in
|
||||
</a>
|
||||
</td>
|
||||
@@ -1,5 +1,7 @@
|
||||
using Bit.Core.AdminConsole.Models.Data.OrganizationUsers;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Models.Data.OrganizationUsers;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.OrganizationConfirmation;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
using Bit.Core.Entities;
|
||||
@@ -25,6 +27,8 @@ public class AutomaticallyConfirmOrganizationUserCommand(IOrganizationUserReposi
|
||||
IPushNotificationService pushNotificationService,
|
||||
IPolicyRequirementQuery policyRequirementQuery,
|
||||
ICollectionRepository collectionRepository,
|
||||
IFeatureService featureService,
|
||||
ISendOrganizationConfirmationCommand sendOrganizationConfirmationCommand,
|
||||
TimeProvider timeProvider,
|
||||
ILogger<AutomaticallyConfirmOrganizationUserCommand> logger) : IAutomaticallyConfirmOrganizationUserCommand
|
||||
{
|
||||
@@ -143,9 +147,7 @@ public class AutomaticallyConfirmOrganizationUserCommand(IOrganizationUserReposi
|
||||
{
|
||||
var user = await userRepository.GetByIdAsync(request.OrganizationUser!.UserId!.Value);
|
||||
|
||||
await mailService.SendOrganizationConfirmedEmailAsync(request.Organization!.Name,
|
||||
user!.Email,
|
||||
request.OrganizationUser.AccessSecretsManager);
|
||||
await SendOrganizationConfirmedEmailAsync(request.Organization!, user!.Email, request.OrganizationUser.AccessSecretsManager);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -183,4 +185,23 @@ public class AutomaticallyConfirmOrganizationUserCommand(IOrganizationUserReposi
|
||||
Organization = await organizationRepository.GetByIdAsync(request.OrganizationId)
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends the organization confirmed email using either the new mailer pattern or the legacy mail service,
|
||||
/// depending on the feature flag.
|
||||
/// </summary>
|
||||
/// <param name="organization">The organization the user was confirmed to.</param>
|
||||
/// <param name="userEmail">The email address of the confirmed user.</param>
|
||||
/// <param name="accessSecretsManager">Whether the user has access to Secrets Manager.</param>
|
||||
internal async Task SendOrganizationConfirmedEmailAsync(Organization organization, string userEmail, bool accessSecretsManager)
|
||||
{
|
||||
if (featureService.IsEnabled(FeatureFlagKeys.OrganizationConfirmationEmail))
|
||||
{
|
||||
await sendOrganizationConfirmationCommand.SendConfirmationAsync(organization, userEmail, accessSecretsManager);
|
||||
}
|
||||
else
|
||||
{
|
||||
await mailService.SendOrganizationConfirmedEmailAsync(organization.Name, userEmail, accessSecretsManager);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// FIXME: Update this file to be null safe and then delete the line below
|
||||
#nullable disable
|
||||
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.OrganizationConfirmation;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Enforcement.AutoConfirm;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
@@ -35,7 +37,7 @@ public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly ICollectionRepository _collectionRepository;
|
||||
private readonly IAutomaticUserConfirmationPolicyEnforcementValidator _automaticUserConfirmationPolicyEnforcementValidator;
|
||||
|
||||
private readonly ISendOrganizationConfirmationCommand _sendOrganizationConfirmationCommand;
|
||||
public ConfirmOrganizationUserCommand(
|
||||
IOrganizationRepository organizationRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
@@ -50,7 +52,7 @@ public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand
|
||||
IPolicyRequirementQuery policyRequirementQuery,
|
||||
IFeatureService featureService,
|
||||
ICollectionRepository collectionRepository,
|
||||
IAutomaticUserConfirmationPolicyEnforcementValidator automaticUserConfirmationPolicyEnforcementValidator)
|
||||
IAutomaticUserConfirmationPolicyEnforcementValidator automaticUserConfirmationPolicyEnforcementValidator, ISendOrganizationConfirmationCommand sendOrganizationConfirmationCommand)
|
||||
{
|
||||
_organizationRepository = organizationRepository;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
@@ -66,8 +68,8 @@ public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand
|
||||
_featureService = featureService;
|
||||
_collectionRepository = collectionRepository;
|
||||
_automaticUserConfirmationPolicyEnforcementValidator = automaticUserConfirmationPolicyEnforcementValidator;
|
||||
_sendOrganizationConfirmationCommand = sendOrganizationConfirmationCommand;
|
||||
}
|
||||
|
||||
public async Task<OrganizationUser> ConfirmUserAsync(Guid organizationId, Guid organizationUserId, string key,
|
||||
Guid confirmingUserId, string defaultUserCollectionName = null)
|
||||
{
|
||||
@@ -170,7 +172,7 @@ public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand
|
||||
orgUser.Email = null;
|
||||
|
||||
await _eventService.LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Confirmed);
|
||||
await _mailService.SendOrganizationConfirmedEmailAsync(organization.DisplayName(), user.Email, orgUser.AccessSecretsManager);
|
||||
await SendOrganizationConfirmedEmailAsync(organization, user.Email, orgUser.AccessSecretsManager);
|
||||
succeededUsers.Add(orgUser);
|
||||
result.Add(Tuple.Create(orgUser, ""));
|
||||
}
|
||||
@@ -339,4 +341,23 @@ public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand
|
||||
|
||||
await _collectionRepository.UpsertDefaultCollectionsAsync(organizationId, eligibleOrganizationUserIds, defaultUserCollectionName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends the organization confirmed email using either the new mailer pattern or the legacy mail service,
|
||||
/// depending on the feature flag.
|
||||
/// </summary>
|
||||
/// <param name="organization">The organization the user was confirmed to.</param>
|
||||
/// <param name="userEmail">The email address of the confirmed user.</param>
|
||||
/// <param name="accessSecretsManager">Whether the user has access to Secrets Manager.</param>
|
||||
internal async Task SendOrganizationConfirmedEmailAsync(Organization organization, string userEmail, bool accessSecretsManager)
|
||||
{
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.OrganizationConfirmationEmail))
|
||||
{
|
||||
await _sendOrganizationConfirmationCommand.SendConfirmationAsync(organization, userEmail, accessSecretsManager);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _mailService.SendOrganizationConfirmedEmailAsync(organization.DisplayName(), userEmail, accessSecretsManager);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.OrganizationConfirmation;
|
||||
|
||||
public interface ISendOrganizationConfirmationCommand
|
||||
{
|
||||
/// <summary>
|
||||
/// Sends an organization confirmation email to the specified user.
|
||||
/// </summary>
|
||||
/// <param name="organization">The organization to send the confirmation email for.</param>
|
||||
/// <param name="userEmail">The email address of the user to send the confirmation to.</param>
|
||||
/// <param name="accessSecretsManager">Whether the user has access to Secrets Manager.</param>
|
||||
Task SendConfirmationAsync(Organization organization, string userEmail, bool accessSecretsManager);
|
||||
|
||||
/// <summary>
|
||||
/// Sends organization confirmation emails to multiple users.
|
||||
/// </summary>
|
||||
/// <param name="organization">The organization to send the confirmation emails for.</param>
|
||||
/// <param name="userEmails">The email addresses of the users to send confirmations to.</param>
|
||||
/// <param name="accessSecretsManager">Whether the users have access to Secrets Manager.</param>
|
||||
Task SendConfirmationsAsync(Organization organization, IEnumerable<string> userEmails, bool accessSecretsManager);
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
using System.Net;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Models.Mail.Mailer.OrganizationConfirmation;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Platform.Mail.Mailer;
|
||||
using Bit.Core.Settings;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.OrganizationConfirmation;
|
||||
|
||||
public class SendOrganizationConfirmationCommand(IMailer mailer, GlobalSettings globalSettings) : ISendOrganizationConfirmationCommand
|
||||
{
|
||||
private const string _titleFirst = "You're confirmed as a member of ";
|
||||
private const string _titleThird = "!";
|
||||
|
||||
private static string GetConfirmationSubject(string organizationName) =>
|
||||
$"You Have Been Confirmed To {organizationName}";
|
||||
private string GetWebVaultUrl(bool accessSecretsManager) => accessSecretsManager
|
||||
? globalSettings.BaseServiceUri.VaultWithHashAndSecretManagerProduct
|
||||
: globalSettings.BaseServiceUri.VaultWithHash;
|
||||
|
||||
public async Task SendConfirmationAsync(Organization organization, string userEmail, bool accessSecretsManager = false)
|
||||
{
|
||||
await SendConfirmationsAsync(organization, [userEmail], accessSecretsManager);
|
||||
}
|
||||
|
||||
public async Task SendConfirmationsAsync(Organization organization, IEnumerable<string> userEmails, bool accessSecretsManager = false)
|
||||
{
|
||||
var userEmailsList = userEmails.ToList();
|
||||
|
||||
if (userEmailsList.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var organizationName = WebUtility.HtmlDecode(organization.Name);
|
||||
|
||||
if (IsEnterpriseOrTeamsPlan(organization.PlanType))
|
||||
{
|
||||
await SendEnterpriseTeamsEmailsAsync(userEmailsList, organizationName, accessSecretsManager);
|
||||
return;
|
||||
}
|
||||
|
||||
await SendFamilyFreeConfirmEmailsAsync(userEmailsList, organizationName, accessSecretsManager);
|
||||
}
|
||||
|
||||
private async Task SendEnterpriseTeamsEmailsAsync(List<string> userEmailsList, string organizationName, bool accessSecretsManager)
|
||||
{
|
||||
var mail = new OrganizationConfirmationEnterpriseTeams
|
||||
{
|
||||
ToEmails = userEmailsList,
|
||||
Subject = GetConfirmationSubject(organizationName),
|
||||
View = new OrganizationConfirmationEnterpriseTeamsView
|
||||
{
|
||||
OrganizationName = organizationName,
|
||||
TitleFirst = _titleFirst,
|
||||
TitleSecondBold = organizationName,
|
||||
TitleThird = _titleThird,
|
||||
WebVaultUrl = GetWebVaultUrl(accessSecretsManager)
|
||||
}
|
||||
};
|
||||
|
||||
await mailer.SendEmail(mail);
|
||||
}
|
||||
|
||||
private async Task SendFamilyFreeConfirmEmailsAsync(List<string> userEmailsList, string organizationName, bool accessSecretsManager)
|
||||
{
|
||||
var mail = new OrganizationConfirmationFamilyFree
|
||||
{
|
||||
ToEmails = userEmailsList,
|
||||
Subject = GetConfirmationSubject(organizationName),
|
||||
View = new OrganizationConfirmationFamilyFreeView
|
||||
{
|
||||
OrganizationName = organizationName,
|
||||
TitleFirst = _titleFirst,
|
||||
TitleSecondBold = organizationName,
|
||||
TitleThird = _titleThird,
|
||||
WebVaultUrl = GetWebVaultUrl(accessSecretsManager)
|
||||
}
|
||||
};
|
||||
|
||||
await mailer.SendEmail(mail);
|
||||
}
|
||||
|
||||
|
||||
private static bool IsEnterpriseOrTeamsPlan(PlanType planType)
|
||||
{
|
||||
return planType switch
|
||||
{
|
||||
PlanType.TeamsMonthly2019 or
|
||||
PlanType.TeamsAnnually2019 or
|
||||
PlanType.TeamsMonthly2020 or
|
||||
PlanType.TeamsAnnually2020 or
|
||||
PlanType.TeamsMonthly2023 or
|
||||
PlanType.TeamsAnnually2023 or
|
||||
PlanType.TeamsStarter2023 or
|
||||
PlanType.TeamsMonthly or
|
||||
PlanType.TeamsAnnually or
|
||||
PlanType.TeamsStarter or
|
||||
PlanType.EnterpriseMonthly2019 or
|
||||
PlanType.EnterpriseAnnually2019 or
|
||||
PlanType.EnterpriseMonthly2020 or
|
||||
PlanType.EnterpriseAnnually2020 or
|
||||
PlanType.EnterpriseMonthly2023 or
|
||||
PlanType.EnterpriseAnnually2023 or
|
||||
PlanType.EnterpriseMonthly or
|
||||
PlanType.EnterpriseAnnually => true,
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -163,6 +163,7 @@ public static class FeatureFlagKeys
|
||||
"pm-23174-manage-account-recovery-permission-drives-the-need-to-set-master-password";
|
||||
public const string MJMLBasedEmailTemplates = "mjml-based-email-templates";
|
||||
public const string MjmlWelcomeEmailTemplates = "pm-21741-mjml-welcome-email";
|
||||
public const string OrganizationConfirmationEmail = "pm-28402-update-confirmed-to-org-email-template";
|
||||
public const string MarketingInitiatedPremiumFlow = "pm-26140-marketing-initiated-premium-flow";
|
||||
public const string RedirectOnSsoRequired = "pm-1632-redirect-on-sso-required";
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
img-src="https://assets.bitwarden.com/email/v1/spot-enterprise.png"
|
||||
title="You can now share passwords with members of {{OrganizationName}}!"
|
||||
button-text="Log in"
|
||||
button-url="https://vault.bitwarden.com"
|
||||
button-url="{{WebVaultUrl}}"
|
||||
/>
|
||||
</mj-wrapper>
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
img-src="https://assets.bitwarden.com/email/v1/spot-family-homes.png"
|
||||
title="You can now share passwords with members of {{OrganizationName}}!"
|
||||
button-text="Log in"
|
||||
button-url="https://vault.bitwarden.com"
|
||||
button-url="{{WebVaultUrl}}"
|
||||
/>
|
||||
</mj-wrapper>
|
||||
|
||||
|
||||
@@ -12,5 +12,5 @@ public class Families2019RenewalMailView : BaseMailView
|
||||
|
||||
public class Families2019RenewalMail : BaseMail<Families2019RenewalMailView>
|
||||
{
|
||||
public override string Subject { get => "Your Bitwarden Families renewal is updating"; }
|
||||
public override string Subject { get; set; } = "Your Bitwarden Families renewal is updating";
|
||||
}
|
||||
|
||||
@@ -9,5 +9,5 @@ public class Families2020RenewalMailView : BaseMailView
|
||||
|
||||
public class Families2020RenewalMail : BaseMail<Families2020RenewalMailView>
|
||||
{
|
||||
public override string Subject { get => "Your Bitwarden Families renewal is updating"; }
|
||||
public override string Subject { get; set; } = "Your Bitwarden Families renewal is updating";
|
||||
}
|
||||
|
||||
@@ -11,5 +11,5 @@ public class PremiumRenewalMailView : BaseMailView
|
||||
|
||||
public class PremiumRenewalMail : BaseMail<PremiumRenewalMailView>
|
||||
{
|
||||
public override string Subject { get => "Your Bitwarden Premium renewal is updating"; }
|
||||
public override string Subject { get; set; } = "Your Bitwarden Premium renewal is updating";
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.V
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.GlobalSettings;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Organization;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.PasswordManager;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.OrganizationConfirmation;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RestoreUser.v1;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.SelfRevokeUser;
|
||||
using Bit.Core.Models.Business.Tokenables;
|
||||
@@ -45,7 +46,6 @@ using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
using V1_RevokeUsersCommand = Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RevokeUser.v1;
|
||||
using V2_RevokeUsersCommand = Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RevokeUser.v2;
|
||||
|
||||
@@ -140,6 +140,7 @@ public static class OrganizationServiceCollectionExtensions
|
||||
services.AddScoped<IUpdateOrganizationUserCommand, UpdateOrganizationUserCommand>();
|
||||
services.AddScoped<IUpdateOrganizationUserGroupsCommand, UpdateOrganizationUserGroupsCommand>();
|
||||
services.AddScoped<IConfirmOrganizationUserCommand, ConfirmOrganizationUserCommand>();
|
||||
services.AddScoped<ISendOrganizationConfirmationCommand, SendOrganizationConfirmationCommand>();
|
||||
services.AddScoped<IAdminRecoverAccountCommand, AdminRecoverAccountCommand>();
|
||||
services.AddScoped<IAutomaticallyConfirmOrganizationUserCommand, AutomaticallyConfirmOrganizationUserCommand>();
|
||||
services.AddScoped<IAutomaticallyConfirmOrganizationUsersValidator, AutomaticallyConfirmOrganizationUsersValidator>();
|
||||
|
||||
@@ -19,7 +19,7 @@ public abstract class BaseMail<TView> where TView : BaseMailView
|
||||
/// <summary>
|
||||
/// The subject of the email.
|
||||
/// </summary>
|
||||
public abstract string Subject { get; }
|
||||
public abstract string Subject { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// An optional category for processing at the upstream email delivery service.
|
||||
|
||||
@@ -124,8 +124,20 @@ public class HandlebarMailRenderer : IMailRenderer
|
||||
|
||||
// TODO: Do we still need layouts with MJML?
|
||||
var assembly = typeof(HandlebarMailRenderer).Assembly;
|
||||
var layoutSource = await ReadSourceAsync(assembly, "Bit.Core.MailTemplates.Handlebars.Layouts.Full.html.hbs");
|
||||
handlebars.RegisterTemplate("FullHtmlLayout", layoutSource);
|
||||
|
||||
// Register Full layouts
|
||||
var fullHtmlLayoutSource = await ReadSourceAsync(assembly, "Bit.Core.MailTemplates.Handlebars.Layouts.Full.html.hbs");
|
||||
handlebars.RegisterTemplate("FullHtmlLayout", fullHtmlLayoutSource);
|
||||
|
||||
var fullTextLayoutSource = await ReadSourceAsync(assembly, "Bit.Core.MailTemplates.Handlebars.Layouts.Full.text.hbs");
|
||||
handlebars.RegisterTemplate("FullTextLayout", fullTextLayoutSource);
|
||||
|
||||
// Register TitleContactUs layouts
|
||||
var titleContactUsHtmlLayoutSource = await ReadSourceAsync(assembly, "Bit.Core.MailTemplates.Handlebars.Layouts.TitleContactUs.html.hbs");
|
||||
handlebars.RegisterTemplate("TitleContactUsHtmlLayout", titleContactUsHtmlLayoutSource);
|
||||
|
||||
var titleContactUsTextLayoutSource = await ReadSourceAsync(assembly, "Bit.Core.MailTemplates.Handlebars.Layouts.TitleContactUs.text.hbs");
|
||||
handlebars.RegisterTemplate("TitleContactUsTextLayout", titleContactUsTextLayoutSource);
|
||||
|
||||
return handlebars;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ using Bit.Core.AdminConsole.Models.Data;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.Models.Data.OrganizationUsers;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUser;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.OrganizationConfirmation;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
using Bit.Core.AdminConsole.Utilities.v2;
|
||||
@@ -727,4 +728,54 @@ public class AutomaticallyConfirmUsersCommandTests
|
||||
Arg.Any<IEnumerable<string>>(),
|
||||
organization.Id.ToString());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task SendOrganizationConfirmedEmailAsync_WithFeatureFlagOn_UsesNewMailer(
|
||||
Organization organization,
|
||||
string userEmail,
|
||||
SutProvider<AutomaticallyConfirmOrganizationUserCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
const bool accessSecretsManager = true;
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.OrganizationConfirmationEmail)
|
||||
.Returns(true);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.SendOrganizationConfirmedEmailAsync(organization, userEmail, accessSecretsManager);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<ISendOrganizationConfirmationCommand>()
|
||||
.Received(1)
|
||||
.SendConfirmationAsync(organization, userEmail, accessSecretsManager);
|
||||
await sutProvider.GetDependency<IMailService>()
|
||||
.DidNotReceive()
|
||||
.SendOrganizationConfirmedEmailAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<bool>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task SendOrganizationConfirmedEmailAsync_WithFeatureFlagOff_UsesLegacyMailService(
|
||||
Organization organization,
|
||||
string userEmail,
|
||||
SutProvider<AutomaticallyConfirmOrganizationUserCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
const bool accessSecretsManager = false;
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.OrganizationConfirmationEmail)
|
||||
.Returns(false);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.SendOrganizationConfirmedEmailAsync(organization, userEmail, accessSecretsManager);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IMailService>()
|
||||
.Received(1)
|
||||
.SendOrganizationConfirmedEmailAsync(organization.Name, userEmail, accessSecretsManager);
|
||||
await sutProvider.GetDependency<ISendOrganizationConfirmationCommand>()
|
||||
.DidNotReceive()
|
||||
.SendConfirmationAsync(Arg.Any<Organization>(), Arg.Any<string>(), Arg.Any<bool>());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUser;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.OrganizationConfirmation;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Enforcement.AutoConfirm;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
@@ -809,4 +810,52 @@ public class ConfirmOrganizationUserCommandTests
|
||||
Assert.Empty(result[1].Item2);
|
||||
Assert.Equal(new OtherOrganizationDoesNotAllowOtherMembership().Message, result[2].Item2);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task SendOrganizationConfirmedEmailAsync_WithFeatureFlagOn_UsesNewMailer(
|
||||
Organization org,
|
||||
string userEmail,
|
||||
SutProvider<ConfirmOrganizationUserCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
const bool accessSecretsManager = true;
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.OrganizationConfirmationEmail)
|
||||
.Returns(true);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.SendOrganizationConfirmedEmailAsync(org, userEmail, accessSecretsManager);
|
||||
|
||||
// Assert - verify new mailer is called, not legacy mail service
|
||||
await sutProvider.GetDependency<ISendOrganizationConfirmationCommand>()
|
||||
.Received(1)
|
||||
.SendConfirmationAsync(org, userEmail, accessSecretsManager);
|
||||
await sutProvider.GetDependency<IMailService>()
|
||||
.DidNotReceive()
|
||||
.SendOrganizationConfirmedEmailAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<bool>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task SendOrganizationConfirmedEmailAsync_WithFeatureFlagOff_UsesLegacyMailService(
|
||||
Organization org,
|
||||
string userEmail,
|
||||
SutProvider<ConfirmOrganizationUserCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
const bool accessSecretsManager = false;
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.OrganizationConfirmationEmail)
|
||||
.Returns(false);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.SendOrganizationConfirmedEmailAsync(org, userEmail, accessSecretsManager);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IMailService>()
|
||||
.Received(1)
|
||||
.SendOrganizationConfirmedEmailAsync(org.DisplayName(), userEmail, accessSecretsManager);
|
||||
await sutProvider.GetDependency<ISendOrganizationConfirmationCommand>()
|
||||
.DidNotReceive()
|
||||
.SendConfirmationAsync(Arg.Any<Organization>(), Arg.Any<string>(), Arg.Any<bool>());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,245 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Models.Mail.Mailer.OrganizationConfirmation;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.OrganizationConfirmation;
|
||||
using Bit.Core.Billing.Enums;
|
||||
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 Xunit;
|
||||
|
||||
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers.OrganizationConfirmation;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class SendOrganizationConfirmationCommandTests
|
||||
{
|
||||
[Theory]
|
||||
[OrganizationCustomize, BitAutoData]
|
||||
public async Task SendConfirmationAsync_EnterpriseOrganization_SendsEnterpriseTeamsEmail(
|
||||
Organization organization,
|
||||
string userEmail,
|
||||
SutProvider<SendOrganizationConfirmationCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
organization.PlanType = PlanType.EnterpriseAnnually;
|
||||
organization.Name = "Test Enterprise Org";
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.SendConfirmationAsync(organization, userEmail, false);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IMailer>().Received(1)
|
||||
.SendEmail(Arg.Is<OrganizationConfirmationEnterpriseTeams>(mail =>
|
||||
mail.ToEmails.Contains(userEmail) &&
|
||||
mail.ToEmails.Count() == 1 &&
|
||||
mail.View.OrganizationName == organization.Name &&
|
||||
mail.Subject == GetSubject(organization.Name)));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[OrganizationCustomize, BitAutoData]
|
||||
public async Task SendConfirmationAsync_TeamsOrganization_SendsEnterpriseTeamsEmail(
|
||||
Organization organization,
|
||||
string userEmail,
|
||||
SutProvider<SendOrganizationConfirmationCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
organization.PlanType = PlanType.TeamsAnnually;
|
||||
organization.Name = "Test Teams Org";
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.SendConfirmationAsync(organization, userEmail, false);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IMailer>().Received(1)
|
||||
.SendEmail(Arg.Is<OrganizationConfirmationEnterpriseTeams>(mail =>
|
||||
mail.ToEmails.Contains(userEmail) &&
|
||||
mail.ToEmails.Count() == 1 &&
|
||||
mail.View.OrganizationName == organization.Name &&
|
||||
mail.Subject == GetSubject(organization.Name)));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[OrganizationCustomize, BitAutoData]
|
||||
public async Task SendConfirmationAsync_FamilyOrganization_SendsFamilyFreeEmail(
|
||||
Organization organization,
|
||||
string userEmail,
|
||||
SutProvider<SendOrganizationConfirmationCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
organization.PlanType = PlanType.FamiliesAnnually;
|
||||
organization.Name = "Test Family Org";
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.SendConfirmationAsync(organization, userEmail, false);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IMailer>().Received(1)
|
||||
.SendEmail(Arg.Is<OrganizationConfirmationFamilyFree>(mail =>
|
||||
mail.ToEmails.Contains(userEmail) &&
|
||||
mail.ToEmails.Count() == 1 &&
|
||||
mail.View.OrganizationName == organization.Name &&
|
||||
mail.Subject == GetSubject(organization.Name)));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[OrganizationCustomize, BitAutoData]
|
||||
public async Task SendConfirmationAsync_FreeOrganization_SendsFamilyFreeEmail(
|
||||
Organization organization,
|
||||
string userEmail,
|
||||
SutProvider<SendOrganizationConfirmationCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
organization.PlanType = PlanType.Free;
|
||||
organization.Name = "Test Free Org";
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.SendConfirmationAsync(organization, userEmail, false);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IMailer>().Received(1)
|
||||
.SendEmail(Arg.Is<OrganizationConfirmationFamilyFree>(mail =>
|
||||
mail.ToEmails.Contains(userEmail) &&
|
||||
mail.ToEmails.Count() == 1 &&
|
||||
mail.View.OrganizationName == organization.Name &&
|
||||
mail.Subject == GetSubject(organization.Name)));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[OrganizationCustomize, BitAutoData]
|
||||
public async Task SendConfirmationsAsync_MultipleUsers_SendsSingleEmail(
|
||||
Organization organization,
|
||||
List<string> userEmails,
|
||||
SutProvider<SendOrganizationConfirmationCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
organization.PlanType = PlanType.EnterpriseAnnually;
|
||||
organization.Name = "Test Enterprise Org";
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.SendConfirmationsAsync(organization, userEmails, false);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IMailer>().Received(1)
|
||||
.SendEmail(Arg.Is<OrganizationConfirmationEnterpriseTeams>(mail =>
|
||||
mail.ToEmails.SequenceEqual(userEmails) &&
|
||||
mail.View.OrganizationName == organization.Name));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[OrganizationCustomize, BitAutoData]
|
||||
public async Task SendConfirmationsAsync_EmptyUserList_DoesNotSendEmail(
|
||||
Organization organization,
|
||||
SutProvider<SendOrganizationConfirmationCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
organization.PlanType = PlanType.EnterpriseAnnually;
|
||||
organization.Name = "Test Enterprise Org";
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.SendConfirmationsAsync(organization, [], false);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IMailer>().DidNotReceive()
|
||||
.SendEmail(Arg.Any<OrganizationConfirmationEnterpriseTeams>());
|
||||
await sutProvider.GetDependency<IMailer>().DidNotReceive()
|
||||
.SendEmail(Arg.Any<OrganizationConfirmationFamilyFree>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[OrganizationCustomize, BitAutoData]
|
||||
public async Task SendConfirmationAsync_HtmlEncodedOrganizationName_DecodesNameCorrectly(
|
||||
Organization organization,
|
||||
string userEmail,
|
||||
SutProvider<SendOrganizationConfirmationCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
organization.PlanType = PlanType.EnterpriseAnnually;
|
||||
organization.Name = "Test & Company";
|
||||
var expectedDecodedName = "Test & Company";
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.SendConfirmationAsync(organization, userEmail, false);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IMailer>().Received(1)
|
||||
.SendEmail(Arg.Is<OrganizationConfirmationEnterpriseTeams>(mail =>
|
||||
mail.View.OrganizationName == expectedDecodedName));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[OrganizationCustomize, BitAutoData]
|
||||
public async Task SendConfirmationAsync_AllEnterpriseTeamsPlanTypes_SendsEnterpriseTeamsEmail(
|
||||
Organization organization,
|
||||
string userEmail,
|
||||
SutProvider<SendOrganizationConfirmationCommand> sutProvider)
|
||||
{
|
||||
// Test all Enterprise and Teams plan types
|
||||
var enterpriseTeamsPlanTypes = new[]
|
||||
{
|
||||
PlanType.TeamsMonthly2019, PlanType.TeamsAnnually2019,
|
||||
PlanType.TeamsMonthly2020, PlanType.TeamsAnnually2020,
|
||||
PlanType.TeamsMonthly2023, PlanType.TeamsAnnually2023,
|
||||
PlanType.TeamsStarter2023, PlanType.TeamsMonthly,
|
||||
PlanType.TeamsAnnually, PlanType.TeamsStarter,
|
||||
PlanType.EnterpriseMonthly2019, PlanType.EnterpriseAnnually2019,
|
||||
PlanType.EnterpriseMonthly2020, PlanType.EnterpriseAnnually2020,
|
||||
PlanType.EnterpriseMonthly2023, PlanType.EnterpriseAnnually2023,
|
||||
PlanType.EnterpriseMonthly, PlanType.EnterpriseAnnually
|
||||
};
|
||||
|
||||
foreach (var planType in enterpriseTeamsPlanTypes)
|
||||
{
|
||||
// Arrange
|
||||
organization.PlanType = planType;
|
||||
organization.Name = "Test Org";
|
||||
sutProvider.GetDependency<IMailer>().ClearReceivedCalls();
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.SendConfirmationAsync(organization, userEmail, false);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IMailer>().Received(1)
|
||||
.SendEmail(Arg.Any<OrganizationConfirmationEnterpriseTeams>());
|
||||
await sutProvider.GetDependency<IMailer>().DidNotReceive()
|
||||
.SendEmail(Arg.Any<OrganizationConfirmationFamilyFree>());
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[OrganizationCustomize, BitAutoData]
|
||||
public async Task SendConfirmationAsync_AllFamilyFreePlanTypes_SendsFamilyFreeEmail(
|
||||
Organization organization,
|
||||
string userEmail,
|
||||
SutProvider<SendOrganizationConfirmationCommand> sutProvider)
|
||||
{
|
||||
// Test all Family, Free, and Custom plan types
|
||||
var familyFreePlanTypes = new[]
|
||||
{
|
||||
PlanType.Free, PlanType.FamiliesAnnually2019,
|
||||
PlanType.FamiliesAnnually2025, PlanType.FamiliesAnnually,
|
||||
PlanType.Custom
|
||||
};
|
||||
|
||||
foreach (var planType in familyFreePlanTypes)
|
||||
{
|
||||
// Arrange
|
||||
organization.PlanType = planType;
|
||||
organization.Name = "Test Org";
|
||||
sutProvider.GetDependency<IMailer>().ClearReceivedCalls();
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.SendConfirmationAsync(organization, userEmail, false);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IMailer>().Received(1)
|
||||
.SendEmail(Arg.Any<OrganizationConfirmationFamilyFree>());
|
||||
await sutProvider.GetDependency<IMailer>().DidNotReceive()
|
||||
.SendEmail(Arg.Any<OrganizationConfirmationEnterpriseTeams>());
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetSubject(string organizationName) => $"You Have Been Confirmed To {organizationName}";
|
||||
|
||||
}
|
||||
@@ -9,5 +9,5 @@ public class TestMailView : BaseMailView
|
||||
|
||||
public class TestMail : BaseMail<TestMailView>
|
||||
{
|
||||
public override string Subject { get; } = "Test Email";
|
||||
public override string Subject { get; set; } = "Test Email";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user