Skip to content
36 changes: 31 additions & 5 deletions src/Api/Vault/Controllers/CiphersController.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
๏ปฟ// FIXME: Update this file to be null safe and then delete the line below
#nullable disable

using System.Globalization;
using System.Text.Json;
using Azure.Messaging.EventGrid;
using Bit.Api.Auth.Models.Request.Accounts;
Expand Down Expand Up @@ -1351,7 +1352,7 @@ await _cipherRepository.GetOrganizationDetailsByIdAsync(id) :
}

var (attachmentId, uploadUrl) = await _cipherService.CreateAttachmentForDelayedUploadAsync(cipher,
request.Key, request.FileName, request.FileSize, request.AdminRequest, user.Id);
request.Key, request.FileName, request.FileSize, request.AdminRequest, user.Id, request.LastKnownRevisionDate);
return new AttachmentUploadDataResponseModel
{
AttachmentId = attachmentId,
Expand Down Expand Up @@ -1404,9 +1405,11 @@ public async Task PostFileForExistingAttachment(Guid id, string attachmentId)
throw new NotFoundException();
}

// Extract lastKnownRevisionDate from form data if present
DateTime? lastKnownRevisionDate = GetLastKnownRevisionDateFromForm();
await Request.GetFileAsync(async (stream) =>
{
await _cipherService.UploadFileForExistingAttachmentAsync(stream, cipher, attachmentData);
await _cipherService.UploadFileForExistingAttachmentAsync(stream, cipher, attachmentData, lastKnownRevisionDate);
});
}

Expand All @@ -1425,10 +1428,12 @@ public async Task<CipherResponseModel> PostAttachmentV1(Guid id)
throw new NotFoundException();
}

// Extract lastKnownRevisionDate from form data if present
DateTime? lastKnownRevisionDate = GetLastKnownRevisionDateFromForm();
await Request.GetFileAsync(async (stream, fileName, key) =>
{
await _cipherService.CreateAttachmentAsync(cipher, stream, fileName, key,
Request.ContentLength.GetValueOrDefault(0), user.Id);
Request.ContentLength.GetValueOrDefault(0), user.Id, false, lastKnownRevisionDate);
});

return new CipherResponseModel(
Expand All @@ -1454,10 +1459,13 @@ public async Task<CipherMiniResponseModel> PostAttachmentAdmin(string id)
throw new NotFoundException();
}

// Extract lastKnownRevisionDate from form data if present
DateTime? lastKnownRevisionDate = GetLastKnownRevisionDateFromForm();

await Request.GetFileAsync(async (stream, fileName, key) =>
{
await _cipherService.CreateAttachmentAsync(cipher, stream, fileName, key,
Request.ContentLength.GetValueOrDefault(0), userId, true);
Request.ContentLength.GetValueOrDefault(0), userId, true, lastKnownRevisionDate);
});

return new CipherMiniResponseModel(cipher, _globalSettings, cipher.OrganizationUseTotp);
Expand Down Expand Up @@ -1500,10 +1508,13 @@ public async Task PostAttachmentShare(string id, string attachmentId, Guid organ
throw new NotFoundException();
}

// Extract lastKnownRevisionDate from form data if present
DateTime? lastKnownRevisionDate = GetLastKnownRevisionDateFromForm();

await Request.GetFileAsync(async (stream, fileName, key) =>
{
await _cipherService.CreateAttachmentShareAsync(cipher, stream, fileName, key,
Request.ContentLength.GetValueOrDefault(0), attachmentId, organizationId);
Request.ContentLength.GetValueOrDefault(0), attachmentId, organizationId, lastKnownRevisionDate);
});
}

Expand Down Expand Up @@ -1615,4 +1626,19 @@ private async Task<CipherDetails> GetByIdAsync(Guid cipherId, Guid userId)
{
return await _cipherRepository.GetByIdAsync(cipherId, userId);
}

private DateTime? GetLastKnownRevisionDateFromForm()
{
DateTime? lastKnownRevisionDate = null;
if (Request.Form.TryGetValue("lastKnownRevisionDate", out var dateValue))
{
if (!DateTime.TryParse(dateValue, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var parsedDate))
{
throw new BadRequestException("Invalid lastKnownRevisionDate format.");
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

โ“ Non-blocking. This kind of begs the question, what format are we expecting, and is this specific error being handled by the client? Or is this just a helpful hint for debugging?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe it's the ISO8601 standard - It's more a helpful hint for debugging. If the client is getting this error message, it indicates a bug with the date format in the first place.

}
lastKnownRevisionDate = parsedDate;
}

return lastKnownRevisionDate;
}
}
5 changes: 5 additions & 0 deletions src/Api/Vault/Models/Request/AttachmentRequestModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,9 @@ public class AttachmentRequestModel
public string FileName { get; set; }
public long FileSize { get; set; }
public bool AdminRequest { get; set; } = false;

/// <summary>
/// The last known revision date of the Cipher that this attachment belongs to.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

โ›๏ธ (non-blocking): I think we can use the proper xml documentation here

Suggested change
/// The last known revision date of the Cipher that this attachment belongs to.
/// <summary>
/// The last known revision date of the Cipher that this attachment belongs to.
/// </summary>

/// </summary>
public DateTime? LastKnownRevisionDate { get; set; }
}
8 changes: 4 additions & 4 deletions src/Core/Vault/Services/ICipherService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@ Task SaveAsync(Cipher cipher, Guid savingUserId, DateTime? lastKnownRevisionDate
Task SaveDetailsAsync(CipherDetails cipher, Guid savingUserId, DateTime? lastKnownRevisionDate,
IEnumerable<Guid> collectionIds = null, bool skipPermissionCheck = false);
Task<(string attachmentId, string uploadUrl)> CreateAttachmentForDelayedUploadAsync(Cipher cipher,
string key, string fileName, long fileSize, bool adminRequest, Guid savingUserId);
string key, string fileName, long fileSize, bool adminRequest, Guid savingUserId, DateTime? lastKnownRevisionDate = null);
Task CreateAttachmentAsync(Cipher cipher, Stream stream, string fileName, string key,
long requestLength, Guid savingUserId, bool orgAdmin = false);
long requestLength, Guid savingUserId, bool orgAdmin = false, DateTime? lastKnownRevisionDate = null);
Task CreateAttachmentShareAsync(Cipher cipher, Stream stream, string fileName, string key, long requestLength,
string attachmentId, Guid organizationShareId);
string attachmentId, Guid organizationShareId, DateTime? lastKnownRevisionDate = null);
Task DeleteAsync(CipherDetails cipherDetails, Guid deletingUserId, bool orgAdmin = false);
Task DeleteManyAsync(IEnumerable<Guid> cipherIds, Guid deletingUserId, Guid? organizationId = null, bool orgAdmin = false);
Task<DeleteAttachmentResponseData> DeleteAttachmentAsync(Cipher cipher, string attachmentId, Guid deletingUserId, bool orgAdmin = false);
Expand All @@ -34,7 +34,7 @@ Task<IEnumerable<CipherDetails>> ShareManyAsync(IEnumerable<(CipherDetails ciphe
Task SoftDeleteManyAsync(IEnumerable<Guid> cipherIds, Guid deletingUserId, Guid? organizationId = null, bool orgAdmin = false);
Task RestoreAsync(CipherDetails cipherDetails, Guid restoringUserId, bool orgAdmin = false);
Task<ICollection<CipherOrganizationDetails>> RestoreManyAsync(IEnumerable<Guid> cipherIds, Guid restoringUserId, Guid? organizationId = null, bool orgAdmin = false);
Task UploadFileForExistingAttachmentAsync(Stream stream, Cipher cipher, CipherAttachment.MetaData attachmentId);
Task UploadFileForExistingAttachmentAsync(Stream stream, Cipher cipher, CipherAttachment.MetaData attachmentId, DateTime? lastKnownRevisionDate = null);
Task<AttachmentResponseData> GetAttachmentDownloadDataAsync(Cipher cipher, string attachmentId);
Task<bool> ValidateCipherAttachmentFile(Cipher cipher, CipherAttachment.MetaData attachmentData);
Task ValidateBulkCollectionAssignmentAsync(IEnumerable<Guid> collectionIds, IEnumerable<Guid> cipherIds, Guid userId);
Expand Down
20 changes: 12 additions & 8 deletions src/Core/Vault/Services/Implementations/CipherService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ public async Task SaveAsync(Cipher cipher, Guid savingUserId, DateTime? lastKnow
}
else
{
ValidateCipherLastKnownRevisionDateAsync(cipher, lastKnownRevisionDate);
ValidateCipherLastKnownRevisionDate(cipher, lastKnownRevisionDate);
cipher.RevisionDate = DateTime.UtcNow;
await _cipherRepository.ReplaceAsync(cipher);
await _eventService.LogCipherEventAsync(cipher, Bit.Core.Enums.EventType.Cipher_Updated);
Expand Down Expand Up @@ -168,7 +168,7 @@ public async Task SaveDetailsAsync(CipherDetails cipher, Guid savingUserId, Date
}
else
{
ValidateCipherLastKnownRevisionDateAsync(cipher, lastKnownRevisionDate);
ValidateCipherLastKnownRevisionDate(cipher, lastKnownRevisionDate);
cipher.RevisionDate = DateTime.UtcNow;
await ValidateChangeInCollectionsAsync(cipher, collectionIds, savingUserId);
await ValidateViewPasswordUserAsync(cipher);
Expand All @@ -180,8 +180,9 @@ public async Task SaveDetailsAsync(CipherDetails cipher, Guid savingUserId, Date
}
}

public async Task UploadFileForExistingAttachmentAsync(Stream stream, Cipher cipher, CipherAttachment.MetaData attachment)
public async Task UploadFileForExistingAttachmentAsync(Stream stream, Cipher cipher, CipherAttachment.MetaData attachment, DateTime? lastKnownRevisionDate = null)
{
ValidateCipherLastKnownRevisionDate(cipher, lastKnownRevisionDate);
if (attachment == null)
{
throw new BadRequestException("Cipher attachment does not exist");
Expand All @@ -196,8 +197,9 @@ public async Task UploadFileForExistingAttachmentAsync(Stream stream, Cipher cip
}

public async Task<(string attachmentId, string uploadUrl)> CreateAttachmentForDelayedUploadAsync(Cipher cipher,
string key, string fileName, long fileSize, bool adminRequest, Guid savingUserId)
string key, string fileName, long fileSize, bool adminRequest, Guid savingUserId, DateTime? lastKnownRevisionDate = null)
{
ValidateCipherLastKnownRevisionDate(cipher, lastKnownRevisionDate);
await ValidateCipherEditForAttachmentAsync(cipher, savingUserId, adminRequest, fileSize);

var attachmentId = Utilities.CoreHelpers.SecureRandomString(32, upper: false, special: false);
Expand Down Expand Up @@ -232,8 +234,9 @@ await _cipherRepository.UpdateAttachmentAsync(new CipherAttachment
}

public async Task CreateAttachmentAsync(Cipher cipher, Stream stream, string fileName, string key,
long requestLength, Guid savingUserId, bool orgAdmin = false)
long requestLength, Guid savingUserId, bool orgAdmin = false, DateTime? lastKnownRevisionDate = null)
{
ValidateCipherLastKnownRevisionDate(cipher, lastKnownRevisionDate);
await ValidateCipherEditForAttachmentAsync(cipher, savingUserId, orgAdmin, requestLength);

var attachmentId = Utilities.CoreHelpers.SecureRandomString(32, upper: false, special: false);
Expand Down Expand Up @@ -284,10 +287,11 @@ public async Task CreateAttachmentAsync(Cipher cipher, Stream stream, string fil
}

public async Task CreateAttachmentShareAsync(Cipher cipher, Stream stream, string fileName, string key,
long requestLength, string attachmentId, Guid organizationId)
long requestLength, string attachmentId, Guid organizationId, DateTime? lastKnownRevisionDate = null)
{
try
{
ValidateCipherLastKnownRevisionDate(cipher, lastKnownRevisionDate);
if (requestLength < 1)
{
throw new BadRequestException("No data to attach.");
Expand Down Expand Up @@ -859,7 +863,7 @@ private async Task<bool> UserCanRestoreAsync(CipherDetails cipher, Guid userId)
return NormalCipherPermissions.CanRestore(user, cipher, organizationAbility);
}

private void ValidateCipherLastKnownRevisionDateAsync(Cipher cipher, DateTime? lastKnownRevisionDate)
private void ValidateCipherLastKnownRevisionDate(Cipher cipher, DateTime? lastKnownRevisionDate)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

๐Ÿ‘๐Ÿผ Thanks for renaming this

{
if (cipher.Id == default || !lastKnownRevisionDate.HasValue)
{
Expand Down Expand Up @@ -1007,7 +1011,7 @@ private async Task ValidateCipherCanBeShared(
throw new BadRequestException("Not enough storage available for this organization.");
}

ValidateCipherLastKnownRevisionDateAsync(cipher, lastKnownRevisionDate);
ValidateCipherLastKnownRevisionDate(cipher, lastKnownRevisionDate);
}

private async Task ValidateViewPasswordUserAsync(Cipher cipher)
Expand Down
Loading
Loading