Skip to content

Commit f193587

Browse files
authorizations: optimize queries & cache data per request (DefectDojo#13989)
* authorizations: add test cases * authorizations: use subqueries instead of exists * fix metrics test * authorizations: cache results per requests if possible * fix metrics calleers * add upgrade note * Update performance test counts after merge Updated expected query and async task counts using update_performance_test_counts.py script. Most tests show improvements with slight reductions in queries/tasks. Product grading tests show small increases due to upstream changes in grading logic. All tests verified passing.
1 parent 7bfea40 commit f193587

24 files changed

Lines changed: 1897 additions & 705 deletions

File tree

docs/content/en/open_source/upgrading/2.55.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22
title: 'Upgrading to DefectDojo Version 2.55.x'
33
toc_hide: true
44
weight: -20260105
5-
description: No special instructions.
5+
description: Authorization related optimizations
66
---
7-
There are no special instructions for upgrading to 2.55.x. Check the [Release Notes](https://github.com/DefectDojo/django-DefectDojo/releases/tag/2.55.0) for the contents of the release.
7+
8+
## Authorization related optimizations
9+
10+
The queries related to authorizations have been optmized. For example retrieving the list of authorized findings for the logged in user.
11+
Some of these are now also cached during that duration of a request. This should have no functional effects and only results in better performance.
12+
13+
Check the [Release Notes](https://github.com/DefectDojo/django-DefectDojo/releases/tag/2.55.0) for the contents of the release.

dojo/cred/queries.py

Lines changed: 68 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,21 @@
11
from crum import get_current_user
2-
from django.db.models import Exists, OuterRef, Q
2+
from django.db.models import Q, Subquery
33

44
from dojo.authorization.authorization import get_roles_for_permission, user_has_global_permission
55
from dojo.models import Cred_Mapping, Product_Group, Product_Member, Product_Type_Group, Product_Type_Member
6+
from dojo.request_cache import cache_for_request
67

78

8-
def get_authorized_cred_mappings(permission, queryset=None):
9+
# Cached: all parameters are hashable, no dynamic queryset filtering
10+
@cache_for_request
11+
def get_authorized_cred_mappings(permission):
12+
"""Cached - returns all cred mappings the user is authorized to see."""
913
user = get_current_user()
1014

1115
if user is None:
1216
return Cred_Mapping.objects.none()
1317

14-
cred_mappings = Cred_Mapping.objects.all().order_by("id") if queryset is None else queryset
18+
cred_mappings = Cred_Mapping.objects.all().order_by("id")
1519

1620
if user.is_superuser:
1721
return cred_mappings
@@ -20,27 +24,69 @@ def get_authorized_cred_mappings(permission, queryset=None):
2024
return cred_mappings
2125

2226
roles = get_roles_for_permission(permission)
27+
28+
# Get authorized product/product_type IDs via subqueries
2329
authorized_product_type_roles = Product_Type_Member.objects.filter(
24-
product_type=OuterRef("product__prod_type_id"),
25-
user=user,
26-
role__in=roles)
30+
user=user, role__in=roles,
31+
).values("product_type_id")
32+
2733
authorized_product_roles = Product_Member.objects.filter(
28-
product=OuterRef("product_id"),
29-
user=user,
30-
role__in=roles)
34+
user=user, role__in=roles,
35+
).values("product_id")
36+
3137
authorized_product_type_groups = Product_Type_Group.objects.filter(
32-
product_type=OuterRef("product__prod_type_id"),
33-
group__users=user,
34-
role__in=roles)
38+
group__users=user, role__in=roles,
39+
).values("product_type_id")
40+
3541
authorized_product_groups = Product_Group.objects.filter(
36-
product=OuterRef("product_id"),
37-
group__users=user,
38-
role__in=roles)
39-
cred_mappings = cred_mappings.annotate(
40-
product__prod_type__member=Exists(authorized_product_type_roles),
41-
product__member=Exists(authorized_product_roles),
42-
product__prod_type__authorized_group=Exists(authorized_product_type_groups),
43-
product__authorized_group=Exists(authorized_product_groups))
42+
group__users=user, role__in=roles,
43+
).values("product_id")
44+
45+
# Filter using IN with Subquery - no annotations needed
4446
return cred_mappings.filter(
45-
Q(product__prod_type__member=True) | Q(product__member=True)
46-
| Q(product__prod_type__authorized_group=True) | Q(product__authorized_group=True))
47+
Q(product__prod_type_id__in=Subquery(authorized_product_type_roles))
48+
| Q(product_id__in=Subquery(authorized_product_roles))
49+
| Q(product__prod_type_id__in=Subquery(authorized_product_type_groups))
50+
| Q(product_id__in=Subquery(authorized_product_groups)),
51+
)
52+
53+
54+
def get_authorized_cred_mappings_for_queryset(permission, queryset):
55+
"""Filters a provided queryset for authorization. Not cached due to dynamic queryset parameter."""
56+
user = get_current_user()
57+
58+
if user is None:
59+
return Cred_Mapping.objects.none()
60+
61+
if user.is_superuser:
62+
return queryset
63+
64+
if user_has_global_permission(user, permission):
65+
return queryset
66+
67+
roles = get_roles_for_permission(permission)
68+
69+
# Get authorized product/product_type IDs via subqueries
70+
authorized_product_type_roles = Product_Type_Member.objects.filter(
71+
user=user, role__in=roles,
72+
).values("product_type_id")
73+
74+
authorized_product_roles = Product_Member.objects.filter(
75+
user=user, role__in=roles,
76+
).values("product_id")
77+
78+
authorized_product_type_groups = Product_Type_Group.objects.filter(
79+
group__users=user, role__in=roles,
80+
).values("product_type_id")
81+
82+
authorized_product_groups = Product_Group.objects.filter(
83+
group__users=user, role__in=roles,
84+
).values("product_id")
85+
86+
# Filter using IN with Subquery - no annotations needed
87+
return queryset.filter(
88+
Q(product__prod_type_id__in=Subquery(authorized_product_type_roles))
89+
| Q(product_id__in=Subquery(authorized_product_roles))
90+
| Q(product__prod_type_id__in=Subquery(authorized_product_type_groups))
91+
| Q(product_id__in=Subquery(authorized_product_groups)),
92+
)

dojo/cred/views.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
from dojo.authorization.authorization_decorators import user_is_authorized, user_is_configuration_authorized
1010
from dojo.authorization.roles_permissions import Permissions
11-
from dojo.cred.queries import get_authorized_cred_mappings
11+
from dojo.cred.queries import get_authorized_cred_mappings_for_queryset
1212
from dojo.forms import CredMappingForm, CredMappingFormProd, CredUserForm, NoteForm
1313
from dojo.models import Cred_Mapping, Cred_User, Engagement, Finding, Product, Test
1414
from dojo.utils import Product_Tab, add_breadcrumb, dojo_crypto_encrypt, prepare_for_view
@@ -85,7 +85,7 @@ def view_cred_details(request, ttid):
8585
notes = cred.notes.all()
8686
cred_products = Cred_Mapping.objects.select_related("product").filter(
8787
product_id__isnull=False, cred_id=ttid).order_by("product__name")
88-
cred_products = get_authorized_cred_mappings(Permissions.Product_View, cred_products)
88+
cred_products = get_authorized_cred_mappings_for_queryset(Permissions.Product_View, cred_products)
8989

9090
if request.method == "POST":
9191
form = NoteForm(request.POST)

dojo/endpoint/queries.py

Lines changed: 135 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from crum import get_current_user
2-
from django.db.models import Exists, OuterRef, Q
2+
from django.db.models import Q, Subquery
33

44
from dojo.authorization.authorization import get_roles_for_permission, user_has_global_permission
55
from dojo.models import (
@@ -10,17 +10,20 @@
1010
Product_Type_Group,
1111
Product_Type_Member,
1212
)
13+
from dojo.request_cache import cache_for_request
1314

1415

15-
def get_authorized_endpoints(permission, queryset=None, user=None):
16-
16+
# Cached: all parameters are hashable, no dynamic queryset filtering
17+
@cache_for_request
18+
def get_authorized_endpoints(permission, user=None):
19+
"""Cached - returns all endpoints the user is authorized to see."""
1720
if user is None:
1821
user = get_current_user()
1922

2023
if user is None:
2124
return Endpoint.objects.none()
2225

23-
endpoints = Endpoint.objects.all().order_by("id") if queryset is None else queryset
26+
endpoints = Endpoint.objects.all().order_by("id")
2427

2528
if user.is_superuser:
2629
return endpoints
@@ -29,41 +32,86 @@ def get_authorized_endpoints(permission, queryset=None, user=None):
2932
return endpoints
3033

3134
roles = get_roles_for_permission(permission)
35+
36+
# Get authorized product/product_type IDs via subqueries
3237
authorized_product_type_roles = Product_Type_Member.objects.filter(
33-
product_type=OuterRef("product__prod_type_id"),
34-
user=user,
35-
role__in=roles)
38+
user=user, role__in=roles,
39+
).values("product_type_id")
40+
3641
authorized_product_roles = Product_Member.objects.filter(
37-
product=OuterRef("product_id"),
38-
user=user,
39-
role__in=roles)
42+
user=user, role__in=roles,
43+
).values("product_id")
44+
4045
authorized_product_type_groups = Product_Type_Group.objects.filter(
41-
product_type=OuterRef("product__prod_type_id"),
42-
group__users=user,
43-
role__in=roles)
46+
group__users=user, role__in=roles,
47+
).values("product_type_id")
48+
4449
authorized_product_groups = Product_Group.objects.filter(
45-
product=OuterRef("product_id"),
46-
group__users=user,
47-
role__in=roles)
48-
endpoints = endpoints.annotate(
49-
product__prod_type__member=Exists(authorized_product_type_roles),
50-
product__member=Exists(authorized_product_roles),
51-
product__prod_type__authorized_group=Exists(authorized_product_type_groups),
52-
product__authorized_group=Exists(authorized_product_groups))
50+
group__users=user, role__in=roles,
51+
).values("product_id")
52+
53+
# Filter using IN with Subquery - no annotations needed
5354
return endpoints.filter(
54-
Q(product__prod_type__member=True) | Q(product__member=True)
55-
| Q(product__prod_type__authorized_group=True) | Q(product__authorized_group=True))
55+
Q(product__prod_type_id__in=Subquery(authorized_product_type_roles))
56+
| Q(product_id__in=Subquery(authorized_product_roles))
57+
| Q(product__prod_type_id__in=Subquery(authorized_product_type_groups))
58+
| Q(product_id__in=Subquery(authorized_product_groups)),
59+
)
5660

5761

58-
def get_authorized_endpoint_status(permission, queryset=None, user=None):
62+
def get_authorized_endpoints_for_queryset(permission, queryset, user=None):
63+
"""Filters a provided queryset for authorization. Not cached due to dynamic queryset parameter."""
64+
if user is None:
65+
user = get_current_user()
5966

67+
if user is None:
68+
return Endpoint.objects.none()
69+
70+
if user.is_superuser:
71+
return queryset
72+
73+
if user_has_global_permission(user, permission):
74+
return queryset
75+
76+
roles = get_roles_for_permission(permission)
77+
78+
# Get authorized product/product_type IDs via subqueries
79+
authorized_product_type_roles = Product_Type_Member.objects.filter(
80+
user=user, role__in=roles,
81+
).values("product_type_id")
82+
83+
authorized_product_roles = Product_Member.objects.filter(
84+
user=user, role__in=roles,
85+
).values("product_id")
86+
87+
authorized_product_type_groups = Product_Type_Group.objects.filter(
88+
group__users=user, role__in=roles,
89+
).values("product_type_id")
90+
91+
authorized_product_groups = Product_Group.objects.filter(
92+
group__users=user, role__in=roles,
93+
).values("product_id")
94+
95+
# Filter using IN with Subquery - no annotations needed
96+
return queryset.filter(
97+
Q(product__prod_type_id__in=Subquery(authorized_product_type_roles))
98+
| Q(product_id__in=Subquery(authorized_product_roles))
99+
| Q(product__prod_type_id__in=Subquery(authorized_product_type_groups))
100+
| Q(product_id__in=Subquery(authorized_product_groups)),
101+
)
102+
103+
104+
# Cached: all parameters are hashable, no dynamic queryset filtering
105+
@cache_for_request
106+
def get_authorized_endpoint_status(permission, user=None):
107+
"""Cached - returns all endpoint statuses the user is authorized to see."""
60108
if user is None:
61109
user = get_current_user()
62110

63111
if user is None:
64112
return Endpoint_Status.objects.none()
65113

66-
endpoint_status = Endpoint_Status.objects.all().order_by("id") if queryset is None else queryset
114+
endpoint_status = Endpoint_Status.objects.all().order_by("id")
67115

68116
if user.is_superuser:
69117
return endpoint_status
@@ -72,27 +120,70 @@ def get_authorized_endpoint_status(permission, queryset=None, user=None):
72120
return endpoint_status
73121

74122
roles = get_roles_for_permission(permission)
123+
124+
# Get authorized product/product_type IDs via subqueries
75125
authorized_product_type_roles = Product_Type_Member.objects.filter(
76-
product_type=OuterRef("endpoint__product__prod_type_id"),
77-
user=user,
78-
role__in=roles)
126+
user=user, role__in=roles,
127+
).values("product_type_id")
128+
79129
authorized_product_roles = Product_Member.objects.filter(
80-
product=OuterRef("endpoint__product_id"),
81-
user=user,
82-
role__in=roles)
130+
user=user, role__in=roles,
131+
).values("product_id")
132+
83133
authorized_product_type_groups = Product_Type_Group.objects.filter(
84-
product_type=OuterRef("endpoint__product__prod_type_id"),
85-
group__users=user,
86-
role__in=roles)
134+
group__users=user, role__in=roles,
135+
).values("product_type_id")
136+
87137
authorized_product_groups = Product_Group.objects.filter(
88-
product=OuterRef("endpoint__product_id"),
89-
group__users=user,
90-
role__in=roles)
91-
endpoint_status = endpoint_status.annotate(
92-
endpoint__product__prod_type__member=Exists(authorized_product_type_roles),
93-
endpoint__product__member=Exists(authorized_product_roles),
94-
endpoint__product__prod_type__authorized_group=Exists(authorized_product_type_groups),
95-
endpoint__product__authorized_group=Exists(authorized_product_groups))
138+
group__users=user, role__in=roles,
139+
).values("product_id")
140+
141+
# Filter using IN with Subquery - no annotations needed
96142
return endpoint_status.filter(
97-
Q(endpoint__product__prod_type__member=True) | Q(endpoint__product__member=True)
98-
| Q(endpoint__product__prod_type__authorized_group=True) | Q(endpoint__product__authorized_group=True))
143+
Q(endpoint__product__prod_type_id__in=Subquery(authorized_product_type_roles))
144+
| Q(endpoint__product_id__in=Subquery(authorized_product_roles))
145+
| Q(endpoint__product__prod_type_id__in=Subquery(authorized_product_type_groups))
146+
| Q(endpoint__product_id__in=Subquery(authorized_product_groups)),
147+
)
148+
149+
150+
def get_authorized_endpoint_status_for_queryset(permission, queryset, user=None):
151+
"""Filters a provided queryset for authorization. Not cached due to dynamic queryset parameter."""
152+
if user is None:
153+
user = get_current_user()
154+
155+
if user is None:
156+
return Endpoint_Status.objects.none()
157+
158+
if user.is_superuser:
159+
return queryset
160+
161+
if user_has_global_permission(user, permission):
162+
return queryset
163+
164+
roles = get_roles_for_permission(permission)
165+
166+
# Get authorized product/product_type IDs via subqueries
167+
authorized_product_type_roles = Product_Type_Member.objects.filter(
168+
user=user, role__in=roles,
169+
).values("product_type_id")
170+
171+
authorized_product_roles = Product_Member.objects.filter(
172+
user=user, role__in=roles,
173+
).values("product_id")
174+
175+
authorized_product_type_groups = Product_Type_Group.objects.filter(
176+
group__users=user, role__in=roles,
177+
).values("product_type_id")
178+
179+
authorized_product_groups = Product_Group.objects.filter(
180+
group__users=user, role__in=roles,
181+
).values("product_id")
182+
183+
# Filter using IN with Subquery - no annotations needed
184+
return queryset.filter(
185+
Q(endpoint__product__prod_type_id__in=Subquery(authorized_product_type_roles))
186+
| Q(endpoint__product_id__in=Subquery(authorized_product_roles))
187+
| Q(endpoint__product__prod_type_id__in=Subquery(authorized_product_type_groups))
188+
| Q(endpoint__product_id__in=Subquery(authorized_product_groups)),
189+
)

0 commit comments

Comments
 (0)