1
0
mirror of https://github.com/bitwarden/server synced 2026-02-10 13:40:10 +00:00

[PM-30247] Previously archived items are not archived after import (#6824)

* support importing archived ciphers
* preserve archived ciphers across org imports
This commit is contained in:
John Harrington
2026-02-02 14:39:01 -07:00
committed by GitHub
parent 0e72257ea1
commit d3aed59fcb
4 changed files with 110 additions and 63 deletions

View File

@@ -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();

View File

@@ -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();

View File

@@ -806,63 +806,6 @@ public class ImportCiphersControllerTests
Arg.Any<Guid>());
}
[Theory, BitAutoData]
public async Task PostImportOrganization_ThrowsException_WhenAnyCipherIsArchived(
SutProvider<ImportCiphersController> sutProvider,
IFixture fixture,
User user
)
{
var orgId = Guid.NewGuid();
sutProvider.GetDependency<GlobalSettings>()
.SelfHosted = false;
sutProvider.GetDependency<GlobalSettings>()
.ImportCiphersLimitation = _organizationCiphersLimitations;
SetupUserService(sutProvider, user);
var ciphers = fixture.Build<CipherRequestModel>()
.With(_ => _.ArchivedDate, DateTime.UtcNow)
.CreateMany(2).ToArray();
var request = new ImportOrganizationCiphersRequestModel
{
Collections = new List<CollectionWithIdRequestModel>().ToArray(),
Ciphers = ciphers,
CollectionRelationships = new List<KeyValuePair<int, int>>().ToArray(),
};
sutProvider.GetDependency<ICurrentContext>()
.AccessImportExport(Arg.Any<Guid>())
.Returns(false);
sutProvider.GetDependency<IAuthorizationService>()
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(),
Arg.Any<IEnumerable<Collection>>(),
Arg.Is<IEnumerable<IAuthorizationRequirement>>(reqs =>
reqs.Contains(BulkCollectionOperations.ImportCiphers)))
.Returns(AuthorizationResult.Failed());
sutProvider.GetDependency<IAuthorizationService>()
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(),
Arg.Any<IEnumerable<Collection>>(),
Arg.Is<IEnumerable<IAuthorizationRequirement>>(reqs =>
reqs.Contains(BulkCollectionOperations.Create)))
.Returns(AuthorizationResult.Success());
sutProvider.GetDependency<ICollectionRepository>()
.GetManyByOrganizationIdAsync(orgId)
.Returns(new List<Collection>());
var exception = await Assert.ThrowsAsync<BadRequestException>(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<ImportCiphersController> sutProvider, User user)
{
// This is a workaround for the NSubstitute issue with ambiguous arguments

View File

@@ -326,4 +326,101 @@ public class ImportCiphersAsyncCommandTests
await sutProvider.GetDependency<IPushNotificationService>().Received(1).PushSyncVaultAsync(importingUserId);
}
[Theory, BitAutoData]
public async Task ImportIntoIndividualVaultAsync_WithArchivedCiphers_PreservesArchiveStatus(
Guid importingUserId,
List<CipherDetails> ciphers,
SutProvider<ImportCiphersCommand> sutProvider)
{
var archivedDate = DateTime.UtcNow.AddDays(-1);
ciphers[0].UserId = importingUserId;
ciphers[0].ArchivedDate = archivedDate;
sutProvider.GetDependency<IPolicyService>()
.AnyPoliciesApplicableToUserAsync(importingUserId, PolicyType.OrganizationDataOwnership)
.Returns(false);
sutProvider.GetDependency<IFolderRepository>()
.GetManyByUserIdAsync(importingUserId)
.Returns(new List<Folder>());
var folders = new List<Folder>();
var folderRelationships = new List<KeyValuePair<int, int>>();
await sutProvider.Sut.ImportIntoIndividualVaultAsync(folders, ciphers, folderRelationships, importingUserId);
await sutProvider.GetDependency<ICipherRepository>()
.Received(1)
.CreateAsync(importingUserId,
Arg.Is<List<CipherDetails>>(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<List<Folder>>());
}
/*
* 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<Collection> collections,
List<CipherDetails> ciphers,
SutProvider<ImportCiphersCommand> 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<int, int>[] collectionRelationships = {
new(0, 0),
new(1, 1),
new(2, 2)
};
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(organization.Id)
.Returns(organization);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByOrganizationAsync(organization.Id, importingUserId)
.Returns(importingOrganizationUser);
sutProvider.GetDependency<ICollectionRepository>()
.GetManyByOrganizationIdAsync(organization.Id)
.Returns(new List<Collection>());
await sutProvider.Sut.ImportIntoOrganizationalVaultAsync(collections, ciphers, collectionRelationships, importingUserId);
await sutProvider.GetDependency<ICipherRepository>()
.Received(1)
.CreateAsync(
Arg.Is<List<CipherDetails>>(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<IEnumerable<Collection>>(),
Arg.Any<IEnumerable<CollectionCipher>>(),
Arg.Any<IEnumerable<CollectionUser>>());
}
}