diff --git a/src/Api/Controllers/CiphersController.cs b/src/Api/Controllers/CiphersController.cs index f7d3b57d05..e3a90319eb 100644 --- a/src/Api/Controllers/CiphersController.cs +++ b/src/Api/Controllers/CiphersController.cs @@ -127,7 +127,7 @@ namespace Bit.Api.Controllers throw new NotFoundException(); } - await _cipherService.DeleteAsync(cipher); + await _cipherService.DeleteAsync(cipher, userId); } } } diff --git a/src/Api/Controllers/LoginsController.cs b/src/Api/Controllers/LoginsController.cs index cf517ed918..3b466766ef 100644 --- a/src/Api/Controllers/LoginsController.cs +++ b/src/Api/Controllers/LoginsController.cs @@ -59,7 +59,7 @@ namespace Bit.Api.Controllers { var userId = _userService.GetProperUserId(User).Value; var login = model.ToCipherDetails(userId); - await _cipherService.SaveAsync(login); + await _cipherService.SaveAsync(login, userId); var response = new LoginResponseModel(login); return response; @@ -76,7 +76,7 @@ namespace Bit.Api.Controllers throw new NotFoundException(); } - await _cipherService.SaveAsync(model.ToCipherDetails(login)); + await _cipherService.SaveAsync(model.ToCipherDetails(login), userId); var response = new LoginResponseModel(login); return response; @@ -86,13 +86,14 @@ namespace Bit.Api.Controllers [HttpPost("{id}/delete")] public async Task Delete(string id) { - var login = await _cipherRepository.GetByIdAsync(new Guid(id), _userService.GetProperUserId(User).Value); + var userId = _userService.GetProperUserId(User).Value; + var login = await _cipherRepository.GetByIdAsync(new Guid(id), userId); if(login == null || login.Type != Core.Enums.CipherType.Login) { throw new NotFoundException(); } - await _cipherService.DeleteAsync(login); + await _cipherService.DeleteAsync(login, userId); } } } diff --git a/src/Core/Repositories/ISubvaultUserRepository.cs b/src/Core/Repositories/ISubvaultUserRepository.cs index 883a118f3d..0e4b65b7a2 100644 --- a/src/Core/Repositories/ISubvaultUserRepository.cs +++ b/src/Core/Repositories/ISubvaultUserRepository.cs @@ -12,5 +12,6 @@ namespace Bit.Core.Repositories Task> GetManyDetailsByUserIdAsync(Guid userId); Task> GetPermissionsByUserIdAsync(Guid userId, IEnumerable subvaultIds, Guid organizationId); + Task GetIsAdminByUserIdCipherIdAsync(Guid userId, Guid cipherId); } } diff --git a/src/Core/Repositories/SqlServer/SubvaultUserRepository.cs b/src/Core/Repositories/SqlServer/SubvaultUserRepository.cs index fecb589239..b2b6c6f1f8 100644 --- a/src/Core/Repositories/SqlServer/SubvaultUserRepository.cs +++ b/src/Core/Repositories/SqlServer/SubvaultUserRepository.cs @@ -60,5 +60,18 @@ namespace Bit.Core.Repositories.SqlServer return results.ToList(); } } + + public async Task GetIsAdminByUserIdCipherIdAsync(Guid userId, Guid cipherId) + { + using(var connection = new SqlConnection(ConnectionString)) + { + var result = await connection.QueryFirstOrDefaultAsync( + $"[{Schema}].[SubvaultUser_ReadIsAdminByCipherIdUserId]", + new { UserId = userId, CipherId = cipherId }, + commandType: CommandType.StoredProcedure); + + return result; + } + } } } diff --git a/src/Core/Services/ICipherService.cs b/src/Core/Services/ICipherService.cs index 1bfdbf6d7f..94d5fc7118 100644 --- a/src/Core/Services/ICipherService.cs +++ b/src/Core/Services/ICipherService.cs @@ -8,8 +8,8 @@ namespace Bit.Core.Services { public interface ICipherService { - Task SaveAsync(CipherDetails cipher); - Task DeleteAsync(Cipher cipher); + Task SaveAsync(CipherDetails cipher, Guid savingUserId); + Task DeleteAsync(CipherDetails cipher, Guid deletingUserId); Task SaveFolderAsync(Folder folder); Task DeleteFolderAsync(Folder folder); Task MoveSubvaultAsync(Cipher cipher, IEnumerable subvaultIds, Guid userId); diff --git a/src/Core/Services/Implementations/CipherService.cs b/src/Core/Services/Implementations/CipherService.cs index 2b423e4a00..85068ac1f5 100644 --- a/src/Core/Services/Implementations/CipherService.cs +++ b/src/Core/Services/Implementations/CipherService.cs @@ -37,8 +37,14 @@ namespace Bit.Core.Services _pushService = pushService; } - public async Task SaveAsync(CipherDetails cipher) + public async Task SaveAsync(CipherDetails cipher, Guid savingUserId) { + if(!(await UserHasAdminRights(cipher, savingUserId))) + { + throw new BadRequestException("Not an admin."); + } + + cipher.UserId = savingUserId; if(cipher.Id == default(Guid)) { await _cipherRepository.CreateAsync(cipher); @@ -56,8 +62,13 @@ namespace Bit.Core.Services } } - public async Task DeleteAsync(Cipher cipher) + public async Task DeleteAsync(CipherDetails cipher, Guid deletingUserId) { + if(!(await UserHasAdminRights(cipher, deletingUserId))) + { + throw new BadRequestException("Not an admin."); + } + await _cipherRepository.DeleteAsync(cipher); // push @@ -151,5 +162,15 @@ namespace Bit.Core.Services await _pushService.PushSyncCiphersAsync(userId.Value); } } + + private async Task UserHasAdminRights(CipherDetails cipher, Guid userId) + { + if(!cipher.OrganizationId.HasValue && cipher.UserId.HasValue && cipher.UserId.Value == userId) + { + return true; + } + + return await _subvaultUserRepository.GetIsAdminByUserIdCipherIdAsync(userId, cipher.Id); + } } } diff --git a/src/Sql/Sql.sqlproj b/src/Sql/Sql.sqlproj index 33f20d595c..8531da6986 100644 --- a/src/Sql/Sql.sqlproj +++ b/src/Sql/Sql.sqlproj @@ -173,5 +173,6 @@ + \ No newline at end of file diff --git a/src/Sql/dbo/Stored Procedures/CipherDetails_Create.sql b/src/Sql/dbo/Stored Procedures/CipherDetails_Create.sql index 2f4ba02504..d3be629e64 100644 --- a/src/Sql/dbo/Stored Procedures/CipherDetails_Create.sql +++ b/src/Sql/dbo/Stored Procedures/CipherDetails_Create.sql @@ -25,7 +25,7 @@ BEGIN VALUES ( @Id, - @UserId, + CASE WHEN @OrganizationId IS NULL THEN @UserId ELSE NULL END, @OrganizationId, @Type, @Data, diff --git a/src/Sql/dbo/Stored Procedures/CipherDetails_Update.sql b/src/Sql/dbo/Stored Procedures/CipherDetails_Update.sql index ee62b5b9e9..127e94bbcd 100644 --- a/src/Sql/dbo/Stored Procedures/CipherDetails_Update.sql +++ b/src/Sql/dbo/Stored Procedures/CipherDetails_Update.sql @@ -15,7 +15,7 @@ BEGIN UPDATE [dbo].[Cipher] SET - [UserId] = @UserId, + [UserId] = CASE WHEN @OrganizationId IS NULL THEN @UserId ELSE NULL END, [OrganizationId] = @OrganizationId, [Type] = @Type, [Data] = @Data, diff --git a/src/Sql/dbo/Stored Procedures/SubvaultUser_ReadIsAdminByCipherIdUserId.sql b/src/Sql/dbo/Stored Procedures/SubvaultUser_ReadIsAdminByCipherIdUserId.sql new file mode 100644 index 0000000000..2e6c6714ed --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/SubvaultUser_ReadIsAdminByCipherIdUserId.sql @@ -0,0 +1,30 @@ +CREATE PROCEDURE [dbo].[SubvaultUser_ReadIsAdminByCipherIdUserId] + @UserId UNIQUEIDENTIFIER, + @CipherId AS UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + ;WITH [CTE] AS( + SELECT + CASE WHEN OU.[Type] = 2 THEN SU.[Admin] ELSE 1 END AS [Admin] -- 2 = Regular User + FROM + [dbo].[SubvaultUser] SU + INNER JOIN + [dbo].[SubvaultCipher] SC ON SC.SubvaultId = SU.SubvaultId + INNER JOIN + [dbo].[Cipher] C ON SC.[CipherId] = C.[Id] + INNER JOIN + [dbo].[OrganizationUser] OU ON OU.Id = SU.OrganizationUserId AND OU.OrganizationId = C.OrganizationId + WHERE + C.[Id] = @CipherId + AND OU.[UserId] = @UserId + AND OU.[Status] = 2 -- 2 = Confirmed + ) + SELECT + CASE WHEN COUNT(1) > 0 THEN 1 ELSE 0 END + FROM + [CTE] + WHERE + [Admin] = 1 +END \ No newline at end of file