mirror of
https://github.com/bitwarden/server.git
synced 2025-05-20 11:04:31 -05:00
[PM-10317] Email Users For Org Claiming Domain (#5094)
* Revoking users when enabling single org and 2fa policies. Fixing tests. * Added migration. * Wrote tests and fixed bugs found. * Patch build process * Fixing tests. * Added unit test around disabling the feature flag. * Updated error message to be public and added test for validating the request. * formatting * Added some tests for single org policy validator. * Fix issues from merge. * Added sending emails to revoked non-compliant users. * Fixing name. Adding two factor policy email. * Send email when user has been revoked. * Correcting migration name. * Fixing templates and logic issue in Revoke command. * Moving interface into its own file. * Correcting namespaces for email templates. * correcting logic that would not allow normal users to revoke non owners. * Actually correcting the test and logic. * dotnet format. Added exec to bottom of bulk sproc * Update src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RevokeNonCompliantOrganizationUserCommand.cs Co-authored-by: Rui Tomé <108268980+r-tome@users.noreply.github.com> * Updated OrgIds to be a json string * Fixing errors. * Updating test * Moving command result. * Formatting and request rename * Realized this would throw a null error from the system domain verification. Adding unknown type to event system user. Adding optional parameter to SaveAsync in policy service in order to pass in event system user. * Code review changes * Removing todos * Corrected test name. * Syncing filename to record name. * Fixing up the tests. * Added happy path test * Naming corrections. And corrected EF query. * added check against event service * Code review changes. * Fixing tests. * splitting up tests * Added templates and email side effect for claiming a domain. * bringing changes from nc user changes. * Switched to enqueue mail message. * Filled in DomainClaimedByOrganization.html.hbs * Added text document for domain claiming * Fixing migration script. * Remove old sproc * Limiting sending of the email down to users who are a part of the domain being claimed. * Added test for change * Renames and fixed up email. * Fixing up CSS --------- Co-authored-by: Matt Bishop <mbishop@bitwarden.com> Co-authored-by: Rui Tomé <108268980+r-tome@users.noreply.github.com> Co-authored-by: Rui Tome <rtome@bitwarden.com>
This commit is contained in:
parent
04f9d7dd8e
commit
f471fffe42
@ -7,6 +7,7 @@ using Bit.Core.Context;
|
||||
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;
|
||||
@ -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)
|
||||
@ -109,7 +111,7 @@ public class VerifyOrganizationDomainCommand(
|
||||
{
|
||||
domain.SetVerifiedDate();
|
||||
|
||||
await EnableSingleOrganizationPolicyAsync(domain.OrganizationId, actingUser);
|
||||
await DomainVerificationSideEffectsAsync(domain, actingUser);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
@ -121,19 +123,37 @@ public class VerifyOrganizationDomainCommand(
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
@ -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}}
|
@ -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)
|
@ -0,0 +1,5 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
|
||||
namespace Bit.Core.Models.Data.Organizations;
|
||||
|
||||
public record ManagedUserDomainClaimedEmails(IEnumerable<string> EmailList, Organization Organization);
|
@ -0,0 +1,6 @@
|
||||
namespace Bit.Core.Models.Mail;
|
||||
|
||||
public class ClaimedDomainUserNotificationViewModel : BaseTitleContactUsMailModel
|
||||
{
|
||||
public string OrganizationName { get; init; }
|
||||
}
|
@ -3,6 +3,7 @@ using Bit.Core.AdminConsole.Entities.Provider;
|
||||
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;
|
||||
@ -93,5 +94,6 @@ public interface IMailService
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -7,6 +7,7 @@ using Bit.Core.Auth.Models.Mail;
|
||||
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;
|
||||
@ -460,6 +461,22 @@ public class HandlebarsMailService : IMailService
|
||||
await _mailDeliveryService.SendEmailAsync(message);
|
||||
}
|
||||
|
||||
public async Task SendClaimedDomainUserEmailAsync(ManagedUserDomainClaimedEmails emailList)
|
||||
{
|
||||
await EnqueueMailAsync(emailList.EmailList.Select(email =>
|
||||
CreateMessage(email, emailList.Organization)));
|
||||
return;
|
||||
|
||||
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)
|
||||
});
|
||||
}
|
||||
|
||||
public async Task SendNewDeviceLoggedInEmail(string email, string deviceType, DateTime timestamp, string ip)
|
||||
{
|
||||
var message = CreateDefaultMessage($"New Device Logged In From {deviceType}", email);
|
||||
|
@ -3,6 +3,7 @@ using Bit.Core.AdminConsole.Entities.Provider;
|
||||
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;
|
||||
@ -309,5 +310,6 @@ public class NoopMailService : IMailService
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
public Task SendClaimedDomainUserEmailAsync(ManagedUserDomainClaimedEmails emailList) => Task.CompletedTask;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
@ -7,6 +8,8 @@ using Bit.Core.Context;
|
||||
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;
|
||||
@ -269,4 +272,53 @@ public class VerifyOrganizationDomainCommandTests
|
||||
.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…
x
Reference in New Issue
Block a user