mirror of
https://github.com/bitwarden/server.git
synced 2024-11-22 12:15:36 +01:00
attachment apis and azure storage service
This commit is contained in:
parent
94be5bc1dd
commit
06ca566be1
@ -8,6 +8,7 @@ using Bit.Core.Models.Api;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core;
|
||||
using Bit.Api.Utilities;
|
||||
|
||||
namespace Bit.Api.Controllers
|
||||
{
|
||||
@ -19,6 +20,7 @@ namespace Bit.Api.Controllers
|
||||
private readonly ICollectionCipherRepository _collectionCipherRepository;
|
||||
private readonly ICipherService _cipherService;
|
||||
private readonly IUserService _userService;
|
||||
private readonly IAttachmentStorageService _attachmentStorageService;
|
||||
private readonly CurrentContext _currentContext;
|
||||
|
||||
public CiphersController(
|
||||
@ -26,12 +28,14 @@ namespace Bit.Api.Controllers
|
||||
ICollectionCipherRepository collectionCipherRepository,
|
||||
ICipherService cipherService,
|
||||
IUserService userService,
|
||||
IAttachmentStorageService attachmentStorageService,
|
||||
CurrentContext currentContext)
|
||||
{
|
||||
_cipherRepository = cipherRepository;
|
||||
_collectionCipherRepository = collectionCipherRepository;
|
||||
_cipherService = cipherService;
|
||||
_userService = userService;
|
||||
_attachmentStorageService = attachmentStorageService;
|
||||
_currentContext = currentContext;
|
||||
}
|
||||
|
||||
@ -214,5 +218,57 @@ namespace Bit.Api.Controllers
|
||||
await _cipherService.MoveManyAsync(model.Ids.Select(i => new Guid(i)),
|
||||
string.IsNullOrWhiteSpace(model.FolderId) ? (Guid?)null : new Guid(model.FolderId), userId);
|
||||
}
|
||||
|
||||
[HttpPost("attachment")]
|
||||
[DisableFormValueModelBinding]
|
||||
public async Task Post(string id)
|
||||
{
|
||||
// throw for now
|
||||
throw new NotImplementedException();
|
||||
|
||||
if(!Request?.ContentType.Contains("multipart/") ?? true)
|
||||
{
|
||||
throw new BadRequestException("Invalid content.");
|
||||
}
|
||||
|
||||
var idGuid = new Guid(id);
|
||||
var userId = _userService.GetProperUserId(User).Value;
|
||||
var cipher = await _cipherRepository.GetByIdAsync(idGuid, userId);
|
||||
if(cipher == null)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
await Request.GetFilesAsync(async (stream, fileName) =>
|
||||
{
|
||||
var attachmentId = Guid.NewGuid();
|
||||
// TODO: store attachmentId + fileName reference in database
|
||||
var storedFilename = $"{idGuid}_{attachmentId}";
|
||||
await _attachmentStorageService.UploadAttachmentAsync(stream, storedFilename);
|
||||
});
|
||||
}
|
||||
|
||||
[HttpDelete("{id}/attachment/{attachmentId}")]
|
||||
[HttpPost("{id}/attachment/{attachmentId}/delete")]
|
||||
public async Task Delete(string id, string attachmentId)
|
||||
{
|
||||
// throw for now
|
||||
throw new NotImplementedException();
|
||||
|
||||
var idGuid = new Guid(id);
|
||||
var userId = _userService.GetProperUserId(User).Value;
|
||||
var cipher = await _cipherRepository.GetByIdAsync(idGuid, userId);
|
||||
if(cipher == null)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var attachmentIdGuid = new Guid(attachmentId);
|
||||
|
||||
// TODO: check and remove attachmentId from cipher in database
|
||||
|
||||
var storedFilename = $"{idGuid}_{attachmentId}";
|
||||
await _attachmentStorageService.DeleteAttachmentAsync(storedFilename);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
30
src/Api/Utilities/DisableFormValueModelBindingAttribute.cs
Normal file
30
src/Api/Utilities/DisableFormValueModelBindingAttribute.cs
Normal file
@ -0,0 +1,30 @@
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace Bit.Api.Utilities
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
|
||||
public class DisableFormValueModelBindingAttribute : Attribute, IResourceFilter
|
||||
{
|
||||
public void OnResourceExecuting(ResourceExecutingContext context)
|
||||
{
|
||||
var formValue = context.ValueProviderFactories.OfType<FormValueProviderFactory>().FirstOrDefault();
|
||||
if(formValue != null)
|
||||
{
|
||||
context.ValueProviderFactories.Remove(formValue);
|
||||
}
|
||||
|
||||
var jqFormValue = context.ValueProviderFactories.OfType<JQueryFormValueProviderFactory>().FirstOrDefault();
|
||||
if(jqFormValue != null)
|
||||
{
|
||||
context.ValueProviderFactories.Remove(jqFormValue);
|
||||
}
|
||||
}
|
||||
|
||||
public void OnResourceExecuted(ResourceExecutedContext context)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
58
src/Api/Utilities/MultipartFormDataHelper.cs
Normal file
58
src/Api/Utilities/MultipartFormDataHelper.cs
Normal file
@ -0,0 +1,58 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
using Microsoft.AspNetCore.WebUtilities;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Bit.Api.Utilities
|
||||
{
|
||||
public static class MultipartFormDataHelper
|
||||
{
|
||||
private static readonly FormOptions _defaultFormOptions = new FormOptions();
|
||||
|
||||
public static async Task GetFilesAsync(this HttpRequest request, Func<Stream, string, Task> callback)
|
||||
{
|
||||
var boundary = GetBoundary(MediaTypeHeaderValue.Parse(request.ContentType),
|
||||
_defaultFormOptions.MultipartBoundaryLengthLimit);
|
||||
var reader = new MultipartReader(boundary, request.Body);
|
||||
|
||||
var section = await reader.ReadNextSectionAsync();
|
||||
while(section != null)
|
||||
{
|
||||
ContentDispositionHeaderValue content;
|
||||
if(ContentDispositionHeaderValue.TryParse(section.ContentDisposition, out content) &&
|
||||
HasFileContentDisposition(content))
|
||||
{
|
||||
await callback(section.Body, HeaderUtilities.RemoveQuotes(content.FileName));
|
||||
}
|
||||
|
||||
section = await reader.ReadNextSectionAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetBoundary(MediaTypeHeaderValue contentType, int lengthLimit)
|
||||
{
|
||||
var boundary = HeaderUtilities.RemoveQuotes(contentType.Boundary);
|
||||
if(string.IsNullOrWhiteSpace(boundary))
|
||||
{
|
||||
throw new InvalidDataException("Missing content-type boundary.");
|
||||
}
|
||||
|
||||
if(boundary.Length > lengthLimit)
|
||||
{
|
||||
throw new InvalidDataException($"Multipart boundary length limit {lengthLimit} exceeded.");
|
||||
}
|
||||
|
||||
return boundary;
|
||||
}
|
||||
|
||||
private static bool HasFileContentDisposition(ContentDispositionHeaderValue content)
|
||||
{
|
||||
// Content-Disposition: form-data; name="myfile1"; filename="Misc 002.jpg"
|
||||
return content != null && content.DispositionType.Equals("form-data") &&
|
||||
(!string.IsNullOrEmpty(content.FileName) || !string.IsNullOrEmpty(content.FileNameStar));
|
||||
}
|
||||
}
|
||||
}
|
@ -27,6 +27,10 @@
|
||||
"storage": {
|
||||
"connectionString": "SECRET"
|
||||
},
|
||||
"attachment": {
|
||||
"connectionString": "SECRET",
|
||||
"baseUrl": "http://localhost:4000/"
|
||||
},
|
||||
"documentDb": {
|
||||
"uri": "SECRET",
|
||||
"key": "SECRET"
|
||||
|
@ -10,6 +10,7 @@
|
||||
public virtual MailSettings Mail { get; set; } = new MailSettings();
|
||||
public virtual PushSettings Push { get; set; } = new PushSettings();
|
||||
public virtual StorageSettings Storage { get; set; } = new StorageSettings();
|
||||
public virtual AttachmentSettings Attachment { get; set; } = new AttachmentSettings();
|
||||
public virtual IdentityServerSettings IdentityServer { get; set; } = new IdentityServerSettings();
|
||||
public virtual DataProtectionSettings DataProtection { get; set; } = new DataProtectionSettings();
|
||||
public virtual DocumentDbSettings DocumentDb { get; set; } = new DocumentDbSettings();
|
||||
@ -26,6 +27,12 @@
|
||||
public string ConnectionString { get; set; }
|
||||
}
|
||||
|
||||
public class AttachmentSettings
|
||||
{
|
||||
public string ConnectionString { get; set; }
|
||||
public string BaseUrl { get; set; }
|
||||
}
|
||||
|
||||
public class MailSettings
|
||||
{
|
||||
public string ReplyToEmail { get; set; }
|
||||
|
11
src/Core/Services/IAttachmentStorageService.cs
Normal file
11
src/Core/Services/IAttachmentStorageService.cs
Normal file
@ -0,0 +1,11 @@
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Bit.Core.Services
|
||||
{
|
||||
public interface IAttachmentStorageService
|
||||
{
|
||||
Task UploadAttachmentAsync(Stream stream, string name);
|
||||
Task DeleteAttachmentAsync(string name);
|
||||
}
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.WindowsAzure.Storage;
|
||||
using Microsoft.WindowsAzure.Storage.Blob;
|
||||
using System.IO;
|
||||
|
||||
namespace Bit.Core.Services
|
||||
{
|
||||
public class AzureAttachmentStorageService : IAttachmentStorageService
|
||||
{
|
||||
private const string AttchmentContainerName = "attachments";
|
||||
|
||||
private readonly CloudBlobClient _blobClient;
|
||||
private CloudBlobContainer _attachmentsContainer;
|
||||
|
||||
public AzureAttachmentStorageService(
|
||||
GlobalSettings globalSettings)
|
||||
{
|
||||
var storageAccount = CloudStorageAccount.Parse(globalSettings.Storage.ConnectionString);
|
||||
_blobClient = storageAccount.CreateCloudBlobClient();
|
||||
}
|
||||
|
||||
public async Task UploadAttachmentAsync(Stream stream, string name)
|
||||
{
|
||||
await InitAsync();
|
||||
var blob = _attachmentsContainer.GetBlockBlobReference(name);
|
||||
await blob.UploadFromStreamAsync(stream);
|
||||
}
|
||||
|
||||
public async Task DeleteAttachmentAsync(string name)
|
||||
{
|
||||
await InitAsync();
|
||||
var blob = _attachmentsContainer.GetBlockBlobReference(name);
|
||||
await blob.DeleteIfExistsAsync();
|
||||
}
|
||||
|
||||
private async Task InitAsync()
|
||||
{
|
||||
if(_attachmentsContainer == null)
|
||||
{
|
||||
_attachmentsContainer = _blobClient.GetContainerReference(AttchmentContainerName);
|
||||
await _attachmentsContainer.CreateIfNotExistsAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Bit.Core.Services
|
||||
{
|
||||
public class NoopAttachmentStorageService : IAttachmentStorageService
|
||||
{
|
||||
public Task DeleteAttachmentAsync(string name)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task UploadAttachmentAsync(Stream stream, string name)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
}
|
||||
}
|
@ -53,6 +53,8 @@ namespace Bit.Core.Utilities
|
||||
services.AddSingleton<IPushNotificationService, NotificationHubPushNotificationService>();
|
||||
services.AddSingleton<IBlockIpService, AzureQueueBlockIpService>();
|
||||
services.AddSingleton<IPushRegistrationService, NotificationHubPushRegistrationService>();
|
||||
// noop for now
|
||||
services.AddSingleton<IAttachmentStorageService, NoopAttachmentStorageService>();
|
||||
}
|
||||
|
||||
public static void AddNoopServices(this IServiceCollection services)
|
||||
@ -61,6 +63,7 @@ namespace Bit.Core.Utilities
|
||||
services.AddSingleton<IPushNotificationService, NoopPushNotificationService>();
|
||||
services.AddSingleton<IBlockIpService, NoopBlockIpService>();
|
||||
services.AddSingleton<IPushRegistrationService, NoopPushRegistrationService>();
|
||||
services.AddSingleton<IAttachmentStorageService, NoopAttachmentStorageService>();
|
||||
}
|
||||
|
||||
public static IdentityBuilder AddCustomIdentityServices(
|
||||
|
Loading…
Reference in New Issue
Block a user