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