diff --git a/src/Api/Controllers/CiphersController.cs b/src/Api/Controllers/CiphersController.cs index 9c6391f430..dc5638a394 100644 --- a/src/Api/Controllers/CiphersController.cs +++ b/src/Api/Controllers/CiphersController.cs @@ -104,6 +104,15 @@ namespace Bit.Api.Controllers // await _cipherService.SaveAsync(cipher); //} + [HttpPut("{id}/partial")] + [HttpPost("{id}/partial")] + public async Task PutPartial(string id, [FromBody]CipherPartialRequestModel model) + { + var userId = _userService.GetProperUserId(User).Value; + var folderId = string.IsNullOrWhiteSpace(model.FolderId) ? null : (Guid?)new Guid(model.FolderId); + 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) diff --git a/src/Api/Controllers/LoginsController.cs b/src/Api/Controllers/LoginsController.cs index 3b466766ef..984bccd81e 100644 --- a/src/Api/Controllers/LoginsController.cs +++ b/src/Api/Controllers/LoginsController.cs @@ -31,16 +31,16 @@ namespace Bit.Api.Controllers } [HttpGet("{id}")] - public async Task Get(string id) + public async Task Get(string id) { var userId = _userService.GetProperUserId(User).Value; - var login = await _cipherRepository.GetByIdAsync(new Guid(id), userId); + var login = await _cipherRepository.GetFullDetailsByIdAsync(new Guid(id), userId); if(login == null || login.Type != Core.Enums.CipherType.Login) { throw new NotFoundException(); } - var response = new LoginResponseModel(login); + var response = new LoginDetailsResponseModel(login); return response; } @@ -70,7 +70,7 @@ namespace Bit.Api.Controllers public async Task Put(string id, [FromBody]LoginRequestModel model) { var userId = _userService.GetProperUserId(User).Value; - var login = await _cipherRepository.GetByIdAsync(new Guid(id), _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(); diff --git a/src/Core/Models/Api/Request/CipherPartialRequestModel.cs b/src/Core/Models/Api/Request/CipherPartialRequestModel.cs new file mode 100644 index 0000000000..ecc866b648 --- /dev/null +++ b/src/Core/Models/Api/Request/CipherPartialRequestModel.cs @@ -0,0 +1,11 @@ +using System.ComponentModel.DataAnnotations; + +namespace Bit.Core.Models.Api +{ + public class CipherPartialRequestModel + { + [StringLength(36)] + public string FolderId { get; set; } + public bool Favorite { get; set; } + } +} diff --git a/src/Core/Models/Api/Response/LoginResponseModel.cs b/src/Core/Models/Api/Response/LoginResponseModel.cs index cc2f2282f2..c91f6e20b8 100644 --- a/src/Core/Models/Api/Response/LoginResponseModel.cs +++ b/src/Core/Models/Api/Response/LoginResponseModel.cs @@ -43,4 +43,15 @@ namespace Bit.Core.Models.Api public string Notes { get; set; } public DateTime RevisionDate { get; set; } } + + public class LoginDetailsResponseModel : LoginResponseModel + { + public LoginDetailsResponseModel(CipherFullDetails cipher) + : base(cipher, "loginDetails") + { + Edit = cipher.Edit; + } + + public bool Edit { get; set; } + } } diff --git a/src/Core/Models/Data/CipherFullDetails.cs b/src/Core/Models/Data/CipherFullDetails.cs new file mode 100644 index 0000000000..c4d86e76b4 --- /dev/null +++ b/src/Core/Models/Data/CipherFullDetails.cs @@ -0,0 +1,7 @@ +namespace Core.Models.Data +{ + public class CipherFullDetails : CipherDetails + { + public bool Edit { get; set; } + } +} diff --git a/src/Core/Repositories/ICipherRepository.cs b/src/Core/Repositories/ICipherRepository.cs index cca07a1071..041de63ad2 100644 --- a/src/Core/Repositories/ICipherRepository.cs +++ b/src/Core/Repositories/ICipherRepository.cs @@ -9,6 +9,7 @@ namespace Bit.Core.Repositories public interface ICipherRepository : IRepository { Task GetByIdAsync(Guid id, Guid userId); + Task GetFullDetailsByIdAsync(Guid id, Guid userId); Task> GetManyByUserIdAsync(Guid userId); Task> GetManyByUserIdHasSubvaultsAsync(Guid userId); Task> GetManyByTypeAndUserIdAsync(Enums.CipherType type, Guid userId); @@ -18,6 +19,7 @@ namespace Bit.Core.Repositories Task ReplaceAsync(CipherDetails cipher); Task UpsertAsync(CipherDetails cipher); Task ReplaceAsync(Cipher obj, IEnumerable subvaultIds); + Task UpdatePartialAsync(Guid id, Guid userId, Guid? folderId, bool favorite); Task UpdateUserEmailPasswordAndCiphersAsync(User user, IEnumerable ciphers); Task CreateAsync(IEnumerable ciphers); } diff --git a/src/Core/Repositories/ISubvaultUserRepository.cs b/src/Core/Repositories/ISubvaultUserRepository.cs index 0e4b65b7a2..17f603b380 100644 --- a/src/Core/Repositories/ISubvaultUserRepository.cs +++ b/src/Core/Repositories/ISubvaultUserRepository.cs @@ -12,6 +12,6 @@ namespace Bit.Core.Repositories Task> GetManyDetailsByUserIdAsync(Guid userId); Task> GetPermissionsByUserIdAsync(Guid userId, IEnumerable subvaultIds, Guid organizationId); - Task GetIsAdminByUserIdCipherIdAsync(Guid userId, Guid cipherId); + Task GetCanEditByUserIdCipherIdAsync(Guid userId, Guid cipherId); } } diff --git a/src/Core/Repositories/SqlServer/CipherRepository.cs b/src/Core/Repositories/SqlServer/CipherRepository.cs index 687d52a6b6..636852fda7 100644 --- a/src/Core/Repositories/SqlServer/CipherRepository.cs +++ b/src/Core/Repositories/SqlServer/CipherRepository.cs @@ -36,6 +36,19 @@ namespace Bit.Core.Repositories.SqlServer } } + public async Task GetFullDetailsByIdAsync(Guid id, Guid userId) + { + using(var connection = new SqlConnection(ConnectionString)) + { + var results = await connection.QueryAsync( + $"[{Schema}].[CipherFullDetails_ReadByIdUserId]", + new { Id = id, UserId = userId }, + commandType: CommandType.StoredProcedure); + + return results.FirstOrDefault(); + } + } + public async Task> GetManyByUserIdAsync(Guid userId) { using(var connection = new SqlConnection(ConnectionString)) @@ -149,6 +162,17 @@ namespace Bit.Core.Repositories.SqlServer } } + public async Task UpdatePartialAsync(Guid id, Guid userId, Guid? folderId, bool favorite) + { + using(var connection = new SqlConnection(ConnectionString)) + { + var results = await connection.ExecuteAsync( + $"[{Schema}].[Cipher_UpdatePartial]", + new { Id = id, UserId = userId, FolderId = folderId, Favorite = favorite }, + commandType: CommandType.StoredProcedure); + } + } + public Task UpdateUserEmailPasswordAndCiphersAsync(User user, IEnumerable ciphers) { if(ciphers.Count() == 0) diff --git a/src/Core/Repositories/SqlServer/SubvaultUserRepository.cs b/src/Core/Repositories/SqlServer/SubvaultUserRepository.cs index b2b6c6f1f8..368e16d6d5 100644 --- a/src/Core/Repositories/SqlServer/SubvaultUserRepository.cs +++ b/src/Core/Repositories/SqlServer/SubvaultUserRepository.cs @@ -61,12 +61,12 @@ namespace Bit.Core.Repositories.SqlServer } } - public async Task GetIsAdminByUserIdCipherIdAsync(Guid userId, Guid cipherId) + public async Task GetCanEditByUserIdCipherIdAsync(Guid userId, Guid cipherId) { using(var connection = new SqlConnection(ConnectionString)) { var result = await connection.QueryFirstOrDefaultAsync( - $"[{Schema}].[SubvaultUser_ReadIsAdminByCipherIdUserId]", + $"[{Schema}].[SubvaultUser_ReadCanEditByCipherIdUserId]", new { UserId = userId, CipherId = cipherId }, commandType: CommandType.StoredProcedure); diff --git a/src/Core/Services/ICipherService.cs b/src/Core/Services/ICipherService.cs index 94d5fc7118..14332fecc1 100644 --- a/src/Core/Services/ICipherService.cs +++ b/src/Core/Services/ICipherService.cs @@ -9,6 +9,7 @@ namespace Bit.Core.Services public interface ICipherService { Task SaveAsync(CipherDetails cipher, Guid savingUserId); + Task UpdatePartialAsync(Guid cipherId, Guid savingUserId, Guid? folderId, bool favorite); Task DeleteAsync(CipherDetails cipher, Guid deletingUserId); Task SaveFolderAsync(Folder folder); Task DeleteFolderAsync(Folder folder); diff --git a/src/Core/Services/Implementations/CipherService.cs b/src/Core/Services/Implementations/CipherService.cs index 85068ac1f5..00cfb52c2c 100644 --- a/src/Core/Services/Implementations/CipherService.cs +++ b/src/Core/Services/Implementations/CipherService.cs @@ -39,7 +39,7 @@ namespace Bit.Core.Services public async Task SaveAsync(CipherDetails cipher, Guid savingUserId) { - if(!(await UserHasAdminRights(cipher, savingUserId))) + if(!(await UserCanEditAsync(cipher, savingUserId))) { throw new BadRequestException("Not an admin."); } @@ -62,9 +62,19 @@ namespace Bit.Core.Services } } + public async Task UpdatePartialAsync(Guid cipherId, Guid savingUserId, Guid? folderId, bool favorite) + { + if(!(await UserCanPartialEditAsync(cipherId, savingUserId))) + { + throw new BadRequestException("Cannot edit."); + } + + await _cipherRepository.UpdatePartialAsync(cipherId, savingUserId, folderId, favorite); + } + public async Task DeleteAsync(CipherDetails cipher, Guid deletingUserId) { - if(!(await UserHasAdminRights(cipher, deletingUserId))) + if(!(await UserCanEditAsync(cipher, deletingUserId))) { throw new BadRequestException("Not an admin."); } @@ -163,14 +173,22 @@ namespace Bit.Core.Services } } - private async Task UserHasAdminRights(CipherDetails cipher, Guid userId) + private async Task UserCanEditAsync(CipherDetails cipher, Guid userId) { if(!cipher.OrganizationId.HasValue && cipher.UserId.HasValue && cipher.UserId.Value == userId) { return true; } - return await _subvaultUserRepository.GetIsAdminByUserIdCipherIdAsync(userId, cipher.Id); + return await _subvaultUserRepository.GetCanEditByUserIdCipherIdAsync(userId, cipher.Id); + } + + private Task UserCanPartialEditAsync(Guid cipherId, Guid userId) + { + // TODO: implement + + return Task.FromResult(true); + //return await _subvaultUserRepository.GetCanEditByUserIdCipherIdAsync(userId, cipherId); } } } diff --git a/src/Sql/Sql.sqlproj b/src/Sql/Sql.sqlproj index 8531da6986..eac203fa74 100644 --- a/src/Sql/Sql.sqlproj +++ b/src/Sql/Sql.sqlproj @@ -173,6 +173,9 @@ - + + + + \ No newline at end of file diff --git a/src/Sql/dbo/Stored Procedures/SubvaultUser_ReadIsAdminByCipherIdUserId.sql b/src/Sql/dbo/Functions/UserCanEditCipher.sql similarity index 56% rename from src/Sql/dbo/Stored Procedures/SubvaultUser_ReadIsAdminByCipherIdUserId.sql rename to src/Sql/dbo/Functions/UserCanEditCipher.sql index 2e6c6714ed..4405ed7d44 100644 --- a/src/Sql/dbo/Stored Procedures/SubvaultUser_ReadIsAdminByCipherIdUserId.sql +++ b/src/Sql/dbo/Functions/UserCanEditCipher.sql @@ -1,13 +1,15 @@ -CREATE PROCEDURE [dbo].[SubvaultUser_ReadIsAdminByCipherIdUserId] - @UserId UNIQUEIDENTIFIER, - @CipherId AS UNIQUEIDENTIFIER -AS +CREATE FUNCTION [dbo].[UserCanEditCipher](@UserId UNIQUEIDENTIFIER, @CipherId UNIQUEIDENTIFIER) +RETURNS BIT AS BEGIN - SET NOCOUNT ON + DECLARE @CanEdit BIT ;WITH [CTE] AS( SELECT - CASE WHEN OU.[Type] = 2 THEN SU.[Admin] ELSE 1 END AS [Admin] -- 2 = Regular User + CASE + WHEN OU.[Type] = 2 AND SU.[Admin] = 1 THEN 1 -- 2 = Regular User + WHEN SU.[ReadOnly] = 0 THEN 1 + ELSE 0 + END [CanEdit] FROM [dbo].[SubvaultUser] SU INNER JOIN @@ -22,9 +24,11 @@ BEGIN AND OU.[Status] = 2 -- 2 = Confirmed ) SELECT - CASE WHEN COUNT(1) > 0 THEN 1 ELSE 0 END + @CanEdit = CASE WHEN COUNT(1) > 0 THEN 1 ELSE 0 END FROM [CTE] WHERE - [Admin] = 1 -END \ No newline at end of file + [CanEdit] = 1 + + RETURN @CanEdit +END diff --git a/src/Sql/dbo/Stored Procedures/CipherDetails_Update.sql b/src/Sql/dbo/Stored Procedures/CipherDetails_Update.sql index 127e94bbcd..6fc46d9fe7 100644 --- a/src/Sql/dbo/Stored Procedures/CipherDetails_Update.sql +++ b/src/Sql/dbo/Stored Procedures/CipherDetails_Update.sql @@ -24,21 +24,5 @@ BEGIN WHERE [Id] = @Id - IF @FolderId IS NULL - BEGIN - EXEC [dbo].[FolderCipher_DeleteByUserId] @UserId, @Id - END - ELSE IF (SELECT COUNT(1) FROM [dbo].[FolderCipher] WHERE [FolderId] = @FolderId AND [CipherId] = @Id) = 0 - BEGIN - EXEC [dbo].[FolderCipher_Create] @FolderId, @Id - END - - IF @Favorite = 0 - BEGIN - EXEC [dbo].[Favorite_Delete] @UserId, @Id - END - ELSE IF (SELECT COUNT(1) FROM [dbo].[Favorite] WHERE [UserId] = @UserId AND [CipherId] = @Id) = 0 - BEGIN - EXEC [dbo].[Favorite_Create] @UserId, @Id - END + EXEC [dbo].[Cipher_UpdatePartial] @Id, @UserId, @FolderId, @Favorite END \ No newline at end of file diff --git a/src/Sql/dbo/Stored Procedures/CipherFullDetails_ReadByIdUserId.sql b/src/Sql/dbo/Stored Procedures/CipherFullDetails_ReadByIdUserId.sql new file mode 100644 index 0000000000..453997a476 --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/CipherFullDetails_ReadByIdUserId.sql @@ -0,0 +1,28 @@ +CREATE PROCEDURE [dbo].[CipherFullDetails_ReadByIdUserId] + @Id UNIQUEIDENTIFIER, + @UserId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT DISTINCT + C.*, + CASE + WHEN C.[OrganizationId] IS NULL THEN 1 + ELSE [dbo].[UserCanEditCipher](@UserId, @Id) + END [Edit] + FROM + [dbo].[CipherDetails](@UserId) C + LEFT JOIN + [dbo].[SubvaultCipher] SC ON SC.[CipherId] = C.[Id] + LEFT JOIN + [dbo].[SubvaultUser] SU ON SU.[SubvaultId] = SC.[SubvaultId] + LEFT JOIN + [dbo].[OrganizationUser] OU ON OU.[Id] = SU.[OrganizationUserId] + WHERE + C.Id = @Id + AND ( + (C.[UserId] IS NOT NULL AND C.[UserId] = @UserId) + OR (OU.[UserId] = @UserId AND OU.[Status] = 2) -- 2 = Confirmed + ) +END \ No newline at end of file diff --git a/src/Sql/dbo/Stored Procedures/Cipher_UpdatePartial.sql b/src/Sql/dbo/Stored Procedures/Cipher_UpdatePartial.sql new file mode 100644 index 0000000000..10b7b5097d --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/Cipher_UpdatePartial.sql @@ -0,0 +1,27 @@ +CREATE PROCEDURE [dbo].[Cipher_UpdatePartial] + @Id UNIQUEIDENTIFIER, + @UserId UNIQUEIDENTIFIER, + @FolderId UNIQUEIDENTIFIER, + @Favorite TINYINT +AS +BEGIN + SET NOCOUNT ON + + IF @FolderId IS NULL + BEGIN + EXEC [dbo].[FolderCipher_DeleteByUserId] @UserId, @Id + END + ELSE IF (SELECT COUNT(1) FROM [dbo].[FolderCipher] WHERE [FolderId] = @FolderId AND [CipherId] = @Id) = 0 + BEGIN + EXEC [dbo].[FolderCipher_Create] @FolderId, @Id + END + + IF @Favorite = 0 + BEGIN + EXEC [dbo].[Favorite_Delete] @UserId, @Id + END + ELSE IF (SELECT COUNT(1) FROM [dbo].[Favorite] WHERE [UserId] = @UserId AND [CipherId] = @Id) = 0 + BEGIN + EXEC [dbo].[Favorite_Create] @UserId, @Id + END +END \ No newline at end of file diff --git a/src/Sql/dbo/Stored Procedures/SubvaultUser_ReadCanEditByCipherIdUserId.sql b/src/Sql/dbo/Stored Procedures/SubvaultUser_ReadCanEditByCipherIdUserId.sql new file mode 100644 index 0000000000..925d779630 --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/SubvaultUser_ReadCanEditByCipherIdUserId.sql @@ -0,0 +1,10 @@ +CREATE PROCEDURE [dbo].[SubvaultUser_ReadCanEditByCipherIdUserId] + @UserId UNIQUEIDENTIFIER, + @CipherId AS UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT + [dbo].[UserCanEditCipher](@UserId, @CipherId) +END