From 6d7ad1e5e4f22694c7942eda9b7166126f4a80a2 Mon Sep 17 00:00:00 2001 From: Mikhail Malyshev Date: Mon, 9 Mar 2026 17:06:40 +0100 Subject: [PATCH] fix: strip whitespace from instance configuration values on save Trailing whitespace in OAuth credentials (e.g. GITHUB_CLIENT_ID) causes authentication failures. When a user accidentally pastes a value with leading/trailing spaces into the God Mode admin UI, the spaces are persisted to the database as-is. For OAuth client IDs this results in the identity provider rejecting the request (e.g. GitHub returns 404 because the client_id has a URL-encoded space appended). Strip whitespace from string configuration values before persisting them in InstanceConfigurationEndpoint.patch(). Non-string values (e.g. None) are left unchanged to preserve the ability to clear a config field. Signed-off-by: Mikhail Malyshev --- .../plane/license/api/views/configuration.py | 2 + apps/api/plane/tests/unit/views/__init__.py | 0 .../unit/views/test_instance_configuration.py | 152 ++++++++++++++++++ 3 files changed, 154 insertions(+) create mode 100644 apps/api/plane/tests/unit/views/__init__.py create mode 100644 apps/api/plane/tests/unit/views/test_instance_configuration.py diff --git a/apps/api/plane/license/api/views/configuration.py b/apps/api/plane/license/api/views/configuration.py index bb9a9e00ee6..896edf9f7de 100644 --- a/apps/api/plane/license/api/views/configuration.py +++ b/apps/api/plane/license/api/views/configuration.py @@ -46,6 +46,8 @@ def patch(self, request): bulk_configurations = [] for configuration in configurations: value = request.data.get(configuration.key, configuration.value) + if isinstance(value, str): + value = value.strip() if configuration.is_encrypted: configuration.value = encrypt_data(value) else: diff --git a/apps/api/plane/tests/unit/views/__init__.py b/apps/api/plane/tests/unit/views/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/apps/api/plane/tests/unit/views/test_instance_configuration.py b/apps/api/plane/tests/unit/views/test_instance_configuration.py new file mode 100644 index 00000000000..55720e834f3 --- /dev/null +++ b/apps/api/plane/tests/unit/views/test_instance_configuration.py @@ -0,0 +1,152 @@ +# Copyright (c) 2023-present Plane Software, Inc. and contributors +# SPDX-License-Identifier: AGPL-3.0-only +# See the LICENSE file for details. + +import pytest +from django.utils import timezone + +from rest_framework.test import APIClient + +from plane.db.models import User +from plane.license.models import Instance, InstanceAdmin, InstanceConfiguration + + +@pytest.fixture +def instance_admin(db): + """Create an instance, an admin user, and link them via InstanceAdmin.""" + user = User.objects.create( + email="admin@plane.so", + first_name="Admin", + last_name="User", + ) + user.set_password("admin-password") + user.save() + + instance = Instance.objects.create( + instance_name="test", + instance_id="test-instance-id", + current_version="1.2.3", + last_checked_at=timezone.now(), + is_setup_done=True, + ) + + InstanceAdmin.objects.create( + instance=instance, + user=user, + role=20, + ) + + return user + + +@pytest.fixture +def admin_client(instance_admin): + """Return an authenticated API client for the instance admin.""" + client = APIClient() + client.force_authenticate(user=instance_admin) + return client + + +@pytest.mark.unit +class TestInstanceConfigurationWhitespaceTrimming: + """Test that instance configuration values are trimmed on save.""" + + @pytest.mark.django_db + def test_patch_strips_trailing_whitespace(self, admin_client): + """Values with trailing whitespace should be stripped when saved.""" + InstanceConfiguration.objects.create( + key="GITHUB_CLIENT_ID", + value="", + category="GITHUB", + is_encrypted=False, + ) + + response = admin_client.patch( + "/api/instances/configurations/", + {"GITHUB_CLIENT_ID": "Ov23li2Dep2t79q18nxD "}, + format="json", + ) + + assert response.status_code == 200 + config = InstanceConfiguration.objects.get(key="GITHUB_CLIENT_ID") + assert config.value == "Ov23li2Dep2t79q18nxD" + + @pytest.mark.django_db + def test_patch_strips_leading_whitespace(self, admin_client): + """Values with leading whitespace should be stripped when saved.""" + InstanceConfiguration.objects.create( + key="GITHUB_CLIENT_ID", + value="", + category="GITHUB", + is_encrypted=False, + ) + + response = admin_client.patch( + "/api/instances/configurations/", + {"GITHUB_CLIENT_ID": " Ov23li2Dep2t79q18nxD"}, + format="json", + ) + + assert response.status_code == 200 + config = InstanceConfiguration.objects.get(key="GITHUB_CLIENT_ID") + assert config.value == "Ov23li2Dep2t79q18nxD" + + @pytest.mark.django_db + def test_patch_strips_both_sides(self, admin_client): + """Values with whitespace on both sides should be fully trimmed.""" + InstanceConfiguration.objects.create( + key="GOOGLE_CLIENT_ID", + value="", + category="GOOGLE", + is_encrypted=False, + ) + + response = admin_client.patch( + "/api/instances/configurations/", + {"GOOGLE_CLIENT_ID": " some-client-id "}, + format="json", + ) + + assert response.status_code == 200 + config = InstanceConfiguration.objects.get(key="GOOGLE_CLIENT_ID") + assert config.value == "some-client-id" + + @pytest.mark.django_db + def test_patch_preserves_clean_values(self, admin_client): + """Values without whitespace should be saved unchanged.""" + InstanceConfiguration.objects.create( + key="GITHUB_CLIENT_ID", + value="", + category="GITHUB", + is_encrypted=False, + ) + + response = admin_client.patch( + "/api/instances/configurations/", + {"GITHUB_CLIENT_ID": "Ov23li2Dep2t79q18nxD"}, + format="json", + ) + + assert response.status_code == 200 + config = InstanceConfiguration.objects.get(key="GITHUB_CLIENT_ID") + assert config.value == "Ov23li2Dep2t79q18nxD" + + @pytest.mark.django_db + def test_patch_null_value_not_coerced_to_string(self, admin_client): + """Null values should remain None, not become the string 'None'.""" + InstanceConfiguration.objects.create( + key="GOOGLE_CLIENT_ID", + value="old-value", + category="GOOGLE", + is_encrypted=False, + ) + + response = admin_client.patch( + "/api/instances/configurations/", + {"GOOGLE_CLIENT_ID": None}, + format="json", + ) + + assert response.status_code == 200 + config = InstanceConfiguration.objects.get(key="GOOGLE_CLIENT_ID") + assert config.value is None