From 1cf9ff34c1cc36d0139dbb61e27b8b542bf6e659 Mon Sep 17 00:00:00 2001 From: Graham Walker Date: Mon, 7 Apr 2025 11:26:06 -0500 Subject: [PATCH] PM-17921 change the GenerateAccessData method to process lists in parallel (#5552) * PM-17921 change the GenerateAccessData method to process lists in parallel. * PM-17921 removing old method --- .../MemberAccessCipherDetailsQuery.cs | 128 +++++++++--------- 1 file changed, 63 insertions(+), 65 deletions(-) diff --git a/src/Core/Tools/ReportFeatures/MemberAccessCipherDetailsQuery.cs b/src/Core/Tools/ReportFeatures/MemberAccessCipherDetailsQuery.cs index a08359a84f..0c165a7dc2 100644 --- a/src/Core/Tools/ReportFeatures/MemberAccessCipherDetailsQuery.cs +++ b/src/Core/Tools/ReportFeatures/MemberAccessCipherDetailsQuery.cs @@ -1,4 +1,5 @@ -using Bit.Core.AdminConsole.Entities; +using System.Collections.Concurrent; +using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Entities; @@ -59,22 +60,21 @@ public class MemberAccessCipherDetailsQuery : IMemberAccessCipherDetailsQuery var orgItems = await _organizationCiphersQuery.GetAllOrganizationCiphers(request.OrganizationId); var organizationUsersTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(orgUsers); - var memberAccessCipherDetails = GenerateAccessData( + var memberAccessCipherDetails = GenerateAccessDataParallel( orgGroups, orgCollectionsWithAccess, orgItems, organizationUsersTwoFactorEnabled, - orgAbility - ); + orgAbility); return memberAccessCipherDetails; } /// /// Generates a report for all members of an organization. Containing summary information - /// such as item, collection, and group counts. Including the cipherIds a member is assigned. + /// such as item, collection, and group counts. Including the cipherIds a member is assigned. /// Child collection includes detailed information on the user and group collections along - /// with their permissions. + /// with their permissions. /// /// Organization groups collection /// Collections for the organization and the groups/users and permissions @@ -82,72 +82,72 @@ public class MemberAccessCipherDetailsQuery : IMemberAccessCipherDetailsQuery /// Organization users and two factor status /// Organization ability for account recovery status /// List of the MemberAccessCipherDetailsModel; - private IEnumerable GenerateAccessData( - ICollection orgGroups, - ICollection> orgCollectionsWithAccess, - IEnumerable orgItems, - IEnumerable<(OrganizationUserUserDetails user, bool twoFactorIsEnabled)> organizationUsersTwoFactorEnabled, - OrganizationAbility orgAbility) + private IEnumerable GenerateAccessDataParallel( + ICollection orgGroups, + ICollection> orgCollectionsWithAccess, + IEnumerable orgItems, + IEnumerable<(OrganizationUserUserDetails user, bool twoFactorIsEnabled)> organizationUsersTwoFactorEnabled, + OrganizationAbility orgAbility) { - var orgUsers = organizationUsersTwoFactorEnabled.Select(x => x.user); - // Create a dictionary to lookup the group names later. + var orgUsers = organizationUsersTwoFactorEnabled.Select(x => x.user).ToList(); var groupNameDictionary = orgGroups.ToDictionary(x => x.Id, x => x.Name); - - // Get collections grouped and into a dictionary for counts var collectionItems = orgItems .SelectMany(x => x.CollectionIds, (cipher, collectionId) => new { Cipher = cipher, CollectionId = collectionId }) .GroupBy(y => y.CollectionId, (key, ciphers) => new { CollectionId = key, Ciphers = ciphers }); - var itemLookup = collectionItems.ToDictionary(x => x.CollectionId.ToString(), x => x.Ciphers.Select(c => c.Cipher.Id.ToString())); + var itemLookup = collectionItems.ToDictionary(x => x.CollectionId.ToString(), x => x.Ciphers.Select(c => c.Cipher.Id.ToString()).ToList()); - // Loop through the org users and populate report and access data - var memberAccessCipherDetails = new List(); - foreach (var user in orgUsers) + var memberAccessCipherDetails = new ConcurrentBag(); + + Parallel.ForEach(orgUsers, user => { var groupAccessDetails = new List(); var userCollectionAccessDetails = new List(); + foreach (var tCollect in orgCollectionsWithAccess) { - var hasItems = itemLookup.TryGetValue(tCollect.Item1.Id.ToString(), out var items); - var collectionCiphers = hasItems ? items.Select(x => x) : null; - - var itemCounts = hasItems ? collectionCiphers.Count() : 0; - if (tCollect.Item2.Groups.Count() > 0) + if (itemLookup.TryGetValue(tCollect.Item1.Id.ToString(), out var items)) { + var itemCounts = items.Count; - var groupDetails = tCollect.Item2.Groups.Where((tCollectGroups) => user.Groups.Contains(tCollectGroups.Id)).Select(x => - new MemberAccessDetails - { - CollectionId = tCollect.Item1.Id, - CollectionName = tCollect.Item1.Name, - GroupId = x.Id, - GroupName = groupNameDictionary[x.Id], - ReadOnly = x.ReadOnly, - HidePasswords = x.HidePasswords, - Manage = x.Manage, - ItemCount = itemCounts, - CollectionCipherIds = items - }); + if (tCollect.Item2.Groups.Any()) + { + var groupDetails = tCollect.Item2.Groups + .Where(tCollectGroups => user.Groups.Contains(tCollectGroups.Id)) + .Select(x => new MemberAccessDetails + { + CollectionId = tCollect.Item1.Id, + CollectionName = tCollect.Item1.Name, + GroupId = x.Id, + GroupName = groupNameDictionary[x.Id], + ReadOnly = x.ReadOnly, + HidePasswords = x.HidePasswords, + Manage = x.Manage, + ItemCount = itemCounts, + CollectionCipherIds = items + }); - groupAccessDetails.AddRange(groupDetails); - } + groupAccessDetails.AddRange(groupDetails); + } - // All collections assigned to users and their permissions - if (tCollect.Item2.Users.Count() > 0) - { - var userCollectionDetails = tCollect.Item2.Users.Where((tCollectUser) => tCollectUser.Id == user.Id).Select(x => - new MemberAccessDetails - { - CollectionId = tCollect.Item1.Id, - CollectionName = tCollect.Item1.Name, - ReadOnly = x.ReadOnly, - HidePasswords = x.HidePasswords, - Manage = x.Manage, - ItemCount = itemCounts, - CollectionCipherIds = items - }); - userCollectionAccessDetails.AddRange(userCollectionDetails); + if (tCollect.Item2.Users.Any()) + { + var userCollectionDetails = tCollect.Item2.Users + .Where(tCollectUser => tCollectUser.Id == user.Id) + .Select(x => new MemberAccessDetails + { + CollectionId = tCollect.Item1.Id, + CollectionName = tCollect.Item1.Name, + ReadOnly = x.ReadOnly, + HidePasswords = x.HidePasswords, + Manage = x.Manage, + ItemCount = itemCounts, + CollectionCipherIds = items + }); + + userCollectionAccessDetails.AddRange(userCollectionDetails); + } } } @@ -156,7 +156,6 @@ public class MemberAccessCipherDetailsQuery : IMemberAccessCipherDetailsQuery UserName = user.Name, Email = user.Email, TwoFactorEnabled = organizationUsersTwoFactorEnabled.FirstOrDefault(u => u.user.Id == user.Id).twoFactorIsEnabled, - // Both the user's ResetPasswordKey must be set and the organization can UseResetPassword AccountRecoveryEnabled = !string.IsNullOrEmpty(user.ResetPasswordKey) && orgAbility.UseResetPassword, UserGuid = user.Id, UsesKeyConnector = user.UsesKeyConnector @@ -169,9 +168,8 @@ public class MemberAccessCipherDetailsQuery : IMemberAccessCipherDetailsQuery userAccessDetails.AddRange(userGroups); } - // There can be edge cases where groups don't have a collection var groupsWithoutCollections = user.Groups.Where(x => !userAccessDetails.Any(y => x == y.GroupId)); - if (groupsWithoutCollections.Count() > 0) + if (groupsWithoutCollections.Any()) { var emptyGroups = groupsWithoutCollections.Select(x => new MemberAccessDetails { @@ -189,20 +187,20 @@ public class MemberAccessCipherDetailsQuery : IMemberAccessCipherDetailsQuery } report.AccessDetails = userAccessDetails; - var userCiphers = - report.AccessDetails - .Where(x => x.ItemCount > 0) - .SelectMany(y => y.CollectionCipherIds) - .Distinct(); + var userCiphers = report.AccessDetails + .Where(x => x.ItemCount > 0) + .SelectMany(y => y.CollectionCipherIds) + .Distinct(); report.CipherIds = userCiphers; report.TotalItemCount = userCiphers.Count(); - // Distinct items only var distinctItems = report.AccessDetails.Where(x => x.CollectionId.HasValue).Select(x => x.CollectionId).Distinct(); report.CollectionsCount = distinctItems.Count(); report.GroupsCount = report.AccessDetails.Select(x => x.GroupId).Where(y => y.HasValue).Distinct().Count(); + memberAccessCipherDetails.Add(report); - } + }); + return memberAccessCipherDetails; } }