1
0
mirror of https://github.com/bitwarden/server.git synced 2025-06-30 15:42:48 -05:00

Send APIs (#979)

* send work

* fix sql proj file

* update

* updates

* access id

* delete job

* fix delete job

* local send storage

* update sprocs for null checks
This commit is contained in:
Kyle Spearrin
2020-11-02 15:55:49 -05:00
committed by GitHub
parent a5db233e51
commit 82dd364e65
39 changed files with 1774 additions and 11 deletions

View File

@ -0,0 +1,66 @@
using System.Threading.Tasks;
using Microsoft.Azure.Storage;
using Microsoft.Azure.Storage.Blob;
using System.IO;
using System;
using Bit.Core.Models.Table;
namespace Bit.Core.Services
{
public class AzureSendFileStorageService : ISendFileStorageService
{
private const string FilesContainerName = "sendfiles";
private readonly CloudBlobClient _blobClient;
private CloudBlobContainer _sendFilesContainer;
public AzureSendFileStorageService(
GlobalSettings globalSettings)
{
var storageAccount = CloudStorageAccount.Parse(globalSettings.Send.ConnectionString);
_blobClient = storageAccount.CreateCloudBlobClient();
}
public async Task UploadNewFileAsync(Stream stream, Send send, string fileId)
{
await InitAsync();
var blob = _sendFilesContainer.GetBlockBlobReference(fileId);
if (send.UserId.HasValue)
{
blob.Metadata.Add("userId", send.UserId.Value.ToString());
}
else
{
blob.Metadata.Add("organizationId", send.OrganizationId.Value.ToString());
}
blob.Properties.ContentDisposition = $"attachment; filename=\"{fileId}\"";
await blob.UploadFromStreamAsync(stream);
}
public async Task DeleteFileAsync(string fileId)
{
await InitAsync();
var blob = _sendFilesContainer.GetBlockBlobReference(fileId);
await blob.DeleteIfExistsAsync();
}
public async Task DeleteFilesForOrganizationAsync(Guid organizationId)
{
await InitAsync();
}
public async Task DeleteFilesForUserAsync(Guid userId)
{
await InitAsync();
}
private async Task InitAsync()
{
if (_sendFilesContainer == null)
{
_sendFilesContainer = _blobClient.GetContainerReference(FilesContainerName);
await _sendFilesContainer.CreateIfNotExistsAsync(BlobContainerPublicAccessType.Blob, null, null);
}
}
}
}

View File

@ -0,0 +1,62 @@
using System.Threading.Tasks;
using System.IO;
using System;
using Bit.Core.Models.Table;
namespace Bit.Core.Services
{
public class LocalSendStorageService : ISendFileStorageService
{
private readonly string _baseDirPath;
public LocalSendStorageService(
GlobalSettings globalSettings)
{
_baseDirPath = globalSettings.Send.BaseDirectory;
}
public async Task UploadNewFileAsync(Stream stream, Send send, string fileId)
{
await InitAsync();
using (var fs = File.Create($"{_baseDirPath}/{fileId}"))
{
stream.Seek(0, SeekOrigin.Begin);
await stream.CopyToAsync(fs);
}
}
public async Task DeleteFileAsync(string fileId)
{
await InitAsync();
DeleteFileIfExists($"{_baseDirPath}/{fileId}");
}
public async Task DeleteFilesForOrganizationAsync(Guid organizationId)
{
await InitAsync();
}
public async Task DeleteFilesForUserAsync(Guid userId)
{
await InitAsync();
}
private void DeleteFileIfExists(string path)
{
if (File.Exists(path))
{
File.Delete(path);
}
}
private Task InitAsync()
{
if (!Directory.Exists(_baseDirPath))
{
Directory.CreateDirectory(_baseDirPath);
}
return Task.FromResult(0);
}
}
}

View File

@ -0,0 +1,170 @@
using System;
using System.IO;
using System.Threading.Tasks;
using Bit.Core.Exceptions;
using Bit.Core.Models.Data;
using Bit.Core.Models.Table;
using Bit.Core.Repositories;
using Microsoft.AspNetCore.Identity;
using Newtonsoft.Json;
namespace Bit.Core.Services
{
public class SendService : ISendService
{
private readonly ISendRepository _sendRepository;
private readonly IUserRepository _userRepository;
private readonly IUserService _userService;
private readonly IOrganizationRepository _organizationRepository;
private readonly ISendFileStorageService _sendFileStorageService;
private readonly IPasswordHasher<User> _passwordHasher;
private readonly GlobalSettings _globalSettings;
public SendService(
ISendRepository sendRepository,
IUserRepository userRepository,
IUserService userService,
IOrganizationRepository organizationRepository,
ISendFileStorageService sendFileStorageService,
IPasswordHasher<User> passwordHasher,
GlobalSettings globalSettings)
{
_sendRepository = sendRepository;
_userRepository = userRepository;
_userService = userService;
_organizationRepository = organizationRepository;
_sendFileStorageService = sendFileStorageService;
_passwordHasher = passwordHasher;
_globalSettings = globalSettings;
}
public async Task SaveSendAsync(Send send)
{
if (send.Id == default(Guid))
{
await _sendRepository.CreateAsync(send);
}
else
{
send.RevisionDate = DateTime.UtcNow;
await _sendRepository.UpsertAsync(send);
}
}
public async Task CreateSendAsync(Send send, SendFileData data, Stream stream, long requestLength)
{
if (send.Type != Enums.SendType.File)
{
throw new BadRequestException("Send is not of type \"file\".");
}
if (requestLength < 1)
{
throw new BadRequestException("No file data.");
}
var storageBytesRemaining = 0L;
if (send.UserId.HasValue)
{
var user = await _userRepository.GetByIdAsync(send.UserId.Value);
if (!(await _userService.CanAccessPremium(user)))
{
throw new BadRequestException("You must have premium status to use file sends.");
}
if (user.Premium)
{
storageBytesRemaining = user.StorageBytesRemaining();
}
else
{
// Users that get access to file storage/premium from their organization get the default
// 1 GB max storage.
storageBytesRemaining = user.StorageBytesRemaining(
_globalSettings.SelfHosted ? (short)10240 : (short)1);
}
}
else if (send.OrganizationId.HasValue)
{
var org = await _organizationRepository.GetByIdAsync(send.OrganizationId.Value);
if (!org.MaxStorageGb.HasValue)
{
throw new BadRequestException("This organization cannot use file sends.");
}
storageBytesRemaining = org.StorageBytesRemaining();
}
if (storageBytesRemaining < requestLength)
{
throw new BadRequestException("Not enough storage available.");
}
var fileId = Utilities.CoreHelpers.SecureRandomString(32, upper: false, special: false);
await _sendFileStorageService.UploadNewFileAsync(stream, send, fileId);
try
{
data.Id = fileId;
data.Size = stream.Length;
send.Data = JsonConvert.SerializeObject(data,
new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore });
await SaveSendAsync(send);
}
catch
{
// Clean up since this is not transactional
await _sendFileStorageService.DeleteFileAsync(fileId);
throw;
}
}
public async Task DeleteSendAsync(Send send)
{
await _sendRepository.DeleteAsync(send);
if (send.Type == Enums.SendType.File)
{
var data = JsonConvert.DeserializeObject<SendFileData>(send.Data);
await _sendFileStorageService.DeleteFileAsync(data.Id);
}
}
// Response: Send, password required, password invalid
public async Task<(Send, bool, bool)> AccessAsync(Guid sendId, string password)
{
var send = await _sendRepository.GetByIdAsync(sendId);
var now = DateTime.UtcNow;
if (send == null || send.MaxAccessCount.GetValueOrDefault(int.MaxValue) <= send.AccessCount ||
send.ExpirationDate.GetValueOrDefault(DateTime.MaxValue) < now || send.Disabled ||
send.DeletionDate < now)
{
return (null, false, false);
}
if (!string.IsNullOrWhiteSpace(send.Password))
{
if (string.IsNullOrWhiteSpace(password))
{
return (null, true, false);
}
var passwordResult = _passwordHasher.VerifyHashedPassword(new User(), send.Password, password);
if (passwordResult == PasswordVerificationResult.SuccessRehashNeeded)
{
send.Password = HashPassword(password);
}
if (passwordResult == PasswordVerificationResult.Failed)
{
return (null, false, true);
}
}
// TODO: maybe move this to a simple ++ sproc?
send.AccessCount++;
await _sendRepository.ReplaceAsync(send);
return (send, false, false);
}
public string HashPassword(string password)
{
return _passwordHasher.HashPassword(new User(), password);
}
}
}