1
0
mirror of https://github.com/bitwarden/server synced 2025-12-26 05:03:18 +00:00

[PM-25126] Add Bulk Policy Details (#6256)

* Added new bulk get for policy details

* Query improvements to avoid unnecessary look-ups.
This commit is contained in:
Jared McCannon
2025-09-09 13:43:14 -05:00
committed by GitHub
parent 3dd5accb56
commit 2986a883eb
7 changed files with 737 additions and 8 deletions

View File

@@ -44,4 +44,15 @@ public interface IPolicyRepository : IRepository<Policy, Guid>
/// You probably do not want to call it directly.
/// </remarks>
Task<IEnumerable<OrganizationPolicyDetails>> GetPolicyDetailsByOrganizationIdAsync(Guid organizationId, PolicyType policyType);
/// <summary>
/// Retrieves policy details for a list of users filtered by the specified policy type.
/// </summary>
/// <param name="userIds">A collection of user identifiers for which the policy details are to be fetched.</param>
/// <param name="policyType">The type of policy for which the details are required.</param>
/// <returns>
/// An asynchronous task that returns a collection of <see cref="OrganizationPolicyDetails"/> objects containing the policy information
/// associated with the specified users and policy type.
/// </returns>
Task<IEnumerable<OrganizationPolicyDetails>> GetPolicyDetailsByUserIdsAndPolicyType(IEnumerable<Guid> userIds, PolicyType policyType);
}

View File

@@ -74,6 +74,21 @@ public class PolicyRepository : Repository<Policy, Guid>, IPolicyRepository
}
}
public async Task<IEnumerable<OrganizationPolicyDetails>> GetPolicyDetailsByUserIdsAndPolicyType(IEnumerable<Guid> userIds, PolicyType type)
{
await using var connection = new SqlConnection(ConnectionString);
var results = await connection.QueryAsync<OrganizationPolicyDetails>(
$"[{Schema}].[PolicyDetails_ReadByUserIdsPolicyType]",
new
{
UserIds = userIds.ToGuidIdArrayTVP(),
PolicyType = (byte)type
},
commandType: CommandType.StoredProcedure);
return results.ToList();
}
public async Task<IEnumerable<OrganizationPolicyDetails>> GetPolicyDetailsByOrganizationIdAsync(Guid organizationId, PolicyType policyType)
{
using (var connection = new SqlConnection(ConnectionString))

View File

@@ -183,4 +183,94 @@ public class PolicyRepository : Repository<AdminConsoleEntities.Policy, Policy,
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();
}
}

View File

@@ -0,0 +1,83 @@
CREATE PROCEDURE [dbo].[PolicyDetails_ReadByUserIdsPolicyType]
@UserIds AS [dbo].[GuidIdArray] READONLY,
@PolicyType AS TINYINT
AS
BEGIN
SET NOCOUNT ON;
WITH AcceptedUsers AS (
-- Branch 1: Accepted users linked by UserId
SELECT
OU.[Id] AS OrganizationUserId,
P.[OrganizationId],
P.[Type] AS PolicyType,
P.[Data] AS PolicyData,
OU.[Type] AS OrganizationUserType,
OU.[Status] AS OrganizationUserStatus,
OU.[Permissions] AS OrganizationUserPermissionsData,
OU.[UserId] AS UserId
FROM [dbo].[PolicyView] P
INNER JOIN [dbo].[OrganizationUserView] OU ON P.[OrganizationId] = OU.[OrganizationId]
INNER JOIN [dbo].[OrganizationView] O ON P.[OrganizationId] = O.[Id]
INNER JOIN @UserIds UI ON OU.[UserId] = UI.Id -- Direct join with TVP
WHERE
P.Enabled = 1
AND O.Enabled = 1
AND O.UsePolicies = 1
AND OU.[Status] != 0 -- Accepted users
AND P.[Type] = @PolicyType
),
InvitedUsers AS (
-- Branch 2: Invited users matched by email
SELECT
OU.[Id] AS OrganizationUserId,
P.[OrganizationId],
P.[Type] AS PolicyType,
P.[Data] AS PolicyData,
OU.[Type] AS OrganizationUserType,
OU.[Status] AS OrganizationUserStatus,
OU.[Permissions] AS OrganizationUserPermissionsData,
U.[Id] AS UserId
FROM [dbo].[PolicyView] P
INNER JOIN [dbo].[OrganizationUserView] OU ON P.[OrganizationId] = OU.[OrganizationId]
INNER JOIN [dbo].[OrganizationView] O ON P.[OrganizationId] = O.[Id]
INNER JOIN [dbo].[UserView] U ON U.[Email] = OU.[Email] -- Join on email
INNER JOIN @UserIds UI ON U.[Id] = UI.Id -- Join with TVP
WHERE
P.Enabled = 1
AND O.Enabled = 1
AND O.UsePolicies = 1
AND OU.[Status] = 0 -- Invited users only
AND P.[Type] = @PolicyType
),
AllUsers AS (
-- Combine both user sets
SELECT * FROM AcceptedUsers
UNION
SELECT * FROM InvitedUsers
),
ProviderLookup AS (
-- Pre-calculate provider relationships for all relevant user/org combinations
SELECT DISTINCT
PU.[UserId],
PO.[OrganizationId]
FROM [dbo].[ProviderUserView] PU
INNER JOIN [dbo].[ProviderOrganizationView] PO ON PO.[ProviderId] = PU.[ProviderId]
INNER JOIN AllUsers AU ON PU.[UserId] = AU.UserId AND PO.[OrganizationId] = AU.OrganizationId
)
-- Final result with efficient IsProvider lookup
SELECT
AU.OrganizationUserId,
AU.OrganizationId,
AU.PolicyType,
AU.PolicyData,
AU.OrganizationUserType,
AU.OrganizationUserStatus,
AU.OrganizationUserPermissionsData,
AU.UserId,
IIF(PL.UserId IS NOT NULL, 1, 0) AS IsProvider
FROM AllUsers AU
LEFT JOIN ProviderLookup PL
ON AU.UserId = PL.UserId
AND AU.OrganizationId = PL.OrganizationId
END