DefectDojo / django-DefectDojo

DevSecOps, ASPM, Vulnerability Management. All on one platform.
https://defectdojo.com
BSD 3-Clause "New" or "Revised" License
3.72k stars 1.56k forks source link

API Authentication fails after restart/redeployment #6468

Open 0x4d4e opened 2 years ago

0x4d4e commented 2 years ago

Bug description

We are running DefectDojo on ECS Fargate using a template that is based on the included docker-compose file. After resuming the service after it was paused due to inactivity on our test environments or after redeploying the service (e.g. due to a new version) accessing DefectDojo via the API/access key fails with the exception included below.

The exception message points at an issue with the request caching?

API access works again after a user authenticates via the web interface.

Steps to reproduce

  1. Resume/Redeploy the service (i.e. all included containers)
  2. Attempt to access the DefectDojo API via the previously generated (and working) API key
  3. Error

Expected behavior Accessing the API via the API key should immediately work after resuming/redeploying the containers.

Deployment method (select with an X)

Environment information

Logs The following logs are output by the uwsgi container when attempting to access the instance via the API after a restart/redeployment.

[27/Jun/2022 11:42:09] ERROR [django.request:224] Internal Server Error: /api/v2/product_types/
Traceback (most recent call last):
File "/app/./dojo/request_cache/__init__.py", line 53, in wrapper
result = getattr(cache, key)
AttributeError: 'RequestCache' object has no attribute '('get_authorized_users', <Permissions.Finding_View: 1402>, <object object at 0x7f7f5b4115b0>)'
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "/app/./dojo/request_cache/__init__.py", line 53, in wrapper
result = getattr(cache, key)
AttributeError: 'RequestCache' object has no attribute '('get_groups', <SimpleLazyObject: <django.contrib.auth.models.AnonymousUser object at 0x7f7f5a02f160>>, <object object at 0x7f7f5b4115b0>)'
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "/usr/local/lib/python3.8/site-packages/django/core/handlers/exception.py", line 47, in inner
response = get_response(request)
File "/usr/local/lib/python3.8/site-packages/django/core/handlers/base.py", line 167, in _get_response
callback, callback_args, callback_kwargs = self.resolve_request(request)
File "/usr/local/lib/python3.8/site-packages/django/core/handlers/base.py", line 290, in resolve_request
resolver_match = resolver.resolve(request.path_info)
File "/usr/local/lib/python3.8/site-packages/django/urls/resolvers.py", line 560, in resolve
for pattern in self.url_patterns:
File "/usr/local/lib/python3.8/site-packages/django/utils/functional.py", line 48, in __get__
res = instance.__dict__[self.name] = self.func(instance)
File "/usr/local/lib/python3.8/site-packages/django/urls/resolvers.py", line 602, in url_patterns
patterns = getattr(self.urlconf_module, "urlpatterns", self.urlconf_module)
File "/usr/local/lib/python3.8/site-packages/django/utils/functional.py", line 48, in __get__
res = instance.__dict__[self.name] = self.func(instance)
File "/usr/local/lib/python3.8/site-packages/django/urls/resolvers.py", line 595, in urlconf_module
return import_module(self.urlconf_name)
File "/usr/local/lib/python3.8/importlib/__init__.py", line 127, in import_module
return _bootstrap._gcd_import(name[level:], package, level)
File "<frozen importlib._bootstrap>", line 1014, in _gcd_import
File "<frozen importlib._bootstrap>", line 991, in _find_and_load
File "<frozen importlib._bootstrap>", line 975, in _find_and_load_unlocked
File "<frozen importlib._bootstrap>", line 671, in _load_unlocked
File "<frozen importlib._bootstrap_external>", line 843, in exec_module
File "<frozen importlib._bootstrap>", line 219, in _call_with_frames_removed
File "/app/./dojo/urls.py", line 11, in <module>
from dojo import views
File "/app/./dojo/views.py", line 13, in <module>
from dojo.forms import ManageFileFormSet
File "/app/./dojo/forms.py", line 1105, in <module>
class FindingForm(forms.ModelForm):
File "/app/./dojo/forms.py", line 1131, in FindingForm
mitigated_by = forms.ModelChoiceField(required=True, queryset=get_authorized_users(Permissions.Finding_View), initial=get_current_user)
File "/app/./dojo/request_cache/__init__.py", line 56, in wrapper
result = fn(*args, **kwargs)
File "/app/./dojo/user/queries.py", line 65, in get_authorized_users
if user.is_superuser or user_has_global_permission(user, permission):
File "/app/./dojo/authorization/authorization.py", line 127, in user_has_global_permission
for group in get_groups(user):
File "/app/./dojo/request_cache/__init__.py", line 56, in wrapper
result = fn(*args, **kwargs)
File "/app/./dojo/authorization/authorization.py", line 260, in get_groups
return Dojo_Group.objects.select_related('global_role').filter(users=user)
File "/usr/local/lib/python3.8/site-packages/django/db/models/query.py", line 941, in filter
return self._filter_or_exclude(False, args, kwargs)
File "/usr/local/lib/python3.8/site-packages/django/db/models/query.py", line 961, in _filter_or_exclude
clone._filter_or_exclude_inplace(negate, args, kwargs)
File "/usr/local/lib/python3.8/site-packages/django/db/models/query.py", line 968, in _filter_or_exclude_inplace
self._query.add_q(Q(*args, **kwargs))
File "/usr/local/lib/python3.8/site-packages/django/db/models/sql/query.py", line 1416, in add_q
clause, _ = self._add_q(q_object, self.used_aliases)
File "/usr/local/lib/python3.8/site-packages/django/db/models/sql/query.py", line 1435, in _add_q
child_clause, needed_inner = self.build_filter(
File "/usr/local/lib/python3.8/site-packages/django/db/models/sql/query.py", line 1343, in build_filter
self.check_related_objects(join_info.final_field, value, join_info.opts)
File "/usr/local/lib/python3.8/site-packages/django/db/models/sql/query.py", line 1172, in check_related_objects
for v in value:
File "/usr/local/lib/python3.8/site-packages/django/utils/functional.py", line 247, in inner
return func(self._wrapped, *args)
TypeError: 'AnonymousUser' object is not iterable
0x4d4e commented 2 years ago

This comment was wrong ;) - not the root cause.

0x4d4e commented 2 years ago

I was able to reproduce this locally using the latest 'dev' branch. Same steps as above:

Interestingly, sending another request via the API succeeded, even though no login/access via the Web UI was performed. Only the first API request after a restart reliably failed. I guess this is caused by some caching. When running the 'debug' environment/configuration to attach a remote debugger, no self-healing was observed and all repeated API requests failed.

I think this issue is caused by the behavior described in this comment: https://github.com/encode/django-rest-framework/issues/760#issuecomment-391127616

Since the result of the TokenAuthentication is not yet available when the Middlewares are executed, request.user is None or AnonymousUser during these steps. For example, the call to https://github.com/DefectDojo/django-DefectDojo/blob/c101e47b294863877cd68a82d0cc60f8017b45b1/dojo/user/queries.py#L56 as seen in the stack trace above is affected and also the call to crum.get_current_user() does not result in the correct user being set (https://github.com/DefectDojo/django-DefectDojo/blob/c101e47b294863877cd68a82d0cc60f8017b45b1/dojo/user/queries.py#L58).

A quick and very hackish poc for a fix is implemented in PR https://github.com/DefectDojo/django-DefectDojo/pull/6478

I think the alternative to fixing request.user via the middleware would be to move the authorization logic to a later step (after TokenAuthentication from restframework was executed). I did not attempt this approach since I did not want to touch the existing authorization logic.