diff --git a/src/Api/Controllers/CiphersController.cs b/src/Api/Controllers/CiphersController.cs index 8af56710cd..06df5eba4a 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 a6bad7e17b..5fb2512b7d 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 93edf4b3df..0000000000 --- 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 a9d54c8daa..ec3f9826f4 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 38cd5042cc..96bd2f7129 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 d6e6f0e4c1..ecf02edd31 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 60d9306dcc..143f589343 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 8acc6f47b3..c7407e3302 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 7d5e140b5e..7a5d033a2c 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 b84f75cdeb..92902d541d 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 0257a34972..aef29fd7ec 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 0000000000..945a6119e4 --- /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 8a395ae7f1..0000000000 --- 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