mirror of
https://github.com/bitwarden/server.git
synced 2024-11-21 12:05:42 +01:00
[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
This commit is contained in:
parent
1dec51bf5a
commit
0e23a07bbc
@ -1,13 +1,9 @@
|
|||||||
using Bit.Api.Tools.Models.Response;
|
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.Context;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Tools.Models.Data;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Tools.ReportFeatures.OrganizationReportMembers.Interfaces;
|
||||||
using Bit.Core.Vault.Queries;
|
using Bit.Core.Tools.ReportFeatures.Requests;
|
||||||
using Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
|
||||||
using Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
@ -17,33 +13,49 @@ namespace Bit.Api.Tools.Controllers;
|
|||||||
[Authorize("Application")]
|
[Authorize("Application")]
|
||||||
public class ReportsController : Controller
|
public class ReportsController : Controller
|
||||||
{
|
{
|
||||||
private readonly IOrganizationUserUserDetailsQuery _organizationUserUserDetailsQuery;
|
|
||||||
private readonly IGroupRepository _groupRepository;
|
|
||||||
private readonly ICollectionRepository _collectionRepository;
|
|
||||||
private readonly ICurrentContext _currentContext;
|
private readonly ICurrentContext _currentContext;
|
||||||
private readonly IOrganizationCiphersQuery _organizationCiphersQuery;
|
private readonly IMemberAccessCipherDetailsQuery _memberAccessCipherDetailsQuery;
|
||||||
private readonly IApplicationCacheService _applicationCacheService;
|
|
||||||
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
|
||||||
|
|
||||||
public ReportsController(
|
public ReportsController(
|
||||||
IOrganizationUserUserDetailsQuery organizationUserUserDetailsQuery,
|
|
||||||
IGroupRepository groupRepository,
|
|
||||||
ICollectionRepository collectionRepository,
|
|
||||||
ICurrentContext currentContext,
|
ICurrentContext currentContext,
|
||||||
IOrganizationCiphersQuery organizationCiphersQuery,
|
IMemberAccessCipherDetailsQuery memberAccessCipherDetailsQuery
|
||||||
IApplicationCacheService applicationCacheService,
|
|
||||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery
|
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
_organizationUserUserDetailsQuery = organizationUserUserDetailsQuery;
|
|
||||||
_groupRepository = groupRepository;
|
|
||||||
_collectionRepository = collectionRepository;
|
|
||||||
_currentContext = currentContext;
|
_currentContext = currentContext;
|
||||||
_organizationCiphersQuery = organizationCiphersQuery;
|
_memberAccessCipherDetailsQuery = memberAccessCipherDetailsQuery;
|
||||||
_applicationCacheService = applicationCacheService;
|
|
||||||
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Organization member information containing a list of cipher ids
|
||||||
|
/// assigned
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="orgId">Organzation Id</param>
|
||||||
|
/// <returns>IEnumerable of MemberCipherDetailsResponseModel</returns>
|
||||||
|
/// <exception cref="NotFoundException">If Access reports permission is not assigned</exception>
|
||||||
|
[HttpGet("member-cipher-details/{orgId}")]
|
||||||
|
public async Task<IEnumerable<MemberCipherDetailsResponseModel>> 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Access details for an organization member. Includes the member information,
|
||||||
|
/// group collection assignment, and item counts
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="orgId">Organization Id</param>
|
||||||
|
/// <returns>IEnumerable of MemberAccessReportResponseModel</returns>
|
||||||
|
/// <exception cref="NotFoundException">If Access reports permission is not assigned</exception>
|
||||||
[HttpGet("member-access/{orgId}")]
|
[HttpGet("member-access/{orgId}")]
|
||||||
public async Task<IEnumerable<MemberAccessReportResponseModel>> GetMemberAccessReport(Guid orgId)
|
public async Task<IEnumerable<MemberAccessReportResponseModel>> GetMemberAccessReport(Guid orgId)
|
||||||
{
|
{
|
||||||
@ -52,26 +64,23 @@ public class ReportsController : Controller
|
|||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
|
|
||||||
var orgUsers = await _organizationUserUserDetailsQuery.GetOrganizationUserUserDetails(
|
var memberCipherDetails = await GetMemberCipherDetails(new MemberAccessCipherDetailsRequest { OrganizationId = orgId });
|
||||||
new OrganizationUserUserDetailsQueryRequest
|
|
||||||
{
|
|
||||||
OrganizationId = orgId,
|
|
||||||
IncludeCollections = true,
|
|
||||||
IncludeGroups = true
|
|
||||||
});
|
|
||||||
|
|
||||||
var orgGroups = await _groupRepository.GetManyByOrganizationIdAsync(orgId);
|
var responses = memberCipherDetails.Select(x => new MemberAccessReportResponseModel(x));
|
||||||
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 reports = MemberAccessReportResponseModel.CreateReport(
|
return responses;
|
||||||
orgGroups,
|
}
|
||||||
orgCollectionsWithAccess,
|
|
||||||
orgItems,
|
/// <summary>
|
||||||
organizationUsersTwoFactorEnabled,
|
/// Contains the organization member info, the cipher ids associated with the member,
|
||||||
orgAbility);
|
/// and details on their collections, groups, and permissions
|
||||||
return reports;
|
/// </summary>
|
||||||
|
/// <param name="request">Request to the MemberAccessCipherDetailsQuery</param>
|
||||||
|
/// <returns>IEnumerable of MemberAccessCipherDetails</returns>
|
||||||
|
private async Task<IEnumerable<MemberAccessCipherDetails>> GetMemberCipherDetails(MemberAccessCipherDetailsRequest request)
|
||||||
|
{
|
||||||
|
var memberCipherDetails =
|
||||||
|
await _memberAccessCipherDetailsQuery.GetMemberAccessCipherDetails(request);
|
||||||
|
return memberCipherDetails;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,30 +1,7 @@
|
|||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.Tools.Models.Data;
|
||||||
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;
|
|
||||||
|
|
||||||
namespace Bit.Api.Tools.Models.Response;
|
namespace Bit.Api.Tools.Models.Response;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 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.
|
|
||||||
/// </summary>
|
|
||||||
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; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Contains the collections and group collections a user has access to including
|
/// Contains the collections and group collections a user has access to including
|
||||||
/// the permission level for the collection and group collection.
|
/// the permission level for the collection and group collection.
|
||||||
@ -40,134 +17,18 @@ public class MemberAccessReportResponseModel
|
|||||||
public int TotalItemCount { get; set; }
|
public int TotalItemCount { get; set; }
|
||||||
public Guid? UserGuid { get; set; }
|
public Guid? UserGuid { get; set; }
|
||||||
public bool UsesKeyConnector { get; set; }
|
public bool UsesKeyConnector { get; set; }
|
||||||
public IEnumerable<MemberAccessReportAccessDetails> AccessDetails { get; set; }
|
public IEnumerable<MemberAccessDetails> AccessDetails { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
public MemberAccessReportResponseModel(MemberAccessCipherDetails memberAccessCipherDetails)
|
||||||
/// 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
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="orgGroups">Organization groups collection</param>
|
|
||||||
/// <param name="orgCollectionsWithAccess">Collections for the organization and the groups/users and permissions</param>
|
|
||||||
/// <param name="orgItems">Cipher items for the organization with the collections associated with them</param>
|
|
||||||
/// <param name="organizationUsersTwoFactorEnabled">Organization users and two factor status</param>
|
|
||||||
/// <param name="orgAbility">Organization ability for account recovery status</param>
|
|
||||||
/// <returns>List of the MemberAccessReportResponseModel</returns>;
|
|
||||||
public static IEnumerable<MemberAccessReportResponseModel> CreateReport(
|
|
||||||
ICollection<Group> orgGroups,
|
|
||||||
ICollection<Tuple<Collection, CollectionAccessDetails>> orgCollectionsWithAccess,
|
|
||||||
IEnumerable<CipherOrganizationDetailsWithCollections> orgItems,
|
|
||||||
IEnumerable<(OrganizationUserUserDetails user, bool twoFactorIsEnabled)> organizationUsersTwoFactorEnabled,
|
|
||||||
OrganizationAbility orgAbility)
|
|
||||||
{
|
{
|
||||||
var orgUsers = organizationUsersTwoFactorEnabled.Select(x => x.user);
|
this.UserName = memberAccessCipherDetails.UserName;
|
||||||
// Create a dictionary to lookup the group names later.
|
this.Email = memberAccessCipherDetails.Email;
|
||||||
var groupNameDictionary = orgGroups.ToDictionary(x => x.Id, x => x.Name);
|
this.TwoFactorEnabled = memberAccessCipherDetails.TwoFactorEnabled;
|
||||||
|
this.AccountRecoveryEnabled = memberAccessCipherDetails.AccountRecoveryEnabled;
|
||||||
// Get collections grouped and into a dictionary for counts
|
this.GroupsCount = memberAccessCipherDetails.GroupsCount;
|
||||||
var collectionItems = orgItems
|
this.CollectionsCount = memberAccessCipherDetails.CollectionsCount;
|
||||||
.SelectMany(x => x.CollectionIds,
|
this.TotalItemCount = memberAccessCipherDetails.TotalItemCount;
|
||||||
(x, b) => new { CipherId = x.Id, CollectionId = b })
|
this.UserGuid = memberAccessCipherDetails.UserGuid;
|
||||||
.GroupBy(y => y.CollectionId,
|
this.AccessDetails = memberAccessCipherDetails.AccessDetails;
|
||||||
(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<MemberAccessReportResponseModel>();
|
|
||||||
foreach (var user in orgUsers)
|
|
||||||
{
|
|
||||||
// Take the collections/groups and create the access details items
|
|
||||||
var groupAccessDetails = new List<MemberAccessReportAccessDetails>();
|
|
||||||
var userCollectionAccessDetails = new List<MemberAccessReportAccessDetails>();
|
|
||||||
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<MemberAccessReportAccessDetails>();
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A distinct list of the cipher ids associated with
|
||||||
|
/// the organization member
|
||||||
|
/// </summary>
|
||||||
|
public IEnumerable<string> CipherIds { get; set; }
|
||||||
|
|
||||||
|
public MemberCipherDetailsResponseModel(MemberAccessCipherDetails memberAccessCipherDetails)
|
||||||
|
{
|
||||||
|
this.UserName = memberAccessCipherDetails.UserName;
|
||||||
|
this.Email = memberAccessCipherDetails.Email;
|
||||||
|
this.UsesKeyConnector = memberAccessCipherDetails.UsesKeyConnector;
|
||||||
|
this.CipherIds = memberAccessCipherDetails.CipherIds;
|
||||||
|
}
|
||||||
|
}
|
43
src/Core/Tools/Models/Data/MemberAccessCipherDetails.cs
Normal file
43
src/Core/Tools/Models/Data/MemberAccessCipherDetails.cs
Normal file
@ -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; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The CipherIds associated with the group/collection access
|
||||||
|
/// </summary>
|
||||||
|
public IEnumerable<string> 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; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The details for the member's collection access depending
|
||||||
|
/// on the collections and groups they are assigned to
|
||||||
|
/// </summary>
|
||||||
|
public IEnumerable<MemberAccessDetails> AccessDetails { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A distinct list of the cipher ids associated with
|
||||||
|
/// the organization member
|
||||||
|
/// </summary>
|
||||||
|
public IEnumerable<string> CipherIds { get; set; }
|
||||||
|
}
|
208
src/Core/Tools/ReportFeatures/MemberAccessCipherDetailsQuery.cs
Normal file
208
src/Core/Tools/ReportFeatures/MemberAccessCipherDetailsQuery.cs
Normal file
@ -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<IEnumerable<MemberAccessCipherDetails>> 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="orgGroups">Organization groups collection</param>
|
||||||
|
/// <param name="orgCollectionsWithAccess">Collections for the organization and the groups/users and permissions</param>
|
||||||
|
/// <param name="orgItems">Cipher items for the organization with the collections associated with them</param>
|
||||||
|
/// <param name="organizationUsersTwoFactorEnabled">Organization users and two factor status</param>
|
||||||
|
/// <param name="orgAbility">Organization ability for account recovery status</param>
|
||||||
|
/// <returns>List of the MemberAccessCipherDetailsModel</returns>;
|
||||||
|
private IEnumerable<MemberAccessCipherDetails> GenerateAccessData(
|
||||||
|
ICollection<Group> orgGroups,
|
||||||
|
ICollection<Tuple<Collection, CollectionAccessDetails>> orgCollectionsWithAccess,
|
||||||
|
IEnumerable<CipherOrganizationDetailsWithCollections> 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<MemberAccessCipherDetails>();
|
||||||
|
foreach (var user in orgUsers)
|
||||||
|
{
|
||||||
|
var groupAccessDetails = new List<MemberAccessDetails>();
|
||||||
|
var userCollectionAccessDetails = new List<MemberAccessDetails>();
|
||||||
|
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<MemberAccessDetails>();
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
@ -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<IEnumerable<MemberAccessCipherDetails>> GetMemberAccessCipherDetails(MemberAccessCipherDetailsRequest request);
|
||||||
|
}
|
@ -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<IMemberAccessCipherDetailsQuery, MemberAccessCipherDetailsQuery>();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
namespace Bit.Core.Tools.ReportFeatures.Requests;
|
||||||
|
|
||||||
|
public class MemberAccessCipherDetailsRequest
|
||||||
|
{
|
||||||
|
public Guid OrganizationId { get; set; }
|
||||||
|
}
|
@ -34,6 +34,7 @@ using Bit.Core.SecretsManager.Repositories.Noop;
|
|||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
using Bit.Core.Tokens;
|
using Bit.Core.Tokens;
|
||||||
|
using Bit.Core.Tools.ReportFeatures;
|
||||||
using Bit.Core.Tools.Services;
|
using Bit.Core.Tools.Services;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Bit.Core.Vault;
|
using Bit.Core.Vault;
|
||||||
@ -116,6 +117,7 @@ public static class ServiceCollectionExtensions
|
|||||||
services.AddLoginServices();
|
services.AddLoginServices();
|
||||||
services.AddScoped<IOrganizationDomainService, OrganizationDomainService>();
|
services.AddScoped<IOrganizationDomainService, OrganizationDomainService>();
|
||||||
services.AddVaultServices();
|
services.AddVaultServices();
|
||||||
|
services.AddReportingServices();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void AddTokenizers(this IServiceCollection services)
|
public static void AddTokenizers(this IServiceCollection services)
|
||||||
|
Loading…
Reference in New Issue
Block a user