From 022e404cc51830f640fa4ae7806e974c33ed93d2 Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Tue, 30 Mar 2021 18:41:14 -0500 Subject: [PATCH] Attachment blob upload (#1229) * Add Cipher attachment upload endpoints * Add validation bool to attachment storage data This bool is used to determine whether or not to renew upload links * Add model to request a new attachment to be made for later upload * Add model to respond with created attachment. The two cipher properties represent the two different cipher model types that can be returned. Cipher Response from personal items and mini response from organizations * Create Azure SAS-authorized upload links for both one-shot and block uploads * Add service methods to handle delayed upload and file size validation * Add emergency access method for downloading attachments direct from Azure * Add new attachment storage methods to other services * Update service interfaces * Log event grid exceptions * Limit Send and Attachment Size to 500MB * capitalize Key property * Add key validation to Azure Event Grid endpoint * Delete blob for unexpected blob creation events * Set Event Grid key at API startup * Change renew attachment upload url request path to match Send * Shore up attachment cleanup method. As long as we have the required information, we should always delete attachments from each the Repository, the cipher in memory, and the file storage service to ensure they're all synched. --- src/Api/Controllers/CiphersController.cs | 138 ++++++++-- .../Controllers/EmergencyAccessController.cs | 7 + src/Api/Controllers/SendsController.cs | 13 +- src/Api/Startup.cs | 6 + src/Api/Utilities/ApiHelpers.cs | 10 +- src/Api/Utilities/MultipartFormDataHelper.cs | 2 +- .../Api/Request/AttachmentRequestModel.cs | 10 + .../AttachmentUploadDataResponseModel.cs | 15 ++ src/Core/Models/Data/CipherAttachment.cs | 1 + .../Services/IAttachmentStorageService.cs | 10 +- src/Core/Services/ICipherService.cs | 7 + src/Core/Services/IEmergencyAccessService.cs | 2 + .../AzureAttachmentStorageService.cs | 149 ++++++++--- .../AzureSendFileStorageService.cs | 6 +- .../Services/Implementations/CipherService.cs | 238 +++++++++++++----- .../Implementations/EmergencyAccessService.cs | 17 ++ .../LocalAttachmentStorageService.cs | 22 ++ .../Services/Implementations/SendService.cs | 5 +- .../NoopAttachmentStorageService.cs | 11 + src/Core/Settings/GlobalSettings.cs | 1 + 20 files changed, 556 insertions(+), 114 deletions(-) create mode 100644 src/Core/Models/Api/Request/AttachmentRequestModel.cs create mode 100644 src/Core/Models/Api/Response/AttachmentUploadDataResponseModel.cs diff --git a/src/Api/Controllers/CiphersController.cs b/src/Api/Controllers/CiphersController.cs index 27ee078d95..669bcd0203 100644 --- a/src/Api/Controllers/CiphersController.cs +++ b/src/Api/Controllers/CiphersController.cs @@ -12,7 +12,11 @@ using Bit.Api.Utilities; using System.Collections.Generic; using Bit.Core.Models.Table; using Bit.Core.Settings; - +using Core.Models.Data; +using Microsoft.Azure.EventGrid.Models; +using Bit.Core.Models.Data; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; namespace Bit.Api.Controllers { @@ -26,6 +30,7 @@ namespace Bit.Api.Controllers private readonly IUserService _userService; private readonly IAttachmentStorageService _attachmentStorageService; private readonly ICurrentContext _currentContext; + private readonly ILogger _logger; private readonly GlobalSettings _globalSettings; public CiphersController( @@ -35,6 +40,7 @@ namespace Bit.Api.Controllers IUserService userService, IAttachmentStorageService attachmentStorageService, ICurrentContext currentContext, + ILogger logger, GlobalSettings globalSettings) { _cipherRepository = cipherRepository; @@ -43,6 +49,7 @@ namespace Bit.Api.Controllers _userService = userService; _attachmentStorageService = attachmentStorageService; _currentContext = currentContext; + _logger = logger; _globalSettings = globalSettings; } @@ -562,6 +569,82 @@ namespace Bit.Api.Controllers } } + [HttpPost("{id}/attachment/v2")] + public async Task PostAttachment(string id, [FromBody] AttachmentRequestModel request) + { + var idGuid = new Guid(id); + var userId = _userService.GetProperUserId(User).Value; + var cipher = request.AdminRequest ? + await _cipherRepository.GetOrganizationDetailsByIdAsync(idGuid) : + await _cipherRepository.GetByIdAsync(idGuid, userId); + + if (cipher == null || (request.AdminRequest && (!cipher.OrganizationId.HasValue || + !_currentContext.ManageAllCollections(cipher.OrganizationId.Value)))) + { + throw new NotFoundException(); + } + + if (request.FileSize > CipherService.MAX_FILE_SIZE && !_globalSettings.SelfHosted) + { + throw new BadRequestException($"Max file size is {CipherService.MAX_FILE_SIZE_READABLE}."); + } + + + var (attachmentId, uploadUrl) = await _cipherService.CreateAttachmentForDelayedUploadAsync(cipher, request, userId); + return new AttachmentUploadDataResponseModel + { + AttachmentId = attachmentId, + Url = uploadUrl, + FileUploadType = _attachmentStorageService.FileUploadType, + CipherResponse = request.AdminRequest ? null : new CipherResponseModel((CipherDetails)cipher, _globalSettings), + CipherMiniResponse = request.AdminRequest ? new CipherMiniResponseModel(cipher, _globalSettings, cipher.OrganizationUseTotp) : null, + }; + } + + [HttpGet("{id}/attachment/{attachmentId}")] + public async Task RenewFileUploadUrl(string id, string attachmentId) + { + var userId = _userService.GetProperUserId(User).Value; + var cipherId = new Guid(id); + var cipher = await _cipherRepository.GetByIdAsync(cipherId, userId); + var attachments = cipher?.GetAttachments(); + + if (attachments == null || !attachments.ContainsKey(attachmentId)) + { + throw new NotFoundException(); + } + + return new AttachmentUploadDataResponseModel + { + Url = await _attachmentStorageService.GetAttachmentUploadUrlAsync(cipher, attachments[attachmentId]), + FileUploadType = _attachmentStorageService.FileUploadType, + }; + } + + [HttpPost("{id}/attachment/{attachmentId}")] + [DisableFormValueModelBinding] + public async Task PostFileForExistingAttachment(string id, string attachmentId) + { + if (!Request?.ContentType.Contains("multipart/") ?? true) + { + throw new BadRequestException("Invalid content."); + } + + var userId = _userService.GetProperUserId(User).Value; + var cipher = await _cipherRepository.GetByIdAsync(new Guid(id), userId); + var attachments = cipher?.GetAttachments(); + if (attachments == null || !attachments.ContainsKey(attachmentId)) + { + throw new NotFoundException(); + } + var attachmentData = attachments[attachmentId]; + + await Request.GetFileAsync(async (stream) => + { + await _cipherService.UploadFileForExistingAttachmentAsync(stream, cipher, attachmentData); + }); + } + [HttpPost("{id}/attachment")] [RequestSizeLimit(105_906_176)] [DisableFormValueModelBinding] @@ -616,20 +699,7 @@ namespace Bit.Api.Controllers { var userId = _userService.GetProperUserId(User).Value; var cipher = await _cipherRepository.GetByIdAsync(new Guid(id), userId); - var attachments = cipher.GetAttachments(); - - if (!attachments.ContainsKey(attachmentId)) - { - throw new NotFoundException(); - } - - var data = attachments[attachmentId]; - var response = new AttachmentResponseModel(attachmentId, data, cipher, _globalSettings) - { - Url = await _attachmentStorageService.GetAttachmentDownloadUrlAsync(cipher, data) - }; - - return response; + return await _cipherService.GetAttachmentDownloadDataAsync(cipher, attachmentId); } [HttpPost("{id}/attachment/{attachmentId}/share")] @@ -684,6 +754,44 @@ namespace Bit.Api.Controllers await _cipherService.DeleteAttachmentAsync(cipher, attachmentId, userId, true); } + [AllowAnonymous] + [HttpPost("attachment/validate/azure")] + public async Task AzureValidateFile() + { + return await ApiHelpers.HandleAzureEvents(Request, new Dictionary> + { + { + "Microsoft.Storage.BlobCreated", async (eventGridEvent) => + { + try + { + var blobName = eventGridEvent.Subject.Split($"{AzureAttachmentStorageService.EventGridEnabledContainerName}/blobs/")[1]; + var (cipherId, organizationId, attachmentId) = AzureAttachmentStorageService.IdentifiersFromBlobName(blobName); + var cipher = await _cipherRepository.GetByIdAsync(new Guid(cipherId)); + var attachments = cipher?.GetAttachments() ?? new Dictionary(); + + if (cipher == null || !attachments.ContainsKey(attachmentId) || attachments[attachmentId].Validated) + { + if (_attachmentStorageService is AzureSendFileStorageService azureFileStorageService) + { + await azureFileStorageService.DeleteBlobAsync(blobName); + } + + return; + } + + await _cipherService.ValidateCipherAttachmentFile(cipher, attachments[attachmentId]); + } + catch (Exception e) + { + _logger.LogError(e, $"Uncaught exception occurred while handling event grid event: {JsonConvert.SerializeObject(eventGridEvent)}"); + return; + } + } + } + }); + } + private void ValidateAttachment() { if (!Request?.ContentType.Contains("multipart/") ?? true) diff --git a/src/Api/Controllers/EmergencyAccessController.cs b/src/Api/Controllers/EmergencyAccessController.cs index dffdfcd6dc..30b8b787b4 100644 --- a/src/Api/Controllers/EmergencyAccessController.cs +++ b/src/Api/Controllers/EmergencyAccessController.cs @@ -163,5 +163,12 @@ namespace Bit.Api.Controllers var user = await _userService.GetUserByPrincipalAsync(User); return await _emergencyAccessService.ViewAsync(new Guid(id), user); } + + [HttpGet("{id}/{cipherId}/attachment/{attachmentId}")] + public async Task GetAttachmentData(string id, string cipherId, string attachmentId) + { + var user = await _userService.GetUserByPrincipalAsync(User); + return await _emergencyAccessService.GetAttachmentDownloadAsync(new Guid(id), cipherId, attachmentId, user); + } } } diff --git a/src/Api/Controllers/SendsController.cs b/src/Api/Controllers/SendsController.cs index cb812be2bc..438a3db62b 100644 --- a/src/Api/Controllers/SendsController.cs +++ b/src/Api/Controllers/SendsController.cs @@ -190,6 +190,11 @@ namespace Bit.Api.Controllers throw new BadRequestException("Invalid content. File size hint is required."); } + if (model.FileLength.Value > SendService.MAX_FILE_SIZE) + { + throw new BadRequestException($"Max file size is {SendService.MAX_FILE_SIZE_READABLE}."); + } + var userId = _userService.GetProperUserId(User).Value; var (send, data) = model.ToSend(userId, model.File.FileName, _sendService); var uploadUrl = await _sendService.SaveFileSendAsync(send, data, model.FileLength.Value); @@ -240,7 +245,7 @@ namespace Bit.Api.Controllers } var send = await _sendRepository.GetByIdAsync(new Guid(id)); - await Request.GetSendFileAsync(async (stream) => + await Request.GetFileAsync(async (stream) => { await _sendService.UploadFileToExistingSendAsync(stream, send); }); @@ -248,7 +253,7 @@ namespace Bit.Api.Controllers [AllowAnonymous] [HttpPost("file/validate/azure")] - public async Task AzureValidateFile() + public async Task AzureValidateFile() { return await ApiHelpers.HandleAzureEvents(Request, new Dictionary> { @@ -262,6 +267,10 @@ namespace Bit.Api.Controllers var send = await _sendRepository.GetByIdAsync(new Guid(sendId)); if (send == null) { + if (_sendFileStorageService is AzureSendFileStorageService azureSendFileStorageService) + { + await azureSendFileStorageService.DeleteBlobAsync(blobName); + } return; } await _sendService.ValidateSendFile(send); diff --git a/src/Api/Startup.cs b/src/Api/Startup.cs index 0cd132ae23..a0ebc051e2 100644 --- a/src/Api/Startup.cs +++ b/src/Api/Startup.cs @@ -50,6 +50,12 @@ namespace Bit.Api // Data Protection services.AddCustomDataProtectionServices(Environment, globalSettings); + // Event Grid + if (!string.IsNullOrWhiteSpace(globalSettings.EventGridKey)) + { + ApiHelpers.EventGridKey = globalSettings.EventGridKey; + } + // Stripe Billing StripeConfiguration.ApiKey = globalSettings.StripeApiKey; diff --git a/src/Api/Utilities/ApiHelpers.cs b/src/Api/Utilities/ApiHelpers.cs index 20ed178762..689b2e1e87 100644 --- a/src/Api/Utilities/ApiHelpers.cs +++ b/src/Api/Utilities/ApiHelpers.cs @@ -12,6 +12,7 @@ namespace Bit.Api.Utilities { public static class ApiHelpers { + public static string EventGridKey { get; set; } public async static Task ReadJsonFileFromBody(HttpContext httpContext, IFormFile file, long maxSize = 51200) { T obj = default(T); @@ -42,9 +43,16 @@ namespace Bit.Api.Utilities /// Dictionary of eventType strings and their associated handlers. /// OkObjectResult /// Reference https://docs.microsoft.com/en-us/azure/event-grid/receive-events - public async static Task HandleAzureEvents(HttpRequest request, + public async static Task HandleAzureEvents(HttpRequest request, Dictionary> eventTypeHandlers) { + var queryKey = request.Query["key"]; + + if (queryKey != EventGridKey) + { + return new UnauthorizedObjectResult("Authentication failed. Please use a valid key."); + } + var response = string.Empty; var requestContent = await new StreamReader(request.Body).ReadToEndAsync(); if (string.IsNullOrWhiteSpace(requestContent)) diff --git a/src/Api/Utilities/MultipartFormDataHelper.cs b/src/Api/Utilities/MultipartFormDataHelper.cs index 01c4e35823..b7313216f1 100644 --- a/src/Api/Utilities/MultipartFormDataHelper.cs +++ b/src/Api/Utilities/MultipartFormDataHelper.cs @@ -108,7 +108,7 @@ namespace Bit.Api.Utilities } } - public static async Task GetSendFileAsync(this HttpRequest request, Func callback) + public static async Task GetFileAsync(this HttpRequest request, Func callback) { var boundary = GetBoundary(MediaTypeHeaderValue.Parse(request.ContentType), _defaultFormOptions.MultipartBoundaryLengthLimit); diff --git a/src/Core/Models/Api/Request/AttachmentRequestModel.cs b/src/Core/Models/Api/Request/AttachmentRequestModel.cs new file mode 100644 index 0000000000..85ed56cf7f --- /dev/null +++ b/src/Core/Models/Api/Request/AttachmentRequestModel.cs @@ -0,0 +1,10 @@ +namespace Bit.Core.Models.Api +{ + public class AttachmentRequestModel + { + public string Key { get; set; } + public string FileName { get; set; } + public long FileSize { get; set; } + public bool AdminRequest { get; set; } = false; + } +} diff --git a/src/Core/Models/Api/Response/AttachmentUploadDataResponseModel.cs b/src/Core/Models/Api/Response/AttachmentUploadDataResponseModel.cs new file mode 100644 index 0000000000..b02f05d211 --- /dev/null +++ b/src/Core/Models/Api/Response/AttachmentUploadDataResponseModel.cs @@ -0,0 +1,15 @@ +using Bit.Core.Enums; + +namespace Bit.Core.Models.Api +{ + public class AttachmentUploadDataResponseModel : ResponseModel + { + public string AttachmentId { get; set; } + public string Url { get; set; } + public FileUploadType FileUploadType { get; set; } + public CipherResponseModel CipherResponse { get; set; } + public CipherMiniResponseModel CipherMiniResponse { get; set; } + + public AttachmentUploadDataResponseModel() : base("attachment-fileUpload") { } + } +} diff --git a/src/Core/Models/Data/CipherAttachment.cs b/src/Core/Models/Data/CipherAttachment.cs index ea6788e05b..29886537bf 100644 --- a/src/Core/Models/Data/CipherAttachment.cs +++ b/src/Core/Models/Data/CipherAttachment.cs @@ -34,6 +34,7 @@ namespace Bit.Core.Models.Data public string Key { get; set; } public string ContainerName { get; set; } = "attachments"; + public bool Validated { get; set; } = true; // This is stored alongside metadata as an identifier. It does not need repeating in serialization [JsonIgnore] diff --git a/src/Core/Services/IAttachmentStorageService.cs b/src/Core/Services/IAttachmentStorageService.cs index 6dbe0b4fe3..e4a2654802 100644 --- a/src/Core/Services/IAttachmentStorageService.cs +++ b/src/Core/Services/IAttachmentStorageService.cs @@ -1,5 +1,6 @@ using Bit.Core.Models.Data; using Bit.Core.Models.Table; +using Bit.Core.Enums; using System; using System.IO; using System.Threading.Tasks; @@ -8,15 +9,18 @@ namespace Bit.Core.Services { public interface IAttachmentStorageService { - Task UploadNewAttachmentAsync(Stream stream, Cipher cipher, CipherAttachment.MetaData attachment); - Task UploadShareAttachmentAsync(Stream stream, Guid cipherId, Guid organizationId, CipherAttachment.MetaData attachment); + FileUploadType FileUploadType { get; } + Task UploadNewAttachmentAsync(Stream stream, Cipher cipher, CipherAttachment.MetaData attachmentData); + Task UploadShareAttachmentAsync(Stream stream, Guid cipherId, Guid organizationId, CipherAttachment.MetaData attachmentData); Task StartShareAttachmentAsync(Guid cipherId, Guid organizationId, CipherAttachment.MetaData attachmentData); Task RollbackShareAttachmentAsync(Guid cipherId, Guid organizationId, CipherAttachment.MetaData attachmentData, string originalContainer); Task CleanupAsync(Guid cipherId); - Task DeleteAttachmentAsync(Guid cipherId, CipherAttachment.MetaData attachment); + Task DeleteAttachmentAsync(Guid cipherId, CipherAttachment.MetaData attachmentData); Task DeleteAttachmentsForCipherAsync(Guid cipherId); Task DeleteAttachmentsForOrganizationAsync(Guid organizationId); Task DeleteAttachmentsForUserAsync(Guid userId); + Task GetAttachmentUploadUrlAsync(Cipher cipher, CipherAttachment.MetaData attachmentData); Task GetAttachmentDownloadUrlAsync(Cipher cipher, CipherAttachment.MetaData attachmentData); + Task<(bool, long?)> ValidateFileAsync(Cipher cipher, CipherAttachment.MetaData attachmentData, long leeway); } } diff --git a/src/Core/Services/ICipherService.cs b/src/Core/Services/ICipherService.cs index 95b1468ec4..cdbd32e987 100644 --- a/src/Core/Services/ICipherService.cs +++ b/src/Core/Services/ICipherService.cs @@ -4,6 +4,8 @@ using Bit.Core.Models.Table; using Core.Models.Data; using System; using System.IO; +using Bit.Core.Models.Api; +using Bit.Core.Models.Data; namespace Bit.Core.Services { @@ -13,6 +15,8 @@ namespace Bit.Core.Services bool skipPermissionCheck = false, bool limitCollectionScope = true); Task SaveDetailsAsync(CipherDetails cipher, Guid savingUserId, DateTime? lastKnownRevisionDate, IEnumerable collectionIds = null, bool skipPermissionCheck = false); + Task<(string attachmentId, string uploadUrl)> CreateAttachmentForDelayedUploadAsync(Cipher cipher, + AttachmentRequestModel request, Guid savingUserId); Task CreateAttachmentAsync(Cipher cipher, Stream stream, string fileName, string key, long requestLength, Guid savingUserId, bool orgAdmin = false); Task CreateAttachmentShareAsync(Cipher cipher, Stream stream, long requestLength, string attachmentId, @@ -37,5 +41,8 @@ namespace Bit.Core.Services Task SoftDeleteManyAsync(IEnumerable cipherIds, Guid deletingUserId, Guid? organizationId = null, bool orgAdmin = false); Task RestoreAsync(Cipher cipher, Guid restoringUserId, bool orgAdmin = false); Task RestoreManyAsync(IEnumerable ciphers, Guid restoringUserId); + Task UploadFileForExistingAttachmentAsync(Stream stream, Cipher cipher, CipherAttachment.MetaData attachmentId); + Task GetAttachmentDownloadDataAsync(Cipher cipher, string attachmentId); + Task ValidateCipherAttachmentFile(Cipher cipher, CipherAttachment.MetaData attachmentData); } } diff --git a/src/Core/Services/IEmergencyAccessService.cs b/src/Core/Services/IEmergencyAccessService.cs index aa28d0021b..a8042e89a7 100644 --- a/src/Core/Services/IEmergencyAccessService.cs +++ b/src/Core/Services/IEmergencyAccessService.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using Bit.Core.Enums; +using Bit.Core.Models.Api; using Bit.Core.Models.Api.Response; using Bit.Core.Models.Data; using Bit.Core.Models.Table; @@ -26,5 +27,6 @@ namespace Bit.Core.Services Task SendNotificationsAsync(); Task HandleTimedOutRequestsAsync(); Task ViewAsync(Guid id, User user); + Task GetAttachmentDownloadAsync(Guid id, string cipherId, string attachmentId, User user); } } diff --git a/src/Core/Services/Implementations/AzureAttachmentStorageService.cs b/src/Core/Services/Implementations/AzureAttachmentStorageService.cs index f84454f270..c4147a01a2 100644 --- a/src/Core/Services/Implementations/AzureAttachmentStorageService.cs +++ b/src/Core/Services/Implementations/AzureAttachmentStorageService.cs @@ -7,16 +7,44 @@ using Bit.Core.Models.Data; using Bit.Core.Models.Table; using Bit.Core.Settings; using System.Collections.Generic; +using Bit.Core.Enums; namespace Bit.Core.Services { public class AzureAttachmentStorageService : IAttachmentStorageService { + public FileUploadType FileUploadType => FileUploadType.Azure; + public const string EventGridEnabledContainerName = "attachments-v2"; private const string _defaultContainerName = "attachments"; private readonly static string[] _attachmentContainerName = { "attachments", "attachments-v2" }; - private static readonly TimeSpan downloadLinkLiveTime = TimeSpan.FromMinutes(1); + private static readonly TimeSpan blobLinkLiveTime = TimeSpan.FromMinutes(1); private readonly CloudBlobClient _blobClient; private readonly Dictionary _attachmentContainers = new Dictionary(); + private string BlobName(Guid cipherId, CipherAttachment.MetaData attachmentData, Guid? organizationId = null, bool temp = false) => + string.Concat( + temp ? "temp/" : "", + $"{cipherId}/", + organizationId != null ? $"{organizationId.Value}/" : "", + attachmentData.AttachmentId + ); + + public static (string cipherId, string organizationId, string attachmentId) IdentifiersFromBlobName(string blobName) { + var parts = blobName.Split('/'); + switch (parts.Length) { + case 4: + return (parts[1], parts[2], parts[3]); + case 3: + if (parts[0] == "temp") { + return (parts[1], null, parts[2]); + } else { + return (parts[0], parts[1], parts[2]); + } + case 2: + return (parts[0], null, parts[1]); + default: + throw new Exception("Cannot determine cipher information from blob name"); + } + } public AzureAttachmentStorageService( GlobalSettings globalSettings) @@ -28,21 +56,35 @@ namespace Bit.Core.Services public async Task GetAttachmentDownloadUrlAsync(Cipher cipher, CipherAttachment.MetaData attachmentData) { await InitAsync(attachmentData.ContainerName); - var blob = _attachmentContainers[attachmentData.ContainerName].GetBlockBlobReference($"{cipher.Id}/{attachmentData.AttachmentId}"); + var blob = _attachmentContainers[attachmentData.ContainerName].GetBlockBlobReference(BlobName(cipher.Id, attachmentData)); var accessPolicy = new SharedAccessBlobPolicy() { - SharedAccessExpiryTime = DateTime.UtcNow.Add(downloadLinkLiveTime), + SharedAccessExpiryTime = DateTime.UtcNow.Add(blobLinkLiveTime), Permissions = SharedAccessBlobPermissions.Read }; return blob.Uri + blob.GetSharedAccessSignature(accessPolicy); } - public async Task UploadNewAttachmentAsync(Stream stream, Cipher cipher, CipherAttachment.MetaData attachment) + public async Task GetAttachmentUploadUrlAsync(Cipher cipher, CipherAttachment.MetaData attachmentData) { - attachment.ContainerName = _defaultContainerName; + await InitAsync(EventGridEnabledContainerName); + var blob = _attachmentContainers[EventGridEnabledContainerName].GetBlockBlobReference(BlobName(cipher.Id, attachmentData)); + attachmentData.ContainerName = EventGridEnabledContainerName; + var accessPolicy = new SharedAccessBlobPolicy() + { + SharedAccessExpiryTime = DateTime.UtcNow.Add(blobLinkLiveTime), + Permissions = SharedAccessBlobPermissions.Create | SharedAccessBlobPermissions.Write, + }; + + return blob.Uri + blob.GetSharedAccessSignature(accessPolicy); + } + + public async Task UploadNewAttachmentAsync(Stream stream, Cipher cipher, CipherAttachment.MetaData attachmentData) + { + attachmentData.ContainerName = _defaultContainerName; await InitAsync(_defaultContainerName); - var blob = _attachmentContainers[_defaultContainerName].GetBlockBlobReference($"{cipher.Id}/{attachment.AttachmentId}"); + var blob = _attachmentContainers[_defaultContainerName].GetBlockBlobReference(BlobName(cipher.Id, attachmentData)); blob.Metadata.Add("cipherId", cipher.Id.ToString()); if (cipher.UserId.HasValue) { @@ -52,7 +94,7 @@ namespace Bit.Core.Services { blob.Metadata.Add("organizationId", cipher.OrganizationId.Value.ToString()); } - blob.Properties.ContentDisposition = $"attachment; filename=\"{attachment.AttachmentId}\""; + blob.Properties.ContentDisposition = $"attachment; filename=\"{attachmentData.AttachmentId}\""; await blob.UploadFromStreamAsync(stream); } @@ -60,7 +102,8 @@ namespace Bit.Core.Services { attachmentData.ContainerName = _defaultContainerName; await InitAsync(_defaultContainerName); - var blob = _attachmentContainers[_defaultContainerName].GetBlockBlobReference($"temp/{cipherId}/{organizationId}/{attachmentData.AttachmentId}"); + var blob = _attachmentContainers[_defaultContainerName].GetBlockBlobReference( + BlobName(cipherId, attachmentData, organizationId, temp: true)); blob.Metadata.Add("cipherId", cipherId.ToString()); blob.Metadata.Add("organizationId", organizationId.ToString()); blob.Properties.ContentDisposition = $"attachment; filename=\"{attachmentData.AttachmentId}\""; @@ -70,20 +113,22 @@ namespace Bit.Core.Services public async Task StartShareAttachmentAsync(Guid cipherId, Guid organizationId, CipherAttachment.MetaData data) { await InitAsync(data.ContainerName); - var source = _attachmentContainers[data.ContainerName].GetBlockBlobReference($"temp/{cipherId}/{organizationId}/{data.AttachmentId}"); + var source = _attachmentContainers[data.ContainerName].GetBlockBlobReference( + BlobName(cipherId, data, organizationId, temp: true)); if (!(await source.ExistsAsync())) { return; } await InitAsync(_defaultContainerName); - var dest = _attachmentContainers[_defaultContainerName].GetBlockBlobReference($"{cipherId}/{data.AttachmentId}"); + var dest = _attachmentContainers[_defaultContainerName].GetBlockBlobReference(BlobName(cipherId, data)); if (!(await dest.ExistsAsync())) { return; } - var original = _attachmentContainers[_defaultContainerName].GetBlockBlobReference($"temp/{cipherId}/{data.AttachmentId}"); + var original = _attachmentContainers[_defaultContainerName].GetBlockBlobReference( + BlobName(cipherId, data, temp: true)); await original.DeleteIfExistsAsync(); await original.StartCopyAsync(dest); @@ -94,30 +139,83 @@ namespace Bit.Core.Services public async Task RollbackShareAttachmentAsync(Guid cipherId, Guid organizationId, CipherAttachment.MetaData attachmentData, string originalContainer) { await InitAsync(attachmentData.ContainerName); - var source = _attachmentContainers[attachmentData.ContainerName].GetBlockBlobReference($"temp/{cipherId}/{organizationId}/{attachmentData.AttachmentId}"); + var source = _attachmentContainers[attachmentData.ContainerName].GetBlockBlobReference( + BlobName(cipherId, attachmentData, organizationId, temp: true)); await source.DeleteIfExistsAsync(); await InitAsync(originalContainer); - var original = _attachmentContainers[originalContainer].GetBlockBlobReference($"temp/{cipherId}/{attachmentData.AttachmentId}"); + var original = _attachmentContainers[originalContainer].GetBlockBlobReference( + BlobName(cipherId, attachmentData, temp: true)); if (!(await original.ExistsAsync())) { return; } - var dest = _attachmentContainers[originalContainer].GetBlockBlobReference($"{cipherId}/{attachmentData.AttachmentId}"); + var dest = _attachmentContainers[originalContainer].GetBlockBlobReference( + BlobName(cipherId, attachmentData)); await dest.DeleteIfExistsAsync(); await dest.StartCopyAsync(original); await original.DeleteIfExistsAsync(); } - public async Task DeleteAttachmentAsync(Guid cipherId, CipherAttachment.MetaData attachment) + public async Task DeleteAttachmentAsync(Guid cipherId, CipherAttachment.MetaData attachmentData) { - await InitAsync(attachment.ContainerName); - var blobName = $"{cipherId}/{attachment.AttachmentId}"; - var blob = _attachmentContainers[attachment.ContainerName].GetBlockBlobReference(blobName); + await InitAsync(attachmentData.ContainerName); + var blob = _attachmentContainers[attachmentData.ContainerName].GetBlockBlobReference( + BlobName(cipherId, attachmentData)); await blob.DeleteIfExistsAsync(); } + public async Task CleanupAsync(Guid cipherId) => await DeleteAttachmentsForPathAsync($"temp/{cipherId}"); + + public async Task DeleteAttachmentsForCipherAsync(Guid cipherId) => + await DeleteAttachmentsForPathAsync(cipherId.ToString()); + + public async Task DeleteAttachmentsForOrganizationAsync(Guid organizationId) + { + await InitAsync(_defaultContainerName); + } + + public async Task DeleteAttachmentsForUserAsync(Guid userId) + { + await InitAsync(_defaultContainerName); + } + + public async Task<(bool, long?)> ValidateFileAsync(Cipher cipher, CipherAttachment.MetaData attachmentData, long leeway) + { + await InitAsync(attachmentData.ContainerName); + + var blob = _attachmentContainers[attachmentData.ContainerName].GetBlockBlobReference(BlobName(cipher.Id, attachmentData)); + + if (!blob.Exists()) + { + return (false, null); + } + + blob.FetchAttributes(); + + blob.Metadata["cipherId"] = cipher.Id.ToString(); + if (cipher.UserId.HasValue) + { + blob.Metadata["userId"] = cipher.UserId.Value.ToString(); + } + else + { + blob.Metadata["organizationId"] = cipher.OrganizationId.Value.ToString(); + } + blob.Properties.ContentDisposition = $"attachment; filename=\"{attachmentData.AttachmentId}\""; + blob.SetMetadata(); + blob.SetProperties(); + + var length = blob.Properties.Length; + if (length < attachmentData.Size - leeway || length > attachmentData.Size + leeway) + { + return (false, length); + } + + return (true, length); + } + private async Task DeleteAttachmentsForPathAsync(string path) { foreach (var container in _attachmentContainerName) @@ -145,21 +243,6 @@ namespace Bit.Core.Services } } - - public async Task CleanupAsync(Guid cipherId) => await DeleteAttachmentsForPathAsync($"temp/{cipherId}"); - - public async Task DeleteAttachmentsForCipherAsync(Guid cipherId) => await DeleteAttachmentsForPathAsync(cipherId.ToString()); - - public async Task DeleteAttachmentsForOrganizationAsync(Guid organizationId) - { - await InitAsync(_defaultContainerName); - } - - public async Task DeleteAttachmentsForUserAsync(Guid userId) - { - await InitAsync(_defaultContainerName); - } - private async Task InitAsync(string containerName) { if (!_attachmentContainers.ContainsKey(containerName) || _attachmentContainers[containerName] == null) diff --git a/src/Core/Services/Implementations/AzureSendFileStorageService.cs b/src/Core/Services/Implementations/AzureSendFileStorageService.cs index 62f998d72c..7009878c94 100644 --- a/src/Core/Services/Implementations/AzureSendFileStorageService.cs +++ b/src/Core/Services/Implementations/AzureSendFileStorageService.cs @@ -44,10 +44,12 @@ namespace Bit.Core.Services await blob.UploadFromStreamAsync(stream); } - public async Task DeleteFileAsync(Send send, string fileId) + public async Task DeleteFileAsync(Send send, string fileId) => await DeleteBlobAsync(BlobName(send, fileId)); + + public async Task DeleteBlobAsync(string blobName) { await InitAsync(); - var blob = _sendFilesContainer.GetBlockBlobReference(BlobName(send, fileId)); + var blob = _sendFilesContainer.GetBlockBlobReference(blobName); await blob.DeleteIfExistsAsync(); } diff --git a/src/Core/Services/Implementations/CipherService.cs b/src/Core/Services/Implementations/CipherService.cs index c781108f8e..220a29dfef 100644 --- a/src/Core/Services/Implementations/CipherService.cs +++ b/src/Core/Services/Implementations/CipherService.cs @@ -12,11 +12,14 @@ using System.IO; using Bit.Core.Enums; using Bit.Core.Utilities; using Bit.Core.Settings; +using Bit.Core.Models.Api; namespace Bit.Core.Services { public class CipherService : ICipherService { + public const long MAX_FILE_SIZE = 500L * 1024L * 1024L; // 500MB + public const string MAX_FILE_SIZE_READABLE = "500 MB"; private readonly ICipherRepository _cipherRepository; private readonly IFolderRepository _folderRepository; private readonly ICollectionRepository _collectionRepository; @@ -30,6 +33,7 @@ namespace Bit.Core.Services private readonly IUserService _userService; private readonly IPolicyRepository _policyRepository; private readonly GlobalSettings _globalSettings; + private const long _fileSizeLeeway = 1024L * 1024L; // 1MB public CipherService( ICipherRepository cipherRepository, @@ -130,7 +134,7 @@ namespace Bit.Core.Services { var org = await _organizationUserRepository.GetDetailsByUserAsync(savingUserId, policy.OrganizationId, OrganizationUserStatusType.Confirmed); - if(org != null && org.Enabled && org.UsePolicies + if (org != null && org.Enabled && org.UsePolicies && org.Type != OrganizationUserType.Admin && org.Type != OrganizationUserType.Owner) { throw new BadRequestException("Due to an Enterprise Policy, you are restricted from saving items to your personal vault."); @@ -166,55 +170,56 @@ namespace Bit.Core.Services } } + public async Task UploadFileForExistingAttachmentAsync(Stream stream, Cipher cipher, CipherAttachment.MetaData attachment) + { + if (attachment == null) + { + throw new BadRequestException("Cipher attachment does not exist"); + } + + await _attachmentStorageService.UploadNewAttachmentAsync(stream, cipher, attachment); + + if (!await ValidateCipherAttachmentFile(cipher, attachment)) + { + throw new BadRequestException("File received does not match expected file length."); + } + } + + public async Task<(string attachmentId, string uploadUrl)> CreateAttachmentForDelayedUploadAsync(Cipher cipher, + AttachmentRequestModel request, Guid savingUserId) + { + await ValidateCipherEditForAttachmentAsync(cipher, savingUserId, request.AdminRequest, request.FileSize); + + var attachmentId = Utilities.CoreHelpers.SecureRandomString(32, upper: false, special: false); + var data = new CipherAttachment.MetaData + { + AttachmentId = attachmentId, + FileName = request.FileName, + Key = request.Key, + Size = request.FileSize, + Validated = false, + }; + + var uploadUrl = await _attachmentStorageService.GetAttachmentUploadUrlAsync(cipher, data); + + await _cipherRepository.UpdateAttachmentAsync(new CipherAttachment + { + Id = cipher.Id, + UserId = cipher.UserId, + OrganizationId = cipher.OrganizationId, + AttachmentId = attachmentId, + AttachmentData = JsonConvert.SerializeObject(data) + }); + cipher.AddAttachment(attachmentId, data); + await _pushService.PushSyncCipherUpdateAsync(cipher, null); + + return (attachmentId, uploadUrl); + } + public async Task CreateAttachmentAsync(Cipher cipher, Stream stream, string fileName, string key, long requestLength, Guid savingUserId, bool orgAdmin = false) { - if (!orgAdmin && !(await UserCanEditAsync(cipher, savingUserId))) - { - throw new BadRequestException("You do not have permissions to edit this."); - } - - if (requestLength < 1) - { - throw new BadRequestException("No data to attach."); - } - - var storageBytesRemaining = 0L; - if (cipher.UserId.HasValue) - { - var user = await _userRepository.GetByIdAsync(cipher.UserId.Value); - if (!(await _userService.CanAccessPremium(user))) - { - throw new BadRequestException("You must have premium status to use attachments."); - } - - if (user.Premium) - { - storageBytesRemaining = user.StorageBytesRemaining(); - } - else - { - // Users that get access to file storage/premium from their organization get the default - // 1 GB max storage. - storageBytesRemaining = user.StorageBytesRemaining( - _globalSettings.SelfHosted ? (short)10240 : (short)1); - } - } - else if (cipher.OrganizationId.HasValue) - { - var org = await _organizationRepository.GetByIdAsync(cipher.OrganizationId.Value); - if (!org.MaxStorageGb.HasValue) - { - throw new BadRequestException("This organization cannot use attachments."); - } - - storageBytesRemaining = org.StorageBytesRemaining(); - } - - if (storageBytesRemaining < requestLength) - { - throw new BadRequestException("Not enough storage available."); - } + await ValidateCipherEditForAttachmentAsync(cipher, savingUserId, orgAdmin, requestLength); var attachmentId = Utilities.CoreHelpers.SecureRandomString(32, upper: false, special: false); var data = new CipherAttachment.MetaData @@ -242,6 +247,10 @@ namespace Bit.Core.Services await _cipherRepository.UpdateAttachmentAsync(attachment); await _eventService.LogCipherEventAsync(cipher, Enums.EventType.Cipher_AttachmentCreated); cipher.AddAttachment(attachmentId, data); + + if (!await ValidateCipherAttachmentFile(cipher, data)) { + throw new Exception("Content-Length does not match uploaded file size"); + } } catch { @@ -314,6 +323,56 @@ namespace Bit.Core.Services } } + public async Task ValidateCipherAttachmentFile(Cipher cipher, CipherAttachment.MetaData attachmentData) + { + var (valid, realSize) = await _attachmentStorageService.ValidateFileAsync(cipher, attachmentData, _fileSizeLeeway); + + if (!valid || realSize > MAX_FILE_SIZE) + { + // File reported differs in size from that promised. Must be a rogue client. Delete Send + await DeleteAttachmentAsync(cipher, attachmentData); + return false; + } + // Update Send data if necessary + if (realSize != attachmentData.Size) + { + attachmentData.Size = realSize.Value; + } + attachmentData.Validated = true; + + var updatedAttachment = new CipherAttachment + { + Id = cipher.Id, + UserId = cipher.UserId, + OrganizationId = cipher.OrganizationId, + AttachmentId = attachmentData.AttachmentId, + AttachmentData = JsonConvert.SerializeObject(attachmentData) + }; + + + await _cipherRepository.UpdateAttachmentAsync(updatedAttachment); + + return valid; + } + + public async Task GetAttachmentDownloadDataAsync(Cipher cipher, string attachmentId) + { + var attachments = cipher?.GetAttachments() ?? new Dictionary(); + + if (!attachments.ContainsKey(attachmentId) || attachments[attachmentId].Validated) + { + throw new NotFoundException(); + } + + var data = attachments[attachmentId]; + var response = new AttachmentResponseModel(attachmentId, data, cipher, _globalSettings) + { + Url = await _attachmentStorageService.GetAttachmentDownloadUrlAsync(cipher, data) + }; + + return response; + } + public async Task DeleteAsync(Cipher cipher, Guid deletingUserId, bool orgAdmin = false) { if (!orgAdmin && !(await UserCanEditAsync(cipher, deletingUserId))) @@ -371,14 +430,7 @@ namespace Bit.Core.Services throw new NotFoundException(); } - var data = cipher.GetAttachments()[attachmentId]; - await _cipherRepository.DeleteAttachmentAsync(cipher.Id, attachmentId); - cipher.DeleteAttachment(attachmentId); - await _attachmentStorageService.DeleteAttachmentAsync(cipher.Id, data); - await _eventService.LogCipherEventAsync(cipher, Enums.EventType.Cipher_AttachmentDeleted); - - // push - await _pushService.PushSyncCipherUpdateAsync(cipher, null); + await DeleteAttachmentAsync(cipher, cipher.GetAttachments()[attachmentId]); } public async Task PurgeAsync(Guid organizationId) @@ -765,7 +817,7 @@ namespace Bit.Core.Services { var ciphers = await _cipherRepository.GetManyByUserIdAsync(deletingUserId); deletingCiphers = ciphers.Where(c => cipherIdsSet.Contains(c.Id) && c.Edit).Select(x => (Cipher)x).ToList(); - await _cipherRepository.SoftDeleteAsync(deletingCiphers.Select(c => c.Id), deletingUserId); + await _cipherRepository.SoftDeleteAsync(deletingCiphers.Select(c => c.Id), deletingUserId); } var events = deletingCiphers.Select(c => @@ -852,5 +904,79 @@ namespace Bit.Core.Services ); } } + + private async Task DeleteAttachmentAsync(Cipher cipher, CipherAttachment.MetaData attachmentData) + { + if (attachmentData == null || string.IsNullOrWhiteSpace(attachmentData.AttachmentId)) + { + return; + } + + await _cipherRepository.DeleteAttachmentAsync(cipher.Id, attachmentData.AttachmentId); + cipher.DeleteAttachment(attachmentData.AttachmentId); + await _attachmentStorageService.DeleteAttachmentAsync(cipher.Id, attachmentData); + await _eventService.LogCipherEventAsync(cipher, Enums.EventType.Cipher_AttachmentDeleted); + + // push + await _pushService.PushSyncCipherUpdateAsync(cipher, null); + } + + private async Task ValidateCipherEditForAttachmentAsync(Cipher cipher, Guid savingUserId, bool orgAdmin, + long requestLength) + { + if (!orgAdmin && !(await UserCanEditAsync(cipher, savingUserId))) + { + throw new BadRequestException("You do not have permissions to edit this."); + } + + if (requestLength < 1) + { + throw new BadRequestException("No data to attach."); + } + + var storageBytesRemaining = await StorageBytesRemainingForCipherAsync(cipher); + + if (storageBytesRemaining < requestLength) + { + throw new BadRequestException("Not enough storage available."); + } + } + + private async Task StorageBytesRemainingForCipherAsync(Cipher cipher) + { + var storageBytesRemaining = 0L; + if (cipher.UserId.HasValue) + { + var user = await _userRepository.GetByIdAsync(cipher.UserId.Value); + if (!(await _userService.CanAccessPremium(user))) + { + throw new BadRequestException("You must have premium status to use attachments."); + } + + if (user.Premium) + { + storageBytesRemaining = user.StorageBytesRemaining(); + } + else + { + // Users that get access to file storage/premium from their organization get the default + // 1 GB max storage. + storageBytesRemaining = user.StorageBytesRemaining( + _globalSettings.SelfHosted ? (short)10240 : (short)1); + } + } + else if (cipher.OrganizationId.HasValue) + { + var org = await _organizationRepository.GetByIdAsync(cipher.OrganizationId.Value); + if (!org.MaxStorageGb.HasValue) + { + throw new BadRequestException("This organization cannot use attachments."); + } + + storageBytesRemaining = org.StorageBytesRemaining(); + } + + return storageBytesRemaining; + } } } diff --git a/src/Core/Services/Implementations/EmergencyAccessService.cs b/src/Core/Services/Implementations/EmergencyAccessService.cs index efcd86a6e1..d6a86c1ca8 100644 --- a/src/Core/Services/Implementations/EmergencyAccessService.cs +++ b/src/Core/Services/Implementations/EmergencyAccessService.cs @@ -13,6 +13,7 @@ using Bit.Core.Utilities; using Bit.Core.Settings; using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Identity; +using Bit.Core.Models.Api; namespace Bit.Core.Services { @@ -23,6 +24,7 @@ namespace Bit.Core.Services private readonly IUserRepository _userRepository; private readonly ICipherRepository _cipherRepository; private readonly IPolicyRepository _policyRepository; + private readonly ICipherService _cipherService; private readonly IMailService _mailService; private readonly IUserService _userService; private readonly IDataProtector _dataProtector; @@ -36,6 +38,7 @@ namespace Bit.Core.Services IUserRepository userRepository, ICipherRepository cipherRepository, IPolicyRepository policyRepository, + ICipherService cipherService, IMailService mailService, IUserService userService, IPasswordHasher passwordHasher, @@ -48,6 +51,7 @@ namespace Bit.Core.Services _userRepository = userRepository; _cipherRepository = cipherRepository; _policyRepository = policyRepository; + _cipherService = cipherService; _mailService = mailService; _userService = userService; _passwordHasher = passwordHasher; @@ -352,6 +356,19 @@ namespace Bit.Core.Services return new EmergencyAccessViewResponseModel(_globalSettings, emergencyAccess, ciphers); } + public async Task GetAttachmentDownloadAsync(Guid id, string cipherId, string attachmentId, User requestingUser) + { + var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(id); + + if (!IsValidRequest(emergencyAccess, requestingUser, EmergencyAccessType.View)) + { + throw new BadRequestException("Emergency Access not valid."); + } + + var cipher = await _cipherRepository.GetByIdAsync(new Guid(cipherId), emergencyAccess.GrantorId); + return await _cipherService.GetAttachmentDownloadDataAsync(cipher, attachmentId); + } + private async Task SendInviteAsync(EmergencyAccess emergencyAccess, string invitingUsersName) { var nowMillis = CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow); diff --git a/src/Core/Services/Implementations/LocalAttachmentStorageService.cs b/src/Core/Services/Implementations/LocalAttachmentStorageService.cs index d02c62db6b..d742931a5c 100644 --- a/src/Core/Services/Implementations/LocalAttachmentStorageService.cs +++ b/src/Core/Services/Implementations/LocalAttachmentStorageService.cs @@ -4,6 +4,7 @@ using System; using Bit.Core.Models.Table; using Bit.Core.Models.Data; using Bit.Core.Settings; +using Bit.Core.Enums; namespace Bit.Core.Services { @@ -13,6 +14,8 @@ namespace Bit.Core.Services private readonly string _baseDirPath; private readonly string _baseTempDirPath; + public FileUploadType FileUploadType => FileUploadType.Direct; + public LocalAttachmentStorageService( IGlobalSettings globalSettings) { @@ -173,6 +176,25 @@ namespace Bit.Core.Services organizationId.HasValue ? AttachmentFilePath(OrganizationDirectoryPath(cipherId, organizationId.Value, temp), attachmentId) : AttachmentFilePath(CipherDirectoryPath(cipherId, temp), attachmentId); + public Task GetAttachmentUploadUrlAsync(Cipher cipher, CipherAttachment.MetaData attachmentData) + => Task.FromResult($"{cipher.Id}/attachment/{attachmentData.AttachmentId}"); + public Task<(bool, long?)> ValidateFileAsync(Cipher cipher, CipherAttachment.MetaData attachmentData, long leeway) + { + long? length = null; + var path = AttachmentFilePath(attachmentData.AttachmentId, cipher.Id, temp: false); + if (!File.Exists(path)) + { + return Task.FromResult((false, length)); + } + + length = new FileInfo(path).Length; + if (attachmentData.Size < length - leeway || attachmentData.Size > length + leeway) + { + return Task.FromResult((false, length)); + } + + return Task.FromResult((true, length)); + } } } diff --git a/src/Core/Services/Implementations/SendService.cs b/src/Core/Services/Implementations/SendService.cs index 8afbdef675..d0bdb1ea97 100644 --- a/src/Core/Services/Implementations/SendService.cs +++ b/src/Core/Services/Implementations/SendService.cs @@ -17,6 +17,8 @@ namespace Bit.Core.Services { public class SendService : ISendService { + public const long MAX_FILE_SIZE = 500L * 1024L * 1024L; // 500MB + public const string MAX_FILE_SIZE_READABLE = "500 MB"; private readonly ISendRepository _sendRepository; private readonly IUserRepository _userRepository; private readonly IPolicyRepository _policyRepository; @@ -142,10 +144,11 @@ namespace Bit.Core.Services var (valid, realSize) = await _sendFileStorageService.ValidateFileAsync(send, fileData.Id, fileData.Size, _fileSizeLeeway); - if (!valid) + if (!valid || realSize > MAX_FILE_SIZE) { // File reported differs in size from that promised. Must be a rogue client. Delete Send await DeleteSendAsync(send); + return false; } // Update Send data if necessary diff --git a/src/Core/Services/NoopImplementations/NoopAttachmentStorageService.cs b/src/Core/Services/NoopImplementations/NoopAttachmentStorageService.cs index 795fc6e9fd..e47f45f968 100644 --- a/src/Core/Services/NoopImplementations/NoopAttachmentStorageService.cs +++ b/src/Core/Services/NoopImplementations/NoopAttachmentStorageService.cs @@ -1,6 +1,7 @@ using System; using System.IO; using System.Threading.Tasks; +using Bit.Core.Enums; using Bit.Core.Models.Data; using Bit.Core.Models.Table; @@ -8,6 +9,8 @@ namespace Bit.Core.Services { public class NoopAttachmentStorageService : IAttachmentStorageService { + public FileUploadType FileUploadType => FileUploadType.Direct; + public Task CleanupAsync(Guid cipherId) { return Task.FromResult(0); @@ -58,5 +61,13 @@ namespace Bit.Core.Services return Task.FromResult((string)null); } + public Task GetAttachmentUploadUrlAsync(Cipher cipher, CipherAttachment.MetaData attachmentData) + { + return Task.FromResult(default(string)); + } + public Task<(bool, long?)> ValidateFileAsync(Cipher cipher, CipherAttachment.MetaData attachmentData, long leeway) + { + return Task.FromResult((false, (long?)null)); + } } } diff --git a/src/Core/Settings/GlobalSettings.cs b/src/Core/Settings/GlobalSettings.cs index 280728ab8d..2f1eab29cc 100644 --- a/src/Core/Settings/GlobalSettings.cs +++ b/src/Core/Settings/GlobalSettings.cs @@ -21,6 +21,7 @@ namespace Bit.Core.Settings public virtual bool DisableUserRegistration { get; set; } public virtual bool DisableEmailNewDevice { get; set; } public virtual int OrganizationInviteExpirationHours { get; set; } = 120; // 5 days + public virtual string EventGridKey { get; set; } public virtual InstallationSettings Installation { get; set; } = new InstallationSettings(); public virtual BaseServiceUriSettings BaseServiceUri { get; set; } = new BaseServiceUriSettings(); public virtual SqlSettings SqlServer { get; set; } = new SqlSettings();