1
0
mirror of https://github.com/bitwarden/server.git synced 2025-04-05 21:18:13 -05:00

[PM-15637] Notify Custom Users with “Manage Account Recovery” permission for Device Approval Requests (#5359)

* Add stored procedure to read organization user details by role

* Add OrganizationUserRepository method to retrieve OrganizationUser details by role

* Enhance AuthRequestService to send notifications to custom users with ManageResetPassword permission

* Enhance AuthRequestServiceTests to include custom user permissions and validate notification email recipients
This commit is contained in:
Rui Tomé 2025-02-05 14:47:06 +00:00 committed by GitHub
parent 617bb5015f
commit 03c390de74
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 131 additions and 5 deletions

View File

@ -60,4 +60,12 @@ public interface IOrganizationUserRepository : IRepository<OrganizationUser, Gui
Task<ICollection<OrganizationUser>> GetManyByOrganizationWithClaimedDomainsAsync(Guid organizationId); Task<ICollection<OrganizationUser>> GetManyByOrganizationWithClaimedDomainsAsync(Guid organizationId);
Task RevokeManyByIdAsync(IEnumerable<Guid> organizationUserIds); Task RevokeManyByIdAsync(IEnumerable<Guid> organizationUserIds);
/// <summary>
/// Returns a list of OrganizationUsersUserDetails with the specified role.
/// </summary>
/// <param name="organizationId">The organization to search within</param>
/// <param name="role">The role to search for</param>
/// <returns>A list of OrganizationUsersUserDetails with the specified role</returns>
Task<IEnumerable<OrganizationUserUserDetails>> GetManyDetailsByRoleAsync(Guid organizationId, OrganizationUserType role);
} }

View File

@ -297,10 +297,34 @@ public class AuthRequestService : IAuthRequestService
return; return;
} }
var admins = await _organizationUserRepository.GetManyByMinimumRoleAsync( var adminEmails = await GetAdminAndAccountRecoveryEmailsAsync(organizationUser.OrganizationId);
await _mailService.SendDeviceApprovalRequestedNotificationEmailAsync(
adminEmails,
organizationUser.OrganizationId, organizationUser.OrganizationId,
user.Email,
user.Name);
}
/// <summary>
/// Returns a list of emails for admins and custom users with the ManageResetPassword permission.
/// </summary>
/// <param name="organizationId">The organization to search within</param>
private async Task<List<string>> GetAdminAndAccountRecoveryEmailsAsync(Guid organizationId)
{
var admins = await _organizationUserRepository.GetManyByMinimumRoleAsync(
organizationId,
OrganizationUserType.Admin); OrganizationUserType.Admin);
var adminEmails = admins.Select(a => a.Email).Distinct().ToList();
await _mailService.SendDeviceApprovalRequestedNotificationEmailAsync(adminEmails, organizationUser.OrganizationId, user.Email, user.Name); var customUsers = await _organizationUserRepository.GetManyDetailsByRoleAsync(
organizationId,
OrganizationUserType.Custom);
return admins.Select(a => a.Email)
.Concat(customUsers
.Where(a => a.GetPermissions().ManageResetPassword)
.Select(a => a.Email))
.Distinct()
.ToList();
} }
} }

View File

@ -567,4 +567,17 @@ public class OrganizationUserRepository : Repository<OrganizationUser, Guid>, IO
new { OrganizationUserIds = JsonSerializer.Serialize(organizationUserIds), Status = OrganizationUserStatusType.Revoked }, new { OrganizationUserIds = JsonSerializer.Serialize(organizationUserIds), Status = OrganizationUserStatusType.Revoked },
commandType: CommandType.StoredProcedure); commandType: CommandType.StoredProcedure);
} }
public async Task<IEnumerable<OrganizationUserUserDetails>> GetManyDetailsByRoleAsync(Guid organizationId, OrganizationUserType role)
{
using (var connection = new SqlConnection(ConnectionString))
{
var results = await connection.QueryAsync<OrganizationUserUserDetails>(
"[dbo].[OrganizationUser_ReadManyDetailsByRole]",
new { OrganizationId = organizationId, Role = role },
commandType: CommandType.StoredProcedure);
return results.ToList();
}
}
} }

View File

@ -733,4 +733,25 @@ public class OrganizationUserRepository : Repository<Core.Entities.OrganizationU
await dbContext.UserBumpAccountRevisionDateByOrganizationUserIdsAsync(organizationUserIds); await dbContext.UserBumpAccountRevisionDateByOrganizationUserIdsAsync(organizationUserIds);
} }
public async Task<IEnumerable<OrganizationUserUserDetails>> GetManyDetailsByRoleAsync(Guid organizationId, OrganizationUserType role)
{
using (var scope = ServiceScopeFactory.CreateScope())
{
var dbContext = GetDatabaseContext(scope);
var query = from ou in dbContext.OrganizationUsers
join u in dbContext.Users
on ou.UserId equals u.Id
where ou.OrganizationId == organizationId &&
ou.Type == role &&
ou.Status == OrganizationUserStatusType.Confirmed
select new OrganizationUserUserDetails
{
Id = ou.Id,
Email = ou.Email ?? u.Email,
Permissions = ou.Permissions
};
return await query.ToListAsync();
}
}
} }

View File

@ -0,0 +1,16 @@
CREATE PROCEDURE [dbo].[OrganizationUser_ReadManyDetailsByRole]
@OrganizationId UNIQUEIDENTIFIER,
@Role TINYINT
AS
BEGIN
SET NOCOUNT ON
SELECT
*
FROM
[dbo].[OrganizationUserUserDetailsView]
WHERE
OrganizationId = @OrganizationId
AND Status = 2 -- 2 = Confirmed
AND [Type] = @Role
END

View File

@ -7,11 +7,13 @@ using Bit.Core.Context;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Models.Data;
using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.Platform.Push; using Bit.Core.Platform.Push;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Settings; using Bit.Core.Settings;
using Bit.Core.Utilities;
using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.AutoFixture.Attributes;
using Bit.Test.Common.Helpers; using Bit.Test.Common.Helpers;
@ -347,14 +349,24 @@ public class AuthRequestServiceTests
User user, User user,
OrganizationUser organizationUser1, OrganizationUser organizationUser1,
OrganizationUserUserDetails admin1, OrganizationUserUserDetails admin1,
OrganizationUserUserDetails customUser1,
OrganizationUser organizationUser2, OrganizationUser organizationUser2,
OrganizationUserUserDetails admin2, OrganizationUserUserDetails admin2,
OrganizationUserUserDetails admin3) OrganizationUserUserDetails admin3,
OrganizationUserUserDetails customUser2)
{ {
createModel.Type = AuthRequestType.AdminApproval; createModel.Type = AuthRequestType.AdminApproval;
user.Email = createModel.Email; user.Email = createModel.Email;
organizationUser1.UserId = user.Id; organizationUser1.UserId = user.Id;
organizationUser2.UserId = user.Id; organizationUser2.UserId = user.Id;
customUser1.Permissions = CoreHelpers.ClassToJsonData(new Permissions
{
ManageResetPassword = false,
});
customUser2.Permissions = CoreHelpers.ClassToJsonData(new Permissions
{
ManageResetPassword = true,
});
sutProvider.GetDependency<IFeatureService>() sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.DeviceApprovalRequestAdminNotifications) .IsEnabled(FeatureFlagKeys.DeviceApprovalRequestAdminNotifications)
@ -392,6 +404,13 @@ public class AuthRequestServiceTests
admin1, admin1,
]); ]);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyDetailsByRoleAsync(organizationUser1.OrganizationId, OrganizationUserType.Custom)
.Returns(
[
customUser1,
]);
sutProvider.GetDependency<IOrganizationUserRepository>() sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyByMinimumRoleAsync(organizationUser2.OrganizationId, OrganizationUserType.Admin) .GetManyByMinimumRoleAsync(organizationUser2.OrganizationId, OrganizationUserType.Admin)
.Returns( .Returns(
@ -400,6 +419,13 @@ public class AuthRequestServiceTests
admin3, admin3,
]); ]);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyDetailsByRoleAsync(organizationUser2.OrganizationId, OrganizationUserType.Custom)
.Returns(
[
customUser2,
]);
sutProvider.GetDependency<IAuthRequestRepository>() sutProvider.GetDependency<IAuthRequestRepository>()
.CreateAsync(Arg.Any<AuthRequest>()) .CreateAsync(Arg.Any<AuthRequest>())
.Returns(c => c.ArgAt<AuthRequest>(0)); .Returns(c => c.ArgAt<AuthRequest>(0));
@ -435,7 +461,9 @@ public class AuthRequestServiceTests
await sutProvider.GetDependency<IMailService>() await sutProvider.GetDependency<IMailService>()
.Received(1) .Received(1)
.SendDeviceApprovalRequestedNotificationEmailAsync( .SendDeviceApprovalRequestedNotificationEmailAsync(
Arg.Is<IEnumerable<string>>(emails => emails.Count() == 2 && emails.Contains(admin2.Email) && emails.Contains(admin3.Email)), Arg.Is<IEnumerable<string>>(emails => emails.Count() == 3 &&
emails.Contains(admin2.Email) && emails.Contains(admin3.Email) &&
emails.Contains(customUser2.Email)),
organizationUser2.OrganizationId, organizationUser2.OrganizationId,
user.Email, user.Email,
user.Name); user.Name);

View File

@ -0,0 +1,16 @@
CREATE OR ALTER PROCEDURE [dbo].[OrganizationUser_ReadManyDetailsByRole]
@OrganizationId UNIQUEIDENTIFIER,
@Role TINYINT
AS
BEGIN
SET NOCOUNT ON
SELECT
*
FROM
[dbo].[OrganizationUserUserDetailsView]
WHERE
OrganizationId = @OrganizationId
AND Status = 2 -- 2 = Confirmed
AND [Type] = @Role
END