Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[AC-1344] Provider users unable to bulk restore vault items for client organizations #2871

30 changes: 24 additions & 6 deletions src/Api/Vault/Controllers/CiphersController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -451,7 +451,7 @@ public async Task<CipherMiniResponseModel> PutRestoreAdmin(string id)
}

[HttpPut("restore")]
public async Task<ListResponseModel<CipherResponseModel>> PutRestoreMany([FromBody] CipherBulkRestoreRequestModel model)
public async Task<ListResponseModel<CipherMiniResponseModel>> PutRestoreMany([FromBody] CipherBulkRestoreRequestModel model)
{
if (!_globalSettings.SelfHosted && model.Ids.Count() > 500)
{
Expand All @@ -461,12 +461,30 @@ public async Task<ListResponseModel<CipherResponseModel>> PutRestoreMany([FromBo
var userId = _userService.GetProperUserId(User).Value;
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);
var restoredCiphers = await _cipherService.RestoreManyAsync(cipherIdsToRestore, userId);
var responses = restoredCiphers.Select(c => new CipherMiniResponseModel(c, _globalSettings, c.OrganizationUseTotp));
return new ListResponseModel<CipherMiniResponseModel>(responses);
}

[HttpPut("restore-admin")]
public async Task<ListResponseModel<CipherMiniResponseModel>> PutRestoreManyAdmin([FromBody] CipherBulkRestoreRequestModel model)
{
if (!_globalSettings.SelfHosted && model.Ids.Count() > 500)
{
throw new BadRequestException("You can only restore up to 500 items at a time.");
}

if (model == null || model.OrganizationId == default || !await _currentContext.EditAnyCollection(model.OrganizationId))
{
throw new NotFoundException();
}

var userId = _userService.GetProperUserId(User).Value;
var cipherIdsToRestore = new HashSet<Guid>(model.Ids.Select(i => new Guid(i)));

await _cipherService.RestoreManyAsync(restoringCiphers, userId);
var responses = restoringCiphers.Select(c => new CipherResponseModel(c, _globalSettings));
return new ListResponseModel<CipherResponseModel>(responses);
var restoredCiphers = await _cipherService.RestoreManyAsync(cipherIdsToRestore, userId, model.OrganizationId, true);
var responses = restoredCiphers.Select(c => new CipherMiniResponseModel(c, _globalSettings, c.OrganizationUseTotp));
return new ListResponseModel<CipherMiniResponseModel>(responses);
}

[HttpPut("move")]
Expand Down
1 change: 1 addition & 0 deletions src/Api/Vault/Models/Request/CipherRequestModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,7 @@ public class CipherBulkRestoreRequestModel
{
[Required]
public IEnumerable<string> Ids { get; set; }
public Guid OrganizationId { get; set; }
}

public class CipherBulkMoveRequestModel
Expand Down
1 change: 1 addition & 0 deletions src/Core/Vault/Repositories/ICipherRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,6 @@ Task CreateAsync(IEnumerable<Cipher> ciphers, IEnumerable<Collection> collection
Task SoftDeleteAsync(IEnumerable<Guid> ids, Guid userId);
Task SoftDeleteByIdsOrganizationIdAsync(IEnumerable<Guid> ids, Guid organizationId);
Task<DateTime> RestoreAsync(IEnumerable<Guid> ids, Guid userId);
Task<DateTime> RestoreByIdsOrganizationIdAsync(IEnumerable<Guid> ids, Guid organizationId);
Task DeleteDeletedAsync(DateTime deletedDateBefore);
}
2 changes: 1 addition & 1 deletion src/Core/Vault/Services/ICipherService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ Task ImportCiphersAsync(List<Collection> collections, List<CipherDetails> cipher
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<CipherDetails> ciphers, Guid restoringUserId);
Task<ICollection<CipherOrganizationDetails>> RestoreManyAsync(IEnumerable<Guid> cipherIds, Guid restoringUserId, Guid? organizationId = null, bool orgAdmin = false);
Task UploadFileForExistingAttachmentAsync(Stream stream, Cipher cipher, CipherAttachment.MetaData attachmentId);
Task<AttachmentResponseData> GetAttachmentDownloadDataAsync(Cipher cipher, string attachmentId);
Task<bool> ValidateCipherAttachmentFile(Cipher cipher, CipherAttachment.MetaData attachmentData);
Expand Down
25 changes: 21 additions & 4 deletions src/Core/Vault/Services/Implementations/CipherService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -849,13 +849,28 @@ public async Task RestoreAsync(Cipher cipher, Guid restoringUserId, bool orgAdmi
await _pushService.PushSyncCipherUpdateAsync(cipher, null);
}

public async Task RestoreManyAsync(IEnumerable<CipherDetails> ciphers, Guid restoringUserId)
public async Task<ICollection<CipherOrganizationDetails>> RestoreManyAsync(IEnumerable<Guid> cipherIds, Guid restoringUserId, Guid? organizationId = null, bool orgAdmin = false)
gbubemismith marked this conversation as resolved.
Show resolved Hide resolved
{
var revisionDate = await _cipherRepository.RestoreAsync(ciphers.Select(c => c.Id), restoringUserId);
var cipherIdsSet = new HashSet<Guid>(cipherIds);
var restoringCiphers = new List<CipherOrganizationDetails>();
DateTime? revisionDate;

var events = ciphers.Select(c =>
if (orgAdmin && organizationId.HasValue)
{
var ciphers = await _cipherRepository.GetManyOrganizationDetailsByOrganizationIdAsync(organizationId.Value);
restoringCiphers = ciphers.Where(c => cipherIdsSet.Contains(c.Id)).ToList();
revisionDate = await _cipherRepository.RestoreByIdsOrganizationIdAsync(restoringCiphers.Select(c => c.Id), organizationId.Value);
}
else
{
c.RevisionDate = revisionDate;
var ciphers = await _cipherRepository.GetManyByUserIdAsync(restoringUserId);
restoringCiphers = ciphers.Where(c => cipherIdsSet.Contains(c.Id) && c.Edit).Select(c => (CipherOrganizationDetails)c).ToList();
revisionDate = await _cipherRepository.RestoreAsync(restoringCiphers.Select(c => c.Id), restoringUserId);
}

var events = restoringCiphers.Select(c =>
{
c.RevisionDate = revisionDate.Value;
c.DeletedDate = null;
return new Tuple<Cipher, EventType, DateTime?>(c, EventType.Cipher_Restored, null);
});
Expand All @@ -866,6 +881,8 @@ public async Task RestoreManyAsync(IEnumerable<CipherDetails> ciphers, Guid rest

// push
await _pushService.PushSyncCiphersAsync(restoringUserId);

return restoringCiphers;
}

public async Task<(IEnumerable<CipherOrganizationDetails>, Dictionary<Guid, IGrouping<Guid, CollectionCipher>>)> GetOrganizationCiphers(Guid userId, Guid organizationId)
Expand Down
13 changes: 13 additions & 0 deletions src/Infrastructure.Dapper/Vault/Repositories/CipherRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -669,6 +669,19 @@ public async Task<DateTime> RestoreAsync(IEnumerable<Guid> ids, Guid userId)
}
}

public async Task<DateTime> RestoreByIdsOrganizationIdAsync(IEnumerable<Guid> ids, Guid organizationId)
{
using (var connection = new SqlConnection(ConnectionString))
{
var results = await connection.ExecuteScalarAsync<DateTime>(
$"[{Schema}].[Cipher_RestoreByIdsOrganizationId]",
new { Ids = ids.ToGuidIdArrayTVP(), OrganizationId = organizationId },
commandType: CommandType.StoredProcedure);

return results;
}
}

public async Task DeleteDeletedAsync(DateTime deletedDateBefore)
{
using (var connection = new SqlConnection(ConnectionString))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -625,6 +625,31 @@ public async Task<DateTime> RestoreAsync(IEnumerable<Guid> ids, Guid userId)
return await ToggleCipherStates(ids, userId, CipherStateAction.Restore);
}

public async Task<DateTime> RestoreByIdsOrganizationIdAsync(IEnumerable<Guid> ids, Guid organizationId)
{
using (var scope = ServiceScopeFactory.CreateScope())
{
var dbContext = GetDatabaseContext(scope);
var utcNow = DateTime.UtcNow;
var ciphers = from c in dbContext.Ciphers
where c.OrganizationId == organizationId &&
ids.Contains(c.Id)
select c;

await ciphers.ForEachAsync(cipher =>
{
dbContext.Attach(cipher);
cipher.DeletedDate = null;
cipher.RevisionDate = utcNow;
});

await OrganizationUpdateStorage(organizationId);
await dbContext.UserBumpAccountRevisionDateByOrganizationIdAsync(organizationId);
await dbContext.SaveChangesAsync();
return utcNow;
}
}

public async Task SoftDeleteAsync(IEnumerable<Guid> ids, Guid userId)
{
await ToggleCipherStates(ids, userId, CipherStateAction.SoftDelete);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
CREATE PROCEDURE [dbo].[Cipher_RestoreByIdsOrganizationId]
@Ids AS [dbo].[GuidIdArray] READONLY,
@OrganizationId AS UNIQUEIDENTIFIER
AS
BEGIN
SET NOCOUNT ON

IF (SELECT COUNT(1) FROM @Ids) < 1
BEGIN
RETURN(-1)
END

-- Delete ciphers
DECLARE @UtcNow DATETIME2(7) = GETUTCDATE();
UPDATE
[dbo].[Cipher]
SET
[DeletedDate] = NULL,
[RevisionDate] = @UtcNow
WHERE
[Id] IN (SELECT * FROM @Ids)
AND OrganizationId = @OrganizationId

-- Cleanup organization
EXEC [dbo].[Organization_UpdateStorage] @OrganizationId
EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationId] @OrganizationId

SELECT @UtcNow
END
10 changes: 6 additions & 4 deletions test/Core.Test/Vault/Services/CipherServiceTests.cs
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be good to add a test for the new parameters/behaviour as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test added!

Original file line number Diff line number Diff line change
Expand Up @@ -167,21 +167,23 @@ public async Task RestoreAsync_UpdatesOrganizationCipher(Guid restoringUserId, C

[Theory]
[BitAutoData]
public async Task RestoreManyAsync_UpdatesCiphers(IEnumerable<CipherDetails> ciphers,
public async Task RestoreManyAsync_UpdatesCiphers(ICollection<CipherDetails> ciphers,
SutProvider<CipherService> sutProvider)
{
var cipherIds = ciphers.Select(c => c.Id).ToArray();
var restoringUserId = ciphers.First().UserId.Value;
var previousRevisionDate = DateTime.UtcNow;
foreach (var cipher in ciphers)
{
cipher.Edit = true;
cipher.RevisionDate = previousRevisionDate;
}

sutProvider.GetDependency<ICipherRepository>().GetManyByUserIdAsync(restoringUserId).Returns(ciphers);
var revisionDate = previousRevisionDate + TimeSpan.FromMinutes(1);
sutProvider.GetDependency<ICipherRepository>().RestoreAsync(Arg.Any<IEnumerable<Guid>>(), restoringUserId)
.Returns(revisionDate);
sutProvider.GetDependency<ICipherRepository>().RestoreAsync(Arg.Any<IEnumerable<Guid>>(), restoringUserId).Returns(revisionDate);

await sutProvider.Sut.RestoreManyAsync(ciphers, restoringUserId);
await sutProvider.Sut.RestoreManyAsync(cipherIds, restoringUserId);

foreach (var cipher in ciphers)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
CREATE OR ALTER PROCEDURE [dbo].[Cipher_RestoreByIdsOrganizationId]
@Ids AS [dbo].[GuidIdArray] READONLY,
@OrganizationId AS UNIQUEIDENTIFIER
AS
BEGIN
SET NOCOUNT ON

IF (SELECT COUNT(1) FROM @Ids) < 1
BEGIN
RETURN(-1)
END

-- Delete ciphers
DECLARE @UtcNow DATETIME2(7) = GETUTCDATE();
UPDATE
[dbo].[Cipher]
SET
[DeletedDate] = NULL,
[RevisionDate] = @UtcNow
WHERE
[Id] IN (SELECT * FROM @Ids)
AND OrganizationId = @OrganizationId

-- Cleanup organization
EXEC [dbo].[Organization_UpdateStorage] @OrganizationId
EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationId] @OrganizationId

SELECT @UtcNow
END
GO
Loading