mirror of
https://github.com/bitwarden/server.git
synced 2025-06-25 05:08:48 -05:00
PM-20112 fixing dapper repository code and adding migration script
This commit is contained in:
parent
a0ccc98109
commit
c082c91108
@ -1,429 +1,34 @@
|
|||||||
using System.Collections.Concurrent;
|
using Bit.Core.Dirt.Reports.Models.Data;
|
||||||
using Bit.Core.AdminConsole.Entities;
|
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
|
||||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
|
||||||
using Bit.Core.Dirt.Reports.Models.Data;
|
|
||||||
using Bit.Core.Dirt.Reports.Repositories;
|
using Bit.Core.Dirt.Reports.Repositories;
|
||||||
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.ReportFeatures.OrganizationReportMembers.Interfaces;
|
using Bit.Core.Tools.ReportFeatures.OrganizationReportMembers.Interfaces;
|
||||||
using Bit.Core.Tools.ReportFeatures.Requests;
|
using Bit.Core.Tools.ReportFeatures.Requests;
|
||||||
using Bit.Core.Vault.Models.Data;
|
|
||||||
using Bit.Core.Vault.Queries;
|
|
||||||
using Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
|
||||||
|
|
||||||
namespace Bit.Core.Tools.ReportFeatures;
|
namespace Bit.Core.Dirt.Reports.ReportFeatures;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generates the member access report.
|
||||||
|
/// </summary>
|
||||||
public class MemberAccessCipherDetailsQuery : IMemberAccessCipherDetailsQuery
|
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;
|
|
||||||
private readonly IMemberAccessCipherDetailsRepository _memberAccessCipherDetailsRepository;
|
private readonly IMemberAccessCipherDetailsRepository _memberAccessCipherDetailsRepository;
|
||||||
|
|
||||||
public MemberAccessCipherDetailsQuery(
|
public MemberAccessCipherDetailsQuery(
|
||||||
IOrganizationUserUserDetailsQuery organizationUserUserDetailsQuery,
|
|
||||||
IGroupRepository groupRepository,
|
|
||||||
ICollectionRepository collectionRepository,
|
|
||||||
IOrganizationCiphersQuery organizationCiphersQuery,
|
|
||||||
IApplicationCacheService applicationCacheService,
|
|
||||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
|
||||||
IMemberAccessCipherDetailsRepository memberAccessCipherDetailsRepository
|
IMemberAccessCipherDetailsRepository memberAccessCipherDetailsRepository
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
_organizationUserUserDetailsQuery = organizationUserUserDetailsQuery;
|
|
||||||
_groupRepository = groupRepository;
|
|
||||||
_collectionRepository = collectionRepository;
|
|
||||||
_organizationCiphersQuery = organizationCiphersQuery;
|
|
||||||
_applicationCacheService = applicationCacheService;
|
|
||||||
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
|
||||||
_memberAccessCipherDetailsRepository = memberAccessCipherDetailsRepository;
|
_memberAccessCipherDetailsRepository = memberAccessCipherDetailsRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<MemberAccessCipherDetails>> GetMemberAccessCipherDetails(MemberAccessCipherDetailsRequest request)
|
|
||||||
{
|
|
||||||
return await _memberAccessCipherDetailsRepository.GetMemberAccessCipherDetailsByOrganizationId(request.OrganizationId);
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Generates a report for all members of an organization. Containing summary information
|
/// 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
|
/// Child collection includes detailed information on the user and group collections along
|
||||||
/// with their permissions.
|
/// with their permissions.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="orgGroups">Organization groups collection</param>
|
/// <param name="request"><see cref="MemberAccessCipherDetailsRequest"/>need organizationId field to get data</param>
|
||||||
/// <param name="orgCollectionsWithAccess">Collections for the organization and the groups/users and permissions</param>
|
/// <returns>List of the <see cref="MemberAccessCipherDetails"/></returns>;
|
||||||
/// <param name="orgItems">Cipher items for the organization with the collections associated with them</param>
|
public async Task<IEnumerable<MemberAccessCipherDetails>> GetMemberAccessCipherDetails(MemberAccessCipherDetailsRequest request)
|
||||||
/// <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> GenerateAccessDataParallel(
|
|
||||||
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).ToList();
|
return await _memberAccessCipherDetailsRepository.GetMemberAccessCipherDetailsByOrganizationId(request.OrganizationId);
|
||||||
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<MemberAccessCipherDetails>();
|
|
||||||
|
|
||||||
Parallel.ForEach(orgUsers, user =>
|
|
||||||
{
|
|
||||||
var groupAccessDetails = new List<MemberAccessDetails>();
|
|
||||||
var userCollectionAccessDetails = new List<MemberAccessDetails>();
|
|
||||||
|
|
||||||
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<MemberAccessDetails>();
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
private IEnumerable<MemberAccessCipherDetails> GenerateAccessDataParallelV2(
|
|
||||||
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).ToList();
|
|
||||||
var groupNameDictionary = orgGroups.ToDictionary(x => x.Id, x => x.Name);
|
|
||||||
|
|
||||||
// Pre-compute and materialize this collection to avoid repeated work in parallel loop
|
|
||||||
var itemLookup = orgItems
|
|
||||||
.SelectMany(x => x.CollectionIds,
|
|
||||||
(cipher, collectionId) => new { Cipher = cipher, CollectionId = collectionId })
|
|
||||||
.GroupBy(y => y.CollectionId,
|
|
||||||
(key, ciphers) => new { CollectionId = key, Ciphers = ciphers })
|
|
||||||
.ToDictionary(x => x.CollectionId.ToString(), x => x.Ciphers.Select(c => c.Cipher.Id.ToString()).ToList());
|
|
||||||
|
|
||||||
var memberAccessCipherDetails = new ConcurrentBag<MemberAccessCipherDetails>();
|
|
||||||
|
|
||||||
// Add parallelism control to prevent thread exhaustion
|
|
||||||
var parallelOptions = new ParallelOptions
|
|
||||||
{
|
|
||||||
MaxDegreeOfParallelism = Math.Max(1, Environment.ProcessorCount / 2)
|
|
||||||
};
|
|
||||||
|
|
||||||
Parallel.ForEach(orgUsers, parallelOptions, user =>
|
|
||||||
{
|
|
||||||
// Each thread gets its own lists - no need for thread-safe collections here
|
|
||||||
var userAccessDetails = new List<MemberAccessDetails>();
|
|
||||||
|
|
||||||
// Process group access details
|
|
||||||
var userGroupIds = new HashSet<Guid>(user.Groups);
|
|
||||||
foreach (var tCollect in orgCollectionsWithAccess)
|
|
||||||
{
|
|
||||||
if (!itemLookup.TryGetValue(tCollect.Item1.Id.ToString(), out var items))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process group-based access
|
|
||||||
foreach (var groupAccess in tCollect.Item2.Groups.Where(g => userGroupIds.Contains(g.Id)))
|
|
||||||
{
|
|
||||||
userAccessDetails.Add(new MemberAccessDetails
|
|
||||||
{
|
|
||||||
CollectionId = tCollect.Item1.Id,
|
|
||||||
CollectionName = tCollect.Item1.Name,
|
|
||||||
GroupId = groupAccess.Id,
|
|
||||||
GroupName = groupNameDictionary[groupAccess.Id],
|
|
||||||
ReadOnly = groupAccess.ReadOnly,
|
|
||||||
HidePasswords = groupAccess.HidePasswords,
|
|
||||||
Manage = groupAccess.Manage,
|
|
||||||
ItemCount = items.Count,
|
|
||||||
CollectionCipherIds = items
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process direct user access
|
|
||||||
var userAccess = tCollect.Item2.Users.FirstOrDefault(u => u.Id == user.Id);
|
|
||||||
if (userAccess != null)
|
|
||||||
{
|
|
||||||
userAccessDetails.Add(new MemberAccessDetails
|
|
||||||
{
|
|
||||||
CollectionId = tCollect.Item1.Id,
|
|
||||||
CollectionName = tCollect.Item1.Name,
|
|
||||||
ReadOnly = userAccess.ReadOnly,
|
|
||||||
HidePasswords = userAccess.HidePasswords,
|
|
||||||
Manage = userAccess.Manage,
|
|
||||||
ItemCount = items.Count,
|
|
||||||
CollectionCipherIds = items
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add empty groups
|
|
||||||
var groupsWithCollections = new HashSet<Guid>(userAccessDetails
|
|
||||||
.Where(x => x.GroupId.HasValue)
|
|
||||||
.Select(x => x.GroupId.Value));
|
|
||||||
|
|
||||||
foreach (var groupId in user.Groups.Where(g => !groupsWithCollections.Contains(g)))
|
|
||||||
{
|
|
||||||
userAccessDetails.Add(new MemberAccessDetails
|
|
||||||
{
|
|
||||||
GroupId = groupId,
|
|
||||||
GroupName = groupNameDictionary[groupId],
|
|
||||||
ItemCount = 0
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate user ciphers efficiently
|
|
||||||
var userCipherIds = userAccessDetails
|
|
||||||
.Where(x => x.ItemCount > 0)
|
|
||||||
.SelectMany(y => y.CollectionCipherIds ?? Enumerable.Empty<string>())
|
|
||||||
.Distinct()
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
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,
|
|
||||||
AccessDetails = userAccessDetails,
|
|
||||||
CipherIds = userCipherIds,
|
|
||||||
TotalItemCount = userCipherIds.Count,
|
|
||||||
CollectionsCount = userAccessDetails.Where(x => x.CollectionId.HasValue).Select(x => x.CollectionId).Distinct().Count(),
|
|
||||||
GroupsCount = userAccessDetails.Select(x => x.GroupId).Where(y => y.HasValue).Distinct().Count()
|
|
||||||
};
|
|
||||||
|
|
||||||
memberAccessCipherDetails.Add(report);
|
|
||||||
});
|
|
||||||
|
|
||||||
return memberAccessCipherDetails;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using Bit.Core.Tools.ReportFeatures.Interfaces;
|
using Bit.Core.Dirt.Reports.ReportFeatures;
|
||||||
|
using Bit.Core.Tools.ReportFeatures.Interfaces;
|
||||||
using Bit.Core.Tools.ReportFeatures.OrganizationReportMembers.Interfaces;
|
using Bit.Core.Tools.ReportFeatures.OrganizationReportMembers.Interfaces;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
#nullable enable
|
using System.Data;
|
||||||
using System.Data;
|
|
||||||
using Bit.Core.Dirt.Reports.Models.Data;
|
using Bit.Core.Dirt.Reports.Models.Data;
|
||||||
using Bit.Core.Dirt.Reports.Repositories;
|
using Bit.Core.Dirt.Reports.Repositories;
|
||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
|
|
||||||
using Bit.Infrastructure.Dapper.Repositories;
|
using Bit.Infrastructure.Dapper.Repositories;
|
||||||
using Dapper;
|
using Dapper;
|
||||||
using Microsoft.Data.SqlClient;
|
using Microsoft.Data.SqlClient;
|
||||||
@ -26,6 +24,7 @@ public class MemberAccessCipherDetailsRepository : BaseRepository, IMemberAccess
|
|||||||
{
|
{
|
||||||
await using var connection = new SqlConnection(ConnectionString);
|
await using var connection = new SqlConnection(ConnectionString);
|
||||||
|
|
||||||
|
|
||||||
var result = await connection.QueryAsync<MemberAccessCipherDetails>(
|
var result = await connection.QueryAsync<MemberAccessCipherDetails>(
|
||||||
"[dbo].[MemberAccessReport_GetMemberAccessCipherDetailsByOrganizationId]",
|
"[dbo].[MemberAccessReport_GetMemberAccessCipherDetailsByOrganizationId]",
|
||||||
new
|
new
|
||||||
@ -35,6 +34,7 @@ public class MemberAccessCipherDetailsRepository : BaseRepository, IMemberAccess
|
|||||||
}, commandType: CommandType.StoredProcedure);
|
}, commandType: CommandType.StoredProcedure);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -114,6 +114,7 @@ public class DatabaseContext : DbContext
|
|||||||
var eOrganizationConnection = builder.Entity<OrganizationConnection>();
|
var eOrganizationConnection = builder.Entity<OrganizationConnection>();
|
||||||
var eOrganizationDomain = builder.Entity<OrganizationDomain>();
|
var eOrganizationDomain = builder.Entity<OrganizationDomain>();
|
||||||
var aWebAuthnCredential = builder.Entity<WebAuthnCredential>();
|
var aWebAuthnCredential = builder.Entity<WebAuthnCredential>();
|
||||||
|
var eMemberAccessCipherDetails = builder.Entity<MemberAccessCipherDetails>();
|
||||||
|
|
||||||
// Shadow property configurations go here
|
// Shadow property configurations go here
|
||||||
|
|
||||||
@ -136,6 +137,8 @@ public class DatabaseContext : DbContext
|
|||||||
eCollectionGroup.HasKey(cg => new { cg.CollectionId, cg.GroupId });
|
eCollectionGroup.HasKey(cg => new { cg.CollectionId, cg.GroupId });
|
||||||
eGroupUser.HasKey(gu => new { gu.GroupId, gu.OrganizationUserId });
|
eGroupUser.HasKey(gu => new { gu.GroupId, gu.OrganizationUserId });
|
||||||
|
|
||||||
|
eMemberAccessCipherDetails.HasNoKey();
|
||||||
|
|
||||||
var dataProtector = this.GetService<DP.IDataProtectionProvider>().CreateProtector(
|
var dataProtector = this.GetService<DP.IDataProtectionProvider>().CreateProtector(
|
||||||
Constants.DatabaseFieldProtectorPurpose);
|
Constants.DatabaseFieldProtectorPurpose);
|
||||||
var dataProtectionConverter = new DataProtectionConverter(dataProtector);
|
var dataProtectionConverter = new DataProtectionConverter(dataProtector);
|
||||||
|
@ -0,0 +1,66 @@
|
|||||||
|
CREATE OR ALTER PROC dbo.MemberAccessReport_GetMemberAccessCipherDetailsByOrganizationId
|
||||||
|
@OrganizationId UNIQUEIDENTIFIER
|
||||||
|
AS
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
|
||||||
|
IF @OrganizationId IS NULL
|
||||||
|
THROW 50000, 'OrganizationId cannot be null', 1;
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
U.Id AS UserGuid,
|
||||||
|
U.Name AS UserName,
|
||||||
|
U.Email,
|
||||||
|
U.TwoFactorProviders,
|
||||||
|
U.UsesKeyConnector,
|
||||||
|
CC.CollectionId,
|
||||||
|
C.Name AS CollectionName,
|
||||||
|
NULL AS GroupId,
|
||||||
|
NULL AS GroupName,
|
||||||
|
CU.ReadOnly,
|
||||||
|
CU.HidePasswords,
|
||||||
|
CU.Manage,
|
||||||
|
cipher.*
|
||||||
|
FROM dbo.OrganizationUser OU
|
||||||
|
INNER JOIN dbo.[User] U ON U.Id = OU.UserId
|
||||||
|
INNER JOIN dbo.Organization O ON O.Id = OU.OrganizationId
|
||||||
|
AND O.Id = @OrganizationId
|
||||||
|
AND O.Enabled = 1
|
||||||
|
INNER JOIN dbo.CollectionUser CU ON CU.OrganizationUserId = OU.Id
|
||||||
|
INNER JOIN dbo.Collection C ON C.Id = CU.CollectionId
|
||||||
|
INNER JOIN dbo.CollectionCipher CC ON CC.CollectionId = C.Id
|
||||||
|
INNER JOIN dbo.Cipher Cipher ON Cipher.Id = CC.CipherId
|
||||||
|
WHERE OU.Status = 2
|
||||||
|
AND Cipher.DeletedDate IS NULL
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
-- Group-based collection permissions
|
||||||
|
SELECT
|
||||||
|
U.Id AS UserGuid,
|
||||||
|
U.Name AS UserName,
|
||||||
|
U.Email,
|
||||||
|
U.TwoFactorProviders,
|
||||||
|
U.UsesKeyConnector,
|
||||||
|
CC.CollectionId,
|
||||||
|
C.Name AS CollectionName,
|
||||||
|
G.Id AS GroupId,
|
||||||
|
G.Name AS GroupName,
|
||||||
|
CG.ReadOnly,
|
||||||
|
CG.HidePasswords,
|
||||||
|
CG.Manage,
|
||||||
|
Cipher.*
|
||||||
|
FROM dbo.OrganizationUser OU
|
||||||
|
INNER JOIN dbo.[User] U ON U.Id = OU.UserId
|
||||||
|
INNER JOIN dbo.Organization O ON O.Id = OU.OrganizationId
|
||||||
|
AND O.Id = @OrganizationId
|
||||||
|
AND O.Enabled = 1
|
||||||
|
INNER JOIN dbo.GroupUser GU ON GU.OrganizationUserId = OU.Id
|
||||||
|
INNER JOIN dbo.[Group] G ON G.Id = GU.GroupId
|
||||||
|
INNER JOIN dbo.CollectionGroup CG ON CG.GroupId = G.Id
|
||||||
|
INNER JOIN dbo.Collection C ON C.Id = CG.CollectionId
|
||||||
|
INNER JOIN dbo.CollectionCipher CC ON CC.CollectionId = C.Id
|
||||||
|
INNER JOIN dbo.Cipher Cipher ON Cipher.Id = CC.CipherId
|
||||||
|
WHERE OU.Status = 2
|
||||||
|
AND Cipher.DeletedDate IS NULL
|
||||||
|
|
||||||
|
GO
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,21 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Bit.MySqlMigrations.Migrations;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class _20250604_00_MemberAccessCipherDetailsByOrgIdsql : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -22,6 +22,41 @@ namespace Bit.MySqlMigrations.Migrations
|
|||||||
|
|
||||||
MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder);
|
MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("Bit.Core.Dirt.Reports.Models.Data.MemberAccessCipherDetails", b =>
|
||||||
|
{
|
||||||
|
b.Property<bool>("AccountRecoveryEnabled")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<string>("CipherIds")
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<int>("CollectionsCount")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<int>("GroupsCount")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int>("TotalItemCount")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<bool>("TwoFactorEnabled")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<Guid?>("UserGuid")
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.Property<string>("UserName")
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<bool>("UsesKeyConnector")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.ToTable("MemberAccessCipherDetails");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b =>
|
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,21 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Bit.PostgresMigrations.Migrations;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class _20250604_00_MemberAccessCipherDetailsByOrgIdsql : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -23,6 +23,41 @@ namespace Bit.PostgresMigrations.Migrations
|
|||||||
|
|
||||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("Bit.Core.Dirt.Reports.Models.Data.MemberAccessCipherDetails", b =>
|
||||||
|
{
|
||||||
|
b.Property<bool>("AccountRecoveryEnabled")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<string[]>("CipherIds")
|
||||||
|
.HasColumnType("text[]");
|
||||||
|
|
||||||
|
b.Property<int>("CollectionsCount")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<int>("GroupsCount")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("TotalItemCount")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<bool>("TwoFactorEnabled")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<Guid?>("UserGuid")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("UserName")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<bool>("UsesKeyConnector")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.ToTable("MemberAccessCipherDetails");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b =>
|
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,21 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Bit.SqliteMigrations.Migrations;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class _20250604_00_MemberAccessCipherDetailsByOrgIdsql : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -17,6 +17,41 @@ namespace Bit.SqliteMigrations.Migrations
|
|||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
modelBuilder.HasAnnotation("ProductVersion", "8.0.8");
|
modelBuilder.HasAnnotation("ProductVersion", "8.0.8");
|
||||||
|
|
||||||
|
modelBuilder.Entity("Bit.Core.Dirt.Reports.Models.Data.MemberAccessCipherDetails", b =>
|
||||||
|
{
|
||||||
|
b.Property<bool>("AccountRecoveryEnabled")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("CipherIds")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("CollectionsCount")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("GroupsCount")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("TotalItemCount")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("TwoFactorEnabled")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<Guid?>("UserGuid")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("UserName")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("UsesKeyConnector")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.ToTable("MemberAccessCipherDetails");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b =>
|
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
|
Loading…
x
Reference in New Issue
Block a user