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..2d1608e680a1 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs @@ -146,6 +146,20 @@ 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(e)) + .SingleOrDefaultAsync(); + } +#nullable disable + 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..9734fa440239 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.Equal(organization.UseEvents, result.UseEvents); + Assert.Equal(organization.Use2fa, result.Use2fa); + 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); + 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); + } + + [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