mirror of
https://github.com/bitwarden/server.git
synced 2025-07-17 23:50:58 -05:00
Merge branch 'main' into ac/pm-21612/unified-mariadb-cannot-edit-an-unconfirmed-user
This commit is contained in:
19
src/Core/Tools/Models/Data/SendAccessResult.cs
Normal file
19
src/Core/Tools/Models/Data/SendAccessResult.cs
Normal file
@ -0,0 +1,19 @@
|
||||
using Bit.Core.Tools.Entities;
|
||||
|
||||
namespace Bit.Core.Tools.Models.Data;
|
||||
|
||||
/// <summary>
|
||||
/// This enum represents the possible results when attempting to access a <see cref="Send"/>.
|
||||
/// </summary>
|
||||
/// <member>name="Granted">Access is granted for the <see cref="Send"/>.</member>
|
||||
/// <member>name="PasswordRequired">Access is denied, but a password is required to access the <see cref="Send"/>.
|
||||
/// </member>
|
||||
/// <member>name="PasswordInvalid">Access is denied due to an invalid password.</member>
|
||||
/// <member>name="Denied">Access is denied for the <see cref="Send"/>.</member>
|
||||
public enum SendAccessResult
|
||||
{
|
||||
Granted,
|
||||
PasswordRequired,
|
||||
PasswordInvalid,
|
||||
Denied
|
||||
}
|
52
src/Core/Tools/SendFeatures/Commands/AnonymousSendCommand.cs
Normal file
52
src/Core/Tools/SendFeatures/Commands/AnonymousSendCommand.cs
Normal file
@ -0,0 +1,52 @@
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Platform.Push;
|
||||
using Bit.Core.Tools.Entities;
|
||||
using Bit.Core.Tools.Enums;
|
||||
using Bit.Core.Tools.Models.Data;
|
||||
using Bit.Core.Tools.Repositories;
|
||||
using Bit.Core.Tools.SendFeatures.Commands.Interfaces;
|
||||
using Bit.Core.Tools.Services;
|
||||
|
||||
namespace Bit.Core.Tools.SendFeatures.Commands;
|
||||
|
||||
public class AnonymousSendCommand : IAnonymousSendCommand
|
||||
{
|
||||
private readonly ISendRepository _sendRepository;
|
||||
private readonly ISendFileStorageService _sendFileStorageService;
|
||||
private readonly IPushNotificationService _pushNotificationService;
|
||||
private readonly ISendAuthorizationService _sendAuthorizationService;
|
||||
|
||||
public AnonymousSendCommand(
|
||||
ISendRepository sendRepository,
|
||||
ISendFileStorageService sendFileStorageService,
|
||||
IPushNotificationService pushNotificationService,
|
||||
ISendAuthorizationService sendAuthorizationService
|
||||
)
|
||||
{
|
||||
_sendRepository = sendRepository;
|
||||
_sendFileStorageService = sendFileStorageService;
|
||||
_pushNotificationService = pushNotificationService;
|
||||
_sendAuthorizationService = sendAuthorizationService;
|
||||
}
|
||||
|
||||
// Response: Send, password required, password invalid
|
||||
public async Task<(string, SendAccessResult)> GetSendFileDownloadUrlAsync(Send send, string fileId, string password)
|
||||
{
|
||||
if (send.Type != SendType.File)
|
||||
{
|
||||
throw new BadRequestException("Can only get a download URL for a file type of Send");
|
||||
}
|
||||
|
||||
var result = _sendAuthorizationService.SendCanBeAccessed(send, password);
|
||||
|
||||
if (!result.Equals(SendAccessResult.Granted))
|
||||
{
|
||||
return (null, result);
|
||||
}
|
||||
|
||||
send.AccessCount++;
|
||||
await _sendRepository.ReplaceAsync(send);
|
||||
await _pushNotificationService.PushSyncSendUpdateAsync(send);
|
||||
return (await _sendFileStorageService.GetSendFileDownloadUrlAsync(send, fileId), result);
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
using Bit.Core.Tools.Entities;
|
||||
using Bit.Core.Tools.Models.Data;
|
||||
|
||||
namespace Bit.Core.Tools.SendFeatures.Commands.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// AnonymousSendCommand interface provides methods for managing anonymous Sends.
|
||||
/// </summary>
|
||||
public interface IAnonymousSendCommand
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the Send file download URL for a Send object.
|
||||
/// </summary>
|
||||
/// <param name="send"><see cref="Send" /> used to help get file download url and validate file</param>
|
||||
/// <param name="fileId">FileId get file download url</param>
|
||||
/// <param name="password">A hashed and base64-encoded password. This is compared with the send's password to authorize access.</param>
|
||||
/// <returns>Async Task object with Tuple containing the string of download url and <see cref="SendAccessResult" />
|
||||
/// to determine if the user can access send.
|
||||
/// </returns>
|
||||
Task<(string, SendAccessResult)> GetSendFileDownloadUrlAsync(Send send, string fileId, string password);
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
using Bit.Core.Tools.Entities;
|
||||
using Bit.Core.Tools.Models.Data;
|
||||
|
||||
namespace Bit.Core.Tools.SendFeatures.Commands.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// NonAnonymousSendCommand interface provides methods for managing non-anonymous Sends.
|
||||
/// </summary>
|
||||
public interface INonAnonymousSendCommand
|
||||
{
|
||||
/// <summary>
|
||||
/// Saves a <see cref="Send" /> to the database.
|
||||
/// </summary>
|
||||
/// <param name="send"><see cref="Send" /> that will save to database</param>
|
||||
/// <returns>Task completes as <see cref="Send" /> saves to the database</returns>
|
||||
Task SaveSendAsync(Send send);
|
||||
|
||||
/// <summary>
|
||||
/// Saves the <see cref="Send" /> and <see cref="SendFileData" /> to the database.
|
||||
/// </summary>
|
||||
/// <param name="send"><see cref="Send" /> that will save to the database</param>
|
||||
/// <param name="data"><see cref="SendFileData" /> that will save to file storage</param>
|
||||
/// <param name="fileLength">Length of file help with saving to file storage</param>
|
||||
/// <returns>Task object for async operations with file upload url</returns>
|
||||
Task<string> SaveFileSendAsync(Send send, SendFileData data, long fileLength);
|
||||
|
||||
/// <summary>
|
||||
/// Upload a file to an existing <see cref="Send" />.
|
||||
/// </summary>
|
||||
/// <param name="stream"><see cref="Stream" /> of file to be uploaded. The <see cref="Stream" /> position
|
||||
/// will be set to 0 before uploading the file.</param>
|
||||
/// <param name="send"><see cref="Send" /> used to help with uploading file</param>
|
||||
/// <returns>Task completes after saving <see cref="Stream" /> and <see cref="Send" /> metadata to the file storage</returns>
|
||||
Task UploadFileToExistingSendAsync(Stream stream, Send send);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a <see cref="Send" /> from the database and file storage.
|
||||
/// </summary>
|
||||
/// <param name="send"><see cref="Send" /> is used to delete from database and file storage</param>
|
||||
/// <returns>Task completes once <see cref="Send" /> has been deleted from database and file storage.</returns>
|
||||
Task DeleteSendAsync(Send send);
|
||||
|
||||
/// <summary>
|
||||
/// Stores the confirmed file size of a send; when the file size cannot be confirmed, the send is deleted.
|
||||
/// </summary>
|
||||
/// <param name="send">The <see cref="Send" /> this command acts upon</param>
|
||||
/// <returns><see langword="true" /> when the file is confirmed, otherwise <see langword="false" /></returns>
|
||||
/// <remarks>
|
||||
/// When a file size cannot be confirmed, we assume we're working with a rogue client. The send is deleted out of
|
||||
/// an abundance of caution.
|
||||
/// </remarks>
|
||||
Task<bool> ConfirmFileSize(Send send);
|
||||
}
|
180
src/Core/Tools/SendFeatures/Commands/NonAnonymousSendCommand.cs
Normal file
180
src/Core/Tools/SendFeatures/Commands/NonAnonymousSendCommand.cs
Normal file
@ -0,0 +1,180 @@
|
||||
using System.Text.Json;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Platform.Push;
|
||||
using Bit.Core.Tools.Entities;
|
||||
using Bit.Core.Tools.Enums;
|
||||
using Bit.Core.Tools.Models.Business;
|
||||
using Bit.Core.Tools.Models.Data;
|
||||
using Bit.Core.Tools.Repositories;
|
||||
using Bit.Core.Tools.SendFeatures.Commands.Interfaces;
|
||||
using Bit.Core.Tools.Services;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Core.Tools.SendFeatures.Commands;
|
||||
|
||||
public class NonAnonymousSendCommand : INonAnonymousSendCommand
|
||||
{
|
||||
private readonly ISendRepository _sendRepository;
|
||||
private readonly ISendFileStorageService _sendFileStorageService;
|
||||
private readonly IPushNotificationService _pushNotificationService;
|
||||
private readonly ISendValidationService _sendValidationService;
|
||||
private readonly IReferenceEventService _referenceEventService;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly ISendCoreHelperService _sendCoreHelperService;
|
||||
|
||||
public NonAnonymousSendCommand(ISendRepository sendRepository,
|
||||
ISendFileStorageService sendFileStorageService,
|
||||
IPushNotificationService pushNotificationService,
|
||||
ISendAuthorizationService sendAuthorizationService,
|
||||
ISendValidationService sendValidationService,
|
||||
IReferenceEventService referenceEventService,
|
||||
ICurrentContext currentContext,
|
||||
ISendCoreHelperService sendCoreHelperService)
|
||||
{
|
||||
_sendRepository = sendRepository;
|
||||
_sendFileStorageService = sendFileStorageService;
|
||||
_pushNotificationService = pushNotificationService;
|
||||
_sendValidationService = sendValidationService;
|
||||
_referenceEventService = referenceEventService;
|
||||
_currentContext = currentContext;
|
||||
_sendCoreHelperService = sendCoreHelperService;
|
||||
}
|
||||
|
||||
public async Task SaveSendAsync(Send send)
|
||||
{
|
||||
// Make sure user can save Sends
|
||||
await _sendValidationService.ValidateUserCanSaveAsync(send.UserId, send);
|
||||
|
||||
if (send.Id == default(Guid))
|
||||
{
|
||||
await _sendRepository.CreateAsync(send);
|
||||
await _pushNotificationService.PushSyncSendCreateAsync(send);
|
||||
await _referenceEventService.RaiseEventAsync(new ReferenceEvent
|
||||
{
|
||||
Id = send.UserId ?? default,
|
||||
Type = ReferenceEventType.SendCreated,
|
||||
Source = ReferenceEventSource.User,
|
||||
SendType = send.Type,
|
||||
MaxAccessCount = send.MaxAccessCount,
|
||||
HasPassword = !string.IsNullOrWhiteSpace(send.Password),
|
||||
SendHasNotes = send.Data?.Contains("Notes"),
|
||||
ClientId = _currentContext.ClientId,
|
||||
ClientVersion = _currentContext.ClientVersion
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
send.RevisionDate = DateTime.UtcNow;
|
||||
await _sendRepository.UpsertAsync(send);
|
||||
await _pushNotificationService.PushSyncSendUpdateAsync(send);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<string> SaveFileSendAsync(Send send, SendFileData data, long fileLength)
|
||||
{
|
||||
if (send.Type != SendType.File)
|
||||
{
|
||||
throw new BadRequestException("Send is not of type \"file\".");
|
||||
}
|
||||
|
||||
if (fileLength < 1)
|
||||
{
|
||||
throw new BadRequestException("No file data.");
|
||||
}
|
||||
|
||||
var storageBytesRemaining = await _sendValidationService.StorageRemainingForSendAsync(send);
|
||||
|
||||
if (storageBytesRemaining < fileLength)
|
||||
{
|
||||
throw new BadRequestException("Not enough storage available.");
|
||||
}
|
||||
|
||||
var fileId = _sendCoreHelperService.SecureRandomString(32, useUpperCase: false, useSpecial: false);
|
||||
|
||||
try
|
||||
{
|
||||
data.Id = fileId;
|
||||
data.Size = fileLength;
|
||||
data.Validated = false;
|
||||
send.Data = JsonSerializer.Serialize(data,
|
||||
JsonHelpers.IgnoreWritingNull);
|
||||
await SaveSendAsync(send);
|
||||
return await _sendFileStorageService.GetSendFileUploadUrlAsync(send, fileId);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Clean up since this is not transactional
|
||||
await _sendFileStorageService.DeleteFileAsync(send, fileId);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
public async Task UploadFileToExistingSendAsync(Stream stream, Send send)
|
||||
{
|
||||
if (stream.Position > 0)
|
||||
{
|
||||
stream.Position = 0;
|
||||
}
|
||||
|
||||
if (send?.Data == null)
|
||||
{
|
||||
throw new BadRequestException("Send does not have file data");
|
||||
}
|
||||
|
||||
if (send.Type != SendType.File)
|
||||
{
|
||||
throw new BadRequestException("Not a File Type Send.");
|
||||
}
|
||||
|
||||
var data = JsonSerializer.Deserialize<SendFileData>(send.Data);
|
||||
|
||||
if (data.Validated)
|
||||
{
|
||||
throw new BadRequestException("File has already been uploaded.");
|
||||
}
|
||||
|
||||
await _sendFileStorageService.UploadNewFileAsync(stream, send, data.Id);
|
||||
|
||||
if (!await ConfirmFileSize(send))
|
||||
{
|
||||
throw new BadRequestException("File received does not match expected file length.");
|
||||
}
|
||||
}
|
||||
public async Task DeleteSendAsync(Send send)
|
||||
{
|
||||
await _sendRepository.DeleteAsync(send);
|
||||
if (send.Type == Enums.SendType.File)
|
||||
{
|
||||
var data = JsonSerializer.Deserialize<SendFileData>(send.Data);
|
||||
await _sendFileStorageService.DeleteFileAsync(send, data.Id);
|
||||
}
|
||||
await _pushNotificationService.PushSyncSendDeleteAsync(send);
|
||||
}
|
||||
|
||||
public async Task<bool> ConfirmFileSize(Send send)
|
||||
{
|
||||
var fileData = JsonSerializer.Deserialize<SendFileData>(send.Data);
|
||||
|
||||
var (valid, realSize) = await _sendFileStorageService.ValidateFileAsync(send, fileData.Id, fileData.Size, SendFileSettingHelper.FILE_SIZE_LEEWAY);
|
||||
|
||||
if (!valid || realSize > SendFileSettingHelper.FILE_SIZE_LEEWAY)
|
||||
{
|
||||
// File reported differs in size from that promised. Must be a rogue client. Delete Send
|
||||
await DeleteSendAsync(send);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Update Send data if necessary
|
||||
if (realSize != fileData.Size)
|
||||
{
|
||||
fileData.Size = realSize.Value;
|
||||
}
|
||||
fileData.Validated = true;
|
||||
send.Data = JsonSerializer.Serialize(fileData,
|
||||
JsonHelpers.IgnoreWritingNull);
|
||||
await SaveSendAsync(send);
|
||||
|
||||
return valid;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
using Bit.Core.Tools.SendFeatures.Commands;
|
||||
using Bit.Core.Tools.SendFeatures.Commands.Interfaces;
|
||||
using Bit.Core.Tools.Services;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Bit.Core.Tools.SendFeatures;
|
||||
|
||||
public static class SendServiceCollectionExtension
|
||||
{
|
||||
public static void AddSendServices(this IServiceCollection services)
|
||||
{
|
||||
services.AddScoped<INonAnonymousSendCommand, NonAnonymousSendCommand>();
|
||||
services.AddScoped<IAnonymousSendCommand, AnonymousSendCommand>();
|
||||
services.AddScoped<ISendAuthorizationService, SendAuthorizationService>();
|
||||
services.AddScoped<ISendValidationService, SendValidationService>();
|
||||
services.AddScoped<ISendCoreHelperService, SendCoreHelperService>();
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
using Bit.Core.Tools.Entities;
|
||||
using Bit.Core.Tools.Models.Data;
|
||||
|
||||
namespace Bit.Core.Tools.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Send Authorization service is responsible for checking if a Send can be accessed.
|
||||
/// </summary>
|
||||
public interface ISendAuthorizationService
|
||||
{
|
||||
/// <summary>
|
||||
/// Checks if a <see cref="Send" /> can be accessed while updating the <see cref="Send" />, pushing a notification, and sending a reference event.
|
||||
/// </summary>
|
||||
/// <param name="send"><see cref="Send" /> used to determine access</param>
|
||||
/// <param name="password">A hashed and base64-encoded password. This is compared with the send's password to authorize access.</param>
|
||||
/// <returns><see cref="SendAccessResult" /> will be returned to determine if the user can access send.
|
||||
/// </returns>
|
||||
Task<SendAccessResult> AccessAsync(Send send, string password);
|
||||
SendAccessResult SendCanBeAccessed(Send send,
|
||||
string password);
|
||||
|
||||
/// <summary>
|
||||
/// Hashes the password using the password hasher.
|
||||
/// </summary>
|
||||
/// <param name="password">Password to be hashed</param>
|
||||
/// <returns>Hashed password of the password given</returns>
|
||||
string HashPassword(string password);
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
namespace Bit.Core.Tools.Services;
|
||||
|
||||
/// <summary>
|
||||
/// This interface provides helper methods for generating secure random strings. Making
|
||||
/// it easier to mock the service in unit tests.
|
||||
/// </summary>
|
||||
public interface ISendCoreHelperService
|
||||
{
|
||||
/// <summary>
|
||||
/// Securely generates a random string of the specified length.
|
||||
/// </summary>
|
||||
/// <param name="length">Desired string length to be returned</param>
|
||||
/// <param name="useUpperCase">Desired casing for the string</param>
|
||||
/// <param name="useSpecial">Determines if special characters will be used in string</param>
|
||||
/// <returns>A secure random string with the desired parameters</returns>
|
||||
string SecureRandomString(int length, bool useUpperCase, bool useSpecial);
|
||||
}
|
@ -0,0 +1,71 @@
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Tools.Entities;
|
||||
|
||||
namespace Bit.Core.Tools.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Send File Storage Service is responsible for uploading, deleting, and validating files
|
||||
/// whether they are in local storage or in cloud storage.
|
||||
/// </summary>
|
||||
public interface ISendFileStorageService
|
||||
{
|
||||
FileUploadType FileUploadType { get; }
|
||||
/// <summary>
|
||||
/// Uploads a new file to the storage.
|
||||
/// </summary>
|
||||
/// <param name="stream"><see cref="Stream" /> of the file</param>
|
||||
/// <param name="send"><see cref="Send" /> for the file</param>
|
||||
/// <param name="fileId">File id</param>
|
||||
/// <returns>Task completes once <see cref="Stream" /> and <see cref="Send" /> have been saved to the database</returns>
|
||||
Task UploadNewFileAsync(Stream stream, Send send, string fileId);
|
||||
/// <summary>
|
||||
/// Deletes a file from the storage.
|
||||
/// </summary>
|
||||
/// <param name="send"><see cref="Send" /> used to delete file</param>
|
||||
/// <param name="fileId">File id of file to be deleted</param>
|
||||
/// <returns>Task completes once <see cref="Send" /> has been deleted to the database</returns>
|
||||
Task DeleteFileAsync(Send send, string fileId);
|
||||
/// <summary>
|
||||
/// Deletes all files for a specific organization.
|
||||
/// </summary>
|
||||
/// <param name="organizationId"><see cref="Guid" /> used to delete all files pertaining to organization</param>
|
||||
/// <returns>Task completes after running code to delete files by organization id</returns>
|
||||
Task DeleteFilesForOrganizationAsync(Guid organizationId);
|
||||
/// <summary>
|
||||
/// Deletes all files for a specific user.
|
||||
/// </summary>
|
||||
/// <param name="userId"><see cref="Guid" /> used to delete all files pertaining to user</param>
|
||||
/// <returns>Task completes after running code to delete files by user id</returns>
|
||||
Task DeleteFilesForUserAsync(Guid userId);
|
||||
/// <summary>
|
||||
/// Gets the download URL for a file.
|
||||
/// </summary>
|
||||
/// <param name="send"><see cref="Send" /> used to help get download url for file</param>
|
||||
/// <param name="fileId">File id to help get download url for file</param>
|
||||
/// <returns>Download url as a string</returns>
|
||||
Task<string> GetSendFileDownloadUrlAsync(Send send, string fileId);
|
||||
/// <summary>
|
||||
/// Gets the upload URL for a file.
|
||||
/// </summary>
|
||||
/// <param name="send"><see cref="Send" /> used to help get upload url for file </param>
|
||||
/// <param name="fileId">File id to help get upload url for file</param>
|
||||
/// <returns>File upload url as string</returns>
|
||||
Task<string> GetSendFileUploadUrlAsync(Send send, string fileId);
|
||||
/// <summary>
|
||||
/// Validates the file size of a file in the storage.
|
||||
/// </summary>
|
||||
/// <param name="send"><see cref="Send" /> used to help validate file</param>
|
||||
/// <param name="fileId">File id to identify which file to validate</param>
|
||||
/// <param name="expectedFileSize">Expected file size of the file</param>
|
||||
/// <param name="leeway">
|
||||
/// Send file size tolerance in bytes. When an uploaded file's `expectedFileSize`
|
||||
/// is outside of the leeway, the storage operation fails.
|
||||
/// </param>
|
||||
/// <throws>
|
||||
/// ❌ Fill this in with an explanation of the error thrown when `leeway` is incorrect
|
||||
/// </throws>
|
||||
/// <returns>Task object for async operations with Tuple of boolean that determines if file was valid and long that
|
||||
/// the actual file size of the file.
|
||||
/// </returns>
|
||||
Task<(bool, long?)> ValidateFileAsync(Send send, string fileId, long expectedFileSize, long leeway);
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
using Bit.Core.Tools.Entities;
|
||||
|
||||
namespace Bit.Core.Tools.Services;
|
||||
|
||||
public interface ISendValidationService
|
||||
{
|
||||
/// <summary>
|
||||
/// Validates a file can be saved by specified user.
|
||||
/// </summary>
|
||||
/// <param name="userId"><see cref="Guid" /> needed to validate file for specific user</param>
|
||||
/// <param name="send"><see cref="Send" /> needed to help validate file</param>
|
||||
/// <returns>Task completes when a conditional statement has been met it will return out of the method or
|
||||
/// throw a BadRequestException.
|
||||
/// </returns>
|
||||
Task ValidateUserCanSaveAsync(Guid? userId, Send send);
|
||||
|
||||
/// <summary>
|
||||
/// Validates a file can be saved by specified user with different policy based on feature flag
|
||||
/// </summary>
|
||||
/// <param name="userId"><see cref="Guid" /> needed to validate file for specific user</param>
|
||||
/// <param name="send"><see cref="Send" /> needed to help validate file</param>
|
||||
/// <returns>Task completes when a conditional statement has been met it will return out of the method or
|
||||
/// throw a BadRequestException.
|
||||
/// </returns>
|
||||
Task ValidateUserCanSaveAsync_vNext(Guid? userId, Send send);
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the remaining storage for a Send.
|
||||
/// </summary>
|
||||
/// <param name="send"><see cref="Send" /> needed to help calculate remaining storage</param>
|
||||
/// <returns>Long with the remaining bytes for storage or will throw a BadRequestException if user cannot access
|
||||
/// file or email is not verified.
|
||||
/// </returns>
|
||||
Task<long> StorageRemainingForSendAsync(Send send);
|
||||
}
|
101
src/Core/Tools/SendFeatures/Services/SendAuthorizationService.cs
Normal file
101
src/Core/Tools/SendFeatures/Services/SendAuthorizationService.cs
Normal file
@ -0,0 +1,101 @@
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Platform.Push;
|
||||
using Bit.Core.Tools.Entities;
|
||||
using Bit.Core.Tools.Enums;
|
||||
using Bit.Core.Tools.Models.Business;
|
||||
using Bit.Core.Tools.Models.Data;
|
||||
using Bit.Core.Tools.Repositories;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace Bit.Core.Tools.Services;
|
||||
|
||||
public class SendAuthorizationService : ISendAuthorizationService
|
||||
{
|
||||
private readonly ISendRepository _sendRepository;
|
||||
private readonly IPasswordHasher<User> _passwordHasher;
|
||||
private readonly IPushNotificationService _pushNotificationService;
|
||||
private readonly IReferenceEventService _referenceEventService;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
|
||||
public SendAuthorizationService(
|
||||
ISendRepository sendRepository,
|
||||
IPasswordHasher<User> passwordHasher,
|
||||
IPushNotificationService pushNotificationService,
|
||||
IReferenceEventService referenceEventService,
|
||||
ICurrentContext currentContext)
|
||||
{
|
||||
_sendRepository = sendRepository;
|
||||
_passwordHasher = passwordHasher;
|
||||
_pushNotificationService = pushNotificationService;
|
||||
_referenceEventService = referenceEventService;
|
||||
_currentContext = currentContext;
|
||||
}
|
||||
|
||||
public SendAccessResult SendCanBeAccessed(Send send,
|
||||
string password)
|
||||
{
|
||||
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 SendAccessResult.Denied;
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(send.Password))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(password))
|
||||
{
|
||||
return SendAccessResult.PasswordRequired;
|
||||
}
|
||||
var passwordResult = _passwordHasher.VerifyHashedPassword(new User(), send.Password, password);
|
||||
if (passwordResult == PasswordVerificationResult.SuccessRehashNeeded)
|
||||
{
|
||||
send.Password = HashPassword(password);
|
||||
}
|
||||
if (passwordResult == PasswordVerificationResult.Failed)
|
||||
{
|
||||
return SendAccessResult.PasswordInvalid;
|
||||
}
|
||||
}
|
||||
|
||||
return SendAccessResult.Granted;
|
||||
}
|
||||
|
||||
public async Task<SendAccessResult> AccessAsync(Send sendToBeAccessed, string password)
|
||||
{
|
||||
var accessResult = SendCanBeAccessed(sendToBeAccessed, password);
|
||||
|
||||
if (!accessResult.Equals(SendAccessResult.Granted))
|
||||
{
|
||||
return accessResult;
|
||||
}
|
||||
|
||||
if (sendToBeAccessed.Type != SendType.File)
|
||||
{
|
||||
// File sends are incremented during file download
|
||||
sendToBeAccessed.AccessCount++;
|
||||
}
|
||||
|
||||
await _sendRepository.ReplaceAsync(sendToBeAccessed);
|
||||
await _pushNotificationService.PushSyncSendUpdateAsync(sendToBeAccessed);
|
||||
await _referenceEventService.RaiseEventAsync(new ReferenceEvent
|
||||
{
|
||||
Id = sendToBeAccessed.UserId ?? default,
|
||||
Type = ReferenceEventType.SendAccessed,
|
||||
Source = ReferenceEventSource.User,
|
||||
SendType = sendToBeAccessed.Type,
|
||||
MaxAccessCount = sendToBeAccessed.MaxAccessCount,
|
||||
HasPassword = !string.IsNullOrWhiteSpace(sendToBeAccessed.Password),
|
||||
SendHasNotes = sendToBeAccessed.Data?.Contains("Notes"),
|
||||
ClientId = _currentContext.ClientId,
|
||||
ClientVersion = _currentContext.ClientVersion
|
||||
});
|
||||
return accessResult;
|
||||
}
|
||||
|
||||
public string HashPassword(string password)
|
||||
{
|
||||
return _passwordHasher.HashPassword(new User(), password);
|
||||
}
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Core.Tools.Services;
|
||||
|
||||
public class SendCoreHelperService : ISendCoreHelperService
|
||||
{
|
||||
public string SecureRandomString(int length, bool useUpperCase, bool useSpecial)
|
||||
{
|
||||
return CoreHelpers.SecureRandomString(length, upper: useUpperCase, special: useSpecial);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
using Bit.Core.Tools.Entities;
|
||||
|
||||
namespace Bit.Core.Tools.SendFeatures;
|
||||
|
||||
/// <summary>
|
||||
/// SendFileSettingHelper is a static class that provides constants and helper methods (if needed) for managing file
|
||||
/// settings.
|
||||
/// </summary>
|
||||
public static class SendFileSettingHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// The leeway for the file size. This is the calculated 1 megabyte of cushion when doing comparisons of file sizes
|
||||
/// within the system.
|
||||
/// </summary>
|
||||
public const long FILE_SIZE_LEEWAY = 1024L * 1024L; // 1MB
|
||||
/// <summary>
|
||||
/// The maximum file size for a file uploaded in a <see cref="Send" />. Units are calculated in bytes but
|
||||
/// represent 501 megabytes. 1 megabyte is added for cushion to account for file size.
|
||||
/// </summary>
|
||||
public const long MAX_FILE_SIZE = Constants.FileSize501mb;
|
||||
|
||||
/// <summary>
|
||||
/// String of the expected file size and to be used when needing to communicate the file size to the client/user.
|
||||
/// </summary>
|
||||
public const string MAX_FILE_SIZE_READABLE = "500 MB";
|
||||
}
|
142
src/Core/Tools/SendFeatures/Services/SendValidationService.cs
Normal file
142
src/Core/Tools/SendFeatures/Services/SendValidationService.cs
Normal file
@ -0,0 +1,142 @@
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Tools.Entities;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Core.Tools.Services;
|
||||
|
||||
public class SendValidationService : ISendValidationService
|
||||
{
|
||||
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly IPolicyService _policyService;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly IUserService _userService;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly IPolicyRequirementQuery _policyRequirementQuery;
|
||||
|
||||
|
||||
|
||||
public SendValidationService(
|
||||
IUserRepository userRepository,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IPolicyService policyService,
|
||||
IFeatureService featureService,
|
||||
IUserService userService,
|
||||
IPolicyRequirementQuery policyRequirementQuery,
|
||||
GlobalSettings globalSettings,
|
||||
|
||||
ICurrentContext currentContext)
|
||||
{
|
||||
_userRepository = userRepository;
|
||||
_organizationRepository = organizationRepository;
|
||||
_policyService = policyService;
|
||||
_featureService = featureService;
|
||||
_userService = userService;
|
||||
_policyRequirementQuery = policyRequirementQuery;
|
||||
_globalSettings = globalSettings;
|
||||
_currentContext = currentContext;
|
||||
}
|
||||
|
||||
public async Task ValidateUserCanSaveAsync(Guid? userId, Send send)
|
||||
{
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements))
|
||||
{
|
||||
await ValidateUserCanSaveAsync_vNext(userId, send);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!userId.HasValue || (!_currentContext.Organizations?.Any() ?? true))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var anyDisableSendPolicies = await _policyService.AnyPoliciesApplicableToUserAsync(userId.Value,
|
||||
PolicyType.DisableSend);
|
||||
if (anyDisableSendPolicies)
|
||||
{
|
||||
throw new BadRequestException("Due to an Enterprise Policy, you are only able to delete an existing Send.");
|
||||
}
|
||||
|
||||
if (send.HideEmail.GetValueOrDefault())
|
||||
{
|
||||
var sendOptionsPolicies = await _policyService.GetPoliciesApplicableToUserAsync(userId.Value, PolicyType.SendOptions);
|
||||
if (sendOptionsPolicies.Any(p => CoreHelpers.LoadClassFromJsonData<SendOptionsPolicyData>(p.PolicyData)?.DisableHideEmail ?? false))
|
||||
{
|
||||
throw new BadRequestException("Due to an Enterprise Policy, you are not allowed to hide your email address from recipients when creating or editing a Send.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task ValidateUserCanSaveAsync_vNext(Guid? userId, Send send)
|
||||
{
|
||||
if (!userId.HasValue)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var disableSendRequirement = await _policyRequirementQuery.GetAsync<DisableSendPolicyRequirement>(userId.Value);
|
||||
if (disableSendRequirement.DisableSend)
|
||||
{
|
||||
throw new BadRequestException("Due to an Enterprise Policy, you are only able to delete an existing Send.");
|
||||
}
|
||||
|
||||
var sendOptionsRequirement = await _policyRequirementQuery.GetAsync<SendOptionsPolicyRequirement>(userId.Value);
|
||||
if (sendOptionsRequirement.DisableHideEmail && send.HideEmail.GetValueOrDefault())
|
||||
{
|
||||
throw new BadRequestException("Due to an Enterprise Policy, you are not allowed to hide your email address from recipients when creating or editing a Send.");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<long> StorageRemainingForSendAsync(Send send)
|
||||
{
|
||||
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.EmailVerified)
|
||||
{
|
||||
throw new BadRequestException("You must confirm your email 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.
|
||||
short limit = _globalSettings.SelfHosted ? (short)10240 : (short)1;
|
||||
storageBytesRemaining = user.StorageBytesRemaining(limit);
|
||||
}
|
||||
}
|
||||
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();
|
||||
}
|
||||
|
||||
return storageBytesRemaining;
|
||||
}
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
using Bit.Core.Tools.Entities;
|
||||
using Bit.Core.Tools.Models.Data;
|
||||
|
||||
namespace Bit.Core.Tools.Services;
|
||||
|
||||
public interface ISendService
|
||||
{
|
||||
Task DeleteSendAsync(Send send);
|
||||
Task SaveSendAsync(Send send);
|
||||
Task<string> SaveFileSendAsync(Send send, SendFileData data, long fileLength);
|
||||
Task UploadFileToExistingSendAsync(Stream stream, Send send);
|
||||
Task<(Send, bool, bool)> AccessAsync(Guid sendId, string password);
|
||||
string HashPassword(string password);
|
||||
Task<(string, bool, bool)> GetSendFileDownloadUrlAsync(Send send, string fileId, string password);
|
||||
Task<bool> ValidateSendFile(Send send);
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Tools.Entities;
|
||||
|
||||
namespace Bit.Core.Tools.Services;
|
||||
|
||||
public interface ISendFileStorageService
|
||||
{
|
||||
FileUploadType FileUploadType { get; }
|
||||
Task UploadNewFileAsync(Stream stream, Send send, string fileId);
|
||||
Task DeleteFileAsync(Send send, string fileId);
|
||||
Task DeleteFilesForOrganizationAsync(Guid organizationId);
|
||||
Task DeleteFilesForUserAsync(Guid userId);
|
||||
Task<string> GetSendFileDownloadUrlAsync(Send send, string fileId);
|
||||
Task<string> GetSendFileUploadUrlAsync(Send send, string fileId);
|
||||
Task<(bool, long?)> ValidateFileAsync(Send send, string fileId, long expectedFileSize, long leeway);
|
||||
}
|
@ -1,383 +0,0 @@
|
||||
using System.Text.Json;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Platform.Push;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Tools.Entities;
|
||||
using Bit.Core.Tools.Enums;
|
||||
using Bit.Core.Tools.Models.Business;
|
||||
using Bit.Core.Tools.Models.Data;
|
||||
using Bit.Core.Tools.Repositories;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace Bit.Core.Tools.Services;
|
||||
|
||||
public class SendService : ISendService
|
||||
{
|
||||
public const long MAX_FILE_SIZE = Constants.FileSize501mb;
|
||||
public const string MAX_FILE_SIZE_READABLE = "500 MB";
|
||||
private readonly ISendRepository _sendRepository;
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly IPolicyService _policyService;
|
||||
private readonly IUserService _userService;
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly ISendFileStorageService _sendFileStorageService;
|
||||
private readonly IPasswordHasher<User> _passwordHasher;
|
||||
private readonly IPushNotificationService _pushService;
|
||||
private readonly IReferenceEventService _referenceEventService;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly IPolicyRequirementQuery _policyRequirementQuery;
|
||||
private readonly IFeatureService _featureService;
|
||||
|
||||
private const long _fileSizeLeeway = 1024L * 1024L; // 1MB
|
||||
|
||||
public SendService(
|
||||
ISendRepository sendRepository,
|
||||
IUserRepository userRepository,
|
||||
IUserService userService,
|
||||
IOrganizationRepository organizationRepository,
|
||||
ISendFileStorageService sendFileStorageService,
|
||||
IPasswordHasher<User> passwordHasher,
|
||||
IPushNotificationService pushService,
|
||||
IReferenceEventService referenceEventService,
|
||||
GlobalSettings globalSettings,
|
||||
IPolicyService policyService,
|
||||
ICurrentContext currentContext,
|
||||
IPolicyRequirementQuery policyRequirementQuery,
|
||||
IFeatureService featureService)
|
||||
{
|
||||
_sendRepository = sendRepository;
|
||||
_userRepository = userRepository;
|
||||
_userService = userService;
|
||||
_policyService = policyService;
|
||||
_organizationRepository = organizationRepository;
|
||||
_sendFileStorageService = sendFileStorageService;
|
||||
_passwordHasher = passwordHasher;
|
||||
_pushService = pushService;
|
||||
_referenceEventService = referenceEventService;
|
||||
_globalSettings = globalSettings;
|
||||
_currentContext = currentContext;
|
||||
_policyRequirementQuery = policyRequirementQuery;
|
||||
_featureService = featureService;
|
||||
}
|
||||
|
||||
public async Task SaveSendAsync(Send send)
|
||||
{
|
||||
// Make sure user can save Sends
|
||||
await ValidateUserCanSaveAsync(send.UserId, send);
|
||||
|
||||
if (send.Id == default(Guid))
|
||||
{
|
||||
await _sendRepository.CreateAsync(send);
|
||||
await _pushService.PushSyncSendCreateAsync(send);
|
||||
await RaiseReferenceEventAsync(send, ReferenceEventType.SendCreated);
|
||||
}
|
||||
else
|
||||
{
|
||||
send.RevisionDate = DateTime.UtcNow;
|
||||
await _sendRepository.UpsertAsync(send);
|
||||
await _pushService.PushSyncSendUpdateAsync(send);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<string> SaveFileSendAsync(Send send, SendFileData data, long fileLength)
|
||||
{
|
||||
if (send.Type != SendType.File)
|
||||
{
|
||||
throw new BadRequestException("Send is not of type \"file\".");
|
||||
}
|
||||
|
||||
if (fileLength < 1)
|
||||
{
|
||||
throw new BadRequestException("No file data.");
|
||||
}
|
||||
|
||||
var storageBytesRemaining = await StorageRemainingForSendAsync(send);
|
||||
|
||||
if (storageBytesRemaining < fileLength)
|
||||
{
|
||||
throw new BadRequestException("Not enough storage available.");
|
||||
}
|
||||
|
||||
var fileId = Utilities.CoreHelpers.SecureRandomString(32, upper: false, special: false);
|
||||
|
||||
try
|
||||
{
|
||||
data.Id = fileId;
|
||||
data.Size = fileLength;
|
||||
data.Validated = false;
|
||||
send.Data = JsonSerializer.Serialize(data,
|
||||
JsonHelpers.IgnoreWritingNull);
|
||||
await SaveSendAsync(send);
|
||||
return await _sendFileStorageService.GetSendFileUploadUrlAsync(send, fileId);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Clean up since this is not transactional
|
||||
await _sendFileStorageService.DeleteFileAsync(send, fileId);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task UploadFileToExistingSendAsync(Stream stream, Send send)
|
||||
{
|
||||
if (send?.Data == null)
|
||||
{
|
||||
throw new BadRequestException("Send does not have file data");
|
||||
}
|
||||
|
||||
if (send.Type != SendType.File)
|
||||
{
|
||||
throw new BadRequestException("Not a File Type Send.");
|
||||
}
|
||||
|
||||
var data = JsonSerializer.Deserialize<SendFileData>(send.Data);
|
||||
|
||||
if (data.Validated)
|
||||
{
|
||||
throw new BadRequestException("File has already been uploaded.");
|
||||
}
|
||||
|
||||
await _sendFileStorageService.UploadNewFileAsync(stream, send, data.Id);
|
||||
|
||||
if (!await ValidateSendFile(send))
|
||||
{
|
||||
throw new BadRequestException("File received does not match expected file length.");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> ValidateSendFile(Send send)
|
||||
{
|
||||
var fileData = JsonSerializer.Deserialize<SendFileData>(send.Data);
|
||||
|
||||
var (valid, realSize) = await _sendFileStorageService.ValidateFileAsync(send, fileData.Id, fileData.Size, _fileSizeLeeway);
|
||||
|
||||
if (!valid || realSize > MAX_FILE_SIZE)
|
||||
{
|
||||
// File reported differs in size from that promised. Must be a rogue client. Delete Send
|
||||
await DeleteSendAsync(send);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Update Send data if necessary
|
||||
if (realSize != fileData.Size)
|
||||
{
|
||||
fileData.Size = realSize.Value;
|
||||
}
|
||||
fileData.Validated = true;
|
||||
send.Data = JsonSerializer.Serialize(fileData,
|
||||
JsonHelpers.IgnoreWritingNull);
|
||||
await SaveSendAsync(send);
|
||||
|
||||
return valid;
|
||||
}
|
||||
|
||||
public async Task DeleteSendAsync(Send send)
|
||||
{
|
||||
await _sendRepository.DeleteAsync(send);
|
||||
if (send.Type == Enums.SendType.File)
|
||||
{
|
||||
var data = JsonSerializer.Deserialize<SendFileData>(send.Data);
|
||||
await _sendFileStorageService.DeleteFileAsync(send, data.Id);
|
||||
}
|
||||
await _pushService.PushSyncSendDeleteAsync(send);
|
||||
}
|
||||
|
||||
public (bool grant, bool passwordRequiredError, bool passwordInvalidError) SendCanBeAccessed(Send send,
|
||||
string password)
|
||||
{
|
||||
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 (false, false, false);
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(send.Password))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(password))
|
||||
{
|
||||
return (false, true, false);
|
||||
}
|
||||
var passwordResult = _passwordHasher.VerifyHashedPassword(new User(), send.Password, password);
|
||||
if (passwordResult == PasswordVerificationResult.SuccessRehashNeeded)
|
||||
{
|
||||
send.Password = HashPassword(password);
|
||||
}
|
||||
if (passwordResult == PasswordVerificationResult.Failed)
|
||||
{
|
||||
return (false, false, true);
|
||||
}
|
||||
}
|
||||
|
||||
return (true, false, false);
|
||||
}
|
||||
|
||||
// Response: Send, password required, password invalid
|
||||
public async Task<(string, bool, bool)> GetSendFileDownloadUrlAsync(Send send, string fileId, string password)
|
||||
{
|
||||
if (send.Type != SendType.File)
|
||||
{
|
||||
throw new BadRequestException("Can only get a download URL for a file type of Send");
|
||||
}
|
||||
|
||||
var (grantAccess, passwordRequired, passwordInvalid) = SendCanBeAccessed(send, password);
|
||||
|
||||
if (!grantAccess)
|
||||
{
|
||||
return (null, passwordRequired, passwordInvalid);
|
||||
}
|
||||
|
||||
send.AccessCount++;
|
||||
await _sendRepository.ReplaceAsync(send);
|
||||
await _pushService.PushSyncSendUpdateAsync(send);
|
||||
return (await _sendFileStorageService.GetSendFileDownloadUrlAsync(send, fileId), false, false);
|
||||
}
|
||||
|
||||
// Response: Send, password required, password invalid
|
||||
public async Task<(Send, bool, bool)> AccessAsync(Guid sendId, string password)
|
||||
{
|
||||
var send = await _sendRepository.GetByIdAsync(sendId);
|
||||
var (grantAccess, passwordRequired, passwordInvalid) = SendCanBeAccessed(send, password);
|
||||
|
||||
if (!grantAccess)
|
||||
{
|
||||
return (null, passwordRequired, passwordInvalid);
|
||||
}
|
||||
|
||||
// TODO: maybe move this to a simple ++ sproc?
|
||||
if (send.Type != SendType.File)
|
||||
{
|
||||
// File sends are incremented during file download
|
||||
send.AccessCount++;
|
||||
}
|
||||
|
||||
await _sendRepository.ReplaceAsync(send);
|
||||
await _pushService.PushSyncSendUpdateAsync(send);
|
||||
await RaiseReferenceEventAsync(send, ReferenceEventType.SendAccessed);
|
||||
return (send, false, false);
|
||||
}
|
||||
|
||||
private async Task RaiseReferenceEventAsync(Send send, ReferenceEventType eventType)
|
||||
{
|
||||
await _referenceEventService.RaiseEventAsync(new ReferenceEvent
|
||||
{
|
||||
Id = send.UserId ?? default,
|
||||
Type = eventType,
|
||||
Source = ReferenceEventSource.User,
|
||||
SendType = send.Type,
|
||||
MaxAccessCount = send.MaxAccessCount,
|
||||
HasPassword = !string.IsNullOrWhiteSpace(send.Password),
|
||||
SendHasNotes = send.Data?.Contains("Notes"),
|
||||
ClientId = _currentContext.ClientId,
|
||||
ClientVersion = _currentContext.ClientVersion
|
||||
});
|
||||
}
|
||||
|
||||
public string HashPassword(string password)
|
||||
{
|
||||
return _passwordHasher.HashPassword(new User(), password);
|
||||
}
|
||||
|
||||
private async Task ValidateUserCanSaveAsync(Guid? userId, Send send)
|
||||
{
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements))
|
||||
{
|
||||
await ValidateUserCanSaveAsync_vNext(userId, send);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!userId.HasValue || (!_currentContext.Organizations?.Any() ?? true))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var anyDisableSendPolicies = await _policyService.AnyPoliciesApplicableToUserAsync(userId.Value,
|
||||
PolicyType.DisableSend);
|
||||
if (anyDisableSendPolicies)
|
||||
{
|
||||
throw new BadRequestException("Due to an Enterprise Policy, you are only able to delete an existing Send.");
|
||||
}
|
||||
|
||||
if (send.HideEmail.GetValueOrDefault())
|
||||
{
|
||||
var sendOptionsPolicies = await _policyService.GetPoliciesApplicableToUserAsync(userId.Value, PolicyType.SendOptions);
|
||||
if (sendOptionsPolicies.Any(p => CoreHelpers.LoadClassFromJsonData<SendOptionsPolicyData>(p.PolicyData)?.DisableHideEmail ?? false))
|
||||
{
|
||||
throw new BadRequestException("Due to an Enterprise Policy, you are not allowed to hide your email address from recipients when creating or editing a Send.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ValidateUserCanSaveAsync_vNext(Guid? userId, Send send)
|
||||
{
|
||||
if (!userId.HasValue)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var disableSendRequirement = await _policyRequirementQuery.GetAsync<DisableSendPolicyRequirement>(userId.Value);
|
||||
if (disableSendRequirement.DisableSend)
|
||||
{
|
||||
throw new BadRequestException("Due to an Enterprise Policy, you are only able to delete an existing Send.");
|
||||
}
|
||||
|
||||
var sendOptionsRequirement = await _policyRequirementQuery.GetAsync<SendOptionsPolicyRequirement>(userId.Value);
|
||||
if (sendOptionsRequirement.DisableHideEmail && send.HideEmail.GetValueOrDefault())
|
||||
{
|
||||
throw new BadRequestException("Due to an Enterprise Policy, you are not allowed to hide your email address from recipients when creating or editing a Send.");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<long> StorageRemainingForSendAsync(Send send)
|
||||
{
|
||||
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.EmailVerified)
|
||||
{
|
||||
throw new BadRequestException("You must confirm your email 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();
|
||||
}
|
||||
|
||||
return storageBytesRemaining;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user