From 2e03ece289eadd1df31efa850f498d0873143ec1 Mon Sep 17 00:00:00 2001 From: Matthias Dellweg Date: Mon, 3 Jun 2024 18:16:16 +0200 Subject: [PATCH] Allow to specify the type of remote authentication When configuring remote authentication (by the reverse proxy), one should be able to augment the openAPI security specification accordingly. fixes #5437 --- .github/template_gitref | 2 +- .github/workflows/scripts/before_script.sh | 6 --- .github/workflows/scripts/install.sh | 2 +- CHANGES/.TEMPLATE.rst | 47 ------------------- CHANGES/5437.feature | 2 + pulpcore/app/settings.py | 1 + pulpcore/openapi/__init__.py | 20 ++++++++ pulpcore/tests/functional/api/test_auth.py | 21 +++++++-- .../functional/api/test_openapi_schema.py | 30 +++++++----- staging_docs/admin/learn/settings.md | 12 ++++- template_config.yml | 1 + 11 files changed, 72 insertions(+), 72 deletions(-) delete mode 100644 CHANGES/.TEMPLATE.rst create mode 100644 CHANGES/5437.feature diff --git a/.github/template_gitref b/.github/template_gitref index bfe538dd7f1..dadaa1c99ce 100644 --- a/.github/template_gitref +++ b/.github/template_gitref @@ -1 +1 @@ -2021.08.26-337-g7c7a09a +2021.08.26-338-g2237db8 diff --git a/.github/workflows/scripts/before_script.sh b/.github/workflows/scripts/before_script.sh index 386e978677a..1cdfe292228 100755 --- a/.github/workflows/scripts/before_script.sh +++ b/.github/workflows/scripts/before_script.sh @@ -36,12 +36,6 @@ tail -v -n +1 .ci/ansible/Containerfile cmd_prefix bash -c "echo '%wheel ALL=(ALL) NOPASSWD: ALL' > /etc/sudoers.d/nopasswd" cmd_prefix bash -c "usermod -a -G wheel pulp" -SCENARIOS=("pulp" "performance" "azure" "gcp" "s3" "generate-bindings" "lowerbounds") -if [[ " ${SCENARIOS[*]} " =~ " ${TEST} " ]]; then - # Many functional tests require these - cmd_prefix dnf install -yq lsof which -fi - if [[ "${REDIS_DISABLED:-false}" == true ]]; then cmd_prefix bash -c "s6-rc -d change redis" echo "The Redis service was disabled for $TEST" diff --git a/.github/workflows/scripts/install.sh b/.github/workflows/scripts/install.sh index b20d5eb7be0..c2470bdd0ed 100755 --- a/.github/workflows/scripts/install.sh +++ b/.github/workflows/scripts/install.sh @@ -126,7 +126,7 @@ if [ "$TEST" = "azure" ]; then - ./azurite:/etc/pulp\ command: "azurite-blob --blobHost 0.0.0.0 --cert /etc/pulp/azcert.pem --key /etc/pulp/azkey.pem"' vars/main.yaml sed -i -e '$a azure_test: true\ -pulp_scenario_settings: {"domain_enabled": true}\ +pulp_scenario_settings: {"domain_enabled": true, "rest_framework__default_authentication_classes": "@merge pulpcore.app.authentication.PulpRemoteUserAuthentication"}\ pulp_scenario_env: {"otel_bsp_max_export_batch_size": 1, "otel_bsp_max_queue_size": 1, "otel_exporter_otlp_endpoint": "http://localhost:4318", "otel_exporter_otlp_protocol": "http/protobuf", "otel_metric_export_interval": 800, "pulp_otel_enabled": "true"}\ ' vars/main.yaml fi diff --git a/CHANGES/.TEMPLATE.rst b/CHANGES/.TEMPLATE.rst deleted file mode 100644 index 49c2305d7b1..00000000000 --- a/CHANGES/.TEMPLATE.rst +++ /dev/null @@ -1,47 +0,0 @@ -{% if render_title %} -{% if versiondata.name %} -{{ versiondata.name }} {{ versiondata.version }} ({{ versiondata.date }}) -{{ top_underline * ((versiondata.name + versiondata.version + versiondata.date)|length + 4)}} -{% else %} -{{ versiondata.version }} ({{ versiondata.date }}) -{{ top_underline * ((versiondata.version + versiondata.date)|length + 3)}} -{% endif %} -{% endif %} -{% for section, _ in sections.items() %} -{% set underline = underlines[0] %}{% if section %}{{section}} -{{ underline * section|length }}{% set underline = underlines[1] %} - -{% endif %} - -{% if sections[section] %} -{% for category, val in definitions.items() if category in sections[section]%} -{{ definitions[category]['name'] }} -{{ underline * definitions[category]['name']|length }} - -{% if definitions[category]['showcontent'] %} -{% for text, values in sections[section][category].items() %} -- {{ text }} - {{ values|join(',\n ') }} -{% endfor %} - -{% else %} -- {{ sections[section][category]['']|join(', ') }} - -{% endif %} -{% if sections[section][category]|length == 0 %} -No significant changes. - -{% else %} -{% endif %} - -{% endfor %} -{% else %} -No significant changes. - - -{% endif %} -{% endfor %} ----- - - - diff --git a/CHANGES/5437.feature b/CHANGES/5437.feature new file mode 100644 index 00000000000..d2c6eafe38b --- /dev/null +++ b/CHANGES/5437.feature @@ -0,0 +1,2 @@ +Add ability to configure the openapi schema for remote user authentication via `REMOTE_USER_OPENAPI_SECURITY_SCHEME`. +Its type defaults to "mutualTLS" for cert based authentication. diff --git a/pulpcore/app/settings.py b/pulpcore/app/settings.py index d7defa5f1fe..2fed015beb9 100644 --- a/pulpcore/app/settings.py +++ b/pulpcore/app/settings.py @@ -264,6 +264,7 @@ TMPFILE_PROTECTION_TIME = 0 REMOTE_USER_ENVIRON_NAME = "REMOTE_USER" +REMOTE_USER_OPENAPI_SECURITY_SCHEME = {"type": "mutualTLS"} AUTHENTICATION_JSON_HEADER = "" AUTHENTICATION_JSON_HEADER_JQ_FILTER = "" diff --git a/pulpcore/openapi/__init__.py b/pulpcore/openapi/__init__.py index cbca50a52ca..de853ca7fb4 100644 --- a/pulpcore/openapi/__init__.py +++ b/pulpcore/openapi/__init__.py @@ -518,9 +518,29 @@ def get_schema(self, request=None, public=False): server_url = "http://localhost:24817" if not request else request.build_absolute_uri("/") result["servers"] = [{"url": server_url}] + if "bindings" in request.query_params: + # Remove all but basic auth for bindings generation + securitySchemes = { + k: v for k, v in result["components"]["securitySchemes"].items() if k == "basicAuth" + } + result["components"]["securitySchemes"] = securitySchemes + for path in result["paths"].values(): + for operation in path.values(): + security = operation.get("security") + if security: + operation["security"] = [item for item in security if "basicAuth" in item] + return normalize_result_object(result) +class PulpRemoteUserAuthenticationScheme(OpenApiAuthenticationExtension): + target_class = "pulpcore.app.authentication.PulpRemoteUserAuthentication" + name = "remoteUserAuthentication" + + def get_security_definition(self, auto_schema): + return settings.REMOTE_USER_OPENAPI_SECURITY_SCHEME + + class JSONHeaderRemoteAuthenticationScheme(OpenApiAuthenticationExtension): target_class = "pulpcore.app.authentication.JSONHeaderRemoteAuthentication" name = "json_header_remote_authentication" diff --git a/pulpcore/tests/functional/api/test_auth.py b/pulpcore/tests/functional/api/test_auth.py index 68644e50408..9a6f84ace75 100644 --- a/pulpcore/tests/functional/api/test_auth.py +++ b/pulpcore/tests/functional/api/test_auth.py @@ -14,6 +14,11 @@ @pytest.mark.parallel +@pytest.mark.skipif( + "rest_framework.authentication.BasicAuthentication" + not in settings.REST_FRAMEWORK["DEFAULT_AUTHENTICATION_CLASSES"], + reason="Test can't run unless BasicAuthentication is enabled", +) def test_base_auth_success(pulpcore_bindings, pulp_admin_user): """Perform HTTP basic authentication with valid credentials. @@ -27,6 +32,11 @@ def test_base_auth_success(pulpcore_bindings, pulp_admin_user): @pytest.mark.parallel +@pytest.mark.skipif( + "rest_framework.authentication.BasicAuthentication" + not in settings.REST_FRAMEWORK["DEFAULT_AUTHENTICATION_CLASSES"], + reason="Test can't run unless BasicAuthentication is enabled", +) def test_base_auth_failure(pulpcore_bindings, invalid_user): """Perform HTTP basic authentication with invalid credentials. @@ -44,6 +54,11 @@ def test_base_auth_failure(pulpcore_bindings, invalid_user): @pytest.mark.parallel +@pytest.mark.skipif( + "rest_framework.authentication.BasicAuthentication" + not in settings.REST_FRAMEWORK["DEFAULT_AUTHENTICATION_CLASSES"], + reason="Test can't run unless BasicAuthentication is enabled", +) def test_base_auth_required(pulpcore_bindings, anonymous_user): """Perform HTTP basic authentication with no credentials. @@ -63,7 +78,7 @@ def test_base_auth_required(pulpcore_bindings, anonymous_user): @pytest.mark.parallel @pytest.mark.skipif( "django.contrib.auth.backends.RemoteUserBackend" not in settings.AUTHENTICATION_BACKENDS - and "pulpcore.app.authentication.JSONHeaderRemoteAuthentication" + or "pulpcore.app.authentication.JSONHeaderRemoteAuthentication" not in settings.REST_FRAMEWORK["DEFAULT_AUTHENTICATION_CLASSES"], reason="Test can't run unless RemoteUserBackend and JSONHeaderRemoteAuthentication are enabled", ) @@ -86,7 +101,7 @@ def test_jq_header_remote_auth(pulpcore_bindings, anonymous_user): @pytest.mark.parallel @pytest.mark.skipif( "django.contrib.auth.backends.RemoteUserBackend" not in settings.AUTHENTICATION_BACKENDS - and "pulpcore.app.authentication.JSONHeaderRemoteAuthentication" + or "pulpcore.app.authentication.JSONHeaderRemoteAuthentication" not in settings.REST_FRAMEWORK["DEFAULT_AUTHENTICATION_CLASSES"], reason="Test can't run unless RemoteUserBackend and JSONHeaderRemoteAuthentication are enabled", ) @@ -115,7 +130,7 @@ def test_jq_header_remote_auth_denied_by_wrong_header(pulpcore_bindings, anonymo @pytest.mark.parallel @pytest.mark.skipif( "django.contrib.auth.backends.RemoteUserBackend" not in settings.AUTHENTICATION_BACKENDS - and "pulpcore.app.authentication.JSONHeaderRemoteAuthentication" + or "pulpcore.app.authentication.JSONHeaderRemoteAuthentication" not in settings.REST_FRAMEWORK["DEFAULT_AUTHENTICATION_CLASSES"], reason="Test can't run unless RemoteUserBackend and JSONHeaderRemoteAuthentication are enabled", ) diff --git a/pulpcore/tests/functional/api/test_openapi_schema.py b/pulpcore/tests/functional/api/test_openapi_schema.py index 6a40354d8a4..bfd916ee5e5 100644 --- a/pulpcore/tests/functional/api/test_openapi_schema.py +++ b/pulpcore/tests/functional/api/test_openapi_schema.py @@ -1,10 +1,9 @@ """Test related to the openapi schema Pulp generates.""" -import asyncio import copy import json -import aiohttp +import requests import pytest import jsonschema @@ -39,19 +38,12 @@ def pulp_openapi_schema_url(pulp_api_v3_url): @pytest.fixture(scope="session") def pulp_openapi_schema(pulp_openapi_schema_url): - return asyncio.run(_download_schema(pulp_openapi_schema_url)) + return requests.get(pulp_openapi_schema_url).json() @pytest.fixture(scope="session") def pulp_openapi_schema_pk_path_set(pulp_openapi_schema_url): - url = f"{pulp_openapi_schema_url}?pk_path=1" - return asyncio.run(_download_schema(url)) - - -async def _download_schema(url): - async with aiohttp.ClientSession() as session: - async with session.get(url) as response: - return await response.json() + return requests.get(f"{pulp_openapi_schema_url}?pk_path=1").json() @pytest.mark.parallel @@ -88,12 +80,26 @@ def test_no_dup_operation_ids(pulp_openapi_schema): assert len(dup_ids) == 0, f"Duplicate operationIds found: {dup_ids}" +@pytest.mark.parallel +def test_remote_user_auth_security_scheme(pulp_settings, pulp_openapi_schema): + if ( + "pulpcore.app.authentication.PulpRemoteUserAuthentication" + not in pulp_settings.REST_FRAMEWORK["DEFAULT_AUTHENTICATION_CLASSES"] + ): + pytest.skip("Test can't run unless PulpRemoteUserAuthentication is enabled.") + + expected_security_scheme = pulp_settings.REMOTE_USER_OPENAPI_SECURITY_SCHEME + security_schemes = pulp_openapi_schema["components"]["securitySchemes"] + + assert security_schemes["remoteUserAuthentication"] == expected_security_scheme + + @pytest.mark.parallel def test_external_auth_on_security_scheme(pulp_settings, pulp_openapi_schema): if ( "django.contrib.auth.backends.RemoteUserBackend" not in pulp_settings.AUTHENTICATION_BACKENDS - and "pulpcore.app.authentication.JSONHeaderRemoteAuthentication" + or "pulpcore.app.authentication.JSONHeaderRemoteAuthentication" not in pulp_settings.REST_FRAMEWORK["DEFAULT_AUTHENTICATION_CLASSES"] ): pytest.skip( diff --git a/staging_docs/admin/learn/settings.md b/staging_docs/admin/learn/settings.md index 0d95f11dcab..c89bdc709f3 100644 --- a/staging_docs/admin/learn/settings.md +++ b/staging_docs/admin/learn/settings.md @@ -272,8 +272,8 @@ Defaults to `30` seconds. ### REMOTE_USER_ENVIRON_NAME -The name of the WSGI environment variable to read for `webserver authentication -`. +The name of the WSGI environment variable to read for `webserver authentication `. +It is only used with the `PulpRemoteUserAuthentication` authentication class. !!! warning Configuring this has serious security implications. See the [Django warning at the end of this @@ -283,6 +283,14 @@ Defaults to `'REMOTE_USER'`. +### REMOTE_USER_OPENAPI_SECURITY_SCHEME + +A JSON object representing the security scheme advertised for the `PulpRemoteUserAuthentication` authentication class. + +Defaults to `{"type": "mutualTLS"}`, which represents x509 certificate based authentication. + + + ### ALLOWED_IMPORT_PATHS One or more real filesystem paths that Remotes with filesystem paths can import from. For example diff --git a/template_config.yml b/template_config.yml index 87c1547ae81..699c43d23ee 100644 --- a/template_config.yml +++ b/template_config.yml @@ -71,6 +71,7 @@ pulp_settings: upload_protection_time: 10 pulp_settings_azure: domain_enabled: true + rest_framework__default_authentication_classes: '@merge pulpcore.app.authentication.PulpRemoteUserAuthentication' pulp_settings_gcp: null pulp_settings_s3: authentication_backends: '@merge django.contrib.auth.backends.RemoteUserBackend'