diff --git a/src/Core/Dirt/Reports/ReportFeatures/MemberAccessCipherDetailsQuery.cs b/src/Core/Dirt/Reports/ReportFeatures/MemberAccessCipherDetailsQuery.cs
index c8cc7212f6..b485c1976a 100644
--- a/src/Core/Dirt/Reports/ReportFeatures/MemberAccessCipherDetailsQuery.cs
+++ b/src/Core/Dirt/Reports/ReportFeatures/MemberAccessCipherDetailsQuery.cs
@@ -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;
+///
+/// Generates the member access report.
+///
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> GetMemberAccessCipherDetails(MemberAccessCipherDetailsRequest request)
- {
- return await _memberAccessCipherDetailsRepository.GetMemberAccessCipherDetailsByOrganizationId(request.OrganizationId);
- }
-
- private IEnumerable GenerateAccessData(
- ICollection orgGroups,
- ICollection> orgCollectionsWithAccess,
- IEnumerable orgItems,
- IEnumerable<(OrganizationUserUserDetails user, bool twoFactorIsEnabled)> organizationUsersTwoFactorEnabled,
- OrganizationAbility orgAbility)
- {
- var orgUsers = organizationUsersTwoFactorEnabled.Select(x => x.user);
- // Create a dictionary to lookup the group names later.
- var groupNameDictionary = orgGroups.ToDictionary(x => x.Id, x => x.Name);
-
- // Get collections grouped and into a dictionary for counts
- var collectionItems = orgItems
- .SelectMany(x => x.CollectionIds,
- (cipher, collectionId) => new { Cipher = cipher, CollectionId = collectionId })
- .GroupBy(y => y.CollectionId,
- (key, ciphers) => new { CollectionId = key, Ciphers = ciphers });
- var itemLookup = collectionItems.ToDictionary(x => x.CollectionId.ToString(), x => x.Ciphers.Select(c => c.Cipher.Id.ToString()));
-
- // Loop through the org users and populate report and access data
- var memberAccessCipherDetails = new List();
- foreach (var user in orgUsers)
- {
- var groupAccessDetails = new List();
- var userCollectionAccessDetails = new List();
- foreach (var tCollect in orgCollectionsWithAccess)
- {
- var hasItems = itemLookup.TryGetValue(tCollect.Item1.Id.ToString(), out var items);
- var collectionCiphers = hasItems ? items.Select(x => x) : null;
-
- var itemCounts = hasItems ? collectionCiphers.Count() : 0;
- if (tCollect.Item2.Groups.Count() > 0)
- {
-
- var groupDetails = tCollect.Item2.Groups.Where((tCollectGroups) => user.Groups.Contains(tCollectGroups.Id)).Select(x =>
- new MemberAccessDetails
- {
- CollectionId = tCollect.Item1.Id,
- CollectionName = tCollect.Item1.Name,
- GroupId = x.Id,
- GroupName = groupNameDictionary[x.Id],
- ReadOnly = x.ReadOnly,
- HidePasswords = x.HidePasswords,
- Manage = x.Manage,
- ItemCount = itemCounts,
- CollectionCipherIds = items
- });
-
- groupAccessDetails.AddRange(groupDetails);
- }
-
- // All collections assigned to users and their permissions
- if (tCollect.Item2.Users.Count() > 0)
- {
- var userCollectionDetails = tCollect.Item2.Users.Where((tCollectUser) => tCollectUser.Id == user.Id).Select(x =>
- new MemberAccessDetails
- {
- CollectionId = tCollect.Item1.Id,
- CollectionName = tCollect.Item1.Name,
- ReadOnly = x.ReadOnly,
- HidePasswords = x.HidePasswords,
- Manage = x.Manage,
- ItemCount = itemCounts,
- CollectionCipherIds = items
- });
- userCollectionAccessDetails.AddRange(userCollectionDetails);
- }
- }
-
- var report = new MemberAccessCipherDetails
- {
- UserName = user.Name,
- Email = user.Email,
- TwoFactorEnabled = organizationUsersTwoFactorEnabled.FirstOrDefault(u => u.user.Id == user.Id).twoFactorIsEnabled,
- // Both the user's ResetPasswordKey must be set and the organization can UseResetPassword
- AccountRecoveryEnabled = !string.IsNullOrEmpty(user.ResetPasswordKey) && orgAbility.UseResetPassword,
- UserGuid = user.Id,
- UsesKeyConnector = user.UsesKeyConnector
- };
-
- var userAccessDetails = new List();
- if (user.Groups.Any())
- {
- var userGroups = groupAccessDetails.Where(x => user.Groups.Contains(x.GroupId.GetValueOrDefault()));
- userAccessDetails.AddRange(userGroups);
- }
-
- // There can be edge cases where groups don't have a collection
- var groupsWithoutCollections = user.Groups.Where(x => !userAccessDetails.Any(y => x == y.GroupId));
- if (groupsWithoutCollections.Count() > 0)
- {
- var emptyGroups = groupsWithoutCollections.Select(x => new MemberAccessDetails
- {
- GroupId = x,
- GroupName = groupNameDictionary[x],
- ItemCount = 0
- });
- userAccessDetails.AddRange(emptyGroups);
- }
-
- if (user.Collections.Any())
- {
- var userCollections = userCollectionAccessDetails.Where(x => user.Collections.Any(y => x.CollectionId == y.Id));
- userAccessDetails.AddRange(userCollections);
- }
- report.AccessDetails = userAccessDetails;
-
- var userCiphers =
- report.AccessDetails
- .Where(x => x.ItemCount > 0)
- .SelectMany(y => y.CollectionCipherIds)
- .Distinct();
- report.CipherIds = userCiphers;
- report.TotalItemCount = userCiphers.Count();
-
- // Distinct items only
- var distinctItems = report.AccessDetails.Where(x => x.CollectionId.HasValue).Select(x => x.CollectionId).Distinct();
- report.CollectionsCount = distinctItems.Count();
- report.GroupsCount = report.AccessDetails.Select(x => x.GroupId).Where(y => y.HasValue).Distinct().Count();
- memberAccessCipherDetails.Add(report);
- }
- return memberAccessCipherDetails;
- }
-
///
/// Generates a report for all members of an organization. Containing summary information
/// such as item, collection, and group counts. Including the cipherIds a member is assigned.
/// Child collection includes detailed information on the user and group collections along
/// with their permissions.
///
- /// Organization groups collection
- /// Collections for the organization and the groups/users and permissions
- /// Cipher items for the organization with the collections associated with them
- /// Organization users and two factor status
- /// Organization ability for account recovery status
- /// List of the MemberAccessCipherDetailsModel;
- private IEnumerable GenerateAccessDataParallel(
- ICollection orgGroups,
- ICollection> orgCollectionsWithAccess,
- IEnumerable orgItems,
- IEnumerable<(OrganizationUserUserDetails user, bool twoFactorIsEnabled)> organizationUsersTwoFactorEnabled,
- OrganizationAbility orgAbility)
+ /// need organizationId field to get data
+ /// List of the ;
+ public async Task> 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();
-
- Parallel.ForEach(orgUsers, user =>
- {
- var groupAccessDetails = new List();
- var userCollectionAccessDetails = new List();
-
- 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();
- 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 GenerateAccessDataParallelV2(
- ICollection orgGroups,
- ICollection> orgCollectionsWithAccess,
- IEnumerable 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();
-
- // 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();
-
- // Process group access details
- var userGroupIds = new HashSet(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(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())
- .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);
}
}
diff --git a/src/Core/Dirt/Reports/ReportFeatures/ReportingServiceCollectionExtensions.cs b/src/Core/Dirt/Reports/ReportFeatures/ReportingServiceCollectionExtensions.cs
index 4970f0515b..e979ef84a2 100644
--- a/src/Core/Dirt/Reports/ReportFeatures/ReportingServiceCollectionExtensions.cs
+++ b/src/Core/Dirt/Reports/ReportFeatures/ReportingServiceCollectionExtensions.cs
@@ -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;
diff --git a/src/Infrastructure.Dapper/Dirt/MemberAccessCipherDetailsRepository.cs b/src/Infrastructure.Dapper/Dirt/MemberAccessCipherDetailsRepository.cs
index c005053320..656d685100 100644
--- a/src/Infrastructure.Dapper/Dirt/MemberAccessCipherDetailsRepository.cs
+++ b/src/Infrastructure.Dapper/Dirt/MemberAccessCipherDetailsRepository.cs
@@ -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(
"[dbo].[MemberAccessReport_GetMemberAccessCipherDetailsByOrganizationId]",
new
@@ -35,6 +34,7 @@ public class MemberAccessCipherDetailsRepository : BaseRepository, IMemberAccess
}, commandType: CommandType.StoredProcedure);
return result;
+
}
}
diff --git a/src/Infrastructure.EntityFramework/Repositories/DatabaseContext.cs b/src/Infrastructure.EntityFramework/Repositories/DatabaseContext.cs
index 71df8591ac..8eaaab62a5 100644
--- a/src/Infrastructure.EntityFramework/Repositories/DatabaseContext.cs
+++ b/src/Infrastructure.EntityFramework/Repositories/DatabaseContext.cs
@@ -114,6 +114,7 @@ public class DatabaseContext : DbContext
var eOrganizationConnection = builder.Entity();
var eOrganizationDomain = builder.Entity();
var aWebAuthnCredential = builder.Entity();
+ var eMemberAccessCipherDetails = builder.Entity();
// 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().CreateProtector(
Constants.DatabaseFieldProtectorPurpose);
var dataProtectionConverter = new DataProtectionConverter(dataProtector);
diff --git a/util/Migrator/DbScripts/2025-06-04_00_MemberAccessCipherDetailsByOrgId.sql b/util/Migrator/DbScripts/2025-06-04_00_MemberAccessCipherDetailsByOrgId.sql
new file mode 100644
index 0000000000..ed3cd260d4
--- /dev/null
+++ b/util/Migrator/DbScripts/2025-06-04_00_MemberAccessCipherDetailsByOrgId.sql
@@ -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
diff --git a/util/MySqlMigrations/Migrations/20250604194110_2025-06-04_00_MemberAccessCipherDetailsByOrgId.sql.Designer.cs b/util/MySqlMigrations/Migrations/20250604194110_2025-06-04_00_MemberAccessCipherDetailsByOrgId.sql.Designer.cs
new file mode 100644
index 0000000000..554386003a
--- /dev/null
+++ b/util/MySqlMigrations/Migrations/20250604194110_2025-06-04_00_MemberAccessCipherDetailsByOrgId.sql.Designer.cs
@@ -0,0 +1,3154 @@
+//
+using System;
+using Bit.Infrastructure.EntityFramework.Repositories;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace Bit.MySqlMigrations.Migrations
+{
+ [DbContext(typeof(DatabaseContext))]
+ [Migration("20250604194110_2025-06-04_00_MemberAccessCipherDetailsByOrgId.sql")]
+ partial class _20250604_00_MemberAccessCipherDetailsByOrgIdsql
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "8.0.8")
+ .HasAnnotation("Relational:MaxIdentifierLength", 64);
+
+ MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder);
+
+ modelBuilder.Entity("Bit.Core.Dirt.Reports.Models.Data.MemberAccessCipherDetails", b =>
+ {
+ b.Property("AccountRecoveryEnabled")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("CipherIds")
+ .HasColumnType("longtext");
+
+ b.Property("CollectionsCount")
+ .HasColumnType("int");
+
+ b.Property("Email")
+ .HasColumnType("longtext");
+
+ b.Property("GroupsCount")
+ .HasColumnType("int");
+
+ b.Property("TotalItemCount")
+ .HasColumnType("int");
+
+ b.Property("TwoFactorEnabled")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("UserGuid")
+ .HasColumnType("char(36)");
+
+ b.Property("UserName")
+ .HasColumnType("longtext");
+
+ b.Property("UsesKeyConnector")
+ .HasColumnType("tinyint(1)");
+
+ b.ToTable("MemberAccessCipherDetails");
+ });
+
+ modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("char(36)");
+
+ b.Property("AllowAdminAccessToAllCollectionItems")
+ .HasColumnType("tinyint(1)")
+ .HasDefaultValue(true);
+
+ b.Property("BillingEmail")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("varchar(256)");
+
+ b.Property("BusinessAddress1")
+ .HasMaxLength(50)
+ .HasColumnType("varchar(50)");
+
+ b.Property("BusinessAddress2")
+ .HasMaxLength(50)
+ .HasColumnType("varchar(50)");
+
+ b.Property("BusinessAddress3")
+ .HasMaxLength(50)
+ .HasColumnType("varchar(50)");
+
+ b.Property("BusinessCountry")
+ .HasMaxLength(2)
+ .HasColumnType("varchar(2)");
+
+ b.Property("BusinessName")
+ .HasMaxLength(50)
+ .HasColumnType("varchar(50)");
+
+ b.Property("BusinessTaxNumber")
+ .HasMaxLength(30)
+ .HasColumnType("varchar(30)");
+
+ b.Property("CreationDate")
+ .HasColumnType("datetime(6)");
+
+ b.Property("Enabled")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("ExpirationDate")
+ .HasColumnType("datetime(6)");
+
+ b.Property("Gateway")
+ .HasColumnType("tinyint unsigned");
+
+ b.Property("GatewayCustomerId")
+ .HasMaxLength(50)
+ .HasColumnType("varchar(50)");
+
+ b.Property("GatewaySubscriptionId")
+ .HasMaxLength(50)
+ .HasColumnType("varchar(50)");
+
+ b.Property("Identifier")
+ .HasMaxLength(50)
+ .HasColumnType("varchar(50)");
+
+ b.Property("LicenseKey")
+ .HasMaxLength(100)
+ .HasColumnType("varchar(100)");
+
+ b.Property("LimitCollectionCreation")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("LimitCollectionDeletion")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("LimitItemDeletion")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("MaxAutoscaleSeats")
+ .HasColumnType("int");
+
+ b.Property("MaxAutoscaleSmSeats")
+ .HasColumnType("int");
+
+ b.Property("MaxAutoscaleSmServiceAccounts")
+ .HasColumnType("int");
+
+ b.Property("MaxCollections")
+ .HasColumnType("smallint");
+
+ b.Property("MaxStorageGb")
+ .HasColumnType("smallint");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("varchar(50)");
+
+ b.Property("OwnersNotifiedOfAutoscaling")
+ .HasColumnType("datetime(6)");
+
+ b.Property("Plan")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("varchar(50)");
+
+ b.Property("PlanType")
+ .HasColumnType("tinyint unsigned");
+
+ b.Property("PrivateKey")
+ .HasColumnType("longtext");
+
+ b.Property("PublicKey")
+ .HasColumnType("longtext");
+
+ b.Property("ReferenceData")
+ .HasColumnType("longtext");
+
+ b.Property("RevisionDate")
+ .HasColumnType("datetime(6)");
+
+ b.Property("Seats")
+ .HasColumnType("int");
+
+ b.Property("SelfHost")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("SmSeats")
+ .HasColumnType("int");
+
+ b.Property("SmServiceAccounts")
+ .HasColumnType("int");
+
+ b.Property("Status")
+ .HasColumnType("tinyint unsigned");
+
+ b.Property("Storage")
+ .HasColumnType("bigint");
+
+ b.Property("TwoFactorProviders")
+ .HasColumnType("longtext");
+
+ b.Property("Use2fa")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("UseAdminSponsoredFamilies")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("UseApi")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("UseCustomPermissions")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("UseDirectory")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("UseEvents")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("UseGroups")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("UseKeyConnector")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("UseOrganizationDomains")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("UsePasswordManager")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("UsePolicies")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("UseResetPassword")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("UseRiskInsights")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("UseScim")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("UseSecretsManager")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("UseSso")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("UseTotp")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("UsersGetPremium")
+ .HasColumnType("tinyint(1)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Id", "Enabled")
+ .HasAnnotation("Npgsql:IndexInclude", new[] { "UseTotp" });
+
+ b.ToTable("Organization", (string)null);
+ });
+
+ modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("char(36)");
+
+ b.Property("Configuration")
+ .HasColumnType("longtext");
+
+ b.Property("CreationDate")
+ .HasColumnType("datetime(6)");
+
+ b.Property("OrganizationId")
+ .HasColumnType("char(36)");
+
+ b.Property("RevisionDate")
+ .HasColumnType("datetime(6)");
+
+ b.Property("Type")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ b.HasIndex("OrganizationId")
+ .HasAnnotation("SqlServer:Clustered", false);
+
+ b.HasIndex("OrganizationId", "Type")
+ .IsUnique()
+ .HasAnnotation("SqlServer:Clustered", false);
+
+ b.ToTable("OrganizationIntegration", (string)null);
+ });
+
+ modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegrationConfiguration", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("char(36)");
+
+ b.Property("Configuration")
+ .HasColumnType("longtext");
+
+ b.Property("CreationDate")
+ .HasColumnType("datetime(6)");
+
+ b.Property("EventType")
+ .HasColumnType("int");
+
+ b.Property("OrganizationIntegrationId")
+ .HasColumnType("char(36)");
+
+ b.Property("RevisionDate")
+ .HasColumnType("datetime(6)");
+
+ b.Property("Template")
+ .HasColumnType("longtext");
+
+ b.HasKey("Id");
+
+ b.HasIndex("OrganizationIntegrationId");
+
+ b.ToTable("OrganizationIntegrationConfiguration", (string)null);
+ });
+
+ modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("char(36)");
+
+ b.Property("CreationDate")
+ .HasColumnType("datetime(6)");
+
+ b.Property("Data")
+ .HasColumnType("longtext");
+
+ b.Property("Enabled")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("OrganizationId")
+ .HasColumnType("char(36)");
+
+ b.Property("RevisionDate")
+ .HasColumnType("datetime(6)");
+
+ b.Property("Type")
+ .HasColumnType("tinyint unsigned");
+
+ b.HasKey("Id");
+
+ b.HasIndex("OrganizationId")
+ .HasAnnotation("SqlServer:Clustered", false);
+
+ b.HasIndex("OrganizationId", "Type")
+ .IsUnique()
+ .HasAnnotation("SqlServer:Clustered", false);
+
+ b.ToTable("Policy", (string)null);
+ });
+
+ modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("char(36)");
+
+ b.Property("BillingEmail")
+ .HasColumnType("longtext");
+
+ b.Property("BillingPhone")
+ .HasColumnType("longtext");
+
+ b.Property("BusinessAddress1")
+ .HasColumnType("longtext");
+
+ b.Property("BusinessAddress2")
+ .HasColumnType("longtext");
+
+ b.Property("BusinessAddress3")
+ .HasColumnType("longtext");
+
+ b.Property("BusinessCountry")
+ .HasColumnType("longtext");
+
+ b.Property("BusinessName")
+ .HasColumnType("longtext");
+
+ b.Property("BusinessTaxNumber")
+ .HasColumnType("longtext");
+
+ b.Property("CreationDate")
+ .HasColumnType("datetime(6)");
+
+ b.Property("DiscountId")
+ .HasColumnType("longtext");
+
+ b.Property("Enabled")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("Gateway")
+ .HasColumnType("tinyint unsigned");
+
+ b.Property("GatewayCustomerId")
+ .HasColumnType("longtext");
+
+ b.Property("GatewaySubscriptionId")
+ .HasColumnType("longtext");
+
+ b.Property("Name")
+ .HasColumnType("longtext");
+
+ b.Property("RevisionDate")
+ .HasColumnType("datetime(6)");
+
+ b.Property("Status")
+ .HasColumnType("tinyint unsigned");
+
+ b.Property("Type")
+ .HasColumnType("tinyint unsigned");
+
+ b.Property("UseEvents")
+ .HasColumnType("tinyint(1)");
+
+ b.HasKey("Id");
+
+ b.ToTable("Provider", (string)null);
+ });
+
+ modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("char(36)");
+
+ b.Property("CreationDate")
+ .HasColumnType("datetime(6)");
+
+ b.Property("Key")
+ .HasColumnType("longtext");
+
+ b.Property("OrganizationId")
+ .HasColumnType("char(36)");
+
+ b.Property("ProviderId")
+ .HasColumnType("char(36)");
+
+ b.Property("RevisionDate")
+ .HasColumnType("datetime(6)");
+
+ b.Property("Settings")
+ .HasColumnType("longtext");
+
+ b.HasKey("Id");
+
+ b.HasIndex("OrganizationId");
+
+ b.HasIndex("ProviderId");
+
+ b.ToTable("ProviderOrganization", (string)null);
+ });
+
+ modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("char(36)");
+
+ b.Property("CreationDate")
+ .HasColumnType("datetime(6)");
+
+ b.Property("Email")
+ .HasColumnType("longtext");
+
+ b.Property("Key")
+ .HasColumnType("longtext");
+
+ b.Property("Permissions")
+ .HasColumnType("longtext");
+
+ b.Property("ProviderId")
+ .HasColumnType("char(36)");
+
+ b.Property("RevisionDate")
+ .HasColumnType("datetime(6)");
+
+ b.Property("Status")
+ .HasColumnType("tinyint unsigned");
+
+ b.Property("Type")
+ .HasColumnType("tinyint unsigned");
+
+ b.Property("UserId")
+ .HasColumnType("char(36)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ProviderId");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("ProviderUser", (string)null);
+ });
+
+ modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("char(36)");
+
+ b.Property("AccessCode")
+ .HasMaxLength(25)
+ .HasColumnType("varchar(25)");
+
+ b.Property("Approved")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("AuthenticationDate")
+ .HasColumnType("datetime(6)");
+
+ b.Property("CreationDate")
+ .HasColumnType("datetime(6)");
+
+ b.Property("Key")
+ .HasColumnType("longtext");
+
+ b.Property("MasterPasswordHash")
+ .HasColumnType("longtext");
+
+ b.Property("OrganizationId")
+ .HasColumnType("char(36)");
+
+ b.Property("PublicKey")
+ .HasColumnType("longtext");
+
+ b.Property("RequestCountryName")
+ .HasMaxLength(200)
+ .HasColumnType("varchar(200)");
+
+ b.Property("RequestDeviceIdentifier")
+ .HasMaxLength(50)
+ .HasColumnType("varchar(50)");
+
+ b.Property("RequestDeviceType")
+ .HasColumnType("tinyint unsigned");
+
+ b.Property("RequestIpAddress")
+ .HasMaxLength(50)
+ .HasColumnType("varchar(50)");
+
+ b.Property("ResponseDate")
+ .HasColumnType("datetime(6)");
+
+ b.Property("ResponseDeviceId")
+ .HasColumnType("char(36)");
+
+ b.Property("Type")
+ .HasColumnType("tinyint unsigned");
+
+ b.Property("UserId")
+ .HasColumnType("char(36)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("OrganizationId");
+
+ b.HasIndex("ResponseDeviceId");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AuthRequest", (string)null);
+ });
+
+ modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("char(36)");
+
+ b.Property("CreationDate")
+ .HasColumnType("datetime(6)");
+
+ b.Property("Email")
+ .HasMaxLength(256)
+ .HasColumnType("varchar(256)");
+
+ b.Property("GranteeId")
+ .HasColumnType("char(36)");
+
+ b.Property("GrantorId")
+ .HasColumnType("char(36)");
+
+ b.Property("KeyEncrypted")
+ .HasColumnType("longtext");
+
+ b.Property("LastNotificationDate")
+ .HasColumnType("datetime(6)");
+
+ b.Property("RecoveryInitiatedDate")
+ .HasColumnType("datetime(6)");
+
+ b.Property("RevisionDate")
+ .HasColumnType("datetime(6)");
+
+ b.Property("Status")
+ .HasColumnType("tinyint unsigned");
+
+ b.Property("Type")
+ .HasColumnType("tinyint unsigned");
+
+ b.Property("WaitTimeDays")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ b.HasIndex("GranteeId");
+
+ b.HasIndex("GrantorId");
+
+ b.ToTable("EmergencyAccess", (string)null);
+ });
+
+ modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.Grant", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id"));
+
+ b.Property("ClientId")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("varchar(200)");
+
+ b.Property("ConsumedDate")
+ .HasColumnType("datetime(6)");
+
+ b.Property("CreationDate")
+ .HasColumnType("datetime(6)");
+
+ b.Property("Data")
+ .IsRequired()
+ .HasColumnType("longtext");
+
+ b.Property("Description")
+ .HasMaxLength(200)
+ .HasColumnType("varchar(200)");
+
+ b.Property("ExpirationDate")
+ .HasColumnType("datetime(6)");
+
+ b.Property("Key")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("varchar(200)");
+
+ b.Property("SessionId")
+ .HasMaxLength(100)
+ .HasColumnType("varchar(100)");
+
+ b.Property("SubjectId")
+ .HasMaxLength(200)
+ .HasColumnType("varchar(200)");
+
+ b.Property("Type")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("varchar(50)");
+
+ b.HasKey("Id")
+ .HasName("PK_Grant")
+ .HasAnnotation("SqlServer:Clustered", true);
+
+ b.HasIndex("ExpirationDate")
+ .HasAnnotation("SqlServer:Clustered", false);
+
+ b.HasIndex("Key")
+ .IsUnique();
+
+ b.ToTable("Grant", (string)null);
+ });
+
+ modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id"));
+
+ b.Property("CreationDate")
+ .HasColumnType("datetime(6)");
+
+ b.Property("Data")
+ .HasColumnType("longtext");
+
+ b.Property("Enabled")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("OrganizationId")
+ .HasColumnType("char(36)");
+
+ b.Property("RevisionDate")
+ .HasColumnType("datetime(6)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("OrganizationId");
+
+ b.ToTable("SsoConfig", (string)null);
+ });
+
+ modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id"));
+
+ b.Property("CreationDate")
+ .HasColumnType("datetime(6)");
+
+ b.Property("ExternalId")
+ .HasMaxLength(300)
+ .HasColumnType("varchar(300)");
+
+ b.Property("OrganizationId")
+ .HasColumnType("char(36)");
+
+ b.Property("UserId")
+ .HasColumnType("char(36)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("OrganizationId")
+ .HasAnnotation("SqlServer:Clustered", false);
+
+ b.HasIndex("UserId");
+
+ b.HasIndex("OrganizationId", "ExternalId")
+ .IsUnique()
+ .HasAnnotation("Npgsql:IndexInclude", new[] { "UserId" })
+ .HasAnnotation("SqlServer:Clustered", false);
+
+ b.HasIndex("OrganizationId", "UserId")
+ .IsUnique()
+ .HasAnnotation("SqlServer:Clustered", false);
+
+ b.ToTable("SsoUser", (string)null);
+ });
+
+ modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("char(36)");
+
+ b.Property("AaGuid")
+ .HasColumnType("char(36)");
+
+ b.Property("Counter")
+ .HasColumnType("int");
+
+ b.Property("CreationDate")
+ .HasColumnType("datetime(6)");
+
+ b.Property("CredentialId")
+ .HasMaxLength(256)
+ .HasColumnType("varchar(256)");
+
+ b.Property("EncryptedPrivateKey")
+ .HasMaxLength(2000)
+ .HasColumnType("varchar(2000)");
+
+ b.Property("EncryptedPublicKey")
+ .HasMaxLength(2000)
+ .HasColumnType("varchar(2000)");
+
+ b.Property("EncryptedUserKey")
+ .HasMaxLength(2000)
+ .HasColumnType("varchar(2000)");
+
+ b.Property("Name")
+ .HasMaxLength(50)
+ .HasColumnType("varchar(50)");
+
+ b.Property("PublicKey")
+ .HasMaxLength(256)
+ .HasColumnType("varchar(256)");
+
+ b.Property("RevisionDate")
+ .HasColumnType("datetime(6)");
+
+ b.Property("SupportsPrf")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("Type")
+ .HasMaxLength(20)
+ .HasColumnType("varchar(20)");
+
+ b.Property("UserId")
+ .HasColumnType("char(36)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("WebAuthnCredential", (string)null);
+ });
+
+ modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ClientOrganizationMigrationRecord", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("char(36)");
+
+ b.Property("ExpirationDate")
+ .HasColumnType("datetime(6)");
+
+ b.Property("GatewayCustomerId")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("varchar(50)");
+
+ b.Property("GatewaySubscriptionId")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("varchar(50)");
+
+ b.Property("MaxAutoscaleSeats")
+ .HasColumnType("int");
+
+ b.Property("MaxStorageGb")
+ .HasColumnType("smallint");
+
+ b.Property("OrganizationId")
+ .HasColumnType("char(36)");
+
+ b.Property("PlanType")
+ .HasColumnType("tinyint unsigned");
+
+ b.Property("ProviderId")
+ .HasColumnType("char(36)");
+
+ b.Property("Seats")
+ .HasColumnType("int");
+
+ b.Property("Status")
+ .HasColumnType("tinyint unsigned");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ProviderId", "OrganizationId")
+ .IsUnique();
+
+ b.ToTable("ClientOrganizationMigrationRecord", (string)null);
+ });
+
+ modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("char(36)");
+
+ b.Property("CreationDate")
+ .HasColumnType("datetime(6)");
+
+ b.Property("InstallationId")
+ .HasColumnType("char(36)");
+
+ b.Property("OrganizationId")
+ .HasColumnType("char(36)");
+
+ b.Property("RevisionDate")
+ .HasColumnType("datetime(6)");
+
+ b.HasKey("Id")
+ .HasAnnotation("SqlServer:Clustered", true);
+
+ b.HasIndex("InstallationId")
+ .HasAnnotation("SqlServer:Clustered", false);
+
+ b.HasIndex("OrganizationId")
+ .HasAnnotation("SqlServer:Clustered", false);
+
+ b.ToTable("OrganizationInstallation", (string)null);
+ });
+
+ modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("char(36)");
+
+ b.Property("AssignedSeats")
+ .HasColumnType("int");
+
+ b.Property("ClientId")
+ .HasColumnType("char(36)");
+
+ b.Property("ClientName")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("varchar(50)");
+
+ b.Property("Created")
+ .HasColumnType("datetime(6)");
+
+ b.Property("InvoiceId")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("varchar(50)");
+
+ b.Property("InvoiceNumber")
+ .HasMaxLength(50)
+ .HasColumnType("varchar(50)");
+
+ b.Property("PlanName")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("varchar(50)");
+
+ b.Property("ProviderId")
+ .HasColumnType("char(36)");
+
+ b.Property("Total")
+ .HasColumnType("decimal(65,30)");
+
+ b.Property("UsedSeats")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ProviderId");
+
+ b.ToTable("ProviderInvoiceItem", (string)null);
+ });
+
+ modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("char(36)");
+
+ b.Property