From 689519c194f5d199b04c455696728a2267157d7f Mon Sep 17 00:00:00 2001 From: Jared McCannon Date: Mon, 9 Mar 2026 16:11:35 -0500 Subject: [PATCH 1/6] Added sproc, view, repo methods, and tests. --- .../Repositories/IOrganizationRepository.cs | 1 + .../Repositories/OrganizationRepository.cs | 13 +++++ .../Repositories/OrganizationRepository.cs | 36 +++++++++++++ .../Organization_ReadAbilityById.sql | 13 +++++ src/Sql/dbo/Views/OrganizationAbilityView.sql | 28 ++++++++++ .../AdminConsole/OrganizationTestHelpers.cs | 1 + .../OrganizationRepositoryTests.cs | 51 +++++++++++++++++++ ...26-03-09_00_AddOrganizationAbilityView.sql | 44 ++++++++++++++++ 8 files changed, 187 insertions(+) create mode 100644 src/Sql/dbo/Stored Procedures/Organization_ReadAbilityById.sql create mode 100644 src/Sql/dbo/Views/OrganizationAbilityView.sql create mode 100644 util/Migrator/DbScripts/2026-03-09_00_AddOrganizationAbilityView.sql diff --git a/src/Core/AdminConsole/Repositories/IOrganizationRepository.cs b/src/Core/AdminConsole/Repositories/IOrganizationRepository.cs index bf424f72a198..37aaacbd41e6 100644 --- a/src/Core/AdminConsole/Repositories/IOrganizationRepository.cs +++ b/src/Core/AdminConsole/Repositories/IOrganizationRepository.cs @@ -18,6 +18,7 @@ public interface IOrganizationRepository : IRepository Task> SearchAsync(string name, string userEmail, bool? paid, int skip, int take); Task UpdateStorageAsync(Guid id); Task> GetManyAbilitiesAsync(); + Task GetAbilityAsync(Guid organizationId); Task GetByLicenseKeyAsync(string licenseKey); Task GetSelfHostedOrganizationDetailsById(Guid id); Task> SearchUnassignedToProviderAsync(string name, string ownerEmail, int skip, int take); diff --git a/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationRepository.cs b/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationRepository.cs index 36d9064b1a35..cce80a9eb4b7 100644 --- a/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationRepository.cs +++ b/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationRepository.cs @@ -131,6 +131,19 @@ public async Task> GetManyAbilitiesAsync() } } + public async Task GetAbilityAsync(Guid organizationId) + { + using (var connection = new SqlConnection(ConnectionString)) + { + var result = await connection.QueryAsync( + "[dbo].[Organization_ReadAbilityById]", + new { Id = organizationId }, + commandType: CommandType.StoredProcedure); + + return result.SingleOrDefault(); + } + } + public async Task GetByLicenseKeyAsync(string licenseKey) { using (var connection = new SqlConnection(ConnectionString)) diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs index c0c6e9da751d..bd45a60c012c 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs @@ -146,6 +146,42 @@ public async Task> GetManyAbilitiesAsync() } } + public async Task GetAbilityAsync(Guid organizationId) + { + using (var scope = ServiceScopeFactory.CreateScope()) + { + var dbContext = GetDatabaseContext(scope); + return await GetDbSet(dbContext) + .Where(e => e.Id == organizationId) + .Select(e => new OrganizationAbility + { + Enabled = e.Enabled, + Id = e.Id, + Use2fa = e.Use2fa, + UseEvents = e.UseEvents, + UsersGetPremium = e.UsersGetPremium, + Using2fa = e.Use2fa && e.TwoFactorProviders != null, + UseSso = e.UseSso, + UseKeyConnector = e.UseKeyConnector, + UseResetPassword = e.UseResetPassword, + UseScim = e.UseScim, + UseCustomPermissions = e.UseCustomPermissions, + UsePolicies = e.UsePolicies, + LimitCollectionCreation = e.LimitCollectionCreation, + LimitCollectionDeletion = e.LimitCollectionDeletion, + LimitItemDeletion = e.LimitItemDeletion, + AllowAdminAccessToAllCollectionItems = e.AllowAdminAccessToAllCollectionItems, + UseRiskInsights = e.UseRiskInsights, + UseOrganizationDomains = e.UseOrganizationDomains, + UseAdminSponsoredFamilies = e.UseAdminSponsoredFamilies, + UseAutomaticUserConfirmation = e.UseAutomaticUserConfirmation, + UseDisableSmAdsForUsers = e.UseDisableSmAdsForUsers, + UsePhishingBlocker = e.UsePhishingBlocker, + UseMyItems = e.UseMyItems + }).SingleOrDefaultAsync(); + } + } + public async Task> SearchUnassignedToProviderAsync(string name, string ownerEmail, int skip, int take) { using var scope = ServiceScopeFactory.CreateScope(); diff --git a/src/Sql/dbo/Stored Procedures/Organization_ReadAbilityById.sql b/src/Sql/dbo/Stored Procedures/Organization_ReadAbilityById.sql new file mode 100644 index 000000000000..85d9fb2f5013 --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/Organization_ReadAbilityById.sql @@ -0,0 +1,13 @@ +CREATE PROCEDURE [dbo].[Organization_ReadAbilityById] + @Id UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT + * + FROM + [dbo].[OrganizationAbilityView] + WHERE + [Id] = @Id +END diff --git a/src/Sql/dbo/Views/OrganizationAbilityView.sql b/src/Sql/dbo/Views/OrganizationAbilityView.sql new file mode 100644 index 000000000000..f87fc879b373 --- /dev/null +++ b/src/Sql/dbo/Views/OrganizationAbilityView.sql @@ -0,0 +1,28 @@ +CREATE VIEW [dbo].[OrganizationAbilityView] +AS +SELECT + [Id], + [UseEvents], + [Use2fa], + IIF([Use2fa] = 1 AND [TwoFactorProviders] IS NOT NULL AND [TwoFactorProviders] != '{}', 1, 0) AS [Using2fa], + [UsersGetPremium], + [Enabled], + [UseSso], + [UseKeyConnector], + [UseScim], + [UseResetPassword], + [UseCustomPermissions], + [UsePolicies], + [LimitCollectionCreation], + [LimitCollectionDeletion], + [LimitItemDeletion], + [AllowAdminAccessToAllCollectionItems], + [UseRiskInsights], + [UseOrganizationDomains], + [UseAdminSponsoredFamilies], + [UseAutomaticUserConfirmation], + [UseDisableSmAdsForUsers], + [UsePhishingBlocker], + [UseMyItems] +FROM + [dbo].[Organization] diff --git a/test/Infrastructure.IntegrationTest/AdminConsole/OrganizationTestHelpers.cs b/test/Infrastructure.IntegrationTest/AdminConsole/OrganizationTestHelpers.cs index 6637c6f0ac9a..e0408d7ad7b0 100644 --- a/test/Infrastructure.IntegrationTest/AdminConsole/OrganizationTestHelpers.cs +++ b/test/Infrastructure.IntegrationTest/AdminConsole/OrganizationTestHelpers.cs @@ -96,6 +96,7 @@ public static Task CreateTestOrganizationAsync(this IOrganizationR UseAutomaticUserConfirmation = true, UsePhishingBlocker = true, UseDisableSmAdsForUsers = true, + UseMyItems = true, }); } diff --git a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationRepositoryTests.cs b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationRepositoryTests.cs index 5f384f919e66..4254ca9d49f6 100644 --- a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationRepositoryTests.cs +++ b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationRepositoryTests.cs @@ -365,6 +365,57 @@ await Assert.ThrowsAsync(async () => Assert.Null(orgUserAfter.UserId); } + [Theory, DatabaseData] + public async Task GetAbilityAsync_WithExistingOrganization_ReturnsCorrectAbility( + IOrganizationRepository organizationRepository) + { + // Arrange + var organization = await organizationRepository.CreateTestOrganizationAsync(); + + // Act + var result = await organizationRepository.GetAbilityAsync(organization.Id); + + // Assert + Assert.NotNull(result); + Assert.Equal(organization.Id, result.Id); + Assert.True(result.UseEvents); + Assert.True(result.Use2fa); + Assert.False(result.Using2fa); // TwoFactorProviders is null in test helper + Assert.True(result.UsersGetPremium); + Assert.True(result.Enabled); + Assert.True(result.UseSso); + Assert.True(result.UseKeyConnector); + Assert.True(result.UseScim); + Assert.True(result.UseResetPassword); + Assert.True(result.UseCustomPermissions); + Assert.True(result.UsePolicies); + Assert.True(result.LimitCollectionCreation); + Assert.True(result.LimitCollectionDeletion); + Assert.True(result.LimitItemDeletion); + Assert.True(result.AllowAdminAccessToAllCollectionItems); + Assert.True(result.UseRiskInsights); + Assert.True(result.UseOrganizationDomains); + Assert.True(result.UseAdminSponsoredFamilies); + Assert.True(result.UseAutomaticUserConfirmation); + Assert.True(result.UseDisableSmAdsForUsers); + Assert.True(result.UsePhishingBlocker); + Assert.True(result.UseMyItems); + + // Clean up + await organizationRepository.DeleteAsync(organization); + } + + [Theory, DatabaseData] + public async Task GetAbilityAsync_WithNonExistentOrganization_ReturnsNull( + IOrganizationRepository organizationRepository) + { + // Act + var result = await organizationRepository.GetAbilityAsync(Guid.NewGuid()); + + // Assert + Assert.Null(result); + } + private static async Task<(User user, Organization organization, OrganizationUser organizationUser)> CreatePendingOrganizationWithUserAsync( IUserRepository userRepository, diff --git a/util/Migrator/DbScripts/2026-03-09_00_AddOrganizationAbilityView.sql b/util/Migrator/DbScripts/2026-03-09_00_AddOrganizationAbilityView.sql new file mode 100644 index 000000000000..b317fe1c4581 --- /dev/null +++ b/util/Migrator/DbScripts/2026-03-09_00_AddOrganizationAbilityView.sql @@ -0,0 +1,44 @@ +CREATE OR ALTER VIEW [dbo].[OrganizationAbilityView] +AS +SELECT + [Id], + [UseEvents], + [Use2fa], + IIF([Use2fa] = 1 AND [TwoFactorProviders] IS NOT NULL AND [TwoFactorProviders] != '{}', 1, 0) AS [Using2fa], + [UsersGetPremium], + [Enabled], + [UseSso], + [UseKeyConnector], + [UseScim], + [UseResetPassword], + [UseCustomPermissions], + [UsePolicies], + [LimitCollectionCreation], + [LimitCollectionDeletion], + [LimitItemDeletion], + [AllowAdminAccessToAllCollectionItems], + [UseRiskInsights], + [UseOrganizationDomains], + [UseAdminSponsoredFamilies], + [UseAutomaticUserConfirmation], + [UseDisableSmAdsForUsers], + [UsePhishingBlocker], + [UseMyItems] +FROM + [dbo].[Organization] +GO + +CREATE OR ALTER PROCEDURE [dbo].[Organization_ReadAbilityById] + @Id UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT + * + FROM + [dbo].[OrganizationAbilityView] + WHERE + [Id] = @Id +END +GO From 341f9f7e3acd94da69027f7dcbe39a2253989e3f Mon Sep 17 00:00:00 2001 From: Jared McCannon Date: Tue, 10 Mar 2026 10:37:00 -0500 Subject: [PATCH 2/6] fixing inconsistency --- .../AdminConsole/Repositories/OrganizationRepository.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs index bd45a60c012c..ce4f728bd609 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs @@ -146,7 +146,8 @@ public async Task> GetManyAbilitiesAsync() } } - public async Task GetAbilityAsync(Guid organizationId) +#nullable enable + public async Task GetAbilityAsync(Guid organizationId) { using (var scope = ServiceScopeFactory.CreateScope()) { @@ -181,6 +182,7 @@ public async Task GetAbilityAsync(Guid organizationId) }).SingleOrDefaultAsync(); } } +#nullable disable public async Task> SearchUnassignedToProviderAsync(string name, string ownerEmail, int skip, int take) { From 99f37e1267f308f4f20121e887ff30182a270892 Mon Sep 17 00:00:00 2001 From: Jared McCannon Date: Tue, 10 Mar 2026 14:44:27 -0500 Subject: [PATCH 3/6] changed up test a bit --- .../OrganizationRepositoryTests.cs | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationRepositoryTests.cs b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationRepositoryTests.cs index 4254ca9d49f6..0f8b09114d75 100644 --- a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationRepositoryTests.cs +++ b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationRepositoryTests.cs @@ -378,28 +378,28 @@ public async Task GetAbilityAsync_WithExistingOrganization_ReturnsCorrectAbility // Assert Assert.NotNull(result); Assert.Equal(organization.Id, result.Id); - Assert.True(result.UseEvents); - Assert.True(result.Use2fa); - Assert.False(result.Using2fa); // TwoFactorProviders is null in test helper - Assert.True(result.UsersGetPremium); - Assert.True(result.Enabled); - Assert.True(result.UseSso); - Assert.True(result.UseKeyConnector); - Assert.True(result.UseScim); - Assert.True(result.UseResetPassword); - Assert.True(result.UseCustomPermissions); - Assert.True(result.UsePolicies); - Assert.True(result.LimitCollectionCreation); - Assert.True(result.LimitCollectionDeletion); - Assert.True(result.LimitItemDeletion); - Assert.True(result.AllowAdminAccessToAllCollectionItems); - Assert.True(result.UseRiskInsights); - Assert.True(result.UseOrganizationDomains); - Assert.True(result.UseAdminSponsoredFamilies); - Assert.True(result.UseAutomaticUserConfirmation); - Assert.True(result.UseDisableSmAdsForUsers); - Assert.True(result.UsePhishingBlocker); - Assert.True(result.UseMyItems); + Assert.Equal(organization.UseEvents, result.UseEvents); + Assert.Equal(organization.Use2fa, result.Use2fa); + Assert.Equal(organization.Use2fa && organization.TwoFactorProviders != null, result.Using2fa); + Assert.Equal(organization.UsersGetPremium, result.UsersGetPremium); + Assert.Equal(organization.Enabled, result.Enabled); + Assert.Equal(organization.UseSso, result.UseSso); + Assert.Equal(organization.UseKeyConnector, result.UseKeyConnector); + Assert.Equal(organization.UseScim, result.UseScim); + Assert.Equal(organization.UseResetPassword, result.UseResetPassword); + Assert.Equal(organization.UseCustomPermissions, result.UseCustomPermissions); + Assert.Equal(organization.UsePolicies, result.UsePolicies); + Assert.Equal(organization.LimitCollectionCreation, result.LimitCollectionCreation); + Assert.Equal(organization.LimitCollectionDeletion, result.LimitCollectionDeletion); + Assert.Equal(organization.LimitItemDeletion, result.LimitItemDeletion); + Assert.Equal(organization.AllowAdminAccessToAllCollectionItems, result.AllowAdminAccessToAllCollectionItems); + Assert.Equal(organization.UseRiskInsights, result.UseRiskInsights); + Assert.Equal(organization.UseOrganizationDomains, result.UseOrganizationDomains); + Assert.Equal(organization.UseAdminSponsoredFamilies, result.UseAdminSponsoredFamilies); + Assert.Equal(organization.UseAutomaticUserConfirmation, result.UseAutomaticUserConfirmation); + Assert.Equal(organization.UseDisableSmAdsForUsers, result.UseDisableSmAdsForUsers); + Assert.Equal(organization.UsePhishingBlocker, result.UsePhishingBlocker); + Assert.Equal(organization.UseMyItems, result.UseMyItems); // Clean up await organizationRepository.DeleteAsync(organization); From 533dc506fa0ff7b0cbf7f0cc92cfe6ba9dcba747 Mon Sep 17 00:00:00 2001 From: Jared McCannon Date: Thu, 12 Mar 2026 09:59:25 -0500 Subject: [PATCH 4/6] adding empty object check to EF query. --- .../AdminConsole/Repositories/OrganizationRepository.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs index ce4f728bd609..f90d9d149290 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs @@ -161,7 +161,7 @@ public async Task> GetManyAbilitiesAsync() Use2fa = e.Use2fa, UseEvents = e.UseEvents, UsersGetPremium = e.UsersGetPremium, - Using2fa = e.Use2fa && e.TwoFactorProviders != null, + Using2fa = e.Use2fa && e.TwoFactorProviders != null && e.TwoFactorProviders != "{}", UseSso = e.UseSso, UseKeyConnector = e.UseKeyConnector, UseResetPassword = e.UseResetPassword, From e598f4e680f6b1a641744f546f38a44f185541eb Mon Sep 17 00:00:00 2001 From: Jared McCannon Date: Thu, 12 Mar 2026 11:03:48 -0500 Subject: [PATCH 5/6] Switching to constructor --- .../Repositories/OrganizationRepository.cs | 40 ++++--------------- 1 file changed, 8 insertions(+), 32 deletions(-) diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs index f90d9d149290..2d1608e680a1 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs @@ -149,38 +149,14 @@ public async Task> GetManyAbilitiesAsync() #nullable enable public async Task GetAbilityAsync(Guid organizationId) { - using (var scope = ServiceScopeFactory.CreateScope()) - { - var dbContext = GetDatabaseContext(scope); - return await GetDbSet(dbContext) - .Where(e => e.Id == organizationId) - .Select(e => new OrganizationAbility - { - Enabled = e.Enabled, - Id = e.Id, - Use2fa = e.Use2fa, - UseEvents = e.UseEvents, - UsersGetPremium = e.UsersGetPremium, - Using2fa = e.Use2fa && e.TwoFactorProviders != null && e.TwoFactorProviders != "{}", - UseSso = e.UseSso, - UseKeyConnector = e.UseKeyConnector, - UseResetPassword = e.UseResetPassword, - UseScim = e.UseScim, - UseCustomPermissions = e.UseCustomPermissions, - UsePolicies = e.UsePolicies, - LimitCollectionCreation = e.LimitCollectionCreation, - LimitCollectionDeletion = e.LimitCollectionDeletion, - LimitItemDeletion = e.LimitItemDeletion, - AllowAdminAccessToAllCollectionItems = e.AllowAdminAccessToAllCollectionItems, - UseRiskInsights = e.UseRiskInsights, - UseOrganizationDomains = e.UseOrganizationDomains, - UseAdminSponsoredFamilies = e.UseAdminSponsoredFamilies, - UseAutomaticUserConfirmation = e.UseAutomaticUserConfirmation, - UseDisableSmAdsForUsers = e.UseDisableSmAdsForUsers, - UsePhishingBlocker = e.UsePhishingBlocker, - UseMyItems = e.UseMyItems - }).SingleOrDefaultAsync(); - } + using var scope = ServiceScopeFactory.CreateScope(); + + var dbContext = GetDatabaseContext(scope); + + return await GetDbSet(dbContext) + .Where(e => e.Id == organizationId) + .Select(e => new OrganizationAbility(e)) + .SingleOrDefaultAsync(); } #nullable disable From 527e9f1a15cf1af170baa7fc8652f59f8b78c694 Mon Sep 17 00:00:00 2001 From: Jared McCannon Date: Thu, 12 Mar 2026 11:04:46 -0500 Subject: [PATCH 6/6] aligning test --- .../AdminConsole/Repositories/OrganizationRepositoryTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationRepositoryTests.cs b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationRepositoryTests.cs index 0f8b09114d75..9734fa440239 100644 --- a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationRepositoryTests.cs +++ b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationRepositoryTests.cs @@ -380,7 +380,7 @@ public async Task GetAbilityAsync_WithExistingOrganization_ReturnsCorrectAbility Assert.Equal(organization.Id, result.Id); Assert.Equal(organization.UseEvents, result.UseEvents); Assert.Equal(organization.Use2fa, result.Use2fa); - Assert.Equal(organization.Use2fa && organization.TwoFactorProviders != null, result.Using2fa); + Assert.Equal(organization.Use2fa && organization.TwoFactorProviders != null && organization.TwoFactorProviders != "{}", result.Using2fa); Assert.Equal(organization.UsersGetPremium, result.UsersGetPremium); Assert.Equal(organization.Enabled, result.Enabled); Assert.Equal(organization.UseSso, result.UseSso);