diff --git a/awx/api/versioning.py b/awx/api/versioning.py index 09da66b620..ff10d9875b 100644 --- a/awx/api/versioning.py +++ b/awx/api/versioning.py @@ -29,9 +29,7 @@ def reverse(viewname, args=None, kwargs=None, request=None, format=None, **extra kwargs = {} if 'version' not in kwargs: kwargs['version'] = settings.REST_FRAMEWORK['DEFAULT_VERSION'] - url = drf_reverse(viewname, args, kwargs, request, format, **extra) - - return transform_optional_api_urlpattern_prefix_url(request, url) + return drf_reverse(viewname, args, kwargs, request, format, **extra) class URLPathVersioning(BaseVersioning): diff --git a/awx/main/middleware.py b/awx/main/middleware.py index b5c39b03a3..d485ce45f7 100644 --- a/awx/main/middleware.py +++ b/awx/main/middleware.py @@ -1,6 +1,7 @@ # Copyright (c) 2015 Ansible, Inc. # All Rights Reserved. +import functools import logging import threading import time @@ -18,6 +19,7 @@ from django.urls import reverse, resolve from awx.main import migrations from awx.main.utils.profiling import AWXProfiler from awx.main.utils.common import memoize +from awx.urls import get_urlpatterns logger = logging.getLogger('awx.main.middleware') @@ -173,3 +175,27 @@ class MigrationRanCheckMiddleware(MiddlewareMixin): def process_request(self, request): if is_migrating() and getattr(resolve(request.path), 'url_name', '') != 'migrations_notran': return redirect(reverse("ui:migrations_notran")) + + +class OptionalURLPrefixPath(MiddlewareMixin): + @functools.lru_cache + def _url_optional(self, prefix): + # Relavant Django code path https://github.com/django/django/blob/stable/4.2.x/django/core/handlers/base.py#L300 + # + # resolve_request(request) + # get_resolver(request.urlconf) + # _get_cached_resolver(request.urlconf) <-- cached via @functools.cache + # + # Django will attempt to cache the value(s) of request.urlconf + # Being hashable is a prerequisit for being cachable. + # tuple() is hashable list() is not. + # Hence the tuple(list()) wrap. + return tuple(get_urlpatterns(prefix=prefix)) + + def process_request(self, request): + prefix = settings.OPTIONAL_API_URLPATTERN_PREFIX + + if request.path.startswith(f"/api/{prefix}"): + request.urlconf = self._url_optional(prefix) + else: + request.urlconf = 'awx.urls' diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index 2ff82d7fc5..b573d042b9 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -1002,6 +1002,7 @@ MIDDLEWARE = [ 'django.contrib.auth.middleware.AuthenticationMiddleware', 'awx.main.middleware.DisableLocalAuthMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', + 'awx.main.middleware.OptionalURLPrefixPath', 'awx.sso.middleware.SocialAuthMiddleware', 'crum.CurrentRequestUserMiddleware', 'awx.main.middleware.URLModificationMiddleware', diff --git a/awx/urls.py b/awx/urls.py index 28ed6148ef..2fcfc650f9 100644 --- a/awx/urls.py +++ b/awx/urls.py @@ -9,36 +9,42 @@ from ansible_base.resource_registry.urls import urlpatterns as resource_api_urls from awx.main.views import handle_400, handle_403, handle_404, handle_500, handle_csp_violation, handle_login_redirect -urlpatterns = [ - re_path(r'', include('awx.ui.urls', namespace='ui')), - re_path(r'^ui_next/.*', include('awx.ui_next.urls', namespace='ui_next')), - path('api/', include('awx.api.urls', namespace='api')), -] +def get_urlpatterns(prefix=None): + if not prefix: + prefix = '/' + else: + prefix = f'/{prefix}/' -if settings.OPTIONAL_API_URLPATTERN_PREFIX: - urlpatterns += [ - path(f'api/{settings.OPTIONAL_API_URLPATTERN_PREFIX}/', include('awx.api.urls')), + urlpatterns = [ + re_path(r'', include('awx.ui.urls', namespace='ui')), + re_path(r'^ui_next/.*', include('awx.ui_next.urls', namespace='ui_next')), + path(f'api{prefix}', include('awx.api.urls', namespace='api')), ] -urlpatterns += [ - re_path(r'^api/v2/', include(resource_api_urls)), - re_path(r'^sso/', include('awx.sso.urls', namespace='sso')), - re_path(r'^sso/', include('social_django.urls', namespace='social')), - re_path(r'^(?:api/)?400.html$', handle_400), - re_path(r'^(?:api/)?403.html$', handle_403), - re_path(r'^(?:api/)?404.html$', handle_404), - re_path(r'^(?:api/)?500.html$', handle_500), - re_path(r'^csp-violation/', handle_csp_violation), - re_path(r'^login/', handle_login_redirect), -] + urlpatterns += [ + path(f'api{prefix}v2/', include(resource_api_urls)), + re_path(r'^sso/', include('awx.sso.urls', namespace='sso')), + re_path(r'^sso/', include('social_django.urls', namespace='social')), + re_path(r'^(?:api/)?400.html$', handle_400), + re_path(r'^(?:api/)?403.html$', handle_403), + re_path(r'^(?:api/)?404.html$', handle_404), + re_path(r'^(?:api/)?500.html$', handle_500), + re_path(r'^csp-violation/', handle_csp_violation), + re_path(r'^login/', handle_login_redirect), + ] -if settings.SETTINGS_MODULE == 'awx.settings.development': - try: - import debug_toolbar + if settings.SETTINGS_MODULE == 'awx.settings.development': + try: + import debug_toolbar - urlpatterns += [re_path(r'^__debug__/', include(debug_toolbar.urls))] - except ImportError: - pass + urlpatterns += [re_path(r'^__debug__/', include(debug_toolbar.urls))] + except ImportError: + pass + + return urlpatterns + + +urlpatterns = get_urlpatterns() handler400 = 'awx.main.views.handle_400' handler403 = 'awx.main.views.handle_403'