From 7e6b335326fd1d1f366e3c5dd81b3f6e75da9e1e Mon Sep 17 00:00:00 2001 From: jctanner Date: Tue, 4 Jun 2024 08:01:01 -0400 Subject: [PATCH] dynamic API_HOSTNAME & CONTENT_ORIGIN via dynaconf+django-crum (#2134) * Use crum to set the content origin and api hostname. No-Issue Signed-off-by: James Tanner --- galaxy_ng/app/dynaconf_hooks.py | 43 ++++++++++++++++++- galaxy_ng/app/renderers.py | 2 +- galaxy_ng/app/settings.py | 2 + galaxy_ng/app/webserver_snippets/nginx.conf | 2 +- .../integration/dab/test_url_resolution.py | 15 +++++++ profiles/dab/pulp_config.env | 9 +++- profiles/dab_jwt/proxy/proxy.go | 2 +- profiles/dab_jwt/pulp_config.env | 2 +- requirements/requirements.common.txt | 4 +- requirements/requirements.insights.txt | 4 +- requirements/requirements.standalone.txt | 4 +- setup.py | 1 + 12 files changed, 80 insertions(+), 10 deletions(-) diff --git a/galaxy_ng/app/dynaconf_hooks.py b/galaxy_ng/app/dynaconf_hooks.py index 16e9d76e10..8c999972a4 100755 --- a/galaxy_ng/app/dynaconf_hooks.py +++ b/galaxy_ng/app/dynaconf_hooks.py @@ -10,6 +10,7 @@ from dynaconf import Dynaconf, Validator from galaxy_ng.app.dynamic_settings import DYNAMIC_SETTINGS_SCHEMA from django.apps import apps +from crum import get_current_request logger = logging.getLogger(__name__) @@ -645,7 +646,10 @@ def configure_dynamic_settings(settings: Dynaconf) -> Dict[str, Any]: change the value before it is returned allowing reading overrides from database and cache. """ - if settings.get("GALAXY_DYNAMIC_SETTINGS") is not True: + # we expect a list of function names here, which have to be in scope of + # locals() for this specific file + enabled_hooks = settings.get("DYNACONF_AFTER_GET_HOOKS") + if not enabled_hooks: return {} # Perform lazy imports here to avoid breaking when system runs with older @@ -710,8 +714,43 @@ def read_settings_from_cache_or_db( return temp_settings.get(key, value.value) + def alter_hostname_settings( + temp_settings: Settings, + value: HookValue, + key: str, + *args, + **kwargs + ) -> Any: + """Use the request headers to dynamically alter the content origin and api hostname. + This is useful in scenarios where the hub is accessible directly and through a + reverse proxy. + """ + + # we only want to modify these settings base on request headers + ALLOWED_KEYS = ['CONTENT_ORIGIN', 'ANSIBLE_API_HOSTNAME'] + + # If app is starting up or key is not on allowed list bypass and just return the value + if not apps.ready or key.upper() not in ALLOWED_KEYS: + return value.value + + # we have to assume the proxy or the edge device(s) set these headers correctly + req = get_current_request() + if req is not None: + headers = dict(req.headers) + proto = headers.get("X-Forwarded-Proto", "http") + host = headers.get("Host", "localhost:5001") + baseurl = proto + "://" + host + return baseurl + + return value.value + + # avoid scope errors by not using a list comprehension + hook_functions = [] + for func_name in enabled_hooks: + hook_functions.append(Hook(locals()[func_name])) + return { "_registered_hooks": { - Action.AFTER_GET: [Hook(read_settings_from_cache_or_db)] + Action.AFTER_GET: hook_functions } } diff --git a/galaxy_ng/app/renderers.py b/galaxy_ng/app/renderers.py index be85e000b3..7f6ac0007f 100644 --- a/galaxy_ng/app/renderers.py +++ b/galaxy_ng/app/renderers.py @@ -6,6 +6,6 @@ class CustomBrowsableAPIRenderer(BrowsableAPIRenderer): def show_form_for_method(self, view, method, request, obj): """Display forms only for superuser.""" - if request.user.is_superuser: + if request.user and request.user.is_superuser: return super().show_form_for_method(view, method, request, obj) return False diff --git a/galaxy_ng/app/settings.py b/galaxy_ng/app/settings.py index d88370b6da..b700c41c3c 100644 --- a/galaxy_ng/app/settings.py +++ b/galaxy_ng/app/settings.py @@ -19,9 +19,11 @@ # END: Pulp standard middleware 'django_prometheus.middleware.PrometheusAfterMiddleware', ] +MIDDLEWARE += ('crum.CurrentRequestUserMiddleware',) INSTALLED_APPS = [ 'rest_framework.authtoken', + 'crum', 'dynaconf_merge', ] diff --git a/galaxy_ng/app/webserver_snippets/nginx.conf b/galaxy_ng/app/webserver_snippets/nginx.conf index 1a9011cd50..5b861e5563 100644 --- a/galaxy_ng/app/webserver_snippets/nginx.conf +++ b/galaxy_ng/app/webserver_snippets/nginx.conf @@ -11,7 +11,7 @@ location /ui/ { location /api/ { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto; proxy_set_header Host $http_host; # we don't want nginx trying to do something clever with # redirects, we set the Host: header above already. diff --git a/galaxy_ng/tests/integration/dab/test_url_resolution.py b/galaxy_ng/tests/integration/dab/test_url_resolution.py index 94c7298804..e3b67f8dcf 100644 --- a/galaxy_ng/tests/integration/dab/test_url_resolution.py +++ b/galaxy_ng/tests/integration/dab/test_url_resolution.py @@ -26,3 +26,18 @@ def test_dab_collection_download_url_hostnames(settings, galaxy_client, publishe # make sure the final redirect was through the gateway ... expected_url = gc.galaxy_root.replace('/api/galaxy/', '') assert dl_resp.url.startswith(expected_url) + + # now check if we access it from localhost that the download url changes accordingly + if gc.galaxy_root == "http://jwtproxy:8080/api/galaxy/": + local_url = os.path.join(gc.galaxy_root, cv_url) + local_url = local_url.replace("http://jwtproxy:8080", "http://localhost:5001") + cv_info = gc.get(local_url, auth=("admin", "admin")) + + download_url = cv_info["download_url"] + assert download_url.startswith("http://localhost:5001") + + # try to GET the tarball ... + dl_resp = gc.get(download_url, parse_json=False, auth=("admin", "admin")) + assert dl_resp.status_code == 200 + assert dl_resp.headers.get('Content-Type') == 'application/gzip' + assert dl_resp.url.startswith("http://localhost:5001") diff --git a/profiles/dab/pulp_config.env b/profiles/dab/pulp_config.env index 9a2ba6be37..99482d7ae0 100644 --- a/profiles/dab/pulp_config.env +++ b/profiles/dab/pulp_config.env @@ -10,5 +10,12 @@ PULP_GALAXY_FEATURE_FLAGS__external_authentication=true PULP_TOKEN_SERVER="https://localhost/token/" +# ease-of-use +ENABLE_SIGNING=1 +PULP_GALAXY_AUTO_SIGN_COLLECTIONS=true +PULP_GALAXY_REQUIRE_CONTENT_APPROVAL=true +PULP_GALAXY_COLLECTION_SIGNING_SERVICE=ansible-default +PULP_GALAXY_CONTAINER_SIGNING_SERVICE=container-default + # dynamic download urls -PULP_GALAXY_DYNAMIC_SETTINGS=true +PULP_DYNACONF_AFTER_GET_HOOKS=["read_settings_from_cache_or_db", "alter_hostname_settings"] diff --git a/profiles/dab_jwt/proxy/proxy.go b/profiles/dab_jwt/proxy/proxy.go index 019b1ab262..aa5a660b22 100644 --- a/profiles/dab_jwt/proxy/proxy.go +++ b/profiles/dab_jwt/proxy/proxy.go @@ -679,7 +679,7 @@ func main() { log.Printf("Request: %s %s", req.Method, req.URL.String()) // just assume this proxy is http ... - req.Header.Add("X-Forwarded-Proto", "https") + req.Header.Add("X-Forwarded-Proto", "http") // https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_conn_man/headers#x-envoy-internal req.Header.Add("X-Envoy-Internal", "true") diff --git a/profiles/dab_jwt/pulp_config.env b/profiles/dab_jwt/pulp_config.env index b14304076a..ea987b2885 100644 --- a/profiles/dab_jwt/pulp_config.env +++ b/profiles/dab_jwt/pulp_config.env @@ -18,4 +18,4 @@ PULP_GALAXY_COLLECTION_SIGNING_SERVICE=ansible-default PULP_GALAXY_CONTAINER_SIGNING_SERVICE=container-default # dynamic download urls -PULP_GALAXY_DYNAMIC_SETTINGS=true +PULP_DYNACONF_AFTER_GET_HOOKS=["read_settings_from_cache_or_db", "alter_hostname_settings"] diff --git a/requirements/requirements.common.txt b/requirements/requirements.common.txt index 3b5b5e030a..66adce274b 100644 --- a/requirements/requirements.common.txt +++ b/requirements/requirements.common.txt @@ -109,7 +109,9 @@ django-ansible-base[jwt_consumer] @ git+https://github.com/ansible/django-ansibl django-auth-ldap==4.0.0 # via galaxy-ng (setup.py) django-crum==0.7.9 - # via django-ansible-base + # via + # django-ansible-base + # galaxy-ng (setup.py) django-filter==23.5 # via pulpcore django-guid==3.4.0 diff --git a/requirements/requirements.insights.txt b/requirements/requirements.insights.txt index f652ef5417..7f06b5e744 100644 --- a/requirements/requirements.insights.txt +++ b/requirements/requirements.insights.txt @@ -123,7 +123,9 @@ django-ansible-base[jwt_consumer] @ git+https://github.com/ansible/django-ansibl django-auth-ldap==4.0.0 # via galaxy-ng (setup.py) django-crum==0.7.9 - # via django-ansible-base + # via + # django-ansible-base + # galaxy-ng (setup.py) django-filter==23.5 # via pulpcore django-guid==3.4.0 diff --git a/requirements/requirements.standalone.txt b/requirements/requirements.standalone.txt index dacaced98b..28d7397026 100644 --- a/requirements/requirements.standalone.txt +++ b/requirements/requirements.standalone.txt @@ -109,7 +109,9 @@ django-ansible-base[jwt_consumer] @ git+https://github.com/ansible/django-ansibl django-auth-ldap==4.0.0 # via galaxy-ng (setup.py) django-crum==0.7.9 - # via django-ansible-base + # via + # django-ansible-base + # galaxy-ng (setup.py) django-filter==23.5 # via pulpcore django-guid==3.4.0 diff --git a/setup.py b/setup.py index 6d0c62247f..333921e7f1 100644 --- a/setup.py +++ b/setup.py @@ -125,6 +125,7 @@ def _format_pulp_requirement(plugin, specifier=None, ref=None, gh_namespace="pul "boto3", "distro", "django-ansible-base[jwt_consumer] @ git+https://github.com/ansible/django-ansible-base@devel", # noqa 501 + "django-crum==0.7.9", # From vendored automated_logging "marshmallow<4.0.0,>=3.6.1", "django-picklefield<4.0.0,>=3.0.1",