mirror of
https://github.com/bitwarden/server.git
synced 2025-06-24 20:58:49 -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.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.Models.Data;
|
||||
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.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
|
||||
{
|
||||
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;
|
||||
|
||||
public MemberAccessCipherDetailsQuery(
|
||||
IOrganizationUserUserDetailsQuery organizationUserUserDetailsQuery,
|
||||
IGroupRepository groupRepository,
|
||||
ICollectionRepository collectionRepository,
|
||||
IOrganizationCiphersQuery organizationCiphersQuery,
|
||||
IApplicationCacheService applicationCacheService,
|
||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
||||
IMemberAccessCipherDetailsRepository memberAccessCipherDetailsRepository
|
||||
)
|
||||
{
|
||||
_organizationUserUserDetailsQuery = organizationUserUserDetailsQuery;
|
||||
_groupRepository = groupRepository;
|
||||
_collectionRepository = collectionRepository;
|
||||
_organizationCiphersQuery = organizationCiphersQuery;
|
||||
_applicationCacheService = applicationCacheService;
|
||||
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
||||
_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>
|
||||
/// 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> GenerateAccessDataParallel(
|
||||
ICollection<Group> orgGroups,
|
||||
ICollection<Tuple<Collection, CollectionAccessDetails>> orgCollectionsWithAccess,
|
||||
IEnumerable<CipherOrganizationDetailsWithCollections> orgItems,
|
||||
IEnumerable<(OrganizationUserUserDetails user, bool twoFactorIsEnabled)> organizationUsersTwoFactorEnabled,
|
||||
OrganizationAbility orgAbility)
|
||||
/// <param name="request"><see cref="MemberAccessCipherDetailsRequest"/>need organizationId field to get data</param>
|
||||
/// <returns>List of the <see cref="MemberAccessCipherDetails"/></returns>;
|
||||
public async Task<IEnumerable<MemberAccessCipherDetails>> GetMemberAccessCipherDetails(MemberAccessCipherDetailsRequest request)
|
||||
{
|
||||
var orgUsers = organizationUsersTwoFactorEnabled.Select(x => x.user).ToList();
|
||||
var groupNameDictionary = orgGroups.ToDictionary(x => x.Id, x => x.Name);
|
||||
var collectionItems = orgItems
|
||||
.SelectMany(x => x.CollectionIds,
|
||||
(cipher, collectionId) => new { Cipher = cipher, CollectionId = collectionId })
|
||||
.GroupBy(y => y.CollectionId,
|
||||
(key, ciphers) => new { CollectionId = key, Ciphers = ciphers });
|
||||
var itemLookup = collectionItems.ToDictionary(x => x.CollectionId.ToString(), x => x.Ciphers.Select(c => c.Cipher.Id.ToString()).ToList());
|
||||
|
||||
var memberAccessCipherDetails = new ConcurrentBag<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;
|
||||
return await _memberAccessCipherDetailsRepository.GetMemberAccessCipherDetailsByOrganizationId(request.OrganizationId);
|
||||
}
|
||||
}
|
||||
|
@ -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 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.Repositories;
|
||||
using Bit.Core.Settings;
|
||||
|
||||
using Bit.Infrastructure.Dapper.Repositories;
|
||||
using Dapper;
|
||||
using Microsoft.Data.SqlClient;
|
||||
@ -26,6 +24,7 @@ public class MemberAccessCipherDetailsRepository : BaseRepository, IMemberAccess
|
||||
{
|
||||
await using var connection = new SqlConnection(ConnectionString);
|
||||
|
||||
|
||||
var result = await connection.QueryAsync<MemberAccessCipherDetails>(
|
||||
"[dbo].[MemberAccessReport_GetMemberAccessCipherDetailsByOrganizationId]",
|
||||
new
|
||||
@ -35,6 +34,7 @@ public class MemberAccessCipherDetailsRepository : BaseRepository, IMemberAccess
|
||||
}, commandType: CommandType.StoredProcedure);
|
||||
|
||||
return result;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -114,6 +114,7 @@ public class DatabaseContext : DbContext
|
||||
var eOrganizationConnection = builder.Entity<OrganizationConnection>();
|
||||
var eOrganizationDomain = builder.Entity<OrganizationDomain>();
|
||||
var aWebAuthnCredential = builder.Entity<WebAuthnCredential>();
|
||||
var eMemberAccessCipherDetails = builder.Entity<MemberAccessCipherDetails>();
|
||||
|
||||
// Shadow property configurations go here
|
||||
|
||||
@ -136,6 +137,8 @@ public class DatabaseContext : DbContext
|
||||
eCollectionGroup.HasKey(cg => new { cg.CollectionId, cg.GroupId });
|
||||
eGroupUser.HasKey(gu => new { gu.GroupId, gu.OrganizationUserId });
|
||||
|
||||
eMemberAccessCipherDetails.HasNoKey();
|
||||
|
||||
var dataProtector = this.GetService<DP.IDataProtectionProvider>().CreateProtector(
|
||||
Constants.DatabaseFieldProtectorPurpose);
|
||||
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);
|
||||
|
||||
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 =>
|
||||
{
|
||||
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);
|
||||
|
||||
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 =>
|
||||
{
|
||||
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
|
||||
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 =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
|
Loading…
x
Reference in New Issue
Block a user