Skip to content

Commit 487d6cb

Browse files
feat(cache): cache authorizations in Celery tasks via cache_for_request_or_task
@cache_for_request is a no-op in Celery (no request), so per-user authorization filtering re-runs on every call inside a task -- notably the rules engine, which runs authorized querysets under impersonate(rule.owner). Add a request-or-task read-through cache and move the authorized-queryset/permission getters onto it. - dojo/request_cache: cache_for_request_or_task resolves the store as request cache -> task cache -> no caching. The task cache is a per-thread store active only inside a DojoAsyncTask (begin/end), so management commands / shells that never hit a task boundary do not cache. The key folds get_current_user().pk so results keyed on user=None (resolved downstream) cannot leak between users on a reused worker thread or within a task that impersonates multiple owners. - dojo/celery: DojoAsyncTask installs a fresh task cache before each task and drops it afterwards (finally), bounding staleness to a single task. - Migrate all get_authorized_* / authorized_*_id_set / get_*_permissions getters from @cache_for_request to @cache_for_request_or_task. - test_caching: cover task-scope caching, per-user isolation, and reset.
1 parent f344253 commit 487d6cb

16 files changed

Lines changed: 195 additions & 51 deletions

File tree

dojo/authorization/query_registrations.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
Tool_Product_Settings,
4141
Vulnerability_Id,
4242
)
43-
from dojo.request_cache import cache_for_request
43+
from dojo.request_cache import cache_for_request_or_task
4444

4545

4646
def _resolve_user(user):
@@ -81,7 +81,7 @@ def _authorized_product_type_ids(user):
8181
return Product_Type.objects.filter(authorized_users=user).values("id")
8282

8383

84-
@cache_for_request
84+
@cache_for_request_or_task
8585
def authorized_product_id_set(user_pk):
8686
"""
8787
Frozen set of product ids the user can access via authorized_users
@@ -101,7 +101,7 @@ def authorized_product_id_set(user_pk):
101101
)
102102

103103

104-
@cache_for_request
104+
@cache_for_request_or_task
105105
def authorized_product_type_id_set(user_pk):
106106
"""
107107
Frozen set of product_type ids the user is a direct member of via

dojo/authorization/template_filters.py

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

33
from dojo.authorization.authorization import user_has_configuration_permission as configuration_permission
44
from dojo.authorization.authorization import user_has_global_permission, user_has_permission
5-
from dojo.request_cache import cache_for_request
5+
from dojo.request_cache import cache_for_request_or_task
66

77

88
def has_object_permission(obj, permission):
@@ -21,7 +21,7 @@ def has_configuration_permission(permission, request):
2121
return configuration_permission(user, permission)
2222

2323

24-
@cache_for_request
24+
@cache_for_request_or_task
2525
def get_user_permissions(user):
2626
return user.user_permissions.all()
2727

@@ -31,7 +31,7 @@ def user_has_configuration_permission_without_group(user, codename):
3131
return any(permission.codename == codename for permission in permissions)
3232

3333

34-
@cache_for_request
34+
@cache_for_request_or_task
3535
def get_group_permissions(group):
3636
return group.permissions.all()
3737

dojo/celery.py

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -46,19 +46,26 @@ def __call__(self, *args, **kwargs):
4646
crum.impersonate in tests or request middleware) are not disrupted.
4747
"""
4848
from dojo.caching import reset_l1_cache # noqa: PLC0415
49+
from dojo.request_cache import begin_task_cache, end_task_cache # noqa: PLC0415
4950
reset_l1_cache()
50-
51-
if "async_user_id" not in kwargs:
52-
return super().__call__(*args, **kwargs)
53-
54-
import crum # noqa: PLC0415
55-
56-
from dojo.models import Dojo_User # noqa: PLC0415 circular import
57-
58-
user_id = kwargs.pop("async_user_id")
59-
user = Dojo_User.objects.filter(pk=user_id).first() if user_id else None
60-
with crum.impersonate(user):
61-
return super().__call__(*args, **kwargs)
51+
# Install a fresh task-scoped request-cache (begin) and drop it afterwards
52+
# (end), so cache_for_request_or_task can memoize within this task without a
53+
# value leaking to the next task on this reused worker thread.
54+
begin_task_cache()
55+
try:
56+
if "async_user_id" not in kwargs:
57+
return super().__call__(*args, **kwargs)
58+
59+
import crum # noqa: PLC0415
60+
61+
from dojo.models import Dojo_User # noqa: PLC0415 circular import
62+
63+
user_id = kwargs.pop("async_user_id")
64+
user = Dojo_User.objects.filter(pk=user_id).first() if user_id else None
65+
with crum.impersonate(user):
66+
return super().__call__(*args, **kwargs)
67+
finally:
68+
end_task_cache()
6269

6370
def apply_async(self, args=None, kwargs=None, **options):
6471
"""Override apply_async to inject user context and track tasks."""

dojo/endpoint/queries.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@ def get_auth_filter(key): return None
77
Endpoint,
88
Endpoint_Status,
99
)
10-
from dojo.request_cache import cache_for_request
10+
from dojo.request_cache import cache_for_request_or_task
1111

1212

1313
# Cached: all parameters are hashable, no dynamic queryset filtering
14-
@cache_for_request
14+
@cache_for_request_or_task
1515
def get_authorized_endpoints(permission, user=None):
1616
impl = get_auth_filter("endpoint.get_authorized_endpoints")
1717
if impl:
@@ -27,7 +27,7 @@ def get_authorized_endpoints_for_queryset(permission, queryset, user=None):
2727

2828

2929
# Cached: all parameters are hashable, no dynamic queryset filtering
30-
@cache_for_request
30+
@cache_for_request_or_task
3131
def get_authorized_endpoint_status(permission, user=None):
3232
impl = get_auth_filter("endpoint.get_authorized_endpoint_status")
3333
if impl:

dojo/engagement/queries.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@
44
def get_auth_filter(key): return None
55

66
from dojo.models import Engagement
7-
from dojo.request_cache import cache_for_request
7+
from dojo.request_cache import cache_for_request_or_task
88

99

1010
# Cached: all parameters are hashable, no dynamic queryset filtering
11-
@cache_for_request
11+
@cache_for_request_or_task
1212
def get_authorized_engagements(permission):
1313
impl = get_auth_filter("engagement.get_authorized_engagements")
1414
if impl:

dojo/finding/queries.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,13 @@ def get_auth_filter(key): return None
1919
Test_Import_Finding_Action,
2020
Vulnerability_Id,
2121
)
22-
from dojo.request_cache import cache_for_request
22+
from dojo.request_cache import cache_for_request_or_task
2323

2424
logger = logging.getLogger(__name__)
2525

2626

2727
# Cached: all parameters are hashable, no dynamic queryset filtering
28-
@cache_for_request
28+
@cache_for_request_or_task
2929
def get_authorized_findings(permission, user=None):
3030
impl = get_auth_filter("finding.get_authorized_findings")
3131
if impl:
@@ -41,7 +41,7 @@ def get_authorized_findings_for_queryset(permission, queryset, user=None):
4141

4242

4343
# Cached: all parameters are hashable, no dynamic queryset filtering
44-
@cache_for_request
44+
@cache_for_request_or_task
4545
def get_authorized_vulnerability_ids(permission, user=None):
4646
impl = get_auth_filter("finding.get_authorized_vulnerability_ids")
4747
if impl:

dojo/finding_group/queries.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@
44
def get_auth_filter(key): return None
55

66
from dojo.models import Finding_Group
7-
from dojo.request_cache import cache_for_request
7+
from dojo.request_cache import cache_for_request_or_task
88

99

1010
# Cached: all parameters are hashable, no dynamic queryset filtering
11-
@cache_for_request
11+
@cache_for_request_or_task
1212
def get_authorized_finding_groups(permission, user=None):
1313
impl = get_auth_filter("finding_group.get_authorized_finding_groups")
1414
if impl:

dojo/jira/queries.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@
44
def get_auth_filter(key): return None
55

66
from dojo.models import JIRA_Issue, JIRA_Project
7-
from dojo.request_cache import cache_for_request
7+
from dojo.request_cache import cache_for_request_or_task
88

99

1010
# Cached: all parameters are hashable, no dynamic queryset filtering
11-
@cache_for_request
11+
@cache_for_request_or_task
1212
def get_authorized_jira_projects(permission, user=None):
1313
impl = get_auth_filter("jira_link.get_authorized_jira_projects")
1414
if impl:
@@ -17,7 +17,7 @@ def get_authorized_jira_projects(permission, user=None):
1717

1818

1919
# Cached: all parameters are hashable, no dynamic queryset filtering
20-
@cache_for_request
20+
@cache_for_request_or_task
2121
def get_authorized_jira_issues(permission):
2222
impl = get_auth_filter("jira_link.get_authorized_jira_issues")
2323
if impl:

dojo/product/queries.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@ def get_auth_filter(key): return None
1111
Product,
1212
Product_API_Scan_Configuration,
1313
)
14-
from dojo.request_cache import cache_for_request
14+
from dojo.request_cache import cache_for_request_or_task
1515

1616

1717
# Cached: all parameters are hashable, no dynamic queryset filtering
18-
@cache_for_request
18+
@cache_for_request_or_task
1919
def get_authorized_products(permission, user=None):
2020
impl = get_auth_filter("product.get_authorized_products")
2121
if impl:
@@ -24,7 +24,7 @@ def get_authorized_products(permission, user=None):
2424

2525

2626
# Cached: all parameters are hashable, no dynamic queryset filtering
27-
@cache_for_request
27+
@cache_for_request_or_task
2828
def get_authorized_app_analysis(permission):
2929
impl = get_auth_filter("product.get_authorized_app_analysis")
3030
if impl:
@@ -33,7 +33,7 @@ def get_authorized_app_analysis(permission):
3333

3434

3535
# Cached: all parameters are hashable, no dynamic queryset filtering
36-
@cache_for_request
36+
@cache_for_request_or_task
3737
def get_authorized_dojo_meta(permission):
3838
impl = get_auth_filter("product.get_authorized_dojo_meta")
3939
if impl:
@@ -42,7 +42,7 @@ def get_authorized_dojo_meta(permission):
4242

4343

4444
# Cached: all parameters are hashable, no dynamic queryset filtering
45-
@cache_for_request
45+
@cache_for_request_or_task
4646
def get_authorized_languages(permission):
4747
impl = get_auth_filter("product.get_authorized_languages")
4848
if impl:
@@ -51,7 +51,7 @@ def get_authorized_languages(permission):
5151

5252

5353
# Cached: all parameters are hashable, no dynamic queryset filtering
54-
@cache_for_request
54+
@cache_for_request_or_task
5555
def get_authorized_engagement_presets(permission):
5656
impl = get_auth_filter("product.get_authorized_engagement_presets")
5757
if impl:
@@ -60,7 +60,7 @@ def get_authorized_engagement_presets(permission):
6060

6161

6262
# Cached: all parameters are hashable, no dynamic queryset filtering
63-
@cache_for_request
63+
@cache_for_request_or_task
6464
def get_authorized_product_api_scan_configurations(permission):
6565
impl = get_auth_filter("product.get_authorized_product_api_scan_configurations")
6666
if impl:

dojo/product_type/queries.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@
44
def get_auth_filter(key): return None
55

66
from dojo.models import Product_Type
7-
from dojo.request_cache import cache_for_request
7+
from dojo.request_cache import cache_for_request_or_task
88

99

1010
# Cached: all parameters are hashable, no dynamic queryset filtering
11-
@cache_for_request
11+
@cache_for_request_or_task
1212
def get_authorized_product_types(permission):
1313
impl = get_auth_filter("product_type.get_authorized_product_types")
1414
if impl:

0 commit comments

Comments
 (0)