mirror of
https://github.com/bitwarden/server.git
synced 2025-12-10 00:42:07 -06: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:
parent
3dd5accb56
commit
2986a883eb
@ -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);
|
||||
}
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
@ -16,7 +16,7 @@ namespace Bit.Infrastructure.IntegrationTest.AdminConsole.Repositories.PolicyRep
|
||||
|
||||
public class GetPolicyDetailsByUserIdTests
|
||||
{
|
||||
[DatabaseTheory, DatabaseData]
|
||||
[Theory, DatabaseData]
|
||||
public async Task GetPolicyDetailsByUserId_NonInvitedUsers_Works(
|
||||
IUserRepository userRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
@ -105,7 +105,7 @@ public class GetPolicyDetailsByUserIdTests
|
||||
Assert.Equivalent(new Permissions { ManagePolicies = true }, actualPolicyDetails2.GetOrganizationUserCustomPermissions(), strict: true);
|
||||
}
|
||||
|
||||
[DatabaseTheory, DatabaseData]
|
||||
[Theory, DatabaseData]
|
||||
public async Task GetPolicyDetailsByUserId_InvitedUser_Works(
|
||||
IUserRepository userRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
@ -148,7 +148,7 @@ public class GetPolicyDetailsByUserIdTests
|
||||
Assert.Equivalent(expectedPolicyDetails, actualPolicyDetails.Single());
|
||||
}
|
||||
|
||||
[DatabaseTheory, DatabaseData]
|
||||
[Theory, DatabaseData]
|
||||
public async Task GetPolicyDetailsByUserId_RevokedConfirmedUser_Works(
|
||||
IUserRepository userRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
@ -192,7 +192,7 @@ public class GetPolicyDetailsByUserIdTests
|
||||
Assert.Equivalent(expectedPolicyDetails, actualPolicyDetails.Single());
|
||||
}
|
||||
|
||||
[DatabaseTheory, DatabaseData]
|
||||
[Theory, DatabaseData]
|
||||
public async Task GetPolicyDetailsByUserId_RevokedInvitedUser_DoesntReturnPolicies(
|
||||
IUserRepository userRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
@ -227,7 +227,7 @@ public class GetPolicyDetailsByUserIdTests
|
||||
Assert.Empty(actualPolicyDetails);
|
||||
}
|
||||
|
||||
[DatabaseTheory, DatabaseData]
|
||||
[Theory, DatabaseData]
|
||||
public async Task GetPolicyDetailsByUserId_SetsIsProvider(
|
||||
IUserRepository userRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
@ -283,7 +283,7 @@ public class GetPolicyDetailsByUserIdTests
|
||||
Assert.Equivalent(expectedPolicyDetails, actualPolicyDetails.Single());
|
||||
}
|
||||
|
||||
[DatabaseTheory, DatabaseData]
|
||||
[Theory, DatabaseData]
|
||||
public async Task GetPolicyDetailsByUserId_IgnoresDisabledOrganizations(
|
||||
IUserRepository userRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
@ -312,7 +312,7 @@ public class GetPolicyDetailsByUserIdTests
|
||||
Assert.Empty(actualPolicyDetails);
|
||||
}
|
||||
|
||||
[DatabaseTheory, DatabaseData]
|
||||
[Theory, DatabaseData]
|
||||
public async Task GetPolicyDetailsByUserId_IgnoresDowngradedOrganizations(
|
||||
IUserRepository userRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
@ -342,7 +342,7 @@ public class GetPolicyDetailsByUserIdTests
|
||||
Assert.Empty(actualPolicyDetails);
|
||||
}
|
||||
|
||||
[DatabaseTheory, DatabaseData]
|
||||
[Theory, DatabaseData]
|
||||
public async Task GetPolicyDetailsByUserId_IgnoresDisabledPolicies(
|
||||
IUserRepository userRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
|
||||
@ -0,0 +1,457 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Repositories;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Infrastructure.IntegrationTest.AdminConsole.Repositories.PolicyRepository;
|
||||
|
||||
public class GetPolicyDetailsByUserIdsAndPolicyTypeTests
|
||||
{
|
||||
[Theory]
|
||||
[DatabaseData]
|
||||
public async Task GetPolicyDetailsByUserIdsAndPolicyType_GivenTwoUsersForAnEnterpriseOrgWithTwoFactorEnabled_WhenUsersHaveBeenConfirmedOrAccepted_ThenShouldReturnCorrectPolicyDetailsAsync(
|
||||
IUserRepository userRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IPolicyRepository policyRepository)
|
||||
{
|
||||
// Arrange
|
||||
var user1 = await userRepository.CreateAsync(GetDefaultUser());
|
||||
|
||||
var user2 = await userRepository.CreateAsync(GetDefaultUser());
|
||||
|
||||
var organization = await CreateEnterpriseOrgAsync(organizationRepository);
|
||||
|
||||
var policy = await policyRepository.CreateAsync(new Policy
|
||||
{
|
||||
OrganizationId = organization.Id,
|
||||
Type = PolicyType.TwoFactorAuthentication,
|
||||
Data = string.Empty,
|
||||
Enabled = true
|
||||
});
|
||||
|
||||
var orgUser1 = await organizationUserRepository.CreateAsync(GetAcceptedOrganizationUser(organization, user1));
|
||||
|
||||
var orgUser2 = await organizationUserRepository.CreateAsync(GetConfirmedOrganizationUser(organization, user2));
|
||||
|
||||
// Act
|
||||
var results = await policyRepository.GetPolicyDetailsByUserIdsAndPolicyType(
|
||||
[user1.Id, user2.Id],
|
||||
PolicyType.TwoFactorAuthentication);
|
||||
|
||||
// Assert
|
||||
var resultsList = results.ToList();
|
||||
Assert.Equal(2, resultsList.Count);
|
||||
|
||||
var result1 = resultsList.First(r => r.UserId == user1.Id);
|
||||
Assert.Equal(orgUser1.Id, result1.OrganizationUserId);
|
||||
Assert.Equal(organization.Id, result1.OrganizationId);
|
||||
Assert.Equal(PolicyType.TwoFactorAuthentication, result1.PolicyType);
|
||||
Assert.Equal(policy.Data, result1.PolicyData);
|
||||
Assert.Equal(OrganizationUserStatusType.Accepted, result1.OrganizationUserStatus);
|
||||
|
||||
var result2 = resultsList.First(r => r.UserId == user2.Id);
|
||||
Assert.Equal(orgUser2.Id, result2.OrganizationUserId);
|
||||
Assert.Equal(organization.Id, result2.OrganizationId);
|
||||
Assert.Equal(PolicyType.TwoFactorAuthentication, result2.PolicyType);
|
||||
Assert.Equal(policy.Data, result2.PolicyData);
|
||||
Assert.Equal(OrganizationUserStatusType.Confirmed, result2.OrganizationUserStatus);
|
||||
|
||||
await organizationRepository.DeleteAsync(organization);
|
||||
await userRepository.DeleteManyAsync([user1, user2]);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[DatabaseData]
|
||||
public async Task GetPolicyDetailsByUserIdsAndPolicyType_GivenTwoUsersForEnterpriseOrgWithMasterPasswordEnabled_WhenUsersHaveBeenInvited_ThenShouldReturnCorrectPolicyDetailsForInvitedUsersAsync(
|
||||
IUserRepository userRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IPolicyRepository policyRepository)
|
||||
{
|
||||
// Arrange
|
||||
var user1 = await userRepository.CreateAsync(GetDefaultUser());
|
||||
|
||||
var user2 = await userRepository.CreateAsync(GetDefaultUser());
|
||||
|
||||
var organization = await CreateEnterpriseOrgAsync(organizationRepository);
|
||||
|
||||
_ = await policyRepository.CreateAsync(new Policy
|
||||
{
|
||||
OrganizationId = organization.Id,
|
||||
Type = PolicyType.MasterPassword,
|
||||
Data = "{\"minComplexity\":4}",
|
||||
Enabled = true,
|
||||
});
|
||||
|
||||
var orgUser1 = await organizationUserRepository.CreateAsync(GetInvitedOrganizationUser(organization, user1));
|
||||
|
||||
var orgUser2 = await organizationUserRepository.CreateAsync(GetInvitedOrganizationUser(organization, user2));
|
||||
|
||||
// Act
|
||||
var results = await policyRepository.GetPolicyDetailsByUserIdsAndPolicyType(
|
||||
[user1.Id, user2.Id],
|
||||
PolicyType.MasterPassword);
|
||||
|
||||
// Assert
|
||||
var resultsList = results.ToList();
|
||||
Assert.Equal(2, resultsList.Count);
|
||||
|
||||
var result1 = resultsList.First(r => r.UserId == user1.Id);
|
||||
Assert.Equal(orgUser1.Id, result1.OrganizationUserId);
|
||||
Assert.Equal(organization.Id, result1.OrganizationId);
|
||||
Assert.Equal(PolicyType.MasterPassword, result1.PolicyType);
|
||||
Assert.Equal(OrganizationUserStatusType.Invited, result1.OrganizationUserStatus);
|
||||
|
||||
var result2 = resultsList.First(r => r.UserId == user2.Id);
|
||||
Assert.Equal(orgUser2.Id, result2.OrganizationUserId);
|
||||
Assert.Equal(organization.Id, result2.OrganizationId);
|
||||
Assert.Equal(PolicyType.MasterPassword, result2.PolicyType);
|
||||
Assert.Equal(OrganizationUserStatusType.Invited, result2.OrganizationUserStatus);
|
||||
|
||||
await organizationRepository.DeleteAsync(organization);
|
||||
await userRepository.DeleteManyAsync([user1, user2]);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[DatabaseData]
|
||||
public async Task GetPolicyDetailsByUserIdsAndPolicyType_GivenConfirmedUserEnterpriseOrgWithPolicyEnabled_WhenUserIsAProvider_ThenShouldContainProviderDataAsync(
|
||||
IUserRepository userRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IProviderRepository providerRepository,
|
||||
IProviderUserRepository providerUserRepository,
|
||||
IProviderOrganizationRepository providerOrganizationRepository,
|
||||
IPolicyRepository policyRepository)
|
||||
{
|
||||
// Arrange
|
||||
var user = await userRepository.CreateAsync(GetDefaultUser());
|
||||
|
||||
var organization = await CreateEnterpriseOrgAsync(organizationRepository);
|
||||
|
||||
_ = await policyRepository.CreateAsync(GetPolicy(PolicyType.SingleOrg, organization));
|
||||
|
||||
_ = await organizationUserRepository.CreateAsync(GetConfirmedOrganizationUser(organization, user));
|
||||
|
||||
var provider = await providerRepository.CreateAsync(new Provider
|
||||
{
|
||||
Name = "Test Provider",
|
||||
BusinessName = "Test Provider Business",
|
||||
BusinessAddress1 = "123 Test St",
|
||||
BusinessAddress2 = "Suite 456",
|
||||
BusinessAddress3 = "Floor 7",
|
||||
BusinessCountry = "US",
|
||||
BusinessTaxNumber = "123456789",
|
||||
BillingEmail = $"billing+{Guid.NewGuid()}@example.com"
|
||||
});
|
||||
|
||||
await providerUserRepository.CreateAsync(new ProviderUser
|
||||
{
|
||||
ProviderId = provider.Id,
|
||||
UserId = user.Id,
|
||||
Status = ProviderUserStatusType.Confirmed,
|
||||
Type = ProviderUserType.ProviderAdmin
|
||||
});
|
||||
|
||||
await providerOrganizationRepository.CreateAsync(new ProviderOrganization
|
||||
{
|
||||
ProviderId = provider.Id,
|
||||
OrganizationId = organization.Id
|
||||
});
|
||||
|
||||
// Act
|
||||
var results = await policyRepository.GetPolicyDetailsByUserIdsAndPolicyType(
|
||||
[user.Id],
|
||||
PolicyType.SingleOrg);
|
||||
|
||||
// Assert
|
||||
var resultsList = results.ToList();
|
||||
Assert.Single(resultsList);
|
||||
|
||||
var result = resultsList.First();
|
||||
Assert.True(result.IsProvider);
|
||||
Assert.Equal(user.Id, result.UserId);
|
||||
Assert.Equal(organization.Id, result.OrganizationId);
|
||||
|
||||
await organizationRepository.DeleteAsync(organization);
|
||||
await userRepository.DeleteManyAsync([user]);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[DatabaseData]
|
||||
public async Task GetPolicyDetailsByUserIdsAndPolicyType_GivenEnterpriseOrgWithTwoEnabledPolicies_WhenRequestingTwoFactor_ShouldOnlyReturnInputPolicyType(
|
||||
IUserRepository userRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IPolicyRepository policyRepository)
|
||||
{
|
||||
// Arrange
|
||||
var user = await userRepository.CreateAsync(GetDefaultUser());
|
||||
|
||||
var organization = await CreateEnterpriseOrgAsync(organizationRepository);
|
||||
|
||||
// Create multiple policies
|
||||
_ = await policyRepository.CreateAsync(GetPolicy(PolicyType.TwoFactorAuthentication, organization));
|
||||
|
||||
_ = await policyRepository.CreateAsync(GetPolicy(PolicyType.MasterPassword, organization));
|
||||
|
||||
_ = await organizationUserRepository.CreateAsync(GetConfirmedOrganizationUser(organization, user));
|
||||
|
||||
// Act - Request only TwoFactorAuthentication policy
|
||||
var results = await policyRepository.GetPolicyDetailsByUserIdsAndPolicyType(
|
||||
[user.Id],
|
||||
PolicyType.TwoFactorAuthentication);
|
||||
|
||||
// Assert
|
||||
var resultsList = results.ToList();
|
||||
Assert.Single(resultsList);
|
||||
Assert.All(resultsList, r => Assert.Equal(PolicyType.TwoFactorAuthentication, r.PolicyType));
|
||||
|
||||
await organizationRepository.DeleteAsync(organization);
|
||||
await userRepository.DeleteManyAsync([user]);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[DatabaseData]
|
||||
public async Task GetPolicyDetailsByUserIdsAndPolicyType_GivenEnterpriseOrg_WhenSendPolicyIsDisabled_ShouldNotReturnDisabledPoliciesAsync(
|
||||
IUserRepository userRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IPolicyRepository policyRepository)
|
||||
{
|
||||
// Arrange
|
||||
var user = await userRepository.CreateAsync(GetDefaultUser());
|
||||
|
||||
var organization = await CreateEnterpriseOrgAsync(organizationRepository);
|
||||
_ = await policyRepository.CreateAsync(new Policy
|
||||
{
|
||||
OrganizationId = organization.Id,
|
||||
Type = PolicyType.DisableSend,
|
||||
Data = "{}",
|
||||
Enabled = false // Disabled policy
|
||||
});
|
||||
|
||||
_ = await organizationUserRepository.CreateAsync(GetConfirmedOrganizationUser(organization, user));
|
||||
|
||||
// Act
|
||||
var results = await policyRepository.GetPolicyDetailsByUserIdsAndPolicyType(
|
||||
[user.Id],
|
||||
PolicyType.DisableSend);
|
||||
|
||||
// Assert
|
||||
Assert.Empty(results);
|
||||
await organizationRepository.DeleteAsync(organization);
|
||||
await userRepository.DeleteManyAsync([user]);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[DatabaseData]
|
||||
public async Task GetPolicyDetailsByUserIdsAndPolicyType_GivenEnterpriseOrgWithPolicies_WhenOrgIsDisabled_ThenShouldNotReturnResults(
|
||||
IUserRepository userRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IPolicyRepository policyRepository)
|
||||
{
|
||||
// Arrange
|
||||
var user = await userRepository.CreateAsync(GetDefaultUser());
|
||||
|
||||
var organization = await organizationRepository.CreateAsync(new Organization
|
||||
{
|
||||
Name = "Test Organization",
|
||||
BillingEmail = $"billing+{Guid.NewGuid()}@example.com",
|
||||
Plan = "EnterpriseAnnually",
|
||||
PlanType = PlanType.EnterpriseAnnually,
|
||||
Seats = 10,
|
||||
MaxCollections = 10,
|
||||
UsePolicies = true,
|
||||
UseDirectory = true,
|
||||
UseTotp = true,
|
||||
Use2fa = true,
|
||||
UseApi = true,
|
||||
SelfHost = true,
|
||||
Enabled = false, // Disabled organization
|
||||
});
|
||||
|
||||
_ = await policyRepository.CreateAsync(GetPolicy(PolicyType.RequireSso, organization));
|
||||
|
||||
_ = await organizationUserRepository.CreateAsync(GetConfirmedOrganizationUser(organization, user));
|
||||
|
||||
// Act
|
||||
var results = await policyRepository.GetPolicyDetailsByUserIdsAndPolicyType(
|
||||
[user.Id],
|
||||
PolicyType.RequireSso);
|
||||
|
||||
// Assert
|
||||
Assert.Empty(results);
|
||||
|
||||
await organizationRepository.DeleteAsync(organization);
|
||||
await userRepository.DeleteManyAsync([user]);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[DatabaseData]
|
||||
public async Task GetPolicyDetailsByUserIdsAndPolicyType_GivenOrganization_WhenNotUsingPolicies_ThenShouldNotReturnResults(
|
||||
IUserRepository userRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IPolicyRepository policyRepository)
|
||||
{
|
||||
// Arrange
|
||||
var user = await userRepository.CreateAsync(GetDefaultUser());
|
||||
|
||||
var organization = await organizationRepository.CreateAsync(new Organization
|
||||
{
|
||||
Name = "Test Organization",
|
||||
BillingEmail = $"billing+{Guid.NewGuid()}@example.com",
|
||||
Plan = "EnterpriseAnnually",
|
||||
PlanType = PlanType.EnterpriseAnnually,
|
||||
Seats = 10,
|
||||
MaxCollections = 10,
|
||||
UsePolicies = false, // Not using policies
|
||||
UseDirectory = true,
|
||||
UseTotp = true,
|
||||
Use2fa = true,
|
||||
UseApi = true,
|
||||
SelfHost = true,
|
||||
Enabled = true,
|
||||
});
|
||||
|
||||
var policy = await policyRepository.CreateAsync(GetPolicy(PolicyType.PasswordGenerator, organization));
|
||||
|
||||
_ = await organizationUserRepository.CreateAsync(GetConfirmedOrganizationUser(organization, user));
|
||||
|
||||
// Act
|
||||
var results = await policyRepository.GetPolicyDetailsByUserIdsAndPolicyType(
|
||||
[user.Id],
|
||||
PolicyType.PasswordGenerator);
|
||||
|
||||
// Assert
|
||||
Assert.Empty(results);
|
||||
|
||||
await organizationRepository.DeleteAsync(organization);
|
||||
await userRepository.DeleteManyAsync([user]);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[DatabaseData]
|
||||
public async Task GetPolicyDetailsByUserIdsAndPolicyType_GivenOrganization_WhenRequestingWithNoUsers_ShouldReturnEmptyList(
|
||||
IOrganizationRepository organizationRepository,
|
||||
IPolicyRepository policyRepository)
|
||||
{
|
||||
// Arrange
|
||||
var organization = await CreateEnterpriseOrgAsync(organizationRepository);
|
||||
|
||||
_ = await policyRepository.CreateAsync(GetPolicy(PolicyType.TwoFactorAuthentication, organization));
|
||||
|
||||
// Act
|
||||
var results = await policyRepository.GetPolicyDetailsByUserIdsAndPolicyType(
|
||||
new List<Guid>(),
|
||||
PolicyType.TwoFactorAuthentication);
|
||||
|
||||
// Assert
|
||||
Assert.Empty(results);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[DatabaseData]
|
||||
public async Task GetPolicyDetailsByUserIdsAndPolicyType_GivenTwoOrganizations_WhenUserIsAMemberOfBoth_ShouldReturnResultsForBothOrganizations(
|
||||
IUserRepository userRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IPolicyRepository policyRepository)
|
||||
{
|
||||
// Arrange
|
||||
var user = await userRepository.CreateAsync(GetDefaultUser());
|
||||
|
||||
var organization1 = await CreateEnterpriseOrgAsync(organizationRepository);
|
||||
var organization2 = await CreateEnterpriseOrgAsync(organizationRepository);
|
||||
|
||||
_ = await policyRepository.CreateAsync(GetPolicy(PolicyType.TwoFactorAuthentication, organization1));
|
||||
|
||||
_ = await policyRepository.CreateAsync(GetPolicy(PolicyType.TwoFactorAuthentication, organization2));
|
||||
|
||||
_ = await organizationUserRepository.CreateAsync(GetConfirmedOrganizationUser(organization1, user));
|
||||
|
||||
_ = await organizationUserRepository.CreateAsync(GetConfirmedOrganizationUser(organization2, user));
|
||||
|
||||
// Act
|
||||
var results = await policyRepository.GetPolicyDetailsByUserIdsAndPolicyType(
|
||||
[user.Id],
|
||||
PolicyType.TwoFactorAuthentication);
|
||||
|
||||
// Assert
|
||||
var resultsList = results.ToList();
|
||||
Assert.Equal(2, resultsList.Count);
|
||||
|
||||
var organizationIds = resultsList.Select(r => r.OrganizationId).ToList();
|
||||
Assert.Contains(organization1.Id, organizationIds);
|
||||
Assert.Contains(organization2.Id, organizationIds);
|
||||
}
|
||||
|
||||
private static async Task<Organization> CreateEnterpriseOrgAsync(IOrganizationRepository orgRepo)
|
||||
{
|
||||
return await orgRepo.CreateAsync(new Organization
|
||||
{
|
||||
Name = "Test Organization",
|
||||
BillingEmail = $"billing+{Guid.NewGuid()}@example.com",
|
||||
Plan = "EnterpriseAnnually",
|
||||
PlanType = PlanType.EnterpriseAnnually,
|
||||
Seats = 10,
|
||||
MaxCollections = 10,
|
||||
UsePolicies = true,
|
||||
UseDirectory = true,
|
||||
UseTotp = true,
|
||||
Use2fa = true,
|
||||
UseApi = true,
|
||||
SelfHost = true,
|
||||
Enabled = true,
|
||||
});
|
||||
}
|
||||
|
||||
private static User GetDefaultUser() => new()
|
||||
{
|
||||
Name = $"Test User {Guid.NewGuid()}",
|
||||
Email = $"test+{Guid.NewGuid()}@example.com",
|
||||
ApiKey = $"test.api.key.{Guid.NewGuid()}"[..30],
|
||||
SecurityStamp = Guid.NewGuid().ToString()
|
||||
};
|
||||
|
||||
private static OrganizationUser GetAcceptedOrganizationUser(Organization organization, User user) => new()
|
||||
{
|
||||
OrganizationId = organization.Id,
|
||||
UserId = user.Id,
|
||||
Status = OrganizationUserStatusType.Accepted,
|
||||
Type = OrganizationUserType.User
|
||||
};
|
||||
|
||||
private static OrganizationUser GetConfirmedOrganizationUser(Organization organization, User user) => new()
|
||||
{
|
||||
OrganizationId = organization.Id,
|
||||
UserId = user.Id,
|
||||
Status = OrganizationUserStatusType.Confirmed,
|
||||
Type = OrganizationUserType.User
|
||||
};
|
||||
|
||||
private static OrganizationUser GetInvitedOrganizationUser(Organization organization, User user) => new()
|
||||
{
|
||||
OrganizationId = organization.Id,
|
||||
UserId = null, // Invited users don't have UserId
|
||||
Email = user.Email,
|
||||
Status = OrganizationUserStatusType.Invited,
|
||||
Type = OrganizationUserType.User,
|
||||
};
|
||||
|
||||
private static Policy GetPolicy(PolicyType policyType, Organization organization) => new()
|
||||
{
|
||||
OrganizationId = organization.Id,
|
||||
Type = policyType,
|
||||
Data = "{\"test\": \"value\"}",
|
||||
Enabled = true
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,73 @@
|
||||
CREATE OR ALTER 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
|
||||
Loading…
x
Reference in New Issue
Block a user