From f7aa6fadbf63c3ffa0a23997ddc9590a2de28ab9 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 17 Apr 2017 23:12:48 -0400 Subject: [PATCH] admin subvault updates for cipher --- src/Api/Controllers/CiphersController.cs | 17 ++++++- .../Models/Api/Request/CipherRequestModel.cs | 11 +---- .../Models/Data/SubvaultUserPermissions.cs | 10 ----- .../Repositories/ISubvaultCipherRepository.cs | 1 + .../Repositories/ISubvaultUserRepository.cs | 2 - .../SqlServer/SubvaultCipherRepository.cs | 11 +++++ .../SqlServer/SubvaultUserRepository.cs | 15 ------- src/Core/Services/ICipherService.cs | 2 +- .../Services/Implementations/CipherService.cs | 36 +++++---------- src/Sql/Sql.sqlproj | 2 +- .../Cipher_UpdateWithSubvaults.sql | 45 ++++++++++++------- .../SubvaultCipher_UpdateSubvaultsAdmin.sql | 35 +++++++++++++++ ...ltUser_ReadPermissionsBySubvaultUserId.sql | 21 --------- 13 files changed, 106 insertions(+), 102 deletions(-) delete mode 100644 src/Core/Models/Data/SubvaultUserPermissions.cs create mode 100644 src/Sql/dbo/Stored Procedures/SubvaultCipher_UpdateSubvaultsAdmin.sql delete mode 100644 src/Sql/dbo/Stored Procedures/SubvaultUser_ReadPermissionsBySubvaultUserId.sql diff --git a/src/Api/Controllers/CiphersController.cs b/src/Api/Controllers/CiphersController.cs index 8af56710c..06df5eba4 100644 --- a/src/Api/Controllers/CiphersController.cs +++ b/src/Api/Controllers/CiphersController.cs @@ -176,7 +176,22 @@ namespace Bit.Api.Controllers throw new NotFoundException(); } - await _cipherService.SaveSubvaultsAsync(cipher, model.SubvaultIds.Select(s => new Guid(s)), userId); + await _cipherService.SaveSubvaultsAsync(cipher, model.SubvaultIds.Select(s => new Guid(s)), userId, false); + } + + [HttpPut("{id}/subvaults-admin")] + [HttpPost("{id}/subvaults-admin")] + public async Task PutSubvaultsAdmin(string id, [FromBody]CipherSubvaultsRequestModel model) + { + var userId = _userService.GetProperUserId(User).Value; + var cipher = await _cipherRepository.GetByIdAsync(new Guid(id)); + if(cipher == null || !cipher.OrganizationId.HasValue || + !_currentContext.OrganizationAdmin(cipher.OrganizationId.Value)) + { + throw new NotFoundException(); + } + + await _cipherService.SaveSubvaultsAsync(cipher, model.SubvaultIds.Select(s => new Guid(s)), userId, true); } [HttpDelete("{id}")] diff --git a/src/Core/Models/Api/Request/CipherRequestModel.cs b/src/Core/Models/Api/Request/CipherRequestModel.cs index a6bad7e17..5fb2512b7 100644 --- a/src/Core/Models/Api/Request/CipherRequestModel.cs +++ b/src/Core/Models/Api/Request/CipherRequestModel.cs @@ -84,18 +84,9 @@ namespace Bit.Core.Models.Api } } - public class CipherSubvaultsRequestModel : IValidatableObject + public class CipherSubvaultsRequestModel { [Required] public IEnumerable SubvaultIds { get; set; } - - public IEnumerable Validate(ValidationContext validationContext) - { - if(!SubvaultIds?.Any() ?? false) - { - yield return new ValidationResult("You must select at least one subvault.", - new string[] { nameof(SubvaultIds) }); - } - } } } diff --git a/src/Core/Models/Data/SubvaultUserPermissions.cs b/src/Core/Models/Data/SubvaultUserPermissions.cs deleted file mode 100644 index 93edf4b3d..000000000 --- a/src/Core/Models/Data/SubvaultUserPermissions.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System; - -namespace Bit.Core.Models.Data -{ - public class SubvaultUserPermissions - { - public Guid SubvaultId { get; set; } - public bool ReadOnly { get; set; } - } -} diff --git a/src/Core/Repositories/ISubvaultCipherRepository.cs b/src/Core/Repositories/ISubvaultCipherRepository.cs index a9d54c8da..ec3f9826f 100644 --- a/src/Core/Repositories/ISubvaultCipherRepository.cs +++ b/src/Core/Repositories/ISubvaultCipherRepository.cs @@ -11,5 +11,6 @@ namespace Bit.Core.Repositories Task> GetManyByOrganizationIdAsync(Guid organizationId); Task> GetManyByUserIdCipherIdAsync(Guid userId, Guid cipherId); Task UpdateSubvaultsAsync(Guid cipherId, Guid userId, IEnumerable subvaultIds); + Task UpdateSubvaultsForAdminAsync(Guid cipherId, Guid organizationId, IEnumerable subvaultIds); } } diff --git a/src/Core/Repositories/ISubvaultUserRepository.cs b/src/Core/Repositories/ISubvaultUserRepository.cs index 38cd5042c..96bd2f712 100644 --- a/src/Core/Repositories/ISubvaultUserRepository.cs +++ b/src/Core/Repositories/ISubvaultUserRepository.cs @@ -11,8 +11,6 @@ namespace Bit.Core.Repositories Task> GetManyByOrganizationUserIdAsync(Guid orgUserId); Task> GetManyDetailsByUserIdAsync(Guid userId); Task> GetManyDetailsBySubvaultIdAsync(Guid subvaultId); - Task> GetPermissionsByUserIdAsync(Guid userId, IEnumerable subvaultIds, - Guid organizationId); Task GetCanEditByUserIdCipherIdAsync(Guid userId, Guid cipherId); } } diff --git a/src/Core/Repositories/SqlServer/SubvaultCipherRepository.cs b/src/Core/Repositories/SqlServer/SubvaultCipherRepository.cs index d6e6f0e4c..ecf02edd3 100644 --- a/src/Core/Repositories/SqlServer/SubvaultCipherRepository.cs +++ b/src/Core/Repositories/SqlServer/SubvaultCipherRepository.cs @@ -69,5 +69,16 @@ namespace Bit.Core.Repositories.SqlServer commandType: CommandType.StoredProcedure); } } + + public async Task UpdateSubvaultsForAdminAsync(Guid cipherId, Guid organizationId, IEnumerable subvaultIds) + { + using(var connection = new SqlConnection(ConnectionString)) + { + var results = await connection.ExecuteAsync( + "[dbo].[SubvaultCipher_UpdateSubvaultsAdmin]", + new { CipherId = cipherId, OrganizationId = organizationId, SubvaultIds = subvaultIds.ToGuidIdArrayTVP() }, + commandType: CommandType.StoredProcedure); + } + } } } diff --git a/src/Core/Repositories/SqlServer/SubvaultUserRepository.cs b/src/Core/Repositories/SqlServer/SubvaultUserRepository.cs index 60d9306dc..143f58934 100644 --- a/src/Core/Repositories/SqlServer/SubvaultUserRepository.cs +++ b/src/Core/Repositories/SqlServer/SubvaultUserRepository.cs @@ -7,7 +7,6 @@ using System.Data.SqlClient; using Dapper; using System.Linq; using Bit.Core.Models.Data; -using Bit.Core.Utilities; namespace Bit.Core.Repositories.SqlServer { @@ -60,20 +59,6 @@ namespace Bit.Core.Repositories.SqlServer } } - public async Task> GetPermissionsByUserIdAsync(Guid userId, - IEnumerable subvaultIds, Guid organizationId) - { - using(var connection = new SqlConnection(ConnectionString)) - { - var results = await connection.QueryAsync( - $"[{Schema}].[SubvaultUser_ReadPermissionsBySubvaultUserId]", - new { UserId = userId, SubvaultIds = subvaultIds.ToGuidIdArrayTVP(), OrganizationId = organizationId }, - commandType: CommandType.StoredProcedure); - - return results.ToList(); - } - } - public async Task GetCanEditByUserIdCipherIdAsync(Guid userId, Guid cipherId) { using(var connection = new SqlConnection(ConnectionString)) diff --git a/src/Core/Services/ICipherService.cs b/src/Core/Services/ICipherService.cs index 8acc6f47b..c7407e330 100644 --- a/src/Core/Services/ICipherService.cs +++ b/src/Core/Services/ICipherService.cs @@ -14,7 +14,7 @@ namespace Bit.Core.Services Task SaveFolderAsync(Folder folder); Task DeleteFolderAsync(Folder folder); Task ShareAsync(Cipher cipher, Guid organizationId, IEnumerable subvaultIds, Guid userId); - Task SaveSubvaultsAsync(Cipher cipher, IEnumerable subvaultIds, Guid savingUserId); + Task SaveSubvaultsAsync(Cipher cipher, IEnumerable subvaultIds, Guid savingUserId, bool orgAdmin); 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 7d5e140b5..7a5d033a2 100644 --- a/src/Core/Services/Implementations/CipherService.cs +++ b/src/Core/Services/Implementations/CipherService.cs @@ -132,27 +132,17 @@ namespace Bit.Core.Services 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(sharingUserId, subvaultIds, - organizationId); - - var writeableSubvaults = subvaultUserDetails.Where(s => !s.ReadOnly).Select(s => s.SubvaultId); - if(!writeableSubvaults.Any()) - { - throw new BadRequestException("No subvaults."); - } - - cipher.UserId = null; + // Sproc will not save this UserId on the cipher. It is used limit scope of the subvaultIds. + cipher.UserId = sharingUserId; cipher.OrganizationId = organizationId; cipher.RevisionDate = DateTime.UtcNow; - await _cipherRepository.ReplaceAsync(cipher, writeableSubvaults); + await _cipherRepository.ReplaceAsync(cipher, subvaultIds); // push //await _pushService.PushSyncCipherUpdateAsync(cipher); } - public async Task SaveSubvaultsAsync(Cipher cipher, IEnumerable subvaultIds, Guid savingUserId) + public async Task SaveSubvaultsAsync(Cipher cipher, IEnumerable subvaultIds, Guid savingUserId, bool orgAdmin) { if(cipher.Id == default(Guid)) { @@ -164,18 +154,16 @@ namespace Bit.Core.Services 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()) + // The sprocs will validate that all subvaults belong to this org/user and that they have proper write permissions. + if(orgAdmin) { - throw new BadRequestException("No subvaults."); + await _subvaultCipherRepository.UpdateSubvaultsForAdminAsync(cipher.Id, cipher.OrganizationId.Value, + subvaultIds); + } + else + { + await _subvaultCipherRepository.UpdateSubvaultsAsync(cipher.Id, savingUserId, subvaultIds); } - - await _subvaultCipherRepository.UpdateSubvaultsAsync(cipher.Id, savingUserId, writeableSubvaults); // push //await _pushService.PushSyncCipherUpdateAsync(cipher); diff --git a/src/Sql/Sql.sqlproj b/src/Sql/Sql.sqlproj index b84f75cde..92902d541 100644 --- a/src/Sql/Sql.sqlproj +++ b/src/Sql/Sql.sqlproj @@ -143,7 +143,6 @@ - @@ -176,5 +175,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 0257a3497..aef29fd7e 100644 --- a/src/Sql/dbo/Stored Procedures/Cipher_UpdateWithSubvaults.sql +++ b/src/Sql/dbo/Stored Procedures/Cipher_UpdateWithSubvaults.sql @@ -24,21 +24,32 @@ BEGIN WHERE [Id] = @Id - MERGE - [dbo].[SubvaultCipher] AS [Target] - USING - @SubvaultIds AS [Source] - ON - [Target].[SubvaultId] = [Source].[Id] - AND [Target].[CipherId] = @Id - WHEN NOT MATCHED BY TARGET THEN - INSERT VALUES - ( - [Source].[Id], - @Id - ) - WHEN NOT MATCHED BY SOURCE - AND [Target].[CipherId] = @Id THEN - DELETE - ; + + ;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 + ) + INSERT INTO [dbo].[SubvaultCipher] + ( + [SubvaultId], + [CipherId] + ) + SELECT + Id, + @Id + FROM + @SubvaultIds + WHERE + Id IN (SELECT SubvaultId FROM [AvailableSubvaultsCTE]) END \ No newline at end of file diff --git a/src/Sql/dbo/Stored Procedures/SubvaultCipher_UpdateSubvaultsAdmin.sql b/src/Sql/dbo/Stored Procedures/SubvaultCipher_UpdateSubvaultsAdmin.sql new file mode 100644 index 000000000..945a6119e --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/SubvaultCipher_UpdateSubvaultsAdmin.sql @@ -0,0 +1,35 @@ +CREATE PROCEDURE [dbo].[SubvaultCipher_UpdateSubvaultsAdmin] + @CipherId UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER, + @SubvaultIds AS [dbo].[GuidIdArray] READONLY +AS +BEGIN + SET NOCOUNT ON + + ;WITH [AvailableSubvaultsCTE] AS( + SELECT + Id + FROM + [dbo].[Subvault] + WHERE + OrganizationId = @OrganizationId + ) + MERGE + [dbo].[SubvaultCipher] AS [Target] + USING + @SubvaultIds AS [Source] + ON + [Target].[SubvaultId] = [Source].[Id] + AND [Target].[CipherId] = @CipherId + WHEN NOT MATCHED BY TARGET + AND [Source].[Id] IN (SELECT [SubvaultId] FROM [AvailableSubvaultsCTE]) THEN + INSERT VALUES + ( + [Source].[Id], + @CipherId + ) + WHEN NOT MATCHED BY SOURCE + AND [Target].[CipherId] = @CipherId THEN + DELETE + ; +END \ No newline at end of file diff --git a/src/Sql/dbo/Stored Procedures/SubvaultUser_ReadPermissionsBySubvaultUserId.sql b/src/Sql/dbo/Stored Procedures/SubvaultUser_ReadPermissionsBySubvaultUserId.sql deleted file mode 100644 index 8a395ae7f..000000000 --- a/src/Sql/dbo/Stored Procedures/SubvaultUser_ReadPermissionsBySubvaultUserId.sql +++ /dev/null @@ -1,21 +0,0 @@ -CREATE PROCEDURE [dbo].[SubvaultUser_ReadPermissionsBySubvaultUserId] - @UserId UNIQUEIDENTIFIER, - @SubvaultIds AS [dbo].[GuidIdArray] READONLY, - @OrganizationId UNIQUEIDENTIFIER -AS -BEGIN - SET NOCOUNT ON - - SELECT - SU.[SubvaultId], - SU.[ReadOnly] - FROM - [dbo].[SubvaultUser] SU - INNER JOIN - [dbo].[OrganizationUser] OU ON OU.Id = SU.OrganizationUserId - WHERE - OU.[UserId] = @UserId - AND OU.[OrganizationId] = @OrganizationId - AND OU.[Status] = 2 -- 2 = Confirmed - AND SU.[SubvaultId] IN (SELECT [Id] FROM @SubvaultIds) -END \ No newline at end of file