1
0
mirror of https://github.com/bitwarden/server synced 2025-12-26 05:03:18 +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

@@ -20,6 +20,7 @@ using Bit.Core.Settings;
using Bit.Core.Tools.Services;
using Bit.Core.Utilities;
using Bit.Core.Vault.Authorization.Permissions;
using Bit.Core.Vault.Commands.Interfaces;
using Bit.Core.Vault.Entities;
using Bit.Core.Vault.Models.Data;
using Bit.Core.Vault.Queries;
@@ -48,6 +49,8 @@ public class CiphersController : Controller
private readonly IOrganizationCiphersQuery _organizationCiphersQuery;
private readonly IApplicationCacheService _applicationCacheService;
private readonly ICollectionRepository _collectionRepository;
private readonly IArchiveCiphersCommand _archiveCiphersCommand;
private readonly IUnarchiveCiphersCommand _unarchiveCiphersCommand;
private readonly IFeatureService _featureService;
public CiphersController(
@@ -63,6 +66,8 @@ public class CiphersController : Controller
IOrganizationCiphersQuery organizationCiphersQuery,
IApplicationCacheService applicationCacheService,
ICollectionRepository collectionRepository,
IArchiveCiphersCommand archiveCiphersCommand,
IUnarchiveCiphersCommand unarchiveCiphersCommand,
IFeatureService featureService)
{
_cipherRepository = cipherRepository;
@@ -77,6 +82,8 @@ public class CiphersController : Controller
_organizationCiphersQuery = organizationCiphersQuery;
_applicationCacheService = applicationCacheService;
_collectionRepository = collectionRepository;
_archiveCiphersCommand = archiveCiphersCommand;
_unarchiveCiphersCommand = unarchiveCiphersCommand;
_featureService = featureService;
}
@@ -846,6 +853,47 @@ public class CiphersController : Controller
}
}
[HttpPut("{id}/archive")]
[RequireFeature(FeatureFlagKeys.ArchiveVaultItems)]
public async Task<CipherMiniResponseModel> PutArchive(Guid id)
{
var userId = _userService.GetProperUserId(User).Value;
var archivedCipherOrganizationDetails = await _archiveCiphersCommand.ArchiveManyAsync([id], userId);
if (archivedCipherOrganizationDetails.Count == 0)
{
throw new BadRequestException("Cipher was not archived. Ensure the provided ID is correct and you have permission to archive it.");
}
return new CipherMiniResponseModel(archivedCipherOrganizationDetails.First(), _globalSettings, archivedCipherOrganizationDetails.First().OrganizationUseTotp);
}
[HttpPut("archive")]
[RequireFeature(FeatureFlagKeys.ArchiveVaultItems)]
public async Task<ListResponseModel<CipherMiniResponseModel>> PutArchiveMany([FromBody] CipherBulkArchiveRequestModel model)
{
if (!_globalSettings.SelfHosted && model.Ids.Count() > 500)
{
throw new BadRequestException("You can only archive up to 500 items at a time.");
}
var userId = _userService.GetProperUserId(User).Value;
var cipherIdsToArchive = new HashSet<Guid>(model.Ids);
var archivedCiphers = await _archiveCiphersCommand.ArchiveManyAsync(cipherIdsToArchive, userId);
if (archivedCiphers.Count == 0)
{
throw new BadRequestException("No ciphers were archived. Ensure the provided IDs are correct and you have permission to archive them.");
}
var responses = archivedCiphers.Select(c => new CipherMiniResponseModel(c, _globalSettings, c.OrganizationUseTotp));
return new ListResponseModel<CipherMiniResponseModel>(responses);
}
[HttpDelete("{id}")]
[HttpPost("{id}/delete")]
public async Task Delete(Guid id)
@@ -979,6 +1027,47 @@ public class CiphersController : Controller
await _cipherService.SoftDeleteManyAsync(cipherIds, userId, new Guid(model.OrganizationId), true);
}
[HttpPut("{id}/unarchive")]
[RequireFeature(FeatureFlagKeys.ArchiveVaultItems)]
public async Task<CipherMiniResponseModel> PutUnarchive(Guid id)
{
var userId = _userService.GetProperUserId(User).Value;
var unarchivedCipherDetails = await _unarchiveCiphersCommand.UnarchiveManyAsync([id], userId);
if (unarchivedCipherDetails.Count == 0)
{
throw new BadRequestException("Cipher was not unarchived. Ensure the provided ID is correct and you have permission to archive it.");
}
return new CipherMiniResponseModel(unarchivedCipherDetails.First(), _globalSettings, unarchivedCipherDetails.First().OrganizationUseTotp);
}
[HttpPut("unarchive")]
[RequireFeature(FeatureFlagKeys.ArchiveVaultItems)]
public async Task<ListResponseModel<CipherMiniResponseModel>> PutUnarchiveMany([FromBody] CipherBulkUnarchiveRequestModel model)
{
if (!_globalSettings.SelfHosted && model.Ids.Count() > 500)
{
throw new BadRequestException("You can only unarchive up to 500 items at a time.");
}
var userId = _userService.GetProperUserId(User).Value;
var cipherIdsToUnarchive = new HashSet<Guid>(model.Ids);
var unarchivedCipherOrganizationDetails = await _unarchiveCiphersCommand.UnarchiveManyAsync(cipherIdsToUnarchive, userId);
if (unarchivedCipherOrganizationDetails.Count == 0)
{
throw new BadRequestException("Ciphers were not unarchived. Ensure the provided ID is correct and you have permission to archive it.");
}
var responses = unarchivedCipherOrganizationDetails.Select(c => new CipherMiniResponseModel(c, _globalSettings, c.OrganizationUseTotp));
return new ListResponseModel<CipherMiniResponseModel>(responses);
}
[HttpPut("{id}/restore")]
public async Task<CipherResponseModel> PutRestore(Guid id)
{

View File

@@ -46,6 +46,7 @@ public class CipherRequestModel
public CipherSecureNoteModel SecureNote { get; set; }
public CipherSSHKeyModel SSHKey { get; set; }
public DateTime? LastKnownRevisionDate { get; set; } = null;
public DateTime? ArchivedDate { get; set; }
public CipherDetails ToCipherDetails(Guid userId, bool allowOrgIdSet = true)
{
@@ -99,6 +100,7 @@ public class CipherRequestModel
existingCipher.Reprompt = Reprompt;
existingCipher.Key = Key;
existingCipher.ArchivedDate = ArchivedDate;
var hasAttachments2 = (Attachments2?.Count ?? 0) > 0;
var hasAttachments = (Attachments?.Count ?? 0) > 0;
@@ -316,6 +318,12 @@ public class CipherCollectionsRequestModel
public IEnumerable<string> CollectionIds { get; set; }
}
public class CipherBulkArchiveRequestModel
{
[Required]
public IEnumerable<Guid> Ids { get; set; }
}
public class CipherBulkDeleteRequestModel
{
[Required]
@@ -323,6 +331,12 @@ public class CipherBulkDeleteRequestModel
public string OrganizationId { get; set; }
}
public class CipherBulkUnarchiveRequestModel
{
[Required]
public IEnumerable<Guid> Ids { get; set; }
}
public class CipherBulkRestoreRequestModel
{
[Required]

View File

@@ -74,6 +74,7 @@ public class CipherMiniResponseModel : ResponseModel
DeletedDate = cipher.DeletedDate;
Reprompt = cipher.Reprompt.GetValueOrDefault(CipherRepromptType.None);
Key = cipher.Key;
ArchivedDate = cipher.ArchivedDate;
}
public Guid Id { get; set; }
@@ -96,6 +97,7 @@ public class CipherMiniResponseModel : ResponseModel
public DateTime? DeletedDate { get; set; }
public CipherRepromptType Reprompt { get; set; }
public string Key { get; set; }
public DateTime? ArchivedDate { get; set; }
}
public class CipherResponseModel : CipherMiniResponseModel