2021-02-22 15:35:16 -06:00
|
|
|
using System.Threading.Tasks;
|
2020-01-10 08:33:13 -05:00
|
|
|
using Microsoft.Azure.Storage;
|
|
|
|
|
using Microsoft.Azure.Storage.Blob;
|
2017-06-15 15:34:12 -04:00
|
|
|
using System.IO;
|
2017-07-10 14:30:12 -04:00
|
|
|
using System;
|
2021-02-22 15:35:16 -06:00
|
|
|
using Bit.Core.Models.Data;
|
2017-07-10 17:08:50 -04:00
|
|
|
using Bit.Core.Models.Table;
|
2021-02-22 15:35:16 -06:00
|
|
|
using Bit.Core.Settings;
|
|
|
|
|
using System.Collections.Generic;
|
2021-03-30 18:41:14 -05:00
|
|
|
using Bit.Core.Enums;
|
2017-06-15 15:34:12 -04:00
|
|
|
|
|
|
|
|
namespace Bit.Core.Services
|
|
|
|
|
{
|
|
|
|
|
public class AzureAttachmentStorageService : IAttachmentStorageService
|
|
|
|
|
{
|
2021-03-30 18:41:14 -05:00
|
|
|
public FileUploadType FileUploadType => FileUploadType.Azure;
|
|
|
|
|
public const string EventGridEnabledContainerName = "attachments-v2";
|
2021-02-22 15:35:16 -06:00
|
|
|
private const string _defaultContainerName = "attachments";
|
|
|
|
|
private readonly static string[] _attachmentContainerName = { "attachments", "attachments-v2" };
|
2021-03-30 18:41:14 -05:00
|
|
|
private static readonly TimeSpan blobLinkLiveTime = TimeSpan.FromMinutes(1);
|
2017-06-15 15:34:12 -04:00
|
|
|
private readonly CloudBlobClient _blobClient;
|
2021-02-22 15:35:16 -06:00
|
|
|
private readonly Dictionary<string, CloudBlobContainer> _attachmentContainers = new Dictionary<string, CloudBlobContainer>();
|
2021-03-30 18:41:14 -05:00
|
|
|
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");
|
|
|
|
|
}
|
|
|
|
|
}
|
2017-06-15 15:34:12 -04:00
|
|
|
|
|
|
|
|
public AzureAttachmentStorageService(
|
|
|
|
|
GlobalSettings globalSettings)
|
|
|
|
|
{
|
2017-06-30 23:01:41 -04:00
|
|
|
var storageAccount = CloudStorageAccount.Parse(globalSettings.Attachment.ConnectionString);
|
2017-06-15 15:34:12 -04:00
|
|
|
_blobClient = storageAccount.CreateCloudBlobClient();
|
|
|
|
|
}
|
|
|
|
|
|
2021-02-22 15:35:16 -06:00
|
|
|
public async Task<string> GetAttachmentDownloadUrlAsync(Cipher cipher, CipherAttachment.MetaData attachmentData)
|
|
|
|
|
{
|
|
|
|
|
await InitAsync(attachmentData.ContainerName);
|
2021-03-30 18:41:14 -05:00
|
|
|
var blob = _attachmentContainers[attachmentData.ContainerName].GetBlockBlobReference(BlobName(cipher.Id, attachmentData));
|
2021-02-22 15:35:16 -06:00
|
|
|
var accessPolicy = new SharedAccessBlobPolicy()
|
|
|
|
|
{
|
2021-03-30 18:41:14 -05:00
|
|
|
SharedAccessExpiryTime = DateTime.UtcNow.Add(blobLinkLiveTime),
|
2021-02-22 15:35:16 -06:00
|
|
|
Permissions = SharedAccessBlobPermissions.Read
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return blob.Uri + blob.GetSharedAccessSignature(accessPolicy);
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-30 18:41:14 -05:00
|
|
|
public async Task<string> GetAttachmentUploadUrlAsync(Cipher cipher, CipherAttachment.MetaData attachmentData)
|
|
|
|
|
{
|
|
|
|
|
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)
|
2017-07-10 14:30:12 -04:00
|
|
|
{
|
2021-03-30 18:41:14 -05:00
|
|
|
attachmentData.ContainerName = _defaultContainerName;
|
2021-02-22 15:35:16 -06:00
|
|
|
await InitAsync(_defaultContainerName);
|
2021-03-30 18:41:14 -05:00
|
|
|
var blob = _attachmentContainers[_defaultContainerName].GetBlockBlobReference(BlobName(cipher.Id, attachmentData));
|
2017-07-10 17:08:50 -04:00
|
|
|
blob.Metadata.Add("cipherId", cipher.Id.ToString());
|
2020-03-27 14:36:37 -04:00
|
|
|
if (cipher.UserId.HasValue)
|
2017-07-10 17:08:50 -04:00
|
|
|
{
|
|
|
|
|
blob.Metadata.Add("userId", cipher.UserId.Value.ToString());
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
blob.Metadata.Add("organizationId", cipher.OrganizationId.Value.ToString());
|
|
|
|
|
}
|
2021-03-30 18:41:14 -05:00
|
|
|
blob.Properties.ContentDisposition = $"attachment; filename=\"{attachmentData.AttachmentId}\"";
|
2017-07-10 17:08:50 -04:00
|
|
|
await blob.UploadFromStreamAsync(stream);
|
2017-07-10 14:30:12 -04:00
|
|
|
}
|
|
|
|
|
|
2021-02-22 15:35:16 -06:00
|
|
|
public async Task UploadShareAttachmentAsync(Stream stream, Guid cipherId, Guid organizationId, CipherAttachment.MetaData attachmentData)
|
2017-07-10 14:30:12 -04:00
|
|
|
{
|
2021-02-22 15:35:16 -06:00
|
|
|
attachmentData.ContainerName = _defaultContainerName;
|
|
|
|
|
await InitAsync(_defaultContainerName);
|
2021-03-30 18:41:14 -05:00
|
|
|
var blob = _attachmentContainers[_defaultContainerName].GetBlockBlobReference(
|
|
|
|
|
BlobName(cipherId, attachmentData, organizationId, temp: true));
|
2017-07-10 17:08:50 -04:00
|
|
|
blob.Metadata.Add("cipherId", cipherId.ToString());
|
|
|
|
|
blob.Metadata.Add("organizationId", organizationId.ToString());
|
2021-02-22 15:35:16 -06:00
|
|
|
blob.Properties.ContentDisposition = $"attachment; filename=\"{attachmentData.AttachmentId}\"";
|
2017-07-10 17:08:50 -04:00
|
|
|
await blob.UploadFromStreamAsync(stream);
|
2017-07-10 14:30:12 -04:00
|
|
|
}
|
|
|
|
|
|
2021-02-22 15:35:16 -06:00
|
|
|
public async Task StartShareAttachmentAsync(Guid cipherId, Guid organizationId, CipherAttachment.MetaData data)
|
2017-06-15 15:34:12 -04:00
|
|
|
{
|
2021-02-22 15:35:16 -06:00
|
|
|
await InitAsync(data.ContainerName);
|
2021-03-30 18:41:14 -05:00
|
|
|
var source = _attachmentContainers[data.ContainerName].GetBlockBlobReference(
|
|
|
|
|
BlobName(cipherId, data, organizationId, temp: true));
|
2020-03-27 14:36:37 -04:00
|
|
|
if (!(await source.ExistsAsync()))
|
2017-07-10 14:30:12 -04:00
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2021-02-22 15:35:16 -06:00
|
|
|
await InitAsync(_defaultContainerName);
|
2021-03-30 18:41:14 -05:00
|
|
|
var dest = _attachmentContainers[_defaultContainerName].GetBlockBlobReference(BlobName(cipherId, data));
|
2020-03-27 14:36:37 -04:00
|
|
|
if (!(await dest.ExistsAsync()))
|
2017-07-10 14:30:12 -04:00
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-30 18:41:14 -05:00
|
|
|
var original = _attachmentContainers[_defaultContainerName].GetBlockBlobReference(
|
|
|
|
|
BlobName(cipherId, data, temp: true));
|
2017-07-10 14:30:12 -04:00
|
|
|
await original.DeleteIfExistsAsync();
|
|
|
|
|
await original.StartCopyAsync(dest);
|
|
|
|
|
|
|
|
|
|
await dest.DeleteIfExistsAsync();
|
|
|
|
|
await dest.StartCopyAsync(source);
|
|
|
|
|
}
|
|
|
|
|
|
2021-02-22 15:35:16 -06:00
|
|
|
public async Task RollbackShareAttachmentAsync(Guid cipherId, Guid organizationId, CipherAttachment.MetaData attachmentData, string originalContainer)
|
2017-06-15 15:34:12 -04:00
|
|
|
{
|
2021-02-22 15:35:16 -06:00
|
|
|
await InitAsync(attachmentData.ContainerName);
|
2021-03-30 18:41:14 -05:00
|
|
|
var source = _attachmentContainers[attachmentData.ContainerName].GetBlockBlobReference(
|
|
|
|
|
BlobName(cipherId, attachmentData, organizationId, temp: true));
|
2017-07-10 16:21:18 -04:00
|
|
|
await source.DeleteIfExistsAsync();
|
|
|
|
|
|
2021-02-22 15:35:16 -06:00
|
|
|
await InitAsync(originalContainer);
|
2021-03-30 18:41:14 -05:00
|
|
|
var original = _attachmentContainers[originalContainer].GetBlockBlobReference(
|
|
|
|
|
BlobName(cipherId, attachmentData, temp: true));
|
2020-03-27 14:36:37 -04:00
|
|
|
if (!(await original.ExistsAsync()))
|
2017-07-10 14:30:12 -04:00
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-30 18:41:14 -05:00
|
|
|
var dest = _attachmentContainers[originalContainer].GetBlockBlobReference(
|
|
|
|
|
BlobName(cipherId, attachmentData));
|
2017-07-10 14:30:12 -04:00
|
|
|
await dest.DeleteIfExistsAsync();
|
|
|
|
|
await dest.StartCopyAsync(original);
|
|
|
|
|
await original.DeleteIfExistsAsync();
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-30 18:41:14 -05:00
|
|
|
public async Task DeleteAttachmentAsync(Guid cipherId, CipherAttachment.MetaData attachmentData)
|
2017-07-10 14:30:12 -04:00
|
|
|
{
|
2021-03-30 18:41:14 -05:00
|
|
|
await InitAsync(attachmentData.ContainerName);
|
|
|
|
|
var blob = _attachmentContainers[attachmentData.ContainerName].GetBlockBlobReference(
|
|
|
|
|
BlobName(cipherId, attachmentData));
|
2017-06-15 15:34:12 -04:00
|
|
|
await blob.DeleteIfExistsAsync();
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-30 18:41:14 -05:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2021-02-22 15:35:16 -06:00
|
|
|
private async Task DeleteAttachmentsForPathAsync(string path)
|
2017-07-10 14:30:12 -04:00
|
|
|
{
|
2021-02-22 15:35:16 -06:00
|
|
|
foreach (var container in _attachmentContainerName)
|
2017-07-10 17:08:50 -04:00
|
|
|
{
|
2021-02-22 15:35:16 -06:00
|
|
|
await InitAsync(container);
|
|
|
|
|
var segment = await _attachmentContainers[container].ListBlobsSegmentedAsync(path, true, BlobListingDetails.None, 100, null, null, null);
|
|
|
|
|
|
|
|
|
|
while (true)
|
2017-07-10 20:48:06 -04:00
|
|
|
{
|
2021-02-22 15:35:16 -06:00
|
|
|
foreach (var blob in segment.Results)
|
2017-07-31 16:58:27 -04:00
|
|
|
{
|
2021-02-22 15:35:16 -06:00
|
|
|
if (blob is CloudBlockBlob blockBlob)
|
|
|
|
|
{
|
|
|
|
|
await blockBlob.DeleteIfExistsAsync();
|
|
|
|
|
}
|
2017-07-31 16:58:27 -04:00
|
|
|
}
|
2017-07-10 17:08:50 -04:00
|
|
|
|
2021-02-22 15:35:16 -06:00
|
|
|
if (segment.ContinuationToken == null)
|
2017-07-31 16:58:27 -04:00
|
|
|
{
|
2021-02-22 15:35:16 -06:00
|
|
|
break;
|
2017-07-31 16:58:27 -04:00
|
|
|
}
|
|
|
|
|
|
2021-02-22 15:35:16 -06:00
|
|
|
segment = await _attachmentContainers[container].ListBlobsSegmentedAsync(segment.ContinuationToken);
|
2017-07-31 16:58:27 -04:00
|
|
|
}
|
2017-07-10 17:08:50 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-02-22 15:35:16 -06:00
|
|
|
private async Task InitAsync(string containerName)
|
2017-06-15 15:34:12 -04:00
|
|
|
{
|
2021-02-22 15:35:16 -06:00
|
|
|
if (!_attachmentContainers.ContainsKey(containerName) || _attachmentContainers[containerName] == null)
|
2017-06-15 15:34:12 -04:00
|
|
|
{
|
2021-02-22 15:35:16 -06:00
|
|
|
_attachmentContainers[containerName] = _blobClient.GetContainerReference(containerName);
|
|
|
|
|
if (containerName == "attachments")
|
|
|
|
|
{
|
|
|
|
|
await _attachmentContainers[containerName].CreateIfNotExistsAsync(BlobContainerPublicAccessType.Blob, null, null);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
await _attachmentContainers[containerName].CreateIfNotExistsAsync(BlobContainerPublicAccessType.Off, null, null);
|
|
|
|
|
}
|
2017-06-15 15:34:12 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|