Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PM-10317] Email Users For Org Claiming Domain #5094

Merged
merged 57 commits into from
Dec 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
0a4c88a
Revoking users when enabling single org and 2fa policies. Fixing tests.
jrmccannon Nov 7, 2024
ff61b83
Added migration.
jrmccannon Nov 7, 2024
49620c7
Wrote tests and fixed bugs found.
jrmccannon Nov 11, 2024
e7a9a4f
Patch build process
withinfocus Nov 11, 2024
8b534b0
Fixing tests.
jrmccannon Nov 11, 2024
6a36767
Added unit test around disabling the feature flag.
jrmccannon Nov 12, 2024
f2f2a62
Updated error message to be public and added test for validating the …
jrmccannon Nov 12, 2024
f009db0
formatting
jrmccannon Nov 12, 2024
595e4b9
Added some tests for single org policy validator.
jrmccannon Nov 12, 2024
d662fff
Merge branch 'main' into ac/jmccannon/pm-10319-revoke-nc-users
jrmccannon Nov 12, 2024
d64032c
Fix issues from merge.
jrmccannon Nov 12, 2024
abbd4f5
Added sending emails to revoked non-compliant users.
jrmccannon Nov 12, 2024
3917114
Fixing name. Adding two factor policy email.
jrmccannon Nov 13, 2024
da02c89
Send email when user has been revoked.
jrmccannon Nov 13, 2024
1c3e4d8
Correcting migration name.
jrmccannon Nov 13, 2024
35643c1
Fixing templates and logic issue in Revoke command.
jrmccannon Nov 13, 2024
d3172c0
Moving interface into its own file.
jrmccannon Nov 13, 2024
fe5af90
Correcting namespaces for email templates.
jrmccannon Nov 13, 2024
95bb1f4
correcting logic that would not allow normal users to revoke non owners.
jrmccannon Nov 13, 2024
eb693a8
Actually correcting the test and logic.
jrmccannon Nov 13, 2024
58702d2
dotnet format. Added exec to bottom of bulk sproc
jrmccannon Nov 13, 2024
47cc545
Merge branch 'main' into ac/jmccannon/pm-10319-revoke-nc-users
jrmccannon Nov 14, 2024
9427b9f
Update src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/R…
jrmccannon Nov 15, 2024
99f98f2
Updated OrgIds to be a json string
jrmccannon Nov 15, 2024
78cd03d
Fixing errors.
jrmccannon Nov 15, 2024
436bebd
Updating test
jrmccannon Nov 15, 2024
8b89ad6
Moving command result.
jrmccannon Nov 15, 2024
0014d5c
Formatting and request rename
jrmccannon Nov 15, 2024
82b65e0
Realized this would throw a null error from the system domain verific…
jrmccannon Nov 15, 2024
51d0b12
Code review changes
jrmccannon Nov 19, 2024
d4bd369
Removing todos
jrmccannon Nov 19, 2024
1d77345
Corrected test name.
jrmccannon Nov 19, 2024
37ca7ff
Syncing filename to record name.
jrmccannon Nov 20, 2024
d0dd776
Fixing up the tests.
jrmccannon Nov 20, 2024
273754a
Added happy path test
jrmccannon Nov 20, 2024
2f3da19
Naming corrections. And corrected EF query.
jrmccannon Nov 20, 2024
1e7c17f
added check against event service
jrmccannon Nov 20, 2024
66d5831
Merge branch 'main' into ac/jmccannon/pm-10319-revoke-nc-users
withinfocus Nov 21, 2024
772b057
Code review changes.
jrmccannon Nov 22, 2024
0253c6f
Merge branch 'main' into ac/jmccannon/pm-10319-revoke-nc-users
jrmccannon Nov 22, 2024
8669e90
Fixing tests.
jrmccannon Nov 22, 2024
d485305
splitting up tests
jrmccannon Nov 26, 2024
68cdc34
Added templates and email side effect for claiming a domain.
jrmccannon Nov 15, 2024
eb79696
bringing changes from nc user changes.
jrmccannon Nov 15, 2024
1d321e4
Switched to enqueue mail message.
jrmccannon Nov 15, 2024
2759afd
Filled in DomainClaimedByOrganization.html.hbs
jrmccannon Nov 18, 2024
baab617
Added text document for domain claiming
jrmccannon Nov 19, 2024
ceaf0f3
Merge branch 'main' into ac/jmccannon/pm-10317-email-user-domain-veri…
jrmccannon Nov 26, 2024
934438a
Fixing migration script.
jrmccannon Nov 27, 2024
11c4de3
Merge branch 'jmccannon/pm-10319-fix-migration-script' into ac/jmccan…
jrmccannon Nov 27, 2024
768a833
Remove old sproc
jrmccannon Nov 27, 2024
e0909e2
Limiting sending of the email down to users who are a part of the dom…
jrmccannon Nov 27, 2024
efa1604
Added test for change
jrmccannon Nov 27, 2024
8db1c25
Renames and fixed up email.
jrmccannon Nov 27, 2024
78f1159
Fixing up CSS
jrmccannon Nov 27, 2024
e5e9c45
Merge branch 'main' into ac/jmccannon/pm-10317-email-user-domain-veri…
jrmccannon Dec 3, 2024
0b2dfc2
Merge branch 'main' into ac/jmccannon/pm-10317-email-user-domain-veri…
r-tome Dec 5, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Data.Organizations;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;
Expand All @@ -22,11 +23,12 @@ public class VerifyOrganizationDomainCommand(
IFeatureService featureService,
ICurrentContext currentContext,
ISavePolicyCommand savePolicyCommand,
IMailService mailService,
IOrganizationUserRepository organizationUserRepository,
IOrganizationRepository organizationRepository,
ILogger<VerifyOrganizationDomainCommand> logger)
: IVerifyOrganizationDomainCommand
{


public async Task<OrganizationDomain> UserVerifyOrganizationDomainAsync(OrganizationDomain organizationDomain)
{
if (currentContext.UserId is null)
Expand Down Expand Up @@ -109,7 +111,7 @@ private async Task<OrganizationDomain> VerifyOrganizationDomainAsync(Organizatio
{
domain.SetVerifiedDate();

await EnableSingleOrganizationPolicyAsync(domain.OrganizationId, actingUser);
await DomainVerificationSideEffectsAsync(domain, actingUser);
}
}
catch (Exception e)
Expand All @@ -121,19 +123,37 @@ private async Task<OrganizationDomain> VerifyOrganizationDomainAsync(Organizatio
return domain;
}

private async Task EnableSingleOrganizationPolicyAsync(Guid organizationId, IActingUser actingUser)
private async Task DomainVerificationSideEffectsAsync(OrganizationDomain domain, IActingUser actingUser)
{
if (featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning))
{
var policyUpdate = new PolicyUpdate
await EnableSingleOrganizationPolicyAsync(domain.OrganizationId, actingUser);
await SendVerifiedDomainUserEmailAsync(domain);
}
}

private async Task EnableSingleOrganizationPolicyAsync(Guid organizationId, IActingUser actingUser) =>
await savePolicyCommand.SaveAsync(
new PolicyUpdate
{
OrganizationId = organizationId,
Type = PolicyType.SingleOrg,
Enabled = true,
PerformedBy = actingUser
};
});

await savePolicyCommand.SaveAsync(policyUpdate);
}
private async Task SendVerifiedDomainUserEmailAsync(OrganizationDomain domain)
{
var orgUserUsers = await organizationUserRepository.GetManyDetailsByOrganizationAsync(domain.OrganizationId);

var domainUserEmails = orgUserUsers
.Where(ou => ou.Email.ToLower().EndsWith($"@{domain.DomainName.ToLower()}") &&
ou.Status != OrganizationUserStatusType.Revoked &&
ou.Status != OrganizationUserStatusType.Invited)
.Select(ou => ou.Email);

var organization = await organizationRepository.GetByIdAsync(domain.OrganizationId);

await mailService.SendClaimedDomainUserEmailAsync(new ManagedUserDomainClaimedEmails(domainUserEmails, organization));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{{#>TitleContactUsHtmlLayout}}
<table width="100%" border="0" cellpadding="0" cellspacing="0" style="display: table; width:100%; padding: 30px; text-align: left;" align="center">
<tr>
<td display="display: table-cell">
As a member of {{OrganizationName}}, your Bitwarden account is claimed and owned by your organization.
</td>
</tr>
<tr>
<td style="font-family:'Helvetica Neue', Helvetica, Arial, sans-serif; font-style: normal; font-weight: 400; font-size: 16px; line-height: 24px; margin-top: 30px; margin-bottom: 25px; margin-left: 35px; margin-right: 35px;">
<b>Here's what that means:</b>
<ul>
<li>This account should only be used to store items related to {{OrganizationName}}</li>
<li>Admins managing your Bitwarden organization manage your email address and other account settings</li>
<li>Admins can also revoke or delete your account at any time</li>
</ul>
</td>
</tr>
<tr>
<td style="font-family:'Helvetica Neue', Helvetica, Arial, sans-serif; font-style: normal; font-weight: 400; font-size: 16px; line-height: 24px; margin-top: 30px; margin-bottom: 25px; margin-left: 35px; margin-right: 35px;">
For more information, please refer to the following help article: <a href="https://bitwarden.com/help/claimed-accounts">Claimed Accounts</a>
</td>
</tr>
</table>
{{/TitleContactUsHtmlLayout}}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
As a member of {{OrganizationName}}, your Bitwarden account is claimed and owned by your organization.

Here's what that means:
- This account should only be used to store items related to {{OrganizationName}}
- Your admins managing your Bitwarden organization manages your email address and other account settings
- Your admins can also revoke or delete your account at any time

For more information, please refer to the following help article: Claimed Accounts (https://bitwarden.com/help/claimed-accounts)
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
using Bit.Core.AdminConsole.Entities;

namespace Bit.Core.Models.Data.Organizations;

public record ManagedUserDomainClaimedEmails(IEnumerable<string> EmailList, Organization Organization);
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Bit.Core.Models.Mail;

public class ClaimedDomainUserNotificationViewModel : BaseTitleContactUsMailModel
{
public string OrganizationName { get; init; }

Check warning on line 5 in src/Core/Models/Mail/ClaimedDomainUserNotificationViewModel.cs

View check run for this annotation

Codecov / codecov/patch

src/Core/Models/Mail/ClaimedDomainUserNotificationViewModel.cs#L5

Added line #L5 was not covered by tests
}
2 changes: 2 additions & 0 deletions src/Core/Services/IMailService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using Bit.Core.Auth.Entities;
using Bit.Core.Billing.Enums;
using Bit.Core.Entities;
using Bit.Core.Models.Data.Organizations;
using Bit.Core.Models.Mail;

namespace Bit.Core.Services;
Expand Down Expand Up @@ -93,5 +94,6 @@ Task SendProviderUpdatePaymentMethod(
Task SendRequestSMAccessToAdminEmailAsync(IEnumerable<string> adminEmails, string organizationName, string userRequestingAccess, string emailContent);
Task SendFamiliesForEnterpriseRemoveSponsorshipsEmailAsync(string email, string offerAcceptanceDate, string organizationId,
string organizationName);
Task SendClaimedDomainUserEmailAsync(ManagedUserDomainClaimedEmails emailList);
}

17 changes: 17 additions & 0 deletions src/Core/Services/Implementations/HandlebarsMailService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Models.Mail;
using Bit.Core.Entities;
using Bit.Core.Models.Data.Organizations;
using Bit.Core.Models.Mail;
using Bit.Core.Models.Mail.FamiliesForEnterprise;
using Bit.Core.Models.Mail.Provider;
Expand Down Expand Up @@ -460,6 +461,22 @@
await _mailDeliveryService.SendEmailAsync(message);
}

public async Task SendClaimedDomainUserEmailAsync(ManagedUserDomainClaimedEmails emailList)
{
await EnqueueMailAsync(emailList.EmailList.Select(email =>
CreateMessage(email, emailList.Organization)));
return;

Check warning on line 468 in src/Core/Services/Implementations/HandlebarsMailService.cs

View check run for this annotation

Codecov / codecov/patch

src/Core/Services/Implementations/HandlebarsMailService.cs#L465-L468

Added lines #L465 - L468 were not covered by tests

MailQueueMessage CreateMessage(string emailAddress, Organization org) =>
new(CreateDefaultMessage($"Your Bitwarden account is claimed by {org.DisplayName()}", emailAddress),
"AdminConsole.DomainClaimedByOrganization",
new ClaimedDomainUserNotificationViewModel
{
TitleFirst = $"Hey {emailAddress}, here is a heads up on your claimed account:",
OrganizationName = CoreHelpers.SanitizeForEmail(org.DisplayName(), false)
});
}

Check warning on line 478 in src/Core/Services/Implementations/HandlebarsMailService.cs

View check run for this annotation

Codecov / codecov/patch

src/Core/Services/Implementations/HandlebarsMailService.cs#L471-L478

Added lines #L471 - L478 were not covered by tests

public async Task SendNewDeviceLoggedInEmail(string email, string deviceType, DateTime timestamp, string ip)
{
var message = CreateDefaultMessage($"New Device Logged In From {deviceType}", email);
Expand Down
2 changes: 2 additions & 0 deletions src/Core/Services/NoopImplementations/NoopMailService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using Bit.Core.Auth.Entities;
using Bit.Core.Billing.Enums;
using Bit.Core.Entities;
using Bit.Core.Models.Data.Organizations;
using Bit.Core.Models.Mail;

namespace Bit.Core.Services;
Expand Down Expand Up @@ -309,5 +310,6 @@
{
return Task.FromResult(0);
}
public Task SendClaimedDomainUserEmailAsync(ManagedUserDomainClaimedEmails emailList) => Task.CompletedTask;

Check warning on line 313 in src/Core/Services/NoopImplementations/NoopMailService.cs

View check run for this annotation

Codecov / codecov/patch

src/Core/Services/NoopImplementations/NoopMailService.cs#L313

Added line #L313 was not covered by tests
}

Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Data;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
Expand All @@ -7,6 +8,8 @@
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Data.Organizations;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Test.Common.AutoFixture;
Expand Down Expand Up @@ -269,4 +272,53 @@ await sutProvider.GetDependency<ISavePolicyCommand>()
.DidNotReceive()
.SaveAsync(Arg.Any<PolicyUpdate>());
}

[Theory, BitAutoData]
public async Task UserVerifyOrganizationDomainAsync_GivenOrganizationDomainWithAccountDeprovisioningEnabled_WhenDomainIsVerified_ThenEmailShouldBeSentToUsersWhoBelongToTheDomain(
ICollection<OrganizationUserUserDetails> organizationUsers,
OrganizationDomain domain,
Organization organization,
SutProvider<VerifyOrganizationDomainCommand> sutProvider)
{
foreach (var organizationUser in organizationUsers)
{
organizationUser.Email = $"{organizationUser.Name}@{domain.DomainName}";
}

var mockedUsers = organizationUsers
.Where(x => x.Status != OrganizationUserStatusType.Invited &&
x.Status != OrganizationUserStatusType.Revoked).ToList();

organization.Id = domain.OrganizationId;

sutProvider.GetDependency<IOrganizationDomainRepository>()
.GetClaimedDomainsByDomainNameAsync(domain.DomainName)
.Returns([]);

sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(domain.OrganizationId)
.Returns(organization);

sutProvider.GetDependency<IDnsResolverService>()
.ResolveAsync(domain.DomainName, domain.Txt)
.Returns(true);

sutProvider.GetDependency<ICurrentContext>()
.UserId.Returns(Guid.NewGuid());

sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
.Returns(true);

sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyDetailsByOrganizationAsync(domain.OrganizationId)
.Returns(mockedUsers);

_ = await sutProvider.Sut.UserVerifyOrganizationDomainAsync(domain);

await sutProvider.GetDependency<IMailService>().Received().SendClaimedDomainUserEmailAsync(
Arg.Is<ManagedUserDomainClaimedEmails>(x =>
x.EmailList.Count(e => e.EndsWith(domain.DomainName)) == mockedUsers.Count &&
x.Organization.Id == organization.Id));
}
}
Loading