1
0
mirror of https://github.com/bitwarden/server synced 2026-01-06 10:34:01 +00:00

[PM-19151] [PM-19161] Innovation/archive/server (#5672)

* Added the ArchivedDate to cipher entity and response model
* Created migration scripts for sqlserver and ef core migration to add the ArchivedDate column

---------

Co-authored-by: gbubemismith <gsmithwalter@gmail.com>
Co-authored-by: SmithThe4th <gsmith@bitwarden.com>
Co-authored-by: Shane <smelton@bitwarden.com>
Co-authored-by: cd-bitwarden <106776772+cd-bitwarden@users.noreply.github.com>
Co-authored-by: jng <jng@bitwarden.com>
This commit is contained in:
Patrick-Pimentel-Bitwarden
2025-09-12 13:24:30 -04:00
committed by GitHub
parent 18aed0bd79
commit 4e64d35f89
43 changed files with 10342 additions and 42 deletions

View File

@@ -0,0 +1,61 @@
using Bit.Core.Exceptions;
using Bit.Core.Platform.Push;
using Bit.Core.Vault.Commands.Interfaces;
using Bit.Core.Vault.Models.Data;
using Bit.Core.Vault.Repositories;
namespace Bit.Core.Vault.Commands;
public class ArchiveCiphersCommand : IArchiveCiphersCommand
{
private readonly ICipherRepository _cipherRepository;
private readonly IPushNotificationService _pushService;
public ArchiveCiphersCommand(
ICipherRepository cipherRepository,
IPushNotificationService pushService
)
{
_cipherRepository = cipherRepository;
_pushService = pushService;
}
public async Task<ICollection<CipherDetails>> ArchiveManyAsync(IEnumerable<Guid> cipherIds,
Guid archivingUserId)
{
var cipherIdEnumerable = cipherIds as Guid[] ?? cipherIds.ToArray();
if (cipherIds == null || cipherIdEnumerable.Length == 0)
throw new BadRequestException("No cipher ids provided.");
var cipherIdsSet = new HashSet<Guid>(cipherIdEnumerable);
var ciphers = await _cipherRepository.GetManyByUserIdAsync(archivingUserId);
if (ciphers == null || ciphers.Count == 0)
{
return [];
}
var archivingCiphers = ciphers
.Where(c => cipherIdsSet.Contains(c.Id) && c is { Edit: true, OrganizationId: null, ArchivedDate: null })
.ToList();
var revisionDate = await _cipherRepository.ArchiveAsync(archivingCiphers.Select(c => c.Id), archivingUserId);
// Adding specifyKind because revisionDate is currently coming back as Unspecified from the database
revisionDate = DateTime.SpecifyKind(revisionDate, DateTimeKind.Utc);
archivingCiphers.ForEach(c =>
{
c.RevisionDate = revisionDate;
c.ArchivedDate = revisionDate;
});
// Will not log an event because the archive feature is limited to individual ciphers, and event logs only apply to organization ciphers.
// Add event logging here if this is expanded to organization ciphers in the future.
await _pushService.PushSyncCiphersAsync(archivingUserId);
return archivingCiphers;
}
}

View File

@@ -0,0 +1,14 @@
using Bit.Core.Vault.Models.Data;
namespace Bit.Core.Vault.Commands.Interfaces;
public interface IArchiveCiphersCommand
{
/// <summary>
/// Archives a cipher. This fills in the ArchivedDate property on a Cipher.
/// </summary>
/// <param name="cipherIds">Cipher ID to archive.</param>
/// <param name="archivingUserId">User ID to check against the Ciphers that are trying to be archived.</param>
/// <returns></returns>
public Task<ICollection<CipherDetails>> ArchiveManyAsync(IEnumerable<Guid> cipherIds, Guid archivingUserId);
}

View File

@@ -0,0 +1,14 @@
using Bit.Core.Vault.Models.Data;
namespace Bit.Core.Vault.Commands.Interfaces;
public interface IUnarchiveCiphersCommand
{
/// <summary>
/// Unarchives a cipher. This nulls the ArchivedDate property on a Cipher.
/// </summary>
/// <param name="cipherIds">Cipher ID to unarchive.</param>
/// <param name="unarchivingUserId">User ID to check against the Ciphers that are trying to be unarchived.</param>
/// <returns></returns>
public Task<ICollection<CipherDetails>> UnarchiveManyAsync(IEnumerable<Guid> cipherIds, Guid unarchivingUserId);
}

View File

@@ -0,0 +1,60 @@
using Bit.Core.Exceptions;
using Bit.Core.Platform.Push;
using Bit.Core.Vault.Commands.Interfaces;
using Bit.Core.Vault.Models.Data;
using Bit.Core.Vault.Repositories;
namespace Bit.Core.Vault.Commands;
public class UnarchiveCiphersCommand : IUnarchiveCiphersCommand
{
private readonly ICipherRepository _cipherRepository;
private readonly IPushNotificationService _pushService;
public UnarchiveCiphersCommand(
ICipherRepository cipherRepository,
IPushNotificationService pushService
)
{
_cipherRepository = cipherRepository;
_pushService = pushService;
}
public async Task<ICollection<CipherDetails>> UnarchiveManyAsync(IEnumerable<Guid> cipherIds,
Guid unarchivingUserId)
{
var cipherIdEnumerable = cipherIds as Guid[] ?? cipherIds.ToArray();
if (cipherIds == null || cipherIdEnumerable.Length == 0)
throw new BadRequestException("No cipher ids provided.");
var cipherIdsSet = new HashSet<Guid>(cipherIdEnumerable);
var ciphers = await _cipherRepository.GetManyByUserIdAsync(unarchivingUserId);
if (ciphers == null || ciphers.Count == 0)
{
return [];
}
var unarchivingCiphers = ciphers
.Where(c => cipherIdsSet.Contains(c.Id) && c is { Edit: true, ArchivedDate: not null })
.ToList();
var revisionDate =
await _cipherRepository.UnarchiveAsync(unarchivingCiphers.Select(c => c.Id), unarchivingUserId);
// Adding specifyKind because revisionDate is currently coming back as Unspecified from the database
revisionDate = DateTime.SpecifyKind(revisionDate, DateTimeKind.Utc);
unarchivingCiphers.ForEach(c =>
{
c.RevisionDate = revisionDate;
c.ArchivedDate = null;
});
// Will not log an event because the archive feature is limited to individual ciphers, and event logs only apply to organization ciphers.
// Add event logging here if this is expanded to organization ciphers in the future.
await _pushService.PushSyncCiphersAsync(unarchivingUserId);
return unarchivingCiphers;
}
}