Files
server/src/Infrastructure.EntityFramework/AdminConsole/Repositories/PolicyRepository.cs
Rui Tomé 3866bc5155 [PM-23134] Update PolicyDetails sprocs for performance (#6421)
* Add integration tests for GetByUserIdWithPolicyDetailsAsync in OrganizationUserRepository

- Implemented multiple test cases to verify the behavior of GetByUserIdWithPolicyDetailsAsync for different user statuses (Confirmed, Accepted, Invited, Revoked).
- Ensured that the method returns correct policy details based on user status and organization.
- Added tests for scenarios with multiple organizations and non-existing policy types.
- Included checks for provider users and custom user permissions.

These tests enhance coverage and ensure the correctness of policy retrieval logic.

* Add UserProviderAccessView to identify which organizations a user can access as a provider

* Refactor PolicyDetails_ReadByUserId stored procedure to improve user access logic

- Introduced a Common Table Expression (CTE) for organization users to streamline the selection process based on user status and email.
- Added a CTE for providers to enhance clarity and maintainability.
- Updated the main query to utilize the new CTEs, improving readability and performance.
- Ensured that the procedure correctly identifies provider access based on user permissions.

* Refactor OrganizationUser_ReadByUserIdWithPolicyDetails stored procedure to enhance user access logic

- Introduced a Common Table Expression (CTE) for organization users to improve selection based on user status and email.
- Updated the main query to utilize the new CTEs, enhancing readability and performance.
- Adjusted the logic for identifying provider access to ensure accurate policy retrieval based on user permissions.

* Add new SQL migration script to refactor policy details queries

- Created a new view, UserProviderAccessView, to streamline user access to provider organizations.
- Introduced two stored procedures: PolicyDetails_ReadByUserId and OrganizationUser_ReadByUserIdWithPolicyDetails, enhancing the logic for retrieving policy details based on user ID and policy type.
- Utilized Common Table Expressions (CTEs) to improve query readability and performance, ensuring accurate policy retrieval based on user permissions and organization status.

* Remove GetPolicyDetailsByUserIdTests

* Refactor PolicyRequirementQuery to use GetPolicyDetailsByUserIdsAndPolicyType and update unit tests

* Remove GetPolicyDetailsByUserId method from IPolicyRepository and its implementations in PolicyRepository classes

* Revert changes to PolicyDetails_ReadByUserId stored procedure

* Refactor OrganizationUser_ReadByUserIdWithPolicyDetails stored procedure to use UNION instead of OR

* Reduce UserEmail variable size from NVARCHAR(320) to NVARCHAR(256) for consistency in stored procedures

* Bump date on migration script
2025-10-22 13:20:53 +01:00

238 lines
10 KiB
C#

// FIXME: Update this file to be null safe and then delete the line below
#nullable disable
using AutoMapper;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Enums;
using Bit.Infrastructure.EntityFramework.AdminConsole.Models;
using Bit.Infrastructure.EntityFramework.AdminConsole.Repositories.Queries;
using Bit.Infrastructure.EntityFramework.Repositories;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using AdminConsoleEntities = Bit.Core.AdminConsole.Entities;
namespace Bit.Infrastructure.EntityFramework.AdminConsole.Repositories;
public class PolicyRepository : Repository<AdminConsoleEntities.Policy, Policy, Guid>, IPolicyRepository
{
public PolicyRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper)
: base(serviceScopeFactory, mapper, (DatabaseContext context) => context.Policies)
{ }
public async Task<AdminConsoleEntities.Policy> GetByOrganizationIdTypeAsync(Guid organizationId, PolicyType type)
{
using (var scope = ServiceScopeFactory.CreateScope())
{
var dbContext = GetDatabaseContext(scope);
var results = await dbContext.Policies
.FirstOrDefaultAsync(p => p.OrganizationId == organizationId && p.Type == type);
return Mapper.Map<AdminConsoleEntities.Policy>(results);
}
}
public async Task<ICollection<AdminConsoleEntities.Policy>> GetManyByOrganizationIdAsync(Guid organizationId)
{
using (var scope = ServiceScopeFactory.CreateScope())
{
var dbContext = GetDatabaseContext(scope);
var results = await dbContext.Policies
.Where(p => p.OrganizationId == organizationId)
.ToListAsync();
return Mapper.Map<List<AdminConsoleEntities.Policy>>(results);
}
}
public async Task<ICollection<AdminConsoleEntities.Policy>> GetManyByUserIdAsync(Guid userId)
{
using (var scope = ServiceScopeFactory.CreateScope())
{
var dbContext = GetDatabaseContext(scope);
var query = new PolicyReadByUserIdQuery(userId);
var results = await query.Run(dbContext).ToListAsync();
return Mapper.Map<List<AdminConsoleEntities.Policy>>(results);
}
}
public async Task<IEnumerable<OrganizationPolicyDetails>> GetPolicyDetailsByOrganizationIdAsync(Guid organizationId, PolicyType policyType)
{
using var scope = ServiceScopeFactory.CreateScope();
var dbContext = GetDatabaseContext(scope);
var givenOrgUsers =
from ou in dbContext.OrganizationUsers
where ou.OrganizationId == organizationId
from u in dbContext.Users
where
(u.Email == ou.Email && ou.Email != null)
|| (ou.UserId == u.Id && ou.UserId != null)
select new
{
ou.Id,
ou.OrganizationId,
UserId = u.Id,
u.Email
};
var orgUsersLinkedByUserId =
from ou in dbContext.OrganizationUsers
join gou in givenOrgUsers
on ou.UserId equals gou.UserId
select new
{
ou.Id,
ou.OrganizationId,
gou.UserId,
ou.Type,
ou.Status,
ou.Permissions
};
var orgUsersLinkedByEmail =
from ou in dbContext.OrganizationUsers
join gou in givenOrgUsers
on ou.Email equals gou.Email
select new
{
ou.Id,
ou.OrganizationId,
gou.UserId,
ou.Type,
ou.Status,
ou.Permissions
};
var allAffectedOrgUsers = orgUsersLinkedByEmail.Union(orgUsersLinkedByUserId);
var providerOrganizations = from pu in dbContext.ProviderUsers
join po in dbContext.ProviderOrganizations
on pu.ProviderId equals po.ProviderId
join ou in allAffectedOrgUsers
on pu.UserId equals ou.UserId
where pu.UserId == ou.UserId
select new
{
pu.UserId,
po.OrganizationId
};
var policyWithAffectedUsers =
from p in dbContext.Policies
join o in dbContext.Organizations
on p.OrganizationId equals o.Id
join ou in allAffectedOrgUsers
on o.Id equals ou.OrganizationId
where p.Enabled
&& o.Enabled
&& o.UsePolicies
&& p.Type == policyType
select new OrganizationPolicyDetails
{
UserId = ou.UserId,
OrganizationUserId = ou.Id,
OrganizationId = p.OrganizationId,
PolicyType = p.Type,
PolicyData = p.Data,
OrganizationUserType = ou.Type,
OrganizationUserStatus = ou.Status,
OrganizationUserPermissionsData = ou.Permissions,
IsProvider = providerOrganizations.Any(po => po.OrganizationId == p.OrganizationId)
};
return await policyWithAffectedUsers.ToListAsync();
}
public async Task<IEnumerable<OrganizationPolicyDetails>> GetPolicyDetailsByUserIdsAndPolicyType(
IEnumerable<Guid> userIds, PolicyType policyType)
{
ArgumentNullException.ThrowIfNull(userIds);
var userIdsList = userIds.Where(id => id != Guid.Empty).ToList();
if (userIdsList.Count == 0)
{
return [];
}
using var scope = ServiceScopeFactory.CreateScope();
await using var dbContext = GetDatabaseContext(scope);
// Get provider relationships
var providerLookup = await (from pu in dbContext.ProviderUsers
join po in dbContext.ProviderOrganizations on pu.ProviderId equals po.ProviderId
where pu.UserId != null && userIdsList.Contains(pu.UserId.Value)
select new { pu.UserId, po.OrganizationId })
.ToListAsync();
// Hashset for lookup
var providerSet = new HashSet<(Guid UserId, Guid OrganizationId)>(
providerLookup.Select(p => (p.UserId!.Value, p.OrganizationId)));
// Branch 1: Accepted users
var acceptedUsers = await (from p in dbContext.Policies
join ou in dbContext.OrganizationUsers on p.OrganizationId equals ou.OrganizationId
join o in dbContext.Organizations on p.OrganizationId equals o.Id
where p.Enabled
&& p.Type == policyType
&& o.Enabled
&& o.UsePolicies
&& ou.Status != OrganizationUserStatusType.Invited
&& ou.UserId != null
&& userIdsList.Contains(ou.UserId.Value)
select new
{
OrganizationUserId = ou.Id,
OrganizationId = p.OrganizationId,
PolicyType = p.Type,
PolicyData = p.Data,
OrganizationUserType = ou.Type,
OrganizationUserStatus = ou.Status,
OrganizationUserPermissionsData = ou.Permissions,
UserId = ou.UserId.Value
}).ToListAsync();
// Branch 2: Invited users
var invitedUsers = await (from p in dbContext.Policies
join ou in dbContext.OrganizationUsers on p.OrganizationId equals ou.OrganizationId
join o in dbContext.Organizations on p.OrganizationId equals o.Id
join u in dbContext.Users on ou.Email equals u.Email
where p.Enabled
&& o.Enabled
&& o.UsePolicies
&& ou.Status == OrganizationUserStatusType.Invited
&& userIdsList.Contains(u.Id)
&& p.Type == policyType
select new
{
OrganizationUserId = ou.Id,
OrganizationId = p.OrganizationId,
PolicyType = p.Type,
PolicyData = p.Data,
OrganizationUserType = ou.Type,
OrganizationUserStatus = ou.Status,
OrganizationUserPermissionsData = ou.Permissions,
UserId = u.Id
}).ToListAsync();
// Combine results with provder lookup
var allResults = acceptedUsers.Concat(invitedUsers)
.Select(item => new OrganizationPolicyDetails
{
OrganizationUserId = item.OrganizationUserId,
OrganizationId = item.OrganizationId,
PolicyType = item.PolicyType,
PolicyData = item.PolicyData,
OrganizationUserType = item.OrganizationUserType,
OrganizationUserStatus = item.OrganizationUserStatus,
OrganizationUserPermissionsData = item.OrganizationUserPermissionsData,
UserId = item.UserId,
IsProvider = providerSet.Contains((item.UserId, item.OrganizationId))
});
return allResults.ToList();
}
}