Skip to content

Commit 3dab9ad

Browse files
Async Delete: Handle exceptions for duplicate requests (#12867)
* Async Delete: Handle exceptions for duplicate requests * correct ruff errors * Apply suggestions from code review Co-authored-by: Blake Owens <76979297+blakeaowens@users.noreply.github.com> --------- Co-authored-by: Blake Owens <76979297+blakeaowens@users.noreply.github.com>
1 parent f28fbfe commit 3dab9ad

7 files changed

Lines changed: 93 additions & 73 deletions

File tree

dojo/engagement/signals.py

Lines changed: 24 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import contextlib
2+
13
from auditlog.models import LogEntry
24
from django.conf import settings
35
from django.contrib.contenttypes.models import ContentType
@@ -6,7 +8,7 @@
68
from django.urls import reverse
79
from django.utils.translation import gettext as _
810

9-
from dojo.models import Engagement
11+
from dojo.models import Engagement, Product
1012
from dojo.notes.helper import delete_related_notes
1113
from dojo.notifications.helper import create_notification
1214

@@ -38,25 +40,28 @@ def engagement_pre_save(sender, instance, **kwargs):
3840

3941
@receiver(post_delete, sender=Engagement)
4042
def engagement_post_delete(sender, instance, using, origin, **kwargs):
41-
if instance == origin:
42-
description = _('The engagement "%(name)s" was deleted') % {"name": instance.name}
43-
if settings.ENABLE_AUDITLOG:
44-
if le := LogEntry.objects.filter(
45-
action=LogEntry.Action.DELETE,
46-
content_type=ContentType.objects.get(app_label="dojo", model="engagement"),
47-
object_id=instance.id,
48-
).order_by("-id").first():
49-
description = _('The engagement "%(name)s" was deleted by %(user)s') % {
50-
"name": instance.name, "user": le.actor}
51-
create_notification(event="engagement_deleted", # template does not exists, it will default to "other" but this event name needs to stay because of unit testing
52-
title=_("Deletion of %(name)s") % {"name": instance.name},
53-
description=description,
54-
product=instance.product,
55-
url=reverse("view_product", args=(instance.product.id, )),
56-
recipients=[instance.lead],
57-
icon="exclamation-triangle")
43+
# Catch instances in async delete where a single object is deleted more than once
44+
with contextlib.suppress(sender.DoesNotExist, Product.DoesNotExist):
45+
if instance == origin:
46+
description = _('The engagement "%(name)s" was deleted') % {"name": instance.name}
47+
if settings.ENABLE_AUDITLOG:
48+
if le := LogEntry.objects.filter(
49+
action=LogEntry.Action.DELETE,
50+
content_type=ContentType.objects.get(app_label="dojo", model="engagement"),
51+
object_id=instance.id,
52+
).order_by("-id").first():
53+
description = _('The engagement "%(name)s" was deleted by %(user)s') % {
54+
"name": instance.name, "user": le.actor}
55+
create_notification(event="engagement_deleted", # template does not exists, it will default to "other" but this event name needs to stay because of unit testing
56+
title=_("Deletion of %(name)s") % {"name": instance.name},
57+
description=description,
58+
product=instance.product,
59+
url=reverse("view_product", args=(instance.product.id, )),
60+
recipients=[instance.lead],
61+
icon="exclamation-triangle")
5862

5963

6064
@receiver(pre_delete, sender=Engagement)
6165
def engagement_pre_delete(sender, instance, **kwargs):
62-
delete_related_notes(instance)
66+
with contextlib.suppress(sender.DoesNotExist):
67+
delete_related_notes(instance)

dojo/finding/helper.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import logging
2+
from contextlib import suppress
23
from time import strftime
34

45
from django.conf import settings
@@ -446,8 +447,10 @@ def finding_delete(instance, **kwargs):
446447

447448
@receiver(post_delete, sender=Finding)
448449
def finding_post_delete(sender, instance, **kwargs):
449-
logger.debug("finding post_delete, sender: %s instance: %s", to_str_typed(sender), to_str_typed(instance))
450-
# calculate_grade(instance.test.engagement.product)
450+
# Catch instances in async delete where a single object is deleted more than once
451+
with suppress(Finding.DoesNotExist):
452+
logger.debug("finding post_delete, sender: %s instance: %s", to_str_typed(sender), to_str_typed(instance))
453+
# calculate_grade(instance.test.engagement.product)
451454

452455

453456
def reset_duplicate_before_delete(dupe):

dojo/models.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1631,7 +1631,7 @@ def delete(self, *args, **kwargs):
16311631
from dojo.finding import helper
16321632
helper.prepare_duplicates_for_delete(engagement=self)
16331633
super().delete(*args, **kwargs)
1634-
with suppress(Product.DoesNotExist):
1634+
with suppress(Engagement.DoesNotExist, Product.DoesNotExist):
16351635
# Suppressing a potential issue created from async delete removing
16361636
# related objects in a separate task
16371637
calculate_grade(self.product)
@@ -2239,7 +2239,7 @@ def hash_code_allows_null_cwe(self):
22392239
def delete(self, *args, **kwargs):
22402240
logger.debug("%d test delete", self.id)
22412241
super().delete(*args, **kwargs)
2242-
with suppress(Engagement.DoesNotExist, Product.DoesNotExist):
2242+
with suppress(Test.DoesNotExist, Engagement.DoesNotExist, Product.DoesNotExist):
22432243
# Suppressing a potential issue created from async delete removing
22442244
# related objects in a separate task
22452245
calculate_grade(self.engagement.product)
@@ -2821,7 +2821,7 @@ def delete(self, *args, **kwargs):
28212821
from dojo.finding import helper
28222822
helper.finding_delete(self)
28232823
super().delete(*args, **kwargs)
2824-
with suppress(Test.DoesNotExist, Engagement.DoesNotExist, Product.DoesNotExist):
2824+
with suppress(Finding.DoesNotExist, Test.DoesNotExist, Engagement.DoesNotExist, Product.DoesNotExist):
28252825
# Suppressing a potential issue created from async delete removing
28262826
# related objects in a separate task
28272827
calculate_grade(self.test.engagement.product)

dojo/product/signals.py

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import contextlib
2+
13
from auditlog.models import LogEntry
24
from django.conf import settings
35
from django.contrib.contenttypes.models import ContentType
@@ -23,17 +25,19 @@ def product_post_save(sender, instance, created, **kwargs):
2325

2426
@receiver(post_delete, sender=Product)
2527
def product_post_delete(sender, instance, **kwargs):
26-
description = _('The product "%(name)s" was deleted') % {"name": instance.name}
27-
if settings.ENABLE_AUDITLOG:
28-
if le := LogEntry.objects.filter(
29-
action=LogEntry.Action.DELETE,
30-
content_type=ContentType.objects.get(app_label="dojo", model="product"),
31-
object_id=instance.id,
32-
).order_by("-id").first():
33-
description = _('The product "%(name)s" was deleted by %(user)s') % {
34-
"name": instance.name, "user": le.actor}
35-
create_notification(event="product_deleted", # template does not exists, it will default to "other" but this event name needs to stay because of unit testing
36-
title=_("Deletion of %(name)s") % {"name": instance.name},
37-
description=description,
38-
url=reverse("product"),
39-
icon="exclamation-triangle")
28+
# Catch instances in async delete where a single object is deleted more than once
29+
with contextlib.suppress(sender.DoesNotExist):
30+
description = _('The product "%(name)s" was deleted') % {"name": instance.name}
31+
if settings.ENABLE_AUDITLOG:
32+
if le := LogEntry.objects.filter(
33+
action=LogEntry.Action.DELETE,
34+
content_type=ContentType.objects.get(app_label="dojo", model="product"),
35+
object_id=instance.id,
36+
).order_by("-id").first():
37+
description = _('The product "%(name)s" was deleted by %(user)s') % {
38+
"name": instance.name, "user": le.actor}
39+
create_notification(event="product_deleted", # template does not exists, it will default to "other" but this event name needs to stay because of unit testing
40+
title=_("Deletion of %(name)s") % {"name": instance.name},
41+
description=description,
42+
url=reverse("product"),
43+
icon="exclamation-triangle")

dojo/product_type/signals.py

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import contextlib
2+
13
from auditlog.models import LogEntry
24
from django.conf import settings
35
from django.contrib.contenttypes.models import ContentType
@@ -23,18 +25,20 @@ def product_type_post_save(sender, instance, created, **kwargs):
2325

2426
@receiver(post_delete, sender=Product_Type)
2527
def product_type_post_delete(sender, instance, **kwargs):
26-
description = _('The product type "%(name)s" was deleted') % {"name": instance.name}
27-
if settings.ENABLE_AUDITLOG:
28-
if le := LogEntry.objects.filter(
29-
action=LogEntry.Action.DELETE,
30-
content_type=ContentType.objects.get(app_label="dojo", model="product_type"),
31-
object_id=instance.id,
32-
).order_by("-id").first():
33-
description = _('The product type "%(name)s" was deleted by %(user)s') % {
34-
"name": instance.name, "user": le.actor}
35-
create_notification(event="product_type_deleted", # template does not exists, it will default to "other" but this event name needs to stay because of unit testing
36-
title=_("Deletion of %(name)s") % {"name": instance.name},
37-
description=description,
38-
no_users=True,
39-
url=reverse("product_type"),
40-
icon="exclamation-triangle")
28+
# Catch instances in async delete where a single object is deleted more than once
29+
with contextlib.suppress(sender.DoesNotExist):
30+
description = _('The product type "%(name)s" was deleted') % {"name": instance.name}
31+
if settings.ENABLE_AUDITLOG:
32+
if le := LogEntry.objects.filter(
33+
action=LogEntry.Action.DELETE,
34+
content_type=ContentType.objects.get(app_label="dojo", model="product_type"),
35+
object_id=instance.id,
36+
).order_by("-id").first():
37+
description = _('The product type "%(name)s" was deleted by %(user)s') % {
38+
"name": instance.name, "user": le.actor}
39+
create_notification(event="product_type_deleted", # template does not exists, it will default to "other" but this event name needs to stay because of unit testing
40+
title=_("Deletion of %(name)s") % {"name": instance.name},
41+
description=description,
42+
no_users=True,
43+
url=reverse("product_type"),
44+
icon="exclamation-triangle")

dojo/test/signals.py

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,30 +8,32 @@
88
from django.urls import reverse
99
from django.utils.translation import gettext as _
1010

11-
from dojo.models import Finding, Test
11+
from dojo.models import Engagement, Finding, Product, Test
1212
from dojo.notes.helper import delete_related_notes
1313
from dojo.notifications.helper import create_notification
1414

1515

1616
@receiver(post_delete, sender=Test)
1717
def test_post_delete(sender, instance, using, origin, **kwargs):
18-
if instance == origin:
19-
description = _('The test "%(name)s" was deleted') % {"name": str(instance)}
20-
if settings.ENABLE_AUDITLOG:
21-
if le := LogEntry.objects.filter(
22-
action=LogEntry.Action.DELETE,
23-
content_type=ContentType.objects.get(app_label="dojo", model="test"),
24-
object_id=instance.id,
25-
).order_by("-id").first():
26-
description = _('The test "%(name)s" was deleted by %(user)s') % {
27-
"name": str(instance), "user": le.actor}
28-
create_notification(event="test_deleted", # template does not exists, it will default to "other" but this event name needs to stay because of unit testing
29-
title=_("Deletion of %(name)s") % {"name": str(instance)},
30-
description=description,
31-
product=instance.engagement.product,
32-
url=reverse("view_engagement", args=(instance.engagement.id, )),
33-
recipients=[instance.engagement.lead],
34-
icon="exclamation-triangle")
18+
# Catch instances in async delete where a single object is deleted more than once
19+
with contextlib.suppress(sender.DoesNotExist, Engagement.DoesNotExist, Product.DoesNotExist):
20+
if instance == origin:
21+
description = _('The test "%(name)s" was deleted') % {"name": str(instance)}
22+
if settings.ENABLE_AUDITLOG:
23+
if le := LogEntry.objects.filter(
24+
action=LogEntry.Action.DELETE,
25+
content_type=ContentType.objects.get(app_label="dojo", model="test"),
26+
object_id=instance.id,
27+
).order_by("-id").first():
28+
description = _('The test "%(name)s" was deleted by %(user)s') % {
29+
"name": str(instance), "user": le.actor}
30+
create_notification(event="test_deleted", # Template does not exist, it will default to "other" but this event name needs to stay because of unit testing
31+
title=_("Deletion of %(name)s") % {"name": str(instance)},
32+
description=description,
33+
product=instance.engagement.product,
34+
url=reverse("view_engagement", args=(instance.engagement.id, )),
35+
recipients=[instance.engagement.lead],
36+
icon="exclamation-triangle")
3537

3638

3739
@receiver(pre_save, sender=Test)
@@ -53,4 +55,5 @@ def update_found_by_for_findings(sender, instance, **kwargs):
5355

5456
@receiver(pre_delete, sender=Test)
5557
def test_pre_delete(sender, instance, **kwargs):
56-
delete_related_notes(instance)
58+
with contextlib.suppress(sender.DoesNotExist):
59+
delete_related_notes(instance)

dojo/utils.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2367,7 +2367,8 @@ def crawl(self, obj, model_list, **kwargs):
23672367
model = model_info[0]
23682368
model_query = model_info[1]
23692369
filter_dict = {model_query: obj}
2370-
objects_to_delete = model.objects.filter(**filter_dict)
2370+
# Only fetch the IDs since we will make a list of IDs in the following function call
2371+
objects_to_delete = model.objects.only("id").filter(**filter_dict)
23712372
logger.debug("ASYNC_DELETE: Deleting " + str(len(objects_to_delete)) + " " + self.get_object_name(model) + "s in chunks")
23722373
chunks = self.chunk_list(model, objects_to_delete)
23732374
for chunk in chunks:

0 commit comments

Comments
 (0)