mirror of
https://github.com/bitwarden/server.git
synced 2025-02-22 02:51:33 +01:00
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.
This commit is contained in:
parent
908decac5e
commit
022e404cc5
@ -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<CiphersController> _logger;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
|
||||
public CiphersController(
|
||||
@ -35,6 +40,7 @@ namespace Bit.Api.Controllers
|
||||
IUserService userService,
|
||||
IAttachmentStorageService attachmentStorageService,
|
||||
ICurrentContext currentContext,
|
||||
ILogger<CiphersController> 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<AttachmentUploadDataResponseModel> 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<AttachmentUploadDataResponseModel> 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<ObjectResult> AzureValidateFile()
|
||||
{
|
||||
return await ApiHelpers.HandleAzureEvents(Request, new Dictionary<string, Func<EventGridEvent, Task>>
|
||||
{
|
||||
{
|
||||
"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<string, CipherAttachment.MetaData>();
|
||||
|
||||
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)
|
||||
|
@ -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<AttachmentResponseModel> GetAttachmentData(string id, string cipherId, string attachmentId)
|
||||
{
|
||||
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||
return await _emergencyAccessService.GetAttachmentDownloadAsync(new Guid(id), cipherId, attachmentId, user);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<OkObjectResult> AzureValidateFile()
|
||||
public async Task<ObjectResult> AzureValidateFile()
|
||||
{
|
||||
return await ApiHelpers.HandleAzureEvents(Request, new Dictionary<string, Func<EventGridEvent, Task>>
|
||||
{
|
||||
@ -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);
|
||||
|
@ -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;
|
||||
|
||||
|
@ -12,6 +12,7 @@ namespace Bit.Api.Utilities
|
||||
{
|
||||
public static class ApiHelpers
|
||||
{
|
||||
public static string EventGridKey { get; set; }
|
||||
public async static Task<T> ReadJsonFileFromBody<T>(HttpContext httpContext, IFormFile file, long maxSize = 51200)
|
||||
{
|
||||
T obj = default(T);
|
||||
@ -42,9 +43,16 @@ namespace Bit.Api.Utilities
|
||||
/// <param name="eventTypeHandlers">Dictionary of eventType strings and their associated handlers.</param>
|
||||
/// <returns>OkObjectResult</returns>
|
||||
/// <remarks>Reference https://docs.microsoft.com/en-us/azure/event-grid/receive-events</remarks>
|
||||
public async static Task<OkObjectResult> HandleAzureEvents(HttpRequest request,
|
||||
public async static Task<ObjectResult> HandleAzureEvents(HttpRequest request,
|
||||
Dictionary<string, Func<EventGridEvent, Task>> 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))
|
||||
|
@ -108,7 +108,7 @@ namespace Bit.Api.Utilities
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task GetSendFileAsync(this HttpRequest request, Func<Stream, Task> callback)
|
||||
public static async Task GetFileAsync(this HttpRequest request, Func<Stream, Task> callback)
|
||||
{
|
||||
var boundary = GetBoundary(MediaTypeHeaderValue.Parse(request.ContentType),
|
||||
_defaultFormOptions.MultipartBoundaryLengthLimit);
|
||||
|
10
src/Core/Models/Api/Request/AttachmentRequestModel.cs
Normal file
10
src/Core/Models/Api/Request/AttachmentRequestModel.cs
Normal file
@ -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;
|
||||
}
|
||||
}
|
@ -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") { }
|
||||
}
|
||||
}
|
@ -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]
|
||||
|
@ -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<string> GetAttachmentUploadUrlAsync(Cipher cipher, CipherAttachment.MetaData attachmentData);
|
||||
Task<string> GetAttachmentDownloadUrlAsync(Cipher cipher, CipherAttachment.MetaData attachmentData);
|
||||
Task<(bool, long?)> ValidateFileAsync(Cipher cipher, CipherAttachment.MetaData attachmentData, long leeway);
|
||||
}
|
||||
}
|
||||
|
@ -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<Guid> 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<Guid> cipherIds, Guid deletingUserId, Guid? organizationId = null, bool orgAdmin = false);
|
||||
Task RestoreAsync(Cipher cipher, Guid restoringUserId, bool orgAdmin = false);
|
||||
Task RestoreManyAsync(IEnumerable<CipherDetails> ciphers, Guid restoringUserId);
|
||||
Task UploadFileForExistingAttachmentAsync(Stream stream, Cipher cipher, CipherAttachment.MetaData attachmentId);
|
||||
Task<AttachmentResponseModel> GetAttachmentDownloadDataAsync(Cipher cipher, string attachmentId);
|
||||
Task<bool> ValidateCipherAttachmentFile(Cipher cipher, CipherAttachment.MetaData attachmentData);
|
||||
}
|
||||
}
|
||||
|
@ -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<EmergencyAccessViewResponseModel> ViewAsync(Guid id, User user);
|
||||
Task<AttachmentResponseModel> GetAttachmentDownloadAsync(Guid id, string cipherId, string attachmentId, User user);
|
||||
}
|
||||
}
|
||||
|
@ -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<string, CloudBlobContainer> _attachmentContainers = new Dictionary<string, CloudBlobContainer>();
|
||||
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<string> 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<string> 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)
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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<bool> 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<AttachmentResponseModel> GetAttachmentDownloadDataAsync(Cipher cipher, string attachmentId)
|
||||
{
|
||||
var attachments = cipher?.GetAttachments() ?? new Dictionary<string, CipherAttachment.MetaData>();
|
||||
|
||||
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<long> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<User> 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<AttachmentResponseModel> 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);
|
||||
|
@ -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<string> 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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<string> 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
|
Loading…
Reference in New Issue
Block a user