From 5ec37b96b4d07e506c04225af989a09de740b75b Mon Sep 17 00:00:00 2001 From: Addison Beck Date: Fri, 16 Jul 2021 13:49:27 -0400 Subject: [PATCH] Organization User Accepted Invite Email Notifications (#1465) --- .../OrganizationUserAccepted.html.hbs | 12 ++++++++--- .../OrganizationUserAccepted.text.hbs | 12 +++++++---- .../Mail/OrganizationUserAcceptedViewModel.cs | 7 +++++-- .../OrganizationUserRepository.cs | 16 ++++++++++++++ .../IOrganizationUserRepository.cs | 1 + .../SqlServer/OrganizationUserRepository.cs | 13 ++++++++++++ src/Core/Services/IMailService.cs | 2 +- .../Implementations/HandlebarsMailService.cs | 9 ++++---- .../Implementations/OrganizationService.cs | 5 ++++- .../NoopImplementations/NoopMailService.cs | 2 +- .../OrganizationUser_ReadByMinimumRole.sql | 15 +++++++++++++ ...5_00_OrganizationUserReadByMinimumRole.sql | 21 +++++++++++++++++++ 12 files changed, 99 insertions(+), 16 deletions(-) create mode 100644 src/Sql/dbo/Stored Procedures/OrganizationUser_ReadByMinimumRole.sql create mode 100644 util/Migrator/DbScripts/2021-07-15_00_OrganizationUserReadByMinimumRole.sql diff --git a/src/Core/MailTemplates/Handlebars/OrganizationUserAccepted.html.hbs b/src/Core/MailTemplates/Handlebars/OrganizationUserAccepted.html.hbs index bd8cc6b8a3..66588e92ac 100644 --- a/src/Core/MailTemplates/Handlebars/OrganizationUserAccepted.html.hbs +++ b/src/Core/MailTemplates/Handlebars/OrganizationUserAccepted.html.hbs @@ -2,17 +2,23 @@
- This email is to notify you that {{UserEmail}} has accepted your invitation to join {{OrganizationName}}. + {{UserIdentifier}} needs to be confirmed to {{OrganizationName}} before they can access the organization vault.
- To confirm this user, log into the Bitwarden web vault, manage your organization "People", and confirm the user. + To confirm users into your organization: +
    +
  1. Log in to your Web Vault and open your Organization.
  2. +
  3. Open the Manage tab and select People from the left-hand menu.
  4. +
  5. Hover over the Accepted user and select the gear dropdown.
  6. +
  7. Select Confirm.
  8. +
- If you do not wish to confirm this user, you can also remove them from the organization on the same page. + For more information, please refer to the following help article: https://bitwarden.com/help/article/managing-users/#confirm
diff --git a/src/Core/MailTemplates/Handlebars/OrganizationUserAccepted.text.hbs b/src/Core/MailTemplates/Handlebars/OrganizationUserAccepted.text.hbs index 188172cfa7..1e1918fe5f 100644 --- a/src/Core/MailTemplates/Handlebars/OrganizationUserAccepted.text.hbs +++ b/src/Core/MailTemplates/Handlebars/OrganizationUserAccepted.text.hbs @@ -1,7 +1,11 @@ {{#>BasicTextLayout}} -This email is to notify you that {{UserEmail}} has accepted your invitation to join {{OrganizationName}}. +{{UserIdentifier}} needs to be confirmed to {{OrganizationName}} before they can access the organization vault. -To confirm this user, log into the Bitwarden web vault, manage your organization "People" and confirm the user. +To confirm users into your organization: +1. Log in to your Web Vault and open your Organization. +2. Open the Manage tab and select People from the left-hand menu. +3. Hover over the Accepted user and select the grear dropdown. +4. Select Confirm. -If you do not wish to confirm this user, you can also remove them from the organization on the same page. -{{/BasicTextLayout}} \ No newline at end of file +For more information, please refer to the following help article: https://bitwarden.com/help/article/managing-user/#confirm +{{/BasicTextLayout}} diff --git a/src/Core/Models/Mail/OrganizationUserAcceptedViewModel.cs b/src/Core/Models/Mail/OrganizationUserAcceptedViewModel.cs index f90b5e2304..c3812367ae 100644 --- a/src/Core/Models/Mail/OrganizationUserAcceptedViewModel.cs +++ b/src/Core/Models/Mail/OrganizationUserAcceptedViewModel.cs @@ -1,8 +1,11 @@ -namespace Bit.Core.Models.Mail +using System; + +namespace Bit.Core.Models.Mail { public class OrganizationUserAcceptedViewModel : BaseMailModel { + public Guid OrganizationId { get; set; } public string OrganizationName { get; set; } - public string UserEmail { get; set; } + public string UserIdentifier { get; set; } } } diff --git a/src/Core/Repositories/EntityFramework/OrganizationUserRepository.cs b/src/Core/Repositories/EntityFramework/OrganizationUserRepository.cs index 5175cb27eb..52f67a2de1 100644 --- a/src/Core/Repositories/EntityFramework/OrganizationUserRepository.cs +++ b/src/Core/Repositories/EntityFramework/OrganizationUserRepository.cs @@ -389,5 +389,21 @@ namespace Bit.Core.Repositories.EntityFramework } Task> IOrganizationUserRepository.SelectKnownEmailsAsync(Guid organizationId, IEnumerable emails, bool onlyRegisteredUsers) => throw new NotImplementedException(); + + public async Task> GetManyByMinimumRoleAsync(Guid organizationId, OrganizationUserType minRole) + { + using (var scope = ServiceScopeFactory.CreateScope()) + { + var dbContext = GetDatabaseContext(scope); + var query = dbContext.OrganizationUsers + .Include(e => e.User) + .Where(e => e.OrganizationId.Equals(organizationId) && e.Type <= minRole) + .Select(e => new OrganizationUserUserDetails() { + Id = e.Id, + Email = e.Email ?? e.User.Email + }); + return await query.ToListAsync(); + } + } } } diff --git a/src/Core/Repositories/IOrganizationUserRepository.cs b/src/Core/Repositories/IOrganizationUserRepository.cs index 19515a2428..2d00ca45cd 100644 --- a/src/Core/Repositories/IOrganizationUserRepository.cs +++ b/src/Core/Repositories/IOrganizationUserRepository.cs @@ -37,5 +37,6 @@ namespace Bit.Core.Repositories Task DeleteManyAsync(IEnumerable userIds); Task GetByOrganizationEmailAsync(Guid organizationId, string email); Task> GetManyPublicKeysByOrganizationUserAsync(Guid organizationId, IEnumerable Ids); + Task> GetManyByMinimumRoleAsync(Guid organizationId, OrganizationUserType minRole); } } diff --git a/src/Core/Repositories/SqlServer/OrganizationUserRepository.cs b/src/Core/Repositories/SqlServer/OrganizationUserRepository.cs index 3fb672708f..dee1eeb218 100644 --- a/src/Core/Repositories/SqlServer/OrganizationUserRepository.cs +++ b/src/Core/Repositories/SqlServer/OrganizationUserRepository.cs @@ -391,5 +391,18 @@ namespace Bit.Core.Repositories.SqlServer return results.ToList(); } } + + public async Task> GetManyByMinimumRoleAsync(Guid organizationId, OrganizationUserType minRole) + { + using (var connection = new SqlConnection(ConnectionString)) + { + var results = await connection.QueryAsync( + "[dbo].[OrganizationUser_ReadByMinimumRole]", + new { OrganizationId = organizationId, MinRole = minRole }, + commandType: CommandType.StoredProcedure); + + return results.ToList(); + } + } } } diff --git a/src/Core/Services/IMailService.cs b/src/Core/Services/IMailService.cs index afbae18a75..43b4426f65 100644 --- a/src/Core/Services/IMailService.cs +++ b/src/Core/Services/IMailService.cs @@ -19,7 +19,7 @@ namespace Bit.Core.Services Task SendMasterPasswordHintEmailAsync(string email, string hint); Task SendOrganizationInviteEmailAsync(string organizationName, OrganizationUser orgUser, string token); Task BulkSendOrganizationInviteEmailAsync(string organizationName, IEnumerable<(OrganizationUser orgUser, string token)> invites); - Task SendOrganizationAcceptedEmailAsync(string organizationName, string userEmail, + Task SendOrganizationAcceptedEmailAsync(Organization organization, string userIdentifier, IEnumerable adminEmails); Task SendOrganizationConfirmedEmailAsync(string organizationName, string email); Task SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(string organizationName, string email); diff --git a/src/Core/Services/Implementations/HandlebarsMailService.cs b/src/Core/Services/Implementations/HandlebarsMailService.cs index 409dcb5623..f98a49d0d0 100644 --- a/src/Core/Services/Implementations/HandlebarsMailService.cs +++ b/src/Core/Services/Implementations/HandlebarsMailService.cs @@ -143,14 +143,15 @@ namespace Bit.Core.Services await _mailDeliveryService.SendEmailAsync(message); } - public async Task SendOrganizationAcceptedEmailAsync(string organizationName, string userEmail, + public async Task SendOrganizationAcceptedEmailAsync(Organization organization, string userIdentifier, IEnumerable adminEmails) { - var message = CreateDefaultMessage($"User {userEmail} Has Accepted Invite", adminEmails); + var message = CreateDefaultMessage($"Action Required: {userIdentifier} Needs to Be Confirmed", adminEmails); var model = new OrganizationUserAcceptedViewModel { - OrganizationName = CoreHelpers.SanitizeForEmail(organizationName), - UserEmail = userEmail, + OrganizationId = organization.Id, + OrganizationName = CoreHelpers.SanitizeForEmail(organization.Name), + UserIdentifier = userIdentifier, WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash, SiteName = _globalSettings.SiteName }; diff --git a/src/Core/Services/Implementations/OrganizationService.cs b/src/Core/Services/Implementations/OrganizationService.cs index 8e26646701..276c4c49c1 100644 --- a/src/Core/Services/Implementations/OrganizationService.cs +++ b/src/Core/Services/Implementations/OrganizationService.cs @@ -1424,7 +1424,10 @@ namespace Bit.Core.Services await _organizationUserRepository.ReplaceAsync(orgUser); - // TODO: send notification emails to org admins and accepting user? + await _mailService.SendOrganizationAcceptedEmailAsync( + (await _organizationRepository.GetByIdAsync(orgUser.OrganizationId)), + user.Email, + (await _organizationUserRepository.GetManyByMinimumRoleAsync(orgUser.OrganizationId, OrganizationUserType.Admin)).Select(a => a.Email).Distinct()); return orgUser; } diff --git a/src/Core/Services/NoopImplementations/NoopMailService.cs b/src/Core/Services/NoopImplementations/NoopMailService.cs index d7b17e78bf..37ab39cb3d 100644 --- a/src/Core/Services/NoopImplementations/NoopMailService.cs +++ b/src/Core/Services/NoopImplementations/NoopMailService.cs @@ -34,7 +34,7 @@ namespace Bit.Core.Services return Task.FromResult(0); } - public Task SendOrganizationAcceptedEmailAsync(string organizationName, string userEmail, IEnumerable adminEmails) + public Task SendOrganizationAcceptedEmailAsync(Organization organization, string userIdentifier, IEnumerable adminEmails) { return Task.FromResult(0); } diff --git a/src/Sql/dbo/Stored Procedures/OrganizationUser_ReadByMinimumRole.sql b/src/Sql/dbo/Stored Procedures/OrganizationUser_ReadByMinimumRole.sql new file mode 100644 index 0000000000..cd5889d6e5 --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/OrganizationUser_ReadByMinimumRole.sql @@ -0,0 +1,15 @@ +CREATE PROCEDURE [dbo].[OrganizationUser_ReadByMinimumRole] + @OrganizationId UNIQUEIDENTIFIER, + @MinRole TINYINT +AS +BEGIN + SET NOCOUNT ON + + SELECT + * + FROM + [dbo].[OrganizationUserUserDetailsView] + WHERE + OrganizationId = @OrganizationId + AND [Type] <= @MinRole +END diff --git a/util/Migrator/DbScripts/2021-07-15_00_OrganizationUserReadByMinimumRole.sql b/util/Migrator/DbScripts/2021-07-15_00_OrganizationUserReadByMinimumRole.sql new file mode 100644 index 0000000000..77779ec59a --- /dev/null +++ b/util/Migrator/DbScripts/2021-07-15_00_OrganizationUserReadByMinimumRole.sql @@ -0,0 +1,21 @@ +IF OBJECT_ID('[dbo].[OrganizationUser_ReadByMinimumRole]') IS NOT NULL +BEGIN + DROP PROCEDURE [dbo].[OrganizationUser_ReadByMinimumRole] +END +GO + +CREATE PROCEDURE [dbo].[OrganizationUser_ReadByMinimumRole] + @OrganizationId UNIQUEIDENTIFIER, + @MinRole TINYINT +AS +BEGIN + SET NOCOUNT ON + + SELECT + * + FROM + [dbo].[OrganizationUserUserDetailsView] + WHERE + OrganizationId = @OrganizationId + AND [Type] <= @MinRole +END