Files
server/src/Core/Services/Implementations/CipherService.cs

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

1004 lines
40 KiB
C#
Raw Normal View History

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Business;
using Bit.Core.Models.Data;
using Bit.Core.Repositories;
using Bit.Core.Settings;
2019-07-25 15:50:13 -04:00
using Bit.Core.Utilities;
using Core.Models.Data;
namespace Bit.Core.Services
{
public class CipherService : ICipherService
{
public const long MAX_FILE_SIZE = Constants.FileSize501mb;
public const string MAX_FILE_SIZE_READABLE = "500 MB";
private readonly ICipherRepository _cipherRepository;
private readonly IFolderRepository _folderRepository;
2018-07-17 13:34:12 -04:00
private readonly ICollectionRepository _collectionRepository;
2017-02-25 23:38:24 -05:00
private readonly IUserRepository _userRepository;
private readonly IOrganizationRepository _organizationRepository;
2017-04-27 09:19:30 -04:00
private readonly ICollectionCipherRepository _collectionCipherRepository;
2017-05-26 09:44:54 -04:00
private readonly IPushNotificationService _pushService;
private readonly IAttachmentStorageService _attachmentStorageService;
2017-12-01 14:06:16 -05:00
private readonly IEventService _eventService;
2018-08-28 17:40:08 -04:00
private readonly IUserService _userService;
private readonly IPolicyRepository _policyRepository;
2018-08-29 09:45:57 -04:00
private readonly GlobalSettings _globalSettings;
private const long _fileSizeLeeway = 1024L * 1024L; // 1MB
private readonly IReferenceEventService _referenceEventService;
public CipherService(
ICipherRepository cipherRepository,
IFolderRepository folderRepository,
2018-07-17 13:34:12 -04:00
ICollectionRepository collectionRepository,
2017-02-25 23:38:24 -05:00
IUserRepository userRepository,
IOrganizationRepository organizationRepository,
2017-04-27 09:19:30 -04:00
ICollectionCipherRepository collectionCipherRepository,
IPushNotificationService pushService,
2017-12-01 14:06:16 -05:00
IAttachmentStorageService attachmentStorageService,
2018-08-28 17:40:08 -04:00
IEventService eventService,
2018-08-29 09:45:57 -04:00
IUserService userService,
IPolicyRepository policyRepository,
GlobalSettings globalSettings,
IReferenceEventService referenceEventService)
{
_cipherRepository = cipherRepository;
_folderRepository = folderRepository;
2018-07-17 13:34:12 -04:00
_collectionRepository = collectionRepository;
2017-02-25 23:38:24 -05:00
_userRepository = userRepository;
_organizationRepository = organizationRepository;
2017-04-27 09:19:30 -04:00
_collectionCipherRepository = collectionCipherRepository;
_pushService = pushService;
_attachmentStorageService = attachmentStorageService;
2017-12-01 14:06:16 -05:00
_eventService = eventService;
2018-08-28 17:40:08 -04:00
_userService = userService;
_policyRepository = policyRepository;
2018-08-29 09:45:57 -04:00
_globalSettings = globalSettings;
_referenceEventService = referenceEventService;
}
Validate cipher updates with revision date (#994) * Add last updated validation to cipher replacements * Add AutoFixture scaffolding. AutoDataAttributes and ICustomizations are meant to automatically produce valid test input. Examples are the Cipher customizations, which enforce the model's mutual exclusivity of UserId and OrganizationId. FixtureExtensions create a fluent way to generate SUTs. We currently use parameter injection to fascilitate service testing, which is nicely handled by AutoNSubstitute. However, in order to gain access to the substitutions, we need to Freeze them onto the Fixture. The For fluent method allows specifying a Freeze to a specific type's constructor and optionally to a parameter name in that constructor. * Unit tests for single Cipher update version checks * Fix test runner Test runner requires Microsoft.NET.Test.Sdk * Move to provider model for SUT generation This model differs from previous in that you no longer need to specify which dependencies you would like access to. Instead, all are remembered and can be queried through the sutProvider. * User cipher provided by Put method reads Every put method already reads all relevant ciphers from database, there's no need to re-read them. JSON serialization of datetimes seems to leave truncate at second precision. Verify last known date time is within one second rather than exact. * validate revision date for share many requests * Update build script to use Github environment path Co-authored-by: Matt Gibson <mdgibson@Matts-MBP.lan>
2020-11-23 08:48:05 -06:00
public async Task SaveAsync(Cipher cipher, Guid savingUserId, DateTime? lastKnownRevisionDate,
IEnumerable<Guid> collectionIds = null, bool skipPermissionCheck = false, bool limitCollectionScope = true)
2017-04-27 14:50:22 -04:00
{
if (!skipPermissionCheck && !(await UserCanEditAsync(cipher, savingUserId)))
2017-04-27 14:50:22 -04:00
{
throw new BadRequestException("You do not have permissions to edit this.");
}
if (cipher.Id == default(Guid))
2017-04-27 14:50:22 -04:00
{
if (cipher.OrganizationId.HasValue && collectionIds != null)
{
if (limitCollectionScope)
{
// Set user ID to limit scope of collection ids in the create sproc
cipher.UserId = savingUserId;
}
await _cipherRepository.CreateAsync(cipher, collectionIds);
await _referenceEventService.RaiseEventAsync(
new ReferenceEvent(ReferenceEventType.CipherCreated, await _organizationRepository.GetByIdAsync(cipher.OrganizationId.Value)));
}
else
{
await _cipherRepository.CreateAsync(cipher);
}
2017-12-01 14:06:16 -05:00
await _eventService.LogCipherEventAsync(cipher, Enums.EventType.Cipher_Created);
2017-04-27 14:50:22 -04:00
// push
await _pushService.PushSyncCipherCreateAsync(cipher, null);
2017-04-27 14:50:22 -04:00
}
else
{
if (collectionIds != null)
{
throw new ArgumentException("Cannot create cipher with collection ids at the same time.");
}
Validate cipher updates with revision date (#994) * Add last updated validation to cipher replacements * Add AutoFixture scaffolding. AutoDataAttributes and ICustomizations are meant to automatically produce valid test input. Examples are the Cipher customizations, which enforce the model's mutual exclusivity of UserId and OrganizationId. FixtureExtensions create a fluent way to generate SUTs. We currently use parameter injection to fascilitate service testing, which is nicely handled by AutoNSubstitute. However, in order to gain access to the substitutions, we need to Freeze them onto the Fixture. The For fluent method allows specifying a Freeze to a specific type's constructor and optionally to a parameter name in that constructor. * Unit tests for single Cipher update version checks * Fix test runner Test runner requires Microsoft.NET.Test.Sdk * Move to provider model for SUT generation This model differs from previous in that you no longer need to specify which dependencies you would like access to. Instead, all are remembered and can be queried through the sutProvider. * User cipher provided by Put method reads Every put method already reads all relevant ciphers from database, there's no need to re-read them. JSON serialization of datetimes seems to leave truncate at second precision. Verify last known date time is within one second rather than exact. * validate revision date for share many requests * Update build script to use Github environment path Co-authored-by: Matt Gibson <mdgibson@Matts-MBP.lan>
2020-11-23 08:48:05 -06:00
ValidateCipherLastKnownRevisionDateAsync(cipher, lastKnownRevisionDate);
2017-04-27 14:50:22 -04:00
cipher.RevisionDate = DateTime.UtcNow;
await _cipherRepository.ReplaceAsync(cipher);
2017-12-01 14:06:16 -05:00
await _eventService.LogCipherEventAsync(cipher, Enums.EventType.Cipher_Updated);
2017-04-27 14:50:22 -04:00
// push
await _pushService.PushSyncCipherUpdateAsync(cipher, null);
2017-04-27 14:50:22 -04:00
}
}
Validate cipher updates with revision date (#994) * Add last updated validation to cipher replacements * Add AutoFixture scaffolding. AutoDataAttributes and ICustomizations are meant to automatically produce valid test input. Examples are the Cipher customizations, which enforce the model's mutual exclusivity of UserId and OrganizationId. FixtureExtensions create a fluent way to generate SUTs. We currently use parameter injection to fascilitate service testing, which is nicely handled by AutoNSubstitute. However, in order to gain access to the substitutions, we need to Freeze them onto the Fixture. The For fluent method allows specifying a Freeze to a specific type's constructor and optionally to a parameter name in that constructor. * Unit tests for single Cipher update version checks * Fix test runner Test runner requires Microsoft.NET.Test.Sdk * Move to provider model for SUT generation This model differs from previous in that you no longer need to specify which dependencies you would like access to. Instead, all are remembered and can be queried through the sutProvider. * User cipher provided by Put method reads Every put method already reads all relevant ciphers from database, there's no need to re-read them. JSON serialization of datetimes seems to leave truncate at second precision. Verify last known date time is within one second rather than exact. * validate revision date for share many requests * Update build script to use Github environment path Co-authored-by: Matt Gibson <mdgibson@Matts-MBP.lan>
2020-11-23 08:48:05 -06:00
public async Task SaveDetailsAsync(CipherDetails cipher, Guid savingUserId, DateTime? lastKnownRevisionDate,
IEnumerable<Guid> collectionIds = null, bool skipPermissionCheck = false)
{
if (!skipPermissionCheck && !(await UserCanEditAsync(cipher, savingUserId)))
{
throw new BadRequestException("You do not have permissions to edit this.");
}
cipher.UserId = savingUserId;
if (cipher.Id == default(Guid))
{
if (cipher.OrganizationId.HasValue && collectionIds != null)
{
var existingCollectionIds = (await _collectionRepository.GetManyByOrganizationIdAsync(cipher.OrganizationId.Value)).Select(c => c.Id);
if (collectionIds.Except(existingCollectionIds).Any())
{
throw new BadRequestException("Specified CollectionId does not exist on the specified Organization.");
}
await _cipherRepository.CreateAsync(cipher, collectionIds);
}
else
{
// Make sure the user can save new ciphers to their personal vault
var personalOwnershipPolicyCount = await _policyRepository.GetCountByTypeApplicableToUserIdAsync(savingUserId,
PolicyType.PersonalOwnership);
if (personalOwnershipPolicyCount > 0)
{
throw new BadRequestException("Due to an Enterprise Policy, you are restricted from saving items to your personal vault.");
}
await _cipherRepository.CreateAsync(cipher);
}
2017-12-01 14:06:16 -05:00
await _eventService.LogCipherEventAsync(cipher, Enums.EventType.Cipher_Created);
if (cipher.OrganizationId.HasValue)
2017-07-07 14:08:30 -04:00
{
var org = await _organizationRepository.GetByIdAsync(cipher.OrganizationId.Value);
cipher.OrganizationUseTotp = org.UseTotp;
}
// push
await _pushService.PushSyncCipherCreateAsync(cipher, null);
}
else
{
if (collectionIds != null)
{
throw new ArgumentException("Cannot create cipher with collection ids at the same time.");
}
Validate cipher updates with revision date (#994) * Add last updated validation to cipher replacements * Add AutoFixture scaffolding. AutoDataAttributes and ICustomizations are meant to automatically produce valid test input. Examples are the Cipher customizations, which enforce the model's mutual exclusivity of UserId and OrganizationId. FixtureExtensions create a fluent way to generate SUTs. We currently use parameter injection to fascilitate service testing, which is nicely handled by AutoNSubstitute. However, in order to gain access to the substitutions, we need to Freeze them onto the Fixture. The For fluent method allows specifying a Freeze to a specific type's constructor and optionally to a parameter name in that constructor. * Unit tests for single Cipher update version checks * Fix test runner Test runner requires Microsoft.NET.Test.Sdk * Move to provider model for SUT generation This model differs from previous in that you no longer need to specify which dependencies you would like access to. Instead, all are remembered and can be queried through the sutProvider. * User cipher provided by Put method reads Every put method already reads all relevant ciphers from database, there's no need to re-read them. JSON serialization of datetimes seems to leave truncate at second precision. Verify last known date time is within one second rather than exact. * validate revision date for share many requests * Update build script to use Github environment path Co-authored-by: Matt Gibson <mdgibson@Matts-MBP.lan>
2020-11-23 08:48:05 -06:00
ValidateCipherLastKnownRevisionDateAsync(cipher, lastKnownRevisionDate);
cipher.RevisionDate = DateTime.UtcNow;
await _cipherRepository.ReplaceAsync(cipher);
2017-12-01 14:06:16 -05:00
await _eventService.LogCipherEventAsync(cipher, Enums.EventType.Cipher_Updated);
// push
await _pushService.PushSyncCipherUpdateAsync(cipher, null);
}
}
public async Task UploadFileForExistingAttachmentAsync(Stream stream, Cipher cipher, CipherAttachment.MetaData attachment)
{
if (attachment == null)
{
throw new BadRequestException("Cipher attachment does not exist");
}
await _attachmentStorageService.UploadNewAttachmentAsync(stream, cipher, attachment);
if (!await ValidateCipherAttachmentFile(cipher, attachment))
{
throw new BadRequestException("File received does not match expected file length.");
}
}
public async Task<(string attachmentId, string uploadUrl)> CreateAttachmentForDelayedUploadAsync(Cipher cipher,
2021-12-14 15:05:07 +00:00
string key, string fileName, long fileSize, bool adminRequest, Guid savingUserId)
{
2021-12-14 15:05:07 +00:00
await ValidateCipherEditForAttachmentAsync(cipher, savingUserId, adminRequest, fileSize);
2017-07-05 16:06:53 -04:00
var attachmentId = Utilities.CoreHelpers.SecureRandomString(32, upper: false, special: false);
var data = new CipherAttachment.MetaData
{
AttachmentId = attachmentId,
2021-12-14 15:05:07 +00:00
FileName = fileName,
Key = key,
Size = fileSize,
Validated = false,
};
2017-07-05 16:06:53 -04:00
var uploadUrl = await _attachmentStorageService.GetAttachmentUploadUrlAsync(cipher, data);
await _cipherRepository.UpdateAttachmentAsync(new CipherAttachment
{
Id = cipher.Id,
UserId = cipher.UserId,
OrganizationId = cipher.OrganizationId,
AttachmentId = attachmentId,
AttachmentData = JsonSerializer.Serialize(data)
});
cipher.AddAttachment(attachmentId, data);
await _pushService.PushSyncCipherUpdateAsync(cipher, null);
return (attachmentId, uploadUrl);
}
public async Task CreateAttachmentAsync(Cipher cipher, Stream stream, string fileName, string key,
long requestLength, Guid savingUserId, bool orgAdmin = false)
{
await ValidateCipherEditForAttachmentAsync(cipher, savingUserId, orgAdmin, requestLength);
var attachmentId = Utilities.CoreHelpers.SecureRandomString(32, upper: false, special: false);
var data = new CipherAttachment.MetaData
{
AttachmentId = attachmentId,
FileName = fileName,
Key = key,
};
await _attachmentStorageService.UploadNewAttachmentAsync(stream, cipher, data);
// Must read stream length after it has been saved, otherwise it's 0
data.Size = stream.Length;
try
{
var attachment = new CipherAttachment
{
Id = cipher.Id,
UserId = cipher.UserId,
OrganizationId = cipher.OrganizationId,
AttachmentId = attachmentId,
AttachmentData = JsonSerializer.Serialize(data)
};
await _cipherRepository.UpdateAttachmentAsync(attachment);
2017-12-01 14:06:16 -05:00
await _eventService.LogCipherEventAsync(cipher, Enums.EventType.Cipher_AttachmentCreated);
2017-07-07 11:07:22 -04:00
cipher.AddAttachment(attachmentId, data);
if (!await ValidateCipherAttachmentFile(cipher, data))
{
throw new Exception("Content-Length does not match uploaded file size");
}
}
catch
{
// Clean up since this is not transactional
await _attachmentStorageService.DeleteAttachmentAsync(cipher.Id, data);
throw;
}
// push
await _pushService.PushSyncCipherUpdateAsync(cipher, null);
}
public async Task CreateAttachmentShareAsync(Cipher cipher, Stream stream, long requestLength,
2017-07-10 14:30:12 -04:00
string attachmentId, Guid organizationId)
{
2017-07-10 16:21:18 -04:00
try
2017-07-10 14:30:12 -04:00
{
if (requestLength < 1)
2017-07-10 16:21:18 -04:00
{
throw new BadRequestException("No data to attach.");
}
2017-07-10 14:30:12 -04:00
if (cipher.Id == default(Guid))
2017-07-10 16:21:18 -04:00
{
throw new BadRequestException(nameof(cipher.Id));
}
2017-07-10 14:30:12 -04:00
if (cipher.OrganizationId.HasValue)
2017-07-10 16:21:18 -04:00
{
throw new BadRequestException("Cipher belongs to an organization already.");
}
var org = await _organizationRepository.GetByIdAsync(organizationId);
if (org == null || !org.MaxStorageGb.HasValue)
2017-07-10 16:21:18 -04:00
{
throw new BadRequestException("This organization cannot use attachments.");
}
var storageBytesRemaining = org.StorageBytesRemaining();
if (storageBytesRemaining < requestLength)
2017-07-10 16:21:18 -04:00
{
throw new BadRequestException("Not enough storage available for this organization.");
}
var attachments = cipher.GetAttachments();
if (!attachments.ContainsKey(attachmentId))
{
throw new BadRequestException($"Cipher does not own specified attachment");
}
await _attachmentStorageService.UploadShareAttachmentAsync(stream, cipher.Id, organizationId,
attachments[attachmentId]);
// Previous call may alter metadata
var updatedAttachment = new CipherAttachment
{
Id = cipher.Id,
UserId = cipher.UserId,
OrganizationId = cipher.OrganizationId,
AttachmentId = attachmentId,
AttachmentData = JsonSerializer.Serialize(attachments[attachmentId])
};
await _cipherRepository.UpdateAttachmentAsync(updatedAttachment);
2017-07-10 14:30:12 -04:00
}
2017-07-10 16:21:18 -04:00
catch
{
2017-07-10 20:48:06 -04:00
await _attachmentStorageService.CleanupAsync(cipher.Id);
2017-07-10 16:21:18 -04:00
throw;
}
2017-07-10 14:30:12 -04:00
}
public async Task<bool> ValidateCipherAttachmentFile(Cipher cipher, CipherAttachment.MetaData attachmentData)
{
var (valid, realSize) = await _attachmentStorageService.ValidateFileAsync(cipher, attachmentData, _fileSizeLeeway);
if (!valid || realSize > MAX_FILE_SIZE)
{
// File reported differs in size from that promised. Must be a rogue client. Delete Send
await DeleteAttachmentAsync(cipher, attachmentData);
return false;
}
// Update Send data if necessary
if (realSize != attachmentData.Size)
{
attachmentData.Size = realSize.Value;
}
attachmentData.Validated = true;
var updatedAttachment = new CipherAttachment
{
Id = cipher.Id,
UserId = cipher.UserId,
OrganizationId = cipher.OrganizationId,
AttachmentId = attachmentData.AttachmentId,
AttachmentData = JsonSerializer.Serialize(attachmentData)
};
await _cipherRepository.UpdateAttachmentAsync(updatedAttachment);
return valid;
}
2021-12-14 15:05:07 +00:00
public async Task<AttachmentResponseData> GetAttachmentDownloadDataAsync(Cipher cipher, string attachmentId)
{
var attachments = cipher?.GetAttachments() ?? new Dictionary<string, CipherAttachment.MetaData>();
if (!attachments.ContainsKey(attachmentId))
{
throw new NotFoundException();
}
var data = attachments[attachmentId];
2021-12-14 15:05:07 +00:00
var response = new AttachmentResponseData
{
2021-12-14 15:05:07 +00:00
Cipher = cipher,
Data = data,
Id = attachmentId,
Url = await _attachmentStorageService.GetAttachmentDownloadUrlAsync(cipher, data),
};
return response;
}
2017-04-19 16:00:47 -04:00
public async Task DeleteAsync(Cipher cipher, Guid deletingUserId, bool orgAdmin = false)
{
if (!orgAdmin && !(await UserCanEditAsync(cipher, deletingUserId)))
{
throw new BadRequestException("You do not have permissions to delete this.");
}
await _cipherRepository.DeleteAsync(cipher);
2017-07-10 20:48:06 -04:00
await _attachmentStorageService.DeleteAttachmentsForCipherAsync(cipher.Id);
await _eventService.LogCipherEventAsync(cipher, EventType.Cipher_Deleted);
// push
await _pushService.PushSyncCipherDeleteAsync(cipher);
}
public async Task DeleteManyAsync(IEnumerable<Guid> cipherIds, Guid deletingUserId, Guid? organizationId = null, bool orgAdmin = false)
2017-06-09 00:30:59 -04:00
{
2019-01-10 22:24:08 -05:00
var cipherIdsSet = new HashSet<Guid>(cipherIds);
var deletingCiphers = new List<Cipher>();
2019-01-10 22:24:08 -05:00
if (orgAdmin && organizationId.HasValue)
{
var ciphers = await _cipherRepository.GetManyByOrganizationIdAsync(organizationId.Value);
deletingCiphers = ciphers.Where(c => cipherIdsSet.Contains(c.Id)).ToList();
await _cipherRepository.DeleteByIdsOrganizationIdAsync(deletingCiphers.Select(c => c.Id), organizationId.Value);
}
else
{
var ciphers = await _cipherRepository.GetManyByUserIdAsync(deletingUserId);
deletingCiphers = ciphers.Where(c => cipherIdsSet.Contains(c.Id) && c.Edit).Select(x => (Cipher)x).ToList();
await _cipherRepository.DeleteAsync(deletingCiphers.Select(c => c.Id), deletingUserId);
}
2019-01-10 22:24:08 -05:00
2019-07-25 15:50:13 -04:00
var events = deletingCiphers.Select(c =>
new Tuple<Cipher, EventType, DateTime?>(c, EventType.Cipher_Deleted, null));
foreach (var eventsBatch in events.Batch(100))
2019-07-25 15:50:13 -04:00
{
await _eventService.LogCipherEventsAsync(eventsBatch);
}
2019-01-10 22:24:08 -05:00
2017-06-09 00:30:59 -04:00
// push
await _pushService.PushSyncCiphersAsync(deletingUserId);
}
public async Task DeleteAttachmentAsync(Cipher cipher, string attachmentId, Guid deletingUserId,
bool orgAdmin = false)
2017-07-07 11:07:22 -04:00
{
if (!orgAdmin && !(await UserCanEditAsync(cipher, deletingUserId)))
2017-07-07 11:07:22 -04:00
{
throw new BadRequestException("You do not have permissions to delete this.");
}
if (!cipher.ContainsAttachment(attachmentId))
2017-07-07 11:07:22 -04:00
{
throw new NotFoundException();
}
await DeleteAttachmentAsync(cipher, cipher.GetAttachments()[attachmentId]);
2017-07-07 11:07:22 -04:00
}
2018-09-25 09:12:50 -04:00
public async Task PurgeAsync(Guid organizationId)
{
var org = await _organizationRepository.GetByIdAsync(organizationId);
if (org == null)
2018-09-25 09:12:50 -04:00
{
throw new NotFoundException();
}
await _cipherRepository.DeleteByOrganizationIdAsync(organizationId);
await _eventService.LogOrganizationEventAsync(org, Enums.EventType.Organization_PurgedVault);
}
public async Task MoveManyAsync(IEnumerable<Guid> cipherIds, Guid? destinationFolderId, Guid movingUserId)
2017-06-09 00:30:59 -04:00
{
if (destinationFolderId.HasValue)
{
var folder = await _folderRepository.GetByIdAsync(destinationFolderId.Value);
if (folder == null || folder.UserId != movingUserId)
{
throw new BadRequestException("Invalid folder.");
}
}
2017-06-09 00:30:59 -04:00
await _cipherRepository.MoveAsync(cipherIds, destinationFolderId, movingUserId);
// push
await _pushService.PushSyncCiphersAsync(movingUserId);
}
public async Task SaveFolderAsync(Folder folder)
{
if (folder.Id == default(Guid))
{
await _folderRepository.CreateAsync(folder);
// push
await _pushService.PushSyncFolderCreateAsync(folder);
}
else
{
folder.RevisionDate = DateTime.UtcNow;
await _folderRepository.UpsertAsync(folder);
// push
await _pushService.PushSyncFolderUpdateAsync(folder);
}
}
public async Task DeleteFolderAsync(Folder folder)
{
await _folderRepository.DeleteAsync(folder);
// push
await _pushService.PushSyncFolderDeleteAsync(folder);
}
2017-07-10 14:30:12 -04:00
public async Task ShareAsync(Cipher originalCipher, Cipher cipher, Guid organizationId,
Validate cipher updates with revision date (#994) * Add last updated validation to cipher replacements * Add AutoFixture scaffolding. AutoDataAttributes and ICustomizations are meant to automatically produce valid test input. Examples are the Cipher customizations, which enforce the model's mutual exclusivity of UserId and OrganizationId. FixtureExtensions create a fluent way to generate SUTs. We currently use parameter injection to fascilitate service testing, which is nicely handled by AutoNSubstitute. However, in order to gain access to the substitutions, we need to Freeze them onto the Fixture. The For fluent method allows specifying a Freeze to a specific type's constructor and optionally to a parameter name in that constructor. * Unit tests for single Cipher update version checks * Fix test runner Test runner requires Microsoft.NET.Test.Sdk * Move to provider model for SUT generation This model differs from previous in that you no longer need to specify which dependencies you would like access to. Instead, all are remembered and can be queried through the sutProvider. * User cipher provided by Put method reads Every put method already reads all relevant ciphers from database, there's no need to re-read them. JSON serialization of datetimes seems to leave truncate at second precision. Verify last known date time is within one second rather than exact. * validate revision date for share many requests * Update build script to use Github environment path Co-authored-by: Matt Gibson <mdgibson@Matts-MBP.lan>
2020-11-23 08:48:05 -06:00
IEnumerable<Guid> collectionIds, Guid sharingUserId, DateTime? lastKnownRevisionDate)
{
2017-07-10 14:30:12 -04:00
var attachments = cipher.GetAttachments();
2018-11-15 12:52:31 -05:00
var hasAttachments = attachments?.Any() ?? false;
var hasOldAttachments = attachments?.Any(a => a.Key == null) ?? false;
2017-07-10 16:21:18 -04:00
var updatedCipher = false;
var migratedAttachments = false;
var originalAttachments = CoreHelpers.CloneObject(attachments);
2017-07-10 14:30:12 -04:00
try
{
if (cipher.Id == default(Guid))
2017-07-10 16:21:18 -04:00
{
throw new BadRequestException(nameof(cipher.Id));
}
if (cipher.OrganizationId.HasValue)
2017-07-10 16:21:18 -04:00
{
throw new BadRequestException("Already belongs to an organization.");
}
if (!cipher.UserId.HasValue || cipher.UserId.Value != sharingUserId)
2017-07-10 16:21:18 -04:00
{
throw new NotFoundException();
}
var org = await _organizationRepository.GetByIdAsync(organizationId);
if (hasAttachments && !org.MaxStorageGb.HasValue)
2017-07-10 16:21:18 -04:00
{
throw new BadRequestException("This organization cannot use attachments.");
}
2017-07-10 22:08:52 -04:00
var storageAdjustment = attachments?.Sum(a => a.Value.Size) ?? 0;
if (org.StorageBytesRemaining() < storageAdjustment)
2017-07-10 16:21:18 -04:00
{
throw new BadRequestException("Not enough storage available for this organization.");
}
Validate cipher updates with revision date (#994) * Add last updated validation to cipher replacements * Add AutoFixture scaffolding. AutoDataAttributes and ICustomizations are meant to automatically produce valid test input. Examples are the Cipher customizations, which enforce the model's mutual exclusivity of UserId and OrganizationId. FixtureExtensions create a fluent way to generate SUTs. We currently use parameter injection to fascilitate service testing, which is nicely handled by AutoNSubstitute. However, in order to gain access to the substitutions, we need to Freeze them onto the Fixture. The For fluent method allows specifying a Freeze to a specific type's constructor and optionally to a parameter name in that constructor. * Unit tests for single Cipher update version checks * Fix test runner Test runner requires Microsoft.NET.Test.Sdk * Move to provider model for SUT generation This model differs from previous in that you no longer need to specify which dependencies you would like access to. Instead, all are remembered and can be queried through the sutProvider. * User cipher provided by Put method reads Every put method already reads all relevant ciphers from database, there's no need to re-read them. JSON serialization of datetimes seems to leave truncate at second precision. Verify last known date time is within one second rather than exact. * validate revision date for share many requests * Update build script to use Github environment path Co-authored-by: Matt Gibson <mdgibson@Matts-MBP.lan>
2020-11-23 08:48:05 -06:00
ValidateCipherLastKnownRevisionDateAsync(cipher, lastKnownRevisionDate);
2017-07-10 14:30:12 -04:00
// Sproc will not save this UserId on the cipher. It is used limit scope of the collectionIds.
cipher.UserId = sharingUserId;
cipher.OrganizationId = organizationId;
cipher.RevisionDate = DateTime.UtcNow;
if (!await _cipherRepository.ReplaceAsync(cipher, collectionIds))
2017-08-30 18:18:39 -04:00
{
throw new BadRequestException("Unable to save.");
}
2017-07-10 16:21:18 -04:00
updatedCipher = true;
2017-12-01 14:06:16 -05:00
await _eventService.LogCipherEventAsync(cipher, Enums.EventType.Cipher_Shared);
2017-07-10 14:30:12 -04:00
if (hasOldAttachments)
2017-07-10 14:30:12 -04:00
{
2018-11-15 12:52:31 -05:00
// migrate old attachments
foreach (var attachment in attachments.Where(a => a.Key == null))
2017-07-10 14:30:12 -04:00
{
await _attachmentStorageService.StartShareAttachmentAsync(cipher.Id, organizationId,
attachment.Value);
2017-07-10 16:21:18 -04:00
migratedAttachments = true;
2017-07-10 14:30:12 -04:00
}
2018-11-15 12:52:31 -05:00
// commit attachment migration
await _attachmentStorageService.CleanupAsync(cipher.Id);
2017-07-10 14:30:12 -04:00
}
2018-11-15 12:52:31 -05:00
// push
await _pushService.PushSyncCipherUpdateAsync(cipher, collectionIds);
2017-07-10 14:30:12 -04:00
}
catch
{
// roll everything back
if (updatedCipher)
2017-07-10 16:21:18 -04:00
{
await _cipherRepository.ReplaceAsync(originalCipher);
}
if (!hasOldAttachments || !migratedAttachments)
2017-07-10 14:30:12 -04:00
{
throw;
}
if (updatedCipher)
2017-07-10 16:21:18 -04:00
{
2017-07-10 22:08:52 -04:00
await _userRepository.UpdateStorageAsync(sharingUserId);
await _organizationRepository.UpdateStorageAsync(organizationId);
2017-07-10 16:21:18 -04:00
}
foreach (var attachment in attachments.Where(a => a.Key == null))
2017-07-10 14:30:12 -04:00
{
await _attachmentStorageService.RollbackShareAttachmentAsync(cipher.Id, organizationId,
attachment.Value, originalAttachments[attachment.Key].ContainerName);
2017-07-10 14:30:12 -04:00
}
2017-07-10 20:48:06 -04:00
await _attachmentStorageService.CleanupAsync(cipher.Id);
2017-07-10 14:30:12 -04:00
throw;
}
2017-04-12 12:42:00 -04:00
}
Validate cipher updates with revision date (#994) * Add last updated validation to cipher replacements * Add AutoFixture scaffolding. AutoDataAttributes and ICustomizations are meant to automatically produce valid test input. Examples are the Cipher customizations, which enforce the model's mutual exclusivity of UserId and OrganizationId. FixtureExtensions create a fluent way to generate SUTs. We currently use parameter injection to fascilitate service testing, which is nicely handled by AutoNSubstitute. However, in order to gain access to the substitutions, we need to Freeze them onto the Fixture. The For fluent method allows specifying a Freeze to a specific type's constructor and optionally to a parameter name in that constructor. * Unit tests for single Cipher update version checks * Fix test runner Test runner requires Microsoft.NET.Test.Sdk * Move to provider model for SUT generation This model differs from previous in that you no longer need to specify which dependencies you would like access to. Instead, all are remembered and can be queried through the sutProvider. * User cipher provided by Put method reads Every put method already reads all relevant ciphers from database, there's no need to re-read them. JSON serialization of datetimes seems to leave truncate at second precision. Verify last known date time is within one second rather than exact. * validate revision date for share many requests * Update build script to use Github environment path Co-authored-by: Matt Gibson <mdgibson@Matts-MBP.lan>
2020-11-23 08:48:05 -06:00
public async Task ShareManyAsync(IEnumerable<(Cipher cipher, DateTime? lastKnownRevisionDate)> cipherInfos,
Guid organizationId, IEnumerable<Guid> collectionIds, Guid sharingUserId)
2018-06-13 14:03:44 -04:00
{
var cipherIds = new List<Guid>();
Validate cipher updates with revision date (#994) * Add last updated validation to cipher replacements * Add AutoFixture scaffolding. AutoDataAttributes and ICustomizations are meant to automatically produce valid test input. Examples are the Cipher customizations, which enforce the model's mutual exclusivity of UserId and OrganizationId. FixtureExtensions create a fluent way to generate SUTs. We currently use parameter injection to fascilitate service testing, which is nicely handled by AutoNSubstitute. However, in order to gain access to the substitutions, we need to Freeze them onto the Fixture. The For fluent method allows specifying a Freeze to a specific type's constructor and optionally to a parameter name in that constructor. * Unit tests for single Cipher update version checks * Fix test runner Test runner requires Microsoft.NET.Test.Sdk * Move to provider model for SUT generation This model differs from previous in that you no longer need to specify which dependencies you would like access to. Instead, all are remembered and can be queried through the sutProvider. * User cipher provided by Put method reads Every put method already reads all relevant ciphers from database, there's no need to re-read them. JSON serialization of datetimes seems to leave truncate at second precision. Verify last known date time is within one second rather than exact. * validate revision date for share many requests * Update build script to use Github environment path Co-authored-by: Matt Gibson <mdgibson@Matts-MBP.lan>
2020-11-23 08:48:05 -06:00
foreach (var (cipher, lastKnownRevisionDate) in cipherInfos)
2018-06-13 14:03:44 -04:00
{
if (cipher.Id == default(Guid))
2018-06-13 14:03:44 -04:00
{
throw new BadRequestException("All ciphers must already exist.");
}
if (cipher.OrganizationId.HasValue)
2018-06-13 14:03:44 -04:00
{
throw new BadRequestException("One or more ciphers already belong to an organization.");
}
if (!cipher.UserId.HasValue || cipher.UserId.Value != sharingUserId)
2018-06-13 14:03:44 -04:00
{
throw new BadRequestException("One or more ciphers do not belong to you.");
}
Validate cipher updates with revision date (#994) * Add last updated validation to cipher replacements * Add AutoFixture scaffolding. AutoDataAttributes and ICustomizations are meant to automatically produce valid test input. Examples are the Cipher customizations, which enforce the model's mutual exclusivity of UserId and OrganizationId. FixtureExtensions create a fluent way to generate SUTs. We currently use parameter injection to fascilitate service testing, which is nicely handled by AutoNSubstitute. However, in order to gain access to the substitutions, we need to Freeze them onto the Fixture. The For fluent method allows specifying a Freeze to a specific type's constructor and optionally to a parameter name in that constructor. * Unit tests for single Cipher update version checks * Fix test runner Test runner requires Microsoft.NET.Test.Sdk * Move to provider model for SUT generation This model differs from previous in that you no longer need to specify which dependencies you would like access to. Instead, all are remembered and can be queried through the sutProvider. * User cipher provided by Put method reads Every put method already reads all relevant ciphers from database, there's no need to re-read them. JSON serialization of datetimes seems to leave truncate at second precision. Verify last known date time is within one second rather than exact. * validate revision date for share many requests * Update build script to use Github environment path Co-authored-by: Matt Gibson <mdgibson@Matts-MBP.lan>
2020-11-23 08:48:05 -06:00
ValidateCipherLastKnownRevisionDateAsync(cipher, lastKnownRevisionDate);
2018-06-13 14:03:44 -04:00
cipher.UserId = null;
cipher.OrganizationId = organizationId;
cipher.RevisionDate = DateTime.UtcNow;
cipherIds.Add(cipher.Id);
}
Validate cipher updates with revision date (#994) * Add last updated validation to cipher replacements * Add AutoFixture scaffolding. AutoDataAttributes and ICustomizations are meant to automatically produce valid test input. Examples are the Cipher customizations, which enforce the model's mutual exclusivity of UserId and OrganizationId. FixtureExtensions create a fluent way to generate SUTs. We currently use parameter injection to fascilitate service testing, which is nicely handled by AutoNSubstitute. However, in order to gain access to the substitutions, we need to Freeze them onto the Fixture. The For fluent method allows specifying a Freeze to a specific type's constructor and optionally to a parameter name in that constructor. * Unit tests for single Cipher update version checks * Fix test runner Test runner requires Microsoft.NET.Test.Sdk * Move to provider model for SUT generation This model differs from previous in that you no longer need to specify which dependencies you would like access to. Instead, all are remembered and can be queried through the sutProvider. * User cipher provided by Put method reads Every put method already reads all relevant ciphers from database, there's no need to re-read them. JSON serialization of datetimes seems to leave truncate at second precision. Verify last known date time is within one second rather than exact. * validate revision date for share many requests * Update build script to use Github environment path Co-authored-by: Matt Gibson <mdgibson@Matts-MBP.lan>
2020-11-23 08:48:05 -06:00
await _cipherRepository.UpdateCiphersAsync(sharingUserId, cipherInfos.Select(c => c.cipher));
2018-06-13 14:03:44 -04:00
await _collectionCipherRepository.UpdateCollectionsForCiphersAsync(cipherIds, sharingUserId,
organizationId, collectionIds);
Validate cipher updates with revision date (#994) * Add last updated validation to cipher replacements * Add AutoFixture scaffolding. AutoDataAttributes and ICustomizations are meant to automatically produce valid test input. Examples are the Cipher customizations, which enforce the model's mutual exclusivity of UserId and OrganizationId. FixtureExtensions create a fluent way to generate SUTs. We currently use parameter injection to fascilitate service testing, which is nicely handled by AutoNSubstitute. However, in order to gain access to the substitutions, we need to Freeze them onto the Fixture. The For fluent method allows specifying a Freeze to a specific type's constructor and optionally to a parameter name in that constructor. * Unit tests for single Cipher update version checks * Fix test runner Test runner requires Microsoft.NET.Test.Sdk * Move to provider model for SUT generation This model differs from previous in that you no longer need to specify which dependencies you would like access to. Instead, all are remembered and can be queried through the sutProvider. * User cipher provided by Put method reads Every put method already reads all relevant ciphers from database, there's no need to re-read them. JSON serialization of datetimes seems to leave truncate at second precision. Verify last known date time is within one second rather than exact. * validate revision date for share many requests * Update build script to use Github environment path Co-authored-by: Matt Gibson <mdgibson@Matts-MBP.lan>
2020-11-23 08:48:05 -06:00
var events = cipherInfos.Select(c =>
new Tuple<Cipher, EventType, DateTime?>(c.cipher, EventType.Cipher_Shared, null));
foreach (var eventsBatch in events.Batch(100))
2019-07-25 15:50:13 -04:00
{
await _eventService.LogCipherEventsAsync(eventsBatch);
}
2018-06-13 14:03:44 -04:00
// push
await _pushService.PushSyncCiphersAsync(sharingUserId);
}
public async Task SaveCollectionsAsync(Cipher cipher, IEnumerable<Guid> collectionIds, Guid savingUserId,
bool orgAdmin)
2017-04-12 12:42:00 -04:00
{
if (cipher.Id == default(Guid))
2017-04-12 12:42:00 -04:00
{
throw new BadRequestException(nameof(cipher.Id));
}
if (!cipher.OrganizationId.HasValue)
2017-04-12 12:42:00 -04:00
{
throw new BadRequestException("Cipher must belong to an organization.");
}
cipher.RevisionDate = DateTime.UtcNow;
// The sprocs will validate that all collections belong to this org/user and that they have
// proper write permissions.
if (orgAdmin)
2017-04-12 12:42:00 -04:00
{
await _collectionCipherRepository.UpdateCollectionsForAdminAsync(cipher.Id,
cipher.OrganizationId.Value, collectionIds);
2017-04-17 23:12:48 -04:00
}
else
{
if (!(await UserCanEditAsync(cipher, savingUserId)))
{
throw new BadRequestException("You do not have permissions to edit this.");
}
2017-04-27 09:19:30 -04:00
await _collectionCipherRepository.UpdateCollectionsAsync(cipher.Id, savingUserId, collectionIds);
2017-04-12 12:42:00 -04:00
}
2017-12-01 14:06:16 -05:00
await _eventService.LogCipherEventAsync(cipher, Enums.EventType.Cipher_UpdatedCollections);
2017-04-12 12:42:00 -04:00
// push
await _pushService.PushSyncCipherUpdateAsync(cipher, collectionIds);
}
public async Task ImportCiphersAsync(
List<Folder> folders,
List<CipherDetails> ciphers,
IEnumerable<KeyValuePair<int, int>> folderRelationships)
{
var userId = folders.FirstOrDefault()?.UserId ?? ciphers.FirstOrDefault()?.UserId;
// Make sure the user can save new ciphers to their personal vault
var personalOwnershipPolicyCount = await _policyRepository.GetCountByTypeApplicableToUserIdAsync(userId.Value,
PolicyType.PersonalOwnership);
if (personalOwnershipPolicyCount > 0)
{
throw new BadRequestException("You cannot import items into your personal vault because you are " +
"a member of an organization which forbids it.");
}
foreach (var cipher in ciphers)
{
cipher.SetNewId();
if (cipher.UserId.HasValue && cipher.Favorite)
{
cipher.Favorites = $"{{\"{cipher.UserId.ToString().ToUpperInvariant()}\":\"true\"}}";
}
}
// Init. ids for folders
foreach (var folder in folders)
{
folder.SetNewId();
}
// Create the folder associations based on the newly created folder ids
foreach (var relationship in folderRelationships)
{
var cipher = ciphers.ElementAtOrDefault(relationship.Key);
var folder = folders.ElementAtOrDefault(relationship.Value);
if (cipher == null || folder == null)
{
continue;
}
cipher.Folders = $"{{\"{cipher.UserId.ToString().ToUpperInvariant()}\":" +
$"\"{folder.Id.ToString().ToUpperInvariant()}\"}}";
}
// Create it all
await _cipherRepository.CreateAsync(ciphers, folders);
// push
if (userId.HasValue)
{
await _pushService.PushSyncVaultAsync(userId.Value);
}
}
public async Task ImportCiphersAsync(
List<Collection> collections,
List<CipherDetails> ciphers,
IEnumerable<KeyValuePair<int, int>> collectionRelationships,
Guid importingUserId)
{
var org = collections.Count > 0 ?
await _organizationRepository.GetByIdAsync(collections[0].OrganizationId) :
await _organizationRepository.GetByIdAsync(ciphers.FirstOrDefault(c => c.OrganizationId.HasValue).OrganizationId.Value);
if (collections.Count > 0 && org != null && org.MaxCollections.HasValue)
2018-07-17 13:34:12 -04:00
{
var collectionCount = await _collectionRepository.GetCountByOrganizationIdAsync(org.Id);
if (org.MaxCollections.Value < (collectionCount + collections.Count))
2018-07-17 13:34:12 -04:00
{
throw new BadRequestException("This organization can only have a maximum of " +
$"{org.MaxCollections.Value} collections.");
2018-07-17 13:34:12 -04:00
}
}
// Init. ids for ciphers
foreach (var cipher in ciphers)
{
cipher.SetNewId();
}
// Init. ids for collections
foreach (var collection in collections)
{
collection.SetNewId();
}
// Create associations based on the newly assigned ids
var collectionCiphers = new List<CollectionCipher>();
foreach (var relationship in collectionRelationships)
{
var cipher = ciphers.ElementAtOrDefault(relationship.Key);
var collection = collections.ElementAtOrDefault(relationship.Value);
if (cipher == null || collection == null)
{
continue;
}
collectionCiphers.Add(new CollectionCipher
{
CipherId = cipher.Id,
CollectionId = collection.Id
});
}
// Create it all
await _cipherRepository.CreateAsync(ciphers, collections, collectionCiphers);
// push
await _pushService.PushSyncVaultAsync(importingUserId);
if (org != null)
{
await _referenceEventService.RaiseEventAsync(
new ReferenceEvent(ReferenceEventType.VaultImported, org));
}
}
public async Task SoftDeleteAsync(Cipher cipher, Guid deletingUserId, bool orgAdmin = false)
{
if (!orgAdmin && !(await UserCanEditAsync(cipher, deletingUserId)))
{
throw new BadRequestException("You do not have permissions to soft delete this.");
}
if (cipher.DeletedDate.HasValue)
{
// Already soft-deleted, we can safely ignore this
return;
}
cipher.DeletedDate = cipher.RevisionDate = DateTime.UtcNow;
if (cipher is CipherDetails details)
{
await _cipherRepository.UpsertAsync(details);
}
else
{
await _cipherRepository.UpsertAsync(cipher);
}
await _eventService.LogCipherEventAsync(cipher, EventType.Cipher_SoftDeleted);
// push
await _pushService.PushSyncCipherUpdateAsync(cipher, null);
}
public async Task SoftDeleteManyAsync(IEnumerable<Guid> cipherIds, Guid deletingUserId, Guid? organizationId, bool orgAdmin)
{
var cipherIdsSet = new HashSet<Guid>(cipherIds);
var deletingCiphers = new List<Cipher>();
if (orgAdmin && organizationId.HasValue)
{
var ciphers = await _cipherRepository.GetManyByOrganizationIdAsync(organizationId.Value);
deletingCiphers = ciphers.Where(c => cipherIdsSet.Contains(c.Id)).ToList();
await _cipherRepository.SoftDeleteByIdsOrganizationIdAsync(deletingCiphers.Select(c => c.Id), organizationId.Value);
}
else
{
var ciphers = await _cipherRepository.GetManyByUserIdAsync(deletingUserId);
deletingCiphers = ciphers.Where(c => cipherIdsSet.Contains(c.Id) && c.Edit).Select(x => (Cipher)x).ToList();
await _cipherRepository.SoftDeleteAsync(deletingCiphers.Select(c => c.Id), deletingUserId);
}
var events = deletingCiphers.Select(c =>
new Tuple<Cipher, EventType, DateTime?>(c, EventType.Cipher_SoftDeleted, null));
foreach (var eventsBatch in events.Batch(100))
{
await _eventService.LogCipherEventsAsync(eventsBatch);
}
// push
await _pushService.PushSyncCiphersAsync(deletingUserId);
}
public async Task RestoreAsync(Cipher cipher, Guid restoringUserId, bool orgAdmin = false)
{
if (!orgAdmin && !(await UserCanEditAsync(cipher, restoringUserId)))
{
throw new BadRequestException("You do not have permissions to delete this.");
}
if (!cipher.DeletedDate.HasValue)
{
// Already restored, we can safely ignore this
return;
}
cipher.DeletedDate = null;
cipher.RevisionDate = DateTime.UtcNow;
if (cipher is CipherDetails details)
{
await _cipherRepository.UpsertAsync(details);
}
else
{
await _cipherRepository.UpsertAsync(cipher);
}
await _eventService.LogCipherEventAsync(cipher, EventType.Cipher_Restored);
// push
await _pushService.PushSyncCipherUpdateAsync(cipher, null);
}
public async Task RestoreManyAsync(IEnumerable<CipherDetails> ciphers, Guid restoringUserId)
{
var revisionDate = await _cipherRepository.RestoreAsync(ciphers.Select(c => c.Id), restoringUserId);
var events = ciphers.Select(c =>
{
c.RevisionDate = revisionDate;
c.DeletedDate = null;
return new Tuple<Cipher, EventType, DateTime?>(c, EventType.Cipher_Restored, null);
});
foreach (var eventsBatch in events.Batch(100))
{
await _eventService.LogCipherEventsAsync(eventsBatch);
}
// push
await _pushService.PushSyncCiphersAsync(restoringUserId);
}
2017-03-25 16:25:10 -04:00
private async Task<bool> UserCanEditAsync(Cipher cipher, Guid userId)
{
if (!cipher.OrganizationId.HasValue && cipher.UserId.HasValue && cipher.UserId.Value == userId)
{
return true;
}
2017-05-06 23:23:01 -04:00
return await _cipherRepository.GetCanEditByIdAsync(userId, cipher.Id);
}
Validate cipher updates with revision date (#994) * Add last updated validation to cipher replacements * Add AutoFixture scaffolding. AutoDataAttributes and ICustomizations are meant to automatically produce valid test input. Examples are the Cipher customizations, which enforce the model's mutual exclusivity of UserId and OrganizationId. FixtureExtensions create a fluent way to generate SUTs. We currently use parameter injection to fascilitate service testing, which is nicely handled by AutoNSubstitute. However, in order to gain access to the substitutions, we need to Freeze them onto the Fixture. The For fluent method allows specifying a Freeze to a specific type's constructor and optionally to a parameter name in that constructor. * Unit tests for single Cipher update version checks * Fix test runner Test runner requires Microsoft.NET.Test.Sdk * Move to provider model for SUT generation This model differs from previous in that you no longer need to specify which dependencies you would like access to. Instead, all are remembered and can be queried through the sutProvider. * User cipher provided by Put method reads Every put method already reads all relevant ciphers from database, there's no need to re-read them. JSON serialization of datetimes seems to leave truncate at second precision. Verify last known date time is within one second rather than exact. * validate revision date for share many requests * Update build script to use Github environment path Co-authored-by: Matt Gibson <mdgibson@Matts-MBP.lan>
2020-11-23 08:48:05 -06:00
private void ValidateCipherLastKnownRevisionDateAsync(Cipher cipher, DateTime? lastKnownRevisionDate)
{
if (cipher.Id == default || !lastKnownRevisionDate.HasValue)
{
return;
}
if ((cipher.RevisionDate - lastKnownRevisionDate.Value).Duration() > TimeSpan.FromSeconds(1))
{
throw new BadRequestException(
"The cipher you are updating is out of date. Please save your work, sync your vault, and try again."
);
}
}
private async Task DeleteAttachmentAsync(Cipher cipher, CipherAttachment.MetaData attachmentData)
{
if (attachmentData == null || string.IsNullOrWhiteSpace(attachmentData.AttachmentId))
{
return;
}
await _cipherRepository.DeleteAttachmentAsync(cipher.Id, attachmentData.AttachmentId);
cipher.DeleteAttachment(attachmentData.AttachmentId);
await _attachmentStorageService.DeleteAttachmentAsync(cipher.Id, attachmentData);
await _eventService.LogCipherEventAsync(cipher, Enums.EventType.Cipher_AttachmentDeleted);
// push
await _pushService.PushSyncCipherUpdateAsync(cipher, null);
}
private async Task ValidateCipherEditForAttachmentAsync(Cipher cipher, Guid savingUserId, bool orgAdmin,
long requestLength)
{
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 to attach.");
}
var storageBytesRemaining = await StorageBytesRemainingForCipherAsync(cipher);
if (storageBytesRemaining < requestLength)
{
throw new BadRequestException("Not enough storage available.");
}
}
private async Task<long> StorageBytesRemainingForCipherAsync(Cipher cipher)
{
var storageBytesRemaining = 0L;
if (cipher.UserId.HasValue)
{
var user = await _userRepository.GetByIdAsync(cipher.UserId.Value);
if (!(await _userService.CanAccessPremium(user)))
{
throw new BadRequestException("You must have premium status to use attachments.");
}
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 (cipher.OrganizationId.HasValue)
{
var org = await _organizationRepository.GetByIdAsync(cipher.OrganizationId.Value);
if (!org.MaxStorageGb.HasValue)
{
throw new BadRequestException("This organization cannot use attachments.");
}
storageBytesRemaining = org.StorageBytesRemaining();
}
return storageBytesRemaining;
}
}
}