1
0
mirror of https://github.com/bitwarden/server.git synced 2024-11-21 12:05:42 +01:00

Add cipher response to restore (#1072)

* Return revised ciphers on restore api call

* Return restored date from restore sproc

* Test Restore updates passed in ciphers

This is necessary for CipherController to appropriately return the
up-to-date ciphers without an extra db call to read them.

* Add missing SELECT
This commit is contained in:
Matt Gibson 2021-01-08 08:52:42 -06:00 committed by GitHub
parent 6143ad2b95
commit 5aba9f7549
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 146 additions and 18 deletions

View File

@ -437,7 +437,7 @@ namespace Bit.Api.Controllers
}
[HttpPut("{id}/restore")]
public async Task PutRestore(string id)
public async Task<CipherResponseModel> PutRestore(string id)
{
var userId = _userService.GetProperUserId(User).Value;
var cipher = await _cipherRepository.GetByIdAsync(new Guid(id), userId);
@ -447,13 +447,14 @@ namespace Bit.Api.Controllers
}
await _cipherService.RestoreAsync(cipher, userId);
return new CipherResponseModel(cipher, _globalSettings);
}
[HttpPut("{id}/restore-admin")]
public async Task PutRestoreAdmin(string id)
public async Task<CipherMiniResponseModel> PutRestoreAdmin(string id)
{
var userId = _userService.GetProperUserId(User).Value;
var cipher = await _cipherRepository.GetByIdAsync(new Guid(id));
var cipher = await _cipherRepository.GetOrganizationDetailsByIdAsync(new Guid(id));
if (cipher == null || !cipher.OrganizationId.HasValue ||
!_currentContext.OrganizationAdmin(cipher.OrganizationId.Value))
{
@ -461,10 +462,11 @@ namespace Bit.Api.Controllers
}
await _cipherService.RestoreAsync(cipher, userId, true);
return new CipherMiniResponseModel(cipher, _globalSettings, cipher.OrganizationUseTotp);
}
[HttpPut("restore")]
public async Task PutRestoreMany([FromBody]CipherBulkRestoreRequestModel model)
public async Task<ListResponseModel<CipherResponseModel>> PutRestoreMany([FromBody] CipherBulkRestoreRequestModel model)
{
if (!_globalSettings.SelfHosted && model.Ids.Count() > 500)
{
@ -472,7 +474,14 @@ namespace Bit.Api.Controllers
}
var userId = _userService.GetProperUserId(User).Value;
await _cipherService.RestoreManyAsync(model.Ids.Select(i => new Guid(i)), userId);
var cipherIdsToRestore = new HashSet<Guid>(model.Ids.Select(i => new Guid(i)));
var ciphers = await _cipherRepository.GetManyByUserIdAsync(userId);
var restoringCiphers = ciphers.Where(c => cipherIdsToRestore.Contains(c.Id) && c.Edit);
await _cipherService.RestoreManyAsync(restoringCiphers, userId);
var responses = restoringCiphers.Select(c => new CipherResponseModel(c, _globalSettings));
return new ListResponseModel<CipherResponseModel>(responses);
}
[HttpPut("move")]

3
src/Core/AssemblyInfo.cs Normal file
View File

@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Core.Test")]

View File

@ -35,6 +35,6 @@ namespace Bit.Core.Repositories
IEnumerable<CollectionCipher> collectionCiphers);
Task SoftDeleteAsync(IEnumerable<Guid> ids, Guid userId);
Task SoftDeleteByIdsOrganizationIdAsync(IEnumerable<Guid> ids, Guid organizationId);
Task RestoreAsync(IEnumerable<Guid> ids, Guid userId);
Task<DateTime> RestoreAsync(IEnumerable<Guid> ids, Guid userId);
}
}

View File

@ -610,14 +610,16 @@ namespace Bit.Core.Repositories.SqlServer
}
}
public async Task RestoreAsync(IEnumerable<Guid> ids, Guid userId)
public async Task<DateTime> RestoreAsync(IEnumerable<Guid> ids, Guid userId)
{
using (var connection = new SqlConnection(ConnectionString))
{
var results = await connection.ExecuteAsync(
var results = await connection.ExecuteScalarAsync<DateTime>(
$"[{Schema}].[Cipher_Restore]",
new { Ids = ids.ToGuidIdArrayTVP(), UserId = userId },
commandType: CommandType.StoredProcedure);
return results;
}
}

View File

@ -36,6 +36,6 @@ namespace Bit.Core.Services
Task SoftDeleteAsync(Cipher cipher, Guid deletingUserId, bool orgAdmin = false);
Task SoftDeleteManyAsync(IEnumerable<Guid> cipherIds, Guid deletingUserId, Guid? organizationId = null, bool orgAdmin = false);
Task RestoreAsync(Cipher cipher, Guid restoringUserId, bool orgAdmin = false);
Task RestoreManyAsync(IEnumerable<Guid> cipherIds, Guid restoringUserId);
Task RestoreManyAsync(IEnumerable<CipherDetails> ciphers, Guid restoringUserId);
}
}

View File

@ -786,16 +786,16 @@ namespace Bit.Core.Services
await _pushService.PushSyncCipherUpdateAsync(cipher, null);
}
public async Task RestoreManyAsync(IEnumerable<Guid> cipherIds, Guid restoringUserId)
public async Task RestoreManyAsync(IEnumerable<CipherDetails> ciphers, Guid restoringUserId)
{
var cipherIdsSet = new HashSet<Guid>(cipherIds);
var ciphers = await _cipherRepository.GetManyByUserIdAsync(restoringUserId);
var restoringCiphers = ciphers.Where(c => cipherIdsSet.Contains(c.Id) && c.Edit);
var revisionDate = await _cipherRepository.RestoreAsync(ciphers.Select(c => c.Id), restoringUserId);
await _cipherRepository.RestoreAsync(cipherIds, restoringUserId);
var events = restoringCiphers.Select(c =>
new Tuple<Cipher, EventType, DateTime?>(c, EventType.Cipher_Restored, null));
var events = ciphers.Select(c =>
{
c.RevisionDate = revisionDate;
c.DeletedDate = null;
return new Tuple<Cipher, EventType, DateTime?>(c, EventType.Cipher_Restored, null);
});
foreach (var eventsBatch in events.Batch(100))
{
await _eventService.LogCipherEventsAsync(eventsBatch);

View File

@ -57,4 +57,6 @@ BEGIN
EXEC [dbo].[User_BumpAccountRevisionDate] @UserId
DROP TABLE #Temp
END
SELECT @UtcNow
END

View File

@ -116,5 +116,46 @@ namespace Bit.Core.Test.Services
await sutProvider.GetDependency<ICipherRepository>().Received(1).UpdateCiphersAsync(sharingUserId,
Arg.Is<IEnumerable<Cipher>>(arg => arg.Except(ciphers).IsNullOrEmpty()));
}
[Theory]
[InlineKnownUserCipherAutoData("c64d8a15-606e-41d6-9c7e-174d4d8f3b2e", "c64d8a15-606e-41d6-9c7e-174d4d8f3b2e")]
[InlineOrganizationCipherAutoData("c64d8a15-606e-41d6-9c7e-174d4d8f3b2e")]
public async Task RestoreAsync_UpdatesCipher(Guid restoringUserId, Cipher cipher, SutProvider<CipherService> sutProvider)
{
sutProvider.GetDependency<ICipherRepository>().GetCanEditByIdAsync(restoringUserId, cipher.Id).Returns(true);
var initialRevisionDate = new DateTime(1970, 1, 1, 0, 0, 0);
cipher.DeletedDate = initialRevisionDate;
cipher.RevisionDate = initialRevisionDate;
await sutProvider.Sut.RestoreAsync(cipher, restoringUserId, cipher.OrganizationId.HasValue);
Assert.Null(cipher.DeletedDate);
Assert.NotEqual(initialRevisionDate, cipher.RevisionDate);
}
[Theory]
[InlineKnownUserCipherAutoData("c64d8a15-606e-41d6-9c7e-174d4d8f3b2e", "c64d8a15-606e-41d6-9c7e-174d4d8f3b2e")]
public async Task RestoreManyAsync_UpdatesCiphers(Guid restoringUserId, IEnumerable<CipherDetails> ciphers,
SutProvider<CipherService> sutProvider)
{
var previousRevisionDate = DateTime.UtcNow;
foreach (var cipher in ciphers)
{
cipher.RevisionDate = previousRevisionDate;
}
var revisionDate = previousRevisionDate + TimeSpan.FromMinutes(1);
sutProvider.GetDependency<ICipherRepository>().RestoreAsync(Arg.Any<IEnumerable<Guid>>(), restoringUserId)
.Returns(revisionDate);
await sutProvider.Sut.RestoreManyAsync(ciphers, restoringUserId);
foreach (var cipher in ciphers)
{
Assert.Null(cipher.DeletedDate);
Assert.Equal(revisionDate, cipher.RevisionDate);
}
}
}
}

View File

@ -0,0 +1,71 @@
IF OBJECT_ID('[dbo].[Cipher_Restore]') IS NOT NULL
BEGIN
DROP PROCEDURE [dbo].[Cipher_Restore]
END
GO
CREATE PROCEDURE [dbo].[Cipher_Restore]
@Ids AS [dbo].[GuidIdArray] READONLY,
@UserId AS UNIQUEIDENTIFIER
AS
BEGIN
SET NOCOUNT ON
CREATE TABLE #Temp
(
[Id] UNIQUEIDENTIFIER NOT NULL,
[UserId] UNIQUEIDENTIFIER NULL,
[OrganizationId] UNIQUEIDENTIFIER NULL
)
INSERT INTO #Temp
SELECT
[Id],
[UserId],
[OrganizationId]
FROM
[dbo].[UserCipherDetails](@UserId)
WHERE
[Edit] = 1
AND [DeletedDate] IS NOT NULL
AND [Id] IN (SELECT *
FROM @Ids)
DECLARE @UtcNow DATETIME2(7) = GETUTCDATE();
UPDATE
[dbo].[Cipher]
SET
[DeletedDate] = NULL,
[RevisionDate] = @UtcNow
WHERE
[Id] IN (SELECT [Id]
FROM #Temp)
-- Bump orgs
DECLARE @OrgId UNIQUEIDENTIFIER
DECLARE [OrgCursor] CURSOR FORWARD_ONLY FOR
SELECT
[OrganizationId]
FROM
#Temp
WHERE
[OrganizationId] IS NOT NULL
GROUP BY
[OrganizationId]
OPEN [OrgCursor]
FETCH NEXT FROM [OrgCursor] INTO @OrgId
WHILE @@FETCH_STATUS = 0 BEGIN
EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationId] @OrgId
FETCH NEXT FROM [OrgCursor] INTO @OrgId
END
CLOSE [OrgCursor]
DEALLOCATE [OrgCursor]
-- Bump user
EXEC [dbo].[User_BumpAccountRevisionDate] @UserId
DROP TABLE #Temp
SELECT @UtcNow
END
GO