1
0
mirror of https://github.com/bitwarden/server.git synced 2025-06-18 10:03:50 -05:00

[PM-20112] Member access stored proc and splitting the query (#5943)

This commit is contained in:
Tom 2025-06-16 17:32:36 -04:00 committed by GitHub
parent 66d1c70dc6
commit b8244908ec
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
32 changed files with 10480 additions and 347 deletions

View File

@ -1,5 +1,6 @@
using Bit.Api.Dirt.Models; using Bit.Api.Dirt.Models;
using Bit.Api.Dirt.Models.Response; using Bit.Api.Dirt.Models.Response;
using Bit.Api.Tools.Models.Response;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Dirt.Reports.Entities; using Bit.Core.Dirt.Reports.Entities;
using Bit.Core.Dirt.Reports.Models.Data; using Bit.Core.Dirt.Reports.Models.Data;
@ -17,21 +18,24 @@ namespace Bit.Api.Dirt.Controllers;
public class ReportsController : Controller public class ReportsController : Controller
{ {
private readonly ICurrentContext _currentContext; private readonly ICurrentContext _currentContext;
private readonly IMemberAccessCipherDetailsQuery _memberAccessCipherDetailsQuery; private readonly IMemberAccessReportQuery _memberAccessReportQuery;
private readonly IRiskInsightsReportQuery _riskInsightsReportQuery;
private readonly IAddPasswordHealthReportApplicationCommand _addPwdHealthReportAppCommand; private readonly IAddPasswordHealthReportApplicationCommand _addPwdHealthReportAppCommand;
private readonly IGetPasswordHealthReportApplicationQuery _getPwdHealthReportAppQuery; private readonly IGetPasswordHealthReportApplicationQuery _getPwdHealthReportAppQuery;
private readonly IDropPasswordHealthReportApplicationCommand _dropPwdHealthReportAppCommand; private readonly IDropPasswordHealthReportApplicationCommand _dropPwdHealthReportAppCommand;
public ReportsController( public ReportsController(
ICurrentContext currentContext, ICurrentContext currentContext,
IMemberAccessCipherDetailsQuery memberAccessCipherDetailsQuery, IMemberAccessReportQuery memberAccessReportQuery,
IRiskInsightsReportQuery riskInsightsReportQuery,
IAddPasswordHealthReportApplicationCommand addPasswordHealthReportApplicationCommand, IAddPasswordHealthReportApplicationCommand addPasswordHealthReportApplicationCommand,
IGetPasswordHealthReportApplicationQuery getPasswordHealthReportApplicationQuery, IGetPasswordHealthReportApplicationQuery getPasswordHealthReportApplicationQuery,
IDropPasswordHealthReportApplicationCommand dropPwdHealthReportAppCommand IDropPasswordHealthReportApplicationCommand dropPwdHealthReportAppCommand
) )
{ {
_currentContext = currentContext; _currentContext = currentContext;
_memberAccessCipherDetailsQuery = memberAccessCipherDetailsQuery; _memberAccessReportQuery = memberAccessReportQuery;
_riskInsightsReportQuery = riskInsightsReportQuery;
_addPwdHealthReportAppCommand = addPasswordHealthReportApplicationCommand; _addPwdHealthReportAppCommand = addPasswordHealthReportApplicationCommand;
_getPwdHealthReportAppQuery = getPasswordHealthReportApplicationQuery; _getPwdHealthReportAppQuery = getPasswordHealthReportApplicationQuery;
_dropPwdHealthReportAppCommand = dropPwdHealthReportAppCommand; _dropPwdHealthReportAppCommand = dropPwdHealthReportAppCommand;
@ -54,9 +58,9 @@ public class ReportsController : Controller
throw new NotFoundException(); 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; return responses;
} }
@ -69,16 +73,16 @@ public class ReportsController : Controller
/// <returns>IEnumerable of MemberAccessReportResponseModel</returns> /// <returns>IEnumerable of MemberAccessReportResponseModel</returns>
/// <exception cref="NotFoundException">If Access reports permission is not assigned</exception> /// <exception cref="NotFoundException">If Access reports permission is not assigned</exception>
[HttpGet("member-access/{orgId}")] [HttpGet("member-access/{orgId}")]
public async Task<IEnumerable<MemberAccessReportResponseModel>> GetMemberAccessReport(Guid orgId) public async Task<IEnumerable<MemberAccessDetailReportResponseModel>> GetMemberAccessReport(Guid orgId)
{ {
if (!await _currentContext.AccessReports(orgId)) if (!await _currentContext.AccessReports(orgId))
{ {
throw new NotFoundException(); 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; return responses;
} }
@ -87,13 +91,28 @@ public class ReportsController : Controller
/// Contains the organization member info, the cipher ids associated with the member, /// Contains the organization member info, the cipher ids associated with the member,
/// and details on their collections, groups, and permissions /// and details on their collections, groups, and permissions
/// </summary> /// </summary>
/// <param name="request">Request to the MemberAccessCipherDetailsQuery</param> /// <param name="request">Request parameters</param>
/// <returns>IEnumerable of MemberAccessCipherDetails</returns> /// <returns>
private async Task<IEnumerable<MemberAccessCipherDetails>> GetMemberCipherDetails(MemberAccessCipherDetailsRequest request) /// List of a user's permissions at a group and collection level as well as the number of ciphers
/// associated with that group/collection
/// </returns>
private async Task<IEnumerable<MemberAccessReportDetail>> GetMemberAccessDetails(
MemberAccessReportRequest request)
{ {
var memberCipherDetails = var accessDetails = await _memberAccessReportQuery.GetMemberAccessReportsAsync(request);
await _memberAccessCipherDetailsQuery.GetMemberAccessCipherDetails(request); return accessDetails;
return memberCipherDetails; }
/// <summary>
/// Gets the risk insights report details from the risk insights query. Associates a user to their cipher ids
/// </summary>
/// <param name="request">Request parameters</param>
/// <returns>A list of risk insights data associating the user to cipher ids</returns>
private async Task<IEnumerable<RiskInsightsReportDetail>> GetRiskInsightsReportDetails(
RiskInsightsReportRequest request)
{
var riskDetails = await _riskInsightsReportQuery.GetRiskInsightsReportDetails(request);
return riskDetails;
} }
/// <summary> /// <summary>

View File

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

View File

@ -15,12 +15,12 @@ public class MemberCipherDetailsResponseModel
/// </summary> /// </summary>
public IEnumerable<string> CipherIds { get; set; } public IEnumerable<string> CipherIds { get; set; }
public MemberCipherDetailsResponseModel(MemberAccessCipherDetails memberAccessCipherDetails) public MemberCipherDetailsResponseModel(RiskInsightsReportDetail reportDetail)
{ {
this.UserGuid = memberAccessCipherDetails.UserGuid; this.UserGuid = reportDetail.UserGuid;
this.UserName = memberAccessCipherDetails.UserName; this.UserName = reportDetail.UserName;
this.Email = memberAccessCipherDetails.Email; this.Email = reportDetail.Email;
this.UsesKeyConnector = memberAccessCipherDetails.UsesKeyConnector; this.UsesKeyConnector = reportDetail.UsesKeyConnector;
this.CipherIds = memberAccessCipherDetails.CipherIds; this.CipherIds = reportDetail.CipherIds;
} }
} }

View File

@ -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<Guid> CipherIds { get; set; }
}

View File

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

View File

@ -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<string> CipherIds { get; set; }
}

View File

@ -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<IEnumerable<MemberAccessCipherDetails>> 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;
}
/// <summary>
/// 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.
/// </summary>
/// <param name="orgGroups">Organization groups collection</param>
/// <param name="orgCollectionsWithAccess">Collections for the organization and the groups/users and permissions</param>
/// <param name="orgItems">Cipher items for the organization with the collections associated with them</param>
/// <param name="organizationUsersTwoFactorEnabled">Organization users and two factor status</param>
/// <param name="orgAbility">Organization ability for account recovery status</param>
/// <returns>List of the MemberAccessCipherDetailsModel</returns>;
private IEnumerable<MemberAccessCipherDetails> GenerateAccessDataParallel(
ICollection<Group> orgGroups,
ICollection<Tuple<Collection, CollectionAccessDetails>> orgCollectionsWithAccess,
IEnumerable<CipherOrganizationDetailsWithCollections> orgItems,
IEnumerable<(OrganizationUserUserDetails user, bool twoFactorIsEnabled)> organizationUsersTwoFactorEnabled,
OrganizationAbility orgAbility)
{
var orgUsers = organizationUsersTwoFactorEnabled.Select(x => x.user).ToList();
var groupNameDictionary = orgGroups.ToDictionary(x => x.Id, x => x.Name);
var collectionItems = orgItems
.SelectMany(x => x.CollectionIds,
(cipher, collectionId) => new { Cipher = cipher, CollectionId = collectionId })
.GroupBy(y => y.CollectionId,
(key, ciphers) => new { CollectionId = key, Ciphers = ciphers });
var itemLookup = collectionItems.ToDictionary(x => x.CollectionId.ToString(), x => x.Ciphers.Select(c => c.Cipher.Id.ToString()).ToList());
var memberAccessCipherDetails = new ConcurrentBag<MemberAccessCipherDetails>();
Parallel.ForEach(orgUsers, user =>
{
var groupAccessDetails = new List<MemberAccessDetails>();
var userCollectionAccessDetails = new List<MemberAccessDetails>();
foreach (var tCollect in orgCollectionsWithAccess)
{
if (itemLookup.TryGetValue(tCollect.Item1.Id.ToString(), out var items))
{
var itemCounts = items.Count;
if (tCollect.Item2.Groups.Any())
{
var groupDetails = tCollect.Item2.Groups
.Where(tCollectGroups => user.Groups.Contains(tCollectGroups.Id))
.Select(x => new MemberAccessDetails
{
CollectionId = tCollect.Item1.Id,
CollectionName = tCollect.Item1.Name,
GroupId = x.Id,
GroupName = groupNameDictionary[x.Id],
ReadOnly = x.ReadOnly,
HidePasswords = x.HidePasswords,
Manage = x.Manage,
ItemCount = itemCounts,
CollectionCipherIds = items
});
groupAccessDetails.AddRange(groupDetails);
}
if (tCollect.Item2.Users.Any())
{
var userCollectionDetails = tCollect.Item2.Users
.Where(tCollectUser => tCollectUser.Id == user.Id)
.Select(x => new MemberAccessDetails
{
CollectionId = tCollect.Item1.Id,
CollectionName = tCollect.Item1.Name,
ReadOnly = x.ReadOnly,
HidePasswords = x.HidePasswords,
Manage = x.Manage,
ItemCount = itemCounts,
CollectionCipherIds = items
});
userCollectionAccessDetails.AddRange(userCollectionDetails);
}
}
}
var report = new MemberAccessCipherDetails
{
UserName = user.Name,
Email = user.Email,
TwoFactorEnabled = organizationUsersTwoFactorEnabled.FirstOrDefault(u => u.user.Id == user.Id).twoFactorIsEnabled,
AccountRecoveryEnabled = !string.IsNullOrEmpty(user.ResetPasswordKey) && orgAbility.UseResetPassword,
UserGuid = user.Id,
UsesKeyConnector = user.UsesKeyConnector
};
var userAccessDetails = new List<MemberAccessDetails>();
if (user.Groups.Any())
{
var userGroups = groupAccessDetails.Where(x => user.Groups.Contains(x.GroupId.GetValueOrDefault()));
userAccessDetails.AddRange(userGroups);
}
var groupsWithoutCollections = user.Groups.Where(x => !userAccessDetails.Any(y => x == y.GroupId));
if (groupsWithoutCollections.Any())
{
var emptyGroups = groupsWithoutCollections.Select(x => new MemberAccessDetails
{
GroupId = x,
GroupName = groupNameDictionary[x],
ItemCount = 0
});
userAccessDetails.AddRange(emptyGroups);
}
if (user.Collections.Any())
{
var userCollections = userCollectionAccessDetails.Where(x => user.Collections.Any(y => x.CollectionId == y.Id));
userAccessDetails.AddRange(userCollections);
}
report.AccessDetails = userAccessDetails;
var userCiphers = report.AccessDetails
.Where(x => x.ItemCount > 0)
.SelectMany(y => y.CollectionCipherIds)
.Distinct();
report.CipherIds = userCiphers;
report.TotalItemCount = userCiphers.Count();
var distinctItems = report.AccessDetails.Where(x => x.CollectionId.HasValue).Select(x => x.CollectionId).Distinct();
report.CollectionsCount = distinctItems.Count();
report.GroupsCount = report.AccessDetails.Select(x => x.GroupId).Where(y => y.HasValue).Distinct().Count();
memberAccessCipherDetails.Add(report);
});
return memberAccessCipherDetails;
}
}

View File

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

View File

@ -3,7 +3,7 @@ using Bit.Core.Dirt.Reports.ReportFeatures.Requests;
namespace Bit.Core.Dirt.Reports.ReportFeatures.OrganizationReportMembers.Interfaces; namespace Bit.Core.Dirt.Reports.ReportFeatures.OrganizationReportMembers.Interfaces;
public interface IMemberAccessCipherDetailsQuery public interface IMemberAccessReportQuery
{ {
Task<IEnumerable<MemberAccessCipherDetails>> GetMemberAccessCipherDetails(MemberAccessCipherDetailsRequest request); Task<IEnumerable<MemberAccessReportDetail>> GetMemberAccessReportsAsync(MemberAccessReportRequest request);
} }

View File

@ -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<IEnumerable<RiskInsightsReportDetail>> GetRiskInsightsReportDetails(RiskInsightsReportRequest request);
}

View File

@ -8,7 +8,8 @@ public static class ReportingServiceCollectionExtensions
{ {
public static void AddReportingServices(this IServiceCollection services) public static void AddReportingServices(this IServiceCollection services)
{ {
services.AddScoped<IMemberAccessCipherDetailsQuery, MemberAccessCipherDetailsQuery>(); services.AddScoped<IRiskInsightsReportQuery, RiskInsightsReportQuery>();
services.AddScoped<IMemberAccessReportQuery, MemberAccessReportQuery>();
services.AddScoped<IAddPasswordHealthReportApplicationCommand, AddPasswordHealthReportApplicationCommand>(); services.AddScoped<IAddPasswordHealthReportApplicationCommand, AddPasswordHealthReportApplicationCommand>();
services.AddScoped<IGetPasswordHealthReportApplicationQuery, GetPasswordHealthReportApplicationQuery>(); services.AddScoped<IGetPasswordHealthReportApplicationQuery, GetPasswordHealthReportApplicationQuery>();
services.AddScoped<IDropPasswordHealthReportApplicationCommand, DropPasswordHealthReportApplicationCommand>(); services.AddScoped<IDropPasswordHealthReportApplicationCommand, DropPasswordHealthReportApplicationCommand>();

View File

@ -1,6 +1,6 @@
namespace Bit.Core.Dirt.Reports.ReportFeatures.Requests; namespace Bit.Core.Dirt.Reports.ReportFeatures.Requests;
public class MemberAccessCipherDetailsRequest public class MemberAccessReportRequest
{ {
public Guid OrganizationId { get; set; } public Guid OrganizationId { get; set; }
} }

View File

@ -0,0 +1,6 @@
namespace Bit.Core.Dirt.Reports.ReportFeatures.Requests;
public class RiskInsightsReportRequest
{
public Guid OrganizationId { get; set; }
}

View File

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

View File

@ -0,0 +1,8 @@
using Bit.Core.Dirt.Reports.Models.Data;
namespace Bit.Core.Dirt.Reports.Repositories;
public interface IOrganizationMemberBaseDetailRepository
{
Task<IEnumerable<OrganizationMemberBaseDetail>> GetOrganizationMemberBaseDetailsByOrganizationId(Guid organizationId);
}

View File

@ -70,6 +70,7 @@ public static class DapperServiceCollectionExtensions
services.AddSingleton<ISecurityTaskRepository, SecurityTaskRepository>(); services.AddSingleton<ISecurityTaskRepository, SecurityTaskRepository>();
services.AddSingleton<IUserAsymmetricKeysRepository, UserAsymmetricKeysRepository>(); services.AddSingleton<IUserAsymmetricKeysRepository, UserAsymmetricKeysRepository>();
services.AddSingleton<IOrganizationInstallationRepository, OrganizationInstallationRepository>(); services.AddSingleton<IOrganizationInstallationRepository, OrganizationInstallationRepository>();
services.AddSingleton<IOrganizationMemberBaseDetailRepository, OrganizationMemberBaseDetailRepository>();
if (selfHosted) if (selfHosted)
{ {

View File

@ -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<IEnumerable<OrganizationMemberBaseDetail>> GetOrganizationMemberBaseDetailsByOrganizationId(
Guid organizationId)
{
await using var connection = new SqlConnection(ConnectionString);
var result = await connection.QueryAsync<OrganizationMemberBaseDetail>(
"[dbo].[MemberAccessReport_GetMemberAccessCipherDetailsByOrganizationId]",
new
{
OrganizationId = organizationId
}, commandType: CommandType.StoredProcedure);
return result;
}
}

View File

@ -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<IEnumerable<OrganizationMemberBaseDetail>> GetOrganizationMemberBaseDetailsByOrganizationId(
Guid organizationId)
{
await using var scope = ServiceScopeFactory.CreateAsyncScope();
var dbContext = GetDatabaseContext(scope);
var result = await dbContext.Set<OrganizationMemberBaseDetail>()
.FromSqlRaw("EXEC [dbo].[MemberAccessReport_GetMemberAccessCipherDetailsByOrganizationId] @OrganizationId",
new SqlParameter("@OrganizationId", organizationId))
.ToListAsync();
return result;
}
}

View File

@ -14,6 +14,7 @@ using Bit.Core.Vault.Repositories;
using Bit.Infrastructure.EntityFramework.AdminConsole.Repositories; using Bit.Infrastructure.EntityFramework.AdminConsole.Repositories;
using Bit.Infrastructure.EntityFramework.Auth.Repositories; using Bit.Infrastructure.EntityFramework.Auth.Repositories;
using Bit.Infrastructure.EntityFramework.Billing.Repositories; using Bit.Infrastructure.EntityFramework.Billing.Repositories;
using Bit.Infrastructure.EntityFramework.Dirt;
using Bit.Infrastructure.EntityFramework.Dirt.Repositories; using Bit.Infrastructure.EntityFramework.Dirt.Repositories;
using Bit.Infrastructure.EntityFramework.KeyManagement.Repositories; using Bit.Infrastructure.EntityFramework.KeyManagement.Repositories;
using Bit.Infrastructure.EntityFramework.NotificationCenter.Repositories; using Bit.Infrastructure.EntityFramework.NotificationCenter.Repositories;
@ -107,6 +108,7 @@ public static class EntityFrameworkServiceCollectionExtensions
services.AddSingleton<ISecurityTaskRepository, SecurityTaskRepository>(); services.AddSingleton<ISecurityTaskRepository, SecurityTaskRepository>();
services.AddSingleton<IUserAsymmetricKeysRepository, UserAsymmetricKeysRepository>(); services.AddSingleton<IUserAsymmetricKeysRepository, UserAsymmetricKeysRepository>();
services.AddSingleton<IOrganizationInstallationRepository, OrganizationInstallationRepository>(); services.AddSingleton<IOrganizationInstallationRepository, OrganizationInstallationRepository>();
services.AddSingleton<IOrganizationMemberBaseDetailRepository, OrganizationMemberBaseDetailRepository>();
if (selfHosted) if (selfHosted)
{ {

View File

@ -1,4 +1,5 @@
using Bit.Core; using Bit.Core;
using Bit.Core.Dirt.Reports.Models.Data;
using Bit.Infrastructure.EntityFramework.AdminConsole.Models; using Bit.Infrastructure.EntityFramework.AdminConsole.Models;
using Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider; using Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider;
using Bit.Infrastructure.EntityFramework.Auth.Models; using Bit.Infrastructure.EntityFramework.Auth.Models;
@ -80,6 +81,7 @@ public class DatabaseContext : DbContext
public DbSet<NotificationStatus> NotificationStatuses { get; set; } public DbSet<NotificationStatus> NotificationStatuses { get; set; }
public DbSet<ClientOrganizationMigrationRecord> ClientOrganizationMigrationRecords { get; set; } public DbSet<ClientOrganizationMigrationRecord> ClientOrganizationMigrationRecords { get; set; }
public DbSet<PasswordHealthReportApplication> PasswordHealthReportApplications { get; set; } public DbSet<PasswordHealthReportApplication> PasswordHealthReportApplications { get; set; }
public DbSet<OrganizationMemberBaseDetail> OrganizationMemberBaseDetails { get; set; }
public DbSet<SecurityTask> SecurityTasks { get; set; } public DbSet<SecurityTask> SecurityTasks { get; set; }
public DbSet<OrganizationInstallation> OrganizationInstallations { get; set; } public DbSet<OrganizationInstallation> OrganizationInstallations { get; set; }
@ -112,6 +114,7 @@ public class DatabaseContext : DbContext
var eOrganizationConnection = builder.Entity<OrganizationConnection>(); var eOrganizationConnection = builder.Entity<OrganizationConnection>();
var eOrganizationDomain = builder.Entity<OrganizationDomain>(); var eOrganizationDomain = builder.Entity<OrganizationDomain>();
var aWebAuthnCredential = builder.Entity<WebAuthnCredential>(); var aWebAuthnCredential = builder.Entity<WebAuthnCredential>();
var eOrganizationMemberBaseDetail = builder.Entity<OrganizationMemberBaseDetail>();
// Shadow property configurations go here // Shadow property configurations go here
@ -134,6 +137,8 @@ public class DatabaseContext : DbContext
eCollectionGroup.HasKey(cg => new { cg.CollectionId, cg.GroupId }); eCollectionGroup.HasKey(cg => new { cg.CollectionId, cg.GroupId });
eGroupUser.HasKey(gu => new { gu.GroupId, gu.OrganizationUserId }); eGroupUser.HasKey(gu => new { gu.GroupId, gu.OrganizationUserId });
eOrganizationMemberBaseDetail.HasNoKey();
var dataProtector = this.GetService<DP.IDataProtectionProvider>().CreateProtector( var dataProtector = this.GetService<DP.IDataProtectionProvider>().CreateProtector(
Constants.DatabaseFieldProtectorPurpose); Constants.DatabaseFieldProtectorPurpose);
var dataProtectionConverter = new DataProtectionConverter(dataProtector); var dataProtectionConverter = new DataProtectionConverter(dataProtector);

View File

@ -0,0 +1,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
)

View File

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

View File

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

View File

@ -0,0 +1,50 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Bit.MySqlMigrations.Migrations;
/// <inheritdoc />
public partial class _20250609_00_AddMemberAccessReportStoreProceduresql : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "OrganizationMemberBaseDetails",
columns: table => new
{
UserGuid = table.Column<Guid>(type: "char(36)", nullable: true, collation: "ascii_general_ci"),
UserName = table.Column<string>(type: "longtext", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
Email = table.Column<string>(type: "longtext", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
TwoFactorProviders = table.Column<string>(type: "longtext", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
UsesKeyConnector = table.Column<bool>(type: "tinyint(1)", nullable: false),
ResetPasswordKey = table.Column<string>(type: "longtext", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
CollectionId = table.Column<Guid>(type: "char(36)", nullable: true, collation: "ascii_general_ci"),
GroupId = table.Column<Guid>(type: "char(36)", nullable: true, collation: "ascii_general_ci"),
GroupName = table.Column<string>(type: "longtext", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
CollectionName = table.Column<string>(type: "longtext", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
ReadOnly = table.Column<bool>(type: "tinyint(1)", nullable: true),
HidePasswords = table.Column<bool>(type: "tinyint(1)", nullable: true),
Manage = table.Column<bool>(type: "tinyint(1)", nullable: true),
CipherId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci")
},
constraints: table =>
{
})
.Annotation("MySql:CharSet", "utf8mb4");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "OrganizationMemberBaseDetails");
}
}

View File

@ -22,6 +22,53 @@ namespace Bit.MySqlMigrations.Migrations
MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder);
modelBuilder.Entity("Bit.Core.Dirt.Reports.Models.Data.OrganizationMemberBaseDetail", b =>
{
b.Property<Guid>("CipherId")
.HasColumnType("char(36)");
b.Property<Guid?>("CollectionId")
.HasColumnType("char(36)");
b.Property<string>("CollectionName")
.HasColumnType("longtext");
b.Property<string>("Email")
.HasColumnType("longtext");
b.Property<Guid?>("GroupId")
.HasColumnType("char(36)");
b.Property<string>("GroupName")
.HasColumnType("longtext");
b.Property<bool?>("HidePasswords")
.HasColumnType("tinyint(1)");
b.Property<bool?>("Manage")
.HasColumnType("tinyint(1)");
b.Property<bool?>("ReadOnly")
.HasColumnType("tinyint(1)");
b.Property<string>("ResetPasswordKey")
.HasColumnType("longtext");
b.Property<string>("TwoFactorProviders")
.HasColumnType("longtext");
b.Property<Guid?>("UserGuid")
.HasColumnType("char(36)");
b.Property<string>("UserName")
.HasColumnType("longtext");
b.Property<bool>("UsesKeyConnector")
.HasColumnType("tinyint(1)");
b.ToTable("OrganizationMemberBaseDetails");
});
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b =>
{ {
b.Property<Guid>("Id") b.Property<Guid>("Id")
@ -920,6 +967,34 @@ namespace Bit.MySqlMigrations.Migrations
b.ToTable("ProviderPlan", (string)null); b.ToTable("ProviderPlan", (string)null);
}); });
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.PasswordHealthReportApplication", b =>
{
b.Property<Guid>("Id")
.HasColumnType("char(36)");
b.Property<DateTime>("CreationDate")
.HasColumnType("datetime(6)");
b.Property<Guid>("OrganizationId")
.HasColumnType("char(36)");
b.Property<DateTime>("RevisionDate")
.HasColumnType("datetime(6)");
b.Property<string>("Uri")
.HasColumnType("longtext");
b.HasKey("Id");
b.HasIndex("Id")
.HasAnnotation("SqlServer:Clustered", true);
b.HasIndex("OrganizationId")
.HasAnnotation("SqlServer:Clustered", false);
b.ToTable("PasswordHealthReportApplication", (string)null);
});
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Cache", b => modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Cache", b =>
{ {
b.Property<string>("Id") b.Property<string>("Id")
@ -2023,34 +2098,6 @@ namespace Bit.MySqlMigrations.Migrations
b.ToTable("ServiceAccount", (string)null); b.ToTable("ServiceAccount", (string)null);
}); });
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Tools.Models.PasswordHealthReportApplication", b =>
{
b.Property<Guid>("Id")
.HasColumnType("char(36)");
b.Property<DateTime>("CreationDate")
.HasColumnType("datetime(6)");
b.Property<Guid>("OrganizationId")
.HasColumnType("char(36)");
b.Property<DateTime>("RevisionDate")
.HasColumnType("datetime(6)");
b.Property<string>("Uri")
.HasColumnType("longtext");
b.HasKey("Id");
b.HasIndex("Id")
.HasAnnotation("SqlServer:Clustered", true);
b.HasIndex("OrganizationId")
.HasAnnotation("SqlServer:Clustered", false);
b.ToTable("PasswordHealthReportApplication", (string)null);
});
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b =>
{ {
b.Property<Guid>("Id") b.Property<Guid>("Id")
@ -2532,6 +2579,17 @@ namespace Bit.MySqlMigrations.Migrations
b.Navigation("Provider"); b.Navigation("Provider");
}); });
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.PasswordHealthReportApplication", b =>
{
b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization")
.WithMany()
.HasForeignKey("OrganizationId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Organization");
});
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b =>
{ {
b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization")
@ -2825,17 +2883,6 @@ namespace Bit.MySqlMigrations.Migrations
b.Navigation("Organization"); b.Navigation("Organization");
}); });
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Tools.Models.PasswordHealthReportApplication", b =>
{
b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization")
.WithMany()
.HasForeignKey("OrganizationId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Organization");
});
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b =>
{ {
b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization")

View File

@ -0,0 +1,43 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Bit.PostgresMigrations.Migrations;
/// <inheritdoc />
public partial class _20250609_00_AddMemberAccessReportStoreProceduresql : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "OrganizationMemberBaseDetails",
columns: table => new
{
UserGuid = table.Column<Guid>(type: "uuid", nullable: true),
UserName = table.Column<string>(type: "text", nullable: true),
Email = table.Column<string>(type: "text", nullable: true),
TwoFactorProviders = table.Column<string>(type: "text", nullable: true),
UsesKeyConnector = table.Column<bool>(type: "boolean", nullable: false),
ResetPasswordKey = table.Column<string>(type: "text", nullable: true),
CollectionId = table.Column<Guid>(type: "uuid", nullable: true),
GroupId = table.Column<Guid>(type: "uuid", nullable: true),
GroupName = table.Column<string>(type: "text", nullable: true),
CollectionName = table.Column<string>(type: "text", nullable: true),
ReadOnly = table.Column<bool>(type: "boolean", nullable: true),
HidePasswords = table.Column<bool>(type: "boolean", nullable: true),
Manage = table.Column<bool>(type: "boolean", nullable: true),
CipherId = table.Column<Guid>(type: "uuid", nullable: false)
},
constraints: table =>
{
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "OrganizationMemberBaseDetails");
}
}

View File

@ -23,6 +23,53 @@ namespace Bit.PostgresMigrations.Migrations
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Bit.Core.Dirt.Reports.Models.Data.OrganizationMemberBaseDetail", b =>
{
b.Property<Guid>("CipherId")
.HasColumnType("uuid");
b.Property<Guid?>("CollectionId")
.HasColumnType("uuid");
b.Property<string>("CollectionName")
.HasColumnType("text");
b.Property<string>("Email")
.HasColumnType("text");
b.Property<Guid?>("GroupId")
.HasColumnType("uuid");
b.Property<string>("GroupName")
.HasColumnType("text");
b.Property<bool?>("HidePasswords")
.HasColumnType("boolean");
b.Property<bool?>("Manage")
.HasColumnType("boolean");
b.Property<bool?>("ReadOnly")
.HasColumnType("boolean");
b.Property<string>("ResetPasswordKey")
.HasColumnType("text");
b.Property<string>("TwoFactorProviders")
.HasColumnType("text");
b.Property<Guid?>("UserGuid")
.HasColumnType("uuid");
b.Property<string>("UserName")
.HasColumnType("text");
b.Property<bool>("UsesKeyConnector")
.HasColumnType("boolean");
b.ToTable("OrganizationMemberBaseDetails");
});
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b =>
{ {
b.Property<Guid>("Id") b.Property<Guid>("Id")
@ -925,6 +972,34 @@ namespace Bit.PostgresMigrations.Migrations
b.ToTable("ProviderPlan", (string)null); b.ToTable("ProviderPlan", (string)null);
}); });
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.PasswordHealthReportApplication", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uuid");
b.Property<DateTime>("CreationDate")
.HasColumnType("timestamp with time zone");
b.Property<Guid>("OrganizationId")
.HasColumnType("uuid");
b.Property<DateTime>("RevisionDate")
.HasColumnType("timestamp with time zone");
b.Property<string>("Uri")
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("Id")
.HasAnnotation("SqlServer:Clustered", true);
b.HasIndex("OrganizationId")
.HasAnnotation("SqlServer:Clustered", false);
b.ToTable("PasswordHealthReportApplication", (string)null);
});
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Cache", b => modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Cache", b =>
{ {
b.Property<string>("Id") b.Property<string>("Id")
@ -2029,34 +2104,6 @@ namespace Bit.PostgresMigrations.Migrations
b.ToTable("ServiceAccount", (string)null); b.ToTable("ServiceAccount", (string)null);
}); });
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Tools.Models.PasswordHealthReportApplication", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uuid");
b.Property<DateTime>("CreationDate")
.HasColumnType("timestamp with time zone");
b.Property<Guid>("OrganizationId")
.HasColumnType("uuid");
b.Property<DateTime>("RevisionDate")
.HasColumnType("timestamp with time zone");
b.Property<string>("Uri")
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("Id")
.HasAnnotation("SqlServer:Clustered", true);
b.HasIndex("OrganizationId")
.HasAnnotation("SqlServer:Clustered", false);
b.ToTable("PasswordHealthReportApplication", (string)null);
});
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b =>
{ {
b.Property<Guid>("Id") b.Property<Guid>("Id")
@ -2538,6 +2585,17 @@ namespace Bit.PostgresMigrations.Migrations
b.Navigation("Provider"); b.Navigation("Provider");
}); });
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.PasswordHealthReportApplication", b =>
{
b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization")
.WithMany()
.HasForeignKey("OrganizationId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Organization");
});
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b =>
{ {
b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization")
@ -2831,17 +2889,6 @@ namespace Bit.PostgresMigrations.Migrations
b.Navigation("Organization"); b.Navigation("Organization");
}); });
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Tools.Models.PasswordHealthReportApplication", b =>
{
b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization")
.WithMany()
.HasForeignKey("OrganizationId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Organization");
});
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b =>
{ {
b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization")

View File

@ -0,0 +1,43 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Bit.SqliteMigrations.Migrations;
/// <inheritdoc />
public partial class _20250609_00_AddMemberAccessReportStoreProceduresql : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "OrganizationMemberBaseDetails",
columns: table => new
{
UserGuid = table.Column<Guid>(type: "TEXT", nullable: true),
UserName = table.Column<string>(type: "TEXT", nullable: true),
Email = table.Column<string>(type: "TEXT", nullable: true),
TwoFactorProviders = table.Column<string>(type: "TEXT", nullable: true),
UsesKeyConnector = table.Column<bool>(type: "INTEGER", nullable: false),
ResetPasswordKey = table.Column<string>(type: "TEXT", nullable: true),
CollectionId = table.Column<Guid>(type: "TEXT", nullable: true),
GroupId = table.Column<Guid>(type: "TEXT", nullable: true),
GroupName = table.Column<string>(type: "TEXT", nullable: true),
CollectionName = table.Column<string>(type: "TEXT", nullable: true),
ReadOnly = table.Column<bool>(type: "INTEGER", nullable: true),
HidePasswords = table.Column<bool>(type: "INTEGER", nullable: true),
Manage = table.Column<bool>(type: "INTEGER", nullable: true),
CipherId = table.Column<Guid>(type: "TEXT", nullable: false)
},
constraints: table =>
{
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "OrganizationMemberBaseDetails");
}
}

View File

@ -17,6 +17,53 @@ namespace Bit.SqliteMigrations.Migrations
#pragma warning disable 612, 618 #pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "8.0.8"); modelBuilder.HasAnnotation("ProductVersion", "8.0.8");
modelBuilder.Entity("Bit.Core.Dirt.Reports.Models.Data.OrganizationMemberBaseDetail", b =>
{
b.Property<Guid>("CipherId")
.HasColumnType("TEXT");
b.Property<Guid?>("CollectionId")
.HasColumnType("TEXT");
b.Property<string>("CollectionName")
.HasColumnType("TEXT");
b.Property<string>("Email")
.HasColumnType("TEXT");
b.Property<Guid?>("GroupId")
.HasColumnType("TEXT");
b.Property<string>("GroupName")
.HasColumnType("TEXT");
b.Property<bool?>("HidePasswords")
.HasColumnType("INTEGER");
b.Property<bool?>("Manage")
.HasColumnType("INTEGER");
b.Property<bool?>("ReadOnly")
.HasColumnType("INTEGER");
b.Property<string>("ResetPasswordKey")
.HasColumnType("TEXT");
b.Property<string>("TwoFactorProviders")
.HasColumnType("TEXT");
b.Property<Guid?>("UserGuid")
.HasColumnType("TEXT");
b.Property<string>("UserName")
.HasColumnType("TEXT");
b.Property<bool>("UsesKeyConnector")
.HasColumnType("INTEGER");
b.ToTable("OrganizationMemberBaseDetails");
});
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b =>
{ {
b.Property<Guid>("Id") b.Property<Guid>("Id")
@ -909,6 +956,34 @@ namespace Bit.SqliteMigrations.Migrations
b.ToTable("ProviderPlan", (string)null); b.ToTable("ProviderPlan", (string)null);
}); });
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.PasswordHealthReportApplication", b =>
{
b.Property<Guid>("Id")
.HasColumnType("TEXT");
b.Property<DateTime>("CreationDate")
.HasColumnType("TEXT");
b.Property<Guid>("OrganizationId")
.HasColumnType("TEXT");
b.Property<DateTime>("RevisionDate")
.HasColumnType("TEXT");
b.Property<string>("Uri")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("Id")
.HasAnnotation("SqlServer:Clustered", true);
b.HasIndex("OrganizationId")
.HasAnnotation("SqlServer:Clustered", false);
b.ToTable("PasswordHealthReportApplication", (string)null);
});
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Cache", b => modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Cache", b =>
{ {
b.Property<string>("Id") b.Property<string>("Id")
@ -2012,34 +2087,6 @@ namespace Bit.SqliteMigrations.Migrations
b.ToTable("ServiceAccount", (string)null); b.ToTable("ServiceAccount", (string)null);
}); });
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Tools.Models.PasswordHealthReportApplication", b =>
{
b.Property<Guid>("Id")
.HasColumnType("TEXT");
b.Property<DateTime>("CreationDate")
.HasColumnType("TEXT");
b.Property<Guid>("OrganizationId")
.HasColumnType("TEXT");
b.Property<DateTime>("RevisionDate")
.HasColumnType("TEXT");
b.Property<string>("Uri")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("Id")
.HasAnnotation("SqlServer:Clustered", true);
b.HasIndex("OrganizationId")
.HasAnnotation("SqlServer:Clustered", false);
b.ToTable("PasswordHealthReportApplication", (string)null);
});
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b =>
{ {
b.Property<Guid>("Id") b.Property<Guid>("Id")
@ -2521,6 +2568,17 @@ namespace Bit.SqliteMigrations.Migrations
b.Navigation("Provider"); b.Navigation("Provider");
}); });
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.PasswordHealthReportApplication", b =>
{
b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization")
.WithMany()
.HasForeignKey("OrganizationId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Organization");
});
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b =>
{ {
b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization")
@ -2814,17 +2872,6 @@ namespace Bit.SqliteMigrations.Migrations
b.Navigation("Organization"); b.Navigation("Organization");
}); });
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Tools.Models.PasswordHealthReportApplication", b =>
{
b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization")
.WithMany()
.HasForeignKey("OrganizationId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Organization");
});
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b =>
{ {
b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization")