diff --git a/src/Core/Tools/ReportFeatures/MemberAccessCipherDetailsQuery.cs b/src/Core/Tools/ReportFeatures/MemberAccessCipherDetailsQuery.cs
index a08359a84f..bfb3b4da60 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,30 @@ public class MemberAccessCipherDetailsQuery : IMemberAccessCipherDetailsQuery
var orgItems = await _organizationCiphersQuery.GetAllOrganizationCiphers(request.OrganizationId);
var organizationUsersTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(orgUsers);
- var memberAccessCipherDetails = GenerateAccessData(
+ // Keep this original method for reference and comparison. Until we are confident in the parallel version.
+ // var memberAccessCipherDetails = GenerateAccessData(
+ // orgGroups,
+ // orgCollectionsWithAccess,
+ // orgItems,
+ // organizationUsersTwoFactorEnabled,
+ // orgAbility
+ // );
+
+ 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
@@ -90,7 +99,7 @@ public class MemberAccessCipherDetailsQuery : IMemberAccessCipherDetailsQuery
OrganizationAbility orgAbility)
{
var orgUsers = organizationUsersTwoFactorEnabled.Select(x => x.user);
- // Create a dictionary to lookup the group names later.
+ // Create a dictionary to lookup the group names later.
var groupNameDictionary = orgGroups.ToDictionary(x => x.Id, x => x.Name);
// Get collections grouped and into a dictionary for counts
@@ -197,7 +206,7 @@ public class MemberAccessCipherDetailsQuery : IMemberAccessCipherDetailsQuery
report.CipherIds = userCiphers;
report.TotalItemCount = userCiphers.Count();
- // Distinct items only
+ // 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();
@@ -205,4 +214,135 @@ public class MemberAccessCipherDetailsQuery : IMemberAccessCipherDetailsQuery
}
return memberAccessCipherDetails;
}
+
+ ///
+ /// Similar to the GenerateAccessData method, but processes things in parallel.
+ ///
+ /// Organization groups collection
+ /// Collections for the organization and the groups/users and permissions
+ /// Cipher items for the organization with the collections associated with them
+ /// Organization users and two factor status
+ /// Organization ability for account recovery status
+ /// List of the MemberAccessCipherDetailsModel;
+ private IEnumerable GenerateAccessDataParallel(
+ ICollection orgGroups,
+ ICollection> orgCollectionsWithAccess,
+ IEnumerable orgItems,
+ IEnumerable<(OrganizationUserUserDetails user, bool twoFactorIsEnabled)> organizationUsersTwoFactorEnabled,
+ OrganizationAbility orgAbility)
+ {
+ var orgUsers = organizationUsersTwoFactorEnabled.Select(x => x.user).ToList();
+ var groupNameDictionary = orgGroups.ToDictionary(x => x.Id, x => x.Name);
+ 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()).ToList());
+
+ var memberAccessCipherDetails = new ConcurrentBag();
+
+ Parallel.ForEach(orgUsers, user =>
+ {
+ var groupAccessDetails = new List();
+ var userCollectionAccessDetails = new List();
+
+ foreach (var tCollect in orgCollectionsWithAccess)
+ {
+ if (itemLookup.TryGetValue(tCollect.Item1.Id.ToString(), out var items))
+ {
+ var itemCounts = items.Count;
+
+ 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);
+ }
+
+ 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);
+ }
+ }
+ }
+
+ var report = new MemberAccessCipherDetails
+ {
+ UserName = user.Name,
+ Email = user.Email,
+ TwoFactorEnabled = organizationUsersTwoFactorEnabled.FirstOrDefault(u => u.user.Id == user.Id).twoFactorIsEnabled,
+ AccountRecoveryEnabled = !string.IsNullOrEmpty(user.ResetPasswordKey) && orgAbility.UseResetPassword,
+ UserGuid = user.Id,
+ UsesKeyConnector = user.UsesKeyConnector
+ };
+
+ var userAccessDetails = new List();
+ if (user.Groups.Any())
+ {
+ var userGroups = groupAccessDetails.Where(x => user.Groups.Contains(x.GroupId.GetValueOrDefault()));
+ userAccessDetails.AddRange(userGroups);
+ }
+
+ var groupsWithoutCollections = user.Groups.Where(x => !userAccessDetails.Any(y => x == y.GroupId));
+ if (groupsWithoutCollections.Any())
+ {
+ var emptyGroups = groupsWithoutCollections.Select(x => new MemberAccessDetails
+ {
+ GroupId = x,
+ GroupName = groupNameDictionary[x],
+ ItemCount = 0
+ });
+ userAccessDetails.AddRange(emptyGroups);
+ }
+
+ if (user.Collections.Any())
+ {
+ var userCollections = userCollectionAccessDetails.Where(x => user.Collections.Any(y => x.CollectionId == y.Id));
+ userAccessDetails.AddRange(userCollections);
+ }
+ report.AccessDetails = userAccessDetails;
+
+ var userCiphers = report.AccessDetails
+ .Where(x => x.ItemCount > 0)
+ .SelectMany(y => y.CollectionCipherIds)
+ .Distinct();
+ report.CipherIds = userCiphers;
+ report.TotalItemCount = userCiphers.Count();
+
+ 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;
+ }
}