From e13b6e9381c07b1aff8b4e725cd08986d0cbf9c4 Mon Sep 17 00:00:00 2001 From: pulpbot Date: Sun, 19 Jul 2026 17:47:28 +0000 Subject: [PATCH 1/3] Update CI files --- .ci/ansible/settings.py.j2 | 1 + .ci/ansible/start_container.yaml | 2 ++ pyproject.toml | 2 +- 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.ci/ansible/settings.py.j2 b/.ci/ansible/settings.py.j2 index ab8ebb3dce0..0b8a1a16a4d 100644 --- a/.ci/ansible/settings.py.j2 +++ b/.ci/ansible/settings.py.j2 @@ -1,3 +1,4 @@ +SECRET_KEY = "{{ django_secret }}" CONTENT_ORIGIN = "{{ pulp_scheme }}://pulp:{{ 443 if pulp_scheme == 'https' else 80 }}" ANSIBLE_API_HOSTNAME = "{{ pulp_scheme }}://pulp:{{ 443 if pulp_scheme == 'https' else 80 }}" ANSIBLE_CONTENT_HOSTNAME = "{{ pulp_scheme }}://pulp:{{ 443 if pulp_scheme == 'https' else 80 }}/pulp/content" diff --git a/.ci/ansible/start_container.yaml b/.ci/ansible/start_container.yaml index e0891b7ab5d..4ef94c861cf 100644 --- a/.ci/ansible/start_container.yaml +++ b/.ci/ansible/start_container.yaml @@ -18,6 +18,8 @@ ansible.builtin.template: src: "settings.py.j2" dest: "settings/settings.py" + vars: + django_secret: "lookup('community.general.random_string', length=50, overwrite_all='abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*(-_=+)')" - name: "Setup docker networking" community.docker.docker_network: diff --git a/pyproject.toml b/pyproject.toml index 8b83cd05ad0..e9029f0fef8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -267,7 +267,6 @@ filename = "./pyproject.toml" search = "version = \"{current_version}\"" replace = "version = \"{new_version}\"" - [tool.ruff] # This section is managed by the plugin template. Do not edit manually. line-length = 100 @@ -289,6 +288,7 @@ extend-select = [ [tool.ruff.lint.flake8-tidy-imports.banned-api] # This section is managed by the plugin template. Do not edit manually. "distutils".msg = "The 'distutils' module has been deprecated since Python 3.9." +"pulpcore.app.settings".msg = "Always import 'settings' from 'django.conf' instead." [tool.ruff.lint.isort] # This section is managed by the plugin template. Do not edit manually. From bf5ea5a44da463dbc219499b2a5911487785bd7c Mon Sep 17 00:00:00 2001 From: Matthias Dellweg Date: Tue, 14 Jul 2026 17:57:15 +0200 Subject: [PATCH 2/3] Remove insecure default Django secret --- CHANGES/+django_secret.bugfix | 1 + pulp_file/tests/functional/api/test_domains.py | 2 +- pulp_file/tests/functional/api/test_filesystem_export.py | 2 +- pulp_file/tests/functional/api/test_pulp_export.py | 2 +- pulpcore/app/access_policy.py | 2 +- pulpcore/app/authentication.py | 3 +-- pulpcore/app/redis_connection.py | 3 +-- pulpcore/app/serializers/exporter.py | 3 ++- pulpcore/app/serializers/importer.py | 3 ++- pulpcore/app/serializers/repository.py | 3 ++- pulpcore/app/settings.py | 3 --- pulpcore/app/views/importer.py | 2 +- pulpcore/cache/cache.py | 2 +- pulpcore/tasking/kafka.py | 2 +- pulpcore/tests/functional/api/test_auth.py | 3 +-- pulpcore/tests/functional/api/using_plugin/test_pulpimport.py | 2 +- 16 files changed, 18 insertions(+), 20 deletions(-) create mode 100644 CHANGES/+django_secret.bugfix diff --git a/CHANGES/+django_secret.bugfix b/CHANGES/+django_secret.bugfix new file mode 100644 index 00000000000..571ee9046f7 --- /dev/null +++ b/CHANGES/+django_secret.bugfix @@ -0,0 +1 @@ +Stopped shipping an insecure default as DJANGO_SECRET. diff --git a/pulp_file/tests/functional/api/test_domains.py b/pulp_file/tests/functional/api/test_domains.py index 51869b8114e..c88eecc0048 100644 --- a/pulp_file/tests/functional/api/test_domains.py +++ b/pulp_file/tests/functional/api/test_domains.py @@ -2,8 +2,8 @@ import uuid import pytest +from django.conf import settings -from pulpcore.app import settings from pulpcore.tests.functional.utils import download_file, generate_iso pytestmark = pytest.mark.skipif(not settings.DOMAIN_ENABLED, reason="Domains not enabled.") diff --git a/pulp_file/tests/functional/api/test_filesystem_export.py b/pulp_file/tests/functional/api/test_filesystem_export.py index 5720840ca5a..da92c1b471b 100644 --- a/pulp_file/tests/functional/api/test_filesystem_export.py +++ b/pulp_file/tests/functional/api/test_filesystem_export.py @@ -2,8 +2,8 @@ import uuid import pytest +from django.conf import settings -from pulpcore.app import settings from pulpcore.client.pulpcore.exceptions import ApiException, BadRequestException from pulpcore.constants import TASK_STATES diff --git a/pulp_file/tests/functional/api/test_pulp_export.py b/pulp_file/tests/functional/api/test_pulp_export.py index 7a1078eede1..792c15812cb 100644 --- a/pulp_file/tests/functional/api/test_pulp_export.py +++ b/pulp_file/tests/functional/api/test_pulp_export.py @@ -3,8 +3,8 @@ from pathlib import Path import pytest +from django.conf import settings -from pulpcore.app import settings from pulpcore.client.pulpcore.exceptions import ApiException, BadRequestException from pulpcore.constants import TASK_STATES diff --git a/pulpcore/app/access_policy.py b/pulpcore/app/access_policy.py index 6cb41c02b7b..6654f5259c8 100644 --- a/pulpcore/app/access_policy.py +++ b/pulpcore/app/access_policy.py @@ -1,9 +1,9 @@ from copy import deepcopy +from django.conf import settings from rest_access_policy import AccessPolicy from rest_framework.exceptions import APIException -from pulpcore.app import settings from pulpcore.app.models import AccessPolicy as AccessPolicyModel from pulpcore.app.util import get_view_urlpattern, get_viewset_for_model diff --git a/pulpcore/app/authentication.py b/pulpcore/app/authentication.py index 0e42ec0871d..745e35e1ffa 100644 --- a/pulpcore/app/authentication.py +++ b/pulpcore/app/authentication.py @@ -5,6 +5,7 @@ from gettext import gettext as _ import jq +from django.conf import settings from django.contrib.auth import authenticate from django.contrib.auth.backends import RemoteUserBackend from rest_framework.authentication import ( @@ -16,8 +17,6 @@ ) from rest_framework.exceptions import AuthenticationFailed -from pulpcore.app import settings - _logger = logging.getLogger(__name__) diff --git a/pulpcore/app/redis_connection.py b/pulpcore/app/redis_connection.py index e612680cbd0..87270c11249 100644 --- a/pulpcore/app/redis_connection.py +++ b/pulpcore/app/redis_connection.py @@ -1,8 +1,7 @@ +from django.conf import settings from redis import Redis from redis.asyncio import Redis as aRedis -from pulpcore.app.settings import settings - _conn = None _a_conn = None diff --git a/pulpcore/app/serializers/exporter.py b/pulpcore/app/serializers/exporter.py index 8142853d196..fd8a98964fd 100644 --- a/pulpcore/app/serializers/exporter.py +++ b/pulpcore/app/serializers/exporter.py @@ -2,9 +2,10 @@ import re from gettext import gettext as _ +from django.conf import settings from rest_framework import serializers -from pulpcore.app import models, settings +from pulpcore.app import models from pulpcore.app.serializers import ( DetailIdentityField, DetailRelatedField, diff --git a/pulpcore/app/serializers/importer.py b/pulpcore/app/serializers/importer.py index e1a28ae60a9..5985e7fc5e3 100644 --- a/pulpcore/app/serializers/importer.py +++ b/pulpcore/app/serializers/importer.py @@ -1,10 +1,11 @@ import os from gettext import gettext as _ +from django.conf import settings from django.core.exceptions import ObjectDoesNotExist from rest_framework import serializers -from pulpcore.app import models, settings +from pulpcore.app import models from pulpcore.app.serializers import ( DetailIdentityField, DomainUniqueValidator, diff --git a/pulpcore/app/serializers/repository.py b/pulpcore/app/serializers/repository.py index fa6edde3e96..5569fcb7ba3 100644 --- a/pulpcore/app/serializers/repository.py +++ b/pulpcore/app/serializers/repository.py @@ -2,10 +2,11 @@ from gettext import gettext as _ from urllib.parse import urlparse +from django.conf import settings from rest_framework import fields, serializers from rest_framework_nested.serializers import NestedHyperlinkedModelSerializer -from pulpcore.app import models, settings +from pulpcore.app import models from pulpcore.app.serializers import ( DetailIdentityField, DetailRelatedField, diff --git a/pulpcore/app/settings.py b/pulpcore/app/settings.py index 4f77675fb7a..991c4147340 100644 --- a/pulpcore/app/settings.py +++ b/pulpcore/app/settings.py @@ -88,9 +88,6 @@ # List of upload handler classes to be applied in order. FILE_UPLOAD_HANDLERS = ("pulpcore.app.files.HashingFileUploadHandler",) -# SECURITY WARNING: this should be set to a unique, unpredictable value -SECRET_KEY = "SECRET" - # Key used to encrypt fields in the database DB_ENCRYPTION_KEY = "/etc/pulp/certs/database_fields.symmetric.key" diff --git a/pulpcore/app/views/importer.py b/pulpcore/app/views/importer.py index 5e5f104588a..e49aa43ba04 100644 --- a/pulpcore/app/views/importer.py +++ b/pulpcore/app/views/importer.py @@ -2,11 +2,11 @@ import os from gettext import gettext as _ +from django.conf import settings from drf_spectacular.utils import extend_schema from rest_framework.response import Response from rest_framework.views import APIView -from pulpcore.app import settings from pulpcore.app.serializers import PulpImportCheckResponseSerializer, PulpImportCheckSerializer diff --git a/pulpcore/cache/cache.py b/pulpcore/cache/cache.py index ec6ca80e6f6..6fcf470e14a 100644 --- a/pulpcore/cache/cache.py +++ b/pulpcore/cache/cache.py @@ -5,6 +5,7 @@ from aiohttp.web import FileResponse, HTTPSuccessful, Request, Response, StreamResponse from aiohttp.web_exceptions import HTTPFound +from django.conf import settings from django.http import FileResponse as ApiFileResponse from django.http import HttpResponse, HttpResponseRedirect from redis import ConnectionError @@ -16,7 +17,6 @@ get_async_redis_connection, get_redis_connection, ) -from pulpcore.app.settings import settings from pulpcore.metrics import artifacts_size_counter from pulpcore.responses import ArtifactResponse diff --git a/pulpcore/tasking/kafka.py b/pulpcore/tasking/kafka.py index 3f6f0fd447d..808e20d7bbf 100644 --- a/pulpcore/tasking/kafka.py +++ b/pulpcore/tasking/kafka.py @@ -6,7 +6,7 @@ from django.conf import settings -_bootstrap_servers = settings.get("KAFKA_BOOTSTRAP_SERVERS") +_bootstrap_servers = getattr(settings, "KAFKA_BOOTSTRAP_SERVERS") if _bootstrap_servers is None: diff --git a/pulpcore/tests/functional/api/test_auth.py b/pulpcore/tests/functional/api/test_auth.py index 4bf6478b7c8..3eeb616d6c8 100644 --- a/pulpcore/tests/functional/api/test_auth.py +++ b/pulpcore/tests/functional/api/test_auth.py @@ -8,8 +8,7 @@ from base64 import b64encode import pytest - -from pulpcore.app import settings +from django.conf import settings @pytest.mark.parallel diff --git a/pulpcore/tests/functional/api/using_plugin/test_pulpimport.py b/pulpcore/tests/functional/api/using_plugin/test_pulpimport.py index fbca0df5417..927e3830575 100644 --- a/pulpcore/tests/functional/api/using_plugin/test_pulpimport.py +++ b/pulpcore/tests/functional/api/using_plugin/test_pulpimport.py @@ -11,8 +11,8 @@ from pathlib import Path import pytest +from django.conf import settings -from pulpcore.app import settings from pulpcore.client.pulp_file import RepositorySyncURL from pulpcore.client.pulpcore.exceptions import ApiException From 1ab56885bc27a024dde1db854c6db402d9c8ff26 Mon Sep 17 00:00:00 2001 From: Grant Gainey Date: Sat, 28 Mar 2026 13:51:21 -0400 Subject: [PATCH 3/3] Added find_api_root() to standardize URL building-blocks. (cherry picked from commit ff7a1905aacd8ed2e50887677a6e15ce8c7164ad) --- CHANGES/+api_root_changes.feature | 1 + pulp_file/tests/unit/test_serializers.py | 8 +--- pulpcore/app/find_url.py | 49 +++++++++++++++++++++++ pulpcore/app/settings.py | 3 ++ pulpcore/app/templatetags/pulp_urls.py | 13 ++++-- pulpcore/app/urls.py | 27 ++++++------- pulpcore/app/util.py | 5 +-- pulpcore/openapi/__init__.py | 19 +++++---- pulpcore/plugin/find_url.py | 3 ++ pulpcore/pytest_plugin.py | 23 +++++------ pulpcore/tests/unit/test_pulp_urls.py | 50 ++++++++++++------------ pulpcore/tests/unit/test_viewsets.py | 21 ++++------ 12 files changed, 137 insertions(+), 85 deletions(-) create mode 100644 CHANGES/+api_root_changes.feature create mode 100644 pulpcore/app/find_url.py create mode 100644 pulpcore/plugin/find_url.py diff --git a/CHANGES/+api_root_changes.feature b/CHANGES/+api_root_changes.feature new file mode 100644 index 00000000000..cb8416841fd --- /dev/null +++ b/CHANGES/+api_root_changes.feature @@ -0,0 +1 @@ +Added `pulpcore.app.find_url.find_api_root()` to standardize Pulp url-creation. diff --git a/pulp_file/tests/unit/test_serializers.py b/pulp_file/tests/unit/test_serializers.py index 39af54ff3b1..dfee34ac9cb 100644 --- a/pulp_file/tests/unit/test_serializers.py +++ b/pulp_file/tests/unit/test_serializers.py @@ -2,17 +2,13 @@ from django.core.files.uploadedfile import SimpleUploadedFile from django.test import TestCase +from pulpcore.plugin.find_url import find_api_root from pulpcore.plugin.models import Artifact from pulp_file.app.models import FileContent from pulp_file.app.serializers import FileContentSerializer -V3_API_ROOT = ( - settings.V3_API_ROOT - if not settings.DOMAIN_ENABLED - else settings.V3_DOMAIN_API_ROOT.replace("", "default") -) - +_, V3_API_ROOT = find_api_root(domain="default") CHECKSUM_LEN = { "md5": 32, diff --git a/pulpcore/app/find_url.py b/pulpcore/app/find_url.py new file mode 100644 index 00000000000..70f34798ff4 --- /dev/null +++ b/pulpcore/app/find_url.py @@ -0,0 +1,49 @@ +from django.conf import settings + +DOMAIN_SLUG = "" +REWRITE_SLUG = "//" + + +# We isolate this utility-function to avoid pulling in random "Django App must be configured" +# code segments from the rest of the .util methods/imports +def find_api_root(version="v3", set_domain=True, domain=None, lstrip=False, rewrite_header=True): + """ + Returns the tuple (api-root, /api/) + + Args: + version (str): API-version desired + set_domain (bool): Should the domain-slug be included if DOMAIN_ENABLED? + domain (str): Domain-name to replace DOMAIN_SLUG + lstrip (bool): Should the full version have it's leading-/ stripped + rewrite_header (bool): Should API_ROOT_REWRITE_HEADER be honored or not + + Examples: + find_api_root() : ("/pulp/", "/pulp/api/v3/") + find_api_root(), DOMAIN_ENABLED: ("/pulp/", "/pulp//api/v3/") + find_api_root(domain="default"), DOMAIN_ENABLED : ("/pulp/", "/pulp/default/api/v3/") + find_api_root(lstrip=True) : ("pulp/", "pulp/api/v3/") + find_api_root(), API_ROOT_REWRITE_HEADER: ("//", "//api/v3/") + find_api_root(version="v4", domain="foo", lstrip=True), API_ROOT_REWRITE_HEADER : + ("/", "/default/api/v4/") + Returns: + (str, str) : (API_ROOT (possibly re-written), API_ROOT/api// + (with if enabled)) + """ + # Some current path-building wants to ignore REWRITE - make that possible + if rewrite_header and settings.API_ROOT_REWRITE_HEADER: + api_root = REWRITE_SLUG + else: + api_root = settings.API_ROOT + + # Some current path-building wants to ignore DOMAIN - make that possible + if set_domain and settings.DOMAIN_ENABLED: + if domain: + path = f"{api_root}{domain}/api/{version}/" + else: + path = f"{api_root}{DOMAIN_SLUG}/api/{version}/" + else: + path = f"{api_root}api/{version}/" + if lstrip: + return api_root.lstrip("/"), path.lstrip("/") + else: + return api_root, path diff --git a/pulpcore/app/settings.py b/pulpcore/app/settings.py index 991c4147340..c64ec3742c7 100644 --- a/pulpcore/app/settings.py +++ b/pulpcore/app/settings.py @@ -583,6 +583,9 @@ def otel_middleware_hook(settings): api_root = "//" else: api_root = settings.API_ROOT +# protocol://host:port/{API_ROOT}{domain}/api/{version}/ +# All of the below are DEPRECATED, and should be replaced by calling +# pulpcore.plugin.find_url.find_api_root() (q.v.) settings.set("V3_API_ROOT", api_root + "api/v3/") # Not user configurable settings.set("V3_DOMAIN_API_ROOT", api_root + "/api/v3/") settings.set("V3_API_ROOT_NO_FRONT_SLASH", settings.V3_API_ROOT.lstrip("/")) diff --git a/pulpcore/app/templatetags/pulp_urls.py b/pulpcore/app/templatetags/pulp_urls.py index b050926eedf..a372581b4e5 100644 --- a/pulpcore/app/templatetags/pulp_urls.py +++ b/pulpcore/app/templatetags/pulp_urls.py @@ -1,8 +1,9 @@ -from django.conf import settings from django.template.defaultfilters import register, stringfilter from django.template.defaultfilters import urlize as orig_urlize from django.utils.safestring import SafeData, mark_safe +from pulpcore.app.find_url import find_api_root + @register.filter(needs_autoescape=True) @stringfilter @@ -12,13 +13,19 @@ def urlize(text, autoescape=True): This filter overwrites the django default implementation to also handle pulp api hrefs. """ + # urlize() will turn strings into links of they're of the form protocol://site/path. + # We force it to recognize Pulp-api-links by replacing strings in the incoming text of + # the form "API_ROOT/api/v3/whatever", forcing them to look like + # "http://SENTINEL.org/(string)", urlize()ing the result, then undoing the replace to + # lose the http://SENTINEL.org's. + _, current_path = find_api_root(set_domain=False, rewrite_header=False) safe_input = isinstance(text, SafeData) - text = text.replace(settings.V3_API_ROOT, "http://SENTINEL.org" + settings.V3_API_ROOT) + text = text.replace(current_path, "http://SENTINEL.org" + current_path) if safe_input: text = mark_safe(text) text = orig_urlize(text, autoescape=autoescape) safe_input = isinstance(text, SafeData) - text = text.replace("http://SENTINEL.org" + settings.V3_API_ROOT, settings.V3_API_ROOT) + text = text.replace("http://SENTINEL.org" + current_path, current_path) if safe_input: text = mark_safe(text) return text diff --git a/pulpcore/app/urls.py b/pulpcore/app/urls.py index 8160b7b31f7..0fb435df4c4 100644 --- a/pulpcore/app/urls.py +++ b/pulpcore/app/urls.py @@ -27,15 +27,13 @@ OrphansCleanupViewset, ReclaimSpaceViewSet, ) +from pulpcore.plugin.find_url import find_api_root -if settings.DOMAIN_ENABLED: - API_ROOT = settings.V3_DOMAIN_API_ROOT_NO_FRONT_SLASH -else: - API_ROOT = settings.V3_API_ROOT_NO_FRONT_SLASH -if settings.API_ROOT_REWRITE_HEADER: - V3_API_ROOT = settings.V3_API_ROOT.replace("//", settings.API_ROOT) -else: - V3_API_ROOT = settings.V3_API_ROOT +_, PATH_DOMAIN_REWRITE_NOFRONT = find_api_root(lstrip=True, set_domain=True, rewrite_header=True) +_, PATH_NODOMAIN_NOREWRITE_NOFRONT = find_api_root( + lstrip=True, set_domain=False, rewrite_header=False +) +_, PATH_NODOMAIN_REWRITE_NOFRONT = find_api_root(lstrip=True, set_domain=False, rewrite_header=True) class ViewSetNode: @@ -199,7 +197,7 @@ class PulpDefaultRouter(routers.DefaultRouter): SpectacularRedocView.as_view( authentication_classes=[], permission_classes=[], - url=f"{V3_API_ROOT}docs/api.json?include_html=1&pk_path=1", + url=f"/{PATH_NODOMAIN_NOREWRITE_NOFRONT}docs/api.json?include_html=1&pk_path=1", ) ), name="schema-redoc", @@ -210,7 +208,7 @@ class PulpDefaultRouter(routers.DefaultRouter): SpectacularSwaggerView.as_view( authentication_classes=[], permission_classes=[], - url=f"{V3_API_ROOT}docs/api.json?include_html=1&pk_path=1", + url=f"/{PATH_NODOMAIN_NOREWRITE_NOFRONT}docs/api.json?include_html=1&pk_path=1", ) ), name="schema-swagger", @@ -218,9 +216,10 @@ class PulpDefaultRouter(routers.DefaultRouter): ] urlpatterns = [ - path(API_ROOT, include(special_views)), + path(PATH_DOMAIN_REWRITE_NOFRONT, include(special_views)), path("auth/", include("rest_framework.urls")), - path(settings.V3_API_ROOT_NO_FRONT_SLASH, include(docs_and_status)), + # docs/status aren't "inside" a domain + path(PATH_NODOMAIN_REWRITE_NOFRONT, include(docs_and_status)), ] if settings.DOMAIN_ENABLED: @@ -235,7 +234,7 @@ class NoSchema(p.callback.cls): view = NoSchema.as_view(**p.callback.initkwargs) name = p.name + "-domains" if p.name else None docs_and_status_no_schema.append(path(str(p.pattern), view, name=name)) - urlpatterns.insert(-1, path(API_ROOT, include(docs_and_status_no_schema))) + urlpatterns.insert(-1, path(PATH_DOMAIN_REWRITE_NOFRONT, include(docs_and_status_no_schema))) if "social_django" in settings.INSTALLED_APPS: urlpatterns.append( @@ -247,7 +246,7 @@ class NoSchema(p.callback.cls): all_routers = [root_router] + vs_tree.register_with(root_router) for router in all_routers: - urlpatterns.append(path(API_ROOT, include(router.urls))) + urlpatterns.append(path(PATH_DOMAIN_REWRITE_NOFRONT, include(router.urls))) # If plugins define a urls.py, include them into the root namespace. for plugin_pattern in plugin_patterns: diff --git a/pulpcore/app/util.py b/pulpcore/app/util.py index 051c7a602cb..2a9b9c8351f 100644 --- a/pulpcore/app/util.py +++ b/pulpcore/app/util.py @@ -31,7 +31,6 @@ # a little cache so viewset_for_model doesn't have to iterate over every app every time _model_viewset_cache = {} -STRIPPED_API_ROOT = settings.API_ROOT.strip("/") def reverse(viewname, args=None, kwargs=None, request=None, relative_url=True, **extra): @@ -45,7 +44,7 @@ def reverse(viewname, args=None, kwargs=None, request=None, relative_url=True, * if settings.DOMAIN_ENABLED: kwargs.setdefault("pulp_domain", get_domain().name) if settings.API_ROOT_REWRITE_HEADER: - kwargs.setdefault("api_root", getattr(request, "api_root", STRIPPED_API_ROOT)) + kwargs.setdefault("api_root", getattr(request, "api_root", settings.API_ROOT.strip("/"))) if relative_url: request = None return drf_reverse(viewname, args=args, kwargs=kwargs, request=request, **extra) @@ -79,7 +78,6 @@ def get_url(model, domain=None, request=None): kwargs["pk"] = model.pk else: view_action = "list" - return reverse(get_view_name_for_model(model, view_action), kwargs=kwargs, request=request) @@ -301,7 +299,6 @@ def get_view_name_for_model(model_obj, view_action): if isinstance(model_obj, models.MasterModel): model_obj = model_obj.cast() viewset = get_viewset_for_model(model_obj) - # return the complete view name, joining the registered viewset base name with # the requested view method. for router in all_routers: diff --git a/pulpcore/openapi/__init__.py b/pulpcore/openapi/__init__.py index 53ffa98e8f4..29106f6311b 100644 --- a/pulpcore/openapi/__init__.py +++ b/pulpcore/openapi/__init__.py @@ -34,16 +34,21 @@ from pulpcore.app.apps import pulp_plugin_configs from pulpcore.app.loggers import deprecation_logger +from pulpcore.plugin.find_url import find_api_root +# Get the API-ROOT for this installation +_unused, FULL_API_PATH_NOFRONT = find_api_root(lstrip=True) + +# Massage some api-affecting vars to "genericize" them for the spec if settings.DOMAIN_ENABLED: - API_ROOT_NO_FRONT_SLASH = settings.V3_DOMAIN_API_ROOT_NO_FRONT_SLASH.replace("slug:", "") -else: - API_ROOT_NO_FRONT_SLASH = settings.V3_API_ROOT_NO_FRONT_SLASH + FULL_API_PATH_NOFRONT = FULL_API_PATH_NOFRONT.replace("slug:", "") if settings.API_ROOT_REWRITE_HEADER: - API_ROOT_NO_FRONT_SLASH = API_ROOT_NO_FRONT_SLASH.replace( + FULL_API_PATH_NOFRONT = FULL_API_PATH_NOFRONT.replace( "", settings.API_ROOT.strip("/") ) -API_ROOT_NO_FRONT_SLASH = API_ROOT_NO_FRONT_SLASH.replace("<", "{").replace(">", "}") + +# Final massage to make api-root "openapi compatible" +FULL_API_PATH_NOFRONT = FULL_API_PATH_NOFRONT.replace("<", "{").replace(">", "}") # Python does not distinguish integer sizes. The safest assumption is that they are large. extend_schema_field(OpenApiTypes.INT64)(serializers.IntegerField) @@ -63,7 +68,7 @@ class PulpAutoSchema(AutoSchema): "patch": "partial_update", "delete": "delete", } - V3_API = API_ROOT_NO_FRONT_SLASH.replace("{pulp_domain}/", "") + V3_API = FULL_API_PATH_NOFRONT.replace("{pulp_domain}/", "") def _tokenize_path(self): """ @@ -120,7 +125,7 @@ class MyViewSet(ViewSet): tokenized_path = self._tokenize_path() subpath = "/".join(tokenized_path) - operation_keys = subpath.replace(settings.V3_API_ROOT_NO_FRONT_SLASH, "").split("/") + operation_keys = subpath.replace(self.V3_API, "").split("/") operation_keys = [i.title() for i in operation_keys] if len(operation_keys) > 2: del operation_keys[1] diff --git a/pulpcore/plugin/find_url.py b/pulpcore/plugin/find_url.py new file mode 100644 index 00000000000..c968c2f8e0b --- /dev/null +++ b/pulpcore/plugin/find_url.py @@ -0,0 +1,3 @@ +from pulpcore.app.find_url import find_api_root + +__all__ = ["find_api_root"] diff --git a/pulpcore/pytest_plugin.py b/pulpcore/pytest_plugin.py index 35a886ffb0d..82f4d042df3 100644 --- a/pulpcore/pytest_plugin.py +++ b/pulpcore/pytest_plugin.py @@ -21,6 +21,7 @@ from packaging.version import parse as parse_version from yarl import URL +from pulpcore.plugin.find_url import find_api_root from pulpcore.tests.functional.utils import ( SLEEP_TIME, TASK_TIMEOUT, @@ -617,7 +618,7 @@ def _settings_factory(storage_class=None, storage_settings=None): ] def get_installation_storage_option(key, backend): - value = pulp_settings.STORAGES["default"]["OPTIONS"].get(key) + value = pulp_settings.STORAGES["default"].get("OPTIONS", {}).get(key) # Some FileSystem backend options may be defined in the top settings module if backend == "pulpcore.app.models.storage.FileSystem" and not value: value = getattr(pulp_settings, key, None) @@ -775,18 +776,18 @@ def pulp_content_origin_with_prefix(pulp_settings, bindings_cfg): @pytest.fixture(scope="session") def pulp_api_v3_path(pulp_settings, pulp_domain_enabled): + root, path = find_api_root(set_domain=True, rewrite_header=False, domain="default") + + # Check that find_api_root() is doing what the tests expect if pulp_domain_enabled: - v3_api_root = pulp_settings.V3_DOMAIN_API_ROOT - v3_api_root = v3_api_root.replace("", "default") + v3_api_root = f"{pulp_settings.API_ROOT}default/api/v3/" else: - v3_api_root = pulp_settings.V3_API_ROOT - if v3_api_root is None: - raise RuntimeError( - "This fixture requires the server to have the `V3_API_ROOT` setting set." - ) - if pulp_settings.API_ROOT_REWRITE_HEADER: - v3_api_root = v3_api_root.replace("", pulp_settings.API_ROOT.strip("/")) - return v3_api_root + v3_api_root = f"{pulp_settings.API_ROOT}api/v3/" + assert path == v3_api_root + + if path is None: + raise RuntimeError("This fixture requires the server to have the `API_ROOT` setting set.") + return path @pytest.fixture(scope="session") diff --git a/pulpcore/tests/unit/test_pulp_urls.py b/pulpcore/tests/unit/test_pulp_urls.py index 7382b2fb4ce..7634f1402e2 100644 --- a/pulpcore/tests/unit/test_pulp_urls.py +++ b/pulpcore/tests/unit/test_pulp_urls.py @@ -1,23 +1,15 @@ import json -import pytest - +from pulpcore.app.find_url import find_api_root from pulpcore.app.templatetags import pulp_urls -@pytest.fixture(autouse=True) -def api_root(settings): - settings.DOMAIN_ENABLED = True - # TODO: dynaconf lazy settings... - # settings.API_ROOT = "/baz/" - settings.V3_API_ROOT = "/baz/api/v3/" - - def test_urlize_basic_url(): """ Text starts with V3_API_ROOT. Should be made clickable. """ - txt = "/baz/api/v3/foo/bar/" + _, curr_path = find_api_root(set_domain=False, rewrite_header=False) + txt = f"{curr_path}foo/bar/" ret = pulp_urls.urlize(txt) assert ret == f'{txt}' @@ -26,11 +18,12 @@ def test_urlize_quoted_href(): """ Text contains V3_API_ROOT in quotes. Should be made clickable. """ - txt = json.dumps({"pulp_href": "/baz/api/v3/foo/bar/"}) + _, curr_path = find_api_root(set_domain=False, rewrite_header=False) + txt = json.dumps({"pulp_href": f"{curr_path}foo/bar/"}) ret = pulp_urls.urlize(txt) assert ret == ( - '{"pulp_href": "/baz/api/v3/foo/bar/"}' + f'{{"pulp_href": "{curr_path}foo/bar/"}}' ) @@ -38,19 +31,20 @@ def test_urlize_mixed_bag(): """ Text contains lots of stuff in json. """ + _, curr_path = find_api_root(set_domain=False, rewrite_header=False) txt = json.dumps( { - "pulp_href": "/baz/api/v3/foo/bar/", - "related": "/baz/api/v3/other/", + "pulp_href": f"{curr_path}foo/bar/", + "related": f"{curr_path}other/", "description": "Test XSS .", } ) ret = pulp_urls.urlize(txt) assert ret == ( - '{"pulp_href": "/baz/api/v3/foo/bar/", ' - '"related": "/baz/api/v3/other/", ' + f'{{"pulp_href": "{curr_path}foo/bar/", ' + f'"related": "{curr_path}other/", ' ""description": " ""Test XSS <script>alert('ALERT')</script>."}" ) @@ -60,10 +54,12 @@ def test_urlize_quoted_href_no_autoescape(): """ Text contains V3_API_ROOT in quotes. Should be made clickable. """ - txt = json.dumps({"pulp_href": "/baz/api/v3/foo/bar/"}) + _, curr_path = find_api_root(set_domain=False, rewrite_header=False) + + txt = json.dumps({"pulp_href": f"{curr_path}foo/bar/"}) ret = pulp_urls.urlize(txt, autoescape=False) assert ret == ( - '{"pulp_href": "/baz/api/v3/foo/bar/"}' + f'{{"pulp_href": "{curr_path}foo/bar/"}}' ) @@ -71,9 +67,10 @@ def test_urlize_url_xss_autoescape(): """ Text starts with API_ROOT, includes XSS. Should be made clickable, escape XSS. """ - txt = "/baz/api/v3/foo/bar/blech/" + _, curr_path = find_api_root(set_domain=False, rewrite_header=False) + txt = f"{curr_path}foo/bar/blech/" escapified_linked_text = ( - '/baz/api/v3/foo/bar/' + f'{curr_path}foo/bar/' "<script>alert('ALERT!')</script>blech/" ) ret = pulp_urls.urlize(txt) @@ -84,9 +81,10 @@ def test_urlize_url_xss_no_autoescape(): """ Text starts with API_ROOT, includes XSS. Should be made clickable, not escape XSS. """ - txt = "/baz/api/v3/foo/bar/blech/" + _, curr_path = find_api_root(set_domain=False, rewrite_header=False) + txt = f"{curr_path}foo/bar/blech/" escapified_linked_text = ( - '/baz/api/v3/foo/bar/' + f'{curr_path}foo/bar/' "blech/" ) ret = pulp_urls.urlize(txt, autoescape=False) diff --git a/pulpcore/tests/unit/test_viewsets.py b/pulpcore/tests/unit/test_viewsets.py index 26bac2f0e0c..9e475ba3fb5 100644 --- a/pulpcore/tests/unit/test_viewsets.py +++ b/pulpcore/tests/unit/test_viewsets.py @@ -1,18 +1,13 @@ from uuid import uuid4 -from django.conf import settings from django.test import TestCase from rest_framework.serializers import ValidationError as DRFValidationError +from pulpcore.plugin.find_url import find_api_root + from pulp_file.app import models, viewsets -API_ROOT = ( - settings.V3_API_ROOT - if not settings.DOMAIN_ENABLED - else settings.V3_DOMAIN_API_ROOT.replace("", "default") -) -if settings.API_ROOT_REWRITE_HEADER: - API_ROOT = API_ROOT.replace("", settings.API_ROOT.strip("/")) +_, V3_PATH = find_api_root(domain="default", rewrite_header=False, lstrip=False) class TestGetResource(TestCase): @@ -30,7 +25,7 @@ def test_no_errors(self): repo = models.FileRepository.objects.create(name="foo") viewset = viewsets.FileRepositoryViewSet() resource = viewset.get_resource( - "{api_root}repositories/file/file/{pk}/".format(api_root=API_ROOT, pk=repo.pk), + "{path}repositories/file/file/{pk}/".format(path=V3_PATH, pk=repo.pk), models.FileRepository, ) assert repo == resource @@ -46,7 +41,7 @@ def test_multiple_matches(self): with self.assertRaises(DRFValidationError): # matches all repositories viewset.get_resource( - "{api_root}repositories/file/file/".format(api_root=API_ROOT), + "{path}repositories/file/file/".format(path=V3_PATH), models.FileRepository, ) @@ -69,7 +64,7 @@ def test_resource_does_not_exist(self): with self.assertRaises(DRFValidationError): viewset.get_resource( - "{api_root}repositories/file/file/{pk}/".format(api_root=API_ROOT, pk=pk), + "{path}repositories/file/file/{pk}/".format(path=V3_PATH, pk=pk), models.FileRepository, ) @@ -84,8 +79,6 @@ def test_resource_with_field_error(self): with self.assertRaises(DRFValidationError): # has no repo versions yet viewset.get_resource( - "{api_root}repositories/file/file/{pk}/versions/1/".format( - api_root=API_ROOT, pk=repo.pk - ), + "{path}repositories/file/file/{pk}/versions/1/".format(path=V3_PATH, pk=repo.pk), models.FileRepository, )