mirror of
https://github.com/bitwarden/server.git
synced 2024-11-21 12:05:42 +01:00
[PM-13346] Email notification impacts (#5027)
* Changes for the email notification Signed-off-by: Cy Okeke <cokeke@bitwarden.com> * Remove Get SponsoringSponsoredEmailAsync method Signed-off-by: Cy Okeke <cokeke@bitwarden.com> * Remove unused policyRepository referrence Signed-off-by: Cy Okeke <cokeke@bitwarden.com> * Removed unused OrganizationSponsorshipResponse * Rollback unrelated code changes Signed-off-by: Cy Okeke <cokeke@bitwarden.com> * Resolve the failing test Signed-off-by: Cy Okeke <cokeke@bitwarden.com> * Method to get policy status without login Signed-off-by: Cy Okeke <cokeke@bitwarden.com> * Refactor the email notification Signed-off-by: Cy Okeke <cokeke@bitwarden.com> * Remove unused property Signed-off-by: Cy Okeke <cokeke@bitwarden.com> * Remove unused property Signed-off-by: Cy Okeke <cokeke@bitwarden.com> * Fix line spacing Signed-off-by: Cy Okeke <cokeke@bitwarden.com> * remove extra line Signed-off-by: Cy Okeke <cokeke@bitwarden.com> * Refactor base on the pr review * Remove the unused interface Signed-off-by: Cy Okeke <cokeke@bitwarden.com> * Add changes for error message for disable policy Signed-off-by: Cy Okeke <cokeke@bitwarden.com> --------- Signed-off-by: Cy Okeke <cokeke@bitwarden.com>
This commit is contained in:
parent
b2b0f1e70e
commit
c76d615fad
@ -1,6 +1,9 @@
|
||||
using Bit.Api.Models.Request.Organizations;
|
||||
using Bit.Api.Models.Response.Organizations;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationConnections.Interfaces;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
@ -31,6 +34,8 @@ public class OrganizationSponsorshipsController : Controller
|
||||
private readonly ICloudSyncSponsorshipsCommand _syncSponsorshipsCommand;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly IUserService _userService;
|
||||
private readonly IPolicyRepository _policyRepository;
|
||||
private readonly IFeatureService _featureService;
|
||||
|
||||
public OrganizationSponsorshipsController(
|
||||
IOrganizationSponsorshipRepository organizationSponsorshipRepository,
|
||||
@ -45,7 +50,9 @@ public class OrganizationSponsorshipsController : Controller
|
||||
IRemoveSponsorshipCommand removeSponsorshipCommand,
|
||||
ICloudSyncSponsorshipsCommand syncSponsorshipsCommand,
|
||||
IUserService userService,
|
||||
ICurrentContext currentContext)
|
||||
ICurrentContext currentContext,
|
||||
IPolicyRepository policyRepository,
|
||||
IFeatureService featureService)
|
||||
{
|
||||
_organizationSponsorshipRepository = organizationSponsorshipRepository;
|
||||
_organizationRepository = organizationRepository;
|
||||
@ -60,6 +67,8 @@ public class OrganizationSponsorshipsController : Controller
|
||||
_syncSponsorshipsCommand = syncSponsorshipsCommand;
|
||||
_userService = userService;
|
||||
_currentContext = currentContext;
|
||||
_policyRepository = policyRepository;
|
||||
_featureService = featureService;
|
||||
}
|
||||
|
||||
[Authorize("Application")]
|
||||
@ -94,9 +103,20 @@ public class OrganizationSponsorshipsController : Controller
|
||||
[Authorize("Application")]
|
||||
[HttpPost("validate-token")]
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public async Task<bool> PreValidateSponsorshipToken([FromQuery] string sponsorshipToken)
|
||||
public async Task<PreValidateSponsorshipResponseModel> PreValidateSponsorshipToken([FromQuery] string sponsorshipToken)
|
||||
{
|
||||
return (await _validateRedemptionTokenCommand.ValidateRedemptionTokenAsync(sponsorshipToken, (await CurrentUser).Email)).valid;
|
||||
var isFreeFamilyPolicyEnabled = false;
|
||||
var (isValid, sponsorship) = await _validateRedemptionTokenCommand.ValidateRedemptionTokenAsync(sponsorshipToken, (await CurrentUser).Email);
|
||||
if (isValid && _featureService.IsEnabled(FeatureFlagKeys.DisableFreeFamiliesSponsorship) && sponsorship.SponsoringOrganizationId.HasValue)
|
||||
{
|
||||
var policy = await _policyRepository.GetByOrganizationIdTypeAsync(sponsorship.SponsoringOrganizationId.Value,
|
||||
PolicyType.FreeFamiliesSponsorshipPolicy);
|
||||
isFreeFamilyPolicyEnabled = policy?.Enabled ?? false;
|
||||
}
|
||||
|
||||
var response = PreValidateSponsorshipResponseModel.From(isValid, isFreeFamilyPolicyEnabled);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
[Authorize("Application")]
|
||||
|
@ -15,6 +15,7 @@ public enum PolicyType : byte
|
||||
DisablePersonalVaultExport = 10,
|
||||
ActivateAutofill = 11,
|
||||
AutomaticAppLogIn = 12,
|
||||
FreeFamiliesSponsorshipPolicy = 13
|
||||
}
|
||||
|
||||
public static class PolicyTypeExtensions
|
||||
@ -40,6 +41,7 @@ public static class PolicyTypeExtensions
|
||||
PolicyType.DisablePersonalVaultExport => "Remove individual vault export",
|
||||
PolicyType.ActivateAutofill => "Active auto-fill",
|
||||
PolicyType.AutomaticAppLogIn => "Automatically log in users for allowed applications",
|
||||
PolicyType.FreeFamiliesSponsorshipPolicy => "Remove Free Bitwarden Families sponsorship"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -18,5 +18,6 @@ public static class PolicyServiceCollectionExtensions
|
||||
services.AddScoped<IPolicyValidator, RequireSsoPolicyValidator>();
|
||||
services.AddScoped<IPolicyValidator, ResetPasswordPolicyValidator>();
|
||||
services.AddScoped<IPolicyValidator, MaximumVaultTimeoutPolicyValidator>();
|
||||
services.AddScoped<IPolicyValidator, FreeFamiliesForEnterprisePolicyValidator>();
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,46 @@
|
||||
#nullable enable
|
||||
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;
|
||||
|
||||
public class FreeFamiliesForEnterprisePolicyValidator(
|
||||
IOrganizationSponsorshipRepository organizationSponsorshipRepository,
|
||||
IMailService mailService,
|
||||
IOrganizationRepository organizationRepository)
|
||||
: IPolicyValidator
|
||||
{
|
||||
public PolicyType Type => PolicyType.FreeFamiliesSponsorshipPolicy;
|
||||
public IEnumerable<PolicyType> RequiredPolicies => [];
|
||||
|
||||
public async Task OnSaveSideEffectsAsync(PolicyUpdate policyUpdate, Policy? currentPolicy)
|
||||
{
|
||||
if (currentPolicy is not { Enabled: true } && policyUpdate is { Enabled: true })
|
||||
{
|
||||
await NotifiesUserWithApplicablePoliciesAsync(policyUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task NotifiesUserWithApplicablePoliciesAsync(PolicyUpdate policy)
|
||||
{
|
||||
var organizationSponsorships = (await organizationSponsorshipRepository.GetManyBySponsoringOrganizationAsync(policy.OrganizationId))
|
||||
.Where(p => p.SponsoredOrganizationId is not null)
|
||||
.ToList();
|
||||
|
||||
var organization = await organizationRepository.GetByIdAsync(policy.OrganizationId);
|
||||
var organizationName = organization?.Name;
|
||||
|
||||
foreach (var org in organizationSponsorships)
|
||||
{
|
||||
var offerAcceptanceDate = org.ValidUntil!.Value.AddDays(-7).ToString("MM/dd/yyyy");
|
||||
await mailService.SendFamiliesForEnterpriseRemoveSponsorshipsEmailAsync(org.FriendlyName, offerAcceptanceDate,
|
||||
org.SponsoredOrganizationId.ToString(), organizationName);
|
||||
}
|
||||
}
|
||||
|
||||
public Task<string> ValidateAsync(PolicyUpdate policyUpdate, Policy? currentPolicy) => Task.FromResult("");
|
||||
}
|
@ -154,6 +154,7 @@ public static class FeatureFlagKeys
|
||||
public const string NewDeviceVerificationPermanentDismiss = "new-device-permanent-dismiss";
|
||||
public const string SecurityTasks = "security-tasks";
|
||||
public const string PM14401_ScaleMSPOnClientOrganizationUpdate = "PM-14401-scale-msp-on-client-organization-update";
|
||||
public const string DisableFreeFamiliesSponsorship = "PM-12274-disable-free-families-sponsorship";
|
||||
|
||||
public static List<string> GetAllKeys()
|
||||
{
|
||||
|
@ -0,0 +1,22 @@
|
||||
{{#>FullHtmlLayout}}
|
||||
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; box-sizing: border-box; line-height: 25px; -webkit-text-size-adjust: none;">
|
||||
<tr style="margin: 0; box-sizing: border-box; line-height: 25px; -webkit-text-size-adjust: none;">
|
||||
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top">
|
||||
{{SponsoringOrgName}} has removed the Free Bitwarden Families plan sponsorship.
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="margin: 0; box-sizing: border-box; line-height: 25px; -webkit-text-size-adjust: none;">
|
||||
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top">
|
||||
<strong>Here’s what that means:</strong></br>
|
||||
Your Free Bitwarden Families sponsorship will charge your stored payment method on {{OfferAcceptanceDate}}. To avoid any disruption in your service, please ensure your payment method on the <a target="_blank" clicktracking=off href="{{SubscriptionUrl}}" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #175DDC; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; text-decoration: underline;">Subscription page</a> is up to date.
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="margin: 0; box-sizing: border-box; line-height: 25px; -webkit-text-size-adjust: none;">
|
||||
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top">
|
||||
Contact your organization administrators for more information.
|
||||
<br style="margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;" />
|
||||
<br style="margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;" />
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{{/FullHtmlLayout}}
|
@ -0,0 +1,6 @@
|
||||
{{#>BasicTextLayout}}
|
||||
{{SponsoringOrgName}} has removed the Free Bitwarden Families plan sponsorship.
|
||||
Here’s what that means:
|
||||
Your Free Bitwarden Families sponsorship will charge your stored payment method on {{OfferAcceptanceDate}}. To avoid any disruption in your service, please ensure your payment method on the Subscription page is up to date. Or click the following link: {{{SubscriptionUrl}}}
|
||||
Contact your organization administrators for more information.
|
||||
{{/BasicTextLayout}}
|
@ -0,0 +1,9 @@
|
||||
namespace Bit.Core.Models.Api.Response.OrganizationSponsorships;
|
||||
|
||||
public record PreValidateSponsorshipResponseModel(
|
||||
bool IsTokenValid,
|
||||
bool IsFreeFamilyPolicyEnabled)
|
||||
{
|
||||
public static PreValidateSponsorshipResponseModel From(bool validToken, bool policyStatus)
|
||||
=> new(validToken, policyStatus);
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
namespace Bit.Core.Models.Mail.FamiliesForEnterprise;
|
||||
|
||||
public class FamiliesForEnterpriseRemoveOfferViewModel : BaseMailModel
|
||||
{
|
||||
public string SponsoringOrgName { get; set; }
|
||||
public string SponsoredOrganizationId { get; set; }
|
||||
public string OfferAcceptanceDate { get; set; }
|
||||
public string SubscriptionUrl =>
|
||||
$"{WebVaultUrl}/organizations/{SponsoredOrganizationId}/billing/subscription";
|
||||
}
|
@ -89,5 +89,7 @@ public interface IMailService
|
||||
Task SendInitiateDeletProviderEmailAsync(string email, Provider provider, string token);
|
||||
Task SendInitiateDeleteOrganzationEmailAsync(string email, Organization organization, string token);
|
||||
Task SendRequestSMAccessToAdminEmailAsync(IEnumerable<string> adminEmails, string organizationName, string userRequestingAccess, string emailContent);
|
||||
Task SendFamiliesForEnterpriseRemoveSponsorshipsEmailAsync(string email, string offerAcceptanceDate, string organizationId,
|
||||
string organizationName);
|
||||
}
|
||||
|
||||
|
@ -1095,6 +1095,22 @@ public class HandlebarsMailService : IMailService
|
||||
await _mailDeliveryService.SendEmailAsync(message);
|
||||
}
|
||||
|
||||
public async Task SendFamiliesForEnterpriseRemoveSponsorshipsEmailAsync(string email, string offerAcceptanceDate, string organizationId,
|
||||
string organizationName)
|
||||
{
|
||||
var message = CreateDefaultMessage("Removal of Free Bitwarden Families plan", email);
|
||||
var model = new FamiliesForEnterpriseRemoveOfferViewModel
|
||||
{
|
||||
SponsoredOrganizationId = organizationId,
|
||||
SponsoringOrgName = CoreHelpers.SanitizeForEmail(organizationName),
|
||||
OfferAcceptanceDate = offerAcceptanceDate,
|
||||
WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash
|
||||
};
|
||||
await AddMessageContentAsync(message, "FamiliesForEnterprise.FamiliesForEnterpriseRemovedFromFamilyUser", model);
|
||||
message.Category = "FamiliesForEnterpriseRemovedFromFamilyUser";
|
||||
await _mailDeliveryService.SendEmailAsync(message);
|
||||
}
|
||||
|
||||
private static string GetUserIdentifier(string email, string userName)
|
||||
{
|
||||
return string.IsNullOrEmpty(userName) ? email : CoreHelpers.SanitizeForEmail(userName, false);
|
||||
|
@ -296,5 +296,12 @@ public class NoopMailService : IMailService
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
public Task SendRequestSMAccessToAdminEmailAsync(IEnumerable<string> adminEmails, string organizationName, string userRequestingAccess, string emailContent) => throw new NotImplementedException();
|
||||
|
||||
public Task SendFamiliesForEnterpriseRemoveSponsorshipsEmailAsync(string email, string offerAcceptanceDate,
|
||||
string organizationId,
|
||||
string organizationName)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,75 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Test.AdminConsole.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class FreeFamiliesForEnterprisePolicyValidatorTests
|
||||
{
|
||||
[Theory, BitAutoData]
|
||||
public async Task OnSaveSideEffectsAsync_DoesNotNotifyUserWhenPolicyDisabled(
|
||||
Organization organization,
|
||||
List<OrganizationSponsorship> organizationSponsorships,
|
||||
[PolicyUpdate(PolicyType.FreeFamiliesSponsorshipPolicy)] PolicyUpdate policyUpdate,
|
||||
[Policy(PolicyType.FreeFamiliesSponsorshipPolicy, true)] Policy policy,
|
||||
SutProvider<FreeFamiliesForEnterprisePolicyValidator> sutProvider)
|
||||
{
|
||||
|
||||
policy.Enabled = true;
|
||||
policyUpdate.Enabled = false;
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetByIdAsync(policyUpdate.OrganizationId)
|
||||
.Returns(organization);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationSponsorshipRepository>()
|
||||
.GetManyBySponsoringOrganizationAsync(policyUpdate.OrganizationId)
|
||||
.Returns(organizationSponsorships);
|
||||
|
||||
await sutProvider.Sut.OnSaveSideEffectsAsync(policyUpdate, policy);
|
||||
|
||||
await sutProvider.GetDependency<IMailService>().DidNotReceive()
|
||||
.SendFamiliesForEnterpriseRemoveSponsorshipsEmailAsync(organizationSponsorships[0].FriendlyName, organizationSponsorships[0].ValidUntil.ToString(),
|
||||
organizationSponsorships[0].SponsoredOrganizationId.ToString(), organization.DisplayName());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task OnSaveSideEffectsAsync_DoesNotifyUserWhenPolicyDisabled(
|
||||
Organization organization,
|
||||
List<OrganizationSponsorship> organizationSponsorships,
|
||||
[PolicyUpdate(PolicyType.FreeFamiliesSponsorshipPolicy)] PolicyUpdate policyUpdate,
|
||||
[Policy(PolicyType.FreeFamiliesSponsorshipPolicy, true)] Policy policy,
|
||||
SutProvider<FreeFamiliesForEnterprisePolicyValidator> sutProvider)
|
||||
{
|
||||
|
||||
policy.Enabled = false;
|
||||
policyUpdate.Enabled = true;
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetByIdAsync(policyUpdate.OrganizationId)
|
||||
.Returns(organization);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationSponsorshipRepository>()
|
||||
.GetManyBySponsoringOrganizationAsync(policyUpdate.OrganizationId)
|
||||
.Returns(organizationSponsorships);
|
||||
// Act
|
||||
await sutProvider.Sut.OnSaveSideEffectsAsync(policyUpdate, policy);
|
||||
|
||||
// Assert
|
||||
var offerAcceptanceDate = organizationSponsorships[0].ValidUntil!.Value.AddDays(-7).ToString("MM/dd/yyyy");
|
||||
await sutProvider.GetDependency<IMailService>().Received(1)
|
||||
.SendFamiliesForEnterpriseRemoveSponsorshipsEmailAsync(organizationSponsorships[0].FriendlyName, offerAcceptanceDate,
|
||||
organizationSponsorships[0].SponsoredOrganizationId.ToString(), organization.Name);
|
||||
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user