mirror of
https://github.com/bitwarden/server
synced 2025-12-25 04:33:26 +00:00
[PM-21031] Optimize GET Members endpoint performance (#5907)
* Add new feature flag for Members Get Endpoint Optimization * Add a new version of OrganizationUser_ReadByOrganizationIdWithClaimedDomains that uses CTE for better performance * Add stored procedure OrganizationUserUserDetails_ReadByOrganizationId_V2 for retrieving user details, group associations, and collection associations by organization ID. * Add the sql migration script to add the new stored procedures * Introduce GetManyDetailsByOrganizationAsync_vNext and GetManyByOrganizationWithClaimedDomainsAsync_vNext in IOrganizationUserRepository to enhance performance by reducing database round trips. * Updated GetOrganizationUsersClaimedStatusQuery to use an optimized query when the feature flag is enabled * Updated OrganizationUserUserDetailsQuery to use optimized queries when the feature flag is enabled * Add integration tests for GetManyDetailsByOrganizationAsync_vNext * Add integration tests for GetManyByOrganizationWithClaimedDomainsAsync_vNext to validate behavior with verified and unverified domains. * Optimize performance by conditionally setting permissions only for Custom user types in OrganizationUserUserDetailsQuery. * Create UserEmailDomainView to extract email domains from users' email addresses * Create stored procedure Organization_ReadByClaimedUserEmailDomain_V2 that uses UserEmailDomainView to fetch Email domains * Add GetByVerifiedUserEmailDomainAsync_vNext method to IOrganizationRepository and its implementations * Refactor OrganizationUser_ReadByOrganizationIdWithClaimedDomains_V2 stored procedure to use UserEmailDomainView for email domain extraction, improving query efficiency and clarity. * Enhance IOrganizationUserRepository with detailed documentation for GetManyDetailsByOrganizationAsync method, clarifying its purpose and performance optimizations. Added remarks for better understanding of its functionality. * Fix missing newline at the end of Organization_ReadByClaimedUserEmailDomain_V2.sql to adhere to coding standards. * Update the database migration script to include UserEmailDomainView * Bumped the date on the migration script * Remove GetByVerifiedUserEmailDomainAsync_vNext method and its stored procedure. * Refactor UserEmailDomainView index creation to check for existence before creation * Update OrganizationUser_ReadByOrganizationIdWithClaimedDomains_V2 to use CTE and add indexes * Remove creation of unique clustered index from UserEmailDomainView and related migration script adjustments * Update indexes and sproc * Fix index name when checking if it already exists * Bump up date on migration script
This commit is contained in:
@@ -404,6 +404,56 @@ public class OrganizationUserRepository : Repository<Core.Entities.OrganizationU
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ICollection<OrganizationUserUserDetails>> GetManyDetailsByOrganizationAsync_vNext(
|
||||
Guid organizationId, bool includeGroups, bool includeCollections)
|
||||
{
|
||||
using var scope = ServiceScopeFactory.CreateScope();
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
|
||||
var query = from ou in dbContext.OrganizationUsers
|
||||
where ou.OrganizationId == organizationId
|
||||
select new OrganizationUserUserDetails
|
||||
{
|
||||
Id = ou.Id,
|
||||
UserId = ou.UserId,
|
||||
OrganizationId = ou.OrganizationId,
|
||||
Name = ou.User.Name,
|
||||
Email = ou.User.Email ?? ou.Email,
|
||||
AvatarColor = ou.User.AvatarColor,
|
||||
TwoFactorProviders = ou.User.TwoFactorProviders,
|
||||
Premium = ou.User.Premium,
|
||||
Status = ou.Status,
|
||||
Type = ou.Type,
|
||||
ExternalId = ou.ExternalId,
|
||||
SsoExternalId = ou.User.SsoUsers
|
||||
.Where(su => su.OrganizationId == ou.OrganizationId)
|
||||
.Select(su => su.ExternalId)
|
||||
.FirstOrDefault(),
|
||||
Permissions = ou.Permissions,
|
||||
ResetPasswordKey = ou.ResetPasswordKey,
|
||||
UsesKeyConnector = ou.User != null && ou.User.UsesKeyConnector,
|
||||
AccessSecretsManager = ou.AccessSecretsManager,
|
||||
HasMasterPassword = ou.User != null && !string.IsNullOrWhiteSpace(ou.User.MasterPassword),
|
||||
|
||||
// Project directly from navigation properties with conditional loading
|
||||
Groups = includeGroups
|
||||
? ou.GroupUsers.Select(gu => gu.GroupId).ToList()
|
||||
: new List<Guid>(),
|
||||
|
||||
Collections = includeCollections
|
||||
? ou.CollectionUsers.Select(cu => new CollectionAccessSelection
|
||||
{
|
||||
Id = cu.CollectionId,
|
||||
ReadOnly = cu.ReadOnly,
|
||||
HidePasswords = cu.HidePasswords,
|
||||
Manage = cu.Manage
|
||||
}).ToList()
|
||||
: new List<CollectionAccessSelection>()
|
||||
};
|
||||
|
||||
return await query.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<ICollection<OrganizationUserOrganizationDetails>> GetManyDetailsByUserAsync(Guid userId,
|
||||
OrganizationUserStatusType? status = null)
|
||||
{
|
||||
@@ -732,6 +782,12 @@ public class OrganizationUserRepository : Repository<Core.Entities.OrganizationU
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ICollection<Core.Entities.OrganizationUser>> GetManyByOrganizationWithClaimedDomainsAsync_vNext(Guid organizationId)
|
||||
{
|
||||
// No EF optimization is required for this query
|
||||
return await GetManyByOrganizationWithClaimedDomainsAsync(organizationId);
|
||||
}
|
||||
|
||||
public async Task RevokeManyByIdAsync(IEnumerable<Guid> organizationUserIds)
|
||||
{
|
||||
using var scope = ServiceScopeFactory.CreateScope();
|
||||
|
||||
Reference in New Issue
Block a user