1
0
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:
Graham Walker 2025-06-04 15:00:42 -05:00
parent a0ccc98109
commit c082c91108
No known key found for this signature in database
14 changed files with 9708 additions and 408 deletions

View File

@ -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;
} }
} }

View File

@ -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;

View File

@ -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;
} }
} }

View File

@ -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);

View File

@ -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

View File

@ -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)
{
}
}

View File

@ -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")

View File

@ -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)
{
}
}

View File

@ -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")

View File

@ -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)
{
}
}

View File

@ -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")