From c6ef3dc283b93531a99f9c18c7f9fa8f0dd4c62a Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 12 Apr 2017 12:42:00 -0400 Subject: [PATCH] update cipher subvaults --- src/Api/Controllers/CiphersController.cs | 32 +++++++++++--- .../Models/Api/Request/CipherRequestModel.cs | 23 +++++++++- .../Repositories/ISubvaultCipherRepository.cs | 1 + .../SqlServer/SubvaultCipherRepository.cs | 16 ++++++- src/Core/Services/ICipherService.cs | 3 +- .../Services/Implementations/CipherService.cs | 38 +++++++++++++++-- src/Sql/Sql.sqlproj | 1 + .../Cipher_UpdateWithSubvaults.sql | 2 +- .../SubvaultCipher_UpdateSubvaults.sql | 42 +++++++++++++++++++ 9 files changed, 144 insertions(+), 14 deletions(-) create mode 100644 src/Sql/dbo/Stored Procedures/SubvaultCipher_UpdateSubvaults.sql diff --git a/src/Api/Controllers/CiphersController.cs b/src/Api/Controllers/CiphersController.cs index c3c23f070..99037c840 100644 --- a/src/Api/Controllers/CiphersController.cs +++ b/src/Api/Controllers/CiphersController.cs @@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Authorization; using Bit.Core.Models.Api; using Bit.Core.Exceptions; using Bit.Core.Services; +using Bit.Core; namespace Bit.Api.Controllers { @@ -18,17 +19,20 @@ namespace Bit.Api.Controllers private readonly ISubvaultCipherRepository _subvaultCipherRepository; private readonly ICipherService _cipherService; private readonly IUserService _userService; + private readonly CurrentContext _currentContext; public CiphersController( ICipherRepository cipherRepository, ISubvaultCipherRepository subvaultCipherRepository, ICipherService cipherService, - IUserService userService) + IUserService userService, + CurrentContext currentContext) { _cipherRepository = cipherRepository; _subvaultCipherRepository = subvaultCipherRepository; _cipherService = cipherService; _userService = userService; + _currentContext = currentContext; } [HttpGet("{id}")] @@ -128,21 +132,37 @@ namespace Bit.Api.Controllers await _cipherService.UpdatePartialAsync(new Guid(id), userId, folderId, model.Favorite); } - [HttpPut("{id}/move")] - [HttpPost("{id}/move")] - public async Task PostMove(string id, [FromBody]CipherMoveRequestModel model) + [HttpPut("{id}/share")] + [HttpPost("{id}/share")] + public async Task PutShare(string id, [FromBody]CipherShareRequestModel model) { var userId = _userService.GetProperUserId(User).Value; var cipher = await _cipherRepository.GetByIdAsync(new Guid(id), userId); - if(cipher == null || cipher.UserId != userId) + if(cipher == null || cipher.UserId != userId || + !_currentContext.OrganizationUser(new Guid(model.Cipher.OrganizationId))) { throw new NotFoundException(); } - await _cipherService.MoveSubvaultAsync(model.Cipher.ToCipher(cipher), new Guid(model.Cipher.OrganizationId), + await _cipherService.ShareAsync(model.Cipher.ToCipher(cipher), new Guid(model.Cipher.OrganizationId), model.SubvaultIds.Select(s => new Guid(s)), userId); } + [HttpPut("{id}/subvaults")] + [HttpPost("{id}/subvaults")] + public async Task PutSubvaults(string id, [FromBody]CipherSubvaultsRequestModel model) + { + var userId = _userService.GetProperUserId(User).Value; + var cipher = await _cipherRepository.GetByIdAsync(new Guid(id), userId); + if(cipher == null || !cipher.OrganizationId.HasValue || + !_currentContext.OrganizationUser(cipher.OrganizationId.Value)) + { + throw new NotFoundException(); + } + + await _cipherService.SaveSubvaultsAsync(cipher, model.SubvaultIds.Select(s => new Guid(s)), userId); + } + [HttpDelete("{id}")] [HttpPost("{id}/delete")] public async Task Delete(string id) diff --git a/src/Core/Models/Api/Request/CipherRequestModel.cs b/src/Core/Models/Api/Request/CipherRequestModel.cs index da9fd88fc..e3008b55f 100644 --- a/src/Core/Models/Api/Request/CipherRequestModel.cs +++ b/src/Core/Models/Api/Request/CipherRequestModel.cs @@ -63,13 +63,34 @@ namespace Bit.Core.Models.Api } } - public class CipherMoveRequestModel : IValidatableObject + public class CipherShareRequestModel : IValidatableObject { [Required] public IEnumerable SubvaultIds { get; set; } [Required] public CipherRequestModel Cipher { get; set; } + public IEnumerable Validate(ValidationContext validationContext) + { + if(string.IsNullOrWhiteSpace(Cipher.OrganizationId)) + { + yield return new ValidationResult("Cipher OrganizationId is required.", + new string[] { nameof(Cipher.OrganizationId) }); + } + + if(!SubvaultIds?.Any() ?? false) + { + yield return new ValidationResult("You must select at least one subvault.", + new string[] { nameof(SubvaultIds) }); + } + } + } + + public class CipherSubvaultsRequestModel : IValidatableObject + { + [Required] + public IEnumerable SubvaultIds { get; set; } + public IEnumerable Validate(ValidationContext validationContext) { if(!SubvaultIds?.Any() ?? false) diff --git a/src/Core/Repositories/ISubvaultCipherRepository.cs b/src/Core/Repositories/ISubvaultCipherRepository.cs index 4b6efefb1..d163b5f46 100644 --- a/src/Core/Repositories/ISubvaultCipherRepository.cs +++ b/src/Core/Repositories/ISubvaultCipherRepository.cs @@ -9,5 +9,6 @@ namespace Bit.Core.Repositories { Task> GetManyByUserIdAsync(Guid userId); Task> GetManyByUserIdCipherIdAsync(Guid userId, Guid cipherId); + Task UpdateSubvaultsAsync(Guid cipherId, Guid userId, IEnumerable subvaultIds); } } diff --git a/src/Core/Repositories/SqlServer/SubvaultCipherRepository.cs b/src/Core/Repositories/SqlServer/SubvaultCipherRepository.cs index 542167c9f..633051143 100644 --- a/src/Core/Repositories/SqlServer/SubvaultCipherRepository.cs +++ b/src/Core/Repositories/SqlServer/SubvaultCipherRepository.cs @@ -6,6 +6,7 @@ using System.Data.SqlClient; using System.Data; using Dapper; using System.Linq; +using Bit.Core.Utilities; namespace Bit.Core.Repositories.SqlServer { @@ -24,7 +25,7 @@ namespace Bit.Core.Repositories.SqlServer using(var connection = new SqlConnection(ConnectionString)) { var results = await connection.QueryAsync( - $"[dbo].[SubvaultCipher_ReadByUserId]", + "[dbo].[SubvaultCipher_ReadByUserId]", new { UserId = userId }, commandType: CommandType.StoredProcedure); @@ -37,12 +38,23 @@ namespace Bit.Core.Repositories.SqlServer using(var connection = new SqlConnection(ConnectionString)) { var results = await connection.QueryAsync( - $"[dbo].[SubvaultCipher_ReadByUserIdCipherId]", + "[dbo].[SubvaultCipher_ReadByUserIdCipherId]", new { UserId = userId, CipherId = cipherId }, commandType: CommandType.StoredProcedure); return results.ToList(); } } + + public async Task UpdateSubvaultsAsync(Guid cipherId, Guid userId, IEnumerable subvaultIds) + { + using(var connection = new SqlConnection(ConnectionString)) + { + var results = await connection.ExecuteAsync( + "[dbo].[SubvaultCipher_UpdateSubvaults]", + new { CipherId = cipherId, UserId = userId, SubvaultIds = subvaultIds.ToGuidIdArrayTVP() }, + commandType: CommandType.StoredProcedure); + } + } } } diff --git a/src/Core/Services/ICipherService.cs b/src/Core/Services/ICipherService.cs index ed2dc3fda..8acc6f47b 100644 --- a/src/Core/Services/ICipherService.cs +++ b/src/Core/Services/ICipherService.cs @@ -13,7 +13,8 @@ namespace Bit.Core.Services Task DeleteAsync(CipherDetails cipher, Guid deletingUserId); Task SaveFolderAsync(Folder folder); Task DeleteFolderAsync(Folder folder); - Task MoveSubvaultAsync(Cipher cipher, Guid organizationId, IEnumerable subvaultIds, Guid userId); + Task ShareAsync(Cipher cipher, Guid organizationId, IEnumerable subvaultIds, Guid userId); + Task SaveSubvaultsAsync(Cipher cipher, IEnumerable subvaultIds, Guid savingUserId); Task ImportCiphersAsync(List folders, List ciphers, IEnumerable> folderRelationships); } diff --git a/src/Core/Services/Implementations/CipherService.cs b/src/Core/Services/Implementations/CipherService.cs index 82f8ea0b9..dda18c4f4 100644 --- a/src/Core/Services/Implementations/CipherService.cs +++ b/src/Core/Services/Implementations/CipherService.cs @@ -17,6 +17,7 @@ namespace Bit.Core.Services private readonly IOrganizationRepository _organizationRepository; private readonly IOrganizationUserRepository _organizationUserRepository; private readonly ISubvaultUserRepository _subvaultUserRepository; + private readonly ISubvaultCipherRepository _subvaultCipherRepository; private readonly IPushService _pushService; public CipherService( @@ -26,6 +27,7 @@ namespace Bit.Core.Services IOrganizationRepository organizationRepository, IOrganizationUserRepository organizationUserRepository, ISubvaultUserRepository subvaultUserRepository, + ISubvaultCipherRepository subvaultCipherRepository, IPushService pushService) { _cipherRepository = cipherRepository; @@ -34,6 +36,7 @@ namespace Bit.Core.Services _organizationRepository = organizationRepository; _organizationUserRepository = organizationUserRepository; _subvaultUserRepository = subvaultUserRepository; + _subvaultCipherRepository = subvaultCipherRepository; _pushService = pushService; } @@ -112,7 +115,7 @@ namespace Bit.Core.Services //await _pushService.PushSyncCipherDeleteAsync(cipher); } - public async Task MoveSubvaultAsync(Cipher cipher, Guid organizationId, IEnumerable subvaultIds, Guid movingUserId) + public async Task ShareAsync(Cipher cipher, Guid organizationId, IEnumerable subvaultIds, Guid sharingUserId) { if(cipher.Id == default(Guid)) { @@ -124,14 +127,14 @@ namespace Bit.Core.Services throw new BadRequestException("Already belongs to an organization."); } - if(!cipher.UserId.HasValue || cipher.UserId.Value != movingUserId) + if(!cipher.UserId.HasValue || cipher.UserId.Value != sharingUserId) { throw new NotFoundException(); } // We do not need to check if the user belongs to this organization since this call will return no subvaults // and therefore be caught by the .Any() check below. - var subvaultUserDetails = await _subvaultUserRepository.GetPermissionsByUserIdAsync(movingUserId, subvaultIds, + var subvaultUserDetails = await _subvaultUserRepository.GetPermissionsByUserIdAsync(sharingUserId, subvaultIds, organizationId); var writeableSubvaults = subvaultUserDetails.Where(s => !s.ReadOnly).Select(s => s.SubvaultId); @@ -149,6 +152,35 @@ namespace Bit.Core.Services //await _pushService.PushSyncCipherUpdateAsync(cipher); } + public async Task SaveSubvaultsAsync(Cipher cipher, IEnumerable subvaultIds, Guid savingUserId) + { + if(cipher.Id == default(Guid)) + { + throw new BadRequestException(nameof(cipher.Id)); + } + + if(!cipher.OrganizationId.HasValue) + { + throw new BadRequestException("Cipher must belong to an organization."); + } + + // We do not need to check if the user belongs to this organization since this call will return no subvaults + // and therefore be caught by the .Any() check below. + var subvaultUserDetails = await _subvaultUserRepository.GetPermissionsByUserIdAsync(savingUserId, subvaultIds, + cipher.OrganizationId.Value); + + var writeableSubvaults = subvaultUserDetails.Where(s => !s.ReadOnly).Select(s => s.SubvaultId); + if(!writeableSubvaults.Any()) + { + throw new BadRequestException("No subvaults."); + } + + await _subvaultCipherRepository.UpdateSubvaultsAsync(cipher.Id, savingUserId, writeableSubvaults); + + // push + //await _pushService.PushSyncCipherUpdateAsync(cipher); + } + public async Task ImportCiphersAsync( List folders, List ciphers, diff --git a/src/Sql/Sql.sqlproj b/src/Sql/Sql.sqlproj index a85a33835..fb2afa972 100644 --- a/src/Sql/Sql.sqlproj +++ b/src/Sql/Sql.sqlproj @@ -184,5 +184,6 @@ + \ No newline at end of file diff --git a/src/Sql/dbo/Stored Procedures/Cipher_UpdateWithSubvaults.sql b/src/Sql/dbo/Stored Procedures/Cipher_UpdateWithSubvaults.sql index 811c9239a..6662e5396 100644 --- a/src/Sql/dbo/Stored Procedures/Cipher_UpdateWithSubvaults.sql +++ b/src/Sql/dbo/Stored Procedures/Cipher_UpdateWithSubvaults.sql @@ -14,7 +14,7 @@ BEGIN UPDATE [dbo].[Cipher] SET - [UserId] = @UserId, + [UserId] = NULL, [OrganizationId] = @OrganizationId, [Type] = @Type, [Data] = @Data, diff --git a/src/Sql/dbo/Stored Procedures/SubvaultCipher_UpdateSubvaults.sql b/src/Sql/dbo/Stored Procedures/SubvaultCipher_UpdateSubvaults.sql new file mode 100644 index 000000000..86f722195 --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/SubvaultCipher_UpdateSubvaults.sql @@ -0,0 +1,42 @@ +CREATE PROCEDURE [dbo].[SubvaultCipher_UpdateSubvaults] + @CipherId UNIQUEIDENTIFIER, + @UserId UNIQUEIDENTIFIER, + @SubvaultIds AS [dbo].[GuidIdArray] READONLY +AS +BEGIN + SET NOCOUNT ON + + ;WITH [AvailableSubvaultsCTE] AS( + SELECT + SU.SubvaultId + FROM + [dbo].[SubvaultUser] SU + INNER JOIN + [dbo].[OrganizationUser] OU ON OU.[Id] = SU.[OrganizationUserId] + INNER JOIN + [dbo].[Organization] O ON O.[Id] = OU.[OrganizationId] + WHERE + OU.[UserId] = @UserId + AND SU.[ReadOnly] = 0 + AND OU.[Status] = 2 -- Confirmed + AND O.[Enabled] = 1 + ) + MERGE + [dbo].[SubvaultCipher] AS [Target] + USING + @SubvaultIds AS [Source] + ON + [Target].[SubvaultId] = [Source].[Id] + AND [Target].[CipherId] = @CipherId + WHEN NOT MATCHED BY TARGET THEN + INSERT VALUES + ( + [Source].[Id], + @CipherId + ) + WHEN NOT MATCHED BY SOURCE + AND [Target].[CipherId] = @CipherId + AND [Target].[SubvaultId] IN (SELECT [SubvaultId] FROM [AvailableSubvaultsCTE]) THEN + DELETE + ; +END \ No newline at end of file