[PM-27882] Add SendOrganizationConfirmationCommand (#6743)

This commit is contained in:
Jimmy Vo
2026-01-06 16:43:36 -05:00
committed by GitHub
parent 530d946857
commit 63784e1f5f
24 changed files with 589 additions and 20 deletions

View File

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

View File

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

View File

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

View File

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