From 3866bc5155136c7212667e766d6019ee07b6c2f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rui=20Tom=C3=A9?= <108268980+r-tome@users.noreply.github.com> Date: Wed, 22 Oct 2025 13:20:53 +0100 Subject: [PATCH] =?UTF-8?q?[PM-23134]=C2=A0Update=20PolicyDetails=20sprocs?= =?UTF-8?q?=20for=20performance=20(#6421)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 --- .../Implementations/PolicyRequirementQuery.cs | 10 +- .../Repositories/IPolicyRepository.cs | 11 - .../Repositories/PolicyRepository.cs | 13 - .../Repositories/PolicyRepository.cs | 39 -- ...tionUser_ReadByUserIdWithPolicyDetails.sql | 91 +++- src/Sql/dbo/Views/UserProviderAccessView.sql | 9 + .../Policies/PolicyRequirementQueryTests.cs | 20 +- .../GetByUserIdWithPolicyDetailsTests.cs | 447 ++++++++++++++++++ .../GetPolicyDetailsByUserIdTests.cs | 385 --------------- ...-10-15_00_RefactorPolicyDetailsQueries.sql | 85 ++++ 10 files changed, 623 insertions(+), 487 deletions(-) create mode 100644 src/Sql/dbo/Views/UserProviderAccessView.sql create mode 100644 test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepository/GetByUserIdWithPolicyDetailsTests.cs delete mode 100644 test/Infrastructure.IntegrationTest/AdminConsole/Repositories/PolicyRepository/GetPolicyDetailsByUserIdTests.cs create mode 100644 util/Migrator/DbScripts/2025-10-15_00_RefactorPolicyDetailsQueries.sql diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyRequirementQuery.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyRequirementQuery.cs index e846e02e46..c1450c6ab5 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyRequirementQuery.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyRequirementQuery.cs @@ -1,6 +1,4 @@ -#nullable enable - -using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; using Bit.Core.AdminConsole.Repositories; @@ -20,7 +18,7 @@ public class PolicyRequirementQuery( throw new NotImplementedException("No Requirement Factory found for " + typeof(T)); } - var policyDetails = await GetPolicyDetails(userId); + var policyDetails = await GetPolicyDetails(userId, factory.PolicyType); var filteredPolicies = policyDetails .Where(p => p.PolicyType == factory.PolicyType) .Where(factory.Enforce); @@ -48,8 +46,8 @@ public class PolicyRequirementQuery( return eligibleOrganizationUserIds; } - private Task> GetPolicyDetails(Guid userId) - => policyRepository.GetPolicyDetailsByUserId(userId); + private async Task> GetPolicyDetails(Guid userId, PolicyType policyType) + => await policyRepository.GetPolicyDetailsByUserIdsAndPolicyType([userId], policyType); private async Task> GetOrganizationPolicyDetails(Guid organizationId, PolicyType policyType) => await policyRepository.GetPolicyDetailsByOrganizationIdAsync(organizationId, policyType); diff --git a/src/Core/AdminConsole/Repositories/IPolicyRepository.cs b/src/Core/AdminConsole/Repositories/IPolicyRepository.cs index 9f5c7f3fc4..d479809b89 100644 --- a/src/Core/AdminConsole/Repositories/IPolicyRepository.cs +++ b/src/Core/AdminConsole/Repositories/IPolicyRepository.cs @@ -20,17 +20,6 @@ public interface IPolicyRepository : IRepository Task GetByOrganizationIdTypeAsync(Guid organizationId, PolicyType type); Task> GetManyByOrganizationIdAsync(Guid organizationId); Task> GetManyByUserIdAsync(Guid userId); - /// - /// Gets all PolicyDetails for a user for all policy types. - /// - /// - /// Each PolicyDetail represents an OrganizationUser and a Policy which *may* be enforced - /// against them. It only returns PolicyDetails for policies that are enabled and where the organization's plan - /// supports policies. It also excludes "revoked invited" users who are not subject to policy enforcement. - /// This is consumed by to create requirements for specific policy types. - /// You probably do not want to call it directly. - /// - Task> GetPolicyDetailsByUserId(Guid userId); /// /// Retrieves of the specified diff --git a/src/Infrastructure.Dapper/AdminConsole/Repositories/PolicyRepository.cs b/src/Infrastructure.Dapper/AdminConsole/Repositories/PolicyRepository.cs index 83d5ef6a70..865c4f8e5c 100644 --- a/src/Infrastructure.Dapper/AdminConsole/Repositories/PolicyRepository.cs +++ b/src/Infrastructure.Dapper/AdminConsole/Repositories/PolicyRepository.cs @@ -61,19 +61,6 @@ public class PolicyRepository : Repository, IPolicyRepository } } - public async Task> GetPolicyDetailsByUserId(Guid userId) - { - using (var connection = new SqlConnection(ConnectionString)) - { - var results = await connection.QueryAsync( - $"[{Schema}].[PolicyDetails_ReadByUserId]", - new { UserId = userId }, - commandType: CommandType.StoredProcedure); - - return results.ToList(); - } - } - public async Task> GetPolicyDetailsByUserIdsAndPolicyType(IEnumerable userIds, PolicyType type) { await using var connection = new SqlConnection(ConnectionString); diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/PolicyRepository.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/PolicyRepository.cs index 72c277f1d7..1cca7a9bbb 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/PolicyRepository.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/PolicyRepository.cs @@ -56,45 +56,6 @@ public class PolicyRepository : Repository> GetPolicyDetailsByUserId(Guid userId) - { - using var scope = ServiceScopeFactory.CreateScope(); - var dbContext = GetDatabaseContext(scope); - - var providerOrganizations = from pu in dbContext.ProviderUsers - where pu.UserId == userId - join po in dbContext.ProviderOrganizations - on pu.ProviderId equals po.ProviderId - select po; - - var query = 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 && - o.Enabled && - o.UsePolicies && - ( - (ou.Status != OrganizationUserStatusType.Invited && ou.UserId == userId) || - // Invited orgUsers do not have a UserId associated with them, so we have to match up their email - (ou.Status == OrganizationUserStatusType.Invited && ou.Email == dbContext.Users.Find(userId).Email) - ) - select new PolicyDetails - { - 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 query.ToListAsync(); - } - public async Task> GetPolicyDetailsByOrganizationIdAsync(Guid organizationId, PolicyType policyType) { using var scope = ServiceScopeFactory.CreateScope(); diff --git a/src/Sql/dbo/Stored Procedures/OrganizationUser_ReadByUserIdWithPolicyDetails.sql b/src/Sql/dbo/Stored Procedures/OrganizationUser_ReadByUserIdWithPolicyDetails.sql index c2bc690a27..105170cd27 100644 --- a/src/Sql/dbo/Stored Procedures/OrganizationUser_ReadByUserIdWithPolicyDetails.sql +++ b/src/Sql/dbo/Stored Procedures/OrganizationUser_ReadByUserIdWithPolicyDetails.sql @@ -4,31 +4,70 @@ AS BEGIN SET NOCOUNT ON -SELECT - OU.[Id] AS OrganizationUserId, - P.[OrganizationId], - P.[Type] AS PolicyType, - P.[Enabled] AS PolicyEnabled, - P.[Data] AS PolicyData, - OU.[Type] AS OrganizationUserType, - OU.[Status] AS OrganizationUserStatus, - OU.[Permissions] AS OrganizationUserPermissionsData, - CASE WHEN EXISTS ( - SELECT 1 - FROM [dbo].[ProviderUserView] PU - INNER JOIN [dbo].[ProviderOrganizationView] PO ON PO.[ProviderId] = PU.[ProviderId] - WHERE PU.[UserId] = OU.[UserId] AND PO.[OrganizationId] = P.[OrganizationId] - ) THEN 1 ELSE 0 END AS IsProvider -FROM [dbo].[PolicyView] P -INNER JOIN [dbo].[OrganizationUserView] OU - ON P.[OrganizationId] = OU.[OrganizationId] -WHERE P.[Type] = @PolicyType AND + + DECLARE @UserEmail NVARCHAR(256) + SELECT @UserEmail = Email + FROM + [dbo].[UserView] + WHERE + Id = @UserId + + ;WITH OrgUsers AS ( - (OU.[Status] != 0 AND OU.[UserId] = @UserId) -- OrgUsers who have accepted their invite and are linked to a UserId - OR EXISTS ( - SELECT 1 - FROM [dbo].[UserView] U - WHERE U.[Id] = @UserId AND OU.[Email] = U.[Email] AND OU.[Status] = 0 -- 'Invited' OrgUsers are not linked to a UserId yet, so we have to look up their email - ) + -- All users except invited (Status <> 0): direct UserId match + SELECT + OU.[Id], + OU.[OrganizationId], + OU.[Type], + OU.[Status], + OU.[Permissions] + FROM + [dbo].[OrganizationUserView] OU + WHERE + OU.[Status] <> 0 + AND OU.[UserId] = @UserId + + UNION ALL + + -- Invited users: email match + SELECT + OU.[Id], + OU.[OrganizationId], + OU.[Type], + OU.[Status], + OU.[Permissions] + FROM + [dbo].[OrganizationUserView] OU + WHERE + OU.[Status] = 0 + AND OU.[Email] = @UserEmail + AND @UserEmail IS NOT NULL + ), + Providers AS + ( + SELECT + OrganizationId + FROM + [dbo].[UserProviderAccessView] + WHERE + UserId = @UserId ) -END \ No newline at end of file + SELECT + OU.[Id] AS [OrganizationUserId], + P.[OrganizationId], + P.[Type] AS [PolicyType], + P.[Enabled] AS [PolicyEnabled], + P.[Data] AS [PolicyData], + OU.[Type] AS [OrganizationUserType], + OU.[Status] AS [OrganizationUserStatus], + OU.[Permissions] AS [OrganizationUserPermissionsData], + CASE WHEN PR.[OrganizationId] IS NULL THEN 0 ELSE 1 END AS [IsProvider] + FROM + [dbo].[PolicyView] P + INNER JOIN + OrgUsers OU ON P.[OrganizationId] = OU.[OrganizationId] + LEFT JOIN + Providers PR ON PR.[OrganizationId] = OU.[OrganizationId] + WHERE + P.[Type] = @PolicyType +END diff --git a/src/Sql/dbo/Views/UserProviderAccessView.sql b/src/Sql/dbo/Views/UserProviderAccessView.sql new file mode 100644 index 0000000000..dedc380311 --- /dev/null +++ b/src/Sql/dbo/Views/UserProviderAccessView.sql @@ -0,0 +1,9 @@ +CREATE VIEW [dbo].[UserProviderAccessView] +AS +SELECT DISTINCT + PU.[UserId], + PO.[OrganizationId] +FROM + [dbo].[ProviderUserView] PU +INNER JOIN + [dbo].[ProviderOrganizationView] PO ON PO.[ProviderId] = PU.[ProviderId] diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirementQueryTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirementQueryTests.cs index 8c25f70454..9115ae5ba1 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirementQueryTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirementQueryTests.cs @@ -14,10 +14,12 @@ public class PolicyRequirementQueryTests [Theory, BitAutoData] public async Task GetAsync_IgnoresOtherPolicyTypes(Guid userId) { - var thisPolicy = new PolicyDetails { PolicyType = PolicyType.SingleOrg }; - var otherPolicy = new PolicyDetails { PolicyType = PolicyType.RequireSso }; + var thisPolicy = new OrganizationPolicyDetails { PolicyType = PolicyType.SingleOrg, UserId = userId }; + var otherPolicy = new OrganizationPolicyDetails { PolicyType = PolicyType.RequireSso, UserId = userId }; var policyRepository = Substitute.For(); - policyRepository.GetPolicyDetailsByUserId(userId).Returns([otherPolicy, thisPolicy]); + policyRepository.GetPolicyDetailsByUserIdsAndPolicyType( + Arg.Is>(ids => ids.Contains(userId)), PolicyType.SingleOrg) + .Returns([otherPolicy, thisPolicy]); var factory = new TestPolicyRequirementFactory(_ => true); var sut = new PolicyRequirementQuery(policyRepository, [factory]); @@ -33,9 +35,11 @@ public class PolicyRequirementQueryTests { // Arrange policies var policyRepository = Substitute.For(); - var thisPolicy = new PolicyDetails { PolicyType = PolicyType.SingleOrg }; - var otherPolicy = new PolicyDetails { PolicyType = PolicyType.SingleOrg }; - policyRepository.GetPolicyDetailsByUserId(userId).Returns([thisPolicy, otherPolicy]); + var thisPolicy = new OrganizationPolicyDetails { PolicyType = PolicyType.SingleOrg, UserId = userId }; + var otherPolicy = new OrganizationPolicyDetails { PolicyType = PolicyType.SingleOrg, UserId = userId }; + policyRepository.GetPolicyDetailsByUserIdsAndPolicyType( + Arg.Is>(ids => ids.Contains(userId)), PolicyType.SingleOrg) + .Returns([thisPolicy, otherPolicy]); // Arrange a substitute Enforce function so that we can inspect the received calls var callback = Substitute.For>(); @@ -70,7 +74,9 @@ public class PolicyRequirementQueryTests public async Task GetAsync_HandlesNoPolicies(Guid userId) { var policyRepository = Substitute.For(); - policyRepository.GetPolicyDetailsByUserId(userId).Returns([]); + policyRepository.GetPolicyDetailsByUserIdsAndPolicyType( + Arg.Is>(ids => ids.Contains(userId)), PolicyType.SingleOrg) + .Returns([]); var factory = new TestPolicyRequirementFactory(x => x.IsProvider); var sut = new PolicyRequirementQuery(policyRepository, [factory]); diff --git a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepository/GetByUserIdWithPolicyDetailsTests.cs b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepository/GetByUserIdWithPolicyDetailsTests.cs new file mode 100644 index 0000000000..a9ec374a94 --- /dev/null +++ b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepository/GetByUserIdWithPolicyDetailsTests.cs @@ -0,0 +1,447 @@ +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.Entities; +using Bit.Core.Enums; +using Bit.Core.Models.Data; +using Bit.Core.Repositories; +using Bit.Core.Utilities; +using Xunit; + +namespace Bit.Infrastructure.IntegrationTest.AdminConsole.Repositories.OrganizationUserRepository; + +public class GetByUserIdWithPolicyDetailsTests +{ + [Theory, DatabaseData] + public async Task GetByUserIdWithPolicyDetailsAsync_WithConfirmedUser_ReturnsPolicy( + IUserRepository userRepository, + IOrganizationUserRepository organizationUserRepository, + IOrganizationRepository organizationRepository, + IPolicyRepository policyRepository) + { + // Arrange + var user = await userRepository.CreateTestUserAsync(); + var org = await organizationRepository.CreateAsync(new Organization + { + Name = "Test Org", + BillingEmail = "billing@example.com", + Plan = "Test", + }); + var orgUser = new OrganizationUser + { + OrganizationId = org.Id, + UserId = user.Id, + Status = OrganizationUserStatusType.Confirmed, + Type = OrganizationUserType.User, + Email = null + }; + await organizationUserRepository.CreateAsync(orgUser); + await policyRepository.CreateAsync(new Policy + { + OrganizationId = org.Id, + Enabled = true, + Type = PolicyType.SingleOrg, + Data = CoreHelpers.ClassToJsonData(new { Setting = "value" }) + }); + + // Act + var result = await organizationUserRepository.GetByUserIdWithPolicyDetailsAsync(user.Id, PolicyType.SingleOrg); + + // Assert + var policyDetails = result.Single(); + Assert.Equal(orgUser.Id, policyDetails.OrganizationUserId); + Assert.Equal(org.Id, policyDetails.OrganizationId); + Assert.Equal(PolicyType.SingleOrg, policyDetails.PolicyType); + Assert.True(policyDetails.PolicyEnabled); + Assert.Equal(OrganizationUserType.User, policyDetails.OrganizationUserType); + Assert.Equal(OrganizationUserStatusType.Confirmed, policyDetails.OrganizationUserStatus); + Assert.False(policyDetails.IsProvider); + } + + [Theory, DatabaseData] + public async Task GetByUserIdWithPolicyDetailsAsync_WithAcceptedUser_ReturnsPolicy( + IUserRepository userRepository, + IOrganizationUserRepository organizationUserRepository, + IOrganizationRepository organizationRepository, + IPolicyRepository policyRepository) + { + // Arrange + var user = await userRepository.CreateTestUserAsync(); + var org = await organizationRepository.CreateAsync(new Organization + { + Name = "Test Org", + BillingEmail = "billing@example.com", + Plan = "Test", + }); + var orgUser = new OrganizationUser + { + OrganizationId = org.Id, + UserId = user.Id, + Status = OrganizationUserStatusType.Accepted, + Type = OrganizationUserType.Admin, + Email = null + }; + await organizationUserRepository.CreateAsync(orgUser); + await policyRepository.CreateAsync(new Policy + { + OrganizationId = org.Id, + Enabled = false, // Note: disabled policy + Type = PolicyType.RequireSso, + }); + + // Act + var result = await organizationUserRepository.GetByUserIdWithPolicyDetailsAsync(user.Id, PolicyType.RequireSso); + + // Assert + var policyDetails = result.Single(); + Assert.Equal(orgUser.Id, policyDetails.OrganizationUserId); + Assert.False(policyDetails.PolicyEnabled); // Should return even if disabled + } + + [Theory, DatabaseData] + public async Task GetByUserIdWithPolicyDetailsAsync_WithInvitedUser_ReturnsPolicy( + IUserRepository userRepository, + IOrganizationUserRepository organizationUserRepository, + IOrganizationRepository organizationRepository, + IPolicyRepository policyRepository) + { + // Arrange + var user = await userRepository.CreateTestUserAsync(); + var org = await organizationRepository.CreateAsync(new Organization + { + Name = "Test Org", + BillingEmail = "billing@example.com", + Plan = "Test", + }); + var orgUser = new OrganizationUser + { + OrganizationId = org.Id, + UserId = null, // invited users have null userId + Status = OrganizationUserStatusType.Invited, + Type = OrganizationUserType.User, + Email = user.Email // invited users have matching Email + }; + await organizationUserRepository.CreateAsync(orgUser); + await policyRepository.CreateAsync(new Policy + { + OrganizationId = org.Id, + Enabled = true, + Type = PolicyType.TwoFactorAuthentication, + }); + + // Act + var result = await organizationUserRepository.GetByUserIdWithPolicyDetailsAsync(user.Id, PolicyType.TwoFactorAuthentication); + + // Assert + var policyDetails = result.Single(); + Assert.Equal(orgUser.Id, policyDetails.OrganizationUserId); + Assert.Equal(OrganizationUserStatusType.Invited, policyDetails.OrganizationUserStatus); + } + + [Theory, DatabaseData] + public async Task GetByUserIdWithPolicyDetailsAsync_WithRevokedUser_ReturnsPolicy( + IUserRepository userRepository, + IOrganizationUserRepository organizationUserRepository, + IOrganizationRepository organizationRepository, + IPolicyRepository policyRepository) + { + // Arrange + var user = await userRepository.CreateTestUserAsync(); + var org = await organizationRepository.CreateAsync(new Organization + { + Name = "Test Org", + BillingEmail = "billing@example.com", + Plan = "Test", + }); + var orgUser = new OrganizationUser + { + OrganizationId = org.Id, + UserId = user.Id, + Status = OrganizationUserStatusType.Revoked, + Type = OrganizationUserType.Owner, + Email = null + }; + await organizationUserRepository.CreateAsync(orgUser); + await policyRepository.CreateAsync(new Policy + { + OrganizationId = org.Id, + Enabled = true, + Type = PolicyType.SingleOrg, + }); + + // Act + var result = await organizationUserRepository.GetByUserIdWithPolicyDetailsAsync(user.Id, PolicyType.SingleOrg); + + // Assert + var policyDetails = result.Single(); + Assert.Equal(OrganizationUserStatusType.Revoked, policyDetails.OrganizationUserStatus); + } + + [Theory, DatabaseData] + public async Task GetByUserIdWithPolicyDetailsAsync_WithMultipleOrganizations_ReturnsAllMatchingPolicies( + IUserRepository userRepository, + IOrganizationUserRepository organizationUserRepository, + IOrganizationRepository organizationRepository, + IPolicyRepository policyRepository) + { + // Arrange + var user = await userRepository.CreateTestUserAsync(); + + // Org1 with SingleOrg policy + var org1 = await organizationRepository.CreateAsync(new Organization + { + Name = "Org 1", + BillingEmail = "billing@example.com", + Plan = "Test", + }); + var orgUser1 = new OrganizationUser + { + OrganizationId = org1.Id, + UserId = user.Id, + Status = OrganizationUserStatusType.Confirmed, + Type = OrganizationUserType.User, + }; + await organizationUserRepository.CreateAsync(orgUser1); + await policyRepository.CreateAsync(new Policy + { + OrganizationId = org1.Id, + Enabled = true, + Type = PolicyType.SingleOrg, + }); + + // Org2 with SingleOrg policy + var org2 = await organizationRepository.CreateAsync(new Organization + { + Name = "Org 2", + BillingEmail = "billing2@example.com", + Plan = "Test", + }); + var orgUser2 = new OrganizationUser + { + OrganizationId = org2.Id, + UserId = user.Id, + Status = OrganizationUserStatusType.Confirmed, + Type = OrganizationUserType.Admin, + }; + await organizationUserRepository.CreateAsync(orgUser2); + await policyRepository.CreateAsync(new Policy + { + OrganizationId = org2.Id, + Enabled = true, + Type = PolicyType.SingleOrg, + }); + + // Org3 with RequireSso policy (different type - should not be returned) + var org3 = await organizationRepository.CreateAsync(new Organization + { + Name = "Org 3", + BillingEmail = "billing3@example.com", + Plan = "Test", + }); + var orgUser3 = new OrganizationUser + { + OrganizationId = org3.Id, + UserId = user.Id, + Status = OrganizationUserStatusType.Confirmed, + Type = OrganizationUserType.Owner, + }; + await organizationUserRepository.CreateAsync(orgUser3); + await policyRepository.CreateAsync(new Policy + { + OrganizationId = org3.Id, + Enabled = true, + Type = PolicyType.RequireSso, + }); + + // Act + var result = (await organizationUserRepository.GetByUserIdWithPolicyDetailsAsync(user.Id, PolicyType.SingleOrg)).ToList(); + + // Assert - should only get 2 policies (org1 and org2) + Assert.Equal(2, result.Count); + Assert.Contains(result, p => p.OrganizationId == org1.Id && p.OrganizationUserType == OrganizationUserType.User); + Assert.Contains(result, p => p.OrganizationId == org2.Id && p.OrganizationUserType == OrganizationUserType.Admin); + Assert.DoesNotContain(result, p => p.OrganizationId == org3.Id); + } + + [Theory, DatabaseData] + public async Task GetByUserIdWithPolicyDetailsAsync_WithNonExistingPolicyType_ReturnsEmpty( + IUserRepository userRepository, + IOrganizationUserRepository organizationUserRepository, + IOrganizationRepository organizationRepository, + IPolicyRepository policyRepository) + { + // Arrange + var user = await userRepository.CreateTestUserAsync(); + var org = await organizationRepository.CreateAsync(new Organization + { + Name = "Test Org", + BillingEmail = "billing@example.com", + Plan = "Test", + }); + await organizationUserRepository.CreateTestOrganizationUserAsync(org, user); + await policyRepository.CreateAsync(new Policy + { + OrganizationId = org.Id, + Enabled = true, + Type = PolicyType.SingleOrg, + }); + + // Act + var result = await organizationUserRepository.GetByUserIdWithPolicyDetailsAsync(user.Id, PolicyType.RequireSso); + + // Assert + Assert.Empty(result); + } + + [Theory, DatabaseData] + public async Task GetByUserIdWithPolicyDetailsAsync_WithProviderUser_ReturnsIsProviderTrue( + IUserRepository userRepository, + IOrganizationUserRepository organizationUserRepository, + IOrganizationRepository organizationRepository, + IPolicyRepository policyRepository, + IProviderRepository providerRepository, + IProviderUserRepository providerUserRepository, + IProviderOrganizationRepository providerOrganizationRepository) + { + // Arrange + var user = await userRepository.CreateTestUserAsync(); + var org = await organizationRepository.CreateAsync(new Organization + { + Name = "Test Org", + BillingEmail = "billing@example.com", + Plan = "Test", + }); + var orgUser = await organizationUserRepository.CreateTestOrganizationUserAsync(org, user); + await policyRepository.CreateAsync(new Policy + { + OrganizationId = org.Id, + Enabled = true, + Type = PolicyType.SingleOrg, + }); + var provider = await providerRepository.CreateAsync(new Provider + { + Name = Guid.NewGuid().ToString(), + Enabled = true + }); + await providerUserRepository.CreateAsync(new ProviderUser + { + ProviderId = provider.Id, + UserId = user.Id, + Status = ProviderUserStatusType.Confirmed + }); + await providerOrganizationRepository.CreateAsync(new ProviderOrganization + { + OrganizationId = org.Id, + ProviderId = provider.Id + }); + + // Act + var result = await organizationUserRepository.GetByUserIdWithPolicyDetailsAsync(user.Id, PolicyType.SingleOrg); + + // Assert + var policyDetails = result.Single(); + Assert.True(policyDetails.IsProvider); + } + + [Theory, DatabaseData] + public async Task GetByUserIdWithPolicyDetailsAsync_WithCustomUserWithPermissions_ReturnsPermissions( + IUserRepository userRepository, + IOrganizationUserRepository organizationUserRepository, + IOrganizationRepository organizationRepository, + IPolicyRepository policyRepository) + { + // Arrange + var user = await userRepository.CreateTestUserAsync(); + var org = await organizationRepository.CreateAsync(new Organization + { + Name = "Test Org", + BillingEmail = "billing@example.com", + Plan = "Test", + }); + var orgUser = new OrganizationUser + { + OrganizationId = org.Id, + UserId = user.Id, + Status = OrganizationUserStatusType.Confirmed, + Type = OrganizationUserType.Custom, + Email = null + }; + orgUser.SetPermissions(new Permissions + { + ManagePolicies = true, + EditAnyCollection = true + }); + await organizationUserRepository.CreateAsync(orgUser); + await policyRepository.CreateAsync(new Policy + { + OrganizationId = org.Id, + Enabled = true, + Type = PolicyType.SingleOrg, + }); + + // Act + var result = await organizationUserRepository.GetByUserIdWithPolicyDetailsAsync(user.Id, PolicyType.SingleOrg); + + // Assert + var policyDetails = result.Single(); + Assert.NotNull(policyDetails.OrganizationUserPermissionsData); + var permissions = CoreHelpers.LoadClassFromJsonData(policyDetails.OrganizationUserPermissionsData); + Assert.True(permissions.ManagePolicies); + Assert.True(permissions.EditAnyCollection); + } + + [Theory, DatabaseData] + public async Task GetByUserIdWithPolicyDetailsAsync_WhenNoPolicyExists_ReturnsEmpty( + IUserRepository userRepository, + IOrganizationUserRepository organizationUserRepository, + IOrganizationRepository organizationRepository) + { + // Arrange + var user = await userRepository.CreateTestUserAsync(); + var org = await organizationRepository.CreateAsync(new Organization + { + Name = "Test Org", + BillingEmail = "billing@example.com", + Plan = "Test", + }); + await organizationUserRepository.CreateTestOrganizationUserAsync(org, user); + + // Act + var result = await organizationUserRepository.GetByUserIdWithPolicyDetailsAsync(user.Id, PolicyType.SingleOrg); + + // Assert + Assert.Empty(result); + } + + [Theory, DatabaseData] + public async Task GetByUserIdWithPolicyDetailsAsync_WhenUserNotInOrg_ReturnsEmpty( + IUserRepository userRepository, + IOrganizationUserRepository organizationUserRepository, + IOrganizationRepository organizationRepository, + IPolicyRepository policyRepository) + { + // Arrange + var user = await userRepository.CreateTestUserAsync(); + var org = await organizationRepository.CreateAsync(new Organization + { + Name = "Test Org", + BillingEmail = "billing@example.com", + Plan = "Test", + }); + await policyRepository.CreateAsync(new Policy + { + OrganizationId = org.Id, + Enabled = true, + Type = PolicyType.SingleOrg, + }); + + // Act + var result = await organizationUserRepository.GetByUserIdWithPolicyDetailsAsync(user.Id, PolicyType.SingleOrg); + + // Assert + Assert.Empty(result); + } +} + diff --git a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/PolicyRepository/GetPolicyDetailsByUserIdTests.cs b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/PolicyRepository/GetPolicyDetailsByUserIdTests.cs deleted file mode 100644 index 0a2ddd7387..0000000000 --- a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/PolicyRepository/GetPolicyDetailsByUserIdTests.cs +++ /dev/null @@ -1,385 +0,0 @@ -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.Models.Data.Organizations.Policies; -using Bit.Core.AdminConsole.Repositories; -using Bit.Core.Billing.Enums; -using Bit.Core.Entities; -using Bit.Core.Enums; -using Bit.Core.Models.Data; -using Bit.Core.Repositories; -using Bit.Core.Utilities; -using Xunit; - -namespace Bit.Infrastructure.IntegrationTest.AdminConsole.Repositories.PolicyRepository; - -public class GetPolicyDetailsByUserIdTests -{ - [Theory, DatabaseData] - public async Task GetPolicyDetailsByUserId_NonInvitedUsers_Works( - IUserRepository userRepository, - IOrganizationUserRepository organizationUserRepository, - IOrganizationRepository organizationRepository, - IPolicyRepository policyRepository) - { - // Arrange - // OrgUser1 - owner of org1 - confirmed - var user = await userRepository.CreateTestUserAsync(); - var org1 = await CreateEnterpriseOrg(organizationRepository); - var orgUser1 = new OrganizationUser - { - OrganizationId = org1.Id, - UserId = user.Id, - Status = OrganizationUserStatusType.Confirmed, - Type = OrganizationUserType.Owner, - Email = null // confirmed OrgUsers use the email on the User table - }; - await organizationUserRepository.CreateAsync(orgUser1); - await policyRepository.CreateAsync(new Policy - { - OrganizationId = org1.Id, - Enabled = true, - Type = PolicyType.SingleOrg, - Data = CoreHelpers.ClassToJsonData(new TestPolicyData { BoolSetting = true, IntSetting = 5 }) - }); - - // OrgUser2 - custom user of org2 - accepted - var org2 = await CreateEnterpriseOrg(organizationRepository); - var orgUser2 = new OrganizationUser - { - OrganizationId = org2.Id, - UserId = user.Id, - Status = OrganizationUserStatusType.Accepted, - Type = OrganizationUserType.Custom, - Email = null // accepted OrgUsers use the email on the User table - }; - orgUser2.SetPermissions(new Permissions - { - ManagePolicies = true - }); - await organizationUserRepository.CreateAsync(orgUser2); - await policyRepository.CreateAsync(new Policy - { - OrganizationId = org2.Id, - Enabled = true, - Type = PolicyType.SingleOrg, - Data = CoreHelpers.ClassToJsonData(new TestPolicyData { BoolSetting = false, IntSetting = 15 }) - }); - - // Act - var policyDetails = (await policyRepository.GetPolicyDetailsByUserId(user.Id)).ToList(); - - // Assert - Assert.Equal(2, policyDetails.Count); - - var actualPolicyDetails1 = policyDetails.Find(p => p.OrganizationUserId == orgUser1.Id); - var expectedPolicyDetails1 = new PolicyDetails - { - OrganizationUserId = orgUser1.Id, - OrganizationId = org1.Id, - PolicyType = PolicyType.SingleOrg, - PolicyData = CoreHelpers.ClassToJsonData(new TestPolicyData { BoolSetting = true, IntSetting = 5 }), - OrganizationUserType = OrganizationUserType.Owner, - OrganizationUserStatus = OrganizationUserStatusType.Confirmed, - OrganizationUserPermissionsData = null, - IsProvider = false - }; - Assert.Equivalent(expectedPolicyDetails1, actualPolicyDetails1); - Assert.Equivalent(expectedPolicyDetails1.GetDataModel(), new TestPolicyData { BoolSetting = true, IntSetting = 5 }); - - var actualPolicyDetails2 = policyDetails.Find(p => p.OrganizationUserId == orgUser2.Id); - var expectedPolicyDetails2 = new PolicyDetails - { - OrganizationUserId = orgUser2.Id, - OrganizationId = org2.Id, - PolicyType = PolicyType.SingleOrg, - PolicyData = CoreHelpers.ClassToJsonData(new TestPolicyData { BoolSetting = false, IntSetting = 15 }), - OrganizationUserType = OrganizationUserType.Custom, - OrganizationUserStatus = OrganizationUserStatusType.Accepted, - OrganizationUserPermissionsData = CoreHelpers.ClassToJsonData(new Permissions { ManagePolicies = true }), - IsProvider = false - }; - Assert.Equivalent(expectedPolicyDetails2, actualPolicyDetails2); - Assert.Equivalent(expectedPolicyDetails2.GetDataModel(), new TestPolicyData { BoolSetting = false, IntSetting = 15 }); - Assert.Equivalent(new Permissions { ManagePolicies = true }, actualPolicyDetails2.GetOrganizationUserCustomPermissions(), strict: true); - } - - [Theory, DatabaseData] - public async Task GetPolicyDetailsByUserId_InvitedUser_Works( - IUserRepository userRepository, - IOrganizationUserRepository organizationUserRepository, - IOrganizationRepository organizationRepository, - IPolicyRepository policyRepository) - { - // Arrange - var user = await userRepository.CreateTestUserAsync(); - var org = await CreateEnterpriseOrg(organizationRepository); - var orgUser = new OrganizationUser - { - OrganizationId = org.Id, - UserId = null, // invited users have null userId - Status = OrganizationUserStatusType.Invited, - Type = OrganizationUserType.Custom, - Email = user.Email // invited users have matching Email - }; - await organizationUserRepository.CreateAsync(orgUser); - await policyRepository.CreateAsync(new Policy - { - OrganizationId = org.Id, - Enabled = true, - Type = PolicyType.SingleOrg, - }); - - // Act - var actualPolicyDetails = await policyRepository.GetPolicyDetailsByUserId(user.Id); - - // Assert - var expectedPolicyDetails = new PolicyDetails - { - OrganizationUserId = orgUser.Id, - OrganizationId = org.Id, - PolicyType = PolicyType.SingleOrg, - OrganizationUserType = OrganizationUserType.Custom, - OrganizationUserStatus = OrganizationUserStatusType.Invited, - IsProvider = false - }; - - Assert.Equivalent(expectedPolicyDetails, actualPolicyDetails.Single()); - } - - [Theory, DatabaseData] - public async Task GetPolicyDetailsByUserId_RevokedConfirmedUser_Works( - IUserRepository userRepository, - IOrganizationUserRepository organizationUserRepository, - IOrganizationRepository organizationRepository, - IPolicyRepository policyRepository) - { - // Arrange - var user = await userRepository.CreateTestUserAsync(); - var org = await CreateEnterpriseOrg(organizationRepository); - // User has been confirmed to the org but then revoked - var orgUser = new OrganizationUser - { - OrganizationId = org.Id, - UserId = user.Id, - Status = OrganizationUserStatusType.Revoked, - Type = OrganizationUserType.Owner, - Email = null - }; - await organizationUserRepository.CreateAsync(orgUser); - await policyRepository.CreateAsync(new Policy - { - OrganizationId = org.Id, - Enabled = true, - Type = PolicyType.SingleOrg, - }); - - // Act - var actualPolicyDetails = await policyRepository.GetPolicyDetailsByUserId(user.Id); - - // Assert - var expectedPolicyDetails = new PolicyDetails - { - OrganizationUserId = orgUser.Id, - OrganizationId = org.Id, - PolicyType = PolicyType.SingleOrg, - OrganizationUserType = OrganizationUserType.Owner, - OrganizationUserStatus = OrganizationUserStatusType.Revoked, - IsProvider = false - }; - - Assert.Equivalent(expectedPolicyDetails, actualPolicyDetails.Single()); - } - - [Theory, DatabaseData] - public async Task GetPolicyDetailsByUserId_RevokedInvitedUser_DoesntReturnPolicies( - IUserRepository userRepository, - IOrganizationUserRepository organizationUserRepository, - IOrganizationRepository organizationRepository, - IPolicyRepository policyRepository) - { - // Arrange - var user = await userRepository.CreateTestUserAsync(); - var org = await CreateEnterpriseOrg(organizationRepository); - // User has been invited to the org but then revoked - without ever being confirmed and linked to a user. - // This is an unhandled edge case because those users will go through policy enforcement later, - // as part of accepting their invite after being restored. For now this is just documented as expected behavior. - var orgUser = new OrganizationUser - { - OrganizationId = org.Id, - UserId = null, - Status = OrganizationUserStatusType.Revoked, - Type = OrganizationUserType.Owner, - Email = user.Email - }; - await organizationUserRepository.CreateAsync(orgUser); - await policyRepository.CreateAsync(new Policy - { - OrganizationId = org.Id, - Enabled = true, - Type = PolicyType.SingleOrg, - }); - - // Act - var actualPolicyDetails = await policyRepository.GetPolicyDetailsByUserId(user.Id); - - Assert.Empty(actualPolicyDetails); - } - - [Theory, DatabaseData] - public async Task GetPolicyDetailsByUserId_SetsIsProvider( - IUserRepository userRepository, - IOrganizationUserRepository organizationUserRepository, - IOrganizationRepository organizationRepository, - IPolicyRepository policyRepository, - IProviderRepository providerRepository, - IProviderUserRepository providerUserRepository, - IProviderOrganizationRepository providerOrganizationRepository) - { - // Arrange - var user = await userRepository.CreateTestUserAsync(); - var org = await CreateEnterpriseOrg(organizationRepository); - var orgUser = await organizationUserRepository.CreateTestOrganizationUserAsync(org, user); - await policyRepository.CreateAsync(new Policy - { - OrganizationId = org.Id, - Enabled = true, - Type = PolicyType.SingleOrg, - }); - - // Arrange provider - var provider = await providerRepository.CreateAsync(new Provider - { - Name = Guid.NewGuid().ToString(), - Enabled = true - }); - await providerUserRepository.CreateAsync(new ProviderUser - { - ProviderId = provider.Id, - UserId = user.Id, - Status = ProviderUserStatusType.Confirmed - }); - await providerOrganizationRepository.CreateAsync(new ProviderOrganization - { - OrganizationId = org.Id, - ProviderId = provider.Id - }); - - // Act - var actualPolicyDetails = await policyRepository.GetPolicyDetailsByUserId(user.Id); - - // Assert - var expectedPolicyDetails = new PolicyDetails - { - OrganizationUserId = orgUser.Id, - OrganizationId = org.Id, - PolicyType = PolicyType.SingleOrg, - OrganizationUserType = OrganizationUserType.Owner, - OrganizationUserStatus = OrganizationUserStatusType.Confirmed, - IsProvider = true - }; - - Assert.Equivalent(expectedPolicyDetails, actualPolicyDetails.Single()); - } - - [Theory, DatabaseData] - public async Task GetPolicyDetailsByUserId_IgnoresDisabledOrganizations( - IUserRepository userRepository, - IOrganizationUserRepository organizationUserRepository, - IOrganizationRepository organizationRepository, - IPolicyRepository policyRepository) - { - // Arrange - var user = await userRepository.CreateTestUserAsync(); - var org = await CreateEnterpriseOrg(organizationRepository); - await organizationUserRepository.CreateTestOrganizationUserAsync(org, user); - await policyRepository.CreateAsync(new Policy - { - OrganizationId = org.Id, - Enabled = true, - Type = PolicyType.SingleOrg, - }); - - // Org is disabled; its policies remain, but it is now inactive - org.Enabled = false; - await organizationRepository.ReplaceAsync(org); - - // Act - var actualPolicyDetails = await policyRepository.GetPolicyDetailsByUserId(user.Id); - - // Assert - Assert.Empty(actualPolicyDetails); - } - - [Theory, DatabaseData] - public async Task GetPolicyDetailsByUserId_IgnoresDowngradedOrganizations( - IUserRepository userRepository, - IOrganizationUserRepository organizationUserRepository, - IOrganizationRepository organizationRepository, - IPolicyRepository policyRepository) - { - // Arrange - var user = await userRepository.CreateTestUserAsync(); - var org = await CreateEnterpriseOrg(organizationRepository); - await organizationUserRepository.CreateTestOrganizationUserAsync(org, user); - await policyRepository.CreateAsync(new Policy - { - OrganizationId = org.Id, - Enabled = true, - Type = PolicyType.SingleOrg, - }); - - // Org is downgraded; its policies remain but its plan no longer supports them - org.UsePolicies = false; - org.PlanType = PlanType.TeamsAnnually; - await organizationRepository.ReplaceAsync(org); - - // Act - var actualPolicyDetails = await policyRepository.GetPolicyDetailsByUserId(user.Id); - - // Assert - Assert.Empty(actualPolicyDetails); - } - - [Theory, DatabaseData] - public async Task GetPolicyDetailsByUserId_IgnoresDisabledPolicies( - IUserRepository userRepository, - IOrganizationUserRepository organizationUserRepository, - IOrganizationRepository organizationRepository, - IPolicyRepository policyRepository) - { - // Arrange - var user = await userRepository.CreateTestUserAsync(); - var org = await CreateEnterpriseOrg(organizationRepository); - await organizationUserRepository.CreateTestOrganizationUserAsync(org, user); - await policyRepository.CreateAsync(new Policy - { - OrganizationId = org.Id, - Enabled = false, - Type = PolicyType.SingleOrg, - }); - - // Act - var actualPolicyDetails = await policyRepository.GetPolicyDetailsByUserId(user.Id); - - // Assert - Assert.Empty(actualPolicyDetails); - } - - private class TestPolicyData : IPolicyDataModel - { - public bool BoolSetting { get; set; } - public int IntSetting { get; set; } - } - - private Task CreateEnterpriseOrg(IOrganizationRepository organizationRepository) - => organizationRepository.CreateAsync(new Organization - { - Name = Guid.NewGuid().ToString(), - BillingEmail = "billing@example.com", // TODO: EF does not enforce this being NOT NULL - Plan = "Test", // TODO: EF does not enforce this being NOT NULl - PlanType = PlanType.EnterpriseAnnually, - UsePolicies = true - }); -} diff --git a/util/Migrator/DbScripts/2025-10-15_00_RefactorPolicyDetailsQueries.sql b/util/Migrator/DbScripts/2025-10-15_00_RefactorPolicyDetailsQueries.sql new file mode 100644 index 0000000000..b764824a42 --- /dev/null +++ b/util/Migrator/DbScripts/2025-10-15_00_RefactorPolicyDetailsQueries.sql @@ -0,0 +1,85 @@ +CREATE OR ALTER VIEW [dbo].[UserProviderAccessView] +AS +SELECT DISTINCT + PU.[UserId], + PO.[OrganizationId] +FROM + [dbo].[ProviderUserView] PU +INNER JOIN + [dbo].[ProviderOrganizationView] PO ON PO.[ProviderId] = PU.[ProviderId] +GO + +CREATE OR ALTER PROCEDURE [dbo].[OrganizationUser_ReadByUserIdWithPolicyDetails] + @UserId UNIQUEIDENTIFIER, + @PolicyType TINYINT +AS +BEGIN + SET NOCOUNT ON + + DECLARE @UserEmail NVARCHAR(256) + SELECT @UserEmail = Email + FROM + [dbo].[UserView] + WHERE + Id = @UserId + + ;WITH OrgUsers AS + ( + -- All users except invited (Status <> 0): direct UserId match + SELECT + OU.[Id], + OU.[OrganizationId], + OU.[Type], + OU.[Status], + OU.[Permissions] + FROM + [dbo].[OrganizationUserView] OU + WHERE + OU.[Status] <> 0 + AND OU.[UserId] = @UserId + + UNION ALL + + -- Invited users: email match + SELECT + OU.[Id], + OU.[OrganizationId], + OU.[Type], + OU.[Status], + OU.[Permissions] + FROM + [dbo].[OrganizationUserView] OU + WHERE + OU.[Status] = 0 + AND OU.[Email] = @UserEmail + AND @UserEmail IS NOT NULL + ), + Providers AS + ( + SELECT + OrganizationId + FROM + [dbo].[UserProviderAccessView] + WHERE + UserId = @UserId + ) + SELECT + OU.[Id] AS [OrganizationUserId], + P.[OrganizationId], + P.[Type] AS [PolicyType], + P.[Enabled] AS [PolicyEnabled], + P.[Data] AS [PolicyData], + OU.[Type] AS [OrganizationUserType], + OU.[Status] AS [OrganizationUserStatus], + OU.[Permissions] AS [OrganizationUserPermissionsData], + CASE WHEN PR.[OrganizationId] IS NULL THEN 0 ELSE 1 END AS [IsProvider] + FROM + [dbo].[PolicyView] P + INNER JOIN + OrgUsers OU ON P.[OrganizationId] = OU.[OrganizationId] + LEFT JOIN + Providers PR ON PR.[OrganizationId] = OU.[OrganizationId] + WHERE + P.[Type] = @PolicyType +END +GO