From 17b22ca5a99f4f0523ac01a473281aab39c43522 Mon Sep 17 00:00:00 2001 From: Robyn MacCallum Date: Wed, 2 Mar 2022 17:37:36 -0500 Subject: [PATCH] Add attachments check before moving ciphers to a free org (#1890) --- .../Services/Implementations/CipherService.cs | 91 ++++++++++--------- test/Core.Test/Services/CipherServiceTests.cs | 60 ++++++++++++ 2 files changed, 106 insertions(+), 45 deletions(-) diff --git a/src/Core/Services/Implementations/CipherService.cs b/src/Core/Services/Implementations/CipherService.cs index 4f2a78559b..485fc83783 100644 --- a/src/Core/Services/Implementations/CipherService.cs +++ b/src/Core/Services/Implementations/CipherService.cs @@ -495,7 +495,6 @@ namespace Bit.Core.Services IEnumerable collectionIds, Guid sharingUserId, DateTime? lastKnownRevisionDate) { var attachments = cipher.GetAttachments(); - var hasAttachments = attachments?.Any() ?? false; var hasOldAttachments = attachments?.Any(a => a.Key == null) ?? false; var updatedCipher = false; var migratedAttachments = false; @@ -503,34 +502,7 @@ namespace Bit.Core.Services try { - if (cipher.Id == default(Guid)) - { - throw new BadRequestException(nameof(cipher.Id)); - } - - if (cipher.OrganizationId.HasValue) - { - throw new BadRequestException("Already belongs to an organization."); - } - - if (!cipher.UserId.HasValue || cipher.UserId.Value != sharingUserId) - { - throw new NotFoundException(); - } - - var org = await _organizationRepository.GetByIdAsync(organizationId); - if (hasAttachments && !org.MaxStorageGb.HasValue) - { - throw new BadRequestException("This organization cannot use attachments."); - } - - var storageAdjustment = attachments?.Sum(a => a.Value.Size) ?? 0; - if (org.StorageBytesRemaining() < storageAdjustment) - { - throw new BadRequestException("Not enough storage available for this organization."); - } - - ValidateCipherLastKnownRevisionDateAsync(cipher, lastKnownRevisionDate); + await ValidateCipherCanBeShared(cipher, sharingUserId, organizationId, lastKnownRevisionDate); // Sproc will not save this UserId on the cipher. It is used limit scope of the collectionIds. cipher.UserId = sharingUserId; @@ -597,22 +569,7 @@ namespace Bit.Core.Services var cipherIds = new List(); foreach (var (cipher, lastKnownRevisionDate) in cipherInfos) { - if (cipher.Id == default(Guid)) - { - throw new BadRequestException("All ciphers must already exist."); - } - - if (cipher.OrganizationId.HasValue) - { - throw new BadRequestException("One or more ciphers already belong to an organization."); - } - - if (!cipher.UserId.HasValue || cipher.UserId.Value != sharingUserId) - { - throw new BadRequestException("One or more ciphers do not belong to you."); - } - - ValidateCipherLastKnownRevisionDateAsync(cipher, lastKnownRevisionDate); + await ValidateCipherCanBeShared(cipher, sharingUserId, organizationId, lastKnownRevisionDate); cipher.UserId = null; cipher.OrganizationId = organizationId; @@ -999,5 +956,49 @@ namespace Bit.Core.Services return storageBytesRemaining; } + + private async Task ValidateCipherCanBeShared( + Cipher cipher, + Guid sharingUserId, + Guid organizationId, + DateTime? lastKnownRevisionDate) + { + if (cipher.Id == default(Guid)) + { + throw new BadRequestException("Cipher must already exist."); + } + + if (cipher.OrganizationId.HasValue) + { + throw new BadRequestException("One or more ciphers already belong to an organization."); + } + + if (!cipher.UserId.HasValue || cipher.UserId.Value != sharingUserId) + { + throw new BadRequestException("One or more ciphers do not belong to you."); + } + + var attachments = cipher.GetAttachments(); + var hasAttachments = attachments?.Any() ?? false; + var org = await _organizationRepository.GetByIdAsync(organizationId); + + if (org == null) + { + throw new BadRequestException("Could not find organization."); + } + + if (hasAttachments && !org.MaxStorageGb.HasValue) + { + throw new BadRequestException("This organization cannot use attachments."); + } + + var storageAdjustment = attachments?.Sum(a => a.Value.Size) ?? 0; + if (org.StorageBytesRemaining() < storageAdjustment) + { + throw new BadRequestException("Not enough storage available for this organization."); + } + + ValidateCipherLastKnownRevisionDateAsync(cipher, lastKnownRevisionDate); + } } } diff --git a/test/Core.Test/Services/CipherServiceTests.cs b/test/Core.Test/Services/CipherServiceTests.cs index 9a3e972528..60513cec1b 100644 --- a/test/Core.Test/Services/CipherServiceTests.cs +++ b/test/Core.Test/Services/CipherServiceTests.cs @@ -55,6 +55,13 @@ namespace Bit.Core.Test.Services public async Task ShareManyAsync_WrongRevisionDate_Throws(SutProvider sutProvider, IEnumerable ciphers, Guid organizationId, List collectionIds) { + sutProvider.GetDependency().GetByIdAsync(organizationId) + .Returns(new Organization + { + PlanType = Enums.PlanType.EnterpriseAnnually, + MaxStorageGb = 100 + }); + var cipherInfos = ciphers.Select(c => (c, (DateTime?)c.RevisionDate.AddDays(-1))); var exception = await Assert.ThrowsAsync( @@ -108,6 +115,13 @@ namespace Bit.Core.Test.Services public async Task ShareManyAsync_CorrectRevisionDate_Passes(string revisionDateString, SutProvider sutProvider, IEnumerable ciphers, Organization organization, List collectionIds) { + sutProvider.GetDependency().GetByIdAsync(organization.Id) + .Returns(new Organization + { + PlanType = Enums.PlanType.EnterpriseAnnually, + MaxStorageGb = 100 + }); + var cipherInfos = ciphers.Select(c => (c, string.IsNullOrEmpty(revisionDateString) ? null : (DateTime?)c.RevisionDate)); var sharingUserId = ciphers.First().UserId.Value; @@ -157,5 +171,51 @@ namespace Bit.Core.Test.Services Assert.Equal(revisionDate, cipher.RevisionDate); } } + + [Theory] + [InlineUserCipherAutoData] + public async Task ShareManyAsync_FreeOrgWithAttachment_Throws(SutProvider sutProvider, + IEnumerable ciphers, Guid organizationId, List collectionIds) + { + sutProvider.GetDependency().GetByIdAsync(organizationId).Returns(new Organization + { + PlanType = Enums.PlanType.Free + }); + ciphers.FirstOrDefault().Attachments = + "{\"attachment1\":{\"Size\":\"250\",\"FileName\":\"superCoolFile\"," + + "\"Key\":\"superCoolFile\",\"ContainerName\":\"testContainer\",\"Validated\":false}}"; + + var cipherInfos = ciphers.Select(c => (c, + (DateTime?)c.RevisionDate)); + var sharingUserId = ciphers.First().UserId.Value; + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.ShareManyAsync(cipherInfos, organizationId, collectionIds, sharingUserId)); + Assert.Contains("This organization cannot use attachments", exception.Message); + } + + [Theory] + [InlineUserCipherAutoData] + public async Task ShareManyAsync_PaidOrgWithAttachment_Passes(SutProvider sutProvider, + IEnumerable ciphers, Guid organizationId, List collectionIds) + { + sutProvider.GetDependency().GetByIdAsync(organizationId) + .Returns(new Organization + { + PlanType = Enums.PlanType.EnterpriseAnnually, + MaxStorageGb = 100 + }); + ciphers.FirstOrDefault().Attachments = + "{\"attachment1\":{\"Size\":\"250\",\"FileName\":\"superCoolFile\"," + + "\"Key\":\"superCoolFile\",\"ContainerName\":\"testContainer\",\"Validated\":false}}"; + + var cipherInfos = ciphers.Select(c => (c, + (DateTime?)c.RevisionDate)); + var sharingUserId = ciphers.First().UserId.Value; + + await sutProvider.Sut.ShareManyAsync(cipherInfos, organizationId, collectionIds, sharingUserId); + await sutProvider.GetDependency().Received(1).UpdateCiphersAsync(sharingUserId, + Arg.Is>(arg => arg.Except(ciphers).IsNullOrEmpty())); + } } }