1
0
mirror of https://github.com/bitwarden/server.git synced 2024-11-25 12:45:18 +01:00

post, upload, and save cipher attachment

This commit is contained in:
Kyle Spearrin 2017-06-30 11:15:58 -04:00
parent 71f755dd44
commit 6cea556ae1
18 changed files with 158 additions and 25 deletions

View File

@ -9,6 +9,8 @@ using Bit.Core.Exceptions;
using Bit.Core.Services;
using Bit.Core;
using Bit.Api.Utilities;
using Bit.Core.Models.Data;
using Newtonsoft.Json;
namespace Bit.Api.Controllers
{
@ -219,13 +221,10 @@ namespace Bit.Api.Controllers
string.IsNullOrWhiteSpace(model.FolderId) ? (Guid?)null : new Guid(model.FolderId), userId);
}
[HttpPost("attachment")]
[HttpPost("{id}/attachment")]
[DisableFormValueModelBinding]
public async Task Post(string id)
public async Task PostAttachment(string id)
{
// throw for now
throw new NotImplementedException();
if(!Request?.ContentType.Contains("multipart/") ?? true)
{
throw new BadRequestException("Invalid content.");
@ -239,22 +238,16 @@ namespace Bit.Api.Controllers
throw new NotFoundException();
}
await Request.GetFilesAsync(async (stream, fileName) =>
await Request.GetFileAsync(async (stream, fileName) =>
{
var attachmentId = Guid.NewGuid();
// TODO: store attachmentId + fileName reference in database
var storedFilename = $"{idGuid}_{attachmentId}";
await _attachmentStorageService.UploadAttachmentAsync(stream, storedFilename);
await _cipherService.AttachAsync(cipher, stream, fileName, Request.ContentLength.GetValueOrDefault(0), userId);
});
}
[HttpDelete("{id}/attachment/{attachmentId}")]
[HttpPost("{id}/attachment/{attachmentId}/delete")]
public async Task Delete(string id, string attachmentId)
public async Task DeleteAttachment(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);
@ -263,11 +256,9 @@ namespace Bit.Api.Controllers
throw new NotFoundException();
}
var attachmentIdGuid = new Guid(attachmentId);
// TODO: check and remove attachmentId from cipher in database
var storedFilename = $"{idGuid}_{attachmentId}";
var storedFilename = $"{idGuid}/{attachmentId}";
await _attachmentStorageService.DeleteAttachmentAsync(storedFilename);
}
}

View File

@ -12,23 +12,38 @@ namespace Bit.Api.Utilities
{
private static readonly FormOptions _defaultFormOptions = new FormOptions();
public static async Task GetFilesAsync(this HttpRequest request, Func<Stream, string, Task> callback)
public static async Task GetFileAsync(this HttpRequest request, Func<Stream, string, Task> callback)
{
await request.GetFilesAsync(1, callback);
}
private static async Task GetFilesAsync(this HttpRequest request, int? fileCount, 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)
var fileNumber = 1;
while(section != null && fileNumber <= fileCount)
{
ContentDispositionHeaderValue content;
if(ContentDispositionHeaderValue.TryParse(section.ContentDisposition, out content) &&
HasFileContentDisposition(content))
{
await callback(section.Body, HeaderUtilities.RemoveQuotes(content.FileName));
var fileName = HeaderUtilities.RemoveQuotes(content.FileName) ?? string.Empty;
await callback(section.Body, fileName);
}
section = await reader.ReadNextSectionAsync();
if(fileNumber >= fileCount)
{
section = null;
}
else
{
section = await reader.ReadNextSectionAsync();
fileNumber++;
}
}
}

View File

@ -0,0 +1,19 @@
using System;
namespace Bit.Core.Models.Data
{
public class CipherAttachment
{
public Guid Id { get; set; }
public Guid? UserId { get; set; }
public Guid? OrganizationId { get; set; }
public string AttachmentId { get; set; }
public string AttachmentData { get; set; }
public class MetaData
{
public long Size { get; set; }
public string FileName { get; set; }
}
}
}

View File

@ -12,6 +12,7 @@ namespace Bit.Core.Models.Table
public string Data { get; set; }
public string Favorites { get; set; }
public string Folders { get; set; }
public string Attachments { get; set; }
public DateTime CreationDate { get; internal set; } = DateTime.UtcNow;
public DateTime RevisionDate { get; internal set; } = DateTime.UtcNow;

View File

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Threading.Tasks;
using Bit.Core.Models.Table;
using Core.Models.Data;
using Bit.Core.Models.Data;
namespace Bit.Core.Repositories
{
@ -19,6 +20,7 @@ namespace Bit.Core.Repositories
Task UpsertAsync(CipherDetails cipher);
Task ReplaceAsync(Cipher obj, IEnumerable<Guid> collectionIds);
Task UpdatePartialAsync(Guid id, Guid userId, Guid? folderId, bool favorite);
Task UpdateAttachmentAsync(CipherAttachment attachment);
Task DeleteAsync(IEnumerable<Guid> ids, Guid userId);
Task MoveAsync(IEnumerable<Guid> ids, Guid? folderId, Guid userId);
Task UpdateUserKeysAndCiphersAsync(User user, IEnumerable<Cipher> ciphers, IEnumerable<Folder> folders);

View File

@ -9,6 +9,7 @@ using Dapper;
using Core.Models.Data;
using Bit.Core.Utilities;
using Newtonsoft.Json;
using Bit.Core.Models.Data;
namespace Bit.Core.Repositories.SqlServer
{
@ -176,6 +177,17 @@ namespace Bit.Core.Repositories.SqlServer
}
}
public async Task UpdateAttachmentAsync(CipherAttachment attachment)
{
using(var connection = new SqlConnection(ConnectionString))
{
var results = await connection.ExecuteAsync(
$"[{Schema}].[Cipher_UpdateAttachment]",
attachment,
commandType: CommandType.StoredProcedure);
}
}
public async Task DeleteAsync(IEnumerable<Guid> ids, Guid userId)
{
using(var connection = new SqlConnection(ConnectionString))

View File

@ -3,6 +3,7 @@ using System.Threading.Tasks;
using Bit.Core.Models.Table;
using Core.Models.Data;
using System;
using System.IO;
namespace Bit.Core.Services
{
@ -10,6 +11,8 @@ namespace Bit.Core.Services
{
Task SaveAsync(Cipher cipher, Guid savingUserId, bool orgAdmin = false);
Task SaveDetailsAsync(CipherDetails cipher, Guid savingUserId);
Task AttachAsync(Cipher cipher, Stream stream, string fileName, long requestLength, Guid savingUserId,
bool orgAdmin = false);
Task DeleteAsync(Cipher cipher, Guid deletingUserId, bool orgAdmin = false);
Task DeleteManyAsync(IEnumerable<Guid> cipherIds, Guid deletingUserId);
Task MoveManyAsync(IEnumerable<Guid> cipherIds, Guid? destinationFolderId, Guid movingUserId);

View File

@ -6,6 +6,9 @@ using Bit.Core.Models.Table;
using Bit.Core.Repositories;
using Core.Models.Data;
using Bit.Core.Exceptions;
using Bit.Core.Models.Data;
using Newtonsoft.Json;
using System.IO;
namespace Bit.Core.Services
{
@ -18,6 +21,7 @@ namespace Bit.Core.Services
private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly ICollectionCipherRepository _collectionCipherRepository;
private readonly IPushNotificationService _pushService;
private readonly IAttachmentStorageService _attachmentStorageService;
public CipherService(
ICipherRepository cipherRepository,
@ -26,7 +30,8 @@ namespace Bit.Core.Services
IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository,
ICollectionCipherRepository collectionCipherRepository,
IPushNotificationService pushService)
IPushNotificationService pushService,
IAttachmentStorageService attachmentStorageService)
{
_cipherRepository = cipherRepository;
_folderRepository = folderRepository;
@ -35,6 +40,7 @@ namespace Bit.Core.Services
_organizationUserRepository = organizationUserRepository;
_collectionCipherRepository = collectionCipherRepository;
_pushService = pushService;
_attachmentStorageService = attachmentStorageService;
}
public async Task SaveAsync(Cipher cipher, Guid savingUserId, bool orgAdmin = false)
@ -86,6 +92,45 @@ namespace Bit.Core.Services
}
}
public async Task AttachAsync(Cipher cipher, Stream stream, string fileName, 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.");
}
// TODO: check available space against requestLength
var attachmentId = Utilities.CoreHelpers.SecureRandomString(32, upper: false, special: false);
await _attachmentStorageService.UploadAttachmentAsync(stream, $"{cipher.Id}/{attachmentId}");
var data = new CipherAttachment.MetaData
{
FileName = fileName,
Size = stream.Length
};
var attachment = new CipherAttachment
{
Id = cipher.Id,
UserId = cipher.UserId,
OrganizationId = cipher.OrganizationId,
AttachmentId = attachmentId,
AttachmentData = JsonConvert.SerializeObject(data)
};
await _cipherRepository.UpdateAttachmentAsync(attachment);
// push
await _pushService.PushSyncCipherUpdateAsync(cipher);
}
public async Task DeleteAsync(Cipher cipher, Guid deletingUserId, bool orgAdmin = false)
{
if(!orgAdmin && !(await UserCanEditAsync(cipher, deletingUserId)))

View File

@ -53,8 +53,7 @@ namespace Bit.Core.Utilities
services.AddSingleton<IPushNotificationService, NotificationHubPushNotificationService>();
services.AddSingleton<IBlockIpService, AzureQueueBlockIpService>();
services.AddSingleton<IPushRegistrationService, NotificationHubPushRegistrationService>();
// noop for now
services.AddSingleton<IAttachmentStorageService, NoopAttachmentStorageService>();
services.AddSingleton<IAttachmentStorageService, AzureAttachmentStorageService>();
}
public static void AddNoopServices(this IServiceCollection services)

View File

@ -200,5 +200,6 @@
<Build Include="dbo\Stored Procedures\Organization_Create.sql" />
<Build Include="dbo\User Defined Types\GuidIdArray.sql" />
<Build Include="dbo\User Defined Types\SelectionReadOnlyArray.sql" />
<Build Include="dbo\Stored Procedures\Cipher_UpdateAttachment.sql" />
</ItemGroup>
</Project>

View File

@ -7,6 +7,7 @@ SELECT
C.[OrganizationId],
C.[Type],
C.[Data],
C.[Attachments],
C.[CreationDate],
C.[RevisionDate],
CASE WHEN

View File

@ -6,6 +6,7 @@
@Data NVARCHAR(MAX),
@Favorites NVARCHAR(MAX), -- not used
@Folders NVARCHAR(MAX), -- not used
@Attachments NVARCHAR(MAX), -- not used
@CreationDate DATETIME2(7),
@RevisionDate DATETIME2(7),
@FolderId UNIQUEIDENTIFIER,

View File

@ -6,6 +6,7 @@
@Data NVARCHAR(MAX),
@Favorites NVARCHAR(MAX), -- not used
@Folders NVARCHAR(MAX), -- not used
@Attachments NVARCHAR(MAX), -- not used
@CreationDate DATETIME2(7),
@RevisionDate DATETIME2(7),
@FolderId UNIQUEIDENTIFIER,

View File

@ -6,6 +6,7 @@
@Data NVARCHAR(MAX),
@Favorites NVARCHAR(MAX),
@Folders NVARCHAR(MAX),
@Attachments NVARCHAR(MAX),
@CreationDate DATETIME2(7),
@RevisionDate DATETIME2(7)
AS
@ -21,6 +22,7 @@ BEGIN
[Data],
[Favorites],
[Folders],
[Attachments],
[CreationDate],
[RevisionDate]
)
@ -33,6 +35,7 @@ BEGIN
@Data,
@Favorites,
@Folders,
@Attachments,
@CreationDate,
@RevisionDate
)

View File

@ -6,6 +6,7 @@
@Data NVARCHAR(MAX),
@Favorites NVARCHAR(MAX),
@Folders NVARCHAR(MAX),
@Attachments NVARCHAR(MAX),
@CreationDate DATETIME2(7),
@RevisionDate DATETIME2(7)
AS
@ -21,6 +22,7 @@ BEGIN
[Data] = @Data,
[Favorites] = @Favorites,
[Folders] = @Folders,
[Attachments] = @Attachments,
[CreationDate] = @CreationDate,
[RevisionDate] = @RevisionDate
WHERE

View File

@ -0,0 +1,35 @@
CREATE PROCEDURE [dbo].[Cipher_UpdateAttachment]
@Id UNIQUEIDENTIFIER,
@UserId UNIQUEIDENTIFIER,
@OrganizationId UNIQUEIDENTIFIER,
@AttachmentId VARCHAR(50),
@AttachmentData NVARCHAR(MAX)
AS
BEGIN
SET NOCOUNT ON
DECLARE @AttachmentIdKey VARCHAR(50) = CONCAT('"', @AttachmentId, '"')
DECLARE @AttachmentIdPath VARCHAR(50) = CONCAT('$.', @AttachmentIdKey)
UPDATE
[dbo].[Cipher]
SET
[Attachments] =
CASE
WHEN [Attachments] IS NULL THEN
CONCAT('{', @AttachmentIdKey, ':', @AttachmentData, '}')
ELSE
JSON_MODIFY([Attachments], @AttachmentIdPath, JSON_QUERY(@AttachmentData, '$'))
END
WHERE
[Id] = @Id
IF @OrganizationId IS NOT NULL
BEGIN
EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationId] @OrganizationId
END
ELSE IF @UserId IS NOT NULL
BEGIN
EXEC [dbo].[User_BumpAccountRevisionDate] @UserId
END
END

View File

@ -6,6 +6,7 @@
@Data NVARCHAR(MAX),
@Favorites NVARCHAR(MAX),
@Folders NVARCHAR(MAX),
@Attachments NVARCHAR(MAX),
@CreationDate DATETIME2(7),
@RevisionDate DATETIME2(7),
@CollectionIds AS [dbo].[GuidIdArray] READONLY
@ -20,7 +21,7 @@ BEGIN
[OrganizationId] = @OrganizationId,
[Data] = @Data,
[RevisionDate] = @RevisionDate
-- No need to update CreationDate, Favorites, Folders, or Type since that data will not change
-- No need to update Attachments, CreationDate, Favorites, Folders, or Type since that data will not change
WHERE
[Id] = @Id

View File

@ -6,6 +6,7 @@
[Data] NVARCHAR (MAX) NOT NULL,
[Favorites] NVARCHAR (MAX) NULL,
[Folders] NVARCHAR (MAX) NULL,
[Attachments] NVARCHAR (MAX) NULL,
[CreationDate] DATETIME2 (7) NOT NULL,
[RevisionDate] DATETIME2 (7) NOT NULL,
CONSTRAINT [PK_Cipher] PRIMARY KEY CLUSTERED ([Id] ASC),