From de13932ffe52d09f5603e7868ff067382ddf7c6c Mon Sep 17 00:00:00 2001 From: Jimmy Vo Date: Thu, 31 Jul 2025 11:24:39 -0400 Subject: [PATCH] [PM-22108] Add PolicyDetails_ReadByOrganizationId proc (#6019) --- .../Policies/OrganizationPolicyDetails.cs | 6 + .../Repositories/IPolicyRepository.cs | 13 + .../Repositories/PolicyRepository.cs | 13 + .../Repositories/PolicyRepository.cs | 89 ++++++ .../PolicyDetails_ReadByOrganizationId.sql | 81 ++++++ src/Sql/dbo/Tables/OrganizationUser.sql | 9 +- src/Sql/dbo/Tables/ProviderOrganization.sql | 4 + src/Sql/dbo/Tables/ProviderUser.sql | 5 + ...PolicyDetailsByOrganizationIdAsyncTests.cs | 258 ++++++++++++++++++ ..._00_PolicyDetails_ReadByOrganizationId.sql | 81 ++++++ .../DbScripts/2025-07-18_00_AddIndices.sql | 34 +++ 11 files changed, 589 insertions(+), 4 deletions(-) create mode 100644 src/Core/AdminConsole/Models/Data/Organizations/Policies/OrganizationPolicyDetails.cs create mode 100644 src/Sql/dbo/Stored Procedures/PolicyDetails_ReadByOrganizationId.sql create mode 100644 test/Infrastructure.IntegrationTest/AdminConsole/Repositories/PolicyRepository/GetPolicyDetailsByOrganizationIdAsyncTests.cs create mode 100644 util/Migrator/DbScripts/2025-07-17_00_PolicyDetails_ReadByOrganizationId.sql create mode 100644 util/Migrator/DbScripts/2025-07-18_00_AddIndices.sql diff --git a/src/Core/AdminConsole/Models/Data/Organizations/Policies/OrganizationPolicyDetails.cs b/src/Core/AdminConsole/Models/Data/Organizations/Policies/OrganizationPolicyDetails.cs new file mode 100644 index 0000000000..eab0c9456f --- /dev/null +++ b/src/Core/AdminConsole/Models/Data/Organizations/Policies/OrganizationPolicyDetails.cs @@ -0,0 +1,6 @@ +namespace Bit.Core.AdminConsole.Models.Data.Organizations.Policies; + +public class OrganizationPolicyDetails : PolicyDetails +{ + public Guid UserId { get; set; } +} diff --git a/src/Core/AdminConsole/Repositories/IPolicyRepository.cs b/src/Core/AdminConsole/Repositories/IPolicyRepository.cs index 4c0c03536d..2b46c040bb 100644 --- a/src/Core/AdminConsole/Repositories/IPolicyRepository.cs +++ b/src/Core/AdminConsole/Repositories/IPolicyRepository.cs @@ -31,4 +31,17 @@ public interface IPolicyRepository : IRepository /// You probably do not want to call it directly. /// Task> GetPolicyDetailsByUserId(Guid userId); + + /// + /// Retrieves of the specified + /// for users in the given organization and for any other organizations those users belong to. + /// + /// + /// 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> GetPolicyDetailsByOrganizationIdAsync(Guid organizationId, PolicyType policyType); } diff --git a/src/Infrastructure.Dapper/AdminConsole/Repositories/PolicyRepository.cs b/src/Infrastructure.Dapper/AdminConsole/Repositories/PolicyRepository.cs index 071ff3153a..c93c66c94d 100644 --- a/src/Infrastructure.Dapper/AdminConsole/Repositories/PolicyRepository.cs +++ b/src/Infrastructure.Dapper/AdminConsole/Repositories/PolicyRepository.cs @@ -73,4 +73,17 @@ public class PolicyRepository : Repository, IPolicyRepository return results.ToList(); } } + + public async Task> GetPolicyDetailsByOrganizationIdAsync(Guid organizationId, PolicyType policyType) + { + using (var connection = new SqlConnection(ConnectionString)) + { + var results = await connection.QueryAsync( + $"[{Schema}].[PolicyDetails_ReadByOrganizationId]", + new { @OrganizationId = organizationId, @PolicyType = policyType }, + commandType: CommandType.StoredProcedure); + + return results.ToList(); + } + } } diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/PolicyRepository.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/PolicyRepository.cs index f9287a20a9..9d25fd5541 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/PolicyRepository.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/PolicyRepository.cs @@ -94,4 +94,93 @@ public class PolicyRepository : Repository> 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(); + } } diff --git a/src/Sql/dbo/Stored Procedures/PolicyDetails_ReadByOrganizationId.sql b/src/Sql/dbo/Stored Procedures/PolicyDetails_ReadByOrganizationId.sql new file mode 100644 index 0000000000..526a9141ac --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/PolicyDetails_ReadByOrganizationId.sql @@ -0,0 +1,81 @@ +CREATE PROCEDURE [dbo].[PolicyDetails_ReadByOrganizationId] + @OrganizationId UNIQUEIDENTIFIER, + @PolicyType TINYINT +AS +BEGIN + SET NOCOUNT ON; + + -- Get users in the given organization (@OrganizationId) by matching either on UserId or Email. + ;WITH GivenOrgUsers AS ( + SELECT + OU.[UserId], + U.[Email] + FROM [dbo].[OrganizationUserView] OU + INNER JOIN [dbo].[UserView] U ON U.[Id] = OU.[UserId] + WHERE OU.[OrganizationId] = @OrganizationId + + UNION ALL + + SELECT + U.[Id] AS [UserId], + U.[Email] + FROM [dbo].[OrganizationUserView] OU + INNER JOIN [dbo].[UserView] U ON U.[Email] = OU.[Email] + WHERE OU.[OrganizationId] = @OrganizationId + ), + + -- Retrieve all organization users that match on either UserId or Email from GivenOrgUsers. + AllOrgUsers AS ( + SELECT + OU.[Id] AS [OrganizationUserId], + OU.[UserId], + OU.[OrganizationId], + AU.[Email], + OU.[Type] AS [OrganizationUserType], + OU.[Status] AS [OrganizationUserStatus], + OU.[Permissions] AS [OrganizationUserPermissionsData] + FROM [dbo].[OrganizationUserView] OU + INNER JOIN GivenOrgUsers AU ON AU.[UserId] = OU.[UserId] + UNION ALL + SELECT + OU.[Id] AS [OrganizationUserId], + AU.[UserId], + OU.[OrganizationId], + AU.[Email], + OU.[Type] AS [OrganizationUserType], + OU.[Status] AS [OrganizationUserStatus], + OU.[Permissions] AS [OrganizationUserPermissionsData] + FROM [dbo].[OrganizationUserView] OU + INNER JOIN GivenOrgUsers AU ON AU.[Email] = OU.[Email] + ) + + -- Return policy details for each matching organization user. + SELECT + OU.[OrganizationUserId], + P.[OrganizationId], + P.[Type] AS [PolicyType], + P.[Data] AS [PolicyData], + OU.[OrganizationUserType], + OU.[OrganizationUserStatus], + OU.[OrganizationUserPermissionsData], + -- Check if user is a provider for the organization + 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].[OrganizationView] O ON P.[OrganizationId] = O.[Id] + INNER JOIN AllOrgUsers OU ON OU.[OrganizationId] = O.[Id] + WHERE P.[Enabled] = 1 + AND O.[Enabled] = 1 + AND O.[UsePolicies] = 1 + AND P.[Type] = @PolicyType + +END +GO diff --git a/src/Sql/dbo/Tables/OrganizationUser.sql b/src/Sql/dbo/Tables/OrganizationUser.sql index 513a5f6696..51ed2115bc 100644 --- a/src/Sql/dbo/Tables/OrganizationUser.sql +++ b/src/Sql/dbo/Tables/OrganizationUser.sql @@ -17,20 +17,21 @@ CONSTRAINT [FK_OrganizationUser_User] FOREIGN KEY ([UserId]) REFERENCES [dbo].[User] ([Id]) ); - GO CREATE NONCLUSTERED INDEX [IX_OrganizationUser_UserIdOrganizationIdStatusV2] ON [dbo].[OrganizationUser]([UserId] ASC, [OrganizationId] ASC, [Status] ASC); - - GO + CREATE NONCLUSTERED INDEX [IX_OrganizationUser_OrganizationId] ON [dbo].[OrganizationUser]([OrganizationId] ASC); +GO +CREATE NONCLUSTERED INDEX IX_OrganizationUser_EmailOrganizationIdStatus + ON OrganizationUser (Email ASC, OrganizationId ASC, [Status] ASC); GO CREATE NONCLUSTERED INDEX [IX_OrganizationUser_OrganizationId_UserId] ON [dbo].[OrganizationUser] ([OrganizationId], [UserId]) - INCLUDE ([Email], [Status], [Type], [ExternalId], [CreationDate], + INCLUDE ([Email], [Status], [Type], [ExternalId], [CreationDate], [RevisionDate], [Permissions], [ResetPasswordKey], [AccessSecretsManager]); GO diff --git a/src/Sql/dbo/Tables/ProviderOrganization.sql b/src/Sql/dbo/Tables/ProviderOrganization.sql index ccf5455ab3..e6a7dd9270 100644 --- a/src/Sql/dbo/Tables/ProviderOrganization.sql +++ b/src/Sql/dbo/Tables/ProviderOrganization.sql @@ -10,3 +10,7 @@ CONSTRAINT [FK_ProviderOrganization_Provider] FOREIGN KEY ([ProviderId]) REFERENCES [dbo].[Provider] ([Id]) ON DELETE CASCADE, CONSTRAINT [FK_ProviderOrganization_Organization] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization] ([Id]) ); + +GO +CREATE NONCLUSTERED INDEX IX_ProviderOrganization_OrganizationIdProviderId + ON [dbo].[ProviderOrganization] ([OrganizationId], [ProviderId]); diff --git a/src/Sql/dbo/Tables/ProviderUser.sql b/src/Sql/dbo/Tables/ProviderUser.sql index 8905242aa9..b18b4a4afe 100644 --- a/src/Sql/dbo/Tables/ProviderUser.sql +++ b/src/Sql/dbo/Tables/ProviderUser.sql @@ -13,3 +13,8 @@ CONSTRAINT [FK_ProviderUser_Provider] FOREIGN KEY ([ProviderId]) REFERENCES [dbo].[Provider] ([Id]) ON DELETE CASCADE, CONSTRAINT [FK_ProviderUser_User] FOREIGN KEY ([UserId]) REFERENCES [dbo].[User] ([Id]) ); + + +GO +CREATE NONCLUSTERED INDEX IX_ProviderUser_UserIdProviderId + ON [dbo].[ProviderUser] ([UserId], [ProviderId]); diff --git a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/PolicyRepository/GetPolicyDetailsByOrganizationIdAsyncTests.cs b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/PolicyRepository/GetPolicyDetailsByOrganizationIdAsyncTests.cs new file mode 100644 index 0000000000..7dc4b6d2b3 --- /dev/null +++ b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/PolicyRepository/GetPolicyDetailsByOrganizationIdAsyncTests.cs @@ -0,0 +1,258 @@ +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.Repositories; +using Xunit; + +namespace Bit.Infrastructure.IntegrationTest.AdminConsole.Repositories.PolicyRepository; + +public class GetPolicyDetailsByOrganizationIdAsyncTests +{ + [DatabaseTheory, DatabaseData] + public async Task ShouldContainProviderData( + IUserRepository userRepository, + IOrganizationUserRepository organizationUserRepository, + IOrganizationRepository organizationRepository, + IProviderRepository providerRepository, + IProviderUserRepository providerUserRepository, + IProviderOrganizationRepository providerOrganizationRepository, + IPolicyRepository policyRepository) + { + // Arrange + var user = await userRepository.CreateTestUserAsync(); + const PolicyType policyType = PolicyType.SingleOrg; + + var userOrgConnectedDirectly = await ArrangeDirectlyConnectedOrgByUserIdAsync(organizationUserRepository, organizationRepository, policyRepository, user, policyType); + + await ArrangeProvider(); + + // Act + var results = (await policyRepository.GetPolicyDetailsByOrganizationIdAsync(userOrgConnectedDirectly.OrganizationId, policyType)).ToList(); + + // Assert + Assert.Single(results); + + Assert.True(results.Single().IsProvider); + + async Task ArrangeProvider() + { + 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 = userOrgConnectedDirectly.OrganizationId, + ProviderId = provider.Id + }); + } + } + + [DatabaseTheory, DatabaseData] + public async Task ShouldNotReturnOtherOrganizations_WhenUserIsNotConnected( + IUserRepository userRepository, + IOrganizationUserRepository organizationUserRepository, + IOrganizationRepository organizationRepository, + IPolicyRepository policyRepository) + { + // Arrange + var user = await userRepository.CreateTestUserAsync(); + + const PolicyType policyType = PolicyType.SingleOrg; + var userOrgConnectedDirectly = await ArrangeDirectlyConnectedOrgByUserIdAsync(organizationUserRepository, organizationRepository, policyRepository, user, policyType); + + var notConnectedOrg = await CreateEnterpriseOrg(organizationRepository); + await policyRepository.CreateAsync(new Policy { OrganizationId = notConnectedOrg.Id, Enabled = true, Type = policyType }); + + // Act + var results = (await policyRepository.GetPolicyDetailsByOrganizationIdAsync(userOrgConnectedDirectly.OrganizationId, PolicyType.SingleOrg)).ToList(); + + // Assert + Assert.Single(results); + + Assert.Contains(results, result => result.OrganizationUserId == userOrgConnectedDirectly.Id + && result.OrganizationId == userOrgConnectedDirectly.OrganizationId); + Assert.DoesNotContain(results, result => result.OrganizationId == notConnectedOrg.Id); + } + + [DatabaseTheory, DatabaseData] + public async Task ShouldOnlyReturnInputPolicyType( + IUserRepository userRepository, + IOrganizationUserRepository organizationUserRepository, + IOrganizationRepository organizationRepository, + IPolicyRepository policyRepository) + { + // Arrange + var user = await userRepository.CreateTestUserAsync(); + + const PolicyType inputPolicyType = PolicyType.SingleOrg; + var orgUser = await ArrangeDirectlyConnectedOrgByUserIdAsync(organizationUserRepository, organizationRepository, policyRepository, user, inputPolicyType); + + const PolicyType notInputPolicyType = PolicyType.RequireSso; + await policyRepository.CreateAsync(new Policy { OrganizationId = orgUser.OrganizationId, Enabled = true, Type = notInputPolicyType }); + + // Act + var results = (await policyRepository.GetPolicyDetailsByOrganizationIdAsync(orgUser.OrganizationId, inputPolicyType)).ToList(); + + // Assert + Assert.Single(results); + + Assert.Contains(results, result => result.OrganizationUserId == orgUser.Id + && result.OrganizationId == orgUser.OrganizationId + && result.PolicyType == inputPolicyType); + + Assert.DoesNotContain(results, result => result.PolicyType == notInputPolicyType); + } + + + [DatabaseTheory, DatabaseData] + public async Task WhenDirectlyConnectedUserHasUserId_ShouldReturnOtherConnectedOrganizationPolicies( + IUserRepository userRepository, + IOrganizationUserRepository organizationUserRepository, + IOrganizationRepository organizationRepository, + IPolicyRepository policyRepository) + { + // Arrange + var user = await userRepository.CreateTestUserAsync(); + const PolicyType policyType = PolicyType.SingleOrg; + + var userOrgConnectedDirectly = await ArrangeDirectlyConnectedOrgByUserIdAsync(organizationUserRepository, organizationRepository, policyRepository, user, policyType); + + var userOrgConnectedByEmail = await ArrangeOtherOrgConnectedByEmailAsync(organizationUserRepository, organizationRepository, policyRepository, user, policyType); + + var userOrgConnectedByUserId = await ArrangeOtherOrgConnectedByUserIdAsync(organizationUserRepository, organizationRepository, policyRepository, user, policyType); + + // Act + var results = (await policyRepository.GetPolicyDetailsByOrganizationIdAsync(userOrgConnectedDirectly.OrganizationId, policyType)).ToList(); + + // Assert + const int expectedCount = 3; + Assert.Equal(expectedCount, results.Count); + + AssertPolicyDetailUserConnections(results, userOrgConnectedDirectly, userOrgConnectedByEmail, userOrgConnectedByUserId); + } + + [DatabaseTheory, DatabaseData] + public async Task WhenDirectlyConnectedUserHasEmail_ShouldReturnOtherConnectedOrganizationPolicies( + IUserRepository userRepository, + IOrganizationUserRepository organizationUserRepository, + IOrganizationRepository organizationRepository, + IPolicyRepository policyRepository) + { + // Arrange + var user = await userRepository.CreateTestUserAsync(); + const PolicyType policyType = PolicyType.SingleOrg; + + var userOrgConnectedDirectly = await ArrangeDirectlyConnectedOrgByEmailAsync(organizationUserRepository, organizationRepository, policyRepository, user, policyType); + + var userOrgConnectedByEmail = await ArrangeOtherOrgConnectedByEmailAsync(organizationUserRepository, organizationRepository, policyRepository, user, policyType); + + var userOrgConnectedByUserId = await ArrangeOtherOrgConnectedByUserIdAsync(organizationUserRepository, organizationRepository, policyRepository, user, policyType); + + // Act + var results = (await policyRepository.GetPolicyDetailsByOrganizationIdAsync(userOrgConnectedDirectly.OrganizationId, policyType)).ToList(); + + // Assert + AssertPolicyDetailUserConnections(results, userOrgConnectedDirectly, userOrgConnectedByEmail, userOrgConnectedByUserId); + } + + private async Task ArrangeOtherOrgConnectedByUserIdAsync(IOrganizationUserRepository organizationUserRepository, + IOrganizationRepository organizationRepository, IPolicyRepository policyRepository, User user, + PolicyType policyType) + { + var organization = await CreateEnterpriseOrg(organizationRepository); + + var organizationUser = await organizationUserRepository.CreateTestOrganizationUserAsync(organization, user); + await policyRepository.CreateAsync(new Policy { OrganizationId = organization.Id, Enabled = true, Type = policyType }); + + return organizationUser; + } + + private async Task ArrangeDirectlyConnectedOrgByUserIdAsync(IOrganizationUserRepository organizationUserRepository, + IOrganizationRepository organizationRepository, IPolicyRepository policyRepository, User user, + PolicyType policyType) + { + var organization = await CreateEnterpriseOrg(organizationRepository); + + var organizationUser = await organizationUserRepository.CreateTestOrganizationUserAsync(organization, user); + + await policyRepository.CreateAsync(new Policy { OrganizationId = organization.Id, Enabled = true, Type = policyType }); + + return organizationUser; + } + + private static void AssertPolicyDetailUserConnections(List results, + OrganizationUser userOrgConnectedDirectly, + OrganizationUser userOrgConnectedByEmail, + OrganizationUser userOrgConnectedByUserId) + { + Assert.Contains(results, result => result.OrganizationUserId == userOrgConnectedDirectly.Id + && result.OrganizationId == userOrgConnectedDirectly.OrganizationId); + Assert.Contains(results, result => result.OrganizationUserId == userOrgConnectedByEmail.Id + && result.OrganizationId == userOrgConnectedByEmail.OrganizationId); + Assert.Contains(results, result => result.OrganizationUserId == userOrgConnectedByUserId.Id + && result.OrganizationId == userOrgConnectedByUserId.OrganizationId); + } + + private async Task ArrangeOtherOrgConnectedByEmailAsync(IOrganizationUserRepository organizationUserRepository, + IOrganizationRepository organizationRepository, IPolicyRepository policyRepository, User user, + PolicyType policyType) + { + var organization = await CreateEnterpriseOrg(organizationRepository); + var organizationUser = new OrganizationUser + { + OrganizationId = organization.Id, + UserId = null, + Status = OrganizationUserStatusType.Invited, + Type = OrganizationUserType.Custom, + Email = user.Email + }; + await organizationUserRepository.CreateAsync(organizationUser); + await policyRepository.CreateAsync(new Policy { OrganizationId = organization.Id, Enabled = true, Type = policyType }); + + return organizationUser; + } + + private async Task ArrangeDirectlyConnectedOrgByEmailAsync(IOrganizationUserRepository organizationUserRepository, + IOrganizationRepository organizationRepository, IPolicyRepository policyRepository, User user, + PolicyType policyType) + { + var organization = await CreateEnterpriseOrg(organizationRepository); + var organizationUser = new OrganizationUser + { + OrganizationId = organization.Id, + UserId = null, + Status = OrganizationUserStatusType.Invited, + Type = OrganizationUserType.Custom, + Email = user.Email + }; + await organizationUserRepository.CreateAsync(organizationUser); + + await policyRepository.CreateAsync(new Policy { OrganizationId = organization.Id, Enabled = true, Type = policyType }); + + return organizationUser; + } + + private Task CreateEnterpriseOrg(IOrganizationRepository orgRepo) + => orgRepo.CreateAsync(new Organization + { + Name = System.Guid.NewGuid().ToString(), + BillingEmail = "billing@example.com", + Plan = "Test", + PlanType = PlanType.EnterpriseAnnually, + UsePolicies = true + }); +} diff --git a/util/Migrator/DbScripts/2025-07-17_00_PolicyDetails_ReadByOrganizationId.sql b/util/Migrator/DbScripts/2025-07-17_00_PolicyDetails_ReadByOrganizationId.sql new file mode 100644 index 0000000000..a318d0af26 --- /dev/null +++ b/util/Migrator/DbScripts/2025-07-17_00_PolicyDetails_ReadByOrganizationId.sql @@ -0,0 +1,81 @@ +CREATE OR ALTER PROCEDURE [dbo].[PolicyDetails_ReadByOrganizationId] + @OrganizationId UNIQUEIDENTIFIER, + @PolicyType TINYINT +AS +BEGIN + SET NOCOUNT ON; + + -- Get users in the given organization (@OrganizationId) by matching either on UserId or Email. + ;WITH GivenOrgUsers AS ( + SELECT + OU.[UserId], + U.[Email] + FROM [dbo].[OrganizationUserView] OU + INNER JOIN [dbo].[UserView] U ON U.[Id] = OU.[UserId] + WHERE OU.[OrganizationId] = @OrganizationId + + UNION ALL + + SELECT + U.[Id] AS [UserId], + U.[Email] + FROM [dbo].[OrganizationUserView] OU + INNER JOIN [dbo].[UserView] U ON U.[Email] = OU.[Email] + WHERE OU.[OrganizationId] = @OrganizationId + ), + + -- Retrieve all organization users that match on either UserId or Email from GivenOrgUsers. + AllOrgUsers AS ( + SELECT + OU.[Id] AS [OrganizationUserId], + OU.[UserId], + OU.[OrganizationId], + AU.[Email], + OU.[Type] AS [OrganizationUserType], + OU.[Status] AS [OrganizationUserStatus], + OU.[Permissions] AS [OrganizationUserPermissionsData] + FROM [dbo].[OrganizationUserView] OU + INNER JOIN GivenOrgUsers AU ON AU.[UserId] = OU.[UserId] + UNION ALL + SELECT + OU.[Id] AS [OrganizationUserId], + AU.[UserId], + OU.[OrganizationId], + AU.[Email], + OU.[Type] AS [OrganizationUserType], + OU.[Status] AS [OrganizationUserStatus], + OU.[Permissions] AS [OrganizationUserPermissionsData] + FROM [dbo].[OrganizationUserView] OU + INNER JOIN GivenOrgUsers AU ON AU.[Email] = OU.[Email] + ) + + -- Return policy details for each matching organization user. + SELECT + OU.[OrganizationUserId], + P.[OrganizationId], + P.[Type] AS [PolicyType], + P.[Data] AS [PolicyData], + OU.[OrganizationUserType], + OU.[OrganizationUserStatus], + OU.[OrganizationUserPermissionsData], + -- Check if user is a provider for the organization + 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].[OrganizationView] O ON P.[OrganizationId] = O.[Id] + INNER JOIN AllOrgUsers OU ON OU.[OrganizationId] = O.[Id] + WHERE P.[Enabled] = 1 + AND O.[Enabled] = 1 + AND O.[UsePolicies] = 1 + AND P.[Type] = @PolicyType + +END +GO diff --git a/util/Migrator/DbScripts/2025-07-18_00_AddIndices.sql b/util/Migrator/DbScripts/2025-07-18_00_AddIndices.sql new file mode 100644 index 0000000000..4082082324 --- /dev/null +++ b/util/Migrator/DbScripts/2025-07-18_00_AddIndices.sql @@ -0,0 +1,34 @@ +-- Adding indices +IF NOT EXISTS (SELECT * + FROM sys.indexes + WHERE [name] = 'IX_OrganizationUser_EmailOrganizationIdStatus' + AND object_id = Object_id('[dbo].[OrganizationUser]')) + BEGIN + CREATE NONCLUSTERED INDEX [IX_OrganizationUser_EmailOrganizationIdStatus] + ON [dbo].[OrganizationUser]([email] ASC, [organizationid] ASC, [status] ASC) + END + +go + +IF NOT EXISTS (SELECT * + FROM sys.indexes + WHERE [name] = + 'IX_ProviderOrganization_OrganizationIdProviderId' + AND object_id = Object_id('[dbo].[ProviderOrganization]')) + BEGIN + CREATE NONCLUSTERED INDEX [IX_ProviderOrganization_OrganizationIdProviderId] + ON [dbo].[ProviderOrganization]([organizationid] ASC, [providerid] ASC) + END + +go + +IF NOT EXISTS (SELECT * + FROM sys.indexes + WHERE [name] = 'IX_ProviderUser_UserIdProviderId' + AND object_id = Object_id('[dbo].[ProviderUser]')) + BEGIN + CREATE NONCLUSTERED INDEX [IX_ProviderUser_UserIdProviderId] + ON [dbo].[ProviderUser]([userid] ASC, [providerid] ASC) + END + +go