diff --git a/src/Api/AdminConsole/Controllers/OrganizationsController.cs b/src/Api/AdminConsole/Controllers/OrganizationsController.cs index da0b8d4e2c..6acdace5e3 100644 --- a/src/Api/AdminConsole/Controllers/OrganizationsController.cs +++ b/src/Api/AdminConsole/Controllers/OrganizationsController.cs @@ -815,6 +815,39 @@ public class OrganizationsController : Controller 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(); + } + + if (organization.FlexibleCollections) + { + throw new BadRequestException("Organization has already been migrated to the new collection enhancements"); + } + + await _organizationRepository.EnableCollectionEnhancements(id); + + organization.FlexibleCollections = true; + await _organizationService.ReplaceAndUpdateCacheAsync(organization); + } + private async Task TryGrantOwnerAccessToSecretsManagerAsync(Guid organizationId, Guid userId) { var organizationUser = await _organizationUserRepository.GetByOrganizationAsync(organizationId, userId); diff --git a/src/Core/AdminConsole/Repositories/IOrganizationRepository.cs b/src/Core/AdminConsole/Repositories/IOrganizationRepository.cs index 4598a11fb9..36a445442b 100644 --- a/src/Core/AdminConsole/Repositories/IOrganizationRepository.cs +++ b/src/Core/AdminConsole/Repositories/IOrganizationRepository.cs @@ -15,4 +15,5 @@ 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 07fe176381..d3e9b3f749 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -106,8 +106,15 @@ public static class FeatureFlagKeys public const string ItemShare = "item-share"; public const string KeyRotationImprovements = "key-rotation-improvements"; public const string DuoRedirect = "duo-redirect"; - public const string FlexibleCollectionsMigration = "flexible-collections-migration"; + /// + /// Enables flexible collections improvements for new organizations on creation + /// public const string FlexibleCollectionsSignup = "flexible-collections-signup"; + /// + /// 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 static List GetAllKeys() { diff --git a/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationRepository.cs b/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationRepository.cs index f4c771adec..9080e17c3e 100644 --- a/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationRepository.cs +++ b/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationRepository.cs @@ -169,4 +169,16 @@ 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/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs index acc36c9449..7610f8dd15 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs @@ -267,4 +267,9 @@ public class OrganizationRepository : Repository()); } + + [Theory, AutoData] + public async Task EnableCollectionEnhancements_Success(Organization organization) + { + organization.FlexibleCollections = false; + _currentContext.OrganizationOwner(organization.Id).Returns(true); + _organizationRepository.GetByIdAsync(organization.Id).Returns(organization); + + await _sut.EnableCollectionEnhancements(organization.Id); + + await _organizationRepository.Received(1).EnableCollectionEnhancements(organization.Id); + await _organizationService.Received(1).ReplaceAndUpdateCacheAsync( + Arg.Is(o => + o.Id == organization.Id && + o.FlexibleCollections)); + } + + [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 _organizationRepository.DidNotReceiveWithAnyArgs().EnableCollectionEnhancements(Arg.Any()); + await _organizationService.DidNotReceiveWithAnyArgs().ReplaceAndUpdateCacheAsync(Arg.Any()); + } + + [Theory, AutoData] + public async Task EnableCollectionEnhancements_WhenAlreadyMigrated_Throws(Organization organization) + { + organization.FlexibleCollections = true; + _currentContext.OrganizationOwner(organization.Id).Returns(true); + _organizationRepository.GetByIdAsync(organization.Id).Returns(organization); + + var exception = await Assert.ThrowsAsync(async () => await _sut.EnableCollectionEnhancements(organization.Id)); + Assert.Contains("has already been migrated", exception.Message); + + await _organizationRepository.DidNotReceiveWithAnyArgs().EnableCollectionEnhancements(Arg.Any()); + await _organizationService.DidNotReceiveWithAnyArgs().ReplaceAndUpdateCacheAsync(Arg.Any()); + } }