diff --git a/bin/docker_start.sh b/bin/docker_start.sh index 489b5930b1..042bb65fa0 100755 --- a/bin/docker_start.sh +++ b/bin/docker_start.sh @@ -13,6 +13,8 @@ uwsgi_port=${UWSGI_PORT:-8000} uwsgi_processes=${UWSGI_PROCESSES:-4} uwsgi_threads=${UWSGI_THREADS:-1} +mountpoint=${SUBPATH:-/} + until pg_isready; do >&2 echo "Waiting for database connection..." sleep 1 @@ -29,7 +31,8 @@ python src/manage.py migrate exec uwsgi \ --http :$uwsgi_port \ --http-keepalive \ - --module openforms.wsgi \ + --mount $mountpoint=openforms.wsgi:application \ + --manage-script-name \ --static-map /static=/app/static \ --static-map /media=/app/media \ --chdir src \ diff --git a/docs/installation/config.rst b/docs/installation/config.rst index c13cc0af99..6396b2c5e8 100644 --- a/docs/installation/config.rst +++ b/docs/installation/config.rst @@ -325,6 +325,8 @@ Other settings * ``FORMS_EXPORT_REMOVED_AFTER_DAYS``: The number of days after which zip files of exported forms should be deleted. Defaults to 7 days. +* ``SUBPATH``: A string with a prefix for all URL paths, for example ``/openforms``. Typically used at the infrastructure level to route to a particular application on the same (sub)domain. Defaults to empty string meaning that Open Forms is hosted at the root (``/``). + .. _`Django DATABASE settings`: https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-DATABASE-ENGINE Specifying the environment variables diff --git a/src/openforms/conf/base.py b/src/openforms/conf/base.py index f9a2081398..07fd976790 100644 --- a/src/openforms/conf/base.py +++ b/src/openforms/conf/base.py @@ -558,6 +558,17 @@ MAX_FILE_UPLOAD_SIZE = config("MAX_FILE_UPLOAD_SIZE", default="50M", cast=Filesize()) +# Deal with being hosted on a subpath +SUBPATH = config("SUBPATH", default="") +if SUBPATH: + if not SUBPATH.startswith("/"): + SUBPATH = f"/{SUBPATH}" + + if SUBPATH != "/": + STATIC_URL = f"{SUBPATH}{STATIC_URL}" + MEDIA_URL = f"{SUBPATH}{MEDIA_URL}" + + ############################## # # # 3RD PARTY LIBRARY SETTINGS # diff --git a/src/openforms/middleware.py b/src/openforms/middleware.py index e5dc6e1467..71df51ca63 100644 --- a/src/openforms/middleware.py +++ b/src/openforms/middleware.py @@ -47,7 +47,7 @@ def __call__(self, request: HttpRequest): response = self.get_response(request) # Only add the CSRF token header if it's an api endpoint - if not request.path.startswith("/api"): + if not request.path_info.startswith("/api"): return response response[CSRF_TOKEN_HEADER_NAME] = get_token(request) @@ -67,7 +67,7 @@ def __call__(self, request: HttpRequest): response = self.get_response(request) # Only add the header if it's an api endpoint - if not request.path.startswith("/api"): + if not request.path_info.startswith("/api"): return response response[IS_FORM_DESIGNER_HEADER_NAME] = "false" diff --git a/src/openforms/tests/test_middleware.py b/src/openforms/tests/test_middleware.py index b29e7a1030..f734f3de4e 100644 --- a/src/openforms/tests/test_middleware.py +++ b/src/openforms/tests/test_middleware.py @@ -19,6 +19,18 @@ def test_csrftoken_not_in_header_root(self): self.assertNotIn(CSRF_TOKEN_HEADER_NAME, response.headers) + def test_csrftoken_in_header_api_endpoint_with_subpath(self): + url = reverse("api:form-list") + + response = self.client.get(url, SCRIPT_NAME="/of") + + self.assertIn(CSRF_TOKEN_HEADER_NAME, response.headers) + + def test_csrftoken_not_in_header_root_with_subpath(self): + response = self.client.get("/", SCRIPT_NAME="/of") + + self.assertNotIn(CSRF_TOKEN_HEADER_NAME, response.headers) + class CanNavigateBetweenStepsMiddlewareTests(TestCase): def test_header_api_endpoint_not_authenticated(self): @@ -63,3 +75,18 @@ def test_header_api_endpoint_staff_with_permissions(self): self.assertIn(IS_FORM_DESIGNER_HEADER_NAME, response.headers) self.assertEqual("true", response.headers[IS_FORM_DESIGNER_HEADER_NAME]) + + def test_header_not_api_endpoint_with_subpath(self): + response = self.client.get("/", SCRIPT_NAME="/of") + + self.assertNotIn(IS_FORM_DESIGNER_HEADER_NAME, response.headers) + + def test_header_api_endpoint_staff_with_permissions_with_subpath(self): + user = StaffUserFactory.create(user_permissions=["forms.change_form"]) + self.client.force_login(user=user) + url = reverse("api:form-list") + + response = self.client.get(url, SCRIPT_NAME="/of") + + self.assertIn(IS_FORM_DESIGNER_HEADER_NAME, response.headers) + self.assertEqual("true", response.headers[IS_FORM_DESIGNER_HEADER_NAME]) diff --git a/src/openforms/utils/tests/test_urls.py b/src/openforms/utils/tests/test_urls.py index d48cb9edfc..8890cdb0f3 100644 --- a/src/openforms/utils/tests/test_urls.py +++ b/src/openforms/utils/tests/test_urls.py @@ -159,3 +159,28 @@ def test_is_admin_request_direct(self): result = is_admin_request(request) self.assertTrue(result) + + def test_is_admin_request_true_with_subpath(self): + factory = RequestFactory() + request = factory.get( + "/api/v1/foo", + HTTP_REFERER="http://testserver/admin/forms/form/1/change/", + SCRIPT_NAME="/of", + ) + + self.assertTrue(is_admin_request(request)) + + def test_is_admin_request_false_with_subpath(self): + factory = RequestFactory() + bad_referers = ( + "http://otherdomain/bar/", + "http://otherdomain/admin/forms/form/", + ) + + for referer in bad_referers: + with self.subTest(referer=referer): + request = factory.get( + "/api/v1/foo", HTTP_REFERER=referer, SCRIPT_NAME="/of" + ) + + self.assertFalse(is_admin_request(request)) diff --git a/src/openforms/utils/urls.py b/src/openforms/utils/urls.py index 45eced233a..6d5245bca8 100644 --- a/src/openforms/utils/urls.py +++ b/src/openforms/utils/urls.py @@ -106,7 +106,7 @@ def is_admin_request(request: RequestType) -> bool: :arg request: the request object to be checked. """ admin_path_prefix = reverse("admin:index") - if request.path.startswith(admin_path_prefix): + if request.path_info.startswith(admin_path_prefix): return True if not (referrer := request.headers.get("Referer")): return False