From 0e23a07bbcc70fe1a9756c6ff8830221c14570f7 Mon Sep 17 00:00:00 2001 From: Tom <144813356+ttalty@users.noreply.github.com> Date: Mon, 11 Nov 2024 11:18:10 -0500 Subject: [PATCH] [PM-13298] Modify members access logic (#4876) * Initial refactor of members acess * Refactor of the members access report to include a list of ciphers * Saving ciphers to parent object * Missed saving the response model * bit.core change and updating references. Removing unused refs * Removing commented code * Adding Bit to the namespaces * The mapping to the response model missed setting the UserId --- .../Tools/Controllers/ReportsController.cs | 97 ++++---- .../Response/MemberAccessReportModel.cs | 163 +------------- .../MemberCipherDetailsResponseModel.cs | 24 ++ .../Models/Data/MemberAccessCipherDetails.cs | 43 ++++ .../MemberAccessCipherDetailsQuery.cs | 208 ++++++++++++++++++ .../IMemberAccessCipherDetailsQuery.cs | 9 + .../ReportingServiceCollectionExtensions.cs | 13 ++ .../MemberAccessCipherDetailsRequest.cs | 6 + .../Utilities/ServiceCollectionExtensions.cs | 2 + 9 files changed, 370 insertions(+), 195 deletions(-) create mode 100644 src/Api/Tools/Models/Response/MemberCipherDetailsResponseModel.cs create mode 100644 src/Core/Tools/Models/Data/MemberAccessCipherDetails.cs create mode 100644 src/Core/Tools/ReportFeatures/MemberAccessCipherDetailsQuery.cs create mode 100644 src/Core/Tools/ReportFeatures/OrganizationReportMembers/Interfaces/IMemberAccessCipherDetailsQuery.cs create mode 100644 src/Core/Tools/ReportFeatures/ReportingServiceCollectionExtensions.cs create mode 100644 src/Core/Tools/ReportFeatures/Requests/MemberAccessCipherDetailsRequest.cs diff --git a/src/Api/Tools/Controllers/ReportsController.cs b/src/Api/Tools/Controllers/ReportsController.cs index 5beb320e4..c8cfc0a21 100644 --- a/src/Api/Tools/Controllers/ReportsController.cs +++ b/src/Api/Tools/Controllers/ReportsController.cs @@ -1,13 +1,9 @@ using Bit.Api.Tools.Models.Response; -using Bit.Core.AdminConsole.Repositories; -using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Context; using Bit.Core.Exceptions; -using Bit.Core.Repositories; -using Bit.Core.Services; -using Bit.Core.Vault.Queries; -using Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; -using Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests; +using Bit.Core.Tools.Models.Data; +using Bit.Core.Tools.ReportFeatures.OrganizationReportMembers.Interfaces; +using Bit.Core.Tools.ReportFeatures.Requests; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -17,33 +13,49 @@ namespace Bit.Api.Tools.Controllers; [Authorize("Application")] public class ReportsController : Controller { - private readonly IOrganizationUserUserDetailsQuery _organizationUserUserDetailsQuery; - private readonly IGroupRepository _groupRepository; - private readonly ICollectionRepository _collectionRepository; private readonly ICurrentContext _currentContext; - private readonly IOrganizationCiphersQuery _organizationCiphersQuery; - private readonly IApplicationCacheService _applicationCacheService; - private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery; + private readonly IMemberAccessCipherDetailsQuery _memberAccessCipherDetailsQuery; public ReportsController( - IOrganizationUserUserDetailsQuery organizationUserUserDetailsQuery, - IGroupRepository groupRepository, - ICollectionRepository collectionRepository, ICurrentContext currentContext, - IOrganizationCiphersQuery organizationCiphersQuery, - IApplicationCacheService applicationCacheService, - ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery + IMemberAccessCipherDetailsQuery memberAccessCipherDetailsQuery ) { - _organizationUserUserDetailsQuery = organizationUserUserDetailsQuery; - _groupRepository = groupRepository; - _collectionRepository = collectionRepository; _currentContext = currentContext; - _organizationCiphersQuery = organizationCiphersQuery; - _applicationCacheService = applicationCacheService; - _twoFactorIsEnabledQuery = twoFactorIsEnabledQuery; + _memberAccessCipherDetailsQuery = memberAccessCipherDetailsQuery; } + /// + /// Organization member information containing a list of cipher ids + /// assigned + /// + /// Organzation Id + /// IEnumerable of MemberCipherDetailsResponseModel + /// If Access reports permission is not assigned + [HttpGet("member-cipher-details/{orgId}")] + public async Task> GetMemberCipherDetails(Guid orgId) + { + // Using the AccessReports permission here until new permissions + // are needed for more control over reports + if (!await _currentContext.AccessReports(orgId)) + { + throw new NotFoundException(); + } + + var memberCipherDetails = await GetMemberCipherDetails(new MemberAccessCipherDetailsRequest { OrganizationId = orgId }); + + var responses = memberCipherDetails.Select(x => new MemberCipherDetailsResponseModel(x)); + + return responses; + } + + /// + /// Access details for an organization member. Includes the member information, + /// group collection assignment, and item counts + /// + /// Organization Id + /// IEnumerable of MemberAccessReportResponseModel + /// If Access reports permission is not assigned [HttpGet("member-access/{orgId}")] public async Task> GetMemberAccessReport(Guid orgId) { @@ -52,26 +64,23 @@ public class ReportsController : Controller throw new NotFoundException(); } - var orgUsers = await _organizationUserUserDetailsQuery.GetOrganizationUserUserDetails( - new OrganizationUserUserDetailsQueryRequest - { - OrganizationId = orgId, - IncludeCollections = true, - IncludeGroups = true - }); + var memberCipherDetails = await GetMemberCipherDetails(new MemberAccessCipherDetailsRequest { OrganizationId = orgId }); - var orgGroups = await _groupRepository.GetManyByOrganizationIdAsync(orgId); - var orgAbility = await _applicationCacheService.GetOrganizationAbilityAsync(orgId); - var orgCollectionsWithAccess = await _collectionRepository.GetManyByOrganizationIdWithAccessAsync(orgId); - var orgItems = await _organizationCiphersQuery.GetAllOrganizationCiphers(orgId); - var organizationUsersTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(orgUsers); + var responses = memberCipherDetails.Select(x => new MemberAccessReportResponseModel(x)); - var reports = MemberAccessReportResponseModel.CreateReport( - orgGroups, - orgCollectionsWithAccess, - orgItems, - organizationUsersTwoFactorEnabled, - orgAbility); - return reports; + return responses; + } + + /// + /// Contains the organization member info, the cipher ids associated with the member, + /// and details on their collections, groups, and permissions + /// + /// Request to the MemberAccessCipherDetailsQuery + /// IEnumerable of MemberAccessCipherDetails + private async Task> GetMemberCipherDetails(MemberAccessCipherDetailsRequest request) + { + var memberCipherDetails = + await _memberAccessCipherDetailsQuery.GetMemberAccessCipherDetails(request); + return memberCipherDetails; } } diff --git a/src/Api/Tools/Models/Response/MemberAccessReportModel.cs b/src/Api/Tools/Models/Response/MemberAccessReportModel.cs index 378d4d94c..b110c316c 100644 --- a/src/Api/Tools/Models/Response/MemberAccessReportModel.cs +++ b/src/Api/Tools/Models/Response/MemberAccessReportModel.cs @@ -1,30 +1,7 @@ -using Bit.Core.AdminConsole.Entities; -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.Vault.Models.Data; +using Bit.Core.Tools.Models.Data; namespace Bit.Api.Tools.Models.Response; -/// -/// Member access details. The individual item for the detailed member access -/// report. A collection can be assigned directly to a user without a group or -/// the user can be assigned to a collection through a group. Group level permissions -/// can override collection level permissions. -/// -public class MemberAccessReportAccessDetails -{ - public Guid? CollectionId { get; set; } - public Guid? GroupId { get; set; } - public string GroupName { get; set; } - public string CollectionName { get; set; } - public int ItemCount { get; set; } - public bool? ReadOnly { get; set; } - public bool? HidePasswords { get; set; } - public bool? Manage { get; set; } -} - /// /// Contains the collections and group collections a user has access to including /// the permission level for the collection and group collection. @@ -40,134 +17,18 @@ public class MemberAccessReportResponseModel public int TotalItemCount { get; set; } public Guid? UserGuid { get; set; } public bool UsesKeyConnector { get; set; } - public IEnumerable AccessDetails { get; set; } + public IEnumerable AccessDetails { get; set; } - /// - /// Generates a report for all members of an organization. Containing summary information - /// such as item, collection, and group counts. As well as 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 MemberAccessReportResponseModel; - public static IEnumerable CreateReport( - ICollection orgGroups, - ICollection> orgCollectionsWithAccess, - IEnumerable orgItems, - IEnumerable<(OrganizationUserUserDetails user, bool twoFactorIsEnabled)> organizationUsersTwoFactorEnabled, - OrganizationAbility orgAbility) + public MemberAccessReportResponseModel(MemberAccessCipherDetails memberAccessCipherDetails) { - var orgUsers = organizationUsersTwoFactorEnabled.Select(x => x.user); - // 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 - var collectionItems = orgItems - .SelectMany(x => x.CollectionIds, - (x, b) => new { CipherId = x.Id, CollectionId = b }) - .GroupBy(y => y.CollectionId, - (key, g) => new { CollectionId = key, Ciphers = g }); - var collectionItemCounts = collectionItems.ToDictionary(x => x.CollectionId, x => x.Ciphers.Count()); - - - // Loop through the org users and populate report and access data - var memberAccessReport = new List(); - foreach (var user in orgUsers) - { - // Take the collections/groups and create the access details items - var groupAccessDetails = new List(); - var userCollectionAccessDetails = new List(); - foreach (var tCollect in orgCollectionsWithAccess) - { - var itemCounts = collectionItemCounts.TryGetValue(tCollect.Item1.Id, out var itemCount) ? itemCount : 0; - if (tCollect.Item2.Groups.Count() > 0) - { - var groupDetails = tCollect.Item2.Groups.Where((tCollectGroups) => user.Groups.Contains(tCollectGroups.Id)).Select(x => - new MemberAccessReportAccessDetails - { - 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, - }); - 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 MemberAccessReportAccessDetails - { - CollectionId = tCollect.Item1.Id, - CollectionName = tCollect.Item1.Name, - ReadOnly = x.ReadOnly, - HidePasswords = x.HidePasswords, - Manage = x.Manage, - ItemCount = itemCounts, - }); - userCollectionAccessDetails.AddRange(userCollectionDetails); - } - } - - var report = new MemberAccessReportResponseModel - { - 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 - }; - - var userAccessDetails = new List(); - if (user.Groups.Any()) - { - var userGroups = groupAccessDetails.Where(x => user.Groups.Contains(x.GroupId.GetValueOrDefault())); - 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) - { - var emptyGroups = groupsWithoutCollections.Select(x => new MemberAccessReportAccessDetails - { - 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; - - report.TotalItemCount = collectionItems - .Where(x => report.AccessDetails.Any(y => x.CollectionId == y.CollectionId)) - .SelectMany(x => x.Ciphers) - .GroupBy(g => g.CipherId).Select(grp => grp.FirstOrDefault()) - .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(); - memberAccessReport.Add(report); - } - return memberAccessReport; + this.UserName = memberAccessCipherDetails.UserName; + this.Email = memberAccessCipherDetails.Email; + this.TwoFactorEnabled = memberAccessCipherDetails.TwoFactorEnabled; + this.AccountRecoveryEnabled = memberAccessCipherDetails.AccountRecoveryEnabled; + this.GroupsCount = memberAccessCipherDetails.GroupsCount; + this.CollectionsCount = memberAccessCipherDetails.CollectionsCount; + this.TotalItemCount = memberAccessCipherDetails.TotalItemCount; + this.UserGuid = memberAccessCipherDetails.UserGuid; + this.AccessDetails = memberAccessCipherDetails.AccessDetails; } - } diff --git a/src/Api/Tools/Models/Response/MemberCipherDetailsResponseModel.cs b/src/Api/Tools/Models/Response/MemberCipherDetailsResponseModel.cs new file mode 100644 index 000000000..5c87264c5 --- /dev/null +++ b/src/Api/Tools/Models/Response/MemberCipherDetailsResponseModel.cs @@ -0,0 +1,24 @@ +using Bit.Core.Tools.Models.Data; + +namespace Bit.Api.Tools.Models.Response; + +public class MemberCipherDetailsResponseModel +{ + public string UserName { get; set; } + public string Email { get; set; } + public bool UsesKeyConnector { get; set; } + + /// + /// A distinct list of the cipher ids associated with + /// the organization member + /// + public IEnumerable CipherIds { get; set; } + + public MemberCipherDetailsResponseModel(MemberAccessCipherDetails memberAccessCipherDetails) + { + this.UserName = memberAccessCipherDetails.UserName; + this.Email = memberAccessCipherDetails.Email; + this.UsesKeyConnector = memberAccessCipherDetails.UsesKeyConnector; + this.CipherIds = memberAccessCipherDetails.CipherIds; + } +} diff --git a/src/Core/Tools/Models/Data/MemberAccessCipherDetails.cs b/src/Core/Tools/Models/Data/MemberAccessCipherDetails.cs new file mode 100644 index 000000000..943d56c53 --- /dev/null +++ b/src/Core/Tools/Models/Data/MemberAccessCipherDetails.cs @@ -0,0 +1,43 @@ +namespace Bit.Core.Tools.Models.Data; + +public class MemberAccessDetails +{ + public Guid? CollectionId { get; set; } + public Guid? GroupId { get; set; } + public string GroupName { get; set; } + public string CollectionName { get; set; } + public int ItemCount { get; set; } + public bool? ReadOnly { get; set; } + public bool? HidePasswords { get; set; } + public bool? Manage { get; set; } + + /// + /// The CipherIds associated with the group/collection access + /// + public IEnumerable CollectionCipherIds { get; set; } +} + +public class MemberAccessCipherDetails +{ + public string UserName { get; set; } + public string Email { get; set; } + public bool TwoFactorEnabled { get; set; } + public bool AccountRecoveryEnabled { get; set; } + public int GroupsCount { get; set; } + public int CollectionsCount { get; set; } + public int TotalItemCount { get; set; } + public Guid? UserGuid { get; set; } + public bool UsesKeyConnector { get; set; } + + /// + /// The details for the member's collection access depending + /// on the collections and groups they are assigned to + /// + public IEnumerable AccessDetails { get; set; } + + /// + /// A distinct list of the cipher ids associated with + /// the organization member + /// + public IEnumerable CipherIds { get; set; } +} diff --git a/src/Core/Tools/ReportFeatures/MemberAccessCipherDetailsQuery.cs b/src/Core/Tools/ReportFeatures/MemberAccessCipherDetailsQuery.cs new file mode 100644 index 000000000..a08359a84 --- /dev/null +++ b/src/Core/Tools/ReportFeatures/MemberAccessCipherDetailsQuery.cs @@ -0,0 +1,208 @@ +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 = GenerateAccessData( + 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 GenerateAccessData( + 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 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())); + + // Loop through the org users and populate report and access data + var memberAccessCipherDetails = new List(); + foreach (var user in orgUsers) + { + 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) + { + + 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); + } + + // 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); + } + } + + var report = new MemberAccessCipherDetails + { + 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 + }; + + var userAccessDetails = new List(); + if (user.Groups.Any()) + { + var userGroups = groupAccessDetails.Where(x => user.Groups.Contains(x.GroupId.GetValueOrDefault())); + 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) + { + 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(); + + // 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; + } +} diff --git a/src/Core/Tools/ReportFeatures/OrganizationReportMembers/Interfaces/IMemberAccessCipherDetailsQuery.cs b/src/Core/Tools/ReportFeatures/OrganizationReportMembers/Interfaces/IMemberAccessCipherDetailsQuery.cs new file mode 100644 index 000000000..c55495fd1 --- /dev/null +++ b/src/Core/Tools/ReportFeatures/OrganizationReportMembers/Interfaces/IMemberAccessCipherDetailsQuery.cs @@ -0,0 +1,9 @@ +using Bit.Core.Tools.Models.Data; +using Bit.Core.Tools.ReportFeatures.Requests; + +namespace Bit.Core.Tools.ReportFeatures.OrganizationReportMembers.Interfaces; + +public interface IMemberAccessCipherDetailsQuery +{ + Task> GetMemberAccessCipherDetails(MemberAccessCipherDetailsRequest request); +} diff --git a/src/Core/Tools/ReportFeatures/ReportingServiceCollectionExtensions.cs b/src/Core/Tools/ReportFeatures/ReportingServiceCollectionExtensions.cs new file mode 100644 index 000000000..5c813b8cb --- /dev/null +++ b/src/Core/Tools/ReportFeatures/ReportingServiceCollectionExtensions.cs @@ -0,0 +1,13 @@ + +using Bit.Core.Tools.ReportFeatures.OrganizationReportMembers.Interfaces; +using Microsoft.Extensions.DependencyInjection; + +namespace Bit.Core.Tools.ReportFeatures; + +public static class ReportingServiceCollectionExtensions +{ + public static void AddReportingServices(this IServiceCollection services) + { + services.AddScoped(); + } +} diff --git a/src/Core/Tools/ReportFeatures/Requests/MemberAccessCipherDetailsRequest.cs b/src/Core/Tools/ReportFeatures/Requests/MemberAccessCipherDetailsRequest.cs new file mode 100644 index 000000000..395230f43 --- /dev/null +++ b/src/Core/Tools/ReportFeatures/Requests/MemberAccessCipherDetailsRequest.cs @@ -0,0 +1,6 @@ +namespace Bit.Core.Tools.ReportFeatures.Requests; + +public class MemberAccessCipherDetailsRequest +{ + public Guid OrganizationId { get; set; } +} diff --git a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs index 5a5585952..1b99b4cc8 100644 --- a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs +++ b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs @@ -34,6 +34,7 @@ using Bit.Core.SecretsManager.Repositories.Noop; using Bit.Core.Services; using Bit.Core.Settings; using Bit.Core.Tokens; +using Bit.Core.Tools.ReportFeatures; using Bit.Core.Tools.Services; using Bit.Core.Utilities; using Bit.Core.Vault; @@ -116,6 +117,7 @@ public static class ServiceCollectionExtensions services.AddLoginServices(); services.AddScoped(); services.AddVaultServices(); + services.AddReportingServices(); } public static void AddTokenizers(this IServiceCollection services)