Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
194 changes: 154 additions & 40 deletions src/Api/Vault/Controllers/CiphersController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ public class CiphersController : Controller
private readonly IArchiveCiphersCommand _archiveCiphersCommand;
private readonly IUnarchiveCiphersCommand _unarchiveCiphersCommand;
private readonly IFeatureService _featureService;
private readonly ICipherHistoryRepository _cipherHistoryRepository;

public CiphersController(
ICipherRepository cipherRepository,
Expand All @@ -69,7 +70,8 @@ public CiphersController(
ICollectionRepository collectionRepository,
IArchiveCiphersCommand archiveCiphersCommand,
IUnarchiveCiphersCommand unarchiveCiphersCommand,
IFeatureService featureService)
IFeatureService featureService,
ICipherHistoryRepository cipherHistoryRepository)
{
_cipherRepository = cipherRepository;
_collectionCipherRepository = collectionCipherRepository;
Expand All @@ -86,6 +88,7 @@ public CiphersController(
_archiveCiphersCommand = archiveCiphersCommand;
_unarchiveCiphersCommand = unarchiveCiphersCommand;
_featureService = featureService;
_cipherHistoryRepository = cipherHistoryRepository;
}

[HttpGet("{id}")]
Expand Down Expand Up @@ -134,6 +137,58 @@ public async Task<CipherDetailsResponseModel> GetDetails(Guid id)
return new CipherDetailsResponseModel(cipher, user, organizationAbilities, _globalSettings, collectionCiphers);
}

[HttpGet("{id}/history")]
public async Task<ListResponseModel<CipherHistoryResponseModel>> GetHistory(Guid id)
{
var user = await _userService.GetUserByPrincipalAsync(User);
var cipher = await GetByIdAsync(id, user.Id);
if (cipher == null)
{
throw new NotFoundException();
}

var historyItems = await _cipherHistoryRepository.GetManyByCipherIdAsync(id) ?? [];
var responses = historyItems.Select(history => new CipherHistoryResponseModel(history));
return new ListResponseModel<CipherHistoryResponseModel>(responses);
}

[HttpPost("{id}/history/{historyId}/restore")]
public async Task<CipherResponseModel> RestoreFromHistory(Guid id, Guid historyId)
{
var user = await _userService.GetUserByPrincipalAsync(User);
var cipher = await GetByIdAsync(id, user.Id);
if (cipher == null)
{
throw new NotFoundException();
}

var history = await _cipherHistoryRepository.GetByIdAsync(historyId);
if (history == null || history.CipherId != id)
{
throw new NotFoundException();
}

if (cipher.OrganizationId.HasValue)
{
if (history.OrganizationId != cipher.OrganizationId)
{
throw new NotFoundException();
}
}
else
{
if (history.OrganizationId.HasValue || history.UserId != cipher.UserId)
{
throw new NotFoundException();
}
}

var restoredCipher = await _cipherService.RestoreFromHistoryAsync(cipher, history, user.Id);
var organizationAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();

return new CipherResponseModel(restoredCipher, user, organizationAbilities, _globalSettings);
}

[HttpGet("{id}/full-details")]
[Obsolete("This endpoint is deprecated. Use GET details method instead.")]
public async Task<CipherDetailsResponseModel> GetFullDetails(Guid id)
Expand Down Expand Up @@ -249,7 +304,7 @@ public async Task<CipherMiniResponseModel> PostAdmin([FromBody] CipherCreateRequ
}

[HttpPut("{id}")]
public async Task<CipherResponseModel> Put(Guid id, [FromBody] CipherRequestModel model)
public async Task<ActionResult<CipherResponseModel>> Put(Guid id, [FromBody] CipherRequestModel model)
{
var user = await _userService.GetUserByPrincipalAsync(User);
var cipher = await GetByIdAsync(id, user.Id);
Expand Down Expand Up @@ -279,25 +334,32 @@ public async Task<CipherResponseModel> Put(Guid id, [FromBody] CipherRequestMode
"then try again.");
}

await _cipherService.SaveDetailsAsync(model.ToCipherDetails(cipher), user.Id, model.LastKnownRevisionDate, collectionIds);
try
{
await _cipherService.SaveDetailsAsync(model.ToCipherDetails(cipher), user.Id, model.LastKnownRevisionDate, collectionIds);

var response = new CipherResponseModel(
cipher,
user,
await _applicationCacheService.GetOrganizationAbilitiesAsync(),
_globalSettings);
return response;
var response = new CipherResponseModel(
cipher,
user,
await _applicationCacheService.GetOrganizationAbilitiesAsync(),
_globalSettings);
return Ok(response);
}
catch (SyncConflictException ex)
{
return await BuildConflictResponseAsync(ex, user);
}
}

[HttpPost("{id}")]
[Obsolete("This endpoint is deprecated. Use PUT method instead.")]
public async Task<CipherResponseModel> PostPut(Guid id, [FromBody] CipherRequestModel model)
public Task<ActionResult<CipherResponseModel>> PostPut(Guid id, [FromBody] CipherRequestModel model)
{
return await Put(id, model);
return Put(id, model);
}

[HttpPut("{id}/admin")]
public async Task<CipherMiniResponseModel> PutAdmin(Guid id, [FromBody] CipherRequestModel model)
public async Task<ActionResult<CipherMiniResponseModel>> PutAdmin(Guid id, [FromBody] CipherRequestModel model)
{
var userId = _userService.GetProperUserId(User).Value;
var cipher = await _cipherRepository.GetOrganizationDetailsByIdAsync(id);
Expand All @@ -323,17 +385,24 @@ public async Task<CipherMiniResponseModel> PutAdmin(Guid id, [FromBody] CipherRe
var collectionIds = (await _collectionCipherRepository.GetManyByUserIdCipherIdAsync(userId, id)).Select(c => c.CollectionId).ToList();
// object cannot be a descendant of CipherDetails, so let's clone it.
var cipherClone = model.ToCipher(cipher).Clone();
await _cipherService.SaveAsync(cipherClone, userId, model.LastKnownRevisionDate, collectionIds, true, false);
try
{
await _cipherService.SaveAsync(cipherClone, userId, model.LastKnownRevisionDate, collectionIds, true, false);

var response = new CipherMiniResponseModel(cipherClone, _globalSettings, cipher.OrganizationUseTotp);
return response;
var response = new CipherMiniResponseModel(cipherClone, _globalSettings, cipher.OrganizationUseTotp);
return Ok(response);
}
catch (SyncConflictException ex)
{
return await BuildAdminConflictResponseAsync(ex);
}
}

[HttpPost("{id}/admin")]
[Obsolete("This endpoint is deprecated. Use PUT method instead.")]
public async Task<CipherMiniResponseModel> PostPutAdmin(Guid id, [FromBody] CipherRequestModel model)
public Task<ActionResult<CipherMiniResponseModel>> PostPutAdmin(Guid id, [FromBody] CipherRequestModel model)
{
return await PutAdmin(id, model);
return PutAdmin(id, model);
}

[HttpGet("organization-details")]
Expand Down Expand Up @@ -712,30 +781,37 @@ private async Task<bool> CanEditItemsInCollections(Guid organizationId, IEnumera
}

[HttpPut("{id}/partial")]
public async Task<CipherResponseModel> PutPartial(Guid id, [FromBody] CipherPartialRequestModel model)
public async Task<ActionResult<CipherResponseModel>> PutPartial(Guid id, [FromBody] CipherPartialRequestModel model)
{
var user = await _userService.GetUserByPrincipalAsync(User);
var folderId = string.IsNullOrWhiteSpace(model.FolderId) ? null : (Guid?)new Guid(model.FolderId);
await _cipherRepository.UpdatePartialAsync(id, user.Id, folderId, model.Favorite);
try
{
await _cipherRepository.UpdatePartialAsync(id, user.Id, folderId, model.Favorite);

var cipher = await GetByIdAsync(id, user.Id);
var response = new CipherResponseModel(
cipher,
user,
await _applicationCacheService.GetOrganizationAbilitiesAsync(),
_globalSettings);
return response;
var cipher = await GetByIdAsync(id, user.Id);
var response = new CipherResponseModel(
cipher,
user,
await _applicationCacheService.GetOrganizationAbilitiesAsync(),
_globalSettings);
return Ok(response);
}
catch (SyncConflictException ex)
{
return await BuildConflictResponseAsync(ex, user);
}
}

[HttpPost("{id}/partial")]
[Obsolete("This endpoint is deprecated. Use PUT method instead.")]
public async Task<CipherResponseModel> PostPartial(Guid id, [FromBody] CipherPartialRequestModel model)
public Task<ActionResult<CipherResponseModel>> PostPartial(Guid id, [FromBody] CipherPartialRequestModel model)
{
return await PutPartial(id, model);
return PutPartial(id, model);
}

[HttpPut("{id}/share")]
public async Task<CipherResponseModel> PutShare(Guid id, [FromBody] CipherShareRequestModel model)
public async Task<ActionResult<CipherResponseModel>> PutShare(Guid id, [FromBody] CipherShareRequestModel model)
{
var user = await _userService.GetUserByPrincipalAsync(User);
var cipher = await _cipherRepository.GetByIdAsync(id);
Expand Down Expand Up @@ -763,23 +839,30 @@ public async Task<CipherResponseModel> PutShare(Guid id, [FromBody] CipherShareR
ValidateClientVersionForFido2CredentialSupport(cipher);

var original = cipher.Clone();
await _cipherService.ShareAsync(original, model.Cipher.ToCipher(cipher), new Guid(model.Cipher.OrganizationId),
model.CollectionIds.Select(c => new Guid(c)), user.Id, model.Cipher.LastKnownRevisionDate);
try
{
await _cipherService.ShareAsync(original, model.Cipher.ToCipher(cipher), new Guid(model.Cipher.OrganizationId),
model.CollectionIds.Select(c => new Guid(c)), user.Id, model.Cipher.LastKnownRevisionDate);

var sharedCipher = await GetByIdAsync(id, user.Id);
var response = new CipherResponseModel(
sharedCipher,
user,
await _applicationCacheService.GetOrganizationAbilitiesAsync(),
_globalSettings);
return response;
var sharedCipher = await GetByIdAsync(id, user.Id);
var response = new CipherResponseModel(
sharedCipher,
user,
await _applicationCacheService.GetOrganizationAbilitiesAsync(),
_globalSettings);
return Ok(response);
}
catch (SyncConflictException ex)
{
return await BuildConflictResponseAsync(ex, user);
}
}

[HttpPost("{id}/share")]
[Obsolete("This endpoint is deprecated. Use PUT method instead.")]
public async Task<CipherResponseModel> PostShare(Guid id, [FromBody] CipherShareRequestModel model)
public Task<ActionResult<CipherResponseModel>> PostShare(Guid id, [FromBody] CipherShareRequestModel model)
{
return await PutShare(id, model);
return PutShare(id, model);
}

[HttpPut("{id}/collections")]
Expand Down Expand Up @@ -1642,6 +1725,37 @@ private async Task<CipherDetails> GetByIdAsync(Guid cipherId, Guid userId)
return await _cipherRepository.GetByIdAsync(cipherId, userId);
}

private async Task<ActionResult<CipherResponseModel>> BuildConflictResponseAsync(SyncConflictException exception, User user)
{
var serverCipher = await _cipherRepository.GetByIdAsync(exception.ServerCipher.Id, user.Id);
if (serverCipher == null)
{
return Conflict();
}

var conflictResponse = new CipherResponseModel(
serverCipher,
user,
await _applicationCacheService.GetOrganizationAbilitiesAsync(),
_globalSettings);
return Conflict(conflictResponse);
}

private async Task<ActionResult<CipherMiniResponseModel>> BuildAdminConflictResponseAsync(SyncConflictException exception)
{
var serverCipher = await _cipherRepository.GetOrganizationDetailsByIdAsync(exception.ServerCipher.Id);
if (serverCipher == null)
{
return Conflict();
}

var response = new CipherMiniResponseModel(
serverCipher,
_globalSettings,
serverCipher.OrganizationUseTotp);
return Conflict(response);
}

private DateTime? GetLastKnownRevisionDateFromForm()
{
DateTime? lastKnownRevisionDate = null;
Expand Down
55 changes: 55 additions & 0 deletions src/Api/Vault/Models/Response/CipherHistoryResponseModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// FIXME: Update this file to be null safe and then delete the line below
#nullable disable

using System;
using Bit.Core.Models.Api;
using Bit.Core.Vault.Entities;
using Bit.Core.Vault.Enums;

namespace Bit.Api.Vault.Models.Response;

public class CipherHistoryResponseModel : ResponseModel
{
public CipherHistoryResponseModel(CipherHistory history, string obj = "cipherHistory")
: base(obj)
{
if (history == null)
{
throw new ArgumentNullException(nameof(history));
}

Id = history.Id;
CipherId = history.CipherId;
UserId = history.UserId;
OrganizationId = history.OrganizationId;
Type = history.Type;
Data = history.Data;
Favorites = history.Favorites;
Folders = history.Folders;
Attachments = history.Attachments;
CreationDate = history.CreationDate;
RevisionDate = history.RevisionDate;
DeletedDate = history.DeletedDate;
Reprompt = history.Reprompt;
Key = history.Key;
ArchivedDate = history.ArchivedDate;
HistoryDate = history.HistoryDate;
}

public Guid Id { get; set; }
public Guid CipherId { get; set; }
public Guid? UserId { get; set; }
public Guid? OrganizationId { get; set; }
public CipherType Type { get; set; }
public string Data { get; set; }
public string Favorites { get; set; }
public string Folders { get; set; }
public string Attachments { get; set; }
public DateTime CreationDate { get; set; }
public DateTime RevisionDate { get; set; }
public DateTime? DeletedDate { get; set; }
public CipherRepromptType? Reprompt { get; set; }
public string Key { get; set; }
public DateTime? ArchivedDate { get; set; }
public DateTime HistoryDate { get; set; }
}
15 changes: 15 additions & 0 deletions src/Core/Exceptions/SyncConflictException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System;
using Bit.Core.Vault.Entities;

namespace Bit.Core.Exceptions;

public class SyncConflictException : ConflictException
{
public SyncConflictException(Cipher serverCipher)
: base("The cipher you are updating is out of date. Please save your work, sync your vault, and try again.")
{
ServerCipher = serverCipher ?? throw new ArgumentNullException(nameof(serverCipher));
}

public Cipher ServerCipher { get; }
}
Loading
Loading