diff --git a/src/Api/Dirt/Controllers/ReportsController.cs b/src/Api/Dirt/Controllers/ReportsController.cs
index 4c0a802da2..b987df2f23 100644
--- a/src/Api/Dirt/Controllers/ReportsController.cs
+++ b/src/Api/Dirt/Controllers/ReportsController.cs
@@ -1,9 +1,9 @@
using Bit.Api.Tools.Models;
using Bit.Api.Tools.Models.Response;
using Bit.Core.Context;
+using Bit.Core.Dirt.Reports.Models.Data;
using Bit.Core.Exceptions;
using Bit.Core.Tools.Entities;
-using Bit.Core.Tools.Models.Data;
using Bit.Core.Tools.ReportFeatures.Interfaces;
using Bit.Core.Tools.ReportFeatures.OrganizationReportMembers.Interfaces;
using Bit.Core.Tools.ReportFeatures.Requests;
diff --git a/src/Api/Dirt/Models/Response/MemberAccessReportModel.cs b/src/Api/Dirt/Models/Response/MemberAccessReportModel.cs
index b110c316c1..92c408e3bd 100644
--- a/src/Api/Dirt/Models/Response/MemberAccessReportModel.cs
+++ b/src/Api/Dirt/Models/Response/MemberAccessReportModel.cs
@@ -1,4 +1,4 @@
-using Bit.Core.Tools.Models.Data;
+using Bit.Core.Dirt.Reports.Models.Data;
namespace Bit.Api.Tools.Models.Response;
diff --git a/src/Api/Dirt/Models/Response/MemberCipherDetailsResponseModel.cs b/src/Api/Dirt/Models/Response/MemberCipherDetailsResponseModel.cs
index d927da8123..ccbeedec80 100644
--- a/src/Api/Dirt/Models/Response/MemberCipherDetailsResponseModel.cs
+++ b/src/Api/Dirt/Models/Response/MemberCipherDetailsResponseModel.cs
@@ -1,4 +1,4 @@
-using Bit.Core.Tools.Models.Data;
+using Bit.Core.Dirt.Reports.Models.Data;
namespace Bit.Api.Tools.Models.Response;
diff --git a/src/Core/Dirt/Reports/Models/Data/MemberAccessCipherDetails.cs b/src/Core/Dirt/Reports/Models/Data/MemberAccessCipherDetails.cs
index 943d56c53e..759337d5cf 100644
--- a/src/Core/Dirt/Reports/Models/Data/MemberAccessCipherDetails.cs
+++ b/src/Core/Dirt/Reports/Models/Data/MemberAccessCipherDetails.cs
@@ -1,4 +1,4 @@
-namespace Bit.Core.Tools.Models.Data;
+namespace Bit.Core.Dirt.Reports.Models.Data;
public class MemberAccessDetails
{
@@ -30,13 +30,13 @@ public class MemberAccessCipherDetails
public bool UsesKeyConnector { get; set; }
///
- /// The details for the member's collection access depending
- /// on the collections and groups they are assigned to
+ /// The details for the member's collection access depending
+ /// on the collections and groups they are assigned to
///
public IEnumerable AccessDetails { get; set; }
///
- /// A distinct list of the cipher ids associated with
+ /// A distinct list of the cipher ids associated with
/// the organization member
///
public IEnumerable CipherIds { get; set; }
diff --git a/src/Core/Dirt/Reports/ReportFeatures/MemberAccessCipherDetailsQuery.cs b/src/Core/Dirt/Reports/ReportFeatures/MemberAccessCipherDetailsQuery.cs
index 23486cac69..c8cc7212f6 100644
--- a/src/Core/Dirt/Reports/ReportFeatures/MemberAccessCipherDetailsQuery.cs
+++ b/src/Core/Dirt/Reports/ReportFeatures/MemberAccessCipherDetailsQuery.cs
@@ -2,19 +2,19 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
+using Bit.Core.Dirt.Reports.Models.Data;
+using Bit.Core.Dirt.Reports.Repositories;
using Bit.Core.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.Models.Data;
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;
-using Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;
namespace Bit.Core.Tools.ReportFeatures;
@@ -26,6 +26,7 @@ public class MemberAccessCipherDetailsQuery : IMemberAccessCipherDetailsQuery
private readonly IOrganizationCiphersQuery _organizationCiphersQuery;
private readonly IApplicationCacheService _applicationCacheService;
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
+ private readonly IMemberAccessCipherDetailsRepository _memberAccessCipherDetailsRepository;
public MemberAccessCipherDetailsQuery(
IOrganizationUserUserDetailsQuery organizationUserUserDetailsQuery,
@@ -33,7 +34,8 @@ public class MemberAccessCipherDetailsQuery : IMemberAccessCipherDetailsQuery
ICollectionRepository collectionRepository,
IOrganizationCiphersQuery organizationCiphersQuery,
IApplicationCacheService applicationCacheService,
- ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery
+ ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
+ IMemberAccessCipherDetailsRepository memberAccessCipherDetailsRepository
)
{
_organizationUserUserDetailsQuery = organizationUserUserDetailsQuery;
@@ -42,32 +44,12 @@ public class MemberAccessCipherDetailsQuery : IMemberAccessCipherDetailsQuery
_organizationCiphersQuery = organizationCiphersQuery;
_applicationCacheService = applicationCacheService;
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
+ _memberAccessCipherDetailsRepository = memberAccessCipherDetailsRepository;
}
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 = GenerateAccessDataParallelV2(
- orgGroups,
- orgCollectionsWithAccess,
- orgItems,
- organizationUsersTwoFactorEnabled,
- orgAbility);
-
- return memberAccessCipherDetails;
+ return await _memberAccessCipherDetailsRepository.GetMemberAccessCipherDetailsByOrganizationId(request.OrganizationId);
}
private IEnumerable GenerateAccessData(
diff --git a/src/Core/Dirt/Reports/ReportFeatures/OrganizationReportMembers/Interfaces/IMemberAccessCipherDetailsQuery.cs b/src/Core/Dirt/Reports/ReportFeatures/OrganizationReportMembers/Interfaces/IMemberAccessCipherDetailsQuery.cs
index c55495fd13..5f19f0bdce 100644
--- a/src/Core/Dirt/Reports/ReportFeatures/OrganizationReportMembers/Interfaces/IMemberAccessCipherDetailsQuery.cs
+++ b/src/Core/Dirt/Reports/ReportFeatures/OrganizationReportMembers/Interfaces/IMemberAccessCipherDetailsQuery.cs
@@ -1,4 +1,4 @@
-using Bit.Core.Tools.Models.Data;
+using Bit.Core.Dirt.Reports.Models.Data;
using Bit.Core.Tools.ReportFeatures.Requests;
namespace Bit.Core.Tools.ReportFeatures.OrganizationReportMembers.Interfaces;
diff --git a/src/Core/Dirt/Reports/Repositories/IMemberAccessCipherDetailsRepository.cs b/src/Core/Dirt/Reports/Repositories/IMemberAccessCipherDetailsRepository.cs
new file mode 100644
index 0000000000..d9be4fa6c7
--- /dev/null
+++ b/src/Core/Dirt/Reports/Repositories/IMemberAccessCipherDetailsRepository.cs
@@ -0,0 +1,8 @@
+using Bit.Core.Dirt.Reports.Models.Data;
+
+namespace Bit.Core.Dirt.Reports.Repositories;
+
+public interface IMemberAccessCipherDetailsRepository
+{
+ Task> GetMemberAccessCipherDetailsByOrganizationId(Guid organizationId);
+}
diff --git a/src/Infrastructure.Dapper/DapperServiceCollectionExtensions.cs b/src/Infrastructure.Dapper/DapperServiceCollectionExtensions.cs
index ba374ae988..f0e0629945 100644
--- a/src/Infrastructure.Dapper/DapperServiceCollectionExtensions.cs
+++ b/src/Infrastructure.Dapper/DapperServiceCollectionExtensions.cs
@@ -2,6 +2,7 @@
using Bit.Core.Auth.Repositories;
using Bit.Core.Billing.Providers.Repositories;
using Bit.Core.Billing.Repositories;
+using Bit.Core.Dirt.Reports.Repositories;
using Bit.Core.KeyManagement.Repositories;
using Bit.Core.NotificationCenter.Repositories;
using Bit.Core.Platform.Installations;
@@ -12,6 +13,7 @@ using Bit.Core.Vault.Repositories;
using Bit.Infrastructure.Dapper.AdminConsole.Repositories;
using Bit.Infrastructure.Dapper.Auth.Repositories;
using Bit.Infrastructure.Dapper.Billing.Repositories;
+using Bit.Infrastructure.Dapper.Dirt;
using Bit.Infrastructure.Dapper.KeyManagement.Repositories;
using Bit.Infrastructure.Dapper.NotificationCenter.Repositories;
using Bit.Infrastructure.Dapper.Platform;
@@ -68,6 +70,7 @@ public static class DapperServiceCollectionExtensions
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
+ services.AddSingleton();
if (selfHosted)
{
diff --git a/src/Infrastructure.Dapper/Dirt/MemberAccessCipherDetailsRepository.cs b/src/Infrastructure.Dapper/Dirt/MemberAccessCipherDetailsRepository.cs
new file mode 100644
index 0000000000..c005053320
--- /dev/null
+++ b/src/Infrastructure.Dapper/Dirt/MemberAccessCipherDetailsRepository.cs
@@ -0,0 +1,40 @@
+#nullable enable
+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 MemberAccessCipherDetailsRepository : BaseRepository, IMemberAccessCipherDetailsRepository
+{
+ public MemberAccessCipherDetailsRepository(GlobalSettings globalSettings)
+ : this(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString)
+ {
+ }
+
+ public MemberAccessCipherDetailsRepository(string connectionString, string readOnlyConnectionString) : base(
+ connectionString, readOnlyConnectionString)
+ {
+ }
+
+ public async Task> GetMemberAccessCipherDetailsByOrganizationId(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/MemberAccessCipherDetailsRepository.cs b/src/Infrastructure.EntityFramework/Dirt/MemberAccessCipherDetailsRepository.cs
new file mode 100644
index 0000000000..78a40bb437
--- /dev/null
+++ b/src/Infrastructure.EntityFramework/Dirt/MemberAccessCipherDetailsRepository.cs
@@ -0,0 +1,31 @@
+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 MemberAccessCipherDetailsRepository : BaseEntityFrameworkRepository, IMemberAccessCipherDetailsRepository
+{
+ public MemberAccessCipherDetailsRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper) : base(
+ serviceScopeFactory,
+ mapper)
+ {
+ }
+
+ public async Task> GetMemberAccessCipherDetailsByOrganizationId(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 22818517d3..7c20efb05e 100644
--- a/src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs
+++ b/src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs
@@ -2,6 +2,7 @@
using Bit.Core.Auth.Repositories;
using Bit.Core.Billing.Providers.Repositories;
using Bit.Core.Billing.Repositories;
+using Bit.Core.Dirt.Reports.Repositories;
using Bit.Core.Enums;
using Bit.Core.KeyManagement.Repositories;
using Bit.Core.NotificationCenter.Repositories;
@@ -13,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.KeyManagement.Repositories;
using Bit.Infrastructure.EntityFramework.NotificationCenter.Repositories;
using Bit.Infrastructure.EntityFramework.Platform;
@@ -105,6 +107,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 5c1c1bc87f..71df8591ac 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 MemberAccessCipherDetails { get; set; }
public DbSet SecurityTasks { get; set; }
public DbSet OrganizationInstallations { get; set; }
diff --git a/src/Sql/Dirt/Stored Procedure/MemberAccessReport_GetMemberAccessCipherDetailsByOrganizationId.sql b/src/Sql/Dirt/Stored Procedure/MemberAccessReport_GetMemberAccessCipherDetailsByOrganizationId.sql
new file mode 100644
index 0000000000..d2765195c1
--- /dev/null
+++ b/src/Sql/Dirt/Stored Procedure/MemberAccessReport_GetMemberAccessCipherDetailsByOrganizationId.sql
@@ -0,0 +1,64 @@
+CREATE PROCEDURE 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