diff --git a/src/Api/Controllers/CiphersController.cs b/src/Api/Controllers/CiphersController.cs index 752ed628bf..65c015528c 100644 --- a/src/Api/Controllers/CiphersController.cs +++ b/src/Api/Controllers/CiphersController.cs @@ -9,8 +9,6 @@ using Bit.Core.Exceptions; using Bit.Core.Services; using Bit.Core; using Bit.Api.Utilities; -using Bit.Core.Models.Data; -using Newtonsoft.Json; namespace Bit.Api.Controllers { @@ -240,7 +238,7 @@ namespace Bit.Api.Controllers await Request.GetFileAsync(async (stream, fileName) => { - await _cipherService.AttachAsync(cipher, stream, fileName, Request.ContentLength.GetValueOrDefault(0), userId); + await _cipherService.CreateAttachmentAsync(cipher, stream, fileName, Request.ContentLength.GetValueOrDefault(0), userId); }); } diff --git a/src/Core/Models/Data/Attachment.cs b/src/Core/Models/Data/Attachment.cs index 17c7283280..20891f273c 100644 --- a/src/Core/Models/Data/Attachment.cs +++ b/src/Core/Models/Data/Attachment.cs @@ -1,4 +1,5 @@ -using System; +using Newtonsoft.Json; +using System; namespace Bit.Core.Models.Data { @@ -12,7 +13,23 @@ namespace Bit.Core.Models.Data public class MetaData { - public long Size { get; set; } + private long _size; + + [JsonIgnore] + public long Size + { + get { return _size; } + set { _size = value; } + } + + // We serialize Size as a string since JSON (or Javascript) doesn't support full precision for long numbers + [JsonProperty("Size")] + public string SizeString + { + get { return _size.ToString(); } + set { _size = Convert.ToInt64(value); } + } + public string FileName { get; set; } } } diff --git a/src/Core/Models/Table/Organization.cs b/src/Core/Models/Table/Organization.cs index 6931bd021b..707bd1524a 100644 --- a/src/Core/Models/Table/Organization.cs +++ b/src/Core/Models/Table/Organization.cs @@ -16,6 +16,8 @@ namespace Bit.Core.Models.Table public short? MaxCollections { get; set; } public bool UseGroups { get; set; } public bool UseDirectory { get; set; } + public long? Storage { get; set; } + public short? MaxStorageGb { get; set; } public string StripeCustomerId { get; set; } public string StripeSubscriptionId { get; set; } public bool Enabled { get; set; } = true; @@ -29,5 +31,21 @@ namespace Bit.Core.Models.Table Id = CoreHelpers.GenerateComb(); } } + + public long StorageBytesRemaining() + { + if(!MaxStorageGb.HasValue) + { + return 0; + } + + var maxStorageBytes = MaxStorageGb.Value * 1073741824L; + if(!Storage.HasValue) + { + return maxStorageBytes; + } + + return maxStorageBytes - Storage.Value; + } } } diff --git a/src/Core/Models/Table/User.cs b/src/Core/Models/Table/User.cs index 40952eaaa8..fe9f1a4a11 100644 --- a/src/Core/Models/Table/User.cs +++ b/src/Core/Models/Table/User.cs @@ -27,6 +27,9 @@ namespace Bit.Core.Models.Table public string Key { get; set; } public string PublicKey { get; set; } public string PrivateKey { get; set; } + public bool Premium { get; set; } + public long? Storage { get; set; } + public short? MaxStorageGb { get; set; } public DateTime CreationDate { get; internal set; } = DateTime.UtcNow; public DateTime RevisionDate { get; internal set; } = DateTime.UtcNow; @@ -99,5 +102,21 @@ namespace Bit.Core.Models.Table return providers[provider]; } + + public long StorageBytesRemaining() + { + if(!MaxStorageGb.HasValue) + { + return 0; + } + + var maxStorageBytes = MaxStorageGb.Value * 1073741824L; + if(!Storage.HasValue) + { + return maxStorageBytes; + } + + return maxStorageBytes - Storage.Value; + } } } diff --git a/src/Core/Services/ICipherService.cs b/src/Core/Services/ICipherService.cs index 5b02af2608..c28bf4970c 100644 --- a/src/Core/Services/ICipherService.cs +++ b/src/Core/Services/ICipherService.cs @@ -11,7 +11,7 @@ namespace Bit.Core.Services { Task SaveAsync(Cipher cipher, Guid savingUserId, bool orgAdmin = false); Task SaveDetailsAsync(CipherDetails cipher, Guid savingUserId); - Task AttachAsync(Cipher cipher, Stream stream, string fileName, long requestLength, Guid savingUserId, + Task CreateAttachmentAsync(Cipher cipher, Stream stream, string fileName, long requestLength, Guid savingUserId, bool orgAdmin = false); Task DeleteAsync(Cipher cipher, Guid deletingUserId, bool orgAdmin = false); Task DeleteManyAsync(IEnumerable cipherIds, Guid deletingUserId); diff --git a/src/Core/Services/Implementations/CipherService.cs b/src/Core/Services/Implementations/CipherService.cs index bdf3a8d6c9..284ad0a532 100644 --- a/src/Core/Services/Implementations/CipherService.cs +++ b/src/Core/Services/Implementations/CipherService.cs @@ -92,7 +92,7 @@ namespace Bit.Core.Services } } - public async Task AttachAsync(Cipher cipher, Stream stream, string fileName, long requestLength, + public async Task CreateAttachmentAsync(Cipher cipher, Stream stream, string fileName, long requestLength, Guid savingUserId, bool orgAdmin = false) { if(!orgAdmin && !(await UserCanEditAsync(cipher, savingUserId))) @@ -102,30 +102,55 @@ namespace Bit.Core.Services if(requestLength < 1) { - throw new BadRequestException("No data."); + throw new BadRequestException("No data to attach."); } - // TODO: check available space against requestLength + var storageBytesRemaining = 0L; + if(cipher.UserId.HasValue) + { + var user = await _userRepository.GetByIdAsync(cipher.UserId.Value); + storageBytesRemaining = user.StorageBytesRemaining(); + } + else if(cipher.OrganizationId.HasValue) + { + var org = await _organizationRepository.GetByIdAsync(cipher.OrganizationId.Value); + storageBytesRemaining = org.StorageBytesRemaining(); + } + + if(storageBytesRemaining < requestLength) + { + throw new BadRequestException("Not enough storage available."); + } var attachmentId = Utilities.CoreHelpers.SecureRandomString(32, upper: false, special: false); - await _attachmentStorageService.UploadAttachmentAsync(stream, $"{cipher.Id}/{attachmentId}"); + var storageId = $"{cipher.Id}/{attachmentId}"; + await _attachmentStorageService.UploadAttachmentAsync(stream, storageId); - var data = new CipherAttachment.MetaData + try { - FileName = fileName, - Size = stream.Length - }; + var data = new CipherAttachment.MetaData + { + FileName = fileName, + Size = stream.Length + }; - var attachment = new CipherAttachment + var attachment = new CipherAttachment + { + Id = cipher.Id, + UserId = cipher.UserId, + OrganizationId = cipher.OrganizationId, + AttachmentId = attachmentId, + AttachmentData = JsonConvert.SerializeObject(data) + }; + + await _cipherRepository.UpdateAttachmentAsync(attachment); + } + catch { - Id = cipher.Id, - UserId = cipher.UserId, - OrganizationId = cipher.OrganizationId, - AttachmentId = attachmentId, - AttachmentData = JsonConvert.SerializeObject(data) - }; - - await _cipherRepository.UpdateAttachmentAsync(attachment); + // Clean up since this is not transactional + await _attachmentStorageService.DeleteAttachmentAsync(storageId); + throw; + } // push await _pushService.PushSyncCipherUpdateAsync(cipher); diff --git a/src/Sql/Sql.sqlproj b/src/Sql/Sql.sqlproj index c1f3e8537f..6215ef35b3 100644 --- a/src/Sql/Sql.sqlproj +++ b/src/Sql/Sql.sqlproj @@ -201,5 +201,10 @@ + + + + + \ No newline at end of file diff --git a/src/Sql/dbo/Stored Procedures/Cipher_UpdateAttachment.sql b/src/Sql/dbo/Stored Procedures/Cipher_UpdateAttachment.sql index 9def1e5175..c12748e33c 100644 --- a/src/Sql/dbo/Stored Procedures/Cipher_UpdateAttachment.sql +++ b/src/Sql/dbo/Stored Procedures/Cipher_UpdateAttachment.sql @@ -10,6 +10,7 @@ BEGIN DECLARE @AttachmentIdKey VARCHAR(50) = CONCAT('"', @AttachmentId, '"') DECLARE @AttachmentIdPath VARCHAR(50) = CONCAT('$.', @AttachmentIdKey) + DECLARE @Size BIGINT = CAST(JSON_VALUE(@AttachmentData, '$.Size') AS BIGINT) UPDATE [dbo].[Cipher] @@ -26,10 +27,12 @@ BEGIN IF @OrganizationId IS NOT NULL BEGIN + EXEC [dbo].[Organization_UpdateStorage] @OrganizationId, @Size EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationId] @OrganizationId END ELSE IF @UserId IS NOT NULL BEGIN + EXEC [dbo].[User_UpdateStorage] @UserId, @Size EXEC [dbo].[User_BumpAccountRevisionDate] @UserId END END \ No newline at end of file diff --git a/src/Sql/dbo/Stored Procedures/Organization_Create.sql b/src/Sql/dbo/Stored Procedures/Organization_Create.sql index daa94e5ffa..f2b0c8f246 100644 --- a/src/Sql/dbo/Stored Procedures/Organization_Create.sql +++ b/src/Sql/dbo/Stored Procedures/Organization_Create.sql @@ -9,6 +9,8 @@ @MaxCollections SMALLINT, @UseGroups BIT, @UseDirectory BIT, + @Storage BIGINT, + @MaxStorageGb SMALLINT, @StripeCustomerId VARCHAR(50), @StripeSubscriptionId VARCHAR(50), @Enabled BIT, @@ -30,6 +32,8 @@ BEGIN [MaxCollections], [UseGroups], [UseDirectory], + [Storage], + [MaxStorageGb], [StripeCustomerId], [StripeSubscriptionId], [Enabled], @@ -48,6 +52,8 @@ BEGIN @MaxCollections, @UseGroups, @UseDirectory, + @Storage, + @MaxStorageGb, @StripeCustomerId, @StripeSubscriptionId, @Enabled, diff --git a/src/Sql/dbo/Stored Procedures/Organization_Update.sql b/src/Sql/dbo/Stored Procedures/Organization_Update.sql index a85c8187fb..131e4ee3d1 100644 --- a/src/Sql/dbo/Stored Procedures/Organization_Update.sql +++ b/src/Sql/dbo/Stored Procedures/Organization_Update.sql @@ -9,6 +9,8 @@ @MaxCollections SMALLINT, @UseGroups BIT, @UseDirectory BIT, + @Storage BIGINT, + @MaxStorageGb SMALLINT, @StripeCustomerId VARCHAR(50), @StripeSubscriptionId VARCHAR(50), @Enabled BIT, @@ -31,6 +33,8 @@ BEGIN [MaxCollections] = @MaxCollections, [UseGroups] = @UseGroups, [UseDirectory] = @UseDirectory, + [Storage] = @Storage, + [MaxStorageGb] = @MaxStorageGb, [StripeCustomerId] = @StripeCustomerId, [StripeSubscriptionId] = @StripeSubscriptionId, [Enabled] = @Enabled, diff --git a/src/Sql/dbo/Stored Procedures/Organization_UpdateStorage.sql b/src/Sql/dbo/Stored Procedures/Organization_UpdateStorage.sql new file mode 100644 index 0000000000..675f5d7224 --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/Organization_UpdateStorage.sql @@ -0,0 +1,16 @@ +CREATE PROCEDURE [dbo].[Organization_UpdateStorage] + @Id UNIQUEIDENTIFIER, + @StorageIncrease BIGINT + +AS +BEGIN + SET NOCOUNT ON + + UPDATE + [dbo].[Organization] + SET + [Storage] = ISNULL([Storage], 0) + @StorageIncrease, + [RevisionDate] = GETUTCDATE() + WHERE + [Id] = @Id +END \ No newline at end of file diff --git a/src/Sql/dbo/Stored Procedures/User_Create.sql b/src/Sql/dbo/Stored Procedures/User_Create.sql index d13de10865..43fe33b9a1 100644 --- a/src/Sql/dbo/Stored Procedures/User_Create.sql +++ b/src/Sql/dbo/Stored Procedures/User_Create.sql @@ -15,6 +15,9 @@ @Key NVARCHAR(MAX), @PublicKey NVARCHAR(MAX), @PrivateKey NVARCHAR(MAX), + @Premium BIT, + @Storage BIGINT, + @MaxStorageGb SMALLINT, @CreationDate DATETIME2(7), @RevisionDate DATETIME2(7) AS @@ -39,6 +42,9 @@ BEGIN [Key], [PublicKey], [PrivateKey], + [Premium], + [Storage], + [MaxStorageGb], [CreationDate], [RevisionDate] ) @@ -60,6 +66,9 @@ BEGIN @Key, @PublicKey, @PrivateKey, + @Premium, + @Storage, + @MaxStorageGb, @CreationDate, @RevisionDate ) diff --git a/src/Sql/dbo/Stored Procedures/User_Update.sql b/src/Sql/dbo/Stored Procedures/User_Update.sql index 9659916717..c4c36c64d7 100644 --- a/src/Sql/dbo/Stored Procedures/User_Update.sql +++ b/src/Sql/dbo/Stored Procedures/User_Update.sql @@ -15,6 +15,9 @@ @Key NVARCHAR(MAX), @PublicKey NVARCHAR(MAX), @PrivateKey NVARCHAR(MAX), + @Premium BIT, + @Storage BIGINT, + @MaxStorageGb SMALLINT, @CreationDate DATETIME2(7), @RevisionDate DATETIME2(7) AS @@ -39,6 +42,9 @@ BEGIN [Key] = @Key, [PublicKey] = @PublicKey, [PrivateKey] = @PrivateKey, + [Premium] = @Premium, + [Storage] = @Storage, + [MaxStorageGb] = @MaxStorageGb, [CreationDate] = @CreationDate, [RevisionDate] = @RevisionDate WHERE diff --git a/src/Sql/dbo/Stored Procedures/User_UpdateStorage.sql b/src/Sql/dbo/Stored Procedures/User_UpdateStorage.sql new file mode 100644 index 0000000000..63d1b46de9 --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/User_UpdateStorage.sql @@ -0,0 +1,16 @@ +CREATE PROCEDURE [dbo].[User_UpdateStorage] + @Id UNIQUEIDENTIFIER, + @StorageIncrease BIGINT + +AS +BEGIN + SET NOCOUNT ON + + UPDATE + [dbo].[User] + SET + [Storage] = ISNULL([Storage], 0) + @StorageIncrease, + [RevisionDate] = GETUTCDATE() + WHERE + [Id] = @Id +END \ No newline at end of file diff --git a/src/Sql/dbo/Tables/Organization.sql b/src/Sql/dbo/Tables/Organization.sql index 8463f89242..088fc64f5b 100644 --- a/src/Sql/dbo/Tables/Organization.sql +++ b/src/Sql/dbo/Tables/Organization.sql @@ -9,6 +9,8 @@ [MaxCollections] SMALLINT NULL, [UseGroups] BIT NOT NULL, [UseDirectory] BIT NOT NULL, + [Storage] BIGINT NULL, + [MaxStorageGb] SMALLINT NULL, [StripeCustomerId] VARCHAR (50) NULL, [StripeSubscriptionId] VARCHAR (50) NULL, [Enabled] BIT NOT NULL, diff --git a/src/Sql/dbo/Tables/User.sql b/src/Sql/dbo/Tables/User.sql index 0ca08aa323..178adb4a89 100644 --- a/src/Sql/dbo/Tables/User.sql +++ b/src/Sql/dbo/Tables/User.sql @@ -15,6 +15,9 @@ [Key] VARCHAR (MAX) NULL, [PublicKey] VARCHAR (MAX) NULL, [PrivateKey] VARCHAR (MAX) NULL, + [Premium] BIT NOT NULL, + [Storage] BIGINT NULL, + [MaxStorageGb] SMALLINT NULL, [CreationDate] DATETIME2 (7) NOT NULL, [RevisionDate] DATETIME2 (7) NOT NULL, CONSTRAINT [PK_User] PRIMARY KEY CLUSTERED ([Id] ASC) diff --git a/util/SqlUpdate/2017-06-30_00_UserPremium.sql b/util/SqlUpdate/2017-06-30_00_UserPremium.sql new file mode 100644 index 0000000000..506e49c7d2 --- /dev/null +++ b/util/SqlUpdate/2017-06-30_00_UserPremium.sql @@ -0,0 +1,8 @@ +alter table [user] add [Premium] BIT NULL +go + +update [user] set [premium] = 0 +go + +alter table [user] alter column [premium] BIT NOT NULL +go