Skip to content

Commit 087c691

Browse files
when ciphers are soft deleted, complete any associated security tasks (#6492)
1 parent a1be1ae commit 087c691

File tree

8 files changed

+240
-0
lines changed

8 files changed

+240
-0
lines changed

src/Core/Vault/Repositories/ISecurityTaskRepository.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,10 @@ public interface ISecurityTaskRepository : IRepository<SecurityTask, Guid>
3535
/// <param name="organizationId">The id of the organization</param>
3636
/// <returns>A collection of security task metrics</returns>
3737
Task<SecurityTaskMetrics> GetTaskMetricsAsync(Guid organizationId);
38+
39+
/// <summary>
40+
/// Marks all tasks associated with the respective ciphers as complete.
41+
/// </summary>
42+
/// <param name="cipherIds">Collection of cipher IDs</param>
43+
Task MarkAsCompleteByCipherIds(IEnumerable<Guid> cipherIds);
3844
}

src/Core/Vault/Services/Implementations/CipherService.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ public class CipherService : ICipherService
3333
private readonly IOrganizationRepository _organizationRepository;
3434
private readonly IOrganizationUserRepository _organizationUserRepository;
3535
private readonly ICollectionCipherRepository _collectionCipherRepository;
36+
private readonly ISecurityTaskRepository _securityTaskRepository;
3637
private readonly IPushNotificationService _pushService;
3738
private readonly IAttachmentStorageService _attachmentStorageService;
3839
private readonly IEventService _eventService;
@@ -53,6 +54,7 @@ public CipherService(
5354
IOrganizationRepository organizationRepository,
5455
IOrganizationUserRepository organizationUserRepository,
5556
ICollectionCipherRepository collectionCipherRepository,
57+
ISecurityTaskRepository securityTaskRepository,
5658
IPushNotificationService pushService,
5759
IAttachmentStorageService attachmentStorageService,
5860
IEventService eventService,
@@ -71,6 +73,7 @@ public CipherService(
7173
_organizationRepository = organizationRepository;
7274
_organizationUserRepository = organizationUserRepository;
7375
_collectionCipherRepository = collectionCipherRepository;
76+
_securityTaskRepository = securityTaskRepository;
7477
_pushService = pushService;
7578
_attachmentStorageService = attachmentStorageService;
7679
_eventService = eventService;
@@ -724,6 +727,7 @@ public async Task SoftDeleteAsync(CipherDetails cipherDetails, Guid deletingUser
724727
cipherDetails.ArchivedDate = null;
725728
}
726729

730+
await _securityTaskRepository.MarkAsCompleteByCipherIds([cipherDetails.Id]);
727731
await _cipherRepository.UpsertAsync(cipherDetails);
728732
await _eventService.LogCipherEventAsync(cipherDetails, EventType.Cipher_SoftDeleted);
729733

@@ -750,6 +754,8 @@ public async Task SoftDeleteManyAsync(IEnumerable<Guid> cipherIds, Guid deleting
750754
await _cipherRepository.SoftDeleteAsync(deletingCiphers.Select(c => c.Id), deletingUserId);
751755
}
752756

757+
await _securityTaskRepository.MarkAsCompleteByCipherIds(deletingCiphers.Select(c => c.Id));
758+
753759
var events = deletingCiphers.Select(c =>
754760
new Tuple<Cipher, EventType, DateTime?>(c, EventType.Cipher_SoftDeleted, null));
755761
foreach (var eventsBatch in events.Chunk(100))

src/Infrastructure.Dapper/Vault/Repositories/SecurityTaskRepository.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,4 +85,19 @@ await connection.ExecuteAsync(
8585

8686
return tasksList;
8787
}
88+
89+
/// <inheritdoc />
90+
public async Task MarkAsCompleteByCipherIds(IEnumerable<Guid> cipherIds)
91+
{
92+
if (!cipherIds.Any())
93+
{
94+
return;
95+
}
96+
97+
await using var connection = new SqlConnection(ConnectionString);
98+
await connection.ExecuteAsync(
99+
$"[{Schema}].[SecurityTask_MarkCompleteByCipherIds]",
100+
new { CipherIds = cipherIds.ToGuidIdArrayTVP() },
101+
commandType: CommandType.StoredProcedure);
102+
}
88103
}

src/Infrastructure.EntityFramework/Vault/Repositories/SecurityTaskRepository.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,4 +96,24 @@ join o in dbContext.Organizations on st.OrganizationId equals o.Id
9696

9797
return metrics ?? new Core.Vault.Entities.SecurityTaskMetrics(0, 0);
9898
}
99+
100+
/// <inheritdoc />
101+
public async Task MarkAsCompleteByCipherIds(IEnumerable<Guid> cipherIds)
102+
{
103+
if (!cipherIds.Any())
104+
{
105+
return;
106+
}
107+
108+
using var scope = ServiceScopeFactory.CreateScope();
109+
var dbContext = GetDatabaseContext(scope);
110+
111+
var cipherIdsList = cipherIds.ToList();
112+
113+
await dbContext.SecurityTasks
114+
.Where(st => st.CipherId.HasValue && cipherIdsList.Contains(st.CipherId.Value) && st.Status != SecurityTaskStatus.Completed)
115+
.ExecuteUpdateAsync(st => st
116+
.SetProperty(s => s.Status, SecurityTaskStatus.Completed)
117+
.SetProperty(s => s.RevisionDate, DateTime.UtcNow));
118+
}
99119
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
CREATE PROCEDURE [dbo].[SecurityTask_MarkCompleteByCipherIds]
2+
@CipherIds AS [dbo].[GuidIdArray] READONLY
3+
AS
4+
BEGIN
5+
SET NOCOUNT ON
6+
7+
UPDATE
8+
[dbo].[SecurityTask]
9+
SET
10+
[Status] = 1, -- completed
11+
[RevisionDate] = SYSUTCDATETIME()
12+
WHERE
13+
[CipherId] IN (SELECT [Id] FROM @CipherIds)
14+
AND [Status] <> 1 -- Not already completed
15+
END

test/Core.Test/Vault/Services/CipherServiceTests.cs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2286,6 +2286,63 @@ await sutProvider.GetDependency<IPushNotificationService>()
22862286
.PushSyncCiphersAsync(deletingUserId);
22872287
}
22882288

2289+
[Theory]
2290+
[BitAutoData]
2291+
public async Task SoftDeleteAsync_CallsMarkAsCompleteByCipherIds(
2292+
Guid deletingUserId, CipherDetails cipherDetails, SutProvider<CipherService> sutProvider)
2293+
{
2294+
cipherDetails.UserId = deletingUserId;
2295+
cipherDetails.OrganizationId = null;
2296+
cipherDetails.DeletedDate = null;
2297+
2298+
sutProvider.GetDependency<IUserService>()
2299+
.GetUserByIdAsync(deletingUserId)
2300+
.Returns(new User
2301+
{
2302+
Id = deletingUserId,
2303+
});
2304+
2305+
await sutProvider.Sut.SoftDeleteAsync(cipherDetails, deletingUserId);
2306+
2307+
await sutProvider.GetDependency<ISecurityTaskRepository>()
2308+
.Received(1)
2309+
.MarkAsCompleteByCipherIds(Arg.Is<IEnumerable<Guid>>(ids =>
2310+
ids.Count() == 1 && ids.First() == cipherDetails.Id));
2311+
}
2312+
2313+
[Theory]
2314+
[BitAutoData]
2315+
public async Task SoftDeleteManyAsync_CallsMarkAsCompleteByCipherIds(
2316+
Guid deletingUserId, List<CipherDetails> ciphers, SutProvider<CipherService> sutProvider)
2317+
{
2318+
var cipherIds = ciphers.Select(c => c.Id).ToArray();
2319+
2320+
foreach (var cipher in ciphers)
2321+
{
2322+
cipher.UserId = deletingUserId;
2323+
cipher.OrganizationId = null;
2324+
cipher.Edit = true;
2325+
cipher.DeletedDate = null;
2326+
}
2327+
2328+
sutProvider.GetDependency<IUserService>()
2329+
.GetUserByIdAsync(deletingUserId)
2330+
.Returns(new User
2331+
{
2332+
Id = deletingUserId,
2333+
});
2334+
sutProvider.GetDependency<ICipherRepository>()
2335+
.GetManyByUserIdAsync(deletingUserId)
2336+
.Returns(ciphers);
2337+
2338+
await sutProvider.Sut.SoftDeleteManyAsync(cipherIds, deletingUserId, null, false);
2339+
2340+
await sutProvider.GetDependency<ISecurityTaskRepository>()
2341+
.Received(1)
2342+
.MarkAsCompleteByCipherIds(Arg.Is<IEnumerable<Guid>>(ids =>
2343+
ids.Count() == cipherIds.Length && ids.All(id => cipherIds.Contains(id))));
2344+
}
2345+
22892346
private async Task AssertNoActionsAsync(SutProvider<CipherService> sutProvider)
22902347
{
22912348
await sutProvider.GetDependency<ICipherRepository>().DidNotReceiveWithAnyArgs().GetManyOrganizationDetailsByOrganizationIdAsync(default);

test/Infrastructure.IntegrationTest/Vault/Repositories/SecurityTaskRepositoryTests.cs

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,4 +345,110 @@ public async Task GetZeroTaskMetricsAsync(
345345
Assert.Equal(0, metrics.CompletedTasks);
346346
Assert.Equal(0, metrics.TotalTasks);
347347
}
348+
349+
[DatabaseTheory, DatabaseData]
350+
public async Task MarkAsCompleteByCipherIds_MarksPendingTasksAsCompleted(
351+
IOrganizationRepository organizationRepository,
352+
ICipherRepository cipherRepository,
353+
ISecurityTaskRepository securityTaskRepository)
354+
{
355+
var organization = await organizationRepository.CreateAsync(new Organization
356+
{
357+
Name = "Test Org",
358+
PlanType = PlanType.EnterpriseAnnually,
359+
Plan = "Test Plan",
360+
BillingEmail = "billing@email.com"
361+
});
362+
363+
var cipher1 = await cipherRepository.CreateAsync(new Cipher
364+
{
365+
Type = CipherType.Login,
366+
OrganizationId = organization.Id,
367+
Data = "",
368+
});
369+
370+
var cipher2 = await cipherRepository.CreateAsync(new Cipher
371+
{
372+
Type = CipherType.Login,
373+
OrganizationId = organization.Id,
374+
Data = "",
375+
});
376+
377+
var task1 = await securityTaskRepository.CreateAsync(new SecurityTask
378+
{
379+
OrganizationId = organization.Id,
380+
CipherId = cipher1.Id,
381+
Status = SecurityTaskStatus.Pending,
382+
Type = SecurityTaskType.UpdateAtRiskCredential,
383+
});
384+
385+
var task2 = await securityTaskRepository.CreateAsync(new SecurityTask
386+
{
387+
OrganizationId = organization.Id,
388+
CipherId = cipher2.Id,
389+
Status = SecurityTaskStatus.Pending,
390+
Type = SecurityTaskType.UpdateAtRiskCredential,
391+
});
392+
393+
await securityTaskRepository.MarkAsCompleteByCipherIds([cipher1.Id, cipher2.Id]);
394+
395+
var updatedTask1 = await securityTaskRepository.GetByIdAsync(task1.Id);
396+
var updatedTask2 = await securityTaskRepository.GetByIdAsync(task2.Id);
397+
398+
Assert.Equal(SecurityTaskStatus.Completed, updatedTask1.Status);
399+
Assert.Equal(SecurityTaskStatus.Completed, updatedTask2.Status);
400+
}
401+
402+
[DatabaseTheory, DatabaseData]
403+
public async Task MarkAsCompleteByCipherIds_OnlyUpdatesSpecifiedCiphers(
404+
IOrganizationRepository organizationRepository,
405+
ICipherRepository cipherRepository,
406+
ISecurityTaskRepository securityTaskRepository)
407+
{
408+
var organization = await organizationRepository.CreateAsync(new Organization
409+
{
410+
Name = "Test Org",
411+
PlanType = PlanType.EnterpriseAnnually,
412+
Plan = "Test Plan",
413+
BillingEmail = "billing@email.com"
414+
});
415+
416+
var cipher1 = await cipherRepository.CreateAsync(new Cipher
417+
{
418+
Type = CipherType.Login,
419+
OrganizationId = organization.Id,
420+
Data = "",
421+
});
422+
423+
var cipher2 = await cipherRepository.CreateAsync(new Cipher
424+
{
425+
Type = CipherType.Login,
426+
OrganizationId = organization.Id,
427+
Data = "",
428+
});
429+
430+
var taskToUpdate = await securityTaskRepository.CreateAsync(new SecurityTask
431+
{
432+
OrganizationId = organization.Id,
433+
CipherId = cipher1.Id,
434+
Status = SecurityTaskStatus.Pending,
435+
Type = SecurityTaskType.UpdateAtRiskCredential,
436+
});
437+
438+
var taskToKeep = await securityTaskRepository.CreateAsync(new SecurityTask
439+
{
440+
OrganizationId = organization.Id,
441+
CipherId = cipher2.Id,
442+
Status = SecurityTaskStatus.Pending,
443+
Type = SecurityTaskType.UpdateAtRiskCredential,
444+
});
445+
446+
await securityTaskRepository.MarkAsCompleteByCipherIds([cipher1.Id]);
447+
448+
var updatedTask = await securityTaskRepository.GetByIdAsync(taskToUpdate.Id);
449+
var unchangedTask = await securityTaskRepository.GetByIdAsync(taskToKeep.Id);
450+
451+
Assert.Equal(SecurityTaskStatus.Completed, updatedTask.Status);
452+
Assert.Equal(SecurityTaskStatus.Pending, unchangedTask.Status);
453+
}
348454
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
CREATE OR ALTER PROCEDURE [dbo].[SecurityTask_MarkCompleteByCipherIds]
2+
@CipherIds AS [dbo].[GuidIdArray] READONLY
3+
AS
4+
BEGIN
5+
SET NOCOUNT ON
6+
7+
UPDATE
8+
[dbo].[SecurityTask]
9+
SET
10+
[Status] = 1, -- Completed
11+
[RevisionDate] = SYSUTCDATETIME()
12+
WHERE
13+
[CipherId] IN (SELECT [Id] FROM @CipherIds)
14+
AND [Status] <> 1 -- Not already completed
15+
END

0 commit comments

Comments
 (0)