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.Exceptions;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core;
|
using Bit.Core;
|
||||||
|
using Bit.Api.Utilities;
|
||||||
|
|
||||||
namespace Bit.Api.Controllers
|
namespace Bit.Api.Controllers
|
||||||
{
|
{
|
||||||
@ -19,6 +20,7 @@ namespace Bit.Api.Controllers
|
|||||||
private readonly ICollectionCipherRepository _collectionCipherRepository;
|
private readonly ICollectionCipherRepository _collectionCipherRepository;
|
||||||
private readonly ICipherService _cipherService;
|
private readonly ICipherService _cipherService;
|
||||||
private readonly IUserService _userService;
|
private readonly IUserService _userService;
|
||||||
|
private readonly IAttachmentStorageService _attachmentStorageService;
|
||||||
private readonly CurrentContext _currentContext;
|
private readonly CurrentContext _currentContext;
|
||||||
|
|
||||||
public CiphersController(
|
public CiphersController(
|
||||||
@ -26,12 +28,14 @@ namespace Bit.Api.Controllers
|
|||||||
ICollectionCipherRepository collectionCipherRepository,
|
ICollectionCipherRepository collectionCipherRepository,
|
||||||
ICipherService cipherService,
|
ICipherService cipherService,
|
||||||
IUserService userService,
|
IUserService userService,
|
||||||
|
IAttachmentStorageService attachmentStorageService,
|
||||||
CurrentContext currentContext)
|
CurrentContext currentContext)
|
||||||
{
|
{
|
||||||
_cipherRepository = cipherRepository;
|
_cipherRepository = cipherRepository;
|
||||||
_collectionCipherRepository = collectionCipherRepository;
|
_collectionCipherRepository = collectionCipherRepository;
|
||||||
_cipherService = cipherService;
|
_cipherService = cipherService;
|
||||||
_userService = userService;
|
_userService = userService;
|
||||||
|
_attachmentStorageService = attachmentStorageService;
|
||||||
_currentContext = currentContext;
|
_currentContext = currentContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -214,5 +218,57 @@ namespace Bit.Api.Controllers
|
|||||||
await _cipherService.MoveManyAsync(model.Ids.Select(i => new Guid(i)),
|
await _cipherService.MoveManyAsync(model.Ids.Select(i => new Guid(i)),
|
||||||
string.IsNullOrWhiteSpace(model.FolderId) ? (Guid?)null : new Guid(model.FolderId), userId);
|
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": {
|
"storage": {
|
||||||
"connectionString": "SECRET"
|
"connectionString": "SECRET"
|
||||||
},
|
},
|
||||||
|
"attachment": {
|
||||||
|
"connectionString": "SECRET",
|
||||||
|
"baseUrl": "http://localhost:4000/"
|
||||||
|
},
|
||||||
"documentDb": {
|
"documentDb": {
|
||||||
"uri": "SECRET",
|
"uri": "SECRET",
|
||||||
"key": "SECRET"
|
"key": "SECRET"
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
public virtual MailSettings Mail { get; set; } = new MailSettings();
|
public virtual MailSettings Mail { get; set; } = new MailSettings();
|
||||||
public virtual PushSettings Push { get; set; } = new PushSettings();
|
public virtual PushSettings Push { get; set; } = new PushSettings();
|
||||||
public virtual StorageSettings Storage { get; set; } = new StorageSettings();
|
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 IdentityServerSettings IdentityServer { get; set; } = new IdentityServerSettings();
|
||||||
public virtual DataProtectionSettings DataProtection { get; set; } = new DataProtectionSettings();
|
public virtual DataProtectionSettings DataProtection { get; set; } = new DataProtectionSettings();
|
||||||
public virtual DocumentDbSettings DocumentDb { get; set; } = new DocumentDbSettings();
|
public virtual DocumentDbSettings DocumentDb { get; set; } = new DocumentDbSettings();
|
||||||
@ -26,6 +27,12 @@
|
|||||||
public string ConnectionString { get; set; }
|
public string ConnectionString { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class AttachmentSettings
|
||||||
|
{
|
||||||
|
public string ConnectionString { get; set; }
|
||||||
|
public string BaseUrl { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
public class MailSettings
|
public class MailSettings
|
||||||
{
|
{
|
||||||
public string ReplyToEmail { get; set; }
|
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<IPushNotificationService, NotificationHubPushNotificationService>();
|
||||||
services.AddSingleton<IBlockIpService, AzureQueueBlockIpService>();
|
services.AddSingleton<IBlockIpService, AzureQueueBlockIpService>();
|
||||||
services.AddSingleton<IPushRegistrationService, NotificationHubPushRegistrationService>();
|
services.AddSingleton<IPushRegistrationService, NotificationHubPushRegistrationService>();
|
||||||
|
// noop for now
|
||||||
|
services.AddSingleton<IAttachmentStorageService, NoopAttachmentStorageService>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void AddNoopServices(this IServiceCollection services)
|
public static void AddNoopServices(this IServiceCollection services)
|
||||||
@ -61,6 +63,7 @@ namespace Bit.Core.Utilities
|
|||||||
services.AddSingleton<IPushNotificationService, NoopPushNotificationService>();
|
services.AddSingleton<IPushNotificationService, NoopPushNotificationService>();
|
||||||
services.AddSingleton<IBlockIpService, NoopBlockIpService>();
|
services.AddSingleton<IBlockIpService, NoopBlockIpService>();
|
||||||
services.AddSingleton<IPushRegistrationService, NoopPushRegistrationService>();
|
services.AddSingleton<IPushRegistrationService, NoopPushRegistrationService>();
|
||||||
|
services.AddSingleton<IAttachmentStorageService, NoopAttachmentStorageService>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IdentityBuilder AddCustomIdentityServices(
|
public static IdentityBuilder AddCustomIdentityServices(
|
||||||
|
Loading…
Reference in New Issue
Block a user