1
0
mirror of https://github.com/bitwarden/server synced 2025-12-25 12:43:14 +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

@@ -229,6 +229,9 @@ public static class FeatureFlagKeys
public const string PM19315EndUserActivationMvp = "pm-19315-end-user-activation-mvp";
public const string PM22136_SdkCipherEncryption = "pm-22136-sdk-cipher-encryption";
/* Innovation Team */
public const string ArchiveVaultItems = "pm-19148-innovation-archive";
public static List<string> GetAllKeys()
{
return typeof(FeatureFlagKeys).GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy)

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;
}
}

View File

@@ -25,6 +25,7 @@ public class Cipher : ITableObject<Guid>, ICloneable
public DateTime? DeletedDate { get; set; }
public Enums.CipherRepromptType? Reprompt { get; set; }
public string Key { get; set; }
public DateTime? ArchivedDate { get; set; }
public void SetNewId()
{

View File

@@ -3,6 +3,8 @@
public enum CipherStateAction
{
Restore,
Unarchive,
Archive,
SoftDelete,
HardDelete,
}

View File

@@ -25,6 +25,7 @@ public interface ICipherRepository : IRepository<Cipher, Guid>
Task<bool> ReplaceAsync(Cipher obj, IEnumerable<Guid> collectionIds);
Task UpdatePartialAsync(Guid id, Guid userId, Guid? folderId, bool favorite);
Task UpdateAttachmentAsync(CipherAttachment attachment);
Task<DateTime> ArchiveAsync(IEnumerable<Guid> ids, Guid userId);
Task DeleteAttachmentAsync(Guid cipherId, string attachmentId);
Task DeleteAsync(IEnumerable<Guid> ids, Guid userId);
Task DeleteByIdsOrganizationIdAsync(IEnumerable<Guid> ids, Guid organizationId);
@@ -56,6 +57,7 @@ public interface ICipherRepository : IRepository<Cipher, Guid>
IEnumerable<CollectionCipher> collectionCiphers, IEnumerable<CollectionUser> collectionUsers);
Task SoftDeleteAsync(IEnumerable<Guid> ids, Guid userId);
Task SoftDeleteByIdsOrganizationIdAsync(IEnumerable<Guid> ids, Guid organizationId);
Task<DateTime> UnarchiveAsync(IEnumerable<Guid> ids, Guid userId);
Task<DateTime> RestoreAsync(IEnumerable<Guid> ids, Guid userId);
Task<DateTime> RestoreByIdsOrganizationIdAsync(IEnumerable<Guid> ids, Guid organizationId);
Task DeleteDeletedAsync(DateTime deletedDateBefore);

View File

@@ -481,7 +481,7 @@ public class CipherService : ICipherService
throw new NotFoundException();
}
await _cipherRepository.DeleteByOrganizationIdAsync(organizationId);
await _eventService.LogOrganizationEventAsync(org, Bit.Core.Enums.EventType.Organization_PurgedVault);
await _eventService.LogOrganizationEventAsync(org, EventType.Organization_PurgedVault);
}
public async Task MoveManyAsync(IEnumerable<Guid> cipherIds, Guid? destinationFolderId, Guid movingUserId)
@@ -697,7 +697,7 @@ public class CipherService : ICipherService
await _collectionCipherRepository.UpdateCollectionsAsync(cipher.Id, savingUserId, collectionIds);
}
await _eventService.LogCipherEventAsync(cipher, Bit.Core.Enums.EventType.Cipher_UpdatedCollections);
await _eventService.LogCipherEventAsync(cipher, EventType.Cipher_UpdatedCollections);
// push
await _pushService.PushSyncCipherUpdateAsync(cipher, collectionIds);
@@ -786,8 +786,8 @@ public class CipherService : ICipherService
}
var cipherIdsSet = new HashSet<Guid>(cipherIds);
var restoringCiphers = new List<CipherOrganizationDetails>();
DateTime? revisionDate;
List<CipherOrganizationDetails> restoringCiphers;
DateTime? revisionDate; // TODO: Make this not nullable
if (orgAdmin && organizationId.HasValue)
{
@@ -971,6 +971,11 @@ public class CipherService : ICipherService
throw new BadRequestException("One or more ciphers do not belong to you.");
}
if (cipher.ArchivedDate.HasValue)
{
throw new BadRequestException("Cipher cannot be shared with organization because it is archived.");
}
var attachments = cipher.GetAttachments();
var hasAttachments = attachments?.Any() ?? false;
var org = await _organizationRepository.GetByIdAsync(organizationId);

View File

@@ -24,6 +24,8 @@ public static class VaultServiceCollectionExtensions
services.AddScoped<IGetSecurityTasksNotificationDetailsQuery, GetSecurityTasksNotificationDetailsQuery>();
services.AddScoped<ICreateManyTaskNotificationsCommand, CreateManyTaskNotificationsCommand>();
services.AddScoped<ICreateManyTasksCommand, CreateManyTasksCommand>();
services.AddScoped<IArchiveCiphersCommand, ArchiveCiphersCommand>();
services.AddScoped<IUnarchiveCiphersCommand, UnarchiveCiphersCommand>();
services.AddScoped<IMarkNotificationsForTaskAsDeletedCommand, MarkNotificationsForTaskAsDeletedCommand>();
services.AddScoped<IGetTaskMetricsForOrganizationQuery, GetTaskMetricsForOrganizationQuery>();
}