1
0
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:
cyprain-okeke 2024-11-19 17:37:01 +01:00 committed by GitHub
parent b2b0f1e70e
commit c76d615fad
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 220 additions and 3 deletions

View File

@ -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")]

View File

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

View File

@ -18,5 +18,6 @@ public static class PolicyServiceCollectionExtensions
services.AddScoped<IPolicyValidator, RequireSsoPolicyValidator>();
services.AddScoped<IPolicyValidator, ResetPasswordPolicyValidator>();
services.AddScoped<IPolicyValidator, MaximumVaultTimeoutPolicyValidator>();
services.AddScoped<IPolicyValidator, FreeFamiliesForEnterprisePolicyValidator>();
}
}

View File

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

View File

@ -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()
{

View File

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

View File

@ -0,0 +1,6 @@
{{#>BasicTextLayout}}
{{SponsoringOrgName}} has removed the Free Bitwarden Families plan sponsorship.
Heres 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}}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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