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; using Bit.Core.Models.Data; using Bit.Core.Models.Data.Organizations; using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Tools.Models.Data; using Bit.Core.Tools.ReportFeatures.OrganizationReportMembers.Interfaces; using Bit.Core.Tools.ReportFeatures.Requests; using Bit.Core.Vault.Models.Data; using Bit.Core.Vault.Queries; using Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests; namespace Bit.Core.Tools.ReportFeatures; public class MemberAccessCipherDetailsQuery : IMemberAccessCipherDetailsQuery { private readonly IOrganizationUserUserDetailsQuery _organizationUserUserDetailsQuery; private readonly IGroupRepository _groupRepository; private readonly ICollectionRepository _collectionRepository; private readonly IOrganizationCiphersQuery _organizationCiphersQuery; private readonly IApplicationCacheService _applicationCacheService; private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery; public MemberAccessCipherDetailsQuery( IOrganizationUserUserDetailsQuery organizationUserUserDetailsQuery, IGroupRepository groupRepository, ICollectionRepository collectionRepository, IOrganizationCiphersQuery organizationCiphersQuery, IApplicationCacheService applicationCacheService, ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery ) { _organizationUserUserDetailsQuery = organizationUserUserDetailsQuery; _groupRepository = groupRepository; _collectionRepository = collectionRepository; _organizationCiphersQuery = organizationCiphersQuery; _applicationCacheService = applicationCacheService; _twoFactorIsEnabledQuery = twoFactorIsEnabledQuery; } public async Task> GetMemberAccessCipherDetails(MemberAccessCipherDetailsRequest request) { var orgUsers = await _organizationUserUserDetailsQuery.GetOrganizationUserUserDetails( new OrganizationUserUserDetailsQueryRequest { OrganizationId = request.OrganizationId, IncludeCollections = true, IncludeGroups = true }); var orgGroups = await _groupRepository.GetManyByOrganizationIdAsync(request.OrganizationId); var orgAbility = await _applicationCacheService.GetOrganizationAbilityAsync(request.OrganizationId); var orgCollectionsWithAccess = await _collectionRepository.GetManyByOrganizationIdWithAccessAsync(request.OrganizationId); var orgItems = await _organizationCiphersQuery.GetAllOrganizationCiphers(request.OrganizationId); var organizationUsersTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(orgUsers); var memberAccessCipherDetails = GenerateAccessDataParallel( orgGroups, orgCollectionsWithAccess, orgItems, organizationUsersTwoFactorEnabled, 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. /// Child collection includes detailed information on the user and group collections along /// with their permissions. /// /// 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; } }