1
0
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:
Kyle Spearrin 2017-06-15 15:34:12 -04:00
parent 94be5bc1dd
commit 06ca566be1
9 changed files with 232 additions and 0 deletions

View File

@ -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);
}
} }
} }

View 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)
{
}
}
}

View 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));
}
}
}

View File

@ -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"

View File

@ -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; }

View 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);
}
}

View File

@ -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();
}
}
}
}

View File

@ -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);
}
}
}

View File

@ -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(