diff --git a/src/Api/Tools/Controllers/ImportCiphersController.cs b/src/Api/Tools/Controllers/ImportCiphersController.cs index 8b3ec5e26c..bebf7cbf29 100644 --- a/src/Api/Tools/Controllers/ImportCiphersController.cs +++ b/src/Api/Tools/Controllers/ImportCiphersController.cs @@ -74,11 +74,6 @@ public class ImportCiphersController : Controller throw new BadRequestException("You cannot import this much data at once."); } - if (model.Ciphers.Any(c => c.ArchivedDate.HasValue)) - { - throw new BadRequestException("You cannot import archived items into an organization."); - } - var orgId = new Guid(organizationId); var collections = model.Collections.Select(c => c.ToCollection(orgId)).ToList(); diff --git a/src/Core/Tools/ImportFeatures/ImportCiphersCommand.cs b/src/Core/Tools/ImportFeatures/ImportCiphersCommand.cs index 9300e3c4bb..3f856e96fc 100644 --- a/src/Core/Tools/ImportFeatures/ImportCiphersCommand.cs +++ b/src/Core/Tools/ImportFeatures/ImportCiphersCommand.cs @@ -76,6 +76,12 @@ public class ImportCiphersCommand : IImportCiphersCommand { cipher.Favorites = $"{{\"{cipher.UserId.ToString().ToUpperInvariant()}\":true}}"; } + + if (cipher.UserId.HasValue && cipher.ArchivedDate.HasValue) + { + cipher.Archives = $"{{\"{cipher.UserId.Value.ToString().ToUpperInvariant()}\":\"" + + $"{cipher.ArchivedDate.Value:yyyy-MM-ddTHH:mm:ss.fffffffZ}\"}}"; + } } var userfoldersIds = (await _folderRepository.GetManyByUserIdAsync(importingUserId)).Select(f => f.Id).ToList(); @@ -135,10 +141,16 @@ public class ImportCiphersCommand : IImportCiphersCommand } } - // Init. ids for ciphers foreach (var cipher in ciphers) { + // Init. ids for ciphers cipher.SetNewId(); + + if (cipher.ArchivedDate.HasValue) + { + cipher.Archives = $"{{\"{importingUserId.ToString().ToUpperInvariant()}\":\"" + + $"{cipher.ArchivedDate.Value:yyyy-MM-ddTHH:mm:ss.fffffffZ}\"}}"; + } } var organizationCollectionsIds = (await _collectionRepository.GetManyByOrganizationIdAsync(org.Id)).Select(c => c.Id).ToList(); diff --git a/test/Api.Test/Tools/Controllers/ImportCiphersControllerTests.cs b/test/Api.Test/Tools/Controllers/ImportCiphersControllerTests.cs index 9ca641a28e..a8465ed0f6 100644 --- a/test/Api.Test/Tools/Controllers/ImportCiphersControllerTests.cs +++ b/test/Api.Test/Tools/Controllers/ImportCiphersControllerTests.cs @@ -806,63 +806,6 @@ public class ImportCiphersControllerTests Arg.Any()); } - [Theory, BitAutoData] - public async Task PostImportOrganization_ThrowsException_WhenAnyCipherIsArchived( - SutProvider sutProvider, - IFixture fixture, - User user - ) - { - var orgId = Guid.NewGuid(); - - sutProvider.GetDependency() - .SelfHosted = false; - sutProvider.GetDependency() - .ImportCiphersLimitation = _organizationCiphersLimitations; - - SetupUserService(sutProvider, user); - - var ciphers = fixture.Build() - .With(_ => _.ArchivedDate, DateTime.UtcNow) - .CreateMany(2).ToArray(); - - var request = new ImportOrganizationCiphersRequestModel - { - Collections = new List().ToArray(), - Ciphers = ciphers, - CollectionRelationships = new List>().ToArray(), - }; - - sutProvider.GetDependency() - .AccessImportExport(Arg.Any()) - .Returns(false); - - sutProvider.GetDependency() - .AuthorizeAsync(Arg.Any(), - Arg.Any>(), - Arg.Is>(reqs => - reqs.Contains(BulkCollectionOperations.ImportCiphers))) - .Returns(AuthorizationResult.Failed()); - - sutProvider.GetDependency() - .AuthorizeAsync(Arg.Any(), - Arg.Any>(), - Arg.Is>(reqs => - reqs.Contains(BulkCollectionOperations.Create))) - .Returns(AuthorizationResult.Success()); - - sutProvider.GetDependency() - .GetManyByOrganizationIdAsync(orgId) - .Returns(new List()); - - var exception = await Assert.ThrowsAsync(async () => - { - await sutProvider.Sut.PostImportOrganization(orgId.ToString(), request); - }); - - Assert.Equal("You cannot import archived items into an organization.", exception.Message); - } - private static void SetupUserService(SutProvider sutProvider, User user) { // This is a workaround for the NSubstitute issue with ambiguous arguments diff --git a/test/Core.Test/Tools/ImportFeatures/ImportCiphersAsyncCommandTests.cs b/test/Core.Test/Tools/ImportFeatures/ImportCiphersAsyncCommandTests.cs index aea06f39a8..f6b1bd200a 100644 --- a/test/Core.Test/Tools/ImportFeatures/ImportCiphersAsyncCommandTests.cs +++ b/test/Core.Test/Tools/ImportFeatures/ImportCiphersAsyncCommandTests.cs @@ -326,4 +326,101 @@ public class ImportCiphersAsyncCommandTests await sutProvider.GetDependency().Received(1).PushSyncVaultAsync(importingUserId); } + + [Theory, BitAutoData] + public async Task ImportIntoIndividualVaultAsync_WithArchivedCiphers_PreservesArchiveStatus( + Guid importingUserId, + List ciphers, + SutProvider sutProvider) + { + var archivedDate = DateTime.UtcNow.AddDays(-1); + ciphers[0].UserId = importingUserId; + ciphers[0].ArchivedDate = archivedDate; + + sutProvider.GetDependency() + .AnyPoliciesApplicableToUserAsync(importingUserId, PolicyType.OrganizationDataOwnership) + .Returns(false); + + sutProvider.GetDependency() + .GetManyByUserIdAsync(importingUserId) + .Returns(new List()); + + var folders = new List(); + var folderRelationships = new List>(); + + await sutProvider.Sut.ImportIntoIndividualVaultAsync(folders, ciphers, folderRelationships, importingUserId); + + await sutProvider.GetDependency() + .Received(1) + .CreateAsync(importingUserId, + Arg.Is>(c => + c[0].Archives != null && + c[0].Archives.Contains(importingUserId.ToString().ToUpperInvariant()) && + c[0].Archives.Contains(archivedDate.ToString("yyyy-MM-ddTHH:mm:ss.fffffffZ"))), + Arg.Any>()); + } + + /* + * Archive functionality is a per-user function. When importing archived ciphers into an organization vault, + * the Archives field should be set for the importing user only. This allows the importing user to see + * items as archived, while other organization members will not see them as archived. + */ + [Theory, BitAutoData] + public async Task ImportIntoOrganizationalVaultAsync_WithArchivedCiphers_SetsArchivesForImportingUserOnly( + Organization organization, + Guid importingUserId, + OrganizationUser importingOrganizationUser, + List collections, + List ciphers, + SutProvider sutProvider) + { + var archivedDate = DateTime.UtcNow.AddDays(-1); + organization.MaxCollections = null; + importingOrganizationUser.OrganizationId = organization.Id; + + foreach (var collection in collections) + { + collection.OrganizationId = organization.Id; + } + + foreach (var cipher in ciphers) + { + cipher.OrganizationId = organization.Id; + } + + ciphers[0].ArchivedDate = archivedDate; + ciphers[0].Archives = null; + + KeyValuePair[] collectionRelationships = { + new(0, 0), + new(1, 1), + new(2, 2) + }; + + sutProvider.GetDependency() + .GetByIdAsync(organization.Id) + .Returns(organization); + + sutProvider.GetDependency() + .GetByOrganizationAsync(organization.Id, importingUserId) + .Returns(importingOrganizationUser); + + sutProvider.GetDependency() + .GetManyByOrganizationIdAsync(organization.Id) + .Returns(new List()); + + await sutProvider.Sut.ImportIntoOrganizationalVaultAsync(collections, ciphers, collectionRelationships, importingUserId); + + await sutProvider.GetDependency() + .Received(1) + .CreateAsync( + Arg.Is>(c => + c[0].ArchivedDate == archivedDate && + c[0].Archives != null && + c[0].Archives.Contains(importingUserId.ToString().ToUpperInvariant()) && + c[0].Archives.Contains(archivedDate.ToString("yyyy-MM-ddTHH:mm:ss.fffffffZ"))), + Arg.Any>(), + Arg.Any>(), + Arg.Any>()); + } }