diff --git a/src/Api/Dirt/Controllers/ReportsController.cs b/src/Api/Dirt/Controllers/ReportsController.cs
index 2f7a5a4328..8281bdaa98 100644
--- a/src/Api/Dirt/Controllers/ReportsController.cs
+++ b/src/Api/Dirt/Controllers/ReportsController.cs
@@ -1,5 +1,6 @@
using Bit.Api.Dirt.Models;
using Bit.Api.Dirt.Models.Response;
+using Bit.Api.Tools.Models.Response;
using Bit.Core.Context;
using Bit.Core.Dirt.Reports.Entities;
using Bit.Core.Dirt.Reports.Models.Data;
@@ -17,21 +18,24 @@ namespace Bit.Api.Dirt.Controllers;
public class ReportsController : Controller
{
private readonly ICurrentContext _currentContext;
- private readonly IMemberAccessCipherDetailsQuery _memberAccessCipherDetailsQuery;
+ private readonly IMemberAccessReportQuery _memberAccessReportQuery;
+ private readonly IRiskInsightsReportQuery _riskInsightsReportQuery;
private readonly IAddPasswordHealthReportApplicationCommand _addPwdHealthReportAppCommand;
private readonly IGetPasswordHealthReportApplicationQuery _getPwdHealthReportAppQuery;
private readonly IDropPasswordHealthReportApplicationCommand _dropPwdHealthReportAppCommand;
public ReportsController(
ICurrentContext currentContext,
- IMemberAccessCipherDetailsQuery memberAccessCipherDetailsQuery,
+ IMemberAccessReportQuery memberAccessReportQuery,
+ IRiskInsightsReportQuery riskInsightsReportQuery,
IAddPasswordHealthReportApplicationCommand addPasswordHealthReportApplicationCommand,
IGetPasswordHealthReportApplicationQuery getPasswordHealthReportApplicationQuery,
IDropPasswordHealthReportApplicationCommand dropPwdHealthReportAppCommand
)
{
_currentContext = currentContext;
- _memberAccessCipherDetailsQuery = memberAccessCipherDetailsQuery;
+ _memberAccessReportQuery = memberAccessReportQuery;
+ _riskInsightsReportQuery = riskInsightsReportQuery;
_addPwdHealthReportAppCommand = addPasswordHealthReportApplicationCommand;
_getPwdHealthReportAppQuery = getPasswordHealthReportApplicationQuery;
_dropPwdHealthReportAppCommand = dropPwdHealthReportAppCommand;
@@ -54,9 +58,9 @@ public class ReportsController : Controller
throw new NotFoundException();
}
- var memberCipherDetails = await GetMemberCipherDetails(new MemberAccessCipherDetailsRequest { OrganizationId = orgId });
+ var riskDetails = await GetRiskInsightsReportDetails(new RiskInsightsReportRequest { OrganizationId = orgId });
- var responses = memberCipherDetails.Select(x => new MemberCipherDetailsResponseModel(x));
+ var responses = riskDetails.Select(x => new MemberCipherDetailsResponseModel(x));
return responses;
}
@@ -69,16 +73,16 @@ public class ReportsController : Controller
/// IEnumerable of MemberAccessReportResponseModel
/// If Access reports permission is not assigned
[HttpGet("member-access/{orgId}")]
- public async Task> GetMemberAccessReport(Guid orgId)
+ public async Task> GetMemberAccessReport(Guid orgId)
{
if (!await _currentContext.AccessReports(orgId))
{
throw new NotFoundException();
}
- var memberCipherDetails = await GetMemberCipherDetails(new MemberAccessCipherDetailsRequest { OrganizationId = orgId });
+ var accessDetails = await GetMemberAccessDetails(new MemberAccessReportRequest { OrganizationId = orgId });
- var responses = memberCipherDetails.Select(x => new MemberAccessReportResponseModel(x));
+ var responses = accessDetails.Select(x => new MemberAccessDetailReportResponseModel(x));
return responses;
}
@@ -87,13 +91,28 @@ public class ReportsController : Controller
/// Contains the organization member info, the cipher ids associated with the member,
/// and details on their collections, groups, and permissions
///
- /// Request to the MemberAccessCipherDetailsQuery
- /// IEnumerable of MemberAccessCipherDetails
- private async Task> GetMemberCipherDetails(MemberAccessCipherDetailsRequest request)
+ /// Request parameters
+ ///
+ /// List of a user's permissions at a group and collection level as well as the number of ciphers
+ /// associated with that group/collection
+ ///
+ private async Task> GetMemberAccessDetails(
+ MemberAccessReportRequest request)
{
- var memberCipherDetails =
- await _memberAccessCipherDetailsQuery.GetMemberAccessCipherDetails(request);
- return memberCipherDetails;
+ var accessDetails = await _memberAccessReportQuery.GetMemberAccessReportsAsync(request);
+ return accessDetails;
+ }
+
+ ///
+ /// Gets the risk insights report details from the risk insights query. Associates a user to their cipher ids
+ ///
+ /// Request parameters
+ /// A list of risk insights data associating the user to cipher ids
+ private async Task> GetRiskInsightsReportDetails(
+ RiskInsightsReportRequest request)
+ {
+ var riskDetails = await _riskInsightsReportQuery.GetRiskInsightsReportDetails(request);
+ return riskDetails;
}
///
diff --git a/src/Api/Dirt/Models/Response/MemberAccessDetailReportResponseModel.cs b/src/Api/Dirt/Models/Response/MemberAccessDetailReportResponseModel.cs
new file mode 100644
index 0000000000..2d5a7b1556
--- /dev/null
+++ b/src/Api/Dirt/Models/Response/MemberAccessDetailReportResponseModel.cs
@@ -0,0 +1,39 @@
+using Bit.Core.Dirt.Reports.Models.Data;
+
+namespace Bit.Api.Tools.Models.Response;
+
+public class MemberAccessDetailReportResponseModel
+{
+ public Guid? UserGuid { get; set; }
+ public string UserName { get; set; }
+ public string Email { get; set; }
+ public bool TwoFactorEnabled { get; set; }
+ public bool AccountRecoveryEnabled { get; set; }
+ public bool UsesKeyConnector { get; set; }
+ public Guid? CollectionId { get; set; }
+ public Guid? GroupId { get; set; }
+ public string GroupName { get; set; }
+ public string CollectionName { get; set; }
+ public bool? ReadOnly { get; set; }
+ public bool? HidePasswords { get; set; }
+ public bool? Manage { get; set; }
+ public IEnumerable CipherIds { get; set; }
+
+ public MemberAccessDetailReportResponseModel(MemberAccessReportDetail reportDetail)
+ {
+ UserGuid = reportDetail.UserGuid;
+ UserName = reportDetail.UserName;
+ Email = reportDetail.Email;
+ TwoFactorEnabled = reportDetail.TwoFactorEnabled;
+ AccountRecoveryEnabled = reportDetail.AccountRecoveryEnabled;
+ UsesKeyConnector = reportDetail.UsesKeyConnector;
+ CollectionId = reportDetail.CollectionId;
+ GroupId = reportDetail.GroupId;
+ GroupName = reportDetail.GroupName;
+ CollectionName = reportDetail.CollectionName;
+ ReadOnly = reportDetail.ReadOnly;
+ HidePasswords = reportDetail.HidePasswords;
+ Manage = reportDetail.Manage;
+ CipherIds = reportDetail.CipherIds;
+ }
+}
diff --git a/src/Api/Dirt/Models/Response/MemberCipherDetailsResponseModel.cs b/src/Api/Dirt/Models/Response/MemberCipherDetailsResponseModel.cs
index 30065ad05a..e5c6235de3 100644
--- a/src/Api/Dirt/Models/Response/MemberCipherDetailsResponseModel.cs
+++ b/src/Api/Dirt/Models/Response/MemberCipherDetailsResponseModel.cs
@@ -15,12 +15,12 @@ public class MemberCipherDetailsResponseModel
///
public IEnumerable CipherIds { get; set; }
- public MemberCipherDetailsResponseModel(MemberAccessCipherDetails memberAccessCipherDetails)
+ public MemberCipherDetailsResponseModel(RiskInsightsReportDetail reportDetail)
{
- this.UserGuid = memberAccessCipherDetails.UserGuid;
- this.UserName = memberAccessCipherDetails.UserName;
- this.Email = memberAccessCipherDetails.Email;
- this.UsesKeyConnector = memberAccessCipherDetails.UsesKeyConnector;
- this.CipherIds = memberAccessCipherDetails.CipherIds;
+ this.UserGuid = reportDetail.UserGuid;
+ this.UserName = reportDetail.UserName;
+ this.Email = reportDetail.Email;
+ this.UsesKeyConnector = reportDetail.UsesKeyConnector;
+ this.CipherIds = reportDetail.CipherIds;
}
}
diff --git a/src/Core/Dirt/Reports/Models/Data/MemberAccessReportDetail.cs b/src/Core/Dirt/Reports/Models/Data/MemberAccessReportDetail.cs
new file mode 100644
index 0000000000..a99b6e2088
--- /dev/null
+++ b/src/Core/Dirt/Reports/Models/Data/MemberAccessReportDetail.cs
@@ -0,0 +1,19 @@
+namespace Bit.Core.Dirt.Reports.Models.Data;
+
+public class MemberAccessReportDetail
+{
+ public Guid? UserGuid { get; set; }
+ public string UserName { get; set; }
+ public string Email { get; set; }
+ public bool TwoFactorEnabled { get; set; }
+ public bool AccountRecoveryEnabled { get; set; }
+ public bool UsesKeyConnector { get; set; }
+ public Guid? CollectionId { get; set; }
+ public Guid? GroupId { get; set; }
+ public string GroupName { get; set; }
+ public string CollectionName { get; set; }
+ public bool? ReadOnly { get; set; }
+ public bool? HidePasswords { get; set; }
+ public bool? Manage { get; set; }
+ public IEnumerable CipherIds { get; set; }
+}
diff --git a/src/Core/Dirt/Reports/Models/Data/OrganizationMemberBaseDetail.cs b/src/Core/Dirt/Reports/Models/Data/OrganizationMemberBaseDetail.cs
new file mode 100644
index 0000000000..a1f0bd81fd
--- /dev/null
+++ b/src/Core/Dirt/Reports/Models/Data/OrganizationMemberBaseDetail.cs
@@ -0,0 +1,19 @@
+namespace Bit.Core.Dirt.Reports.Models.Data;
+
+public class OrganizationMemberBaseDetail
+{
+ public Guid? UserGuid { get; set; }
+ public string UserName { get; set; }
+ public string Email { get; set; }
+ public string TwoFactorProviders { get; set; }
+ public bool UsesKeyConnector { get; set; }
+ public string ResetPasswordKey { get; set; }
+ public Guid? CollectionId { get; set; }
+ public Guid? GroupId { get; set; }
+ public string GroupName { get; set; }
+ public string CollectionName { get; set; }
+ public bool? ReadOnly { get; set; }
+ public bool? HidePasswords { get; set; }
+ public bool? Manage { get; set; }
+ public Guid CipherId { get; set; }
+}
diff --git a/src/Core/Dirt/Reports/Models/Data/RiskInsightsReportDetail.cs b/src/Core/Dirt/Reports/Models/Data/RiskInsightsReportDetail.cs
new file mode 100644
index 0000000000..1ea805edf1
--- /dev/null
+++ b/src/Core/Dirt/Reports/Models/Data/RiskInsightsReportDetail.cs
@@ -0,0 +1,10 @@
+namespace Bit.Core.Dirt.Reports.Models.Data;
+
+public class RiskInsightsReportDetail
+{
+ public Guid? UserGuid { get; set; }
+ public string UserName { get; set; }
+ public string Email { get; set; }
+ public bool UsesKeyConnector { get; set; }
+ public IEnumerable CipherIds { get; set; }
+}
diff --git a/src/Core/Dirt/Reports/ReportFeatures/MemberAccessCipherDetailsQuery.cs b/src/Core/Dirt/Reports/ReportFeatures/MemberAccessCipherDetailsQuery.cs
deleted file mode 100644
index 4a8039e6bc..0000000000
--- a/src/Core/Dirt/Reports/ReportFeatures/MemberAccessCipherDetailsQuery.cs
+++ /dev/null
@@ -1,206 +0,0 @@
-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.ReportFeatures.OrganizationReportMembers.Interfaces;
-using Bit.Core.Dirt.Reports.ReportFeatures.Requests;
-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.Vault.Models.Data;
-using Bit.Core.Vault.Queries;
-using Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
-using Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;
-
-namespace Bit.Core.Dirt.Reports.ReportFeatures;
-
-public class MemberAccessCipherDetailsQuery : IMemberAccessCipherDetailsQuery
-{
- private readonly IOrganizationUserUserDetailsQuery _organizationUserUserDetailsQuery;
- private readonly IGroupRepository _groupRepository;
- private readonly ICollectionRepository _collectionRepository;
- private readonly IOrganizationCiphersQuery _organizationCiphersQuery;
- private readonly IApplicationCacheService _applicationCacheService;
- private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
-
- public MemberAccessCipherDetailsQuery(
- IOrganizationUserUserDetailsQuery organizationUserUserDetailsQuery,
- IGroupRepository groupRepository,
- ICollectionRepository collectionRepository,
- IOrganizationCiphersQuery organizationCiphersQuery,
- IApplicationCacheService applicationCacheService,
- ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery
- )
- {
- _organizationUserUserDetailsQuery = organizationUserUserDetailsQuery;
- _groupRepository = groupRepository;
- _collectionRepository = collectionRepository;
- _organizationCiphersQuery = organizationCiphersQuery;
- _applicationCacheService = applicationCacheService;
- _twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
- }
-
- public async Task> GetMemberAccessCipherDetails(MemberAccessCipherDetailsRequest request)
- {
- var orgUsers = await _organizationUserUserDetailsQuery.GetOrganizationUserUserDetails(
- new OrganizationUserUserDetailsQueryRequest
- {
- OrganizationId = request.OrganizationId,
- IncludeCollections = true,
- IncludeGroups = true
- });
-
- var orgGroups = await _groupRepository.GetManyByOrganizationIdAsync(request.OrganizationId);
- var orgAbility = await _applicationCacheService.GetOrganizationAbilityAsync(request.OrganizationId);
- var orgCollectionsWithAccess = await _collectionRepository.GetManyByOrganizationIdWithAccessAsync(request.OrganizationId);
- var orgItems = await _organizationCiphersQuery.GetAllOrganizationCiphers(request.OrganizationId);
- var organizationUsersTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(orgUsers);
-
- var memberAccessCipherDetails = GenerateAccessDataParallel(
- orgGroups,
- orgCollectionsWithAccess,
- orgItems,
- organizationUsersTwoFactorEnabled,
- orgAbility);
-
- 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)
- {
- 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;
- }
-}
diff --git a/src/Core/Dirt/Reports/ReportFeatures/MemberAccessReportQuery.cs b/src/Core/Dirt/Reports/ReportFeatures/MemberAccessReportQuery.cs
new file mode 100644
index 0000000000..7ab8acb8dc
--- /dev/null
+++ b/src/Core/Dirt/Reports/ReportFeatures/MemberAccessReportQuery.cs
@@ -0,0 +1,65 @@
+using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
+using Bit.Core.Dirt.Reports.Models.Data;
+using Bit.Core.Dirt.Reports.ReportFeatures.OrganizationReportMembers.Interfaces;
+using Bit.Core.Dirt.Reports.ReportFeatures.Requests;
+using Bit.Core.Dirt.Reports.Repositories;
+using Bit.Core.Services;
+
+namespace Bit.Core.Dirt.Reports.ReportFeatures;
+
+public class MemberAccessReportQuery(
+ IOrganizationMemberBaseDetailRepository organizationMemberBaseDetailRepository,
+ ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
+ IApplicationCacheService applicationCacheService)
+ : IMemberAccessReportQuery
+{
+ public async Task> GetMemberAccessReportsAsync(
+ MemberAccessReportRequest request)
+ {
+ var baseDetails =
+ await organizationMemberBaseDetailRepository.GetOrganizationMemberBaseDetailsByOrganizationId(
+ request.OrganizationId);
+
+ var orgUsers = baseDetails.Select(x => x.UserGuid.GetValueOrDefault()).Distinct();
+ var orgUsersTwoFactorEnabled = await twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(orgUsers);
+
+ var orgAbility = await applicationCacheService.GetOrganizationAbilityAsync(request.OrganizationId);
+
+ var accessDetails = baseDetails
+ .GroupBy(b => new
+ {
+ b.UserGuid,
+ b.UserName,
+ b.Email,
+ b.TwoFactorProviders,
+ b.ResetPasswordKey,
+ b.UsesKeyConnector,
+ b.GroupId,
+ b.GroupName,
+ b.CollectionId,
+ b.CollectionName,
+ b.ReadOnly,
+ b.HidePasswords,
+ b.Manage
+ })
+ .Select(g => new MemberAccessReportDetail
+ {
+ UserGuid = g.Key.UserGuid,
+ UserName = g.Key.UserName,
+ Email = g.Key.Email,
+ TwoFactorEnabled = orgUsersTwoFactorEnabled.FirstOrDefault(x => x.userId == g.Key.UserGuid).twoFactorIsEnabled,
+ AccountRecoveryEnabled = !string.IsNullOrWhiteSpace(g.Key.ResetPasswordKey) && orgAbility.UseResetPassword,
+ UsesKeyConnector = g.Key.UsesKeyConnector,
+ GroupId = g.Key.GroupId,
+ GroupName = g.Key.GroupName,
+ CollectionId = g.Key.CollectionId,
+ CollectionName = g.Key.CollectionName,
+ ReadOnly = g.Key.ReadOnly,
+ HidePasswords = g.Key.HidePasswords,
+ Manage = g.Key.Manage,
+ CipherIds = g.Select(c => c.CipherId)
+ });
+
+ return accessDetails;
+ }
+}
diff --git a/src/Core/Dirt/Reports/ReportFeatures/OrganizationReportMembers/Interfaces/IMemberAccessCipherDetailsQuery.cs b/src/Core/Dirt/Reports/ReportFeatures/OrganizationReportMembers/Interfaces/IMemberAccessReportQuery.cs
similarity index 52%
rename from src/Core/Dirt/Reports/ReportFeatures/OrganizationReportMembers/Interfaces/IMemberAccessCipherDetailsQuery.cs
rename to src/Core/Dirt/Reports/ReportFeatures/OrganizationReportMembers/Interfaces/IMemberAccessReportQuery.cs
index 98ed780db3..44bb4f33c5 100644
--- a/src/Core/Dirt/Reports/ReportFeatures/OrganizationReportMembers/Interfaces/IMemberAccessCipherDetailsQuery.cs
+++ b/src/Core/Dirt/Reports/ReportFeatures/OrganizationReportMembers/Interfaces/IMemberAccessReportQuery.cs
@@ -3,7 +3,7 @@ using Bit.Core.Dirt.Reports.ReportFeatures.Requests;
namespace Bit.Core.Dirt.Reports.ReportFeatures.OrganizationReportMembers.Interfaces;
-public interface IMemberAccessCipherDetailsQuery
+public interface IMemberAccessReportQuery
{
- Task> GetMemberAccessCipherDetails(MemberAccessCipherDetailsRequest request);
+ Task> GetMemberAccessReportsAsync(MemberAccessReportRequest request);
}
diff --git a/src/Core/Dirt/Reports/ReportFeatures/OrganizationReportMembers/Interfaces/IRiskInsightsReportQuery.cs b/src/Core/Dirt/Reports/ReportFeatures/OrganizationReportMembers/Interfaces/IRiskInsightsReportQuery.cs
new file mode 100644
index 0000000000..c6ba69dfff
--- /dev/null
+++ b/src/Core/Dirt/Reports/ReportFeatures/OrganizationReportMembers/Interfaces/IRiskInsightsReportQuery.cs
@@ -0,0 +1,9 @@
+using Bit.Core.Dirt.Reports.Models.Data;
+using Bit.Core.Dirt.Reports.ReportFeatures.Requests;
+
+namespace Bit.Core.Dirt.Reports.ReportFeatures.OrganizationReportMembers.Interfaces;
+
+public interface IRiskInsightsReportQuery
+{
+ Task> GetRiskInsightsReportDetails(RiskInsightsReportRequest request);
+}
diff --git a/src/Core/Dirt/Reports/ReportFeatures/ReportingServiceCollectionExtensions.cs b/src/Core/Dirt/Reports/ReportFeatures/ReportingServiceCollectionExtensions.cs
index d847c8051e..4339d0f2f4 100644
--- a/src/Core/Dirt/Reports/ReportFeatures/ReportingServiceCollectionExtensions.cs
+++ b/src/Core/Dirt/Reports/ReportFeatures/ReportingServiceCollectionExtensions.cs
@@ -8,7 +8,8 @@ public static class ReportingServiceCollectionExtensions
{
public static void AddReportingServices(this IServiceCollection services)
{
- services.AddScoped();
+ services.AddScoped();
+ services.AddScoped();
services.AddScoped();
services.AddScoped();
services.AddScoped();
diff --git a/src/Core/Dirt/Reports/ReportFeatures/Requests/MemberAccessCipherDetailsRequest.cs b/src/Core/Dirt/Reports/ReportFeatures/Requests/MemberAccessReportRequest.cs
similarity index 70%
rename from src/Core/Dirt/Reports/ReportFeatures/Requests/MemberAccessCipherDetailsRequest.cs
rename to src/Core/Dirt/Reports/ReportFeatures/Requests/MemberAccessReportRequest.cs
index b40dfc6dec..5fe28810a6 100644
--- a/src/Core/Dirt/Reports/ReportFeatures/Requests/MemberAccessCipherDetailsRequest.cs
+++ b/src/Core/Dirt/Reports/ReportFeatures/Requests/MemberAccessReportRequest.cs
@@ -1,6 +1,6 @@
namespace Bit.Core.Dirt.Reports.ReportFeatures.Requests;
-public class MemberAccessCipherDetailsRequest
+public class MemberAccessReportRequest
{
public Guid OrganizationId { get; set; }
}
diff --git a/src/Core/Dirt/Reports/ReportFeatures/Requests/RiskInsightsReportRequest.cs b/src/Core/Dirt/Reports/ReportFeatures/Requests/RiskInsightsReportRequest.cs
new file mode 100644
index 0000000000..1b843ea002
--- /dev/null
+++ b/src/Core/Dirt/Reports/ReportFeatures/Requests/RiskInsightsReportRequest.cs
@@ -0,0 +1,6 @@
+namespace Bit.Core.Dirt.Reports.ReportFeatures.Requests;
+
+public class RiskInsightsReportRequest
+{
+ public Guid OrganizationId { get; set; }
+}
diff --git a/src/Core/Dirt/Reports/ReportFeatures/RiskInsightsReportQuery.cs b/src/Core/Dirt/Reports/ReportFeatures/RiskInsightsReportQuery.cs
new file mode 100644
index 0000000000..e686698c51
--- /dev/null
+++ b/src/Core/Dirt/Reports/ReportFeatures/RiskInsightsReportQuery.cs
@@ -0,0 +1,39 @@
+using Bit.Core.Dirt.Reports.Models.Data;
+using Bit.Core.Dirt.Reports.ReportFeatures.OrganizationReportMembers.Interfaces;
+using Bit.Core.Dirt.Reports.ReportFeatures.Requests;
+using Bit.Core.Dirt.Reports.Repositories;
+
+namespace Bit.Core.Dirt.Reports.ReportFeatures;
+
+public class RiskInsightsReportQuery : IRiskInsightsReportQuery
+{
+ private readonly IOrganizationMemberBaseDetailRepository _organizationMemberBaseDetailRepository;
+
+ public RiskInsightsReportQuery(IOrganizationMemberBaseDetailRepository repository)
+ {
+ _organizationMemberBaseDetailRepository = repository;
+ }
+
+ public async Task> GetRiskInsightsReportDetails(
+ RiskInsightsReportRequest request)
+ {
+ var baseDetails =
+ await _organizationMemberBaseDetailRepository.GetOrganizationMemberBaseDetailsByOrganizationId(
+ request.OrganizationId);
+
+ var insightsDetails = baseDetails
+ .GroupBy(b => new { b.UserGuid, b.UserName, b.Email, b.UsesKeyConnector })
+ .Select(g => new RiskInsightsReportDetail
+ {
+ UserGuid = g.Key.UserGuid,
+ UserName = g.Key.UserName,
+ Email = g.Key.Email,
+ UsesKeyConnector = g.Key.UsesKeyConnector,
+ CipherIds = g
+ .Select(x => x.CipherId.ToString())
+ .Distinct()
+ });
+
+ return insightsDetails;
+ }
+}
diff --git a/src/Core/Dirt/Reports/Repositories/IOrganizationMemberBaseDetailRepository.cs b/src/Core/Dirt/Reports/Repositories/IOrganizationMemberBaseDetailRepository.cs
new file mode 100644
index 0000000000..e2a161aa9c
--- /dev/null
+++ b/src/Core/Dirt/Reports/Repositories/IOrganizationMemberBaseDetailRepository.cs
@@ -0,0 +1,8 @@
+using Bit.Core.Dirt.Reports.Models.Data;
+
+namespace Bit.Core.Dirt.Reports.Repositories;
+
+public interface IOrganizationMemberBaseDetailRepository
+{
+ Task> GetOrganizationMemberBaseDetailsByOrganizationId(Guid organizationId);
+}
diff --git a/src/Infrastructure.Dapper/DapperServiceCollectionExtensions.cs b/src/Infrastructure.Dapper/DapperServiceCollectionExtensions.cs
index a95c2bd4c6..e64eabd5bf 100644
--- a/src/Infrastructure.Dapper/DapperServiceCollectionExtensions.cs
+++ b/src/Infrastructure.Dapper/DapperServiceCollectionExtensions.cs
@@ -70,6 +70,7 @@ public static class DapperServiceCollectionExtensions
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
+ services.AddSingleton();
if (selfHosted)
{
diff --git a/src/Infrastructure.Dapper/Dirt/OrganizationMemberBaseDetailRepository.cs b/src/Infrastructure.Dapper/Dirt/OrganizationMemberBaseDetailRepository.cs
new file mode 100644
index 0000000000..458e72f996
--- /dev/null
+++ b/src/Infrastructure.Dapper/Dirt/OrganizationMemberBaseDetailRepository.cs
@@ -0,0 +1,39 @@
+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;
+
+namespace Bit.Infrastructure.Dapper.Dirt;
+
+public class OrganizationMemberBaseDetailRepository : BaseRepository, IOrganizationMemberBaseDetailRepository
+{
+ public OrganizationMemberBaseDetailRepository(GlobalSettings globalSettings)
+ : this(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString)
+ {
+ }
+
+ public OrganizationMemberBaseDetailRepository(string connectionString, string readOnlyConnectionString) : base(
+ connectionString, readOnlyConnectionString)
+ {
+ }
+
+ public async Task> GetOrganizationMemberBaseDetailsByOrganizationId(
+ Guid organizationId)
+ {
+ await using var connection = new SqlConnection(ConnectionString);
+
+
+ var result = await connection.QueryAsync(
+ "[dbo].[MemberAccessReport_GetMemberAccessCipherDetailsByOrganizationId]",
+ new
+ {
+ OrganizationId = organizationId
+
+ }, commandType: CommandType.StoredProcedure);
+
+ return result;
+ }
+}
diff --git a/src/Infrastructure.EntityFramework/Dirt/OrganizationMemberBaseDetailRepository.cs b/src/Infrastructure.EntityFramework/Dirt/OrganizationMemberBaseDetailRepository.cs
new file mode 100644
index 0000000000..123379da90
--- /dev/null
+++ b/src/Infrastructure.EntityFramework/Dirt/OrganizationMemberBaseDetailRepository.cs
@@ -0,0 +1,32 @@
+using AutoMapper;
+using Bit.Core.Dirt.Reports.Models.Data;
+using Bit.Core.Dirt.Reports.Repositories;
+using Bit.Infrastructure.EntityFramework.Repositories;
+using Microsoft.Data.SqlClient;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Bit.Infrastructure.EntityFramework.Dirt;
+
+public class OrganizationMemberBaseDetailRepository : BaseEntityFrameworkRepository, IOrganizationMemberBaseDetailRepository
+{
+ public OrganizationMemberBaseDetailRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper) : base(
+ serviceScopeFactory,
+ mapper)
+ {
+ }
+
+ public async Task> GetOrganizationMemberBaseDetailsByOrganizationId(
+ Guid organizationId)
+ {
+ await using var scope = ServiceScopeFactory.CreateAsyncScope();
+ var dbContext = GetDatabaseContext(scope);
+
+ var result = await dbContext.Set()
+ .FromSqlRaw("EXEC [dbo].[MemberAccessReport_GetMemberAccessCipherDetailsByOrganizationId] @OrganizationId",
+ new SqlParameter("@OrganizationId", organizationId))
+ .ToListAsync();
+
+ return result;
+ }
+}
diff --git a/src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs b/src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs
index 321c4c90e5..616b2bc434 100644
--- a/src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs
+++ b/src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs
@@ -14,6 +14,7 @@ using Bit.Core.Vault.Repositories;
using Bit.Infrastructure.EntityFramework.AdminConsole.Repositories;
using Bit.Infrastructure.EntityFramework.Auth.Repositories;
using Bit.Infrastructure.EntityFramework.Billing.Repositories;
+using Bit.Infrastructure.EntityFramework.Dirt;
using Bit.Infrastructure.EntityFramework.Dirt.Repositories;
using Bit.Infrastructure.EntityFramework.KeyManagement.Repositories;
using Bit.Infrastructure.EntityFramework.NotificationCenter.Repositories;
@@ -107,6 +108,7 @@ public static class EntityFrameworkServiceCollectionExtensions
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
+ services.AddSingleton();
if (selfHosted)
{
diff --git a/src/Infrastructure.EntityFramework/Repositories/DatabaseContext.cs b/src/Infrastructure.EntityFramework/Repositories/DatabaseContext.cs
index 647b3e3ab1..e1e29cbf41 100644
--- a/src/Infrastructure.EntityFramework/Repositories/DatabaseContext.cs
+++ b/src/Infrastructure.EntityFramework/Repositories/DatabaseContext.cs
@@ -1,4 +1,5 @@
using Bit.Core;
+using Bit.Core.Dirt.Reports.Models.Data;
using Bit.Infrastructure.EntityFramework.AdminConsole.Models;
using Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider;
using Bit.Infrastructure.EntityFramework.Auth.Models;
@@ -80,6 +81,7 @@ public class DatabaseContext : DbContext
public DbSet NotificationStatuses { get; set; }
public DbSet ClientOrganizationMigrationRecords { get; set; }
public DbSet PasswordHealthReportApplications { get; set; }
+ public DbSet OrganizationMemberBaseDetails { get; set; }
public DbSet SecurityTasks { get; set; }
public DbSet OrganizationInstallations { get; set; }
@@ -112,6 +114,7 @@ public class DatabaseContext : DbContext
var eOrganizationConnection = builder.Entity();
var eOrganizationDomain = builder.Entity();
var aWebAuthnCredential = builder.Entity();
+ var eOrganizationMemberBaseDetail = builder.Entity();
// Shadow property configurations go here
@@ -134,6 +137,8 @@ public class DatabaseContext : DbContext
eCollectionGroup.HasKey(cg => new { cg.CollectionId, cg.GroupId });
eGroupUser.HasKey(gu => new { gu.GroupId, gu.OrganizationUserId });
+ eOrganizationMemberBaseDetail.HasNoKey();
+
var dataProtector = this.GetService().CreateProtector(
Constants.DatabaseFieldProtectorPurpose);
var dataProtectionConverter = new DataProtectionConverter(dataProtector);
diff --git a/src/Sql/Dirt/Stored Procedure/MemberAccessDetail_GetMemberAccessDetailByOrganizationId.sql b/src/Sql/Dirt/Stored Procedure/MemberAccessDetail_GetMemberAccessDetailByOrganizationId.sql
new file mode 100644
index 0000000000..1aaf667f6a
--- /dev/null
+++ b/src/Sql/Dirt/Stored Procedure/MemberAccessDetail_GetMemberAccessDetailByOrganizationId.sql
@@ -0,0 +1,92 @@
+CREATE PROCEDURE dbo.MemberAccessReport_GetMemberAccessCipherDetailsByOrganizationId
+ @OrganizationId UNIQUEIDENTIFIER
+AS
+ SET NOCOUNT ON;
+
+IF @OrganizationId IS NULL
+ THROW 50000, 'OrganizationId cannot be null', 1;
+
+ SELECT
+ OU.Id AS UserGuid,
+ U.Name AS UserName,
+ ISNULL(U.Email, OU.Email) as 'Email',
+ U.TwoFactorProviders,
+ U.UsesKeyConnector,
+ OU.ResetPasswordKey,
+ CC.CollectionId,
+ C.Name AS CollectionName,
+ NULL AS GroupId,
+ NULL AS GroupName,
+ CU.ReadOnly,
+ CU.HidePasswords,
+ CU.Manage,
+ Cipher.Id AS CipherId
+ FROM dbo.OrganizationUser OU
+ LEFT 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 and C.OrganizationId = @OrganizationId
+ INNER JOIN dbo.CollectionCipher CC ON CC.CollectionId = C.Id
+ INNER JOIN dbo.Cipher Cipher ON Cipher.Id = CC.CipherId AND Cipher.OrganizationId = @OrganizationId
+ WHERE OU.Status IN (0,1,2) -- Invited, Accepted and Confirmed Users
+ AND Cipher.DeletedDate IS NULL
+UNION ALL
+ -- Group-based collection permissions
+ SELECT
+ OU.Id AS UserGuid,
+ U.Name AS UserName,
+ ISNULL(U.Email, OU.Email) as 'Email',
+ U.TwoFactorProviders,
+ U.UsesKeyConnector,
+ OU.ResetPasswordKey,
+ CC.CollectionId,
+ C.Name AS CollectionName,
+ G.Id AS GroupId,
+ G.Name AS GroupName,
+ CG.ReadOnly,
+ CG.HidePasswords,
+ CG.Manage,
+ Cipher.Id AS CipherId
+ FROM dbo.OrganizationUser OU
+ LEFT 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 AND C.OrganizationId = @OrganizationId
+ INNER JOIN dbo.CollectionCipher CC ON CC.CollectionId = C.Id
+ INNER JOIN dbo.Cipher Cipher ON Cipher.Id = CC.CipherId and Cipher.OrganizationId = @OrganizationId
+ WHERE OU.Status IN (0,1,2) -- Invited, Accepted and Confirmed Users
+ AND Cipher.DeletedDate IS NULL
+UNION ALL
+ -- Users without collection access (invited users)
+ -- typically invited users who have not yet accepted the invitation
+ -- and not yet assigned to any collection
+ SELECT
+ OU.Id AS UserGuid,
+ U.Name AS UserName,
+ ISNULL(U.Email, OU.Email) as 'Email',
+ U.TwoFactorProviders,
+ U.UsesKeyConnector,
+ OU.ResetPasswordKey,
+ null as CollectionId,
+ null AS CollectionName,
+ NULL AS GroupId,
+ NULL AS GroupName,
+ null as [ReadOnly],
+ null as HidePasswords,
+ null as Manage,
+ null AS CipherId
+ FROM dbo.OrganizationUser OU
+ LEFT 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
+ WHERE OU.Status IN (0,1,2) -- Invited, Accepted and Confirmed Users
+ AND OU.Id not in (
+ select OU1.Id from dbo.OrganizationUser OU1
+ inner join dbo.CollectionUser CU1 on CU1.OrganizationUserId = OU1.Id
+ WHERE OU1.OrganizationId = @organizationId
+ )
diff --git a/util/Migrator/DbScripts/2025-06-09_00_AddMemberAccessReportStoreProcedure.sql b/util/Migrator/DbScripts/2025-06-09_00_AddMemberAccessReportStoreProcedure.sql
new file mode 100644
index 0000000000..afb682e97a
--- /dev/null
+++ b/util/Migrator/DbScripts/2025-06-09_00_AddMemberAccessReportStoreProcedure.sql
@@ -0,0 +1,70 @@
+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,
+ OU.ResetPasswordKey,
+ CC.CollectionId,
+ C.Name AS CollectionName,
+ NULL AS GroupId,
+ NULL AS GroupName,
+ CU.ReadOnly,
+ CU.HidePasswords,
+ CU.Manage,
+ Cipher.Id AS CipherId
+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
+ AND Cipher.OrganizationId = @OrganizationId
+WHERE OU.Status in (0,1,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,
+ OU.ResetPasswordKey,
+ CC.CollectionId,
+ C.Name AS CollectionName,
+ G.Id AS GroupId,
+ G.Name AS GroupName,
+ CG.ReadOnly,
+ CG.HidePasswords,
+ CG.Manage,
+ Cipher.Id AS CipherId
+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
+ AND Cipher.OrganizationId = @OrganizationId
+WHERE OU.Status in (0,1,2)
+ AND Cipher.DeletedDate IS NULL
+
+GO
diff --git a/util/Migrator/DbScripts/2025-06-12_00_AlterMemberAccessReportStoreProcedure.sql b/util/Migrator/DbScripts/2025-06-12_00_AlterMemberAccessReportStoreProcedure.sql
new file mode 100644
index 0000000000..fb3b842fc6
--- /dev/null
+++ b/util/Migrator/DbScripts/2025-06-12_00_AlterMemberAccessReportStoreProcedure.sql
@@ -0,0 +1,94 @@
+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
+ OU.Id AS UserGuid,
+ U.Name AS UserName,
+ ISNULL(U.Email, OU.Email) as 'Email',
+ U.TwoFactorProviders,
+ U.UsesKeyConnector,
+ OU.ResetPasswordKey,
+ CC.CollectionId,
+ C.Name AS CollectionName,
+ NULL AS GroupId,
+ NULL AS GroupName,
+ CU.ReadOnly,
+ CU.HidePasswords,
+ CU.Manage,
+ Cipher.Id AS CipherId
+ FROM dbo.OrganizationUser OU
+ LEFT 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 and C.OrganizationId = @OrganizationId
+ INNER JOIN dbo.CollectionCipher CC ON CC.CollectionId = C.Id
+ INNER JOIN dbo.Cipher Cipher ON Cipher.Id = CC.CipherId AND Cipher.OrganizationId = @OrganizationId
+ WHERE OU.Status IN (0,1,2) -- Invited, Accepted and Confirmed Users
+ AND Cipher.DeletedDate IS NULL
+UNION ALL
+ -- Group-based collection permissions
+ SELECT
+ OU.Id AS UserGuid,
+ U.Name AS UserName,
+ ISNULL(U.Email, OU.Email) as 'Email',
+ U.TwoFactorProviders,
+ U.UsesKeyConnector,
+ OU.ResetPasswordKey,
+ CC.CollectionId,
+ C.Name AS CollectionName,
+ G.Id AS GroupId,
+ G.Name AS GroupName,
+ CG.ReadOnly,
+ CG.HidePasswords,
+ CG.Manage,
+ Cipher.Id AS CipherId
+ FROM dbo.OrganizationUser OU
+ LEFT 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 AND C.OrganizationId = @OrganizationId
+ INNER JOIN dbo.CollectionCipher CC ON CC.CollectionId = C.Id
+ INNER JOIN dbo.Cipher Cipher ON Cipher.Id = CC.CipherId and Cipher.OrganizationId = @OrganizationId
+ WHERE OU.Status IN (0,1,2) -- Invited, Accepted and Confirmed Users
+ AND Cipher.DeletedDate IS NULL
+UNION ALL
+ -- Users without collection access (invited users)
+ -- typically invited users who have not yet accepted the invitation
+ -- and not yet assigned to any collection
+ SELECT
+ OU.Id AS UserGuid,
+ U.Name AS UserName,
+ ISNULL(U.Email, OU.Email) as 'Email',
+ U.TwoFactorProviders,
+ U.UsesKeyConnector,
+ OU.ResetPasswordKey,
+ null as CollectionId,
+ null AS CollectionName,
+ NULL AS GroupId,
+ NULL AS GroupName,
+ null as [ReadOnly],
+ null as HidePasswords,
+ null as Manage,
+ null AS CipherId
+ FROM dbo.OrganizationUser OU
+ LEFT 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
+ WHERE OU.Status IN (0,1,2) -- Invited, Accepted and Confirmed Users
+ AND OU.Id not in (
+ select OU1.Id from dbo.OrganizationUser OU1
+ inner join dbo.CollectionUser CU1 on CU1.OrganizationUserId = OU1.Id
+ WHERE OU1.OrganizationId = @organizationId
+ )
+
+GO
diff --git a/util/MySqlMigrations/Migrations/20250609182150_2025-06-09_00_AddMemberAccessReportStoreProcedure.sql.Designer.cs b/util/MySqlMigrations/Migrations/20250609182150_2025-06-09_00_AddMemberAccessReportStoreProcedure.sql.Designer.cs
new file mode 100644
index 0000000000..bd813ae927
--- /dev/null
+++ b/util/MySqlMigrations/Migrations/20250609182150_2025-06-09_00_AddMemberAccessReportStoreProcedure.sql.Designer.cs
@@ -0,0 +1,3166 @@
+//
+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("20250609182150_2025-06-09_00_AddMemberAccessReportStoreProcedure.sql")]
+ partial class _20250609_00_AddMemberAccessReportStoreProceduresql
+ {
+ ///
+ 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.OrganizationMemberBaseDetail", b =>
+ {
+ b.Property("CipherId")
+ .HasColumnType("char(36)");
+
+ b.Property("CollectionId")
+ .HasColumnType("char(36)");
+
+ b.Property("CollectionName")
+ .HasColumnType("longtext");
+
+ b.Property("Email")
+ .HasColumnType("longtext");
+
+ b.Property("GroupId")
+ .HasColumnType("char(36)");
+
+ b.Property("GroupName")
+ .HasColumnType("longtext");
+
+ b.Property("HidePasswords")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("Manage")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("ReadOnly")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("ResetPasswordKey")
+ .HasColumnType("longtext");
+
+ b.Property("TwoFactorProviders")
+ .HasColumnType("longtext");
+
+ b.Property("UserGuid")
+ .HasColumnType("char(36)");
+
+ b.Property("UserName")
+ .HasColumnType("longtext");
+
+ b.Property("UsesKeyConnector")
+ .HasColumnType("tinyint(1)");
+
+ b.ToTable("OrganizationMemberBaseDetails");
+ });
+
+ 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