diff --git a/src/Api/AdminConsole/Controllers/OrganizationsController.cs b/src/Api/AdminConsole/Controllers/OrganizationsController.cs index b75d053c49..498631b2e9 100644 --- a/src/Api/AdminConsole/Controllers/OrganizationsController.cs +++ b/src/Api/AdminConsole/Controllers/OrganizationsController.cs @@ -14,7 +14,6 @@ using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Models.Business.Tokenables; using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationApiKeys.Interfaces; -using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationCollectionEnhancements.Interfaces; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Repositories; @@ -53,7 +52,6 @@ public class OrganizationsController : Controller private readonly IFeatureService _featureService; private readonly GlobalSettings _globalSettings; private readonly IPushNotificationService _pushNotificationService; - private readonly IOrganizationEnableCollectionEnhancementsCommand _organizationEnableCollectionEnhancementsCommand; private readonly IProviderRepository _providerRepository; private readonly IProviderBillingService _providerBillingService; private readonly IDataProtectorTokenFactory _orgDeleteTokenDataFactory; @@ -74,7 +72,6 @@ public class OrganizationsController : Controller IFeatureService featureService, GlobalSettings globalSettings, IPushNotificationService pushNotificationService, - IOrganizationEnableCollectionEnhancementsCommand organizationEnableCollectionEnhancementsCommand, IProviderRepository providerRepository, IProviderBillingService providerBillingService, IDataProtectorTokenFactory orgDeleteTokenDataFactory) @@ -94,7 +91,6 @@ public class OrganizationsController : Controller _featureService = featureService; _globalSettings = globalSettings; _pushNotificationService = pushNotificationService; - _organizationEnableCollectionEnhancementsCommand = organizationEnableCollectionEnhancementsCommand; _providerRepository = providerRepository; _providerBillingService = providerBillingService; _orgDeleteTokenDataFactory = orgDeleteTokenDataFactory; @@ -558,38 +554,4 @@ public class OrganizationsController : Controller await _organizationService.UpdateAsync(model.ToOrganization(organization), eventType: EventType.Organization_CollectionManagement_Updated); return new OrganizationResponseModel(organization); } - - /// - /// Migrates user, collection, and group data to the new Flexible Collections permissions scheme, - /// then sets organization.FlexibleCollections to true to enable these new features for the organization. - /// This is irreversible. - /// - /// - /// - [HttpPost("{id}/enable-collection-enhancements")] - [RequireFeature(FeatureFlagKeys.FlexibleCollectionsMigration)] - public async Task EnableCollectionEnhancements(Guid id) - { - if (!await _currentContext.OrganizationOwner(id)) - { - throw new NotFoundException(); - } - - var organization = await _organizationRepository.GetByIdAsync(id); - if (organization == null) - { - throw new NotFoundException(); - } - - await _organizationEnableCollectionEnhancementsCommand.EnableCollectionEnhancements(organization); - - // Force a vault sync for all owners and admins of the organization so that changes show immediately - // Custom users are intentionally not handled as they are likely to be less impacted and we want to limit simultaneous syncs - var orgUsers = await _organizationUserRepository.GetManyByOrganizationAsync(id, null); - await Task.WhenAll(orgUsers - .Where(ou => ou.UserId.HasValue && - ou.Status == OrganizationUserStatusType.Confirmed && - ou.Type is OrganizationUserType.Admin or OrganizationUserType.Owner) - .Select(ou => _pushNotificationService.PushSyncOrganizationsAsync(ou.UserId.Value))); - } } diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationCollectionEnhancements/Interfaces/IOrganizationEnableCollectionEnhancementsCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationCollectionEnhancements/Interfaces/IOrganizationEnableCollectionEnhancementsCommand.cs deleted file mode 100644 index 58a639c745..0000000000 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationCollectionEnhancements/Interfaces/IOrganizationEnableCollectionEnhancementsCommand.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Bit.Core.AdminConsole.Entities; - -namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationCollectionEnhancements.Interfaces; - -/// -/// Enable collection enhancements for an organization. -/// This command will be deprecated once all organizations have collection enhancements enabled. -/// -public interface IOrganizationEnableCollectionEnhancementsCommand -{ - Task EnableCollectionEnhancements(Organization organization); -} diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationCollectionEnhancements/OrganizationEnableCollectionEnhancementsCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationCollectionEnhancements/OrganizationEnableCollectionEnhancementsCommand.cs deleted file mode 100644 index 80f2a935ca..0000000000 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationCollectionEnhancements/OrganizationEnableCollectionEnhancementsCommand.cs +++ /dev/null @@ -1,113 +0,0 @@ -using System.Text.Json; -using Bit.Core.AdminConsole.Entities; -using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationCollectionEnhancements.Interfaces; -using Bit.Core.AdminConsole.Repositories; -using Bit.Core.Enums; -using Bit.Core.Exceptions; -using Bit.Core.Repositories; -using Bit.Core.Services; -using Microsoft.Extensions.Logging; - -namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationCollectionEnhancements; - -public class OrganizationEnableCollectionEnhancementsCommand : IOrganizationEnableCollectionEnhancementsCommand -{ - private readonly ICollectionRepository _collectionRepository; - private readonly IGroupRepository _groupRepository; - private readonly IOrganizationRepository _organizationRepository; - private readonly IOrganizationUserRepository _organizationUserRepository; - private readonly IOrganizationService _organizationService; - private readonly ILogger _logger; - - public OrganizationEnableCollectionEnhancementsCommand(ICollectionRepository collectionRepository, - IGroupRepository groupRepository, - IOrganizationRepository organizationRepository, - IOrganizationUserRepository organizationUserRepository, - IOrganizationService organizationService, - ILogger logger) - { - _collectionRepository = collectionRepository; - _groupRepository = groupRepository; - _organizationRepository = organizationRepository; - _organizationUserRepository = organizationUserRepository; - _organizationService = organizationService; - _logger = logger; - } - - public async Task EnableCollectionEnhancements(Organization organization) - { - if (organization.FlexibleCollections) - { - throw new BadRequestException("Organization has already been migrated to the new collection enhancements"); - } - - // Log the Organization data that will change when the migration is complete - await LogPreMigrationDataAsync(organization.Id); - - // Run the data migration script - await _organizationRepository.EnableCollectionEnhancements(organization.Id); - - organization.FlexibleCollections = true; - await _organizationService.ReplaceAndUpdateCacheAsync(organization); - } - - /// - /// This method logs the data that will be migrated to the new collection enhancements so that it can be restored if needed - /// - /// - private async Task LogPreMigrationDataAsync(Guid organizationId) - { - var groups = await _groupRepository.GetManyByOrganizationIdAsync(organizationId); - // Grab Group Ids that have AccessAll enabled as it will be removed in the data migration - var groupIdsWithAccessAllEnabled = groups - .Where(g => g.AccessAll) - .Select(g => g.Id) - .ToList(); - - var organizationUsers = await _organizationUserRepository.GetManyByOrganizationAsync(organizationId, type: null); - // Grab OrganizationUser Ids that have AccessAll enabled as it will be removed in the data migration - var organizationUserIdsWithAccessAllEnabled = organizationUsers - .Where(ou => ou.AccessAll) - .Select(ou => ou.Id) - .ToList(); - // Grab OrganizationUser Ids of Manager users as that will be downgraded to User in the data migration - var migratedManagers = organizationUsers - .Where(ou => ou.Type == OrganizationUserType.Manager) - .Select(ou => ou.Id) - .ToList(); - - var usersEligibleToManageCollections = organizationUsers - .Where(ou => - ou.Type == OrganizationUserType.Manager || - (ou.Type == OrganizationUserType.Custom && - !string.IsNullOrEmpty(ou.Permissions) && - ou.GetPermissions().EditAssignedCollections) - ) - .Select(ou => ou.Id) - .ToList(); - var collectionUsers = await _collectionRepository.GetManyByOrganizationIdWithAccessAsync(organizationId); - // Grab CollectionUser permissions that will change in the data migration - var collectionUsersData = collectionUsers.SelectMany(tuple => - tuple.Item2.Users.Select(user => - new - { - CollectionId = tuple.Item1.Id, - OrganizationUserId = user.Id, - user.ReadOnly, - user.HidePasswords - })) - .Where(cud => usersEligibleToManageCollections.Any(ou => ou == cud.OrganizationUserId)) - .ToList(); - - var logObject = new - { - OrganizationId = organizationId, - GroupAccessAll = groupIdsWithAccessAllEnabled, - UserAccessAll = organizationUserIdsWithAccessAllEnabled, - MigratedManagers = migratedManagers, - CollectionUsers = collectionUsersData - }; - - _logger.LogWarning("Flexible Collections data migration started. Backup data: {LogObject}", JsonSerializer.Serialize(logObject)); - } -} diff --git a/src/Core/AdminConsole/Repositories/IOrganizationRepository.cs b/src/Core/AdminConsole/Repositories/IOrganizationRepository.cs index 36a445442b..4598a11fb9 100644 --- a/src/Core/AdminConsole/Repositories/IOrganizationRepository.cs +++ b/src/Core/AdminConsole/Repositories/IOrganizationRepository.cs @@ -15,5 +15,4 @@ public interface IOrganizationRepository : IRepository Task GetSelfHostedOrganizationDetailsById(Guid id); Task> SearchUnassignedToProviderAsync(string name, string ownerEmail, int skip, int take); Task> GetOwnerEmailAddressesById(Guid organizationId); - Task EnableCollectionEnhancements(Guid organizationId); } diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 6804a9a223..fc1d69ffb0 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -114,11 +114,6 @@ public static class FeatureFlagKeys public const string ItemShare = "item-share"; public const string KeyRotationImprovements = "key-rotation-improvements"; public const string DuoRedirect = "duo-redirect"; - /// - /// Exposes a migration button in the web vault which allows users to migrate an existing organization to - /// flexible collections - /// - public const string FlexibleCollectionsMigration = "flexible-collections-migration"; public const string PM5766AutomaticTax = "PM-5766-automatic-tax"; public const string PM5864DollarThreshold = "PM-5864-dollar-threshold"; public const string ShowPaymentMethodWarningBanners = "show-payment-method-warning-banners"; diff --git a/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs index 3cc422decf..daf5caf000 100644 --- a/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs +++ b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs @@ -4,8 +4,6 @@ using Bit.Core.AdminConsole.OrganizationFeatures.Groups; using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationApiKeys; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationApiKeys.Interfaces; -using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationCollectionEnhancements; -using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationCollectionEnhancements.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationConnections; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationConnections.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains; @@ -52,7 +50,6 @@ public static class OrganizationServiceCollectionExtensions services.AddOrganizationUserCommands(); services.AddOrganizationUserCommandsQueries(); services.AddBaseOrganizationSubscriptionCommandsQueries(); - services.AddOrganizationCollectionEnhancementsCommands(); } private static void AddOrganizationConnectionCommands(this IServiceCollection services) @@ -148,11 +145,6 @@ public static class OrganizationServiceCollectionExtensions services.AddScoped(); } - private static void AddOrganizationCollectionEnhancementsCommands(this IServiceCollection services) - { - services.AddScoped(); - } - private static void AddTokenizers(this IServiceCollection services) { services.AddSingleton>(serviceProvider => diff --git a/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationRepository.cs b/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationRepository.cs index 9080e17c3e..f4c771adec 100644 --- a/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationRepository.cs +++ b/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationRepository.cs @@ -169,16 +169,4 @@ public class OrganizationRepository : Repository, IOrganizat new { OrganizationId = organizationId }, commandType: CommandType.StoredProcedure); } - - public async Task EnableCollectionEnhancements(Guid organizationId) - { - using (var connection = new SqlConnection(ConnectionString)) - { - await connection.ExecuteAsync( - "[dbo].[Organization_EnableCollectionEnhancements]", - new { OrganizationId = organizationId }, - commandType: CommandType.StoredProcedure, - commandTimeout: 180); - } - } } diff --git a/src/Sql/dbo/Stored Procedures/Organization_EnableCollectionEnhancements.sql b/src/Sql/dbo/Stored Procedures/Organization_EnableCollectionEnhancements.sql deleted file mode 100644 index c69fa39772..0000000000 --- a/src/Sql/dbo/Stored Procedures/Organization_EnableCollectionEnhancements.sql +++ /dev/null @@ -1,155 +0,0 @@ -CREATE PROCEDURE [dbo].[Organization_EnableCollectionEnhancements] - @OrganizationId UNIQUEIDENTIFIER -AS -BEGIN - SET NOCOUNT ON - - -- Step 1: AccessAll migration for Groups - -- Create a temporary table to store the groups with AccessAll = 1 - SELECT [Id] AS [GroupId], [OrganizationId] - INTO #TempGroupsAccessAll - FROM [dbo].[Group] - WHERE [OrganizationId] = @OrganizationId - AND [AccessAll] = 1; - - -- Step 2: AccessAll migration for OrganizationUsers - -- Create a temporary table to store the OrganizationUsers with AccessAll = 1 - SELECT [Id] AS [OrganizationUserId], [OrganizationId] - INTO #TempUsersAccessAll - FROM [dbo].[OrganizationUser] - WHERE [OrganizationId] = @OrganizationId - AND [AccessAll] = 1; - - -- Step 3: For all OrganizationUsers with Manager role or 'EditAssignedCollections' permission update their existing CollectionUser rows and insert new rows with [Manage] = 1 - -- and finally update all OrganizationUsers with Manager role to User role - -- Create a temporary table to store the OrganizationUsers with Manager role or 'EditAssignedCollections' permission - SELECT ou.[Id] AS [OrganizationUserId], - CASE WHEN ou.[Type] = 3 THEN 1 ELSE 0 END AS [IsManager] - INTO #TempUserManagers - FROM [dbo].[OrganizationUser] ou - WHERE ou.[OrganizationId] = @OrganizationId - AND (ou.[Type] = 3 OR (ou.[Permissions] IS NOT NULL - AND ISJSON(ou.[Permissions]) > 0 AND JSON_VALUE(ou.[Permissions], '$.editAssignedCollections') = 'true')); - - -- Step 4: Bump AccountRevisionDate for all OrganizationUsers updated in the previous steps - -- Combine and union the distinct OrganizationUserIds from all steps into a single variable - DECLARE @OrgUsersToBump [dbo].[GuidIdArray] - INSERT INTO @OrgUsersToBump - SELECT DISTINCT [OrganizationUserId] AS Id - FROM ( - -- Step 1 - SELECT GU.[OrganizationUserId] - FROM [dbo].[GroupUser] GU - INNER JOIN #TempGroupsAccessAll TG ON GU.[GroupId] = TG.[GroupId] - - UNION - - -- Step 2 - SELECT [OrganizationUserId] - FROM #TempUsersAccessAll - - UNION - - -- Step 3 - SELECT [OrganizationUserId] - FROM #TempUserManagers - ) AS CombinedOrgUsers; - - BEGIN TRY - BEGIN TRANSACTION; - -- Step 1 - -- Update existing rows in [dbo].[CollectionGroup] - UPDATE CG - SET - CG.[ReadOnly] = 0, - CG.[HidePasswords] = 0, - CG.[Manage] = 0 - FROM [dbo].[CollectionGroup] CG - INNER JOIN [dbo].[Collection] C ON CG.[CollectionId] = C.[Id] - INNER JOIN #TempGroupsAccessAll TG ON CG.[GroupId] = TG.[GroupId] - WHERE C.[OrganizationId] = TG.[OrganizationId]; - - -- Insert new rows into [dbo].[CollectionGroup] - INSERT INTO [dbo].[CollectionGroup] ([CollectionId], [GroupId], [ReadOnly], [HidePasswords], [Manage]) - SELECT C.[Id], TG.[GroupId], 0, 0, 0 - FROM [dbo].[Collection] C - INNER JOIN #TempGroupsAccessAll TG ON C.[OrganizationId] = TG.[OrganizationId] - LEFT JOIN [dbo].[CollectionGroup] CG ON CG.[CollectionId] = C.[Id] AND CG.[GroupId] = TG.[GroupId] - WHERE CG.[CollectionId] IS NULL; - - -- Update Group to clear AccessAll flag and update RevisionDate - UPDATE G - SET [AccessAll] = 0, [RevisionDate] = GETUTCDATE() - FROM [dbo].[Group] G - INNER JOIN #TempGroupsAccessAll TG ON G.[Id] = TG.[GroupId]; - - -- Step 2 - -- Update existing rows in [dbo].[CollectionUser] - UPDATE target - SET - target.[ReadOnly] = 0, - target.[HidePasswords] = 0, - target.[Manage] = 0 - FROM [dbo].[CollectionUser] AS target - INNER JOIN [dbo].[Collection] AS C ON target.[CollectionId] = C.[Id] - INNER JOIN #TempUsersAccessAll AS TU ON C.[OrganizationId] = TU.[OrganizationId] AND target.[OrganizationUserId] = TU.[OrganizationUserId]; - - -- Insert new rows into [dbo].[CollectionUser] - INSERT INTO [dbo].[CollectionUser] ([CollectionId], [OrganizationUserId], [ReadOnly], [HidePasswords], [Manage]) - SELECT C.[Id] AS [CollectionId], TU.[OrganizationUserId], 0, 0, 0 - FROM [dbo].[Collection] C - INNER JOIN #TempUsersAccessAll TU ON C.[OrganizationId] = TU.[OrganizationId] - LEFT JOIN [dbo].[CollectionUser] target - ON target.[CollectionId] = C.[Id] AND target.[OrganizationUserId] = TU.[OrganizationUserId] - WHERE target.[CollectionId] IS NULL; - - -- Update OrganizationUser to clear AccessAll flag - UPDATE OU - SET [AccessAll] = 0, [RevisionDate] = GETUTCDATE() - FROM [dbo].[OrganizationUser] OU - INNER JOIN #TempUsersAccessAll TU ON OU.[Id] = TU.[OrganizationUserId]; - - -- Step 3 - -- Update [dbo].[CollectionUser] with [Manage] = 1 using the temporary table - UPDATE CU - SET CU.[ReadOnly] = 0, - CU.[HidePasswords] = 0, - CU.[Manage] = 1 - FROM [dbo].[CollectionUser] CU - INNER JOIN #TempUserManagers TUM ON CU.[OrganizationUserId] = TUM.[OrganizationUserId]; - - -- Insert rows to [dbo].[CollectionUser] with [Manage] = 1 using the temporary table - -- This is for orgUsers who are Managers / EditAssignedCollections but have access via a group - -- We cannot give the whole group Manage permissions so we have to give them a direct assignment - INSERT INTO [dbo].[CollectionUser] ([CollectionId], [OrganizationUserId], [ReadOnly], [HidePasswords], [Manage]) - SELECT DISTINCT CG.[CollectionId], TUM.[OrganizationUserId], 0, 0, 1 - FROM [dbo].[CollectionGroup] CG - INNER JOIN [dbo].[GroupUser] GU ON CG.[GroupId] = GU.[GroupId] - INNER JOIN #TempUserManagers TUM ON GU.[OrganizationUserId] = TUM.[OrganizationUserId] - WHERE NOT EXISTS ( - SELECT 1 FROM [dbo].[CollectionUser] CU - WHERE CU.[CollectionId] = CG.[CollectionId] AND CU.[OrganizationUserId] = TUM.[OrganizationUserId] - ); - - -- Update [dbo].[OrganizationUser] to migrate all OrganizationUsers with Manager role to User role - UPDATE OU - SET OU.[Type] = 2, OU.[RevisionDate] = GETUTCDATE() -- User - FROM [dbo].[OrganizationUser] OU - INNER JOIN #TempUserManagers TUM ON ou.[Id] = TUM.[OrganizationUserId] - WHERE TUM.[IsManager] = 1; -- Filter for Managers - - -- Step 4 - -- Execute User_BumpAccountRevisionDateByOrganizationUserIds for the distinct OrganizationUserIds - EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationUserIds] @OrgUsersToBump; - COMMIT TRANSACTION; - END TRY - BEGIN CATCH - ROLLBACK TRANSACTION; - THROW; - END CATCH; - - -- Drop the temporary table - DROP TABLE #TempGroupsAccessAll; - DROP TABLE #TempUsersAccessAll; - DROP TABLE #TempUserManagers; -END diff --git a/test/Api.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs b/test/Api.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs index abc0655476..c772104b33 100644 --- a/test/Api.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs +++ b/test/Api.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs @@ -7,7 +7,6 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Models.Business.Tokenables; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationApiKeys.Interfaces; -using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationCollectionEnhancements.Interfaces; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Auth.Entities; using Bit.Core.Auth.Enums; @@ -46,7 +45,6 @@ public class OrganizationsControllerTests : IDisposable private readonly ICreateOrganizationApiKeyCommand _createOrganizationApiKeyCommand; private readonly IFeatureService _featureService; private readonly IPushNotificationService _pushNotificationService; - private readonly IOrganizationEnableCollectionEnhancementsCommand _organizationEnableCollectionEnhancementsCommand; private readonly IProviderRepository _providerRepository; private readonly IProviderBillingService _providerBillingService; private readonly IDataProtectorTokenFactory _orgDeleteTokenDataFactory; @@ -70,7 +68,6 @@ public class OrganizationsControllerTests : IDisposable _createOrganizationApiKeyCommand = Substitute.For(); _featureService = Substitute.For(); _pushNotificationService = Substitute.For(); - _organizationEnableCollectionEnhancementsCommand = Substitute.For(); _providerRepository = Substitute.For(); _providerBillingService = Substitute.For(); _orgDeleteTokenDataFactory = Substitute.For>(); @@ -91,7 +88,6 @@ public class OrganizationsControllerTests : IDisposable _featureService, _globalSettings, _pushNotificationService, - _organizationEnableCollectionEnhancementsCommand, _providerRepository, _providerBillingService, _orgDeleteTokenDataFactory); @@ -162,48 +158,6 @@ public class OrganizationsControllerTests : IDisposable await _organizationService.Received(1).DeleteUserAsync(orgId, user.Id); } - [Theory, AutoData] - public async Task EnableCollectionEnhancements_Success(Organization organization) - { - organization.FlexibleCollections = false; - var admin = new OrganizationUser { UserId = Guid.NewGuid(), Type = OrganizationUserType.Admin, Status = OrganizationUserStatusType.Confirmed }; - var owner = new OrganizationUser { UserId = Guid.NewGuid(), Type = OrganizationUserType.Owner, Status = OrganizationUserStatusType.Confirmed }; - var user = new OrganizationUser { UserId = Guid.NewGuid(), Type = OrganizationUserType.User, Status = OrganizationUserStatusType.Confirmed }; - var invited = new OrganizationUser - { - UserId = null, - Type = OrganizationUserType.Admin, - Email = "invited@example.com", - Status = OrganizationUserStatusType.Invited - }; - var orgUsers = new List { admin, owner, user, invited }; - - _currentContext.OrganizationOwner(organization.Id).Returns(true); - _organizationRepository.GetByIdAsync(organization.Id).Returns(organization); - _organizationUserRepository.GetManyByOrganizationAsync(organization.Id, null).Returns(orgUsers); - - await _sut.EnableCollectionEnhancements(organization.Id); - - await _organizationEnableCollectionEnhancementsCommand.Received(1).EnableCollectionEnhancements(organization); - await _pushNotificationService.Received(1).PushSyncOrganizationsAsync(admin.UserId.Value); - await _pushNotificationService.Received(1).PushSyncOrganizationsAsync(owner.UserId.Value); - await _pushNotificationService.DidNotReceive().PushSyncOrganizationsAsync(user.UserId.Value); - // Invited orgUser does not have a UserId we can use to assert here, but sut will throw if that null isn't handled - } - - [Theory, AutoData] - public async Task EnableCollectionEnhancements_WhenNotOwner_Throws(Organization organization) - { - organization.FlexibleCollections = false; - _currentContext.OrganizationOwner(organization.Id).Returns(false); - _organizationRepository.GetByIdAsync(organization.Id).Returns(organization); - - await Assert.ThrowsAsync(async () => await _sut.EnableCollectionEnhancements(organization.Id)); - - await _organizationEnableCollectionEnhancementsCommand.DidNotReceiveWithAnyArgs().EnableCollectionEnhancements(Arg.Any()); - await _pushNotificationService.DidNotReceiveWithAnyArgs().PushSyncOrganizationsAsync(Arg.Any()); - } - [Theory, AutoData] public async Task Delete_OrganizationIsConsolidatedBillingClient_ScalesProvidersSeats( Provider provider, diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationCollectionEnhancements/OrganizationEnableCollectionEnhancementsCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationCollectionEnhancements/OrganizationEnableCollectionEnhancementsCommandTests.cs deleted file mode 100644 index c63c100adc..0000000000 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationCollectionEnhancements/OrganizationEnableCollectionEnhancementsCommandTests.cs +++ /dev/null @@ -1,46 +0,0 @@ -using Bit.Core.AdminConsole.Entities; -using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationCollectionEnhancements; -using Bit.Core.Exceptions; -using Bit.Core.Repositories; -using Bit.Core.Services; -using Bit.Test.Common.AutoFixture; -using Bit.Test.Common.AutoFixture.Attributes; -using NSubstitute; -using Xunit; - -namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationCollectionEnhancements; - -[SutProviderCustomize] -public class OrganizationEnableCollectionEnhancementsCommandTests -{ - [Theory] - [BitAutoData] - public async Task EnableCollectionEnhancements_Success( - SutProvider sutProvider, - Organization organization) - { - organization.FlexibleCollections = false; - - await sutProvider.Sut.EnableCollectionEnhancements(organization); - - await sutProvider.GetDependency().Received(1).EnableCollectionEnhancements(organization.Id); - await sutProvider.GetDependency().Received(1).ReplaceAndUpdateCacheAsync( - Arg.Is(o => - o.Id == organization.Id && - o.FlexibleCollections)); - } - - [Theory] - [BitAutoData] - public async Task EnableCollectionEnhancements_WhenAlreadyMigrated_Throws( - SutProvider sutProvider, - Organization organization) - { - organization.FlexibleCollections = true; - - var exception = await Assert.ThrowsAsync(async () => await sutProvider.Sut.EnableCollectionEnhancements(organization)); - Assert.Contains("has already been migrated", exception.Message); - - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().EnableCollectionEnhancements(Arg.Any()); - } -} diff --git a/test/Infrastructure.IntegrationTest/AdminConsole/MssqlDatabaseDataAttribute.cs b/test/Infrastructure.IntegrationTest/AdminConsole/MssqlDatabaseDataAttribute.cs deleted file mode 100644 index 01180ca312..0000000000 --- a/test/Infrastructure.IntegrationTest/AdminConsole/MssqlDatabaseDataAttribute.cs +++ /dev/null @@ -1,54 +0,0 @@ -using Bit.Core.Enums; -using Bit.Core.Settings; -using Bit.Infrastructure.Dapper; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; - -namespace Bit.Infrastructure.IntegrationTest.AdminConsole; - -/// -/// Used to test the mssql database only. -/// This is generally NOT what you want and is only used for Flexible Collections which has an opt-in method specific -/// to cloud (and therefore mssql) only. This should be deleted during cleanup so that others don't use it. -/// -internal class MssqlDatabaseDataAttribute : DatabaseDataAttribute -{ - protected override IEnumerable GetDatabaseProviders(IConfiguration config) - { - var configureLogging = (ILoggingBuilder builder) => - { - if (!config.GetValue("Quiet")) - { - builder.AddConfiguration(config); - builder.AddConsole(); - builder.AddDebug(); - } - }; - - var databases = config.GetDatabases(); - - foreach (var database in databases) - { - if (database.Type == SupportedDatabaseProviders.SqlServer && !database.UseEf) - { - var dapperSqlServerCollection = new ServiceCollection(); - dapperSqlServerCollection.AddLogging(configureLogging); - dapperSqlServerCollection.AddDapperRepositories(SelfHosted); - var globalSettings = new GlobalSettings - { - DatabaseProvider = "sqlServer", - SqlServer = new GlobalSettings.SqlSettings - { - ConnectionString = database.ConnectionString, - }, - }; - dapperSqlServerCollection.AddSingleton(globalSettings); - dapperSqlServerCollection.AddSingleton(globalSettings); - dapperSqlServerCollection.AddSingleton(database); - dapperSqlServerCollection.AddDataProtection(); - yield return dapperSqlServerCollection.BuildServiceProvider(); - } - } - } -} diff --git a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationEnableCollectionEnhancementTests.cs b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationEnableCollectionEnhancementTests.cs deleted file mode 100644 index 27f7419255..0000000000 --- a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationEnableCollectionEnhancementTests.cs +++ /dev/null @@ -1,611 +0,0 @@ -using Bit.Core.AdminConsole.Entities; -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; - -public class OrganizationEnableCollectionEnhancementTests -{ - [DatabaseTheory, MssqlDatabaseData] - public async Task Migrate_User_WithAccessAll_GivesCanEditAccessToAllCollections( - IUserRepository userRepository, - IOrganizationRepository organizationRepository, - IOrganizationUserRepository organizationUserRepository, - ICollectionRepository collectionRepository) - { - var user = await CreateUser(userRepository); - var organization = await CreateOrganization(organizationRepository); - var orgUser = await CreateOrganizationUser(user, organization, OrganizationUserType.User, accessAll: true, organizationUserRepository); - var collection1 = await CreateCollection(organization, collectionRepository); - var collection2 = await CreateCollection(organization, collectionRepository); - var collection3 = await CreateCollection(organization, collectionRepository); - - await organizationRepository.EnableCollectionEnhancements(organization.Id); - - var (updatedOrgUser, collectionAccessSelections) = await organizationUserRepository.GetDetailsByIdWithCollectionsAsync(orgUser.Id); - - Assert.False(updatedOrgUser.AccessAll); - - Assert.Equal(3, collectionAccessSelections.Count); - Assert.Contains(collectionAccessSelections, cas => - cas.Id == collection1.Id && - CanEdit(cas)); - Assert.Contains(collectionAccessSelections, cas => - cas.Id == collection2.Id && - CanEdit(cas)); - Assert.Contains(collectionAccessSelections, cas => - cas.Id == collection3.Id && - CanEdit(cas)); - } - - [DatabaseTheory, MssqlDatabaseData] - public async Task Migrate_Group_WithAccessAll_GivesCanEditAccessToAllCollections( - IGroupRepository groupRepository, - IOrganizationRepository organizationRepository, - ICollectionRepository collectionRepository) - { - var organization = await CreateOrganization(organizationRepository); - var group = await CreateGroup(organization, accessAll: true, groupRepository); - var collection1 = await CreateCollection(organization, collectionRepository); - var collection2 = await CreateCollection(organization, collectionRepository); - var collection3 = await CreateCollection(organization, collectionRepository); - - await organizationRepository.EnableCollectionEnhancements(organization.Id); - - var (updatedGroup, collectionAccessSelections) = await groupRepository.GetByIdWithCollectionsAsync(group.Id); - - Assert.False(updatedGroup.AccessAll); - - Assert.Equal(3, collectionAccessSelections.Count); - Assert.Contains(collectionAccessSelections, cas => - cas.Id == collection1.Id && - CanEdit(cas)); - Assert.Contains(collectionAccessSelections, cas => - cas.Id == collection2.Id && - CanEdit(cas)); - Assert.Contains(collectionAccessSelections, cas => - cas.Id == collection3.Id && - CanEdit(cas)); - } - - [DatabaseTheory, MssqlDatabaseData] - public async Task Migrate_Manager_WithAccessAll_GivesCanManageAccessToAllCollections( - IUserRepository userRepository, - IOrganizationRepository organizationRepository, - IOrganizationUserRepository organizationUserRepository, - ICollectionRepository collectionRepository) - { - var user = await CreateUser(userRepository); - var organization = await CreateOrganization(organizationRepository); - var orgUser = await CreateOrganizationUser(user, organization, OrganizationUserType.Manager, accessAll: true, organizationUserRepository); - var collection1 = await CreateCollection(organization, collectionRepository); - var collection2 = await CreateCollection(organization, collectionRepository); - var collection3 = await CreateCollection(organization, collectionRepository); - - await organizationRepository.EnableCollectionEnhancements(organization.Id); - - var (updatedOrgUser, collectionAccessSelections) = await organizationUserRepository.GetDetailsByIdWithCollectionsAsync(orgUser.Id); - - Assert.False(updatedOrgUser.AccessAll); - Assert.Equal(OrganizationUserType.User, updatedOrgUser.Type); - - Assert.Equal(3, collectionAccessSelections.Count); - Assert.Contains(collectionAccessSelections, cas => - cas.Id == collection1.Id && - CanManage(cas)); - Assert.Contains(collectionAccessSelections, cas => - cas.Id == collection2.Id && - CanManage(cas)); - Assert.Contains(collectionAccessSelections, cas => - cas.Id == collection3.Id && - CanManage(cas)); - } - - [DatabaseTheory, MssqlDatabaseData] - public async Task Migrate_Manager_WithoutAccessAll_GivesCanManageAccessToAssignedCollections( - IUserRepository userRepository, - IOrganizationRepository organizationRepository, - IOrganizationUserRepository organizationUserRepository, - ICollectionRepository collectionRepository) - { - var user = await CreateUser(userRepository); - var organization = await CreateOrganization(organizationRepository); - var orgUser = await CreateOrganizationUser(user, organization, OrganizationUserType.Manager, accessAll: false, organizationUserRepository); - var collection1 = await CreateCollection(organization, collectionRepository, null, [new CollectionAccessSelection { Id = orgUser.Id, HidePasswords = true, ReadOnly = false, Manage = false }]); - var collection2 = await CreateCollection(organization, collectionRepository, null, [new CollectionAccessSelection { Id = orgUser.Id, HidePasswords = false, ReadOnly = false, Manage = false }]); - var collection3 = await CreateCollection(organization, collectionRepository); // no access - - await organizationRepository.EnableCollectionEnhancements(organization.Id); - - var (updatedOrgUser, collectionAccessSelections) = await organizationUserRepository.GetDetailsByIdWithCollectionsAsync(orgUser.Id); - - Assert.Equal(OrganizationUserType.User, updatedOrgUser.Type); - - Assert.Equal(2, collectionAccessSelections.Count); - Assert.Contains(collectionAccessSelections, cas => - cas.Id == collection1.Id && - CanManage(cas)); - Assert.Contains(collectionAccessSelections, cas => - cas.Id == collection2.Id && - CanManage(cas)); - Assert.DoesNotContain(collectionAccessSelections, cas => - cas.Id == collection3.Id); - } - - [DatabaseTheory, MssqlDatabaseData] - public async Task Migrate_Manager_WithoutAccessAll_GivesCanManageAccess_ToGroupAssignedCollections( - IUserRepository userRepository, - IOrganizationRepository organizationRepository, - IOrganizationUserRepository organizationUserRepository, - ICollectionRepository collectionRepository, - IGroupRepository groupRepository) - { - var user = await CreateUser(userRepository); - var organization = await CreateOrganization(organizationRepository); - var orgUser = await CreateOrganizationUser(user, organization, OrganizationUserType.Manager, accessAll: false, organizationUserRepository); - var group = await CreateGroup(organization, accessAll: false, groupRepository, orgUser); - - var collection1 = await CreateCollection(organization, collectionRepository, new[] { new CollectionAccessSelection { Id = group.Id, HidePasswords = false, Manage = false, ReadOnly = false } }); - var collection2 = await CreateCollection(organization, collectionRepository, new[] { new CollectionAccessSelection { Id = group.Id, HidePasswords = false, Manage = false, ReadOnly = false } }); - var collection3 = await CreateCollection(organization, collectionRepository); // no access - - await organizationRepository.EnableCollectionEnhancements(organization.Id); - - var (updatedOrgUser, updatedUserAccess) = await organizationUserRepository.GetDetailsByIdWithCollectionsAsync(orgUser.Id); - - // Assert: orgUser should be downgraded from Manager to User - // and given Can Manage permissions over all group assigned collections - Assert.Equal(OrganizationUserType.User, updatedOrgUser.Type); - Assert.Equal(2, updatedUserAccess.Count); - Assert.Contains(updatedUserAccess, cas => - cas.Id == collection1.Id && - CanManage(cas)); - Assert.Contains(updatedUserAccess, cas => - cas.Id == collection2.Id && - CanManage(cas)); - Assert.DoesNotContain(updatedUserAccess, cas => - cas.Id == collection3.Id); - - // Assert: group should only have Can Edit permissions (making sure no side-effects from the Manager migration) - var (updatedGroup, updatedGroupAccess) = await groupRepository.GetByIdWithCollectionsAsync(group.Id); - Assert.Equal(2, updatedGroupAccess.Count); - Assert.Contains(updatedGroupAccess, cas => - cas.Id == collection1.Id && - CanEdit(cas)); - Assert.Contains(updatedGroupAccess, cas => - cas.Id == collection2.Id && - CanEdit(cas)); - Assert.DoesNotContain(updatedGroupAccess, cas => - cas.Id == collection3.Id); - } - - [DatabaseTheory, MssqlDatabaseData] - public async Task Migrate_Manager_WithoutAccessAll_InGroupWithAccessAll_GivesCanManageAccessToAllCollections( - IUserRepository userRepository, - IGroupRepository groupRepository, - IOrganizationRepository organizationRepository, - IOrganizationUserRepository organizationUserRepository, - ICollectionRepository collectionRepository) - { - var user = await CreateUser(userRepository); - var organization = await CreateOrganization(organizationRepository); - var orgUser = await CreateOrganizationUser(user, organization, OrganizationUserType.Manager, accessAll: false, organizationUserRepository); - - // Use 2 groups to test for overlapping access - var group1 = await CreateGroup(organization, accessAll: true, groupRepository, orgUser); - var group2 = await CreateGroup(organization, accessAll: true, groupRepository, orgUser); - - var collection1 = await CreateCollection(organization, collectionRepository); - var collection2 = await CreateCollection(organization, collectionRepository); - var collection3 = await CreateCollection(organization, collectionRepository); - - await organizationRepository.EnableCollectionEnhancements(organization.Id); - - var (updatedOrgUser, collectionAccessSelections) = await organizationUserRepository.GetDetailsByIdWithCollectionsAsync(orgUser.Id); - - Assert.Equal(OrganizationUserType.User, updatedOrgUser.Type); - - // OrgUser has direct Can Manage access to all collections - Assert.Equal(3, collectionAccessSelections.Count); - Assert.Contains(collectionAccessSelections, cas => - cas.Id == collection1.Id && - CanManage(cas)); - Assert.Contains(collectionAccessSelections, cas => - cas.Id == collection2.Id && - CanManage(cas)); - Assert.Contains(collectionAccessSelections, cas => - cas.Id == collection3.Id && - CanManage(cas)); - - // Assert: group should only have Can Edit permissions (making sure no side-effects from the Manager migration) - var (updatedGroup1, updatedGroupAccess1) = await groupRepository.GetByIdWithCollectionsAsync(group1.Id); - Assert.Equal(3, updatedGroupAccess1.Count); - Assert.Contains(updatedGroupAccess1, cas => - cas.Id == collection1.Id && - CanEdit(cas)); - Assert.Contains(updatedGroupAccess1, cas => - cas.Id == collection2.Id && - CanEdit(cas)); - Assert.Contains(updatedGroupAccess1, cas => - cas.Id == collection3.Id && - CanEdit(cas)); - - var (updatedGroup2, updatedGroupAccess2) = await groupRepository.GetByIdWithCollectionsAsync(group2.Id); - Assert.Equal(3, updatedGroupAccess2.Count); - Assert.Contains(updatedGroupAccess2, cas => - cas.Id == collection1.Id && - CanEdit(cas)); - Assert.Contains(updatedGroupAccess2, cas => - cas.Id == collection2.Id && - CanEdit(cas)); - Assert.Contains(updatedGroupAccess2, cas => - cas.Id == collection3.Id && - CanEdit(cas)); - } - - [DatabaseTheory, MssqlDatabaseData] - public async Task Migrate_CustomUser_WithEditAssignedCollections_WithAccessAll_GivesCanManageAccessToAllCollections( - IUserRepository userRepository, - IOrganizationRepository organizationRepository, - IOrganizationUserRepository organizationUserRepository, - ICollectionRepository collectionRepository) - { - var user = await CreateUser(userRepository); - var organization = await CreateOrganization(organizationRepository); - var orgUser = await CreateOrganizationUser(user, organization, OrganizationUserType.Custom, accessAll: true, - organizationUserRepository, new Permissions { EditAssignedCollections = true }); - var collection1 = await CreateCollection(organization, collectionRepository); - var collection2 = await CreateCollection(organization, collectionRepository); - var collection3 = await CreateCollection(organization, collectionRepository); - - await organizationRepository.EnableCollectionEnhancements(organization.Id); - - var (updatedOrgUser, collectionAccessSelections) = await organizationUserRepository.GetDetailsByIdWithCollectionsAsync(orgUser.Id); - - Assert.False(updatedOrgUser.AccessAll); - // Note: custom users do not have their types changed yet, this was done in code with a migration to follow - Assert.Equal(OrganizationUserType.Custom, updatedOrgUser.Type); - - Assert.Equal(3, collectionAccessSelections.Count); - Assert.Contains(collectionAccessSelections, cas => - cas.Id == collection1.Id && - CanManage(cas)); - Assert.Contains(collectionAccessSelections, cas => - cas.Id == collection2.Id && - CanManage(cas)); - Assert.Contains(collectionAccessSelections, cas => - cas.Id == collection3.Id && - CanManage(cas)); - } - - [DatabaseTheory, MssqlDatabaseData] - public async Task Migrate_CustomUser_WithEditAssignedCollections_WithoutAccessAll_GivesCanManageAccessToAssignedCollections( - IUserRepository userRepository, - IOrganizationRepository organizationRepository, - IOrganizationUserRepository organizationUserRepository, - ICollectionRepository collectionRepository) - { - var user = await CreateUser(userRepository); - var organization = await CreateOrganization(organizationRepository); - var orgUser = await CreateOrganizationUser(user, organization, OrganizationUserType.Custom, accessAll: false, - organizationUserRepository, new Permissions { EditAssignedCollections = true }); - var collection1 = await CreateCollection(organization, collectionRepository, null, [new CollectionAccessSelection { Id = orgUser.Id, HidePasswords = true, ReadOnly = false, Manage = false }]); - var collection2 = await CreateCollection(organization, collectionRepository, null, [new CollectionAccessSelection { Id = orgUser.Id, HidePasswords = false, ReadOnly = false, Manage = false }]); - var collection3 = await CreateCollection(organization, collectionRepository); // no access - - await organizationRepository.EnableCollectionEnhancements(organization.Id); - - var (updatedOrgUser, collectionAccessSelections) = await organizationUserRepository.GetDetailsByIdWithCollectionsAsync(orgUser.Id); - - Assert.Equal(OrganizationUserType.Custom, updatedOrgUser.Type); - - Assert.Equal(2, collectionAccessSelections.Count); - Assert.Contains(collectionAccessSelections, cas => - cas.Id == collection1.Id && - CanManage(cas)); - Assert.Contains(collectionAccessSelections, cas => - cas.Id == collection2.Id && - CanManage(cas)); - Assert.DoesNotContain(collectionAccessSelections, cas => - cas.Id == collection3.Id); - } - - [DatabaseTheory, MssqlDatabaseData] - public async Task Migrate_CustomUser_WithEditAssignedCollections_WithoutAccessAll_GivesCanManageAccess_ToGroupAssignedCollections( - IUserRepository userRepository, - IOrganizationRepository organizationRepository, - IOrganizationUserRepository organizationUserRepository, - ICollectionRepository collectionRepository, - IGroupRepository groupRepository) - { - var user = await CreateUser(userRepository); - var organization = await CreateOrganization(organizationRepository); - var orgUser = await CreateOrganizationUser(user, organization, OrganizationUserType.Custom, accessAll: false, - organizationUserRepository, new Permissions { EditAssignedCollections = true }); - var group = await CreateGroup(organization, accessAll: false, groupRepository, orgUser); - - var collection1 = await CreateCollection(organization, collectionRepository, new[] { new CollectionAccessSelection { Id = group.Id, HidePasswords = false, Manage = false, ReadOnly = false } }); - var collection2 = await CreateCollection(organization, collectionRepository, new[] { new CollectionAccessSelection { Id = group.Id, HidePasswords = false, Manage = false, ReadOnly = false } }); - var collection3 = await CreateCollection(organization, collectionRepository); // no access - - await organizationRepository.EnableCollectionEnhancements(organization.Id); - - var (updatedOrgUser, updatedUserAccess) = await organizationUserRepository.GetDetailsByIdWithCollectionsAsync(orgUser.Id); - - // Assert: user should be given Can Manage permissions over all group assigned collections - Assert.Equal(OrganizationUserType.Custom, updatedOrgUser.Type); - Assert.Equal(2, updatedUserAccess.Count); - Assert.Contains(updatedUserAccess, cas => - cas.Id == collection1.Id && - CanManage(cas)); - Assert.Contains(updatedUserAccess, cas => - cas.Id == collection2.Id && - CanManage(cas)); - Assert.DoesNotContain(updatedUserAccess, cas => - cas.Id == collection3.Id); - - // Assert: group should only have Can Edit permissions (making sure no side-effects from the Manager migration) - var (updatedGroup, updatedGroupAccess) = await groupRepository.GetByIdWithCollectionsAsync(group.Id); - Assert.Equal(2, updatedGroupAccess.Count); - Assert.Contains(updatedGroupAccess, cas => - cas.Id == collection1.Id && - CanEdit(cas)); - Assert.Contains(updatedGroupAccess, cas => - cas.Id == collection2.Id && - CanEdit(cas)); - Assert.DoesNotContain(updatedGroupAccess, cas => - cas.Id == collection3.Id); - } - - [DatabaseTheory, MssqlDatabaseData] - public async Task Migrate_CustomUser_WithEditAssignedCollections_WithoutAccessAll_InGroupWithAccessAll_GivesCanManageAccessToAllCollections( - IUserRepository userRepository, - IGroupRepository groupRepository, - IOrganizationRepository organizationRepository, - IOrganizationUserRepository organizationUserRepository, - ICollectionRepository collectionRepository) - { - var user = await CreateUser(userRepository); - var organization = await CreateOrganization(organizationRepository); - var orgUser = await CreateOrganizationUser(user, organization, OrganizationUserType.Custom, accessAll: false, - organizationUserRepository, new Permissions { EditAssignedCollections = true }); - - // Use 2 groups to test for overlapping access - var group1 = await CreateGroup(organization, accessAll: true, groupRepository, orgUser); - var group2 = await CreateGroup(organization, accessAll: true, groupRepository, orgUser); - - var collection1 = await CreateCollection(organization, collectionRepository); - var collection2 = await CreateCollection(organization, collectionRepository); - var collection3 = await CreateCollection(organization, collectionRepository); - - await organizationRepository.EnableCollectionEnhancements(organization.Id); - - var (updatedOrgUser, collectionAccessSelections) = await organizationUserRepository.GetDetailsByIdWithCollectionsAsync(orgUser.Id); - - Assert.Equal(OrganizationUserType.Custom, updatedOrgUser.Type); - - // OrgUser has direct Can Manage access to all collections - Assert.Equal(3, collectionAccessSelections.Count); - Assert.Contains(collectionAccessSelections, cas => - cas.Id == collection1.Id && - CanManage(cas)); - Assert.Contains(collectionAccessSelections, cas => - cas.Id == collection2.Id && - CanManage(cas)); - Assert.Contains(collectionAccessSelections, cas => - cas.Id == collection3.Id && - CanManage(cas)); - - // Assert: group should only have Can Edit permissions (making sure no side-effects from the Manager migration) - var (updatedGroup1, updatedGroupAccess1) = await groupRepository.GetByIdWithCollectionsAsync(group1.Id); - Assert.Equal(3, updatedGroupAccess1.Count); - Assert.Contains(updatedGroupAccess1, cas => - cas.Id == collection1.Id && - CanEdit(cas)); - Assert.Contains(updatedGroupAccess1, cas => - cas.Id == collection2.Id && - CanEdit(cas)); - Assert.Contains(updatedGroupAccess1, cas => - cas.Id == collection3.Id && - CanEdit(cas)); - - var (updatedGroup2, updatedGroupAccess2) = await groupRepository.GetByIdWithCollectionsAsync(group2.Id); - Assert.Equal(3, updatedGroupAccess2.Count); - Assert.Contains(updatedGroupAccess2, cas => - cas.Id == collection1.Id && - CanEdit(cas)); - Assert.Contains(updatedGroupAccess2, cas => - cas.Id == collection2.Id && - CanEdit(cas)); - Assert.Contains(updatedGroupAccess2, cas => - cas.Id == collection3.Id && - CanEdit(cas)); - } - - [DatabaseTheory, MssqlDatabaseData] - public async Task Migrate_NonManagers_WithoutAccessAll_NoChangeToRoleOrCollectionAccess( - IUserRepository userRepository, - IOrganizationRepository organizationRepository, - IOrganizationUserRepository organizationUserRepository, - ICollectionRepository collectionRepository) - { - var userUser = await CreateUser(userRepository); - var adminUser = await CreateUser(userRepository); - var ownerUser = await CreateUser(userRepository); - var customUser = await CreateUser(userRepository); - - var organization = await CreateOrganization(organizationRepository); - - // All roles that are unaffected by this change without AccessAll - var orgUser = await CreateOrganizationUser(userUser, organization, OrganizationUserType.User, accessAll: false, organizationUserRepository); - var admin = await CreateOrganizationUser(adminUser, organization, OrganizationUserType.Admin, accessAll: false, organizationUserRepository); - var owner = await CreateOrganizationUser(ownerUser, organization, OrganizationUserType.Owner, accessAll: false, organizationUserRepository); - var custom = await CreateOrganizationUser(customUser, organization, OrganizationUserType.Custom, accessAll: false, organizationUserRepository, new Permissions { DeleteAssignedCollections = true, AccessReports = true }); - - var collection1 = await CreateCollection(organization, collectionRepository, null, new[] - { - new CollectionAccessSelection {Id = orgUser.Id}, - new CollectionAccessSelection {Id = custom.Id, HidePasswords = true} - }); - var collection2 = await CreateCollection(organization, collectionRepository, null, new[] - { - new CollectionAccessSelection { Id = owner.Id, HidePasswords = true} , - new CollectionAccessSelection { Id = admin.Id, ReadOnly = true} - }); - var collection3 = await CreateCollection(organization, collectionRepository, null, new[] - { - new CollectionAccessSelection { Id = owner.Id } - }); - - await organizationRepository.EnableCollectionEnhancements(organization.Id); - - var (updatedOrgUser, orgUserAccess) = await organizationUserRepository - .GetDetailsByIdWithCollectionsAsync(orgUser.Id); - Assert.Equal(OrganizationUserType.User, updatedOrgUser.Type); - Assert.Equal(1, orgUserAccess.Count); - Assert.Contains(orgUserAccess, cas => - cas.Id == collection1.Id && - CanEdit(cas)); - - var (updatedAdmin, adminAccess) = await organizationUserRepository - .GetDetailsByIdWithCollectionsAsync(admin.Id); - Assert.Equal(OrganizationUserType.Admin, updatedAdmin.Type); - Assert.Equal(1, adminAccess.Count); - Assert.Contains(adminAccess, cas => - cas.Id == collection2.Id && - cas is { HidePasswords: false, ReadOnly: true, Manage: false }); - - var (updatedOwner, ownerAccess) = await organizationUserRepository - .GetDetailsByIdWithCollectionsAsync(owner.Id); - Assert.Equal(OrganizationUserType.Owner, updatedOwner.Type); - Assert.Equal(2, ownerAccess.Count); - Assert.Contains(ownerAccess, cas => - cas.Id == collection2.Id && - cas is { HidePasswords: true, ReadOnly: false, Manage: false }); - Assert.Contains(ownerAccess, cas => - cas.Id == collection3.Id && - CanEdit(cas)); - - var (updatedCustom, customAccess) = await organizationUserRepository - .GetDetailsByIdWithCollectionsAsync(custom.Id); - Assert.Equal(OrganizationUserType.Custom, updatedCustom.Type); - Assert.Equal(1, customAccess.Count); - Assert.Contains(customAccess, cas => - cas.Id == collection1.Id && - cas is { HidePasswords: true, ReadOnly: false, Manage: false }); - } - - [DatabaseTheory, MssqlDatabaseData] - public async Task Migrate_DoesNotAffect_OtherOrganizations( - IUserRepository userRepository, - IOrganizationRepository organizationRepository, - IOrganizationUserRepository organizationUserRepository, - ICollectionRepository collectionRepository) - { - // Target organization to be migrated - var targetUser = await CreateUser(userRepository); - var targetOrganization = await CreateOrganization(organizationRepository); - await CreateOrganizationUser(targetUser, targetOrganization, OrganizationUserType.Manager, accessAll: true, organizationUserRepository); - await CreateCollection(targetOrganization, collectionRepository); - await CreateCollection(targetOrganization, collectionRepository); - await CreateCollection(targetOrganization, collectionRepository); - - // Unrelated organization - var user = await CreateUser(userRepository); - var organization = await CreateOrganization(organizationRepository); - var orgUser = await CreateOrganizationUser(user, organization, OrganizationUserType.Manager, accessAll: true, organizationUserRepository); - await CreateCollection(organization, collectionRepository); - await CreateCollection(organization, collectionRepository); - await CreateCollection(organization, collectionRepository); - - await organizationRepository.EnableCollectionEnhancements(targetOrganization.Id); - - var (updatedOrgUser, collectionAccessSelections) = await organizationUserRepository - .GetDetailsByIdWithCollectionsAsync(orgUser.Id); - - // OrgUser should not have changed - Assert.Equal(OrganizationUserType.Manager, updatedOrgUser.Type); - Assert.True(updatedOrgUser.AccessAll); - Assert.Equal(0, collectionAccessSelections.Count); - - var updatedOrganization = await organizationRepository.GetByIdAsync(organization.Id); - Assert.False(updatedOrganization.FlexibleCollections); - } - - private async Task CreateUser(IUserRepository userRepository) - { - return await userRepository.CreateAsync(new User - { - Name = "Test User", - Email = $"test+{Guid.NewGuid()}@example.com", - ApiKey = "TEST", - SecurityStamp = "stamp", - }); - } - - private async Task CreateGroup(Organization organization, bool accessAll, IGroupRepository groupRepository, - OrganizationUser? orgUser = null) - { - var group = await groupRepository.CreateAsync(new Group - { - Name = $"Test Group {Guid.NewGuid()}", - OrganizationId = organization.Id, - AccessAll = accessAll - }); - - if (orgUser != null) - { - await groupRepository.UpdateUsersAsync(group.Id, [orgUser.Id]); - } - - return group; - } - - private async Task CreateOrganization(IOrganizationRepository organizationRepository) - { - return await organizationRepository.CreateAsync(new Organization - { - Name = $"Test Org {Guid.NewGuid()}", - BillingEmail = "Billing Email", // TODO: EF does not enforce this being NOT NULL - Plan = "Test Plan", // TODO: EF does not enforce this being NOT NULl - }); - } - - private async Task CreateOrganizationUser(User user, Organization organization, - OrganizationUserType type, bool accessAll, IOrganizationUserRepository organizationUserRepository, - Permissions? permissions = null) - { - return await organizationUserRepository.CreateAsync(new OrganizationUser - { - OrganizationId = organization.Id, - UserId = user.Id, - Status = OrganizationUserStatusType.Confirmed, - Type = type, - AccessAll = accessAll, - Permissions = permissions == null ? null : CoreHelpers.ClassToJsonData(permissions) - }); - } - - private async Task CreateCollection(Organization organization, ICollectionRepository collectionRepository, - IEnumerable? groups = null, IEnumerable? users = null) - { - var collection = new Collection { Name = $"Test collection {Guid.NewGuid()}", OrganizationId = organization.Id }; - await collectionRepository.CreateAsync(collection, groups: groups, users: users); - return collection; - } - - private bool CanEdit(CollectionAccessSelection collectionAccess) - { - return collectionAccess is { HidePasswords: false, ReadOnly: false, Manage: false }; - } - - private bool CanManage(CollectionAccessSelection collectionAccess) - { - return collectionAccess is { HidePasswords: false, ReadOnly: false, Manage: true }; - } -} diff --git a/util/Migrator/DbScripts/2024-05-23_00_DropEnableCollectionEnhancements.sql b/util/Migrator/DbScripts/2024-05-23_00_DropEnableCollectionEnhancements.sql new file mode 100644 index 0000000000..5ddea7d709 --- /dev/null +++ b/util/Migrator/DbScripts/2024-05-23_00_DropEnableCollectionEnhancements.sql @@ -0,0 +1,5 @@ +IF OBJECT_ID('[dbo].[Organization_EnableCollectionEnhancements]') IS NOT NULL +BEGIN + DROP PROCEDURE [dbo].[Organization_EnableCollectionEnhancements] +END +GO