From 163abf3441799a6a94f56de26d29779197ee70ef Mon Sep 17 00:00:00 2001 From: Giovanni Allegri Date: Fri, 15 May 2026 10:26:33 +0200 Subject: [PATCH 01/98] Upgrade to NGINX 1.31.0 (#14226) (cherry picked from commit d27537e612abeba3c9254a77d70d1c4bef69599d) --- docker-compose-dev.yml | 2 +- docker-compose-test.yml | 2 +- docker-compose.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index 90b46ad7bae..ab2398edafc 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -52,7 +52,7 @@ services: # Nginx is serving django static and media files and proxies to django and geonode geonode: - image: geonode/nginx:1.28.0-v1 + image: geonode/nginx:1.31.0-latest container_name: nginx4${COMPOSE_PROJECT_NAME} env_file: - .env diff --git a/docker-compose-test.yml b/docker-compose-test.yml index a5217585bc8..c1d018a3d64 100644 --- a/docker-compose-test.yml +++ b/docker-compose-test.yml @@ -40,7 +40,7 @@ services: # Nginx is serving django static and media files and proxies to django and geonode geonode: - image: geonode/nginx:1.28.0-v1 + image: geonode/nginx:1.31.0-latest container_name: nginx4${COMPOSE_PROJECT_NAME} env_file: - .env_test diff --git a/docker-compose.yml b/docker-compose.yml index d583ce337de..a0d35841bba 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -65,7 +65,7 @@ services: # Nginx is serving django static and media files and proxies to django and geonode geonode: - image: geonode/nginx:1.28.0-v1 + image: geonode/nginx:1.31.0-latest container_name: nginx4${COMPOSE_PROJECT_NAME} env_file: - .env From 02a2544c9fe878692e5715bf24fe60a5984385e3 Mon Sep 17 00:00:00 2001 From: "G.Allegri" Date: Fri, 15 May 2026 15:39:54 +0200 Subject: [PATCH 02/98] extende docs validation (cherry picked from commit 1d1f43f6dcab5c2d69725749f669d2b3d6d77e84) --- geonode/documents/forms.py | 19 ++++++++++++++++++ geonode/documents/tests.py | 33 ++++++++++++++++++++++++++++++++ geonode/upload/zip_validation.py | 8 ++++++-- 3 files changed, 58 insertions(+), 2 deletions(-) diff --git a/geonode/documents/forms.py b/geonode/documents/forms.py index 62b27562638..155e9cce6ae 100644 --- a/geonode/documents/forms.py +++ b/geonode/documents/forms.py @@ -30,6 +30,7 @@ from geonode.documents.models import Document from geonode.upload.models import UploadSizeLimit from geonode.upload.api.exceptions import FileUploadLimitException +from geonode.upload.zip_validation import ZipValidationError, is_zip_extension, validate_safe_zip logger = logging.getLogger(__name__) @@ -141,4 +142,22 @@ def clean_doc_file(self): logger.debug("This file type is not allowed") raise forms.ValidationError(_("This file type is not allowed")) + # Mirror the importer's zip-safety gate: inspect the central directory + # of any zip-based document upload (.zip plus OOXML / ODF formats) to + # reject path-traversal entries, symlinks, oversized archives and zip + # bombs before the file is persisted. + if doc_file and is_zip_extension(doc_file.name): + source = doc_file.temporary_file_path() if hasattr(doc_file, "temporary_file_path") else doc_file + try: + validate_safe_zip(source) + except ZipValidationError: + logger.warning("ZIP validation failed for uploaded document.", exc_info=True) + raise forms.ValidationError(_("Invalid or unsafe ZIP archive.")) + finally: + if hasattr(doc_file, "seek"): + try: + doc_file.seek(0) + except (OSError, ValueError): + pass + return doc_file diff --git a/geonode/documents/tests.py b/geonode/documents/tests.py index 278b1f46fb0..177d4fd6ff7 100644 --- a/geonode/documents/tests.py +++ b/geonode/documents/tests.py @@ -25,6 +25,7 @@ import os import io import json +import zipfile import gisdata from PIL import Image @@ -378,6 +379,38 @@ def test_upload_document_form_size_limit(self): ) self.assertEqual(form.errors, {"doc_file": [expected_error]}) + def test_upload_document_form_rejects_unsafe_zip(self): + """A zip-based document carrying a path-traversal entry must be rejected by the form.""" + buf = io.BytesIO() + with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as zf: + zf.writestr("../../etc/passwd", b"root:x:0:0") + buf.seek(0) + + form_data = { + "title": "Malicious archive", + "permissions": '{"anonymous":"document_readonly","authenticated":"resourcebase_readwrite","users":[]}', + } + file_data = {"doc_file": SimpleUploadedFile("evil.zip", buf.read(), "application/zip")} + form = DocumentCreateForm(form_data, file_data) + self.assertFalse(form.is_valid()) + self.assertIn("doc_file", form.errors) + self.assertIn("Invalid or unsafe ZIP archive.", str(form.errors["doc_file"])) + + def test_upload_document_form_accepts_clean_zip(self): + """A well-formed zip document must pass the safety check and validate.""" + buf = io.BytesIO() + with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as zf: + zf.writestr("readme.txt", b"hello") + buf.seek(0) + + form_data = { + "title": "Clean archive", + "permissions": '{"anonymous":"document_readonly","authenticated":"resourcebase_readwrite","users":[]}', + } + file_data = {"doc_file": SimpleUploadedFile("clean.zip", buf.read(), "application/zip")} + form = DocumentCreateForm(form_data, file_data) + self.assertTrue(form.is_valid(), msg=form.errors) + def test_document_embed(self): """/documents/1 -> Test accessing the embed view of a document""" d = Document.objects.all().first() diff --git a/geonode/upload/zip_validation.py b/geonode/upload/zip_validation.py index 9ae61f4c9a9..f951f6b0eb4 100644 --- a/geonode/upload/zip_validation.py +++ b/geonode/upload/zip_validation.py @@ -131,8 +131,12 @@ def _check_cumulative_ratio(total_compressed, total_uncompressed): def is_zip_extension(filename): - """Return True if the filename suggests a zip-based upload we should inspect.""" + """Return True if the filename suggests a zip-based upload we should inspect. + + Covers plain zips, the geospatial KMZ used by the importer, and the + zip-based Office Open XML / OpenDocument formats accepted as documents. + """ if not filename: return False ext = os.path.splitext(filename)[1].lower().lstrip(".") - return ext in {"zip", "kmz", "xlsx"} + return ext in {"zip", "kmz", "xlsx", "docx", "pptx", "ods", "odt", "odp"} From ae9469c7e611415b02fa5f0b4b4cb2a9b0a7fa66 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 May 2026 00:02:38 +0000 Subject: [PATCH 03/98] build(deps): bump django-allauth from 65.16.1 to 65.17.0 Bumps [django-allauth](https://github.com/sponsors/pennersr) from 65.16.1 to 65.17.0. - [Commits](https://github.com/sponsors/pennersr/commits) --- updated-dependencies: - dependency-name: django-allauth dependency-version: 65.17.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] (cherry picked from commit 92a21b036ddbe8094456bc80ce75559ba85a62f9) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 17712bd831d..e2de8322d59 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,7 +48,7 @@ dependencies = [ "zipstream-ng==1.8.0", # Django Apps - "django-allauth==65.16.1", + "django-allauth==65.17.0", "django-appconf==1.0.6", "django-filter==24.2", "django-imagekit==5.0.0", From 6ea701e12abf10f8c3eaba923adb748b4cf31f1b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 May 2026 00:03:42 +0000 Subject: [PATCH 04/98] build(deps): bump requests from 2.33.1 to 2.34.2 Bumps [requests](https://github.com/psf/requests) from 2.33.1 to 2.34.2. - [Release notes](https://github.com/psf/requests/releases) - [Changelog](https://github.com/psf/requests/blob/main/HISTORY.md) - [Commits](https://github.com/psf/requests/compare/v2.33.1...v2.34.2) --- updated-dependencies: - dependency-name: requests dependency-version: 2.34.2 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] (cherry picked from commit 440d2ae29b635cb33bd05712f7457bdd9a4fb111) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index e2de8322d59..e14d27e8f7c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -127,7 +127,7 @@ dependencies = [ "mock<6.0.0", "python-dateutil==2.9.0.post0", "pytz==2024.1", - "requests==2.33.1", + "requests==2.34.2", "timeout-decorator==0.5.0", "pylibmc==1.6.3", "sherlock==0.4.1", From bd69c1da7d5e0e55732e63a2c4dc3ae487d34a2b Mon Sep 17 00:00:00 2001 From: EHJ-52n Date: Mon, 20 Apr 2026 12:21:03 +0200 Subject: [PATCH 05/98] Add read DEFAULT_MAX_UPLOAD_SIZE from environment (cherry picked from commit e2e365c57c0fe2c539b98d8415568b8ea442e1f6) --- geonode/settings.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/geonode/settings.py b/geonode/settings.py index 2d5338be81b..1a4646e4a2b 100644 --- a/geonode/settings.py +++ b/geonode/settings.py @@ -2206,7 +2206,13 @@ def get_geonode_catalogue_service(): "django.core.files.uploadhandler.MemoryFileUploadHandler", ] -DEFAULT_MAX_UPLOAD_SIZE = 104857600 # 100 MB +# This setting is used only once during installation. +# Further adjustments need to be done using the admin WebUI: +# +# https://docs.geonode.org/en/5.0.x/admin/upload-size-limits/index.html#upload-size-limits +# +# 100MB +DEFAULT_MAX_UPLOAD_SIZE = int(os.getenv("DEFAULT_MAX_UPLOAD_SIZE") or 100 * 1024 * 1024) DEFAULT_BUFFER_CHUNK_SIZE = int(os.getenv("DEFAULT_BUFFER_CHUNK_SIZE", 64 * 1024)) DEFAULT_MAX_PARALLEL_UPLOADS_PER_USER = int(os.getenv("DEFAULT_MAX_PARALLEL_UPLOADS_PER_USER", 5)) From 32a8636531f92eeafb7154aada178918b0b50a20 Mon Sep 17 00:00:00 2001 From: mattiagiupponi <51856725+mattiagiupponi@users.noreply.github.com> Date: Wed, 27 May 2026 11:44:00 +0200 Subject: [PATCH 06/98] backport pillow upgrade (#14269) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index e14d27e8f7c..c677d198208 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ classifiers = [ # Core dependencies (consolidated from requirements.txt) dependencies = [ - "Pillow==12.1.1", + "Pillow==12.2.0", "lxml==5.2.1", "psycopg2==2.9.12", "Django==5.2.14", From 91589477f3693504554512455f70ac0ddf441273 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 27 May 2026 11:46:06 +0200 Subject: [PATCH 07/98] [Fixes #14244] Do not return WWW-Authenticate inside API exception response (#14265) (#14272) * Do not return WWW-Authenticate inside API exception response * Test WWW-Authenticate stripped * Fix test docstring (cherry picked from commit 3c9b610051c6f7d9bb1178fa851d1d513dcd945d) Co-authored-by: Giovanni Allegri --- geonode/base/api/exceptions.py | 20 ++++++++++++-------- geonode/base/api/tests.py | 15 +++++++++++++++ 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/geonode/base/api/exceptions.py b/geonode/base/api/exceptions.py index 32812298acc..c3e8cf3548b 100644 --- a/geonode/base/api/exceptions.py +++ b/geonode/base/api/exceptions.py @@ -32,14 +32,18 @@ def geonode_exception_handler(exc, context): # to get the standard error response. response = exception_handler(exc, context) - if response is not None and isinstance(exc, APIException): - # for the upload exception we need a custom response - detail = _extract_detail(exc) - response.data = { - "success": False, - "errors": [str(detail)], - "code": exc.code if hasattr(exc, "code") else exc.default_code, - } + if response is not None: + if response.status_code == 401: + response.headers.pop("WWW-Authenticate", None) + + if isinstance(exc, APIException): + # for the upload exception we need a custom response + detail = _extract_detail(exc) + response.data = { + "success": False, + "errors": [str(detail)], + "code": exc.code if hasattr(exc, "code") else exc.default_code, + } return response diff --git a/geonode/base/api/tests.py b/geonode/base/api/tests.py index 19fc9d49f87..eafbab419b6 100644 --- a/geonode/base/api/tests.py +++ b/geonode/base/api/tests.py @@ -3296,6 +3296,21 @@ def test_metadata_uploaded_preserve_can_be_updated(self): self.assertTrue(doc.metadata_uploaded_preserve) self.assertTrue(response.json()["resource"]["metadata_uploaded_preserve"]) + def test_www_authenticate_header_is_removed_for_401_responses(self): + """ + Ensure WWW-Authenticate header is removed for 401 responses to prevent browsers from showing a login prompt + """ + try: + user = get_user_model().objects.create_user( + username="user_test_delete", email="user_test_delete@geonode.org", password="user" + ) + url = reverse("users-detail", kwargs={"pk": user.pk}) + # Anonymous can't read + response = self.client.get(url, format="json") + self.assertNotIn("WWW-Authenticate", response.headers) + finally: + user.delete() + class TestExtraMetadataBaseApi(GeoNodeBaseTestSupport): def setUp(self): From c9c657bc2c032d04edf80bd0068f1a619af3fee9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 27 May 2026 12:51:46 +0200 Subject: [PATCH 08/98] [Fixes #14262] Avoid returning all permissions in dataset owner payload (#14263) (#14273) (cherry picked from commit e9553e8eb9279bfed92e004069e67ab484c12678) Co-authored-by: Sijan Dhungana --- geonode/security/registry.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/geonode/security/registry.py b/geonode/security/registry.py index a8ee3bc0e96..7a83de590d7 100644 --- a/geonode/security/registry.py +++ b/geonode/security/registry.py @@ -433,9 +433,6 @@ def _get_global_perms(self, user): if codename in PERMISSIONS: # return constant names defined by GeoNode perms.add(PERMISSIONS[codename]) - else: - # add custom permissions - perms.add(codename) # Create a synthetic permission for adding remote resources if user.is_superuser or user.is_staff or getattr(settings, "REGISTERED_USERS_CAN_ADD_REMOTE_RESOURCES", False): From 979d760a23bb8e7eee4932098c2d183269c97878 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 27 May 2026 18:20:30 +0200 Subject: [PATCH 09/98] [Fixes #13512] Create a task that loads required thesauri automatically (#14187) * Add autoload subcommand to thesaurus management command, task in tasks.py, and entrypoint call * Add documentation for thesaurus autoload command and boot-time thesauri loading * Move autoload_thesauri into its own thesaurus_subcommands/autoload.py * Log improvements, lang selection * Apply suggestions from code review * Fix mutable default argument for langs parameter in load_thesaurus * Fix patch apps.get_app_configs in correct autoload module * Fix code formatting * Improve load final log Co-authored-by: etj <717359+etj@users.noreply.github.com> Co-authored-by: Emanuele Tajariol (cherry picked from commit 016fdd99e3962862bbc20be6a3a12891620cc979) --- docs/src/admin/thesauri/thesauri.md | 65 ++++++++- .../docker/vanilla-docker-installation.md | 9 ++ entrypoint.sh | 1 + geonode/base/management/commands/thesaurus.py | 17 ++- .../thesaurus_subcommands/autoload.py | 39 ++++++ .../commands/thesaurus_subcommands/load.py | 57 +++++--- geonode/tests/test_autoload_thesaurus.py | 124 ++++++++++++++++++ tasks.py | 9 ++ 8 files changed, 298 insertions(+), 23 deletions(-) create mode 100644 geonode/base/management/commands/thesaurus_subcommands/autoload.py create mode 100644 geonode/tests/test_autoload_thesaurus.py diff --git a/docs/src/admin/thesauri/thesauri.md b/docs/src/admin/thesauri/thesauri.md index 59f0577105d..74f3996deee 100644 --- a/docs/src/admin/thesauri/thesauri.md +++ b/docs/src/admin/thesauri/thesauri.md @@ -92,6 +92,7 @@ GeoNode provides a single command (``thesaurus``) with multiple actions: * ``list``: list existing thesauri * ``load``: load a RDF file * ``dump``: dump a thesaurus into a file +* ``autoload``: automatically discover and load all thesauri shipped by installed apps .. code-block:: @@ -102,12 +103,13 @@ GeoNode provides a single command (``thesaurus``) with multiple actions: [--format {json-ld,n3,nt,pretty-xml,sorted-xml,trig,ttl,xml}] [--default-lang LANG] [--version] [-v {0,1,2,3}] [--settings SETTINGS] [--pythonpath PYTHONPATH] [--traceback] [--no-color] [--force-color] [--skip-checks] - [{list,load,dump}] + [{list,load,dump,autoload}] - Handles thesaurus commands ['list', 'load', 'dump'] + Handles thesaurus commands ['list', 'load', 'dump', 'autoload'] positional arguments: - {list,load,dump} thesaurus operation to run + {list,load,dump,autoload} + thesaurus operation to run options: -h, --help show this help message and exit @@ -227,6 +229,63 @@ In order to only export the entries we edited, we'll issue the command:: python manage.py thesaurus dump -i labels-i18n --include "proj1_*" --include "*_ovr" -f labels-i18n.proj1.rdf +### Auto-loading thesauri: ``thesaurus autoload`` + +The ``autoload`` subcommand scans every installed Django app for a ``thesauri/`` directory +at the top level of the app package, then loads all ``.rdf`` files it finds there. +This is how GeoNode and third-party apps can ship thesauri that are loaded automatically at start-up. + +```bash +python manage.py thesaurus autoload +``` + +For each ``.rdf`` file discovered, the command runs the equivalent of ``thesaurus load --action update``, +so the operation is **idempotent**: running it multiple times will not create duplicates; instead, +existing records are updated and missing ones are created. + +**Convention for app-provided thesauri** + +Place one or more ``.rdf`` files inside a ``thesauri/`` directory at the root of your app package: + +``` +my_geonode_app/ + thesauri/ + my_vocabulary.rdf + another_vocab.rdf + models.py + ... +``` + +All ``.rdf`` files in that directory are picked up automatically whenever ``thesaurus autoload`` +(or ``invoke loadthesauri``) is executed. + +!!! note + The ``autoload`` command is automatically run during GeoNode's Docker container start-up sequence (see [Initialization at boot](#initialization-at-boot)). + + +## Initialization at boot { #initialization-at-boot } + +When GeoNode starts (e.g. via the Docker entrypoint), the following initialization steps are executed in order: + +1. **Database migrations** – applies any pending schema migrations. +2. **Fixtures** – loads default OAuth2 apps, admin user, and site data (only on first boot or when ``FORCE_REINIT=true``). +3. **Static files** – collects static assets. +4. **Thesauri autoload** – runs ``thesaurus autoload`` to load or update all ``.rdf`` files found in any installed app's ``thesauri/`` directory. This step runs on **every** boot so that thesaurus updates shipped with an upgraded app are applied automatically. + +To run the thesaurus autoload step manually: + +```bash +# Inside the GeoNode container +python manage.py thesaurus autoload +``` + +Or using the invoke task: + +```bash +invoke loadthesauri +``` + + ## Configuring a Thesaurus diff --git a/docs/src/setup/docker/vanilla-docker-installation.md b/docs/src/setup/docker/vanilla-docker-installation.md index 5136c1a2d0d..bef557550a8 100644 --- a/docs/src/setup/docker/vanilla-docker-installation.md +++ b/docs/src/setup/docker/vanilla-docker-installation.md @@ -81,6 +81,15 @@ Executing UWSGI server uwsgi --ini /usr/src/app/uwsgi.ini for Production [uWSGI] getting INI configuration from /usr/src/app/uwsgi.ini ``` +The container performs these initialization steps before starting the application server: + +1. **Database migrations** – applies any pending schema migrations. +2. **Fixtures** – loads default OAuth2 apps, admin user and site data (only on first boot or when ``FORCE_REINIT=true``). +3. **Static files** – collects static assets. +4. **Thesauri autoload** – scans all installed apps for a ``thesauri/`` directory and loads (or updates) any ``.rdf`` files found there. This makes sure thesauri shipped by GeoNode apps are always up-to-date. + +See [Thesauri – Initialization at boot](../../../admin/thesauri/thesauri.md#initialization-at-boot) for more details on the thesaurus autoload step. + To exit just hit `CTRL+C`. This message means that the GeoNode containers have been started. Browsing to `http://localhost/` will show the GeoNode home page. You should be able to successfully log with the credentials of admin user which are defined in the .env file and start using it right away. diff --git a/entrypoint.sh b/entrypoint.sh index ed9469efac2..18c8ab5269f 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -56,6 +56,7 @@ else fi invoke statics + invoke loadthesauri echo "Executing UWSGI server $cmd for Production" fi diff --git a/geonode/base/management/commands/thesaurus.py b/geonode/base/management/commands/thesaurus.py index 95802eaf451..c7cfd9d0c1c 100644 --- a/geonode/base/management/commands/thesaurus.py +++ b/geonode/base/management/commands/thesaurus.py @@ -2,6 +2,7 @@ from django.core.management.base import BaseCommand, CommandError from geonode.base.management.command_utils import setup_logger +from geonode.base.management.commands.thesaurus_subcommands.autoload import autoload_thesauri from geonode.base.management.commands.thesaurus_subcommands.dump import ( dump_thesaurus, DUMP_FORMATS, @@ -16,7 +17,8 @@ COMMAND_LIST = "list" COMMAND_DUMP = "dump" COMMAND_LOAD = "load" -COMMANDS = [COMMAND_LIST, COMMAND_LOAD, COMMAND_DUMP] +COMMAND_AUTOLOAD = "autoload" +COMMANDS = [COMMAND_LIST, COMMAND_LOAD, COMMAND_DUMP, COMMAND_AUTOLOAD] class Command(BaseCommand): @@ -41,6 +43,12 @@ def add_arguments(self, parser): choices=ACTIONS, help="Actions to run upon data loading (default: create)", ) + load_group.add_argument( + "--langs", + dest="langs", + action="append", + help="Only import labels for the requested languages; can be repeated", + ) dump_group = parser.add_argument_group('Params for "dump" subcommand') dump_group.add_argument("-o", "--out", nargs="?", help="Full path to the output file to be created") @@ -99,6 +107,8 @@ def handle(self, *args, **options): input_file = options.get("file") action = options.get("action") identifier = options.get("identifier") + lang = options.get("lang") + langs = options.get("langs") or [] if not input_file: raise CommandError("'load' command requires the parameter.") @@ -107,7 +117,10 @@ def handle(self, *args, **options): action = ACTION_CREATE logger.info(f"Missing action param: setting actions as '{action}'") - load_thesaurus(input_file, identifier, action) + load_thesaurus(input_file, identifier, action, default_lang=lang, langs=langs) + + elif subcommand == COMMAND_AUTOLOAD: + autoload_thesauri() else: raise CommandError(f"Unknown subcommand: {subcommand}") diff --git a/geonode/base/management/commands/thesaurus_subcommands/autoload.py b/geonode/base/management/commands/thesaurus_subcommands/autoload.py new file mode 100644 index 00000000000..df2725d6dfa --- /dev/null +++ b/geonode/base/management/commands/thesaurus_subcommands/autoload.py @@ -0,0 +1,39 @@ +import os + +from django.apps import apps + +from geonode.base.management.command_utils import setup_logger +from geonode.base.management.commands.thesaurus_subcommands.load import load_thesaurus, ACTION_UPDATE + +logger = setup_logger() + + +def autoload_thesauri(): + """ + Discover and load all thesauri (.rdf files) found in a `thesauri/` directory + within each installed Django app. Uses the `update` action so existing entries + are updated and new ones are created without duplicates. + """ + loaded = 0 + for app_config in apps.get_app_configs(): + thesauri_dir = os.path.join(app_config.path, "thesauri") + logger.debug(f"Looking for auto thesaurus in app '{app_config.name}' path: {thesauri_dir}") + if not os.path.isdir(thesauri_dir): + continue + try: + rdf_files = [f for f in os.listdir(thesauri_dir) if f.lower().endswith(".rdf")] + except OSError as e: + logger.error( + f"Failed to scan thesauri directory for app '{app_config.name}' at '{thesauri_dir}': {e}", + exc_info=True, + ) + continue + for rdf_file in sorted(rdf_files): + rdf_path = os.path.join(thesauri_dir, rdf_file) + logger.info(f"Autoloading thesaurus from app '{app_config.name}': {rdf_path}") + try: + load_thesaurus(rdf_path, identifier=None, action=ACTION_UPDATE, log_details=False) + loaded += 1 + except Exception as e: + logger.error(f"Failed to load thesaurus '{rdf_path}': {e}", exc_info=True) + logger.info(f"Autoload complete: {loaded} thesaurus file(s) loaded.") diff --git a/geonode/base/management/commands/thesaurus_subcommands/load.py b/geonode/base/management/commands/thesaurus_subcommands/load.py index 09498010a6a..d64d664cf45 100644 --- a/geonode/base/management/commands/thesaurus_subcommands/load.py +++ b/geonode/base/management/commands/thesaurus_subcommands/load.py @@ -45,13 +45,16 @@ FAKE_BASE_URI = "http://automatically/added/uri/" -def load_thesaurus(input_file, identifier: str, action: str = ACTION_CREATE): +def load_thesaurus(input_file, identifier: str, action: str = ACTION_CREATE, default_lang: str = None, langs: List[str] = None, log_details=True): g = Graph() # if the input_file is an UploadedFile object rather than a file path the Graph.parse() # method may not have enough info to correctly guess the type; in this case supply the # name, which should include the extension, to guess_format manually... + # explodes list of comma separated langs into single list of langs + langs = [lang.strip() for item in (langs or []) for lang in item.split(",") if lang.strip()] + filename = input_file.name if isinstance(input_file, UploadedFile) else input_file rdf_format = guess_format(filename) if not identifier: @@ -65,7 +68,7 @@ def load_thesaurus(input_file, identifier: str, action: str = ACTION_CREATE): if scheme is None: raise CommandError("ConceptScheme not found in file") - default_lang = getattr(settings, "THESAURUS_DEFAULT_LANG", None) + default_lang = default_lang or getattr(settings, "THESAURUS_DEFAULT_LANG", None) or getattr(settings, "LANGUAGE_CODE", 'en') available_titles = [t for t in itertools.chain(g.objects(scheme, DC.title), g.objects(scheme, DCTERMS.title)) @@ -81,15 +84,21 @@ def load_thesaurus(input_file, identifier: str, action: str = ACTION_CREATE): Thesaurus, {"identifier": identifier}, {"date": date_issued, "description": description, "title": thesaurus_title, "about": str(scheme)}, - {"card_min": 0, "card_max": 0, "facet": False} + {"card_min": 0, "card_max": 0, "facet": False}, + log_details ) - tl_cnt = tl_add = 0 + tl_cnt = tl_add = tl_skp = 0 tk_cnt = tk_add = 0 - tkl_cnt = tkl_add = 0 + tkl_cnt = tkl_add = tkl_skp = 0 for lang in available_titles: if lang.language is not None: + tl_cnt += 1 + if langs and lang.language not in langs: + logger.debug(f"Skipping thesaurus label for language '{lang.language}' not in requested langs {langs}") + tl_skp += 1 + continue thesaurus_label, c = _run_action( action, ThesaurusLabel, @@ -99,8 +108,8 @@ def load_thesaurus(input_file, identifier: str, action: str = ACTION_CREATE): }, {"label": lang.value}, {}, + log_details ) - tl_cnt += 1 tl_add += 1 if c else 0 for concept in g.subjects(RDF.type, SKOS.Concept): @@ -115,7 +124,8 @@ def load_thesaurus(input_file, identifier: str, action: str = ACTION_CREATE): available_labels = [t for t in g.objects(concept, SKOS.prefLabel) if isinstance(t, Literal)] alt_label = value_for_language(available_labels, default_lang) or about - logger.info(f" - Parsed Concept -> about:'{about}' alt:'{alt_label}' pref:'{str(pref)}' ") + if log_details: + logger.info(f" - Parsed Concept -> about:'{about}' alt:'{alt_label}' pref:'{str(pref)}' ") tk, c = _run_action( action, @@ -126,14 +136,21 @@ def load_thesaurus(input_file, identifier: str, action: str = ACTION_CREATE): }, {"alt_label": alt_label}, {}, + log_details ) tk_cnt += 1 tk_add += 1 if c else 0 for _, pref_label in preferredLabel(g, concept): + tkl_cnt += 1 lang = pref_label.language + if langs and lang not in langs: + logger.debug(f"Skipping label for language '{lang}' not in requested langs {langs}") + tkl_skp += 1 + continue label = str(pref_label) - logger.info(f" - Label {lang}: {label}") + if log_details: + logger.info(f" - Label {lang}: {label}") tkl, c = _run_action( action, @@ -144,17 +161,17 @@ def load_thesaurus(input_file, identifier: str, action: str = ACTION_CREATE): }, {"label": label}, {}, + log_details ) - tkl_cnt += 1 tkl_add += 1 if c else 0 - logger.warning(f"Thesaurus added: {cr_t}") - logger.warning(f"ThesaurusLabel added: {tl_add:3}/{tl_cnt:3}") - logger.warning(f"ThesaurusKeyword added: {tk_add:3}/{tk_cnt:3}") - logger.warning(f"ThesaurusKeywordLabel added: {tkl_add:3}/{tkl_cnt:3}") + logger.warning(f"Thesaurus added: {cr_t}") + logger.warning(f"ThesaurusLabel: found: {tl_cnt:3} - added: {tl_add:3} - skipped: {tl_skp:3}") + logger.warning(f"ThesaurusKeyword: found: {tk_cnt:3} - added: {tk_add:3}") + logger.warning(f"ThesaurusKeywordLabel: found: {tkl_cnt:3} - added: {tkl_add:3} - skipped: {tkl_skp:3}") -def _run_action(action: str, model: type[models.Model], pk_dict, upd_dict, create_dict) -> tuple[models.Model, bool]: +def _run_action(action: str, model: type[models.Model], pk_dict, upd_dict, create_dict, log_details) -> tuple[models.Model, bool]: def update_or_create(defaults=upd_dict, create_defaults=create_dict, **pk_dict): # this signature is available since django 5 obj, created = model.objects.get_or_create(defaults=upd_dict | create_dict, **pk_dict) @@ -162,7 +179,8 @@ def update_or_create(defaults=upd_dict, create_defaults=create_dict, **pk_dict): if not created: rows = model.objects.filter(pk=obj.pk).update(**upd_dict) if rows != 1: - logger.error(f"UPDATED {rows} rows for {model.__name__} -> {pk_dict}") + if log_details: + logger.error(f"UPDATED {rows} rows for {model.__name__} -> {pk_dict}") return obj, created @@ -176,14 +194,17 @@ def update_or_create(defaults=upd_dict, create_defaults=create_dict, **pk_dict): elif action == ACTION_UPDATE: obj, created = update_or_create(defaults=upd_dict, create_defaults=create_dict, **pk_dict) if created: - logger.info(f"{model.__name__} -> Created id:{pk_dict}") + if log_details: + logger.info(f"{model.__name__} -> Created id:{pk_dict}") else: - logger.info(f"{model.__name__} -> Updated id:{pk_dict} DATA:{upd_dict}") + if log_details: + logger.info(f"{model.__name__} -> Updated id:{pk_dict} DATA:{upd_dict}") elif action == ACTION_APPEND: obj, created = model.objects.get_or_create(defaults=upd_dict | create_dict, **pk_dict) if created: - logger.info(f"{model.__name__} -> Created {pk_dict}") + if log_details: + logger.info(f"{model.__name__} -> Created {pk_dict}") else: raise CommandError("No valid action found") diff --git a/geonode/tests/test_autoload_thesaurus.py b/geonode/tests/test_autoload_thesaurus.py new file mode 100644 index 00000000000..a0c60e49689 --- /dev/null +++ b/geonode/tests/test_autoload_thesaurus.py @@ -0,0 +1,124 @@ +######################################################################### +# +# Copyright (C) 2016 OSGeo +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +######################################################################### + +import os +import shutil +import tempfile +from unittest.mock import patch, MagicMock + +from django.test import TestCase + +from geonode.base.management.commands.thesaurus_subcommands.autoload import autoload_thesauri +from geonode.base.models import Thesaurus + + +RDF_CONTENT = """\ + + + + Autoload Test Thesaurus + 2024-01-01 + + + Concept One + + + +""" + + +class TestAutoloadThesauri(TestCase): + def setUp(self): + self.tmp_dir = tempfile.mkdtemp() + self.thesauri_dir = os.path.join(self.tmp_dir, "thesauri") + os.makedirs(self.thesauri_dir) + self.rdf_file = os.path.join(self.thesauri_dir, "autoload_test.rdf") + with open(self.rdf_file, "w", encoding="utf-8") as f: + f.write(RDF_CONTENT) + + def tearDown(self): + shutil.rmtree(self.tmp_dir, ignore_errors=True) + + def _make_app_config(self, name, path): + app_config = MagicMock() + app_config.name = name + app_config.path = path + return app_config + + def test_autoload_loads_rdf_files_from_thesauri_dirs(self): + """autoload_thesauri should load .rdf files found in thesauri/ dirs of installed apps.""" + app_configs = [self._make_app_config("fake_app", self.tmp_dir)] + with patch( + "geonode.base.management.commands.thesaurus_subcommands.autoload.apps.get_app_configs", + return_value=app_configs, + ): + autoload_thesauri() + + self.assertTrue( + Thesaurus.objects.filter(about="http://example.com/autoload-test-scheme").exists(), + "The thesaurus should have been loaded from the app's thesauri/ directory", + ) + + def test_autoload_is_idempotent(self): + """Calling autoload_thesauri twice should not create duplicate thesauri (uses update action).""" + app_configs = [self._make_app_config("fake_app", self.tmp_dir)] + with patch( + "geonode.base.management.commands.thesaurus_subcommands.autoload.apps.get_app_configs", + return_value=app_configs, + ): + autoload_thesauri() + autoload_thesauri() + + count = Thesaurus.objects.filter(about="http://example.com/autoload-test-scheme").count() + self.assertEqual(1, count, "Running autoload twice should not create duplicate thesauri") + + def test_autoload_skips_apps_without_thesauri_dir(self): + """autoload_thesauri should silently skip apps that have no thesauri/ directory.""" + app_without_thesauri = self._make_app_config("no_thesauri_app", self.tmp_dir.rstrip("/") + "_no_dir") + app_configs = [app_without_thesauri] + with patch( + "geonode.base.management.commands.thesaurus_subcommands.autoload.apps.get_app_configs", + return_value=app_configs, + ): + # Should not raise + autoload_thesauri() + + self.assertEqual(0, Thesaurus.objects.count(), "No thesauri should be loaded when thesauri/ dir is absent") + + def test_autoload_continues_after_error(self): + """autoload_thesauri should continue loading other files if one fails.""" + bad_rdf = os.path.join(self.thesauri_dir, "aaaa_bad.rdf") + with open(bad_rdf, "w") as f: + f.write("THIS IS NOT VALID RDF") + + app_configs = [self._make_app_config("fake_app", self.tmp_dir)] + with patch( + "geonode.base.management.commands.thesaurus_subcommands.autoload.apps.get_app_configs", + return_value=app_configs, + ): + # Should not raise despite the bad file + autoload_thesauri() + + # The valid thesaurus (sorted after the bad one alphabetically: autoload_test > aaaa_bad) should still load + self.assertTrue( + Thesaurus.objects.filter(about="http://example.com/autoload-test-scheme").exists(), + "Valid thesaurus should be loaded even if another file in the same dir fails", + ) diff --git a/tasks.py b/tasks.py index 4f5585ec6aa..86513d3380b 100755 --- a/tasks.py +++ b/tasks.py @@ -392,6 +392,15 @@ def updateadmin(ctx): ) +@task +def loadthesauri(ctx): + print("**************************thesauri*******************************") + ctx.run( + f"python manage.py thesaurus autoload --settings={_localsettings()}", + pty=True, + ) + + @task def collectmetrics(ctx): print("************************collect metrics******************************") From c860a307780973ede6b9fbada4dec9761bdbcc3e Mon Sep 17 00:00:00 2001 From: gpetrak Date: Fri, 29 May 2026 16:12:46 +0300 Subject: [PATCH 10/98] adding improvements in the new docs (cherry picked from commit 08bd4c781e7db0635159e3a47f95d25a5778028c) --- docs/mkdocs.yml | 5 +- .../admin/admin_panel/admin_panel_theming.md | 4 +- docs/src/admin/admin_panel/group.md | 28 +++---- docs/src/admin/admin_panel/oauth.md | 22 ++--- docs/src/admin/default_lang.md | 75 ------------------ docs/src/overview/basics.md | 15 +--- docs/src/overview/img/geonode.png | Bin 23203 -> 0 bytes docs/src/overview/img/gn_is_made_for.png | Bin 130895 -> 0 bytes docs/src/overview/img/gn_publication_data.png | Bin 187248 -> 0 bytes .../overview/img/gn_publication_data_2.png | Bin 82376 -> 0 bytes .../img/gn_simplified_architecture.png | Bin 75365 -> 0 bytes .../setup/bare/vanilla-bare-installation.md | 16 ++-- .../docker/project-docker-installation.md | 5 +- .../prod_enhancements/prod_enhancements.md | 12 +-- docs/src/user-guide/default-language.md | 62 +++++++++++++++ .../user-guide/img/profile_lang_switcher.png | Bin 0 -> 3764 bytes .../img/topbar_language_switcher.png | Bin 0 -> 8367 bytes 17 files changed, 112 insertions(+), 132 deletions(-) delete mode 100644 docs/src/admin/default_lang.md delete mode 100644 docs/src/overview/img/geonode.png delete mode 100644 docs/src/overview/img/gn_is_made_for.png delete mode 100644 docs/src/overview/img/gn_publication_data.png delete mode 100644 docs/src/overview/img/gn_publication_data_2.png delete mode 100644 docs/src/overview/img/gn_simplified_architecture.png create mode 100644 docs/src/user-guide/default-language.md create mode 100644 docs/src/user-guide/img/profile_lang_switcher.png create mode 100644 docs/src/user-guide/img/topbar_language_switcher.png diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 8121b37edda..f8eed73595a 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -63,12 +63,12 @@ nav: - Attributes Table: "user-guide/maps/maps_configuration/attribute_table.md" - Timeline: "user-guide/maps/maps_configuration/timeline.md" - Other Menu Tools: "user-guide/maps/maps_configuration/options_menu.md" - - Documents: - - Upload/Add Documents: "user-guide/documents/upload.md" + - Documents: "user-guide/documents/upload.md" - GeoStories: "user-guide/geostory/geostory.md" - Dashboards: "user-guide/dashboard/dashboard.md" - Styling: "user-guide/resource_styling.md" - Sharing: "user-guide/sharing.md" + - Default Language: "user-guide/default-language.md" - Metadata: "user-guide/metadata.md" - Administration: - GeoNode Admins Guide: @@ -104,7 +104,6 @@ nav: - Customize the look and feel: - GeoNode Themes: "admin/gnode_theme/geonode_themes.md" - Theming your GeoNode Project: "admin/gnode_theme/theme_geonode_project.md" - - Changing the Default Language: "admin/default_lang.md" - Thesauri: "admin/thesauri/thesauri.md" - Developer Guide: - API Usage: "development/api.md" diff --git a/docs/src/admin/admin_panel/admin_panel_theming.md b/docs/src/admin/admin_panel/admin_panel_theming.md index f9edaf45041..b19f989839c 100644 --- a/docs/src/admin/admin_panel/admin_panel_theming.md +++ b/docs/src/admin/admin_panel/admin_panel_theming.md @@ -30,8 +30,6 @@ Just below the `Description` field, you will find the `Enabled` checkbox, which *Theme Name and Description* /// -## Jumbotron and Get Started link - !!! Note Remember, every time you want to apply some changes to the theme, you **must** save the theme and reload the GeoNode browser tab. @@ -39,6 +37,8 @@ Just below the `Description` field, you will find the `Enabled` checkbox, which ![](img/theming/view_site.png){ align=center } +## Jumbotron and Get Started link + The next section allows you to define the first important theme properties. This part involves the GeoNode main page sections. ![](img/theming/theme_properties.png){ align=center } diff --git a/docs/src/admin/admin_panel/group.md b/docs/src/admin/admin_panel/group.md index 10c2cbea526..37738a241ec 100644 --- a/docs/src/admin/admin_panel/group.md +++ b/docs/src/admin/admin_panel/group.md @@ -1,7 +1,7 @@ ## Creating a Group -| In GeoNode is possible to create new groups with set of permissions which will be inherited by all the group members. -| The creation of a Group can be done both on the GeoNode UI and on the *Admin Panel*, we will explain how in this paragraph. +In GeoNode is possible to create new groups with set of permissions which will be inherited by all the group members. +The creation of a Group can be done both on the GeoNode UI and on the *Admin Panel*, we will explain how in this paragraph. The `Create Groups` link of *About* menu in the navigation bar allows administrators to reach the *Group Creation Page*. @@ -26,9 +26,9 @@ The new created group will be searchable in the *Groups List Page*. !!! note "Important notes" The `Create a New Group` button on the *Groups List Page* allows to reach the *Group Creation Form*. -| As already mentioned above, groups can also be created from the Django-based *Admin Interface* of GeoNode. -| The *Groups* link of the *AUTHENTICATION AND AUTHORIZATION* section allows to manage basic Django groups which only care about permissions. -| To create a GeoNode group you should take a look at the *GROUPS* section. +As already mentioned above, groups can also be created from the Django-based *Admin Interface* of GeoNode. +The *Groups* link of the *AUTHENTICATION AND AUTHORIZATION* section allows to manage basic Django groups which only care about permissions. +To create a GeoNode group you should take a look at the *GROUPS* section. ![groups_admin_section](img/groups_admin_section.png) @@ -143,8 +143,8 @@ On the *Group Profile Form* page you can insert a logo from your disk by click o ![editing_group_logo](img/editing_group_logo.png) -| Click on `Update` to apply the changes. -| Take a look at your group now, you should be able to see that logo. +Click on `Update` to apply the changes. +Take a look at your group now, you should be able to see that logo. ![group_logo](img/group_logo.png) @@ -156,12 +156,12 @@ The `Manage Group Members` link opens the *Group Members Page* which shows *Grou **Managers** can edit group details, can delete the group, can see the group activities and can manage memberships. Other **Members** can only see the group activities. -| In Public Groups, users can join the group without any approval. - Other types of groups require the user to be invited by the group managers. -| Only group managers can *Add new members*. - In the picture below, you can see the manager can search for users by typing their names into the *User Identifiers* search bar. - Once found, he can add them to the group by clicking the `Add Group Members` button. - The *Assign manager role* flag implies that all the users found will become managers of the group. +In Public Groups, users can join the group without any approval. +Other types of groups require the user to be invited by the group managers. +Only group managers can *Add new members*. +In the picture below, you can see the manager can search for users by typing their names into the *User Identifiers* search bar. +Once found, he can add them to the group by clicking the `Add Group Members` button. +The *Assign manager role* flag implies that all the users found will become managers of the group. ![add_new_member](img/add_new_member.png) @@ -171,4 +171,4 @@ The following picture shows you the results. ![new_members](img/new_members.png) -If you want to change the role of group members after adding them, you can use the "promote" button to make a member into a manager, and the "demote" button to make a manager into a regular member. \ No newline at end of file +If you want to change the role of group members after adding them, you can use the "promote" button to make a member into a manager, and the "demote" button to make a manager into a regular member. diff --git a/docs/src/admin/admin_panel/oauth.md b/docs/src/admin/admin_panel/oauth.md index d5c510afbef..a9250e87024 100644 --- a/docs/src/admin/admin_panel/oauth.md +++ b/docs/src/admin/admin_panel/oauth.md @@ -1,11 +1,11 @@ # OAuth2 Access Tokens This small section won't cover entirely the GeoNode OAuth2 security integration, this is explained in detail in other sections of the documentation -(refer to :ref:`oauth2_fixtures_and_migration` and :ref:`oauth2_tokens_and_sessions`). +(refer to the [Django OAuth Toolkit Admin Setup](../../setup/configuration/components/geonode_security_backend/geonode_security_backend.md#django-oauth-toolkit-admin-setup) and [GeoNode / GeoServer Authentication Mechanism](../../setup/configuration/components/geonode_security_backend/geonode_security_backend.md#geonode-geoserver-authentication-mechanism) sections). -Here we will focus mainly on the :guilabel:`Admin > DJANGO/GEONODE OAUTH TOOLKIT` panel items with a specific attention to the ``Access tokens`` management. +Here we will focus mainly on the `Admin > DJANGO/GEONODE OAUTH TOOLKIT` panel items with a specific attention to the ``Access tokens`` management. -The :guilabel:`Admin > DJANGO/GEONODE OAUTH TOOLKIT` panel (as shown in the figure below) allows an admin to manage everything related to +The `Admin > DJANGO/GEONODE OAUTH TOOLKIT` panel (as shown in the figure below) allows an admin to manage everything related to GeoNode OAuth2 grants and permissions. As better explained in other sections of the documentation, this is needed to correctly handle the communication between GeoNode and GeoServer. @@ -14,24 +14,24 @@ As better explained in other sections of the documentation, this is needed to co Specifically from this panel an admin can create, delete or extend OAuth2 ``Access tokens``. -The section :ref:`oauth2_tokens_and_sessions` better explains the concepts behind OAuth2 sessions; we want just to refresh the mind here +The [GeoNode / GeoServer Authentication Mechanism](../../setup/configuration/components/geonode_security_backend/geonode_security_backend.md#geonode-geoserver-authentication-mechanism) section better explains the concepts behind OAuth2 sessions; we want just to refresh the mind here about the basic concepts: -* If the `SESSION_EXPIRED_CONTROL_ENABLED <../../basic/settings/index.html#session-expired-control-enabled>`_ setting is set to `True` (by default it is set to `True`) +* If the `SESSION_EXPIRED_CONTROL_ENABLED` setting is set to `True` (by default it is set to `True`) a registered user cannot login to neither GeoNode nor GeoServer without a valid ``Access token``. * When logging-in into GeoNode through the sign-up form, GeoNode checks if a valid ``Access token`` exists and it creates a new one if not, or extends the existing one if expired. -* New ``Access tokens`` expire automatically after `ACCESS_TOKEN_EXPIRE_SECONDS <../../basic/settings/index.html#access-token-expire-seconds>`_ setting (by default 86400) +* New ``Access tokens`` expire automatically after the [`ACCESS_TOKEN_EXPIRE_SECONDS`](../../setup/configuration/settings.md) setting (by default 86400) * When an ``Access token`` expires, the user will be kicked out from the session and forced to login again ## Create a new token or extend an existing one -It is possible from the :guilabel:`Admin > DJANGO/GEONODE OAUTH TOOLKIT` panel to create a new ``Access token`` for a user. +It is possible from the `Admin > DJANGO/GEONODE OAUTH TOOLKIT` panel to create a new ``Access token`` for a user. -In order to do that, just click on the :guilabel:`Add` button beside ``Access tokens`` topic +In order to do that, just click on the `Add` button beside ``Access tokens`` topic ![oauth2-tokens/admin-panel-tokens-0001](img/oauth2-tokens/admin-panel-tokens-0002.png) @@ -58,7 +58,7 @@ select the followings: ![oauth2-tokens/admin-panel-tokens-0003b](img/oauth2-tokens/admin-panel-tokens-0003c.png) -5. ``Expires``; select an expiration date by using the :guilabel:`date-time` widgets. +5. ``Expires``; select an expiration date by using the `date-time` widgets. ![oauth2-tokens/admin-panel-tokens-0003b](img/oauth2-tokens/admin-panel-tokens-0003d.png) @@ -67,7 +67,7 @@ select the followings: ![oauth2-tokens/admin-panel-tokens-0003b](img/oauth2-tokens/admin-panel-tokens-0003e.png) -Do not forget to :guilabel:`Save`. +Do not forget to `Save`. From now on, GeoNode will use this ``Access Token`` to control the user session (notice that the user need to login again if closing the browser session), and the user will be able to access the OWS Services by using the new ``Access Token``, e.g.: @@ -87,4 +87,4 @@ force its session to expire. Remember that the user could activate another session by logging-in again on GeoNode with its credentials. -In order to be sure the user won't force GeoNode to refresh the token, reset first its password or de-activate it. \ No newline at end of file +In order to be sure the user won't force GeoNode to refresh the token, reset first its password or de-activate it. diff --git a/docs/src/admin/default_lang.md b/docs/src/admin/default_lang.md deleted file mode 100644 index 28561c0892e..00000000000 --- a/docs/src/admin/default_lang.md +++ /dev/null @@ -1,75 +0,0 @@ -# Changing the Default Language - -GeoNode's default language is English, but GeoNode users can change the interface language with the pulldown menu at the top-right of most GeoNode pages. Once a user selects a language, GeoNode remembers that language for subsequent pages. - -## GeoNode Configuration - -As root, edit the GeoNode config file `/home/geonode/geonode/geonode/settings.py`, or `/etc/geonode/settings.py` if GeoNode has been installed using **apt-get**, and change `LANGUAGE_CODE` to the desired default language. - -!!! Note - A list of language codes can be found in the global Django config file `/usr/local/lib/python2.7/dist-packages/django/conf/global_settings.py`, or `/var/lib/geonode/lib/python2.7/site-packages/django/conf/global_settings.py` if GeoNode has been installed using **apt-get**. - -For example, to make French the default language use: - -```python -LANGUAGE_CODE = 'fr' -``` - -Unfortunately, Django overrides this setting, giving the language setting of a user's browser priority. For example, if `LANGUAGE_CODE` is set to French, but the user has configured their operating system for Spanish, they may see the Spanish version when they first visit GeoNode. - -## Additional Steps - -If this is not the desired behavior, and all users should initially see the default `LANGUAGE_CODE`, regardless of their browser settings, do the following steps to ensure Django ignores the browser language settings. Users can always use the pulldown language menu to change the language at any time. - -As **root**, create a new directory within GeoNode's site packages: - -```bash -mkdir /usr/lib/python2.7/dist-packages/setmydefaultlanguage -``` - -or: - -```bash -mkdir /var/lib/geonode/lib/python2.7/site-packages/setmydefaultlanguage -``` - -if GeoNode has been installed using **apt-get**. - -As root, create and edit a new file `/usr/lib/python2.7/dist-packages/setmydefaultlanguage/__init__.py` and add the following lines: - -```python -class ForceDefaultLanguageMiddleware(object): - """ - Ignore Accept-Language HTTP headers - - This will force the I18N machinery to always choose settings.LANGUAGE_CODE - as the default initial language, unless another one is set via sessions or cookies - - Should be installed *before* any middleware that checks request.META['HTTP_ACCEPT_LANGUAGE'], - namely django.middleware.locale.LocaleMiddleware - """ - def process_request(self, request): - if request.META.has_key('HTTP_ACCEPT_LANGUAGE'): - del request.META['HTTP_ACCEPT_LANGUAGE'] -``` - -At the end of the GeoNode configuration file `/home/geonode/geonode/geonode/settings.py`, or `/etc/geonode/settings.py` if GeoNode has been installed using **apt-get**, add the following lines to ensure the above class is executed: - -```python -MIDDLEWARE_CLASSES += ( - 'setmydefaultlanguage.ForceDefaultLanguageMiddleware', -) -``` - -## Restart - -You need to restart GeoNode according to the installation method you have chosen. - -For example, if you are using `NGINX` with `UWSGI`, as root you need to run the following commands: - -```bash -service uwsgi restart -service nginx restart -``` - -Please refer to Translating GeoNode for information on editing GeoNode pages in different languages and creating new GeoNode translations. diff --git a/docs/src/overview/basics.md b/docs/src/overview/basics.md index f516aa3abb4..fa794a5fd4c 100644 --- a/docs/src/overview/basics.md +++ b/docs/src/overview/basics.md @@ -1,20 +1,9 @@ # GeoNode Basics { #geonode_basics } -![](img/geonode.png){ align=center } - -is a platform for the management and publication of geospatial data. +GeoNode is a platform for the management and publication of geospatial data. It brings together mature open-source software projects under an easy to use interface. -![](img/gn_simplified_architecture.png){ align=center } -/// caption -*GeoNode simplified architecture* -/// - -## *With GeoNode, non-specialized users can share data and create interactive maps.* - -![](img/gn_is_made_for.png){ align=center } -![](img/gn_publication_data.png){ align=center } -![](img/gn_publication_data_2.png){ align=center } +*With GeoNode, non-specialized users can share data and create interactive maps.* ## Geospatial data storage diff --git a/docs/src/overview/img/geonode.png b/docs/src/overview/img/geonode.png deleted file mode 100644 index 2419c3f26b10123ae612fab960764009a6e48829..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 23203 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 i_J7K6afItL0$tBN^AQk`d@zuJBh$044Ztr% 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%0DnppC*u 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�J~#&;7Pslr=5W$Hv@3&jGdj&eg6B z&`kiX4woar#o%HmQ%&Vf6Q_~2S6{qQX|=>9MF>MeQq5`5EYSVgy;ID0j~lc?bP!q6 zny!WW^ck1oaEHv`0~#ixu@8ZHzP64OIaHcb=*Cbrt1L?;oY?G)0`^IXS#d z)w@TT8{4w%BF79$fa*MjNjmTQc#7}z`as22q;tV=m~b;N>7tb1bFOim4~uGiT;E}1 z(|!)b%Rf&J3h@>hC-mP*PQs6@JS6c6t?}vbaCuc$l5xFOduZ~k#X>Soe<3ZBuR$zZ z3o%K^C4&J3Fh7hcs~*|(wz#GtKnG02XT-Mu1D3KnJKrYessi6H ztCMww1g_?an>HTt_6EcPcmA`_S4uT#PE+qnp-X<9shb@lF_@(mMod(c0ik!on^z?U z1BCCeDvkH6;u2z%)qH?Zk9+2?%WZ;BnniO!s2ns`z7QS2Jo-qjD9g4>dS*8n)=yE{O$ zO*>TIuuIEs$_MbX!7wGyNbxg#%!f$i$x+-$+w-;HA8z*^XIZ`@P80Ivyk)Cz!X?e` z@*-{N;$<#44n0t65ot7Hy(wJ}rlX9OCl!ry=jy<4Zn?c&Y!I<$@Q-hsj}fk{J55aZ zbPw-_ct$3eF|Y-YjCHZ|W{vCZSr($s>oq5Y-ZJc2U@*s9ykXNqrc8w64o}ke;KiOi z*LEDJ&#r}{l%U82P&Uf{`WLeI(O0@19)0v-(g0e_ zKnEHy(570)fZ!hfm$f)^dd*=ev7QA&&ydc6o0ppm^!!9>9!E()o0iK)GeRLx@Do5} zKthTi%MOOmymUx6el5_)7dISczenozTtb4MQFnAd!!2+oCoBewe*G(`^j!LVh7Um7 zD=z`hmSpAvd9ppZw3XVk!^zFv>`7wG>8qeDbAvnUPyCHPP5ex&k#{oE3>cnKPSB5ZZga{Kl;t~(x^=1>=|`cp^bw@{ z-#xTSN59FvlpATxlx8P!zS1Q8tHP$0Pr&hYnEguVMc(+G;bKl6Ki5xXv~=?8*eP2!K>2qVOgE?@ z=ivsxY)7`KZ#x_4>^Gx1&}I0;xXKsH!7FG&4y@;NCQ}VUxm|EvK`V!vEZvzvQs=KL|z<)~)@XQk6W5t<*W9%r9cA`*<0e<;}io z4odCf3D)Rb?QWc@4Rk^_LLuxOs)U53@8_DPQsro%Wk^r zW$*NVpD9-ON&nFP!b@{uK3t+FY9j$*$Le<@`})N)k#{3}N^sk?iKD2nBF;}xslMuG z9Lgc{n#mi1&!qfu!(-JxOm?ViT))y~`D>RzCKhZ~t@e)v0ZM8VFL_+!V_GFz?-KN( zdq3rG&?K5EPbA?VZ!;zba#@Vtcox4CV}ykgj=fy(&k1k0Dz9q!(Q`L0k;g5*}3 zeIqqf;mzlb_881s^d)OQ3IQ_9zxL^0Uq92v{B&60eUH4z-bmr4idA2rj_p7(CI<2p zUO$Ge=l1|YKkI4m1nHK^ghELF z&!@F{ffaCO;zVYo)p)#*Y+C5H&Z13;Bj_uXSib$IG$x0>zThF4jD`V?7pe-M;AO;> zLHz|EH;qdD?>aHBMpb_ajCh5#OnP-11gV`_{R;Q^pUCXWSM|D!-OG=Wl2CLjChLq?gLY*eOllT_;S$r_%_&|oX4AnZfV1IN7D1HC+} zWWU$S^@-|}U(C0PZYSs?Mp9u%s(h`gzeWxlBT*3kKDIMxDPn3Q#Qs z&zf#IEQx$TgD4{pCf;GrcT5@3wm2ey%uG+N{4tPQl-{s0iy_qAnTR8E_jk>(etW7!t}wfE9Dm8P z?svp75GUXK{&>OU0lljnrVx5Dl(uyd)3okVc?C{Lp84&|YTVY`Qu;vf#g3E}G;hZd zX4KI3g;cwT1tEaU{@{`GYZ=tj9C5#Y{Czg5yS~uzVUNj=$BI6#Ts`k{l=tZl=2=nh z{O=RIpf$D-rkx2AKO)nbR5DTQJp_%pV7Q+)$i?6~;#>D6yUVgQ>=)_JR!&}->pN*O zert>H6@M-uo0?<%+TFcr8>o%_LGf|xS|DXMe7-^VqrL?gxNA<_TO~(*oinD1%S>#G z{j%eI027{`_Jo8y*cl=Ce!gA}`n&!;$EkpP0}q)+={mr2JZl{g0T&)C`rQgQq5i_m z%u`?J$95~C1RfL=boGb1aUfNcHu07ls!wle{*4A|B||7@HsjZCR~L6Lf8FJ{Nu=7S zq?&_T-eoV#sbiwZ%*zu~S`U(x$AQk$!WZ11P8mZQc!Bl&t0&l`kBm-uo-&PZv3{mQP9opVbg+=7LX_N0?{q5CxEMbVr0_KKQlfo~kQ z2)kOY@7OS=IQ8aZdIgvD&QE|(L<$^syTIfQHQS#_w9NiC{Bvjc-hVLdC@nAnfh6Pe zM80pxpUYxQC;kD+8!XjW{bENrz1w^i22pYM=i$l=lrn99f(L=cSiu8L0l0MS z^0lBI8``+fUAeQ=Q3pSa7V^fcms|S|gov0@=AYKvR`0wHhWw2o|G8oJH}KE{c7~&h z77V3IdFL~NudP+I;a*VTHD4RPdy{B9-6`}6kl;N@ts5C=k1`cHRvjqmeEb&BVBVK* zz6H+-irBI0YVhT_ZJN~T*3xVsKFS@Yo^q2Op5|iPE#6@9#dar0 zEuiQg3c=Z6&Z3ptB(iWH+I_UUgIhaqqtiyqIp7U;_INL^oJw2j+x= zhch;>edMt6JXuQ5%E4b3Yfqs=_CFP9KF zhk>q%F5vz|G{Wc#zd;zsN&XDpR}?SgE6pzQh(ai@c4DYzZ-J*8r4IrPlW`?Yo^>hw z9FHh6&5@kw7e_VVSY_Lc7=$*!K0Z6H$MfhB+7WKx42t^r9;wAj2N&JNkOe-|EI)1b z<%WWBVx~BxcVUL-O8M80;A!*joswdYtFqn9b?-&B^FN-dYxnAEOs6{XPv_D5Zcx>9 zI-6+5Ib5$hmwF5&i zBE}?t+eN3=W=f^!%!xp%oF?V#ZKchEU8+w#rn#+S)qSC+&^HH-BG9MoOgE&N$p`pW zf;Ple*y9c2u36QRZMot8M1#^vs-(r2MoHGkWpxEWgE72tpmBvjTfwxs=eHnJ;1h_N zE8c`zwU)@R7Q65-t`XzjmPO|Cg*ucf8s#;Ga0y}2%P2drig?F|dPD_;oewOh z9dRS21a_xvTjfKa#jbDWAacf%zm$=pRXDBIX_&szNUPjzFtQT*30G&UU6NJM4cp3g zO_Vzv$&N@Q{)yh?h6D+U;<7x+q`UO})I1-4^-p?b0%fRK^~^spu2Q^X9+)Q9NM8gE zbScTiyfAOFbRr02uZ8p6nM4yT`WKj17ftsa(+QP4am%_+RXKX3t~O8x1pC~JS{LM4 z6B9KMxhr?kft9Q6856y5LQNEwNdQE)AB;%nR9^t9(+~eOQvbf&hpN1h8?F@@%2M{v z8!=z2UV_*>v}gXb#znrdso^t(;L-BHR%y|9=UqVq8tE8B&Sz)?N~H zx(tc!0H-!PQF@fy3sGEvrd~T1Q~<98vG{#nXHZx8u_&zZ{wnloxu~hB+z8D;+^2*c zNS-z#n~C)2+p?>pZmT#!eQbB7_=ygKySDG9%HX z@oJTI_W4dwHk=nrnIy_>kxFTK`4kjm z#)D!?2vM8(4>mEihB_)tsbG1Oi0#Dr#ED5Ep`N8(lP&S8#{QyD(?-y})3U-p;^p~A z(4qj>KV`^^`+_i@;0xV!hcgAXE_rI|KXZiM83Lx(n{#*{=g)5v*z3sw*T3b~&#A=M zkmoGX0?eO!y!`f2&oPgA&Fcdgw)^^@*P9pUst6eOc9_!1|spoN+9!i9Y5Ya>aTvwc47SquS0EDes(uU)i%OU zXE8bDXOmAK<~#98f?jR%7AckA+jkQlqw!}&xKveo&oy`D@_d8%-jYppkL|@Kjqy9QgJrgQ#hU zwuZ9{XIVYa1}3tLUosJ-j*|5s9AJKIo^~dJWz`qIc1j+2O`*h@=RBH)r>1)|7-8@e zyulkQO_?WG~3m#)^zjaCyDaYo?tnj9y+!VF1p}kE|PVfU; zqo72qD@e#)ayVLvl*5W4$#j<`Na9Kkz>dVeCu>dRZ`^{f`%BE}hq&6j<^8kvHPKC1 zUm3}s*mGO4vg=BT%h$RXoId|_(&R|}s(>~YTI&RZC{FNSo|EeDO3q&>+KiI?L5NNj zuxMkl66JhdZc(H?CEsLb;L&Faf!fNHXvZf#Xl>(K#O_<=OV1B@9 zA0Uus#%=I!N-&*9^UO!5d)n#Fu&pLWf~-Ng7j+f&P@?VJU;jk7@Mj~8@>|*65u!ZX zK&&*#zu(@YQ6h%8MWrdU2<%4_g`Jb#8S z&|6a7w%xMI6!c}U%lW#jvrS=@k~11-c6XCt9`DaW;&KEsa#|1})fOeb{PqV3jFou6 z>i8^4G4N{Ao7JQ58dJV&r zB1%JAoP85lsJXHZV01?}5U-s*sJX=<)tQr~!X{3^v%guKu}TboAq}N?7X6EFkH`mX zuVoNj{OIvmcw07|6DNAtsIaE*p;wJM-AmMK^JYc+44ugb_)$43{(}8W_|71Qdc8Sm zGpnigB8WSt+%cq&(1fg3SAV+kow-`Hn(Z&SS89f`t0%IP$=CQz`^<^C#(TcyM#y2iK0H&kGFtc2XBB)%wk?V0hm5y7RP@1?AnZj3YT)yS z&&Q)w140gMYNn~!E^5^>ya$$n(CaJJ$^7~Gm1GBpXUQH@Jld_DAr(DnVE%y@1|EQS zWuEoiF>%sn=4JT@rSym5uyzY8hLZSV75;;*$bPc0nf8qPE~u%Fh7Vi@^z|~VE0Dg7 z6oF7r%VxJC5JK{$UCu=t#=W|0@MKUxiVZ?oq)pm)(5~f9gHmb;WsiCZoKQvRwI^Hd z5zY*gv1RR()H7nDsY85L-}V1|Qy+ix{dpT})(+F8u={tMBMZ&CiEYz!%FW3l-N?hm zo<&Pnv364$q8SeSyllvfc*A;UTU{8vod+l!_;Z7ol7j>n zOo1D#VVGKGb%De@GA4MOiBkaw@Tzv$)#Kzvf}F``bwxAI#oe>K_2E-|RtfKpzCl*L zvl6yTG~Q`=%C^NTyTMO>4Bg{nl`hJKROf88#hg-B)GiDB@>%lm+#?nqTEcv6goUjc zOvE&Y(Y<(LuPDl>$)GnoEmq>YK;yhfbfFa*&UGu`aS~3&CH4>Jd_}s!&B-PqUs>*$ z78b-^*C!20o)UBCE?mHk9nGOMG1$e3877A04kXCU*YRktbIs4LfdI1+M%ckWlXtgr zD*UX_C{nCKB)hW6g`|!Dv5OHo!i3-1 z@kSVUfE<-qoT(Qf8rWV$c8dF6-p~%MiwZ#$5&m)C_MNd%POrT?@+NKLBDgoJ)!DY} zWU;8mcFY|x9f%wk)m$;dpB)i)=hhy~-r(GXwm(#XBv*&)Lzp*J``FaIlZM0!WkaCoAYr6<;Up~hKF>xAg! zB*N%)(%x1Du+a9VcVbv6;07e_4b~^~o=KCx;q7}h`eN6)A1tdCj#+&0i0w1DH*R+C z^1YADk3x3rpFmw(ydKQtKE$NThk{jpV0hnaoe-yPO*1dHGHD%X99RFbBU`3;m#(v359_bE{ zg=~#PVt@8y7%(oW0%;7jaf~4Q6BL-1?9qO704qHoyN2EPeekkm^u9e?kPCgGvP!-Q za2DyjsmFonA<>GK^4$&nBmYR@g2g3yj0x=|TL11s;W3+koF9AL1PL^cctUR=UQY6Z zbiIMF?);Mvp28cNYA*w?Dv1ZV=7^b5EvY5Fwv2?tgVuI$aDnu`0fV1wIkLc1(urHl zquLCYwfq=Twxi|b^5?U;T$QWwdfv(qnxnh~&&BOr*0mKurPH^kFMOza^b)C+#1v4=)uRxX?ta{J~rb9?)lZ4FifXL3Q><#~X^zp^gKE@y_^PA=D>j^!Kl?fX27W-rS8^`y z{)1=VIh#6Zjgh~aXo|Ml2WEy-c!LnR3K2#@Cfa-`WtVZzeZ2PuNoYBPHV|&Kj16hF zH^QPewZgUt7CY6GBS{t>mTLPg!-w;x8ev967c@89`O{uJ!v2hpZ7fl(5-P>lS}a4L zO|NlBcBV+4xSC3sQV`sDbe*P0z?QfPo*gmGWrv`~P`6av+TEeL{Q-*agMJHp%eJKf zob3uPehDR>-lAE=^2lG$WlMwey5L{kCCFlNi@|z>1=4 zi=uQ2fsBmzVm{j95I@0-Je`#bM#}xL2D}!H+{A}_3ycR6Lnqp>A(EMaIf39|%t1m* zmA^)uMx!T3;vq+MNt+ltC}wCN?wM5X;K%#Qm|D4VkIz~j9dR=gj?r#$U8$x-&bA!E zz+%ik`@3qLrFv@>sp2AOYfjx|kOz?O!CM4YdwtdKeqTBk2!F!5u4X z+lqs7Mdli$cXO;d-REX~z5oH$+)fL|PpSHAX89o)@1{mr>qX&>2{Y**)?U~}>pa*? zHxA*)Av-9p6slZHU3606p*U6%GZXe$56|8G{qhPtvYsnm51M_F=&E7($=QsL0^ zJJ%cVh{*Q22`j*abWlJdZ1TLZ)k7J$>MYYmYqsCA`qAmhx9P{-&&_H16bsIqdDr#t z;u$gQjNP2hRVd|Ym&kW%kj__-??oYs@=juI&%L!;a9rqa$#cu|Go84F0+1$NBMew) z?k4bca_lMRJ->DyNN{X&YYWy+h2A!@rER}s=ZI6j|U^5j=hXFJtW9ZxaJl;GUD z!)$M`zV*nPBnF9#W$>(vf2yTy5<)&1}KYyYlao`9NqN`^OC zxwvR1uC`n9amOcy)S&$Jk&vQ`X2By3x!CMl1@XhD^}8J*;n0a-lTDO$Cd>N8p?;NHMGJR{4RfJgWrFJdj*;)^b$}ZSZ}>J?-~J+!Kgi#ZW&hr^ zpp~DP>;NV)H|r7!A#>Kq9eaqi4HZQ48kO%=OKtwpS6L*H>CL)%Yw!OsBEJ_wzsxy; zHusavq`u6wYkiZ1)`Nm@z3l#xYvnuK2E)WO*OErbqO5#~(oRr$f@Eo9YHce_1iR(- z*e$n|Wdf6h%XH@_wS&+sMhwl}V(Kq~kao|1XTGT8IkDa@26e7$=L>M_yhdY*mNHX~ zM%V;@1CfbAWiY1cNymeRthz6*pW1*|#B-3izgu|7DF?O#C{VGFYDb#eMX=mbcerLm zWN8I8ux(+N%&*&g_q=Jdk|1nNlVTdNz_2~&cgUe0|BUGzA*1Xpyl?klZuYUygqiaT zVRrz=4Zx=gwMdRK4h36bEh?ASlpE&=0=Rs#a#(>u!^J+Z@s#(OEK`urB&9Z+OYY8E zejnfOYzpm>Za$(vxOEn;e*#d_fkXwPI#l1GQ;>Nz!uji*9-dJJDsraiIA7~fU7toR zo`e!JVvh`ez4=4API;9O4;vTJVjWk-8x}A%UabW0L9Zay!Ihot682?1x2yK4LO~wS zhvKg9f|r`3%2cIw<-GfV=B(WnWGG=iWnh~xqTnw6%tf%p z5(s{%d5i=PkLU{7E8`WOn9$oH9kAabXD z+u3rWNAWQP>)l_HbKQ9MeXF9Xk)|3NNTWH1Ni;jtlqd5n4z#0eg1gQSBrGtI3QzJp zUxiT4*pRx%Vk=&T2I6dPD*~XbvMhypZCxFA4M7LuT@j z7-DO10MFzmaQl>+{TRnhOgsg%Y=5JY^F?cz5e7E^<}g}sg9HEY2)XP!>hWJolsEbdO+lk zuin~%@z!9^0tIWrEN>7|PePvFO8~U#!Ys)UklyQ+<##uM2)!`lY0W^wv#yd+K4?`X zbNH)fxCjd7e!I?+z67*4mpCpPUygJ2iNbgr+|oY<*1VB%_G3PPMV`#c4Kb!cac;Ugh5p4_ zmE=6kz5pzcThU@n#r4lEPaw~WyK@K3={}{zp>)I#U`D{d6uuyi4FVT5suU1cd|uiI z0Q5MhmB@YQP&*NgSEbJDAl4yCs7-opm7|XCYB36bwpfAK-`M~Zab^HC*?BsJ(|2=mUolgeV+Dw#(S#t>9)hM7Ftf)uDiGSiX9`;ut!; zTCoK{<}%yA?fj~DMNi5^S%b|#*vli9>6sH)Vj==lw`YMo{(j! z*w>bPDTcle-pvI)_5iQH>aPu}Xbt)T;xw_?_s9yW1L=HJB0?hb`#VgH+ZjJ=guQ#E z3R3POOX|Wd!{4kgJ&JF|D-K!V@9|r}{yUE2x6lP0HLKB#D<}q19zrhEFS;LHQ2M-a zW|W%4H5CE?9-)D;(Zk*r8O13`pBK-1q1Ky^0NK!O>#()}#%d<-MK5mg9H{lNUna%x zZ&|49?_fa(nwLdv9%%4O=$r`7We6WUs?}y)8Sg%WWh%r;;H&%Z!PqGXv9I;VjQ}cY zoSWmiaYen2aF@GAtGR8AqB#ZzmfldV0))PQ3E*OKh97f%|Qa*N_)Pn2uaohn;@F z7Qg@?ziDta^NppqGd-$(~5JClk3?rbUzr z_Pc@MK#j=xeaghZ+=^qcg*-)rdG_2@v&=3)&8g|cJ^c6`2QTh*;lRYw`RC?Wb^Pgh zZs1t1kB?$LegOnK3PKjFee+?;i~{AVu%_)eLzD8GmI~2U{Gl(bjDld9Lxsv(Sb{Os+n;O>umApS!~Yb* zy&0w-=ib#k4pmK}$KxJhK9|a+zGSv5npQPI{zbU~FXEAqh3c8dtnFCijAvx%0^;Yf zMRsH#idzu-`y#R46I>8B@GJe=3u&_DUZGP$9ztiCv`Z-Ap3on2z#=8yMiBv6dP^Pg zS|rmj$gb5oRR!HVo}SWmyzWy`vFv>nOQUG7x8{i0_Xe}O z3MA}0p3ov2MC_zZGv@}u;th~RhnhLJ3v95VRKAVY=2u2ka%2y8{H=-o=c<(V|BU@S z!I_tD^YF%`aQQAv&*XKA5cF)?ZIGA$+Qp(U87Wa98rC8FmMW0xu^4X{`7-ghz^G^o zZS~gsiGcq);agyNxDIvua1OuxcI3Bx{gQy#j4qP|V`K;MPxVA@2qIQDb)MsUrviw7 zQDZmVnjbGHqdzB42DY|0fwj#H6$oP~PVDg@w*h!*roJe8Skwfn*0Z!0e6txoI`~n)JA!HDsifQ@)UnwUn#vy(8RQb)i;@S%btc>6=a4gC;3H!<4i#KMgF+q(kzfQ|Nr+2dK*SSD28F5<P0apW{GS3YHYnh3N4jex6+wD5zg|q+lMu6t{ z8OheP1aItbu@CWY&>;@{^%EtjkDXyATT}j#oxkDTP4R_VdsUFcX_2uvp6|aIU5pK6w&v}lU`IeXHDeGVlAg6$PwGsB@k}>>p VR+uLPXe@$%igIet3T57Z{Xb8rkyHQx diff --git a/docs/src/overview/img/gn_publication_data_2.png b/docs/src/overview/img/gn_publication_data_2.png deleted file mode 100644 index f74438eb9669d6cfd3c2a8acf7f5e1fec7ed224b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 82376 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 F|1S}}E35zj diff --git a/docs/src/overview/img/gn_simplified_architecture.png b/docs/src/overview/img/gn_simplified_architecture.png deleted file mode 100644 index a530d4064c9cd482411e69c3585fe6516d5352ad..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 75365 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 O1|vN)$a`J)hyMcpwjluk literal 0 HcmV?d00001 diff --git a/docs/src/user-guide/img/topbar_language_switcher.png b/docs/src/user-guide/img/topbar_language_switcher.png new file mode 100644 index 0000000000000000000000000000000000000000..e0be594b48684ff939c8b75bbd83e8d4a7230613 GIT binary patch literal 8367 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 Date: Thu, 4 Jun 2026 14:36:16 +0200 Subject: [PATCH 11/98] Upgrade to Geoserver 2.28.4 (#14289) --- docker-compose-dev.yml | 4 ++-- docker-compose-geoserver-server.yml | 4 ++-- docker-compose-test.yml | 4 ++-- docker-compose.yml | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index ab2398edafc..25da411312f 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -79,7 +79,7 @@ services: # Geoserver backend geoserver: - image: geonode/geoserver:2.28.x-latest + image: geonode/geoserver:2.28.4-latest container_name: geoserver4${COMPOSE_PROJECT_NAME} healthcheck: test: "curl -m 10 --fail --silent --write-out 'HTTP CODE : %{http_code}\n' --output /dev/null http://geoserver:8080/geoserver/ows" @@ -105,7 +105,7 @@ services: condition: service_healthy data-dir-conf: - image: geonode/geoserver_data:2.28.x-latest + image: geonode/geoserver_data:2.28.4-latest container_name: gsconf4${COMPOSE_PROJECT_NAME} entrypoint: sleep infinity volumes: diff --git a/docker-compose-geoserver-server.yml b/docker-compose-geoserver-server.yml index b0fa460421c..c62683831e9 100644 --- a/docker-compose-geoserver-server.yml +++ b/docker-compose-geoserver-server.yml @@ -2,7 +2,7 @@ version: '2.2' services: data-dir-conf: - image: geonode/geoserver_data:2.28.x-latest + image: geonode/geoserver_data:2.28.4-latest restart: on-failure container_name: gsconf4${COMPOSE_PROJECT_NAME} labels: @@ -13,7 +13,7 @@ services: - geoserver-data-dir:/geoserver_data/data geoserver: - image: geonode/geoserver:2.28.x-latest + image: geonode/geoserver:2.28.4-latest restart: unless-stopped container_name: geoserver4${COMPOSE_PROJECT_NAME} stdin_open: true diff --git a/docker-compose-test.yml b/docker-compose-test.yml index c1d018a3d64..5ac1b1da494 100644 --- a/docker-compose-test.yml +++ b/docker-compose-test.yml @@ -80,7 +80,7 @@ services: # Geoserver backend geoserver: - image: geonode/geoserver:2.28.x-latest + image: geonode/geoserver:2.28.4-latest container_name: geoserver4${COMPOSE_PROJECT_NAME} healthcheck: test: "curl -m 10 --fail --silent --write-out 'HTTP CODE : %{http_code}\n' --output /dev/null http://geoserver:8080/geoserver/ows" @@ -106,7 +106,7 @@ services: condition: service_healthy data-dir-conf: - image: geonode/geoserver_data:2.28.x-latest + image: geonode/geoserver_data:2.28.4-latest container_name: gsconf4${COMPOSE_PROJECT_NAME} entrypoint: sleep infinity volumes: diff --git a/docker-compose.yml b/docker-compose.yml index a0d35841bba..c2519e1bece 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -92,7 +92,7 @@ services: # Geoserver backend geoserver: - image: geonode/geoserver:2.28.x-latest + image: geonode/geoserver:2.28.4-latest container_name: geoserver4${COMPOSE_PROJECT_NAME} healthcheck: test: "curl -m 10 --fail --silent --write-out 'HTTP CODE : %{http_code}\n' --output /dev/null http://geoserver:8080/geoserver/ows" @@ -118,7 +118,7 @@ services: condition: service_healthy data-dir-conf: - image: geonode/geoserver_data:2.28.x-latest + image: geonode/geoserver_data:2.28.4-latest container_name: gsconf4${COMPOSE_PROJECT_NAME} entrypoint: sleep infinity volumes: From a36ec4abc50b6310bc7d46c87cd55814745f9d81 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 8 Jun 2026 14:41:33 +0200 Subject: [PATCH 12/98] [Fixes #14291] Add parameter for beat DB file location (#14292) (#14303) * Add parameter for beat DB file location * Cleaner location for BEAT_DB (review comment from Gpetrak) (cherry picked from commit d295a18ac62b0df9a044a0fa45dadcbb352dd4a0) Co-authored-by: Wolfgang Kaltz --- celery-cmd | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/celery-cmd b/celery-cmd index bb2f04529e5..90715732296 100644 --- a/celery-cmd +++ b/celery-cmd @@ -16,6 +16,7 @@ CELERY__WORKER_CONCURRENCY=${CELERY__WORKER_CONCURRENCY:-"4"} # Celery beat settings CELERY__BEAT_SCHEDULE=${CELERY__BEAT_SCHEDULE:-"celery.beat:PersistentScheduler"} +CELERY__BEAT_DB=${CELERY__BEAT_DB:-"/mnt/volumes/statics/celerybeat-schedule"} CELERY__BEAT_LOG=${CELERY__BEAT_LOG:-"/var/log/celery_beat.log"} # Harvester settings @@ -43,6 +44,7 @@ fi echo "Starting Celery Beat..." $CELERY_BIN -A $CELERY_APP beat --scheduler=$CELERY__BEAT_SCHEDULE \ + --schedule=$CELERY__BEAT_DB \ --loglevel=$CELERY__LOG_LEVEL -f $CELERY__BEAT_LOG --pidfile=/tmp/celerybeat.pid & echo "Starting Default Celery Worker..." @@ -67,4 +69,4 @@ wait -n # Exit with the status of the process that exited first # Docker will restart the container if this is non-zero (i.e., a failure) -exit $? \ No newline at end of file +exit $? From c958223700207058a929eac09b92125bd16fe437 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 9 Jun 2026 11:47:00 +0200 Subject: [PATCH 13/98] build(deps): bump django from 5.2.14 to 5.2.15 (#14301) Bumps [django](https://github.com/django/django) from 5.2.14 to 5.2.15. - [Commits](https://github.com/django/django/compare/5.2.14...5.2.15) --- updated-dependencies: - dependency-name: django dependency-version: 5.2.15 dependency-type: direct:production update-type: version-update:semver-patch ... (cherry picked from commit c9ef9d1cb65d93f92cb2a8744ed003fe5546727a) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index c677d198208..160f3403bb7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,7 +27,7 @@ dependencies = [ "Pillow==12.2.0", "lxml==5.2.1", "psycopg2==2.9.12", - "Django==5.2.14", + "Django==5.2.15", # Other "beautifulsoup4==4.12.3", From 918429ca7bcfa01691702b5d431278f35654e78f Mon Sep 17 00:00:00 2001 From: Giovanni Allegri Date: Tue, 9 Jun 2026 16:41:48 +0200 Subject: [PATCH 14/98] Update django_geonode_mapstore_client version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 160f3403bb7..5802ab741d8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -98,7 +98,7 @@ dependencies = [ "geonode-pinax-notifications==6.0.0.3", # GeoNode org maintained apps. - "django_geonode_mapstore_client @ git+https://github.com/GeoNode/geonode-mapstore-client.git@master", + "django_geonode_mapstore_client @ git+https://github.com/GeoNode/geonode-mapstore-client.git@5.1.x", "django-avatar==8.0.1", "geonode-oauth-toolkit==2.2.2.2", "geonode-announcements==2.0.2.2", From 5e4108a61cc66920e2f46d312a0d1bd0856f4af2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 9 Jun 2026 18:21:26 +0200 Subject: [PATCH 15/98] [Backport 5.1.x][Fixes #14245] Management command to clear GWC cache (#14312) * Management command to clear gwc cache * GWC Module to handle gwc related stuffs * Updated gwc truncate command to use gwc module * Improve GWC client and truncate command error handling * Fix masstruncate URL * Improvements and fixes * Improvements --------- (cherry picked from commit b4a695d14e541d72f6a3afb841638d16efc46890) Co-authored-by: Niraj Adhikari <41701707+nrjadkry@users.noreply.github.com> Co-authored-by: Emanuele Tajariol --- geonode/geoserver/gwc.py | 84 ++++++++++ geonode/geoserver/management/commands/gwc.py | 67 ++++++++ .../commands/gwc_subcommands/__init__.py | 0 .../commands/gwc_subcommands/truncate.py | 76 +++++++++ geonode/geoserver/tests/test_gwc.py | 157 ++++++++++++++++++ 5 files changed, 384 insertions(+) create mode 100644 geonode/geoserver/gwc.py create mode 100644 geonode/geoserver/management/commands/gwc.py create mode 100644 geonode/geoserver/management/commands/gwc_subcommands/__init__.py create mode 100644 geonode/geoserver/management/commands/gwc_subcommands/truncate.py create mode 100644 geonode/geoserver/tests/test_gwc.py diff --git a/geonode/geoserver/gwc.py b/geonode/geoserver/gwc.py new file mode 100644 index 00000000000..077d6500947 --- /dev/null +++ b/geonode/geoserver/gwc.py @@ -0,0 +1,84 @@ +######################################################################### +# +# Copyright (C) 2026 OSGeo +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +######################################################################### + +import logging +import requests + +from xml.sax.saxutils import escape + +from geoserver.catalog import FailedRequestError +from requests.auth import HTTPBasicAuth + +from geonode.geoserver.helpers import ogc_server_settings +from django.conf import settings + +logger = logging.getLogger(__name__) + + +class GWCClient: + """ + A GeoWebCache REST client for interacting with the GWC API. + """ + + def __init__(self) -> None: + self.base_url = f"{ogc_server_settings.LOCATION}gwc/rest/" + + def _post(self, urlpath, data): + _user, _password = ogc_server_settings.credentials + + url = f"{self.base_url}{urlpath}" + r = requests.post(url=url, data=data, auth=HTTPBasicAuth(_user, _password), timeout=30) + logger.debug(f'POST response: code:{r.status_code} --> "{r.text}"') + return r + + @staticmethod + def _validate_layer_name(layer_name: str, workspace: str | None = None) -> str: + if ":" not in layer_name: + workspace = workspace or getattr(settings, "DEFAULT_WORKSPACE", "geonode") + logger.info( + "Workspace not provided for layer '%s'. Using default workspace '%s'.", + layer_name, + workspace, + ) + + layer_name = f"{workspace}:{layer_name}" + return layer_name + + def truncate_layer(self, layer_name: str, workspace: str | None = None) -> None: + """ + Truncate all cached tiles for a GWC layer. + """ + layer_name = self._validate_layer_name(layer_name, workspace) + body = f"{escape(layer_name)}" + r = self._post("masstruncate", body) + if r.status_code != 200: + raise FailedRequestError(f'Error truncating GWC layer: {r.status_code}: "{r.text}"') + + logger.info("Successfully truncated GWC cache for layer '%s'.", layer_name) + + def truncate_all(self) -> None: + """ + Clear the entire GWC cache. + """ + r = self._post("masstruncate", "") + + if r.status_code != 200: + raise FailedRequestError(f'Error truncating all GWC layers: {r.status_code}: "{r.text}"') + + logger.info("Successfully truncated the whole GWC cache.") diff --git a/geonode/geoserver/management/commands/gwc.py b/geonode/geoserver/management/commands/gwc.py new file mode 100644 index 00000000000..ccf516837a2 --- /dev/null +++ b/geonode/geoserver/management/commands/gwc.py @@ -0,0 +1,67 @@ +######################################################################### +# +# Copyright (C) 2026 OSGeo +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +######################################################################### + +from django.core.management.base import BaseCommand, CommandError + +from geonode.base.management.command_utils import setup_logger +from geonode.geoserver.management.commands.gwc_subcommands import truncate + +logger = setup_logger() + +COMMAND_TRUNCATE = "truncate" +COMMANDS = [COMMAND_TRUNCATE] + + +class Command(BaseCommand): + help = f"Handles GWC commands {COMMANDS}" + + def add_arguments(self, parser): + parser.add_argument("subcommand", nargs="?", choices=COMMANDS, help="GWC operation to run") + + truncate_group = parser.add_argument_group('Params for "truncate" subcommand') + truncate_group.add_argument( + "-l", + "--layer", + dest="layers", + action="append", + help="Name of the layer(s) to truncate. Can be repeated.", + ) + truncate_group.add_argument( + "--all", + dest="truncate_all", + action="store_true", + help="Truncate all cache in GWC", + ) + + def handle(self, *args, **options): + subcommand = options["subcommand"] + logger.info("Starting GWC command.") + + if not subcommand: + logger.warning("No GWC subcommand provided.") + self.print_help("manage.py", "gwc") + return + + logger.info("Executing GWC subcommand '%s'.", subcommand) + + if subcommand == COMMAND_TRUNCATE: + truncate.handle(options) + + else: + raise CommandError(f"Unknown subcommand: {subcommand}") diff --git a/geonode/geoserver/management/commands/gwc_subcommands/__init__.py b/geonode/geoserver/management/commands/gwc_subcommands/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/geonode/geoserver/management/commands/gwc_subcommands/truncate.py b/geonode/geoserver/management/commands/gwc_subcommands/truncate.py new file mode 100644 index 00000000000..111b4a3ebc7 --- /dev/null +++ b/geonode/geoserver/management/commands/gwc_subcommands/truncate.py @@ -0,0 +1,76 @@ +######################################################################### +# +# Copyright (C) 2026 OSGeo +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +######################################################################### + +from geonode.base.management.command_utils import setup_logger +from geonode.geoserver.gwc import GWCClient + +from django.core.management.base import CommandError + +logger = setup_logger() + + +def handle(options: dict) -> None: + layers = options.get("layers") or [] + truncate_all = options.get("truncate_all") or False + + logger.info( + "Truncate command received. layers=%s, truncate_all=%s", + layers, + truncate_all, + ) + + if not layers and not truncate_all: + raise CommandError("'truncate' command requires either the -l/--layer parameter(s) or the --all flag.") + + if layers and truncate_all: + raise CommandError("Cannot use both -l/--layer and --all at the same time.") + + if truncate_all: + truncate_all_layers() + else: + logger.info(f"Truncating {len(layers)} layer{'s' if len(layers) > 1 else ''}") + truncate_layers(layers) + + +def truncate_layers(layer_names): + """Truncates one or more specified layers in GWC.""" + logger.info(f"Truncating layers in GWC: {', '.join(layer_names)}") + client = GWCClient() + failed_layers = [] + for layer_name in layer_names: + try: + logger.info(f"Truncating layer: {layer_name}") + client.truncate_layer(layer_name) + except Exception as e: + logger.error(f"Error invalidating cache for layer {layer_name}: {e}") + failed_layers.append(layer_name) + if failed_layers: + raise CommandError(f"Failed to truncate the following layers: {', '.join(failed_layers)}") + + +def truncate_all_layers(): + """Truncates all layers in GWC.""" + logger.info("Truncating ALL layers in GWC") + client = GWCClient() + try: + client.truncate_all() + logger.info("Successfully truncated all layers in GWC.") + except Exception as e: + logger.error(f"Error executing truncateAll on GeoWebCache: {e}") + raise CommandError(f"Error executing truncateAll on GeoWebCache: {e}") diff --git a/geonode/geoserver/tests/test_gwc.py b/geonode/geoserver/tests/test_gwc.py new file mode 100644 index 00000000000..3e37e1e6636 --- /dev/null +++ b/geonode/geoserver/tests/test_gwc.py @@ -0,0 +1,157 @@ +######################################################################### +# +# Copyright (C) 2016 OSGeo +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +######################################################################### + +from unittest.mock import patch, MagicMock +from django.core.management import call_command +from django.core.management.base import CommandError +from django.test import TestCase + +from geoserver.catalog import FailedRequestError +from geonode.geoserver.gwc import GWCClient + + +class GWCClientTest(TestCase): + + @patch("geonode.geoserver.gwc.requests.post") + def test_truncate_layer_calls_api(self, mock_post): + mock_response = MagicMock() + mock_response.status_code = 200 + mock_post.return_value = mock_response + + client = GWCClient() + client.truncate_layer("test_layer1") + + self.assertEqual(mock_post.call_count, 1) + + call_kwargs = mock_post.call_args[1] + self.assertEqual( + call_kwargs["data"], + "geonode:test_layer1", + ) + + @patch("geonode.geoserver.gwc.requests.post") + def test_truncate_layer_preserves_workspace(self, mock_post): + """Layer already qualified should not get default workspace prepended.""" + mock_response = MagicMock() + mock_response.status_code = 200 + mock_post.return_value = mock_response + + client = GWCClient() + client.truncate_layer("workspace:test_layer") + + call_kwargs = mock_post.call_args[1] + self.assertEqual( + call_kwargs["data"], + "workspace:test_layer", + ) + + @patch("geonode.geoserver.gwc.requests.post") + def test_truncate_layer_escapes_xml(self, mock_post): + """Ensure XML injection is prevented via escaping.""" + mock_response = MagicMock() + mock_response.status_code = 200 + mock_post.return_value = mock_response + + client = GWCClient() + client.truncate_layer("layer&<>name") + + call_kwargs = mock_post.call_args[1]["data"] + + self.assertIn("layer&<>name", call_kwargs) + self.assertNotIn("", call_kwargs) + + @patch("geonode.geoserver.gwc.requests.post") + def test_truncate_layer_raises_on_error(self, mock_post): + mock_response = MagicMock() + mock_response.status_code = 500 + mock_response.text = "Internal Server Error" + mock_post.return_value = mock_response + + client = GWCClient() + + with self.assertRaises(FailedRequestError): + client.truncate_layer("test_layer") + + @patch("geonode.geoserver.gwc.requests.post") + def test_truncate_all_calls_api(self, mock_post): + mock_response = MagicMock() + mock_response.status_code = 200 + mock_post.return_value = mock_response + + client = GWCClient() + client.truncate_all() + + self.assertEqual(mock_post.call_count, 1) + self.assertEqual( + mock_post.call_args[1]["data"], + "", + ) + + @patch("geonode.geoserver.gwc.requests.post") + def test_truncate_all_raises_on_error(self, mock_post): + mock_response = MagicMock() + mock_response.status_code = 500 + mock_response.text = "Internal Server Error" + mock_post.return_value = mock_response + + client = GWCClient() + + with self.assertRaises(FailedRequestError): + client.truncate_all() + + +class GwcManagementCommandTest(TestCase): + + @patch("geonode.geoserver.management.commands.gwc_subcommands.truncate.GWCClient") + def test_management_command_layers(self, MockGWCClient): + """Ensure multiple layers trigger truncate_layer calls.""" + call_command("gwc", "truncate", "-l", "layer1", "-l", "layer2") + + instance = MockGWCClient.return_value + + self.assertEqual(instance.truncate_layer.call_count, 2) + instance.truncate_layer.assert_any_call("layer1") + instance.truncate_layer.assert_any_call("layer2") + + @patch("geonode.geoserver.management.commands.gwc_subcommands.truncate.GWCClient") + def test_management_command_all(self, MockGWCClient): + """Ensure --all triggers truncate_all.""" + call_command("gwc", "truncate", "--all") + + MockGWCClient.return_value.truncate_all.assert_called_once() + + def test_management_command_error_missing_args(self): + """Command must fail when no arguments are provided.""" + with self.assertRaises(CommandError) as context: + call_command("gwc", "truncate") + + self.assertIn( + "requires either the -l/--layer parameter(s) or the --all flag", + str(context.exception), + ) + + def test_management_command_error_both_args(self): + """Command must fail when both layer and --all are used.""" + with self.assertRaises(CommandError) as context: + call_command("gwc", "truncate", "-l", "layer1", "--all") + + self.assertIn( + "Cannot use both -l/--layer and --all at the same time", + str(context.exception), + ) From 96cfb17702052964bbf86f6890ac11d5bc5bac8b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 9 Jun 2026 18:28:18 +0200 Subject: [PATCH 16/98] [Backport 5.1.x][Fixes #14306] Refact create_tile_layers as a subcommand of gwc (#14313) (cherry picked from commit 820b5a8ae3ac00228f8f751eace4cf7f6375fb3d) Co-authored-by: Emanuele Tajariol --- docs/src/admin/management_commands/gwc.md | 52 +++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 docs/src/admin/management_commands/gwc.md diff --git a/docs/src/admin/management_commands/gwc.md b/docs/src/admin/management_commands/gwc.md new file mode 100644 index 00000000000..d64d54cb860 --- /dev/null +++ b/docs/src/admin/management_commands/gwc.md @@ -0,0 +1,52 @@ +# Handle GWC tile layers + +GeoWebCache (GWC) is a tile caching mechanism that can be used to speed up the rendering of map layers. + +The `gwc` management command allows you to manage GWC tile layers for your GeoNode instance. + +## Create GWC Tile Layers + +To create GWC tile layers for all your GeoNode layers configured within the local GeoServer, +run the following command: + +```bash +DJANGO_SETTINGS_MODULE=geonode.settings python manage.py gwc create --all +``` + +This will create GWC tile layers for all your GeoNode layers. + +In case you want to create GWC tile layers for a specific layer, you can use the `--layer` option followed by the layer name: + +```bash +DJANGO_SETTINGS_MODULE=geonode.settings python manage.py gwc create --layer +``` + +You can specify multiple layers by repeating the `--layer` option: + +```bash +DJANGO_SETTINGS_MODULE=geonode.settings python manage.py gwc create --layer --layer +``` + +The `gwc create` command is usually quite conservative and will not mess with GWC tile layers if they already exist. +However, it also accepts the `--force` option, which forces the creation of GWC tile layers even if they already exist. +This may be useful if you want to recreate GWC tile layers for some reason, for example reset GWC tile layers after a GeoServer layer update. + +Notice: the `gwc create` command replaces the old `create_tile_layers` command, which is now deprecated and will be removed in a future release. + +## Truncate GWC Tile Layers + +To truncate GWC tile layers for all your GeoNode layers, run the following command: + +```bash +DJANGO_SETTINGS_MODULE=geonode.settings python manage.py gwc truncate --all +``` + +This will truncate GWC tile layers for all your GeoNode layers. + +In case you want to truncate GWC tile layers for one or more specific layers, +you can use the `--layer` option followed by the layer name: + +```bash +DJANGO_SETTINGS_MODULE=geonode.settings python manage.py gwc truncate --layer [--layer ...] +``` + From 8eadd6fab035467e53a8468b9e99c5fafbef17ba Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 10 Jun 2026 09:50:58 +0200 Subject: [PATCH 17/98] Fix #14305 (#14307) (#14316) (cherry picked from commit 929ad55c3b5e1bc0ababb92e0d205db6571eb174) Co-authored-by: Wolfgang Kaltz --- celery-cmd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/celery-cmd b/celery-cmd index 90715732296..4872be0e2c6 100644 --- a/celery-cmd +++ b/celery-cmd @@ -23,7 +23,7 @@ CELERY__BEAT_LOG=${CELERY__BEAT_LOG:-"/var/log/celery_beat.log"} CELERY__HARVESTER_WORKER_NAME=${CELERY__HARVESTER_WORKER_NAME:-"harvesting_worker@%h"} CELERY__HARVESTER_CONCURRENCY=${CELERY__HARVESTER_CONCURRENCY:-"10"} CELERY__HARVESTER_AUTOSCALE_VALUES=${CELERY__HARVESTER_AUTOSCALE_VALUES:-"15,10"} -CELERY__HARVESTER_MAX_MEMORY_PER_CHILD=${CELERY__MAX_MEMORY_PER_CHILD:-"500000"} +CELERY__HARVESTER_MAX_MEMORY_PER_CHILD=${CELERY__HARVESTER_MAX_MEMORY_PER_CHILD:-"500000"} # --- FIX: Remove stale Beat pidfile before starting beat --- BEAT_PIDFILE="/tmp/celerybeat.pid" From 4fa63e8d4c2bb14dde1e36be62d9fccc2d561c1c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 10 Jun 2026 16:21:52 +0200 Subject: [PATCH 18/98] [Fixes #11920] Remove call to command rebuild_index in tasks.py (#14304) (#14317) (cherry picked from commit 77f4f510195f2384572252984744211f504eecf2) Co-authored-by: Emanuele Tajariol --- tasks.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/tasks.py b/tasks.py index 86513d3380b..fa3bf0be821 100755 --- a/tasks.py +++ b/tasks.py @@ -294,13 +294,6 @@ def migrations(ctx): f"python manage.py migrate --noinput --settings={_localsettings()} --database=datastore", pty=True, ) - try: - ctx.run( - f"python manage.py rebuild_index --noinput --settings={_localsettings()}", - pty=True, - ) - except Exception: - pass @task From 026505bb3fca900c416ccef94e2c3c8034830f68 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 10 Jun 2026 16:22:56 +0200 Subject: [PATCH 19/98] [Fixes #14318] Improve logging (#14319) (#14320) (cherry picked from commit 2c0fdfdff427eefd07af563dc39f2ee070041470) Co-authored-by: Emanuele Tajariol --- geonode/settings.py | 39 ++++++++++++++++++--------------------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/geonode/settings.py b/geonode/settings.py index 1a4646e4a2b..948abfa0db0 100644 --- a/geonode/settings.py +++ b/geonode/settings.py @@ -651,31 +651,34 @@ "version": 1, "disable_existing_loggers": False, "formatters": { - "verbose": {"format": "%(levelname)s %(asctime)s %(module)s %(process)d " "%(thread)d %(message)s"}, - "simple": { - "format": "%(message)s", + "verbose": { + "format": "%(asctime)s.%(msecs)03d %(levelname)-7s %(module)-15s %(process)d:%(thread)x - %(message)s", + "datefmt": "%Y-%m-%dT%H:%M:%S", + }, + "simple": {"format": "%(message)s"}, + "br": { + "format": "%(asctime)s.%(msecs)03d %(levelname)-7s %(message)s", + "datefmt": "%Y-%m-%d %H:%M:%S", }, - "br": {"format": "%(levelname)-7s %(asctime)s %(message)s"}, }, - "filters": {"require_debug_false": {"()": "django.utils.log.RequireDebugFalse"}}, + "filters": {}, "handlers": { - "console": {"level": "ERROR", "class": "logging.StreamHandler", "formatter": "simple"}, - "mail_admins": { - "level": "ERROR", - "filters": ["require_debug_false"], - "class": "django.utils.log.AdminEmailHandler", - }, + "console": {"level": "DEBUG", "class": "logging.StreamHandler", "formatter": "verbose"}, "br": {"level": "DEBUG", "class": "logging.StreamHandler", "formatter": "br"}, }, + "root": { + "handlers": ["console"], + "level": "WARNING", + }, "loggers": { "django": { - "level": "WARN", + "level": "WARNING", }, "geonode": { - "level": "WARN", + "level": "WARNING", }, "importer": { - "level": "INFO", + "level": "WARNING", }, "geonode.br": {"level": "INFO", "handlers": ["br"], "propagate": False}, "geoserver-restconfig.catalog": { @@ -688,13 +691,7 @@ "level": "ERROR", }, "celery": { - "level": "WARN", - }, - "mapstore2_adapter.plugins.serializers": { - "level": "ERROR", - }, - "geonode_logstash.logstash": { - "level": "ERROR", + "level": "WARNING", }, }, } From 55bdd6c42fcc5f4a1e9caf835352d6195813ad08 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 11 Jun 2026 15:50:13 +0200 Subject: [PATCH 20/98] [Fixes #14321] copy_with_dump is missing from copy_table_with_ogr2ogr (#14323) (#14324) * [Fixes #14321] copy_with_dump is missing from copy_table_with_ogr2ogr --------- (cherry picked from commit a0f50e5eb4cf6d19693967879aa78a296037d7ac) Co-authored-by: mattiagiupponi <51856725+mattiagiupponi@users.noreply.github.com> Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- .env_dev | 2 +- .../upload/handlers/common/tests_vector.py | 30 +++++++- geonode/upload/handlers/common/vector.py | 68 +++++++++++++++---- 3 files changed, 84 insertions(+), 16 deletions(-) diff --git a/.env_dev b/.env_dev index 9c9b7eaa176..755e6c3b39a 100644 --- a/.env_dev +++ b/.env_dev @@ -210,4 +210,4 @@ UPSERT_CHUNK_SIZE= 100 UPSERT_LIMIT_ERROR_LOG=100 # Enable or not the XLSX / XLS upload -XLSX_UPLOAD_ENABLED=False \ No newline at end of file +XLSX_UPLOAD_ENABLED=False diff --git a/geonode/upload/handlers/common/tests_vector.py b/geonode/upload/handlers/common/tests_vector.py index 916564701bf..a0b8b214dce 100644 --- a/geonode/upload/handlers/common/tests_vector.py +++ b/geonode/upload/handlers/common/tests_vector.py @@ -482,7 +482,35 @@ def test_import_with_ogr2ogr_without_errors_should_call_the_right_command_if_dum # 4. Verify the password is passed securely in 'env', not in the command list self.assertIn("PGPASSWORD", psql_kwargs["env"]) - # Ensure the password is NOT in the actual command list (the security fix!) + + self.assertFalse(any("PGPASSWORD" in str(arg) for arg in psql_args)) + + @patch.dict(os.environ, {"OGR2OGR_COPY_WITH_DUMP": "True"}) + @patch("geonode.upload.handlers.common.vector.Popen") + def test_copy_with_ogr2ogr_without_errors_should_call_the_right_command_if_dump_is_enabled(self, _open): + # We need the second process (psql) to return the communicate values + comm = MagicMock() + comm.communicate.return_value = b"", b"" + _open.return_value = comm + + BaseVectorFileHandler.copy_table_with_ogr2ogr("original_table", "new_table", "datastore") + + self.assertEqual(_open.call_count, 2) + + ogr_args = _open.mock_calls[0][1][0] + self.assertIn("-f", ogr_args) + self.assertIn("PGDump", ogr_args) + self.assertIn("/vsistdout/", ogr_args) + self.assertNotIn("PostgreSQL", ogr_args) + + psql_args = _open.mock_calls[1][1][0] + psql_kwargs = _open.mock_calls[1][2] + + self.assertIn("psql", psql_args) + self.assertIn("-d", psql_args) + + self.assertIn("PGPASSWORD", psql_kwargs["env"]) + self.assertFalse(any("PGPASSWORD" in str(arg) for arg in psql_args)) def test_select_valid_layers(self): diff --git a/geonode/upload/handlers/common/vector.py b/geonode/upload/handlers/common/vector.py index 59ebf0f08b2..b0b2cb9df15 100644 --- a/geonode/upload/handlers/common/vector.py +++ b/geonode/upload/handlers/common/vector.py @@ -319,19 +319,48 @@ def copy_table_with_ogr2ogr(original_table_name: str, new_table_name: str, db_na if not ogr_exe: raise Exception("ogr2ogr executable not found.") - command = [ - ogr_exe, - "-f", - "PostgreSQL", - db_connection_string, - db_connection_string, - "-nln", - new_table_name, - original_table_name, - "-overwrite", - ] - process = Popen(command, stdout=PIPE, stderr=PIPE) - stdout, stderr = process.communicate() + copy_with_dump = ast.literal_eval(os.getenv("OGR2OGR_COPY_WITH_DUMP", "False")) + + base_args = ["-nln", new_table_name, original_table_name, "-overwrite"] + + if copy_with_dump: + command = [ + ogr_exe, + "--config", + "PG_USE_COPY", + "YES", + "-f", + "PGDump", + "/vsistdout/", + db_connection_string, + ] + base_args + + host = _datastore.get("HOST") or "localhost" + port = str(_datastore.get("PORT") or 5432) + env = {**os.environ, "PGPASSWORD": _datastore["PASSWORD"]} + psql_cmd = [ + "psql", + "-v ON_ERROR_STOP=1", + "-d", + _datastore["NAME"], + "-h", + host, + "-p", + port, + "-U", + _datastore["USER"], + "-f", + "-", + ] + + p1 = Popen(command, stdout=PIPE, stderr=PIPE) + p2 = Popen(psql_cmd, stdin=p1.stdout, stdout=PIPE, stderr=PIPE, env=env) + p1.stdout.close() + stdout, stderr = p2.communicate() + else: + command = [ogr_exe, "-f", "PostgreSQL", db_connection_string, db_connection_string] + base_args + process = Popen(command, stdout=PIPE, stderr=PIPE) + stdout, stderr = process.communicate() if ( stderr is not None @@ -1670,7 +1699,18 @@ def import_with_ogr2ogr( # If using a pipe (ogr2ogr | psql), we must handle it via Python # because shell=False doesn't understand the "|" symbol. _datastore = settings.DATABASES["datastore"] - psql_cmd = ["psql", "-d", _datastore["NAME"], "-h", _datastore["HOST"], "-U", _datastore["USER"], "-f", "-"] + psql_cmd = [ + "psql", + "-v ON_ERROR_STOP=1", + "-d", + _datastore["NAME"], + "-h", + _datastore["HOST"], + "-U", + _datastore["USER"], + "-f", + "-", + ] env = os.environ.copy() env["PGPASSWORD"] = _datastore["PASSWORD"] From 2168d419c8fa74e3ff22daf739d97bffbb4695dd Mon Sep 17 00:00:00 2001 From: Emanuele Tajariol Date: Wed, 10 Jun 2026 19:51:35 +0200 Subject: [PATCH 21/98] Sanitize metadata input (cherry picked from commit 0803857b22eba5b55729145ef401b1bc1c23e332) --- geonode/metadata/handlers/abstract.py | 2 +- geonode/metadata/handlers/meta.py | 102 ++++++++++++++++++ geonode/metadata/manager.py | 3 +- geonode/metadata/settings.py | 1 + geonode/metadata/tests/tests.py | 62 ++++++++++- .../thesauri/labels-i18n.metadata.rdf | 70 ++++++++++++ 6 files changed, 236 insertions(+), 4 deletions(-) create mode 100644 geonode/metadata/handlers/meta.py create mode 100644 geonode/metadata/thesauri/labels-i18n.metadata.rdf diff --git a/geonode/metadata/handlers/abstract.py b/geonode/metadata/handlers/abstract.py index 19e4f9d55f8..74d0dce3f97 100644 --- a/geonode/metadata/handlers/abstract.py +++ b/geonode/metadata/handlers/abstract.py @@ -160,7 +160,7 @@ def _set_error(errors: dict, path: list, msg: str): @staticmethod def localize_message(context: dict, msg_code: str, msg_info: dict): - msg_loc: str = labelResolver.gettext(msg_code) + msg_loc: str = labelResolver.gettext(msg_code, lang=context.get("lang", None)) if msg_loc: tokens = defaultdict(lambda: "N/A", msg_info or {}) return msg_loc.format_map(tokens) diff --git a/geonode/metadata/handlers/meta.py b/geonode/metadata/handlers/meta.py new file mode 100644 index 00000000000..fd3d42120af --- /dev/null +++ b/geonode/metadata/handlers/meta.py @@ -0,0 +1,102 @@ +######################################################################### +# +# Copyright (C) 2026 OSGeo +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +######################################################################### + +import logging +from html import unescape +import re + +from bs4 import BeautifulSoup + +from geonode.base.models import ResourceBase +from geonode.metadata.handlers.abstract import MetadataHandler + +logger = logging.getLogger(__name__) + + +class CleanupHandler(MetadataHandler): + _HTML_LIKE_PATTERN = re.compile(r"<\s*/?\s*[a-zA-Z][^>]*>") + _DANGEROUS_TAGS = ("script", "style", "noscript", "iframe", "object", "embed") + + @staticmethod + def _preview(value, max_len=120): + text = repr(value) + return text if len(text) <= max_len else f"{text[: max_len - 1]}…" + + @classmethod + def _sanitize_string(cls, value: str): + normalized = unescape(value) + if not cls._HTML_LIKE_PATTERN.search(normalized): + return value, False + + soup = BeautifulSoup(normalized, "html.parser") + for tag in soup(cls._DANGEROUS_TAGS): + tag.decompose() + + sanitized = soup.get_text() + return sanitized, sanitized != value + + def _sanitize_instance(self, value, context, errors, path=None): + path = path or [] + + if isinstance(value, dict): + for key, nested_value in list(value.items()): + nested_path = path + [str(key)] + value[key] = self._sanitize_instance(nested_value, context, errors, nested_path) + return value + + if isinstance(value, list): + for idx, nested_value in enumerate(list(value)): + nested_path = path + [f"[{idx}]"] + value[idx] = self._sanitize_instance(nested_value, context, errors, nested_path) + return value + + if isinstance(value, str): + sanitized, changed = self._sanitize_string(value) + if changed: + logger.warning( + "Sanitized potentially unsafe metadata field '%s': %s -> %s", + ".".join(path) if path else "ROOT", + self._preview(value), + self._preview(sanitized), + ) + self._set_error( + errors, + path[0:1], # set error on root field + self.localize_message(context, "metadata_error_sanitized", {}), + ) + return sanitized + + return value + + def update_schema(self, jsonschema: dict, context: dict, lang=None): + return jsonschema + + def get_jsonschema_instance( + self, resource: ResourceBase, field_name: str, context: dict, errors: dict, lang: str = None + ): + pass + + def update_resource( + self, resource: ResourceBase, field_name: str, json_instance: dict, context: dict, errors: dict, **kwargs + ): + pass + + def pre_deserialization(self, resource, jsonschema: dict, instance: dict, partial: set, context: dict): + errors = context["errors"] + self._sanitize_instance(instance, context, errors) diff --git a/geonode/metadata/manager.py b/geonode/metadata/manager.py index 51081a66ab3..c973f082dfc 100644 --- a/geonode/metadata/manager.py +++ b/geonode/metadata/manager.py @@ -59,7 +59,7 @@ def post_init(self): handler.post_init() def _init_schema_context(self, lang): - return {} + return {"lang": lang} if lang else {} def build_schema(self, lang=None): logger.debug(f"build_schema {lang}") @@ -144,6 +144,7 @@ def update_schema_instance(self, resource, request_obj, lang=None, partial=None) handler.load_deserialization_context(resource, schema, context) errors = {} + context["errors"] = errors for handler in self.handlers.values(): handler.pre_deserialization(resource, schema, json_instance, partial, context) diff --git a/geonode/metadata/settings.py b/geonode/metadata/settings.py index f5a20ac2845..b1f4bee797f 100644 --- a/geonode/metadata/settings.py +++ b/geonode/metadata/settings.py @@ -13,6 +13,7 @@ JSONSCHEMA_BASE = os.path.join(PROJECT_ROOT, "metadata/schemas/base.json") METADATA_HANDLERS = { + "metadata_cleaner": "geonode.metadata.handlers.meta.CleanupHandler", "base": "geonode.metadata.handlers.base.BaseHandler", "thesaurus": "geonode.metadata.handlers.thesaurus.TKeywordsHandler", "hkeyword": "geonode.metadata.handlers.hkeyword.HKeywordHandler", diff --git a/geonode/metadata/tests/tests.py b/geonode/metadata/tests/tests.py index 4f82dd55bf1..a0e9649b3c9 100644 --- a/geonode/metadata/tests/tests.py +++ b/geonode/metadata/tests/tests.py @@ -25,13 +25,16 @@ from django.urls import reverse from django.contrib.auth import get_user_model -from django.test import RequestFactory +from django.test import RequestFactory, TestCase, override_settings from rest_framework import status from django.utils.translation import gettext as _ from rest_framework.test import APITestCase + +from geonode.metadata.handlers.multilang import MultiLangHandler from geonode.metadata.settings import MODEL_SCHEMA from geonode.metadata.manager import metadata_manager, CACHE_KEY_SCHEMA +from geonode.metadata.handlers.meta import CleanupHandler from geonode.metadata.api.views import ( ProfileAutocomplete, MetadataLinkedResourcesAutocomplete, @@ -919,7 +922,7 @@ def test_update_schema_instance_no_errors(self, mock_get_schema): mock_request.data = {"field1": "new_value1", "new_field2": "new_value2"} mock_request.user = self.test_user_1 - expected_context = {"user": self.test_user_1} + expected_context = {"user": self.test_user_1, "errors": {}} mock_get_schema.return_value = self.fake_schema @@ -1151,3 +1154,58 @@ def test_delete_schema_conflict_returns_409(self, mock_get_schema): url = self._url(self.resource.pk, "title") response = self.client.delete(url) self.assertEqual(response.status_code, status.HTTP_409_CONFLICT) + + +class CleanupHandlerTests(TestCase): + def setUp(self): + self.handler = CleanupHandler() + self.owner = get_user_model().objects.create_user( + "cleanup_owner", "cleanup_owner@fakemail.com", "cleanup_owner_password", is_active=True + ) + self.resource = ResourceBase.objects.create(title="Cleanup Test Resource", uuid=str(uuid4()), owner=self.owner) + + @override_settings(LANGUAGE_CODE="en") + def test_pre_deserialization_sanitizes_nested_values_and_logs_warnings(self): + instance = { + "title": "xss", + "details": { + "summary": "plain text", + "body": "safe", + }, + "items": ["ok", "bad"], + "count": 3, + } + + with self.assertLogs("geonode.metadata.handlers.meta", level="WARNING") as cm: + context = {"errors": {}} + self.handler.pre_deserialization(self.resource, {}, instance, partial=set(), context=context) + + self.assertEqual(instance["title"], "xss") + self.assertEqual(instance["details"]["body"], "safe") + self.assertEqual(instance["items"][1], "bad") + self.assertEqual(instance["count"], 3) + + logs = "\n".join(cm.output) + self.assertIn("Sanitized potentially unsafe metadata field 'title'", logs) + self.assertIn("Sanitized potentially unsafe metadata field 'details.body'", logs) + self.assertIn("Sanitized potentially unsafe metadata field 'items.[1]'", logs) + + self.assertIn("title", context["errors"]) + self.assertIn("__errors", context["errors"]["title"]) + self.assertIn("metadata_error_sanitized", context["errors"]["title"]["__errors"]) + + @override_settings(LANGUAGE_CODE="en", MULTILANG_FIELDS=["title"]) + def test_pre_deserialization_copies_sanitized_default_lang_value(self): + instance = { + "title_multilang_en": 'Hello', + } + context = {"errors": {}} + + ml_handler = MultiLangHandler() + with self.assertLogs("geonode.metadata.handlers.meta", level="WARNING") as cm: + self.handler.pre_deserialization(self.resource, {}, instance, partial=set(), context=context) + ml_handler.pre_deserialization(self.resource, {}, instance, partial=set(), context=context) + + self.assertEqual(instance["title_multilang_en"], "Hello") + self.assertEqual(instance["title"], "Hello") + self.assertIn("Sanitized potentially unsafe metadata field 'title_multilang_en'", "\n".join(cm.output)) diff --git a/geonode/metadata/thesauri/labels-i18n.metadata.rdf b/geonode/metadata/thesauri/labels-i18n.metadata.rdf new file mode 100644 index 00000000000..dd929a007c9 --- /dev/null +++ b/geonode/metadata/thesauri/labels-i18n.metadata.rdf @@ -0,0 +1,70 @@ + + + + Localizzazione labels + Labels localization + Localizzazione labels + 2026-06-10T16:54:03 + 2026-06-10T16:54:03 + + + + metadata_error_empty_field fieldname:{fieldname} + Missing value + Valore richiesto + + + + metadata_error_indexing exc:{exc} + Error while indexing metadata: {exc} + Errore nell'indicizzazione dei metadati: {exc} + + + + metadata_error_post_save handler:{handler} exc:{exc} + Error in post-save procedure: {exc} + Errore nella procedura di post-save: {exc} + + + + metadata_error_pre_save handler:{handler} exc:{exc} + Error in pre-save procedure: {exc} + Errore nella procedura di pre-save: {exc} + + + + metadata_error_sanitized + WARNING: possible injection attempt, this field has been sanitized. Reload this page. + ATTENZIONE: possibile injection, il campo è stato modificato. Ricaricare la pagina. + + + + metadata_error_save: {exc} + Error while saving metadata: {exc} + Errore nel salvataggio dei metadati: {exc} + + + + metadata_error_store fieldname:{fieldname} exc:{exc} + Error while saving metadata: {exc} + Errore nel salvataggio dei metadati: {exc} + + + + metadata_error_update fieldname:{fieldname} handler:{handler} exc:{exc} + Error while updating metadata: {exc} + Errore nell'aggiornamento dei metadati: {exc} + + + + metadata_sparse_error_parse fieldname:{fieldname} type:{type} value:{value} + Parsing error + Errore nel parsing + + + + metadata_sparse_error_type fieldname:{fieldname} type:{type} + Unexpected field type: {type} + Tipo inaspettato: {type} + + From 2263d0b2b6273a37239ce6e4c8fcd04fe7c8752b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 17 Jun 2026 17:10:54 +0200 Subject: [PATCH 22/98] [Fixes #14339] Drop support Dropbox storage manager (#14347) (cherry picked from commit 6b1d8db27130f0a3ce00cbe59bcf3f42dc7e5189) Co-authored-by: Mattia Giupponi --- geonode/storage/dropbox.py | 64 ----------------------------- geonode/storage/tests.py | 84 -------------------------------------- pyproject.toml | 1 - 3 files changed, 149 deletions(-) delete mode 100644 geonode/storage/dropbox.py diff --git a/geonode/storage/dropbox.py b/geonode/storage/dropbox.py deleted file mode 100644 index 2d012da89d6..00000000000 --- a/geonode/storage/dropbox.py +++ /dev/null @@ -1,64 +0,0 @@ -######################################################################### -# -# Copyright (C) 2021 OSGeo -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# -######################################################################### -from storages.utils import setting -from storages.backends.dropbox import _DEFAULT_MODE, _DEFAULT_TIMEOUT, DropBoxStorage - -from geonode.storage.manager import StorageManagerInterface - - -class DropboxStorageManager(StorageManagerInterface): - def __init__(self): - location = setting("DROPBOX_ROOT_PATH", "/") - oauth2_access_token = setting("DROPBOX_OAUTH2_TOKEN") - timeout = setting("DROPBOX_TIMEOUT", _DEFAULT_TIMEOUT) - write_mode = setting("DROPBOX_WRITE_MODE", _DEFAULT_MODE) - - self._drx = DropBoxStorage( - oauth2_access_token=oauth2_access_token, root_path=location, timeout=timeout, write_mode=write_mode - ) - - def _get_concrete_manager(self): - return DropboxStorageManager() - - def delete(self, name): - return self._drx.delete(name) - - def exists(self, name): - return self._drx.exists(name) - - def listdir(self, path): - return self._drx.listdir(path) - - def open(self, name, mode="rb"): - return self._drx.open(name, mode=mode) - - def path(self, name): - return self._drx._full_path(name) - - def save(self, name, content, max_length=None): - return self._drx.save(name, content) - - def url(self, name): - return self._drx.url(name) - - def size(self, name): - return self._drx.size(name) - - def generate_filename(self, filename): - return self._drx.generate_filename(filename) diff --git a/geonode/storage/tests.py b/geonode/storage/tests.py index 9a4401c36d5..7642d726f33 100644 --- a/geonode/storage/tests.py +++ b/geonode/storage/tests.py @@ -30,94 +30,10 @@ from geonode.storage.exceptions import DataRetrieverExcepion from geonode.storage.manager import StorageManager from geonode.storage.gcs import GoogleStorageManager -from geonode.storage.dropbox import DropboxStorageManager from geonode.base.populate_test_data import create_single_dataset from geonode.tests.base import GeoNodeBaseTestSupport -class TestDropboxStorageManager(SimpleTestCase): - def setUp(self): - with self.settings(DROPBOX_OAUTH2_TOKEN="auth_token"): - self.sut = DropboxStorageManager() - - @patch("geonode.storage.dropbox.DropBoxStorage.delete") - def test_dropbox_deleted(self, dbx): - """ - Will test that the function returns the expected result - and that the DropBoxStorage function as been called with the expected parameters - """ - dbx.return_value = None - output = self.sut.delete("filename") - self.assertIsNone(output) - dbx.assert_called_once_with("filename") - - @patch("geonode.storage.dropbox.DropBoxStorage.exists") - def test_dropbox_exists(self, dbx): - """ - Will test that the function returns the expected result - and that the DropBoxStorage function as been called with the expected parameters - """ - dbx.return_value = True - output = self.sut.exists("filename") - self.assertTrue(output) - dbx.assert_called_once_with("filename") - - @patch("geonode.storage.dropbox.DropBoxStorage.listdir") - def test_dropbox_listdir(self, dbx): - """ - Will test that the function returns the expected result - and that the DropBoxStorage function as been called with the expected parameters - """ - dbx.return_value = (["folder1"], ["file1", "file2"]) - output = self.sut.listdir("Apps/") - self.assertTupleEqual((["folder1"], ["file1", "file2"]), output) - dbx.assert_called_once_with("Apps/") - - @patch("geonode.storage.dropbox.DropBoxStorage._open") - def test_dropbox_open(self, dbx): - """ - Will test that the function returns the expected result - and that the DropBoxStorage function as been called with the expected parameters - """ - dbx.return_value = io.StringIO() - output = self.sut.open("name", mode="xx") - self.assertEqual(type(output), io.StringIO().__class__) - dbx.assert_called_once_with("name", "xx") - - @patch("geonode.storage.dropbox.DropBoxStorage._full_path") - def test_dropbox_path(self, dbx): - """ - Will test that the function returns the expected result - and that the DropBoxStorage function as been called with the expected parameters - """ - dbx.return_value = "/opt/full/path/to/file" - output = self.sut.path("file") - self.assertEqual("/opt/full/path/to/file", output) - dbx.assert_called_once_with("file") - - @patch("geonode.storage.dropbox.DropBoxStorage.save") - def test_dropbox_save(self, dbx): - """ - Will test that the function returns the expected result - and that the DropBoxStorage function as been called with the expected parameters - """ - dbx.return_value = "cleaned_name" - output = self.sut.save("file_name", "content") - self.assertEqual("cleaned_name", output) - dbx.assert_called_once_with("file_name", "content") - - @patch("geonode.storage.dropbox.DropBoxStorage.size") - def test_dropbox_size(self, dbx): - """ - Will test that the function returns the expected result - and that the DropBoxStorage function as been called with the expected parameters - """ - dbx.return_value = 1 - output = self.sut.size("name") - self.assertEqual(1, output) - dbx.assert_called_once_with("name") - - class TestGoogleStorageManager(SimpleTestCase): def setUp(self): self.sut = GoogleStorageManager diff --git a/pyproject.toml b/pyproject.toml index 5802ab741d8..13369ce9fe4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -116,7 +116,6 @@ dependencies = [ "django-bootstrap3-datetimepicker-2==2.8.3", # storage manager dependencies "django-storages==1.14.3", - "dropbox==11.36.2", "google-cloud-storage==2.16.0", "boto3==1.39.3", # Django Caches From 7950e61b55a2b0ff69e352d48bf5b2903e3968c8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 17 Jun 2026 17:11:10 +0200 Subject: [PATCH 23/98] build(deps): bump cryptography from 48.0.0 to 49.0.0 (#14346) Bumps [cryptography](https://github.com/pyca/cryptography) from 48.0.0 to 49.0.0. - [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pyca/cryptography/compare/48.0.0...49.0.0) --- updated-dependencies: - dependency-name: cryptography dependency-version: 49.0.0 dependency-type: direct:production update-type: version-update:semver-major ... (cherry picked from commit 2b7c0ff9f68147cc9db139aee56229acb8235eac) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 13369ce9fe4..8d7bbd25809 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -155,7 +155,7 @@ dependencies = [ "selenium-requests==2.0.4", "webdriver_manager==4.0.1", # Security and audit - "cryptography==48.0.0", + "cryptography==49.0.0", "jwcrypto>=1.5.7", # dependency for XLSX handler "python-calamine==0.6.2", From eadafcdc2a8797926189538a574f6990b2e773fe Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 17 Jun 2026 17:11:31 +0200 Subject: [PATCH 24/98] Upgrade of setuptools with fixes to dependencies (#14330) (#14349) * Upgrade of setupttols to 82 with fixes to dependencies * Sanitize metadata input * build(deps): bump cryptography from 48.0.0 to 49.0.0 Bumps [cryptography](https://github.com/pyca/cryptography) from 48.0.0 to 49.0.0. - [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pyca/cryptography/compare/48.0.0...49.0.0) --- updated-dependencies: - dependency-name: cryptography dependency-version: 49.0.0 dependency-type: direct:production update-type: version-update:semver-major ... * [Fixes #14339] Drop support Dropbox storage manager --------- (cherry picked from commit 3d929fcb0501106b0c183946a24778bb5e95e7fc) Signed-off-by: dependabot[bot] Co-authored-by: Giovanni Allegri Co-authored-by: Emanuele Tajariol Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Mattia Giupponi --- pyproject.toml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 8d7bbd25809..6b27ef3bcf4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,8 +57,8 @@ dependencies = [ "django-mptt==0.16.0", "django-treebeard==4.7.1", "django-guardian<2.4.1", - "django-downloadview==2.3.0", - "django-polymorphic==3.1.0", + "django-downloadview==2.4.0", + "django-polymorphic==4.0.0", "django-tastypie<0.15.0", "django-tinymce==3.7.1", "django-uuid-upload-path==1.0.0", @@ -95,12 +95,12 @@ dependencies = [ "djangorestframework-guardian==0.3.0", "drf-extensions==0.7.1", "geonode-dynamic-rest==2.3.0.2", - "geonode-pinax-notifications==6.0.0.3", + "geonode-pinax-notifications==6.0.0.4", # GeoNode org maintained apps. "django_geonode_mapstore_client @ git+https://github.com/GeoNode/geonode-mapstore-client.git@5.1.x", "django-avatar==8.0.1", - "geonode-oauth-toolkit==2.2.2.2", + "geonode-oauth-toolkit==2.2.3.1", "geonode-announcements==2.0.2.2", "django-activity-stream==2.0.0", "gn-arcrest==10.5.6", @@ -109,7 +109,7 @@ dependencies = [ "gisdata==0.5.4", # importer dependencies # blocked to <80 to avoid warning during the runtime for with py312 related to the polymorphic model - "setuptools>=59,<80", + "setuptools<83", "pdok-geopackage-validator==0.14.4", "geonode-django-dynamic-model==0.4.0", # datetimepicker widget From 1ca8488a9c9e47674dcdbb84d8c11736acb7ee16 Mon Sep 17 00:00:00 2001 From: Giovanni Allegri Date: Thu, 11 Jun 2026 18:37:43 +0200 Subject: [PATCH 25/98] Update lxml dependency version to 6.1.1 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 6b27ef3bcf4..4bd3093f9f3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,7 +25,7 @@ classifiers = [ # Core dependencies (consolidated from requirements.txt) dependencies = [ "Pillow==12.2.0", - "lxml==5.2.1", + "lxml==6.1.1", "psycopg2==2.9.12", "Django==5.2.15", From 2f11330c8fce83ce8030f7cae932a118fbeef84d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 17 Jun 2026 17:59:42 +0200 Subject: [PATCH 26/98] [Fixes #14344] Pack rdf files in build distribution (#14345) (#14350) (cherry picked from commit 109bc4bc635e0bde7adc3f525ad288d1311a1be3) Co-authored-by: Emanuele Tajariol --- pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 4bd3093f9f3..8c2b88051ee 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -182,6 +182,9 @@ Documentation = "https://docs.geonode.org/" [tool.setuptools] include-package-data = true +[tool.setuptools.package-data] +"geonode" = ["**/thesauri/*.rdf"] + [tool.setuptools.packages.find] where = ["."] exclude = ["tests*"] From c78706ce5ec75804efb06bf43fff5edc03b92069 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 17 Jun 2026 18:01:44 +0200 Subject: [PATCH 27/98] [Fixes #14000] Refact extraMetadata (#14286) (#14351) * Squashed PR #14001 (cherry picked from commit a9ade4357dd3aaecd37654627c6d694a283679a1) Co-authored-by: Emanuele Tajariol --- geonode/api/resourcebase_api.py | 83 +++- geonode/api/tests.py | 50 ++- geonode/base/api/deprecated_extra_metadata.py | 353 ++++++++++++++++++ geonode/base/api/serializers.py | 26 +- geonode/base/api/tests.py | 221 +++++++---- geonode/base/api/views.py | 84 +---- ...0_migrate_extrametadata_to_sparsefields.py | 52 +++ geonode/base/models.py | 12 - geonode/base/utils.py | 31 -- geonode/geoapps/api/tests.py | 13 - geonode/layers/api/serializers.py | 1 - geonode/layers/api/tests.py | 11 - geonode/maps/api/tests.py | 11 - geonode/resource/manager.py | 2 - geonode/resource/utils.py | 8 - geonode/settings.py | 45 +-- 16 files changed, 688 insertions(+), 315 deletions(-) create mode 100644 geonode/base/api/deprecated_extra_metadata.py create mode 100644 geonode/base/migrations/0100_migrate_extrametadata_to_sparsefields.py diff --git a/geonode/api/resourcebase_api.py b/geonode/api/resourcebase_api.py index cf98190ac8e..9ab377cab32 100644 --- a/geonode/api/resourcebase_api.py +++ b/geonode/api/resourcebase_api.py @@ -17,6 +17,7 @@ # ######################################################################### from geonode.base.enumerations import LAYER_TYPES +import json import logging from django.db.models import Q @@ -45,6 +46,7 @@ from geonode.groups.models import GroupProfile from geonode.utils import check_ogc_backend from geonode.security.utils import get_visible_resources +from geonode.metadata.models import SparseField from .authentication import OAuthAuthentication from .authorization import GeoNodeAuthorization, GeonodeApiKeyAuthentication @@ -133,6 +135,11 @@ class CommonModelApi(ModelResource): "metadata_only", ] + @staticmethod + def _extract_deprecated_metadata_filters(filters): + """Extract legacy ``metadata__*`` query params for sparse-field lookup.""" + return {key: value for key, value in filters.items() if key.startswith("metadata__")} + def build_filters(self, filters=None, ignore_bad_filters=False, **kwargs): if filters is None: filters = {} @@ -142,7 +149,9 @@ def build_filters(self, filters=None, ignore_bad_filters=False, **kwargs): if "app_type__in" in filters: orm_filters.update({"resource_type": filters["app_type__in"].lower()}) - _metadata = {f"metadata__{_k}": _v for _k, _v in filters.items() if _k.startswith("metadata__")} + # Deprecated compatibility: keep supporting metadata__* filters + # by mapping them to SparseField lookups in apply_filters. + _metadata = self._extract_deprecated_metadata_filters(filters) if _metadata: orm_filters.update({"metadata_filters": _metadata}) @@ -208,7 +217,7 @@ def apply_filters(self, request, applicable_filters): filtered = self.filter_h_keywords(filtered, keywords) if metadata_filters: - filtered = filtered.filter(**metadata_filters) + filtered = self.filter_sparse_fields(filtered, metadata_filters) # return filtered return get_visible_resources( @@ -236,6 +245,75 @@ def filter_h_keywords(self, queryset, keywords): filtered = queryset return filtered + def filter_sparse_fields(self, queryset, metadata_filters): + """ + Filter queryset by sparse field values (metadata custom fields). + + Queryset is filtered by interrogating SparseField entries that match + the given metadata filter specifications. + + Args: + queryset: ResourceBase queryset to filter + metadata_filters: dict with keys like "metadata__key" and values to match + + Returns: + Filtered queryset containing only resources with matching sparse fields + """ + if not metadata_filters: + return queryset + + filtered_pks = set() + found_metadata_filter = False + + for filter_key, filter_value in metadata_filters.items(): + # Extract field name from "metadata__fieldname" + if not filter_key.startswith("metadata__"): + continue + + found_metadata_filter = True + field_name = filter_key[len("metadata__") :] + + # Text prefilter to reduce the set to deserialize; the actual + # semantic match is performed after json.loads. + sparse_fields = ( + SparseField.objects.filter(name__startswith="extra_") + .filter(value__icontains=field_name) + .filter(value__icontains=str(filter_value)) + ) + + batch_pks = set() + for sf in sparse_fields: + try: + # ExtraMetadata was a JSONfield + stored_value = json.loads(sf.value) if sf.value and sf.value.startswith("{") else sf.value + except (json.JSONDecodeError, TypeError): + logger.warning( + f"Bad migrated ExtraMetadata into SparseField: {sf.name} for resource {sf.resource.id}:{sf.resource.title}" + ) + continue + + if not isinstance(stored_value, dict): + logger.warning( + f"Unexpected non-dict value in SparseField: {sf.name} for resource {sf.resource.id}:{sf.resource.title}" + ) + continue + + # Compare values in a type-agnostic way. + if str(stored_value.get(field_name, None)) == str(filter_value): + batch_pks.add(sf.resource.pk) + + filtered_pks.update(batch_pks) + + # Filter by the collected PKs + # If we processed metadata filters, return only resources with matching values + if found_metadata_filter: + filtered = queryset.filter(pk__in=filtered_pks) if filtered_pks else queryset.none() + else: + # No metadata filters found, return queryset as-is + filtered = queryset + + return filtered + def get_list(self, request, **kwargs): """ Returns a serialized list of resources. @@ -249,6 +327,7 @@ def get_list(self, request, **kwargs): # impossible. base_bundle = self.build_bundle(request=request) objects = self.obj_get_list(bundle=base_bundle, **self.remove_api_resource_names(kwargs)) + sorted_objects = self.apply_sorting(objects, options=request.GET) paginator = self._meta.paginator_class( diff --git a/geonode/api/tests.py b/geonode/api/tests.py index 3538dc8b987..448c6775bfb 100644 --- a/geonode/api/tests.py +++ b/geonode/api/tests.py @@ -37,7 +37,6 @@ from geonode.layers.models import Dataset from geonode.documents.models import Document from geonode.base.models import ( - ExtraMetadata, Thesaurus, ThesaurusLabel, ThesaurusKeyword, @@ -52,6 +51,7 @@ from geonode.tests.base import GeoNodeBaseTestSupport from geonode.base.populate_test_data import all_public, create_models, remove_models from geonode.security.registry import permissions_registry +from geonode.metadata.models import SparseField from geonode.assets.models import Asset from django.core.files.uploadedfile import SimpleUploadedFile from geonode.base.models import Link @@ -536,35 +536,49 @@ def test_category_filters(self): self.assertEqual(len(self.deserialize(resp)["objects"]), 5) def test_metadata_filters(self): - """Test category filtering""" + """Test metadata filtering against sparse fields.""" _r = Dataset.objects.first() - _m = ExtraMetadata.objects.create( + + # Create sparse field using migrated ExtraMetadata format. + SparseField.objects.update_or_create( resource=_r, - metadata={ - "name": "metadata-updated", - "slug": "metadata-slug-updated", - "help_text": "this is the help text-updated", - "field_type": "str-updated", - "value": "my value-updated", - "category": "category", - }, + name="extra_1", + defaults={"value": json.dumps({"category": "category"})}, ) + # Verify sparse field was created + _sf_check = SparseField.objects.filter(resource=_r, name="extra_1") + self.assertTrue(_sf_check.exists(), "SparseField should have been created") + self.assertEqual(_sf_check.first().value, json.dumps({"category": "category"})) + list_url = reverse("api_dispatch_list", kwargs={"api_name": "api", "resource_name": "datasets"}) - _r.metadata.add(_m) - # check we get the correct layers number returnered filtering on one - # and then two different categories - filter_url = f"{list_url}?metadata__category=category" + # Test 1: Filter for existing category value + filter_url = f"{list_url}?metadata__category=category" resp = self.api_client.get(filter_url) self.assertValidJSONResponse(resp) - self.assertEqual(len(self.deserialize(resp)["objects"]), 1) - filter_url = f"{list_url}?metadata__category=not-existing-category" + result = self.deserialize(resp) + result_count = len(result["objects"]) + logger.debug(f"Test 1 - Filter metadata__category=category: Got {result_count} results (expected 1)") + logger.debug(f"Result PKs: {[obj['id'] for obj in result['objects']]}") + logger.debug(f"Created resource PK: {_r.pk}") + self.assertEqual(result_count, 1, f"Expected 1 result, got {result_count}") + self.assertEqual(result["objects"][0]["id"], _r.pk) + + # Test 2: Filter for non-existing category value + filter_url = f"{list_url}?metadata__category=not-existing-category" resp = self.api_client.get(filter_url) self.assertValidJSONResponse(resp) - self.assertEqual(len(self.deserialize(resp)["objects"]), 0) + + result = self.deserialize(resp) + result_count = len(result["objects"]) + logger.debug( + f"Test 2 - Filter metadata__category=not-existing-category: Got {result_count} results (expected 0)" + ) + + self.assertEqual(result_count, 0, f"Expected 0 results, got {result_count}") def test_tag_filters(self): """Test keywords filtering""" diff --git a/geonode/base/api/deprecated_extra_metadata.py b/geonode/base/api/deprecated_extra_metadata.py new file mode 100644 index 00000000000..43ee5a02b6c --- /dev/null +++ b/geonode/base/api/deprecated_extra_metadata.py @@ -0,0 +1,353 @@ +# ######################################################################### +# +# Copyright (C) 2025 OSGeo +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# ######################################################################### +""" +Deprecated backward-compatible adapters for the ExtraMetadata API. + +These adapters re-expose the old ``/extra_metadata/`` endpoint and the +``metadata`` serializer field using :class:`SparseField` as the storage +backend. They are marked **deprecated** and should be removed after the +deprecation period. + +To remove these adapters: + 1. Delete this file. + 2. Remove the ``metadata`` field and import from ``serializers.py``. + 3. Remove ``DeprecatedExtraMetadataMixin`` and its import from ``views.py``. + 4. Remove deprecated settings from ``settings.py`` + (``DEFAULT_EXTRA_METADATA_SCHEMA``, ``CUSTOM_METADATA_SCHEMA``, + ``EXTRA_METADATA_SCHEMA``, and the ``from schema import Optional`` import). +""" + +import json +import logging +import uuid +import warnings + +from deprecated import deprecated +from rest_framework.decorators import action +from rest_framework.response import Response + +from dynamic_rest.fields.fields import DynamicComputedField + +from django.db import IntegrityError + +from geonode.base.api.permissions import IsOwnerOrAdmin, UserHasPerms +from geonode.base.models import ResourceBase +from geonode.metadata.models import SparseField + +logger = logging.getLogger(__name__) + +DEPRECATION_VERSION = "5.1.0" +DEPRECATION_REASON = ( + "The extra_metadata API is deprecated and will be removed in a future " + "version. Use the sparse fields API instead." +) +SPARSE_FIELD_PREFIX = "extra_" +# SparseField.value is CharField(max_length=1024); entries exceeding this +# limit cannot be stored and are silently skipped with a log warning. +SPARSE_FIELD_VALUE_MAX_LENGTH = 1024 + +# Query parameter names that should *not* be treated as legacy +# ``metadata__`` filters in the deprecated GET endpoint. +_NON_FILTER_QUERY_PARAMS = { + "api_preset", + "page", + "page_size", + "format", + "include[]", + "exclude[]", + "sort[]", +} + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _sparse_fields_for_resource(resource): + """Return SparseField entries that represent migrated ExtraMetadata.""" + return SparseField.objects.filter( + resource=resource, + name__startswith=SPARSE_FIELD_PREFIX, + ) + + +def _sparse_to_legacy(sparse_field): + """Convert a SparseField into the old ExtraMetadata representation. + + Returns ``{"id": , ...metadata_dict}`` so existing consumers see the + same shape they used to get. + """ + try: + metadata = json.loads(sparse_field.value) if sparse_field.value else {} + except (json.JSONDecodeError, TypeError): + metadata = {} + if not isinstance(metadata, dict): + logger.warning(f"Skipping unexpected non-dict metadata in SparseField pk={sparse_field.pk}") + metadata = {} + return {**{"id": sparse_field.pk}, **metadata} + + +def _next_sparse_name(resource): + """Generate the next available ``extra_`` name. + + Falls back to a UUID-based suffix if a name collision occurs (e.g. under + concurrent requests), keeping the ``extra_`` prefix intact. + The numeric part is still preferred for readability. + """ + existing = ( + SparseField.objects.filter( + resource=resource, + name__startswith=SPARSE_FIELD_PREFIX, + ) + .order_by("-name") + .values_list("name", flat=True) + ) + max_n = 0 + for name in existing: + suffix = name[len(SPARSE_FIELD_PREFIX) :] + try: + max_n = max(max_n, int(suffix)) + except (ValueError, TypeError): + pass + candidate = f"{SPARSE_FIELD_PREFIX}{max_n + 1}" + # Guard against a race: if the candidate already exists, use a UUID suffix + if SparseField.objects.filter(resource=resource, name=candidate).exists(): + candidate = f"{SPARSE_FIELD_PREFIX}{uuid.uuid4().hex[:12]}" + return candidate + + +# --------------------------------------------------------------------------- +# Deprecated serializer field (for ``metadata`` on ResourceBaseSerializer) +# --------------------------------------------------------------------------- + + +class DeprecatedExtraMetadataField(DynamicComputedField): + """Deferred computed field that reconstructs the legacy ``metadata`` + representation from :class:`SparseField` entries. + + .. deprecated:: 5.1.0 + Use the sparse fields API instead. + """ + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + @deprecated(version=DEPRECATION_VERSION, reason=DEPRECATION_REASON) + def get_attribute(self, instance): + warnings.warn(DEPRECATION_REASON, DeprecationWarning, stacklevel=2) + try: + qs = _sparse_fields_for_resource(instance) + return [_sparse_to_legacy(sf) for sf in qs] + except Exception as e: + logger.exception(e) + return [] + + +# --------------------------------------------------------------------------- +# Deprecated extra_metadata view action mixin +# --------------------------------------------------------------------------- + + +class DeprecatedExtraMetadataMixin: + """Mixin that adds the deprecated ``extra_metadata`` action back to a + ``ViewSet``. + + Import this mixin and add it to your ViewSet's bases to restore the + old ``/{pk}/extra_metadata/`` endpoint backed by SparseFields. + + .. deprecated:: 4.4.0 + Use the sparse fields API instead. + """ + + @action( + detail=True, + methods=["get", "put", "delete", "post"], + url_path=r"extra_metadata", + url_name="extra-metadata", + permission_classes=[IsOwnerOrAdmin, UserHasPerms(perms_dict={"default": {"POST": ["base.add_resourcebase"]}})], + ) + def extra_metadata(self, request, pk, *args, **kwargs): + """Deprecated endpoint – delegates to SparseField storage.""" + warnings.warn(DEPRECATION_REASON, DeprecationWarning, stacklevel=2) + logger.warning(DEPRECATION_REASON) + + resource = ResourceBase.objects.filter(pk=pk).first() + if resource is None: + return Response({"detail": "Not found."}, status=404) + + if request.method == "GET": + return self._extra_metadata_get(request, resource) + elif request.method == "POST": + return self._extra_metadata_post(request, resource) + elif request.method == "PUT": + return self._extra_metadata_put(request, resource) + elif request.method == "DELETE": + return self._extra_metadata_delete(request, resource) + + # -- GET ---------------------------------------------------------------- + + @staticmethod + def _extra_metadata_get(request, resource): + qs = _sparse_fields_for_resource(resource) + # Support the old query-param filtering (e.g. ?field_name=value) + for key, value in request.query_params.items(): + if key in _NON_FILTER_QUERY_PARAMS or key.startswith("filter{"): + continue + # Old API used metadata__=value JSONField lookups. + # We approximate this by filtering on the JSON string. + filtered = [] + for sf in qs: + try: + meta = json.loads(sf.value) if sf.value else {} + if not isinstance(meta, dict): + logger.warning(f"Skipping unexpected non-dict metadata in SparseField pk={sf.pk}") + continue + except (json.JSONDecodeError, TypeError): + logger.warning(f"Skipping unparsable json in SparseField pk={sf.pk}") + continue + if str(meta.get(key)) == str(value): + filtered.append(sf) + qs = filtered + break # Old API only used the first filter pair + + if isinstance(qs, list): + return Response([_sparse_to_legacy(sf) for sf in qs]) + return Response([_sparse_to_legacy(sf) for sf in qs.iterator()]) + + # -- POST --------------------------------------------------------------- + + @staticmethod + def _extra_metadata_post(request, resource): + data = request.data + if isinstance(data, str): + try: + data = json.loads(data) + except (json.JSONDecodeError, TypeError): + return Response( + {"detail": "Invalid JSON payload."}, + status=400, + ) + + if not isinstance(data, list): + return Response( + {"detail": "Expected a JSON list of metadata objects."}, + status=400, + ) + + cleaned = [] + for meta_dict in data: + if not isinstance(meta_dict, dict): + logger.warning(f"Skipping non-dict metadata entry in POST payload: {meta_dict}") + continue + meta_dict = dict(meta_dict) # avoid mutating caller input + meta_dict.pop("id", None) + value = json.dumps(meta_dict) + if len(value) > SPARSE_FIELD_VALUE_MAX_LENGTH: + return Response( + {"detail": f"serialized value exceeds {SPARSE_FIELD_VALUE_MAX_LENGTH} characters"}, status=400 + ) + cleaned.append(value) + + for value in cleaned: + name = _next_sparse_name(resource) + try: + SparseField.objects.create(resource=resource, name=name, value=value) + except IntegrityError: + # Concurrent request created the same name; retry with UUID + name = f"{SPARSE_FIELD_PREFIX}{uuid.uuid4().hex[:12]}" + SparseField.objects.create(resource=resource, name=name, value=value) + + result = [_sparse_to_legacy(sf) for sf in _sparse_fields_for_resource(resource)] + return Response(result, status=201) + + # -- PUT ---------------------------------------------------------------- + + @staticmethod + def _extra_metadata_put(request, resource): + data = request.data + if isinstance(data, str): + try: + data = json.loads(data) + except (json.JSONDecodeError, TypeError): + return Response( + {"detail": "Invalid JSON payload."}, + status=400, + ) + + if not isinstance(data, list): + return Response( + {"detail": "Expected a JSON list of metadata objects."}, + status=400, + ) + + cleaned_updates = [] + for meta_dict in data: + if not isinstance(meta_dict, dict): + continue + meta_dict = dict(meta_dict) # avoid mutating caller input + sf_id = meta_dict.pop("id", None) + if sf_id is None: + continue + value = json.dumps(meta_dict) + if len(value) > SPARSE_FIELD_VALUE_MAX_LENGTH: + return Response( + {"detail": f"serialized value exceeds {SPARSE_FIELD_VALUE_MAX_LENGTH} characters"}, status=400 + ) + cleaned_updates.append((sf_id, value)) + + for sf_id, value in cleaned_updates: + SparseField.objects.filter( + pk=sf_id, + resource=resource, + name__startswith=SPARSE_FIELD_PREFIX, + ).update(value=value) + + result = [_sparse_to_legacy(sf) for sf in _sparse_fields_for_resource(resource)] + return Response(result) + + # -- DELETE ------------------------------------------------------------- + + @staticmethod + def _extra_metadata_delete(request, resource): + data = request.data + if isinstance(data, str): + try: + data = json.loads(data) + except (json.JSONDecodeError, TypeError): + return Response( + {"detail": "Invalid JSON payload."}, + status=400, + ) + + if not isinstance(data, list): + return Response( + {"detail": "Expected a JSON list of IDs."}, + status=400, + ) + + ids = [int(i) for i in data if isinstance(i, (int, str)) and str(i).isdigit()] + SparseField.objects.filter( + pk__in=ids, + resource=resource, + name__startswith=SPARSE_FIELD_PREFIX, + ).delete() + + result = [_sparse_to_legacy(sf) for sf in _sparse_fields_for_resource(resource)] + return Response(result) diff --git a/geonode/base/api/serializers.py b/geonode/base/api/serializers.py index fa1add81264..6d8094518d0 100644 --- a/geonode/base/api/serializers.py +++ b/geonode/base/api/serializers.py @@ -27,7 +27,7 @@ from django.contrib.auth.models import Group from django.forms.models import model_to_dict from django.contrib.auth import get_user_model -from django.db.models.query import QuerySet + from geonode.assets.utils import get_default_asset, is_asset_deletable from geonode.metadata.multilang.serializers import MultiLangOutputMixin from geonode.people import Roles @@ -57,7 +57,6 @@ SpatialRepresentationType, ThesaurusKeyword, ThesaurusKeywordLabel, - ExtraMetadata, LinkedResource, ) from geonode.documents.models import Document @@ -68,6 +67,7 @@ from geonode.assets.handlers import asset_handler_registry from geonode.utils import build_absolute_uri from geonode.security.utils import get_resources_with_perms, get_geoapp_subtypes +from geonode.base.api.deprecated_extra_metadata import DeprecatedExtraMetadataField from geonode.resource.models import ExecutionRequest from django.contrib.gis.geos import Polygon from geonode.security.registry import permissions_registry @@ -282,23 +282,6 @@ def get_attribute(self, instance): return build_absolute_uri(instance.detail_url) -class ExtraMetadataSerializer(DynamicModelSerializer): - class Meta: - model = ExtraMetadata - name = "ExtraMetadata" - fields = ("pk", "metadata") - - def to_representation(self, obj): - if isinstance(obj, QuerySet): - out = [] - for el in obj: - out.append({**{"id": el.id}, **el.metadata}) - return out - elif isinstance(obj, list): - return obj - return {**{"id": obj.id}, **obj.metadata} - - class ThumbnailUrlField(DynamicComputedField): def __init__(self, **kwargs): super().__init__(**kwargs) @@ -672,7 +655,8 @@ class ResourceBaseSerializer(MultiLangOutputMixin, DynamicModelSerializer): links = DynamicRelationField(LinksSerializer, source="id", read_only=True) # Deferred fields - metadata = ComplexDynamicRelationField(ExtraMetadataSerializer, many=True, deferred=True) + # Deprecated: use the sparse fields API instead of ``metadata``. + metadata = DeprecatedExtraMetadataField(deferred=True, read_only=True) data = DataBlobField(DataBlobSerializer, source="id", deferred=True, required=False) executions = DynamicRelationField( ResourceExecutionRequestSerializer, source="id", deferred=True, required=False, read_only=True @@ -754,7 +738,7 @@ class Meta: "sourcetype", "is_copyable", "blob", - "metadata", + "metadata", # Deprecated: use sparse fields API instead "executions", "linked_resources", "download_url", diff --git a/geonode/base/api/tests.py b/geonode/base/api/tests.py index eafbab419b6..0d0be73cabd 100644 --- a/geonode/base/api/tests.py +++ b/geonode/base/api/tests.py @@ -71,7 +71,6 @@ ResourceBase, TopicCategory, ThesaurusKeyword, - ExtraMetadata, RestrictionCodeType, License, Group, @@ -3312,79 +3311,6 @@ def test_www_authenticate_header_is_removed_for_401_responses(self): user.delete() -class TestExtraMetadataBaseApi(GeoNodeBaseTestSupport): - def setUp(self): - self.layer = create_single_dataset("single_layer") - self.metadata = { - "filter_header": "Foo Filter header", - "field_name": "metadata-name", - "field_label": "this is the help text", - "field_value": "foo", - } - m = ExtraMetadata.objects.create(resource=self.layer, metadata=self.metadata) - self.layer.metadata.add(m) - self.mdata = ExtraMetadata.objects.first() - - def test_get_will_return_the_list_of_extra_metadata(self): - self.client.login(username="admin", password="admin") - url = reverse("base-resources-extra-metadata", args=[self.layer.id]) - response = self.client.get(url, content_type="application/json") - self.assertTrue(200, response.status_code) - expected = [{**{"id": self.mdata.id}, **self.metadata}] - self.assertEqual(expected, response.json()) - - def test_put_will_update_the_whole_metadata(self): - self.client.login(username="admin", password="admin") - url = reverse("base-resources-extra-metadata", args=[self.layer.id]) - input_metadata = { - "id": self.mdata.id, - "filter_header": "Foo Filter header", - "field_name": "metadata-updated", - "field_label": "this is the help text", - "field_value": "foo", - } - response = self.client.put(url, data=[input_metadata], content_type="application/json") - self.assertTrue(200, response.status_code) - self.assertEqual([input_metadata], response.json()) - - def test_post_will_add_new_metadata(self): - self.client.login(username="admin", password="admin") - url = reverse("base-resources-extra-metadata", args=[self.layer.id]) - input_metadata = { - "filter_header": "Foo Filter header", - "field_name": "metadata-updated", - "field_label": "this is the help text", - "field_value": "foo", - } - response = self.client.post(url, data=[input_metadata], content_type="application/json") - self.assertTrue(201, response.status_code) - self.assertEqual(2, len(response.json())) - - def test_delete_will_delete_single_metadata(self): - self.client.login(username="admin", password="admin") - url = reverse("base-resources-extra-metadata", args=[self.layer.id]) - response = self.client.delete(url, data=[self.mdata.id], content_type="application/json") - self.assertTrue(200, response.status_code) - self.assertEqual([], response.json()) - - def test_user_without_view_perms_cannot_see_the_endpoint(self): - from geonode.resource.registry import resource_manager_registry - - self.client.login(username="bobby", password="bob") - resource_manager_registry.get_for_instance(self.layer.get_self_resource()).remove_permissions( - self.layer.uuid, instance=self.layer.get_self_resource() - ) - url = reverse("base-resources-extra-metadata", args=[self.layer.id]) - response = self.client.get(url, content_type="application/json") - self.assertTrue(401, response.status_code) - - perm_spec = {"users": {"bobby": ["view_resourcebase"]}, "groups": {}} - self.layer.set_permissions(perm_spec) - url = reverse("base-resources-extra-metadata", args=[self.layer.id]) - response = self.client.get(url, content_type="application/json") - self.assertTrue(200, response.status_code) - - class TestApiLinkedResources(GeoNodeBaseTestSupport): @classmethod def setUpClass(cls) -> None: @@ -4477,3 +4403,150 @@ def test_map_layer_permission_caching(self): # Check that the permissions in the layers are the same for layer1, layer2 in zip(data1["map"]["maplayers"], data2["map"]["maplayers"]): self.assertEqual(layer1["dataset"]["perms"], layer2["dataset"]["perms"]) + + +class DeprecatedExtraMetadataApiTest(GeoNodeBaseTestSupport): + """Tests for the deprecated backward-compatible ExtraMetadata API adapters. + + These adapters re-expose the old ``/extra_metadata/`` endpoint and the + ``metadata`` serializer field using SparseField as the storage backend. + """ + + def setUp(self): + super().setUp() + self.admin = get_user_model().objects.get(username="admin") + self.dataset = create_single_dataset("deprecated_em_test") + + def _url(self, pk=None): + pk = pk or self.dataset.pk + return urljoin( + f"{reverse('base-resources-list')}/", + f"{pk}/extra_metadata/", + ) + + def test_get_empty_extra_metadata(self): + """GET should return an empty list when no extra metadata exists.""" + self.client.login(username="admin", password="admin") + response = self.client.get(self._url()) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), []) + + def test_post_extra_metadata(self): + """POST should create new extra metadata entries.""" + from geonode.metadata.models import SparseField + + self.client.login(username="admin", password="admin") + payload = [ + {"field_name": "test_field", "field_value": "test_value"}, + {"field_name": "another", "field_value": "value2"}, + ] + response = self.client.post( + self._url(), + data=json.dumps(payload), + content_type="application/json", + ) + self.assertEqual(response.status_code, 201) + result = response.json() + self.assertEqual(len(result), 2) + # Each entry should have an id and the metadata fields + for item in result: + self.assertIn("id", item) + self.assertIn("field_name", item) + self.assertIn("field_value", item) + + # Verify SparseField entries were created + sf_count = SparseField.objects.filter( + resource=self.dataset.resourcebase_ptr, + name__startswith="extra_", + ).count() + self.assertEqual(sf_count, 2) + + def test_get_returns_posted_metadata(self): + """GET after POST should return the created metadata.""" + self.client.login(username="admin", password="admin") + payload = [{"field_name": "myfield", "field_value": "myvalue"}] + self.client.post( + self._url(), + data=json.dumps(payload), + content_type="application/json", + ) + response = self.client.get(self._url()) + self.assertEqual(response.status_code, 200) + result = response.json() + self.assertEqual(len(result), 1) + self.assertEqual(result[0]["field_name"], "myfield") + self.assertEqual(result[0]["field_value"], "myvalue") + + def test_put_updates_existing_metadata(self): + """PUT should update an existing entry by id.""" + self.client.login(username="admin", password="admin") + # Create first + payload = [{"field_name": "original", "field_value": "v1"}] + response = self.client.post( + self._url(), + data=json.dumps(payload), + content_type="application/json", + ) + created_id = response.json()[0]["id"] + + # Update + update_payload = [{"id": created_id, "field_name": "updated", "field_value": "v2"}] + response = self.client.put( + self._url(), + data=json.dumps(update_payload), + content_type="application/json", + ) + self.assertEqual(response.status_code, 200) + result = response.json() + self.assertEqual(len(result), 1) + self.assertEqual(result[0]["field_name"], "updated") + self.assertEqual(result[0]["field_value"], "v2") + + def test_delete_removes_metadata(self): + """DELETE should remove entries by id.""" + self.client.login(username="admin", password="admin") + # Create + payload = [ + {"field_name": "to_delete", "field_value": "val"}, + {"field_name": "to_keep", "field_value": "keep"}, + ] + response = self.client.post( + self._url(), + data=json.dumps(payload), + content_type="application/json", + ) + items = response.json() + delete_id = items[0]["id"] + + # Delete one + response = self.client.delete( + self._url(), + data=json.dumps([delete_id]), + content_type="application/json", + ) + self.assertEqual(response.status_code, 200) + result = response.json() + self.assertEqual(len(result), 1) + self.assertNotEqual(result[0]["id"], delete_id) + + def test_metadata_field_in_serializer(self): + """The deprecated ``metadata`` field should appear when requested + via include[] and return data from SparseField entries.""" + from geonode.metadata.models import SparseField + + self.client.login(username="admin", password="admin") + SparseField.objects.create( + resource=self.dataset.resourcebase_ptr, + name="extra_1", + value=json.dumps({"field_name": "test", "field_value": "val"}), + ) + url = f"{reverse('base-resources-list')}/{self.dataset.pk}?include[]=metadata" + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + data = response.json().get("resource", response.json()) + self.assertIn("metadata", data) + metadata = data["metadata"] + self.assertIsInstance(metadata, list) + if metadata: + self.assertIn("id", metadata[0]) + self.assertIn("field_name", metadata[0]) diff --git a/geonode/base/api/views.py b/geonode/base/api/views.py index 59167c1dd23..cf6c6b839da 100644 --- a/geonode/base/api/views.py +++ b/geonode/base/api/views.py @@ -64,7 +64,6 @@ TopicCategory, ThesaurusKeyword, Configuration, - ExtraMetadata, LinkedResource, ) from geonode.base.api.filters import ( @@ -89,7 +88,6 @@ from geonode.resource.registry import resource_manager_registry from .permissions import ( - IsOwnerOrAdmin, IsManagerEditOrAdmin, ResourceBasePermissionsFilter, UserHasPerms, @@ -104,18 +102,17 @@ TopicCategorySerializer, RegionSerializer, ThesaurusKeywordSerializer, - ExtraMetadataSerializer, LinkedResourceSerializer, ) from geonode.people.api.serializers import UserSerializer from .pagination import GeoNodeApiPagination -from geonode.base.utils import validate_extra_metadata, patch_perms +from geonode.base.utils import patch_perms +from geonode.base.api.deprecated_extra_metadata import DeprecatedExtraMetadataMixin from geonode.assets.models import Asset from geonode.assets.utils import create_asset_and_link, unlink_asset from geonode.assets.handlers import asset_handler_registry from geonode.utils import get_supported_datasets_file_types - logger = logging.getLogger(__name__) @@ -291,7 +288,7 @@ def replace_presets(self, request): request.GET._mutable = False -class ResourceBaseViewSet(ApiPresetsInitializer, MultiLangViewMixin, DynamicModelViewSet): +class ResourceBaseViewSet(ApiPresetsInitializer, MultiLangViewMixin, DeprecatedExtraMetadataMixin, DynamicModelViewSet): """ API endpoint that allows base resources to be viewed or edited. """ @@ -1222,81 +1219,6 @@ def set_thumbnail(self, request, pk, *args, **kwargs): return Response({"thumbnail_url": resource.thumbnail_url}) return Response("Unable to set thumbnail", status=status.HTTP_400_BAD_REQUEST) - @action( - detail=True, - methods=["get", "put", "delete", "post"], - permission_classes=[IsOwnerOrAdmin, UserHasPerms(perms_dict={"default": {"POST": ["base.add_resourcebase"]}})], - url_path=r"extra_metadata", # noqa - url_name="extra-metadata", - ) - def extra_metadata(self, request, pk, *args, **kwargs): - """Get/Update/Delete/Add extra metadata for resource""" - _obj = get_object_or_404(ResourceBase, pk=pk) - - if request.method == "GET": - # get list of available metadata - queryset = _obj.metadata.all() - _filters = [{f"metadata__{key}": value} for key, value in request.query_params.items()] - if _filters: - queryset = queryset.filter(**_filters[0]) - return Response(ExtraMetadataSerializer().to_representation(queryset)) - if not request.method == "DELETE": - try: - extra_metadata = validate_extra_metadata(request.data, _obj) - except Exception as e: - return Response(status=500, data=e.args[0]) - - if request.method == "PUT": - """ - update specific metadata. The ID of the metadata is required to perform the update - [ - { - "id": 1, - "name": "foo_name", - "slug": "foo_sug", - "help_text": "object", - "field_type": "int", - "value": "object", - "category": "object" - } - ] - """ - for _m in extra_metadata: - _id = _m.pop("id") - ResourceBase.objects.filter(id=_obj.id).first().metadata.filter(id=_id).update(metadata=_m) - logger.info("metadata updated for the selected resource") - _obj.refresh_from_db() - return Response(ExtraMetadataSerializer().to_representation(_obj.metadata.all())) - elif request.method == "DELETE": - # delete single metadata - """ - Expect a payload with the IDs of the metadata that should be deleted. Payload be like: - [4, 3] - """ - ResourceBase.objects.filter(id=_obj.id).first().metadata.filter(id__in=request.data).delete() - _obj.refresh_from_db() - return Response(ExtraMetadataSerializer().to_representation(_obj.metadata.all())) - elif request.method == "POST": - # add new metadata - """ - [ - { - "name": "foo_name", - "slug": "foo_sug", - "help_text": "object", - "field_type": "int", - "value": "object", - "category": "object" - } - ] - """ - for _m in extra_metadata: - new_m = ExtraMetadata.objects.create(resource=_obj, metadata=_m) - new_m.save() - _obj.metadata.add(new_m) - _obj.refresh_from_db() - return Response(ExtraMetadataSerializer().to_representation(_obj.metadata.all()), status=201) - @action( detail=True, methods=["get"], diff --git a/geonode/base/migrations/0100_migrate_extrametadata_to_sparsefields.py b/geonode/base/migrations/0100_migrate_extrametadata_to_sparsefields.py new file mode 100644 index 00000000000..7d4667d6b77 --- /dev/null +++ b/geonode/base/migrations/0100_migrate_extrametadata_to_sparsefields.py @@ -0,0 +1,52 @@ +import json +import logging + +from django.db import migrations + +logger = logging.getLogger(__name__) + + +def migrate_extrametadata_to_sparsefields(apps, schema_editor): + """ + Migrate ExtraMetadata to SparseField. + Each ExtraMetadata object (with its JSON dict) is stored as a single + SparseField entry with name 'extra_' and value as JSON string. + Entries whose serialized JSON exceeds 1024 characters are skipped with a warning. + """ + ExtraMetadata = apps.get_model("base", "ExtraMetadata") + SparseField = apps.get_model("metadata", "SparseField") + + for extra_meta in ExtraMetadata.objects.select_related("resource").iterator(): + name = f"extra_{extra_meta.pk}" + value = json.dumps(extra_meta.metadata) + if len(value) > 1024: + logger.warning( + f"ExtraMetadata pk={extra_meta.pk} for resource {extra_meta.resource.id}:{extra_meta.resource.title} " + f"skipped during migration to SparseField: " + f"serialized value exceeds 1024 characters" + ) + continue + SparseField.objects.get_or_create( + resource=extra_meta.resource, + name=name, + defaults={"value": value}, + ) + + +class Migration(migrations.Migration): + + dependencies = [ + ("base", "0099_resourcebase_auth_config"), + ("metadata", "0001_initial"), + ] + + operations = [ + migrations.RunPython(migrate_extrametadata_to_sparsefields, migrations.RunPython.noop), + migrations.RemoveField( + model_name="resourcebase", + name="metadata", + ), + migrations.DeleteModel( + name="ExtraMetadata", + ), + ] diff --git a/geonode/base/models.py b/geonode/base/models.py index a04830adc2b..90850fe7f16 100644 --- a/geonode/base/models.py +++ b/geonode/base/models.py @@ -679,9 +679,6 @@ class ResourceBase(PolymorphicModel, PermissionLevelMixin, ItemBase): data_quality_statement_help_text = _( "general explanation of the data producer's knowledge about the lineage of a" " dataset" ) - extra_metadata_help_text = _( - 'Additional metadata, must be in format [ {"metadata_key": "metadata_value"}, {"metadata_key": "metadata_value"} ]' - ) # internal fields uuid = models.CharField(max_length=36, unique=True, default=uuid.uuid4) title = models.CharField(_("title"), max_length=255, help_text=_("name by which the cited resource is known")) @@ -894,10 +891,6 @@ class ResourceBase(PolymorphicModel, PermissionLevelMixin, ItemBase): subtype = models.CharField(max_length=128, null=True, blank=True) - metadata = models.ManyToManyField( - "ExtraMetadata", verbose_name=_("Extra Metadata"), null=True, blank=True, help_text=extra_metadata_help_text - ) - objects = ResourceBaseManager() class Meta: @@ -2174,8 +2167,3 @@ class GroupGeoLimit(models.Model): group = models.ForeignKey(GroupProfile, null=False, blank=False, on_delete=models.CASCADE) resource = models.ForeignKey(ResourceBase, null=False, blank=False, on_delete=models.CASCADE) wkt = models.TextField(db_column="wkt", blank=True) - - -class ExtraMetadata(models.Model): - resource = models.ForeignKey(ResourceBase, null=False, blank=False, on_delete=models.CASCADE) - metadata = JSONField(null=True, default=dict, blank=True) diff --git a/geonode/base/utils.py b/geonode/base/utils.py index c73623ce146..91df3069bfb 100644 --- a/geonode/base/utils.py +++ b/geonode/base/utils.py @@ -22,16 +22,12 @@ # Standard Modules import re -import json import logging -from schema import Schema from dateutil.parser import isoparse from datetime import datetime, timedelta # Django functionality -from django.conf import settings from django.contrib.auth import get_user_model -from django.core.exceptions import ValidationError # Geonode functionality from geonode.layers.models import Dataset @@ -177,33 +173,6 @@ def get_resource(resource_base): return resource_base.get_real_instance() -def validate_extra_metadata(data, instance): - if not data: - return data - - # starting validation of extra metadata passed via JSON - # if schema for metadata validation is not defined, an error is raised - resource_type = instance.polymorphic_ctype.model if instance.polymorphic_ctype else instance.class_name.lower() - extra_metadata_validation_schema = settings.EXTRA_METADATA_SCHEMA.get(resource_type, None) - if not extra_metadata_validation_schema: - raise ValidationError(f"EXTRA_METADATA_SCHEMA validation schema is not available for resource {resource_type}") - # starting json structure validation. The Field can contain multiple metadata - try: - if isinstance(data, str): - data = json.loads(data) - except Exception: - raise ValidationError("The value provided for the Extra metadata field is not a valid JSON") - - # looping on all the single metadata provided. If it doen't match the schema an error is raised - for _index, _metadata in enumerate(data): - try: - Schema(extra_metadata_validation_schema).validate(_metadata) - except Exception as e: - raise ValidationError(f"{e} at index {_index} for input json: {json.dumps(_metadata)}") - # conerted because in this case, we can store a well formated json instead of the user input - return data - - def remove_country_from_languagecode(language: str): """Remove country code (us) from language name (en-us) >>> remove_country_from_lanugecode("en-us") diff --git a/geonode/geoapps/api/tests.py b/geonode/geoapps/api/tests.py index 7fb78d4160e..a6198e47283 100644 --- a/geonode/geoapps/api/tests.py +++ b/geonode/geoapps/api/tests.py @@ -19,8 +19,6 @@ import json import logging from unittest.mock import MagicMock -from urllib.parse import urljoin - from django.contrib.auth import get_user_model from django.urls import reverse from django.test import override_settings @@ -114,17 +112,6 @@ def test_geoapp_listing_advertised(self): GeoApp.objects.update(advertised=True) - def test_extra_metadata_included_with_param(self): - _app = GeoApp.objects.first() - url = urljoin(f"{reverse('geoapps-list')}/", f"{_app.pk}") - data = {"include[]": "metadata"} - - response = self.client.get(url, format="json", data=data) - self.assertIsNotNone(response.data["geoapp"].get("metadata")) - - response = self.client.get(url, format="json") - self.assertNotIn("metadata", response.data["geoapp"]) - def test_geoapps_crud(self): """ Ensure we can create/update GeoApps. diff --git a/geonode/layers/api/serializers.py b/geonode/layers/api/serializers.py index 8eae5a79fd6..93841e27a55 100644 --- a/geonode/layers/api/serializers.py +++ b/geonode/layers/api/serializers.py @@ -171,7 +171,6 @@ class Meta: + ( "uuid", "name", - "metadata", "attribute_set", "charset", "is_mosaic", diff --git a/geonode/layers/api/tests.py b/geonode/layers/api/tests.py index a68e61c68ec..27c6992c3bf 100644 --- a/geonode/layers/api/tests.py +++ b/geonode/layers/api/tests.py @@ -209,17 +209,6 @@ def test_dataset_listing_advertised(self): Dataset.objects.update(advertised=True) - def test_extra_metadata_included_with_param(self): - _dataset = Dataset.objects.first() - url = urljoin(f"{reverse('datasets-list')}/", f"{_dataset.pk}") - data = {"include[]": "metadata"} - - response = self.client.get(url, format="json", data=data) - self.assertIsNotNone(response.data["dataset"].get("metadata")) - - response = self.client.get(url, format="json") - self.assertNotIn("metadata", response.data["dataset"]) - def test_get_dataset_related_maps_and_maplayers(self): dataset = Dataset.objects.first() assign_perm("base.view_resourcebase", get_anonymous_user(), dataset.get_self_resource()) diff --git a/geonode/maps/api/tests.py b/geonode/maps/api/tests.py index 24c154d5bf1..24781461654 100644 --- a/geonode/maps/api/tests.py +++ b/geonode/maps/api/tests.py @@ -129,17 +129,6 @@ def test_maps(self): self.assertEqual(response.data["map"]["maplayers"][0]["order"], 0) self.assertEqual(response.data["map"]["maplayers"][0]["opacity"], 1.0) - def test_extra_metadata_included_with_param(self): - resource = Map.objects.first() - url = urljoin(f"{reverse('maps-list')}/", f"{resource.pk}") - data = {"include[]": "metadata"} - - response = self.client.get(url, format="json", data=data) - self.assertIsNotNone(response.data["map"].get("metadata")) - - response = self.client.get(url, format="json") - self.assertNotIn("map", response.data["map"]) - def test_patch_map(self): """ Patch to maps// diff --git a/geonode/resource/manager.py b/geonode/resource/manager.py index 882686e7c1a..120d77e94d5 100644 --- a/geonode/resource/manager.py +++ b/geonode/resource/manager.py @@ -403,7 +403,6 @@ def update( keywords: list = [], custom: dict = {}, notify: bool = True, - extra_metadata: list = [], *args, **kwargs, ) -> ResourceBase: @@ -445,7 +444,6 @@ def update( regions=regions, keywords=keywords, vals=vals, - extra_metadata=extra_metadata, ) ji = custom.get("jsoninstance", None) diff --git a/geonode/resource/utils.py b/geonode/resource/utils.py index 2e8edb6173a..546a49d9455 100644 --- a/geonode/resource/utils.py +++ b/geonode/resource/utils.py @@ -37,7 +37,6 @@ from ..base import enumerations from ..base.models import ( - ExtraMetadata, Link, License, ResourceBase, @@ -142,7 +141,6 @@ def update_resource( regions: list = [], keywords: list = [], vals: dict = {}, - extra_metadata: list = [], ): if xml_file: instance.metadata_xml = open(xml_file).read() @@ -277,12 +275,6 @@ def update_resource( # Refresh from DB instance.refresh_from_db() - if extra_metadata: - instance.metadata.all().delete() - for _m in extra_metadata: - new_m = ExtraMetadata.objects.create(resource=instance, metadata=_m) - instance.metadata.add(new_m) - return instance diff --git a/geonode/settings.py b/geonode/settings.py index 948abfa0db0..ead2bc9cf6c 100644 --- a/geonode/settings.py +++ b/geonode/settings.py @@ -25,7 +25,6 @@ import logging import subprocess import dj_database_url -from schema import Optional from urllib.parse import urlparse, urljoin # @@ -2109,38 +2108,24 @@ def get_geonode_catalogue_service(): DEFAULT_AUTO_FIELD = "django.db.models.AutoField" -""" -Default schema used to store extra and dynamic metadata for the resource -""" +for deprecated_env_key in ( + "CUSTOM_METADATA_SCHEMA", + "MAP_EXTRA_METADATA_SCHEMA", + "DATASET_EXTRA_METADATA_SCHEMA", + "DOCUMENT_EXTRA_METADATA_SCHEMA", + "GEOAPP_EXTRA_METADATA_SCHEMA", +): + if os.getenv(deprecated_env_key) is not None: + logger.warning(f"Found deprecated environment variable '{deprecated_env_key}'.") -DEFAULT_EXTRA_METADATA_SCHEMA = { - Optional("id"): int, - "filter_header": object, - "field_name": object, - "field_label": object, - "field_value": object, -} +# DEPRECATED -- Declaration kept for backward compatibility +DEFAULT_EXTRA_METADATA_SCHEMA = {} -""" -If present, will extend the available metadata schema used for store -new value for each resource. By default overrided the existing one. -The expected schema is the same as the default -""" -CUSTOM_METADATA_SCHEMA = os.getenv("CUSTOM_METADATA_SCHEMA ", {}) +# DEPRECATED -- Declaration kept for backward compatibility +CUSTOM_METADATA_SCHEMA = {} -""" -Variable used to actually get the expected metadata schema for each resource_type. -In this way, each resource type can have a different metadata schema -""" -EXTRA_METADATA_SCHEMA = { - **{ - "map": os.getenv("MAP_EXTRA_METADATA_SCHEMA", DEFAULT_EXTRA_METADATA_SCHEMA), - "dataset": os.getenv("DATASET_EXTRA_METADATA_SCHEMA", DEFAULT_EXTRA_METADATA_SCHEMA), - "document": os.getenv("DOCUMENT_EXTRA_METADATA_SCHEMA", DEFAULT_EXTRA_METADATA_SCHEMA), - "geoapp": os.getenv("GEOAPP_EXTRA_METADATA_SCHEMA", DEFAULT_EXTRA_METADATA_SCHEMA), - }, - **CUSTOM_METADATA_SCHEMA, -} +# DEPRECATED -- Declaration kept for backward compatibility +EXTRA_METADATA_SCHEMA = {} """ List of modules that implement custom metadata storers that will be called when the metadata of a resource is saved From 2592448c17df10645a73f14cf8dbe3a69cf8d47d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 18 Jun 2026 11:20:56 +0200 Subject: [PATCH 28/98] Fix to exception handling for XLSX (#14138) (#14352) (cherry picked from commit 0e09ec4d8a94e3e3a8a97f8f2f14081907a45983) Co-authored-by: Giovanni Allegri --- geonode/upload/handlers/xlsx/handler.py | 8 ++++++-- geonode/upload/handlers/xlsx/tests.py | 3 +-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/geonode/upload/handlers/xlsx/handler.py b/geonode/upload/handlers/xlsx/handler.py index 384a37b324e..c6e3abf59cf 100644 --- a/geonode/upload/handlers/xlsx/handler.py +++ b/geonode/upload/handlers/xlsx/handler.py @@ -236,9 +236,13 @@ def pre_processing(self, files, execution_id, **kwargs): # Note: rows_gen continues from the row after the headers self._convert_to_csv(headers, rows_gen, output_file) + except InvalidInputFileException as e: + logger.exception(f"XLSX Pre-processing failed: {e}") + raise + except Exception as e: - logger.exception("XLSX Pre-processing failed") - raise InvalidInputFileException(detail=f"Failed to securely parse Excel: {str(e)}") + logger.exception(f"XLSX Pre-processing failed: {e}") + raise InvalidInputFileException(detail="Failed to parse Excel.") # update the file path in the payload _data["files"]["base_file"] = output_file diff --git a/geonode/upload/handlers/xlsx/tests.py b/geonode/upload/handlers/xlsx/tests.py index fb432f799b0..6eafb1334d4 100644 --- a/geonode/upload/handlers/xlsx/tests.py +++ b/geonode/upload/handlers/xlsx/tests.py @@ -80,7 +80,6 @@ def test_pre_processing_fails_on_missing_lat(self, mock_get_exec): ): with self.assertRaises(InvalidInputFileException) as context: self.handler.pre_processing(files, exec_id) - self.assertIn("geometry headers", str(context.exception)) @patch("geonode.upload.orchestrator.orchestrator.get_execution_object") @@ -158,7 +157,7 @@ def test_security_billion_laughs_protection(self): with self.assertRaises(InvalidInputFileException) as context: self.handler.pre_processing(files, exec_id) - self.assertIn("Failed to securely parse Excel", str(context.exception)) + self.assertIn("Failed to parse Excel", str(context.exception)) finally: if os.path.exists(malicious_path): os.remove(malicious_path) From de298eec3f0fd70920681511426b4cb2ced9f408 Mon Sep 17 00:00:00 2001 From: mattiagiupponi <51856725+mattiagiupponi@users.noreply.github.com> Date: Fri, 19 Jun 2026 11:40:51 +0200 Subject: [PATCH 29/98] [Backport Fixes #14354] Cannot upload multiple layers from GPKG file (#14357) --- geonode/upload/handlers/base.py | 21 ++++---- .../upload/handlers/common/tests_vector.py | 2 +- geonode/upload/handlers/common/vector.py | 45 +++++++++++++----- geonode/upload/handlers/gpkg/handler.py | 6 +-- geonode/upload/handlers/gpkg/tests.py | 8 ++-- geonode/upload/tests/end2end/test_end2end.py | 26 +++++++++- .../upload/tests/fixture/multiple_layers.gpkg | Bin 0 -> 118784 bytes 7 files changed, 78 insertions(+), 30 deletions(-) create mode 100755 geonode/upload/tests/fixture/multiple_layers.gpkg diff --git a/geonode/upload/handlers/base.py b/geonode/upload/handlers/base.py index 49d9ced2c53..cceba710162 100644 --- a/geonode/upload/handlers/base.py +++ b/geonode/upload/handlers/base.py @@ -478,12 +478,17 @@ def identify_authority(self, layer): if _code is None: raise Exception("CRS authority code not found, fallback to default behaviour") except Exception: - spatial_ref = layer.GetSpatialRef() - spatial_ref.AutoIdentifyEPSG() - _name = spatial_ref.GetAuthorityName(None) or spatial_ref.GetAttrValue("AUTHORITY", 0) - _code = ( - spatial_ref.GetAuthorityCode("PROJCS") - or spatial_ref.GetAuthorityCode("GEOGCS") - or spatial_ref.GetAttrValue("AUTHORITY", 1) - ) + try: + spatial_ref = layer.GetSpatialRef() + spatial_ref.AutoIdentifyEPSG() + _name = spatial_ref.GetAuthorityName(None) or spatial_ref.GetAttrValue("AUTHORITY", 0) + _code = ( + spatial_ref.GetAuthorityCode("PROJCS") + or spatial_ref.GetAuthorityCode("GEOGCS") + or spatial_ref.GetAttrValue("AUTHORITY", 1) + ) + except Exception: + logger.error( + f"The following layer {layer.GetName()} does not have a Coordinate Reference System (CRS) and will be skipped." + ) return f"{_name}:{_code}" diff --git a/geonode/upload/handlers/common/tests_vector.py b/geonode/upload/handlers/common/tests_vector.py index a0b8b214dce..939d0773408 100644 --- a/geonode/upload/handlers/common/tests_vector.py +++ b/geonode/upload/handlers/common/tests_vector.py @@ -518,7 +518,7 @@ def test_select_valid_layers(self): The function should return only the datasets with a geometry The other one are discarded """ - all_layers = GPKGFileHandler().get_ogr2ogr_driver().Open(self.no_crs_gpkg) + all_layers = GPKGFileHandler().open_source_file({"base_file": self.no_crs_gpkg}) with self.assertLogs(level="ERROR") as _log: valid_layer = GPKGFileHandler()._select_valid_layers(all_layers) diff --git a/geonode/upload/handlers/common/vector.py b/geonode/upload/handlers/common/vector.py index b0b2cb9df15..929f2812f19 100644 --- a/geonode/upload/handlers/common/vector.py +++ b/geonode/upload/handlers/common/vector.py @@ -443,7 +443,7 @@ def extract_resource_to_publish(self, files, action, layer_name, alternate, **kw } ] - layers = self.open_source_file(files) + layers = self._select_valid_layers(self.open_source_file(files), filter_layer=layer_name) if not layers: return [] return [ @@ -589,18 +589,39 @@ def open_source_file(self, files): return [gdal_proxy] def _select_valid_layers(self, all_layers, **kwargs): + """ + Select valid layers from GDAL datasource objects. + If more than one layer is found, it loop over all the possibility + to extract all the layers. + Is possible to pass a filter_layer argument with the name of the layer + to retrieve only the needed one + """ + filter_layer = kwargs.get("filter_layer", None) layers = [] - for layer in all_layers: - try: - layer = self._extract_layer(layer) - self.identify_authority(layer) - layers.append(layer) - except Exception as e: - logger.error(e) - logger.error( - f"The following layer {layer.GetName()} does not have a Coordinate Reference System (CRS) and will be skipped." - ) - pass + + for ds in all_layers: + if not ds: + continue + if ds.GetLayerCount() > 0: + candidates_layers = [ds.GetLayerByIndex(i) for i in range(ds.GetLayerCount())] + else: + candidates_layers = [ds] + for layer in candidates_layers: + try: + lry = self._extract_layer(layer) + if not lry: + continue + lry._parent_ds = ds + self.identify_authority(lry) + if filter_layer and self.fixup_name(lry.GetName()) == filter_layer: + return [lry] + layers.append(lry) + except Exception as e: + logger.error(f"Layer skipped due to error: {e}") + + if filter_layer and not layers: + logger.warning(f"No layer matching filter '{filter_layer}' was found.") + return layers def can_overwrite(self, _exec_obj, dataset): diff --git a/geonode/upload/handlers/gpkg/handler.py b/geonode/upload/handlers/gpkg/handler.py index c46af42e077..5bd494ce5bf 100644 --- a/geonode/upload/handlers/gpkg/handler.py +++ b/geonode/upload/handlers/gpkg/handler.py @@ -107,12 +107,12 @@ def is_valid(files, user, **kwargs): actual_upload = upload_validator._get_parallel_uploads_count() max_upload = upload_validator._get_max_parallel_uploads() - layers = GPKGFileHandler().get_ogr2ogr_driver().Open(files.get("base_file")) + layers = GPKGFileHandler().open_source_file(files) if not layers: raise InvalidGeopackageException("The geopackage provided is invalid") - layers_count = len(layers) + layers_count = layers[0].GetLayerCount() if layers_count >= max_upload: raise UploadParallelismLimitException( @@ -152,7 +152,7 @@ def handle_xml_file(self, saved_dataset, _exec): pass def _select_valid_layers(self, all_layers, **kwargs): - layers = super()._select_valid_layers(all_layers=all_layers) + layers = super()._select_valid_layers(all_layers=all_layers, **kwargs) if execution_id := kwargs.get("execution_id", None): exec_obj = orchestrator.get_execution_object(execution_id) if exec_obj.action in (ira.REPLACE.value, ira.UPSERT.value): diff --git a/geonode/upload/handlers/gpkg/tests.py b/geonode/upload/handlers/gpkg/tests.py index 7e6fd50298b..90ffc85f12c 100644 --- a/geonode/upload/handlers/gpkg/tests.py +++ b/geonode/upload/handlers/gpkg/tests.py @@ -41,7 +41,7 @@ def setUpClass(cls): super().setUpClass() cls.handler = GPKGFileHandler() cls.valid_gpkg = f"{project_dir}/tests/fixture/valid.gpkg" - cls.multiple_layer = f"{project_dir}/tests/fixture/multiple_layer.gpkg" + cls.multiple_layer = f"{project_dir}/tests/fixture/multiple_layers.gpkg" cls.invalid_gpkg = f"{project_dir}/tests/fixture/invalid.gpkg" cls.user, _ = get_user_model().objects.get_or_create(username="admin") cls.invalid_files = {"base_file": cls.invalid_gpkg} @@ -181,14 +181,14 @@ def test_select_valid_layers(self): step="step", action="replace", input_params={ - "files": {"base_file": "/tmp/multiple_layer.gpkg"}, + "files": {"base_file": "/tmp/multiple_layers.gpkg"}, "skip_existing_layer": True, "store_spatial_file": False, "handler_module_path": str(self.handler), }, ) - all_layers = GPKGFileHandler().get_ogr2ogr_driver().Open("/tmp/multiple_layer.gpkg") + all_layers = GPKGFileHandler().open_source_file({"base_file": ("/tmp/multiple_layers.gpkg")}) with self.assertRaises(Exception) as exp: GPKGFileHandler()._select_valid_layers(all_layers, execution_id=str(exec_id)) @@ -198,4 +198,4 @@ def test_select_valid_layers(self): exp.exception.args[0], ) - os.remove("/tmp/multiple_layer.gpkg") + os.remove("/tmp/multiple_layers.gpkg") diff --git a/geonode/upload/tests/end2end/test_end2end.py b/geonode/upload/tests/end2end/test_end2end.py index 6bd4a017b92..180b39ea7c4 100644 --- a/geonode/upload/tests/end2end/test_end2end.py +++ b/geonode/upload/tests/end2end/test_end2end.py @@ -55,6 +55,7 @@ def setUpClass(cls) -> None: cls.valid_gkpg = f"{project_dir}/tests/fixture/valid.gpkg" cls.valid_geojson = f"{project_dir}/tests/fixture/valid.geojson" cls.no_crs_gpkg = f"{project_dir}/tests/fixture/noCrsTable.gpkg" + cls.valid_multiple_layers_gkpg = f"{project_dir}/tests/fixture/multiple_layers.gpkg" file_path = gisdata.PROJECT_ROOT filename = os.path.join(file_path, "both/good/sangis.org/Airport/Air_Runways.shp") cls.valid_shp = { @@ -130,9 +131,9 @@ def _assertimport( while ( ExecutionRequest.objects.get(exec_id=response.json().get("execution_id")) != ExecutionRequest.STATUS_FINISHED - and tentative <= 10 + and tentative <= 15 ): - time.sleep(10) + time.sleep(2) tentative += 1 exc_obj = ExecutionRequest.objects.get(exec_id=response.json().get("execution_id")) if exc_obj.status != ExecutionRequest.STATUS_FINISHED: @@ -226,6 +227,27 @@ def test_file_upload_permissions_none_uploads_successfully(self): self._cleanup_layers(name="stazioni_metropolitana") +class ImporterMultupleGeoPackageImportTest(BaseImporterEndToEndTest): + @mock.patch.dict(os.environ, {"GEONODE_GEODATABASE": "test_geonode_data"}) + @override_settings(GEODATABASE_URL=f"{geourl.split('/geonode_data')[0]}/test_geonode_data") + def test_import_multiple_layers_from_geopackage(self): + self._cleanup_layers(name="area") + self._cleanup_layers(name="example") + try: + resource = None + payload = {"base_file": open(self.valid_multiple_layers_gkpg, "rb"), "action": "upload"} + self._assertimport(payload, "area") + resource = ResourceBase.objects.filter( + Q(alternate__icontains="geonode:example") | Q(alternate__icontains="example") + ) + self.assertTrue(resource.exists()) + finally: + if resource.first(): + resource.first().delete() + self._cleanup_layers(name="area") + self._cleanup_layers(name="example") + + class ImporterNoCRSImportTest(BaseImporterEndToEndTest): @override_settings(ASYNC_SIGNALS=False) @mock.patch.dict(os.environ, {"GEONODE_GEODATABASE": "test_geonode_data"}) diff --git a/geonode/upload/tests/fixture/multiple_layers.gpkg b/geonode/upload/tests/fixture/multiple_layers.gpkg new file mode 100755 index 0000000000000000000000000000000000000000..883c20b2604e181f05b20de9f92b05f7f269745b GIT binary patch literal 118784 zcmeI5U2Gdie!zFBkCm(s%1Ia|$M`(vol5e&`k-W4mTlFcr4`LYB4u(JTXA!H47s8f zCYKC%6-h>OEhRgL>r;VyyO*Lx`fx9OO4Ga)$m5}TD0;vhiU2KoDd3m%p=p83p$U-A z><78KT++0pi{;=?n51^*KePY&&3``rv*t|Z=BlKMXtPo)=TtOHjZrjBy^Ih=QP1G_ z8T>Y1=kXuC<^}$yDJt}@% zgQ2_3j|b28?J$eImpq@*?^zSV-%SGh%VSI+J27zov|5vjMX^?_-Yp8MR1yVTX*Frr z{MwNU>XsnMida*fGD~SL#&d{IC*pA~jY7_Nw>(1VI=7Nab10F_aA_W;lC9}N2umO? zwsZrfQ@3z}cp`~1+$y)kqjW5h;li=&sWi``kggZ1$Vkn-SrSpGMTbxby)9KrIaO3p zz9K7XEhow9Wn`36D;~<{WVxcEH$_wu6$PnVIT=Mm5p?SYhcyi+xm%Mq`Ku@zxq`Ul z^8PDcCU6sDJl786y-KAll*G5ilCWJZU^SeO9tp7}hX%sz`ZD2P*+fm5BLdZRJPc5a z3T`;3?KmycSJAWq^XjM$v#$%7hqTD%eGQv#z#H1c#-gy6i6H07BI3E*JW8hUJG;8dS~N{0d5*OFMx3pcw2R#mrQ()~ zI{-&wt@qIzq^YN(_~?zu-UrIHyQcPU98RqqO_T1M;X z#9A!9fnMb{tPEI76N|8xWSclyxhz;{k=`v4PdammpftCFyOZP+m(g+K?no77Rk5Is zM6QkW`!f?XCCLSGS9z<1M^YiTtyZ*a!C6g0)EP2*@ZD34e{PO`pzGv}7ZkCjA17f$ z9nFDASaH}*!U~O5YKkBgT5Go~$--_+?U2yD*3jIpn=B4(m9^JJQ7tRSRvC_Z*7C;1 zBcoHZhV}Bc)~-^`sZy>a)Wl6e*;7#15RAl!`u%t2x~^eNzO|M~eGK2H7{)(8Pygg> zv%wd|N?BBEdqTca+AhnAR{hSvf!4ZqinOn6R?pe3XJ}-i?o~xMfp)fW5AliQ25E(j zW?OCzveYzgtmI~-l#12*tZK7(yeAAaMQm+$8>h}&tt6XF+|1&EzS)Xx^0O1>vx)l zSt-_VZRs7Jrhqk|ztteE(UyAY(D7`FId!`x;=63i&Cz9g>+knxr#j-Y z-q&b%)*$BKB@g2dh3E%6&9d3bWnT2OmdT8=8d`?};n`wNAWg|G!%77_YDk-sSkohP z>tzd~lCMcsRl>7jZNxT<9tyFgoT3VOJgqI_0TRy{`NUdth(xAf7|)J3RjDk7L*pA0 zC*0zZD^^7z{2@y*vmY)G!`gd!2v+LN=$+Jm!YOUI3)EFHFETxe|!HPRf&$Z?q7 zwb8jQ^!EGLLLF<^tOcVXn3sJEJ>4hO4c|;kJlFx^TkZGI+Sq##X&Q%P9MEjv5$0(% z6^DdAdbmu++VrT_K9Q}|iaA-jhleB6G8({ct8Pholu2ijsU){;#x?%13TghLB;#jV2rqJqHBrus2>Xw! zSibYM`C_h6KKkt4=3SO@HT*12!cW+Yn|$5KyzvKW3U#>2!E=ATIe4D(dG1T~Gq>Uy zbm;<7uOO;Yo?Fkv#~#MHRD3COC#XjTS-kX-UDGaw=)$E7K{m6FpYWv;%T1Asv-6jt zv-9lC!llWXx%ruy*}03eY%I&)NTn0}#+@L^Imia*r)Q^wZ@l3Wb8&X&B8eeS5;%Of zcuf@L9Vx#RWT#!kFD*<*i8x-x;qL?sViAjD$;s*H{DryMnT46T=)%H6wcY)3X!NSw1>@c`kZ+ z_TuEFix*#iITBp^)#tzX>g88|^UL=aZjU*y#~#M$A>SF_$YKwEd2%%|XXZ@)cFlJ7 z!s}PA{eE!%N9pt7mp=J$?BOX~Dv$TSDDUs_2fTm)5C8%|00;m9AOHk_01yBIKmZ5; zfv29pP)~q1)*L)ck8hBy5`h2z)K@DM00;m9AOHk_01yBIKmZ5;0U!VbfPjsFoByx> z|NqY@?-$1b_e#00KY&2mk>f00e*l5C8%|00=yd1WtK+0)xi$e};zV|4(Bb zg3dqy2mk>f00e*l5C8%|00;m9AOHj&Nud4n|Bor}L+{6r%m4&{01yBIKmZ5;0U!Vb zfB+Bx0zd!=0D&h>;6)D;@cBIh1I9Cca@6N~3Ln%u_V5Lo@%aKciu~kX4`1Dgr;_IW zf1gs`|Mhf00e*l5C8%|00;m9AOHjoOJJ-w;4`=VBd)(^xYwue z{YS1~|G&d>g>*mw2mk>f00e*l5C8%|00;m9AOHkTCV_VU|DPH=nfiiOKmZ5;0U!Vb zfB+Bx0zd!=00AHX1c1Qt5FpS0!T&!VmXHe&00KY&2mk>f00e*l5C8%|00;nqlS+V` z{|Em6Nv$2w3kU!KAOHk_01yBIKmZ5;0U!VbfWYw(fam|m!x3@;0zd!=00AHX1b_e# z00KY&2mk>faFPj(jQ$UGnp&eq|8Mx;M*m^-PlxH@pAT*geK2&F`SIY{z7P9$dM|lC zqu;|KkACg*raYOA+D3xr@Cxmbc#Ko3wpmgdM&LEyh zq71jnE%7KFOJulk?0PEAvnZtNi7GNub8nVJ6l&2a6hd!Hl@czEg7OtvQENF#RxjI? z$me9aqM|oNR1y^hsarW2MMDvE>jsB44=1@>lQyNVqG;p_;*!hz%VRp$(_LYeWJRp0 zPHc~dRnMb=HIdA4Y5s^9^*o;-##g*d;3l@tbL}wRt5nKDNqk!@9o9Bma%dnlEVHtS znleWOdU(6EsNiOm({`)0NMA+MhD}}_)nWE^0rQX+*}SiCvh1;`7&;`rYa{*s%mhtI zazWfx-YQ9|DCD-)igqnH+pZ9GhKwF84Kx0U3Hsdytxs~sJFiE~<{!Jh${}Yg3Tv4N za;_{Qp1aMXWD38ttE;S~sQ4rqj`YZlI9qSkE_O?lid!mbiPlC1y?1txYnQMp?c(;i zD=5-E5iO^(q(BXo^vFH;$fZ;=!{e?!$)oCBK~>9WJ)KyKr8m&4+=i6_YiXkq){<-! zCo7i)3oSD0B;rYD4iS{*R&ckTT;ei1ZagfgqO2+w)RD-+cTX|?xjFiQo~5&11VwD= z$4S^w$J!(k*3#H()LLd%sVRa~Xsw*GBn!JOa7pN1YiMrQO%{i?%GyyD)v|JIEFIOP z<&BF+2KiE&&$9Hp95rCdpN({`|r$k-A*z2*0xIOWB5MBF#h>@ z`X^_b?Un4mEUL9VAzvwNmt{q(erMo7Yh61<+E+HK=j_(78=0tkRnbkLoo(Dhd?L9) z8d0Owm0N=>HI3UExfv;?VzoZ2+AN+n5eAwfwkEfYQ|GN#l1(OVX7PNf*$8d&xQP;Z z*HU?afHk1M)gZ0WmU`*X@ob7Yb-O0wyKKwN!C{$OiDg&yktY)C@Aqe? zI^we4*JyXvAm-pD591Go=m$H^vf0XIUi7q<$&9iZT89E*PbMdjrev34r2_8srA{tTjhCWD16{ld-8vWicEY z-Si@#57!AR^>|5yRKB;c_X3}2l#tslycE5+##@>TS(>NUCfM)xS zFi)$gI3)DZ!(}ejrbo5*iEO1-%*oO{JRF&p(dcEZQMln$Y6U!0xenh2aZ{2dEenS^ zI_)kpGWai)mlmjjcbOmb{d@0z&qewZTJZke==-DFBmX_}*6{C!<)IlYBs|*vcaf;S z6J!Ecd;K}-xS@0y=^C1| zVe8y9mgVt5jyTmCXB~HuQHb@FcA6uLg2d{ATAfGsH~jjl!IgF%nsu^#Sclgo52wqE z7h|kaPt57HDN){5w56CLt_~G<&f02B)9)pXPBQ~rqCakUzlrB{wt=qL9@77{-ur}k zz*g1J5m|2dy0TN<<2#z2+VW^?r-pC&q!tZzwuTaq$JR(>vD4?evQvZ0k?ho#M}zGX zUO7BYmK)D9fm?X_aG_nBH`YV+g^R-)s#_)vn$0oKCicX-RJ#^#YhmsS+{W5YbA|iH zGdf#;cWe)7s5W&^SdVJc&2?F^JCqbf=g5rf{*HmteHs<*e#R9&-WeAjTD2(`nTe5M zw)4Ep>_5`jZC_uFEpcSAD&;Z-do8w_#cOT)+>K28Se)^iB;U5q=2&vKwcM^fP#4^A z6SM^yE8h!<2`pg1Q*8k2#@P*DUDCvB7S%qD4a?%rq%qGkT>ilND>d#K+&XLc6~hUE%&xGRknP0J*j)pI8Tk-hI~u3z2fss<5p=1;<&@2mn5 z>8Jo%BZ$0nMq3B0zvz1FS_LnOJMXOp`a0iSlL>~kFW1r&?c_?N?s;z7+tt4%dC$1s3q}?L{r;Qh zJ39X2E?zBHuoJ8p3p>q#=juD(A!VQsbSu)l^k%f<@>v@4_+tiRY#3cjJ$o$=U65#QPt<-F*)e8yI8 zM`W1zi7ZYTA;15py$Xf@!3zig0U!VbfB+Bx0zd!=00AHX1b_e#I6eaPQ*YesVQN_uvQ`zx#*;ewo6ikTa2aO%hw>o!2Bi z@&7&E&nWK~_yb-*00;m9AOHk_01yBIKmZ5;0U!VbfWXsE;FPB)FevWk%GHv{&^^9E z@?Vs6|DU70?|HeWy?~%U5C8%|00;m9AOHk_01yBIKmZ5;fj=mLH$43T-^5$z4Ck8s z99{dNf&NPgZBM5yRjpp0n%ddfnZ(i6T>fsZC{E&^^iL^OF&~|op88(%Kl^Lxc4~N2 zsELYP+IyYu_mS+0zkb|#ll}ia-j68nr}zV2KmZ5;0U!VbfB+Bx0zd!=00AHX1c1Pa zByg?|uRPEl79Koj`tag`_N?D+?Lhzi|Cf~a%M)2VPzneD0U!VbfB+Bx0zd!=00AHX z1b_e#I2wT-ys^Nb=Kl{(Q1~CbfB+Bx0zd!=00AHX1b_e#uoC#`I^9eC?E5%~Y{x}j z^ed&W|KpYSURzH*`!KmZ6l1q6uyPZ^sKJ_YK6ia-De00AHX1b_e# z00KY&2mk>f00e-*Aqb2N{+b%0e@l(t@#YlN9c6Q&dKIF~jLb9GB#o{efAe2mXV zHN=AG)(tL+Qmf08L2}$HI=6_D+%4mR9FS@`&y^fB`xuALE_16K&!LrcYRyeK?bc}y zNd>gnRNx9eb~D4Jd6aTLtJYe)Ao|9RtF4c+jQ`p-x_;hr;HOm=!FuYdOTFxBhA@z<}Qk5`BWl_6RvSdUOVoYG(&tqEa?@3XdOrL_=+4y8bss> zReX`psbaBG+tULgbveWYQoezM9;16PzJ!8U5>-*y6m#l!O%(E#ZCMq_9x3E)>S}f| zk4$9tG)CJ`HBMo^&czc+WOP8H)kK7Ba9~b?Y1#X+jEU?*Y9bbXZKW>2+$F&o+_vdW` z2tlHc9(ql?Rjb}QoRVhITIy08GAgA)%YoW@CA;+djjY>s`do5(-}8Nj9JzjQt&RTM zRebcbc%hpD7@}qgEaM(Y{dj|3BBPCYcs%NS#(I~<#;qXS7;f3-V5?j0!Ql5a2oKge zLYVCy!tAj?IP%E!SRR*C(F5{%!xpgx15{kJ$iQO9oRUEbtumbt~>49nt9%j|)8o0#b%hFLRD_i@6i{5R+x>c9X0nDTzy!NZ3rAOHk_ z01yBIKmZ5;0U!VbfB+Bx0zlxT5SXR=sFjc}zE05(@&D+j$#0at`o-tJS_?*A9=pBp z{x5&?>dTyd{2yAcNV3X{imLnnu>aplsYK8N2mk>f00e*l5C8%|00;m9AOHk_z%dhm z{r`>`C}al&fB+Bx0zd!=00AKIEhFHv|6eTeOc}o`*MIoIGJdao%SsHz0RlkaTTB4< z|N9nKE))$200AHX1b_e#00KY&2mk>f00e-5L4f%G%m77={^dx0=s!>W^C@bO!k6#@ z0zd!=00AHX1ir-t>hD}&0@=X8fzQ}V$@p^LUDMd4sLifQ-6Sx2Ed zwf%`t=>FWDw^Fq3o<7s2+ih#Hoj2_?a% Date: Fri, 19 Jun 2026 15:15:41 +0200 Subject: [PATCH 30/98] increase pyjwt-version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 8c2b88051ee..7661d2878de 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,7 +65,7 @@ dependencies = [ "django-widget-tweaks==1.5.0", "django-sequences==3.0", "oauthlib==3.3.1", - "pyjwt==2.8.0", + "pyjwt==2.13.0", "redis==6.2.0", # geopython dependencies From a31df5a68709b2ff4580725a5a4deb0817f9e145 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 22 Jun 2026 14:52:01 +0200 Subject: [PATCH 31/98] [Fixes #14366] Upload error message (#14370) (#14371) * [Fixes #14366] Upload error message * Update geonode/upload/handlers/shapefile/handler.py --------- (cherry picked from commit fe0f3197808cd0db8d87567f7673919b19358404) Co-authored-by: mattiagiupponi <51856725+mattiagiupponi@users.noreply.github.com> Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- geonode/upload/handlers/shapefile/handler.py | 2 +- geonode/upload/handlers/shapefile/tests.py | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/geonode/upload/handlers/shapefile/handler.py b/geonode/upload/handlers/shapefile/handler.py index ff544166fcb..e376ae16dcb 100644 --- a/geonode/upload/handlers/shapefile/handler.py +++ b/geonode/upload/handlers/shapefile/handler.py @@ -156,7 +156,7 @@ def is_valid(files, user, **kwargs): ) if not is_valid: raise InvalidShapeFileException( - detail=f"Some file is missing files with the same name and with the following extension are required: {_shp_ext_needed}" + detail=f"Some files are missing. The files need to have the same name with all extensions: {', '.join(ShapeFileHandler._get_ext_needed())}" ) return True diff --git a/geonode/upload/handlers/shapefile/tests.py b/geonode/upload/handlers/shapefile/tests.py index eb49a2a7047..fdb5e5d22d5 100644 --- a/geonode/upload/handlers/shapefile/tests.py +++ b/geonode/upload/handlers/shapefile/tests.py @@ -25,6 +25,7 @@ from django.contrib.auth import get_user_model from django.test import TestCase from geonode.upload.api.exceptions import UploadParallelismLimitException +from geonode.upload.handlers.shapefile.exceptions import InvalidShapeFileException from geonode.upload.models import UploadParallelismLimit from mock import MagicMock, patch, mock_open from geonode.upload import project_dir @@ -108,6 +109,17 @@ def test_promote_to_multi(self): def test_is_valid_should_pass_with_valid_shp(self): self.handler.is_valid(files=self.valid_shp, user=self.user) + def test_is_valid_should_not_pass_if_file_is_missing(self): + files = self.valid_shp.copy() + files.pop("dbf_file") + with self.assertRaises(InvalidShapeFileException) as _exp: + self.handler.is_valid(files=files, user=self.user) + self.assertIn( + "Some files are missing. The files need to have the same name with all extensions: shp, prj, dbf, shx", + str(_exp.exception), + "error message is not the expected one", + ) + def test_get_ogr2ogr_driver_should_return_the_expected_driver(self): expected = ogr.GetDriverByName("ESRI Shapefile") actual = self.handler.get_ogr2ogr_driver() From f537413e5a1cf33bca53b34a5451f8ed45d747e9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 22 Jun 2026 17:23:48 +0200 Subject: [PATCH 32/98] [Fixes #14374] Add deletion of temporary files in case of rollback (#14375) (#14376) * [Fixes #14374] Add deletion of temporary files in case of rollback * [Fixes #14374] Fix PR suggestions (cherry picked from commit 7dc7f6bc4a4d8111272df22b14577e8bebeb2702) Co-authored-by: mattiagiupponi <51856725+mattiagiupponi@users.noreply.github.com> --- geonode/upload/handlers/common/vector.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/geonode/upload/handlers/common/vector.py b/geonode/upload/handlers/common/vector.py index 929f2812f19..ad81ca983ba 100644 --- a/geonode/upload/handlers/common/vector.py +++ b/geonode/upload/handlers/common/vector.py @@ -420,6 +420,12 @@ def perform_last_step(execution_id): for asset in assets: asset.delete() + BaseVectorFileHandler.__remove_temporary_file(_exec) + + @staticmethod + def __remove_temporary_file(_exec): + if not _exec: + return tmp_data = _exec.input_params.get("temporary_files") if tmp_data: # Delete at the end of the operations, the temporary files created at the beginning @@ -1091,6 +1097,12 @@ def _import_resource_rollback(self, exec_id, instance_name=None, *args, **kwargs We use the schema editor directly, because the model itself is not managed on creation, but for the delete since we are going to handle, we can use it """ + logger.info(f"Rollback temporary file uploaded for execid: {exec_id} resource published was: {instance_name}") + try: + BaseVectorFileHandler.__remove_temporary_file(orchestrator.get_execution_object(exec_id=exec_id)) + except Exception as e: + logger.warning(f"Failed to remove temporary files during rollback: {e}") + logger.info( f"Rollback dynamic model & ogr2ogr step in progress for execid: {exec_id} resource published was: {instance_name}" ) From 2b73f3219f0b442d2756b170ef96cebb85d13eef Mon Sep 17 00:00:00 2001 From: Emanuele Tajariol Date: Tue, 9 Jun 2026 12:32:49 +0200 Subject: [PATCH 33/98] [Fixes #14306] Refact create_tile_layers as a subcommand of gwc - add create --- geonode/geoserver/gwc.py | 40 ++++- geonode/geoserver/management/commands/gwc.py | 56 +++--- .../commands/gwc_subcommands/create.py | 170 ++++++++++++++++++ .../gwc_subcommands/create_template.xml | 41 +++++ .../commands/gwc_subcommands/truncate.py | 54 ++++-- 5 files changed, 319 insertions(+), 42 deletions(-) create mode 100644 geonode/geoserver/management/commands/gwc_subcommands/create.py create mode 100644 geonode/geoserver/management/commands/gwc_subcommands/create_template.xml diff --git a/geonode/geoserver/gwc.py b/geonode/geoserver/gwc.py index 077d6500947..9e54f967203 100644 --- a/geonode/geoserver/gwc.py +++ b/geonode/geoserver/gwc.py @@ -36,15 +36,40 @@ class GWCClient: A GeoWebCache REST client for interacting with the GWC API. """ - def __init__(self) -> None: + def __init__(self, **kwargs) -> None: self.base_url = f"{ogc_server_settings.LOCATION}gwc/rest/" + self.debug = kwargs.get("debug", False) + + def __log_response(self, r): + if self.debug: + logger.debug(f'{r.request.method} response: code:{r.status_code} --> "{r.text}"') + else: + logger.debug(f"{r.request.method} response: code:{r.status_code} --> {len(r.text)} bytes") + + def _get(self, urlpath): + _user, _password = ogc_server_settings.credentials + + url = f"{self.base_url}{urlpath}" + r = requests.get(url=url, auth=HTTPBasicAuth(_user, _password), timeout=30) + self.__log_response(r) + return r def _post(self, urlpath, data): _user, _password = ogc_server_settings.credentials url = f"{self.base_url}{urlpath}" r = requests.post(url=url, data=data, auth=HTTPBasicAuth(_user, _password), timeout=30) - logger.debug(f'POST response: code:{r.status_code} --> "{r.text}"') + self.__log_response(r) + return r + + def _put(self, urlpath, data): + _user, _password = ogc_server_settings.credentials + + url = f"{self.base_url}{urlpath}" + r = requests.put( + url=url, data=data, headers={"Content-Type": "text/xml"}, auth=HTTPBasicAuth(_user, _password), timeout=30 + ) + self.__log_response(r) return r @staticmethod @@ -60,6 +85,17 @@ def _validate_layer_name(layer_name: str, workspace: str | None = None) -> str: layer_name = f"{workspace}:{layer_name}" return layer_name + def get_layer(self, layer_name: str, workspace: str | None = None) -> requests.Response: + """ + Get GWC layer information. Returns the raw response from the GWC API. + """ + layer_name = self._validate_layer_name(layer_name, workspace) + return self._get(f"layers/{layer_name}.xml") + + def set_layer(self, layer_name: str, workspace: str | None = None, data=None) -> requests.Response: + layer_name = self._validate_layer_name(layer_name, workspace) + return self._put(f"layers/{layer_name}.xml", data=data) + def truncate_layer(self, layer_name: str, workspace: str | None = None) -> None: """ Truncate all cached tiles for a GWC layer. diff --git a/geonode/geoserver/management/commands/gwc.py b/geonode/geoserver/management/commands/gwc.py index ccf516837a2..1cd1698451e 100644 --- a/geonode/geoserver/management/commands/gwc.py +++ b/geonode/geoserver/management/commands/gwc.py @@ -21,47 +21,53 @@ from geonode.base.management.command_utils import setup_logger from geonode.geoserver.management.commands.gwc_subcommands import truncate +from geonode.geoserver.management.commands.gwc_subcommands.create import CreateTileLayers logger = setup_logger() COMMAND_TRUNCATE = "truncate" -COMMANDS = [COMMAND_TRUNCATE] +COMMAND_CREATE = "create" +COMMANDS = [COMMAND_CREATE, COMMAND_TRUNCATE] class Command(BaseCommand): help = f"Handles GWC commands {COMMANDS}" + def _add_common_arguments(self, parser): + parser.add_argument( + '-d', + '--dry-run', + dest="dry-run", + action='store_true', + help="Do not actually perform any change on GWC") + + parser.add_argument( + '--debug', + dest="debug", + action='store_true', + help="Show debug logging") + def add_arguments(self, parser): - parser.add_argument("subcommand", nargs="?", choices=COMMANDS, help="GWC operation to run") - truncate_group = parser.add_argument_group('Params for "truncate" subcommand') - truncate_group.add_argument( - "-l", - "--layer", - dest="layers", - action="append", - help="Name of the layer(s) to truncate. Can be repeated.", - ) - truncate_group.add_argument( - "--all", - dest="truncate_all", - action="store_true", - help="Truncate all cache in GWC", - ) + subparsers = parser.add_subparsers(dest="subcommand", required=True) - def handle(self, *args, **options): - subcommand = options["subcommand"] - logger.info("Starting GWC command.") + parser_truncate = subparsers.add_parser(COMMAND_TRUNCATE, help="Truncate tile layers") + truncate.add_arguments(parser_truncate) + self._add_common_arguments(parser_truncate) - if not subcommand: - logger.warning("No GWC subcommand provided.") - self.print_help("manage.py", "gwc") - return + parser_create = subparsers.add_parser(COMMAND_CREATE, help="Create tile layers") + CreateTileLayers().add_arguments(parser_create) + self._add_common_arguments(parser_create) - logger.info("Executing GWC subcommand '%s'.", subcommand) + def handle(self, *args, **options): + subcommand = options["subcommand"] + logger.info(f"Executing GWC subcommand '{subcommand}'...") if subcommand == COMMAND_TRUNCATE: truncate.handle(options) - + elif subcommand == COMMAND_CREATE: + CreateTileLayers().handle(**options) else: raise CommandError(f"Unknown subcommand: {subcommand}") + + logger.info(f"GWC subcommand {subcommand} completed.") diff --git a/geonode/geoserver/management/commands/gwc_subcommands/create.py b/geonode/geoserver/management/commands/gwc_subcommands/create.py new file mode 100644 index 00000000000..378d80aa6bc --- /dev/null +++ b/geonode/geoserver/management/commands/gwc_subcommands/create.py @@ -0,0 +1,170 @@ +# -*- coding: utf-8 -*- +######################################################################### +# +# Copyright (C) 2023 OSGeo +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +######################################################################### + +import logging +import os +import xml.etree.ElementTree as ET + +from django.core.management import CommandError + +from geonode.base.management.command_utils import setup_logger +from geonode.geoserver.gwc import GWCClient +from geonode.layers.models import Dataset + + +logger = setup_logger() + + +class CreateTileLayers: + help = "Create TileLayers in GWC" + + def add_arguments(self, parser): + parser.add_argument( + '-f', + '--force', + dest="force", + action='store_true', + help="Force tile layer re-creation also if it already exists in GWC") + + parser.add_argument( + '-l', + '--layer', + dest="layers", + action='append', + help="Only process specified layers ") + + parser.add_argument( + "--all", + dest="create_all", + action="store_true", + help="Create caches for all layers", + ) + + parser.add_argument( + "-t", + "--template", + dest="xmltemplatefilepath", + help="Use the specified XML template file path", + ) + + def handle(self, **options): + dry_run = options.get('dry-run') + debug = options.get('debug') + setup_logger(level=logging.DEBUG if debug else logging.INFO) + + force = options.get('force') + + requested_layers = options.get('layers', []) + create_all = options.get('create_all') + + if not requested_layers and not create_all: + raise CommandError("'create' command requires either the -l/--layer parameter(s) or the --all flag.") + + if requested_layers and create_all: + raise CommandError("Cannot use both -l/--layer and --all at the same time.") + + xmltemplate = options.get('xmltemplatefilepath') + + if debug: + logger.debug(f"FORCE is {force}") + logger.debug(f"DRY-RUN is {dry_run}") + logger.debug(f"LAYERS is {requested_layers}") + + self.run(requested_layers=requested_layers, create_all=create_all, force=force, xmltemplate=xmltemplate, dry_run=dry_run, debug=debug) + + def run(self, requested_layers=None, create_all=False, force=False, xmltemplate=None, dry_run=False, debug=False): + if create_all: + datasets_fields = Dataset.objects.values_list('typename', 'alternate') + layer_names = [typename or alternate for typename, alternate in datasets_fields] + else: + layer_names = requested_layers + tot = len(layer_names) + logger.info(f"Total layers to be processed: {tot}") + + template = self.load_xml_template(xmltemplate, debug=debug) + gwc = GWCClient(debug=debug) + + i = cnt_old = cnt_new = cnt_bad = cnt_force = 0 + + for layername in layer_names: + i += 1 + logger.info(f"- {i}/{tot} Processing layer: {layername}") + + r = gwc.get_layer(layername) + if r.status_code == 200: + if force: + logger.info(" - Forcing layer configuration in GWC") + cnt_force += 1 + else: + logger.info(f" - Layer already configured in GWC (code {r.status_code})") + cnt_old += 1 + continue + try: + logger.info(" - Configuring...") + data = self.generate_xml(template, layername) + if not dry_run: + response = gwc.set_layer(layername, data=data) + + if dry_run or response.status_code == 200: + logger.info(f" - Done {layername}") + cnt_new += 1 + else: + logger.warning(f"Layer {layername} couldn't be configured: code {response.status_code}") + cnt_bad += 1 + + except Exception as e: + logger.warning(f"Error processing {layername}: {e}") + cnt_bad += 1 + + logger.info("Work completed") + logger.info(f"- TileLayers configured: {cnt_new}" + (f" (forced {cnt_force})" if cnt_force else "")) + logger.info(f"- TileLayers in error : {cnt_bad}") + logger.info(f"- TileLayers untouched : {cnt_old}") + + def load_xml_template(self, xmltemplate=None, debug=False): + """Load the XML template""" + if xmltemplate: + logger.info(f"Using provided xml template at {xmltemplate}") + xml_path = os.path.abspath(xmltemplate) + else: + current_dir = os.path.dirname(os.path.abspath(__file__)) + xml_path = os.path.join(current_dir, "create_template.xml") + + if not os.path.isfile(xml_path): + raise CommandError(f"XML template could not be found at {xml_path}") + + try: + tree = ET.parse(xml_path) + except ET.ParseError as e: + if debug: + logger.exception(f"Error parsing XML template {xml_path}") + raise CommandError(f"Error parsing XML template {xml_path}: {e}") + + root = tree.getroot() + return root + + def generate_xml(self, root, layer_name): + name_node = root.find('name') + + if name_node is None: + raise ValueError("name node not found in XML template") + + name_node.text = layer_name + return ET.tostring(root, encoding="utf-8", method="xml") diff --git a/geonode/geoserver/management/commands/gwc_subcommands/create_template.xml b/geonode/geoserver/management/commands/gwc_subcommands/create_template.xml new file mode 100644 index 00000000000..31dd8dfc919 --- /dev/null +++ b/geonode/geoserver/management/commands/gwc_subcommands/create_template.xml @@ -0,0 +1,41 @@ + + + true + true + {} + + 2 + 1 + + + application/json;type=utfgrid + image/gif + image/jpeg + image/png + image/png8 + image/vnd.jpeg-png + image/vnd.jpeg-png8 + + + + EPSG:3857 + + + EPSG:3857x2 + + + EPSG:4326 + + + EPSG:4326x2 + + + EPSG:900913 + + + 0 + 0 + true + 0 + + diff --git a/geonode/geoserver/management/commands/gwc_subcommands/truncate.py b/geonode/geoserver/management/commands/gwc_subcommands/truncate.py index 111b4a3ebc7..2f0e487c400 100644 --- a/geonode/geoserver/management/commands/gwc_subcommands/truncate.py +++ b/geonode/geoserver/management/commands/gwc_subcommands/truncate.py @@ -16,6 +16,7 @@ # along with this program. If not, see . # ######################################################################### +import logging from geonode.base.management.command_utils import setup_logger from geonode.geoserver.gwc import GWCClient @@ -25,38 +26,60 @@ logger = setup_logger() -def handle(options: dict) -> None: - layers = options.get("layers") or [] - truncate_all = options.get("truncate_all") or False +def add_arguments(parser): + parser.add_argument( + "-l", + "--layer", + dest="layers", + action="append", + help="Name of the layer(s) to truncate. Can be repeated.", + ) - logger.info( - "Truncate command received. layers=%s, truncate_all=%s", - layers, - truncate_all, + parser.add_argument( + "--all", + dest="truncate_all", + action="store_true", + help="Truncate all caches in GWC", ) + +def handle(options: dict) -> None: + dry_run = options.get('dry-run', False) + debug = options.get('debug', False) + setup_logger(level=logging.DEBUG if debug else logging.INFO) + + layers = options.get("layers", []) + truncate_all = options.get("truncate_all") + if not layers and not truncate_all: raise CommandError("'truncate' command requires either the -l/--layer parameter(s) or the --all flag.") if layers and truncate_all: raise CommandError("Cannot use both -l/--layer and --all at the same time.") + logger.info( + "Truncate command received. layers=%s, truncate_all=%s", + layers, + truncate_all, + ) + if truncate_all: - truncate_all_layers() + truncate_all_layers(dryrun=dry_run, debug=debug) else: logger.info(f"Truncating {len(layers)} layer{'s' if len(layers) > 1 else ''}") - truncate_layers(layers) + truncate_layers(layers, dryrun=dry_run, debug=debug) -def truncate_layers(layer_names): +def truncate_layers(layer_names, dryrun=False, debug=False): """Truncates one or more specified layers in GWC.""" logger.info(f"Truncating layers in GWC: {', '.join(layer_names)}") - client = GWCClient() + client = GWCClient(debug=debug) failed_layers = [] for layer_name in layer_names: try: logger.info(f"Truncating layer: {layer_name}") - client.truncate_layer(layer_name) + if not dryrun: + client.truncate_layer(layer_name) except Exception as e: logger.error(f"Error invalidating cache for layer {layer_name}: {e}") failed_layers.append(layer_name) @@ -64,12 +87,13 @@ def truncate_layers(layer_names): raise CommandError(f"Failed to truncate the following layers: {', '.join(failed_layers)}") -def truncate_all_layers(): +def truncate_all_layers(dryrun=False, debug=False): """Truncates all layers in GWC.""" logger.info("Truncating ALL layers in GWC") - client = GWCClient() + client = GWCClient(debug=debug) try: - client.truncate_all() + if not dryrun: + client.truncate_all() logger.info("Successfully truncated all layers in GWC.") except Exception as e: logger.error(f"Error executing truncateAll on GeoWebCache: {e}") From 6b9c5ec1474e733a56b063b48370cff3570701d1 Mon Sep 17 00:00:00 2001 From: Emanuele Tajariol Date: Tue, 9 Jun 2026 15:36:38 +0200 Subject: [PATCH 34/98] [Fixes #14306] Refact create_tile_layers as a subcommand of gwc - refact --- .../management/commands/create_tile_layers.py | 152 +++--------------- 1 file changed, 21 insertions(+), 131 deletions(-) diff --git a/geonode/br/management/commands/create_tile_layers.py b/geonode/br/management/commands/create_tile_layers.py index 57b81d805aa..1b40295806e 100644 --- a/geonode/br/management/commands/create_tile_layers.py +++ b/geonode/br/management/commands/create_tile_layers.py @@ -19,155 +19,45 @@ ######################################################################### import logging -import requests -from requests.auth import HTTPBasicAuth - from django.core.management.base import BaseCommand -from django.conf import settings - -from geonode.layers.models import Dataset +from typing_extensions import deprecated +from geonode.geoserver.management.commands.gwc_subcommands.create import CreateTileLayers as gwc_create logger = logging.getLogger(__name__) -REQ_TEMPLATE = """ - - true - true - {} - - 2 - 1 - - - application/json;type=utfgrid - image/gif - image/jpeg - image/png - image/png8 - image/vnd.jpeg-png - image/vnd.jpeg-png8 - - - - EPSG:3857 - - - EPSG:3857x2 - - - EPSG:4326 - - - EPSG:4326x2 - - - EPSG:900913 - - - 0 - 0 - true - 0 - -""" - - +@deprecated("THIS COMMAND IS DEPRECATED - USE THE gwc COMMAND INSTEAD") class Command(BaseCommand): - help = "Create missing TileLayers in GWC" + help = "DEPRECATED COMMAND - USE gwc COMMAND INSTEAD - Create missing TileLayers in GWC" def add_arguments(self, parser): parser.add_argument( - '-f', - '--force', + "-f", + "--force", dest="force", - action='store_true', - help="Force tile layer re-creation also if it already exists in GWC") + action="store_true", + help="Force tile layer re-creation also if it already exists in GWC", + ) - parser.add_argument( - '-l', - '--layer', - dest="layers", - action='append', - help="Only process specified layers ") + parser.add_argument("-l", "--layer", dest="layers", action="append", help="Only process specified layers ") parser.add_argument( - '-d', - '--dry-run', - dest="dry-run", - action='store_true', - help="Do not actually perform any change on GWC") + "-d", "--dry-run", dest="dry-run", action="store_true", help="Do not actually perform any change on GWC" + ) def handle(self, **options): - force = options.get('force') - requested_layers = options.get('layers') - dry_run = options.get('dry-run') + logger.error("THIS COMMAND IS DEPRECATED - USE THE gwc COMMAND INSTEAD") + + force = options.get("force") + requested_layers = options.get("layers") or [] + dry_run = options.get("dry-run") logger.debug(f"FORCE is {force}") logger.debug(f"DRY-RUN is {dry_run}") logger.debug(f"LAYERS is {requested_layers}") - try: - baseurl = settings.OGC_SERVER["default"]["LOCATION"] - user = settings.OGC_SERVER["default"]["USER"] - passwd = settings.OGC_SERVER["default"]["PASSWORD"] - """ - curl -v -u admin:geoserver -XGET \ - "http://:/geoserver/gwc/rest/layers/geonode:tasmania_roads.xml" - """ - layers = Dataset.objects.all() - tot = len(layers) - logger.info(f"Total layers in GeoNode: {tot}") - i = 0 - cnt_old = 0 - cnt_new = 0 - cnt_bad = 0 - cnt_skip = 0 - cnt_force = 0 - for layer in layers: - i += 1 - logger.info(f"- {i}/{tot} Processing layer: {layer.typename}") - - if requested_layers and layer.typename not in requested_layers: - logger.info(" - Layer filtered out by args") - cnt_skip += 1 - continue - - r = requests.get(f"{baseurl}gwc/rest/layers/{layer.typename}.xml", auth=HTTPBasicAuth(user, passwd)) - - if r.status_code == 200: - if force: - logger.info(" - Forcing layer configuration in GWC") - cnt_force += 1 - else: - logger.info(" - Layer already configured in GWC") - cnt_old += 1 - continue - try: - data = REQ_TEMPLATE.format(layer.name) - url = f"{baseurl}gwc/rest/layers/{layer.typename}.xml" - logger.info(" - Configuring...") - - if not dry_run: - response = requests.put( - url, data=data, headers={"Content-Type": "text/xml"}, auth=HTTPBasicAuth(user, passwd) - ) - - if dry_run or response.status_code == 200: - logger.info(f" - Done {layer.name}") - cnt_new += 1 - else: - logger.warning(f"Layer {layer.typename} couldn't be configured: code {response.status_code}") - cnt_bad += 1 - - except Exception as e: - raise e - except Exception as e: - raise e - - logger.info("Work completed") - logger.info(f"- TileLayers configured: {cnt_new}" + (f" (forced {cnt_force})" if cnt_force else "")) - logger.info(f"- TileLayers in error : {cnt_bad}") - logger.info(f"- TileLayers untouched : {cnt_old}") - logger.info(f"- TileLayers skipped : {cnt_skip}") + subcommand = gwc_create() + subcommand.run( + force=force, requested_layers=requested_layers, create_all=len(requested_layers) == 0, dry_run=dry_run + ) From ed585c07c34e28adb779b35aeaa5ef192f1b7c80 Mon Sep 17 00:00:00 2001 From: Emanuele Tajariol Date: Tue, 23 Jun 2026 09:35:48 +0200 Subject: [PATCH 35/98] [Fixes #14358] Align GWC template to the default GWC configuration --- .../commands/gwc_subcommands/create_template.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/geonode/geoserver/management/commands/gwc_subcommands/create_template.xml b/geonode/geoserver/management/commands/gwc_subcommands/create_template.xml index 31dd8dfc919..a907561b566 100644 --- a/geonode/geoserver/management/commands/gwc_subcommands/create_template.xml +++ b/geonode/geoserver/management/commands/gwc_subcommands/create_template.xml @@ -5,7 +5,7 @@ {} 2 - 1 + 2 application/json;type=utfgrid @@ -32,10 +32,10 @@ EPSG:900913 + + EPSG:900913x2 + - 0 - 0 true 0 - From acf7f7814530b6240d346d7006b039bd02cb4c2c Mon Sep 17 00:00:00 2001 From: Mattia Giupponi Date: Tue, 23 Jun 2026 13:02:18 +0200 Subject: [PATCH 36/98] [Fixes #14369] Map thumbnail is not saved if remote WMS without services --- geonode/layers/models.py | 5 +++++ geonode/thumbs/thumbnails.py | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/geonode/layers/models.py b/geonode/layers/models.py index b9d753ecf50..ada0787e9dc 100644 --- a/geonode/layers/models.py +++ b/geonode/layers/models.py @@ -283,6 +283,11 @@ def capabilities_url(self): @property def dataset_ows_url(self): + if self.remote_service: + return self.remote_service.service_url + elif self.subtype == "remote" and self.ows_url: + return self.ows_url + from geonode.geoserver.helpers import get_layer_ows_url return get_layer_ows_url(self) diff --git a/geonode/thumbs/thumbnails.py b/geonode/thumbs/thumbnails.py index 2fdbc941be5..a701808cb8c 100644 --- a/geonode/thumbs/thumbnails.py +++ b/geonode/thumbs/thumbnails.py @@ -337,7 +337,7 @@ def _datasets_locations( # Check if remote service requires authentication auth = _get_auth(dataset) # limit number of locations, ensuring dataset order - if len(locations) and locations[-1][0] == dataset.remote_service.service_url: + if len(locations) and locations[-1][0] == dataset.dataset_ows_url: # if previous dataset's location is the same as the current one - append current dataset there locations[-1][1].append(dataset.alternate) # update the styles too @@ -346,7 +346,7 @@ def _datasets_locations( else: locations.append( [ - dataset.remote_service.service_url, + dataset.dataset_ows_url, [dataset.alternate], [map_dataset_style] if map_dataset_style else [], auth, From 692274f3e218218b5951a8a9c51fe71038ac4f6e Mon Sep 17 00:00:00 2001 From: Giovanni Allegri Date: Thu, 25 Jun 2026 10:09:57 +0200 Subject: [PATCH 37/98] Update Nginx image version to 1.31.2-latest --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index c2519e1bece..358203d3892 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -65,7 +65,7 @@ services: # Nginx is serving django static and media files and proxies to django and geonode geonode: - image: geonode/nginx:1.31.0-latest + image: geonode/nginx:1.31.2-latest container_name: nginx4${COMPOSE_PROJECT_NAME} env_file: - .env From 166345ed2c04da001fb4888546b15ca82fac3998 Mon Sep 17 00:00:00 2001 From: Emanuele Tajariol Date: Fri, 26 Jun 2026 14:49:16 +0200 Subject: [PATCH 38/98] Improve asset file retrieval (cherry picked from commit 51d19cc55b339f04201c6f033d54c28a2fe6fbef) --- geonode/assets/local.py | 13 ++++++++----- geonode/assets/tests.py | 43 +++++++++++++++++++++++++++++++++++++++++ geonode/assets/views.py | 4 ++-- 3 files changed, 53 insertions(+), 7 deletions(-) diff --git a/geonode/assets/local.py b/geonode/assets/local.py index 650fd0cc2dc..6bfbb347e4e 100644 --- a/geonode/assets/local.py +++ b/geonode/assets/local.py @@ -256,7 +256,7 @@ def create_response( return HttpResponse("Asset does not contain any data", status=500) if len(asset.location) > 1: - logger.warning("TODO: Asset contains more than one file. Download needs to be implemented") + logger.warning("TODO: Asset contains more than one file. Only first file will be returned") file0 = asset.location[0] if not path: # use the file definition @@ -266,10 +266,6 @@ def create_response( localfile = file0 else: # a specific file is requested - if "/../" in path: # we may want to improve fraudolent request detection - logger.warning(f"Tentative path traversal for asset {asset.id}") - return HttpResponse(f"File not found for asset {asset.id}", status=400) - if os.path.isfile(file0): dir0 = os.path.dirname(file0) elif os.path.isdir(file0): @@ -280,6 +276,13 @@ def create_response( localfile = os.path.join(dir0, path) logger.debug(f"Requested path {dir0} + {path}") + # check the requested file is within the asset's directory + localfile = os.path.realpath(localfile) + realassetpath = os.path.realpath(dir0) + if os.path.commonpath([realassetpath, localfile]) != realassetpath: + logger.error(f"Tentative path traversal for asset {asset.id} on path [{path}]") + return HttpResponse(f"File not found for asset {asset.id}", status=400) + if os.path.isfile(localfile): filename = os.path.basename(localfile) orig_base, ext = os.path.splitext(filename) diff --git a/geonode/assets/tests.py b/geonode/assets/tests.py index 3ccc6ccff6d..8ece6c35b34 100644 --- a/geonode/assets/tests.py +++ b/geonode/assets/tests.py @@ -415,6 +415,49 @@ def test_download_with_attachment_for_anonymous(self): if resource: resource.delete() + def test_cross_download(self): + + admin, _ = get_user_model().objects.get_or_create(username="admin") + user1, _ = get_user_model().objects.get_or_create(username="user1") + user2, _ = get_user_model().objects.get_or_create(username="user2") + + asset_handler = asset_handler_registry.get_default_handler() + + assets = [] + for user, file in ((user1, ONE_JSON), (user2, TWO_JSON)): + asset1 = asset_handler.create( + title="Test Asset1", + description="Description of test asset", + type="NeverMind", + owner=user1, + files=[file], + clone_files=True, + ) + asset1.save() + self.assertIsInstance(asset1, LocalAsset) + assets.append(asset1) + + # check that user1 can access file1 + self.client.force_login(user1) + + args = {"pk": assets[0].pk, "path": "one.json"} + url = reverse("assets-link", kwargs=args) + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + + # check that user1 can NOT access file2 using asset1 link/download + a2path = assets[1].location[0] + logger.debug(f"Asset path {a2path}") + a2dir = a2path.split("/")[-2] + logger.debug(f"Asset dir {a2dir}") + forged_path = f"../{a2dir}/two.json" # path will be automatically urlencoded into "%2e%2e%2f{a2dir}%2ftwo.json" + + args = {"pk": assets[0].pk, "path": forged_path} + url = reverse("assets-link", kwargs=args) + logger.debug(f"Reverse link URL is {url}") + response = self.client.get(url) + self.assertEqual(response.status_code, 400) + def _setup_test(self, u, _file=ONE_JSON): asset_handler = asset_handler_registry.get_default_handler() asset = asset_handler.create( diff --git a/geonode/assets/views.py b/geonode/assets/views.py index e7d22bcdc4f..11bebea3013 100644 --- a/geonode/assets/views.py +++ b/geonode/assets/views.py @@ -63,7 +63,7 @@ def list(self, request, *args, **kwargs): queryset = self.filter_queryset(self.get_queryset()) user = request.user - is_admin = user.is_superuser if user and user.is_authenticated else False + is_admin = user and user.is_authenticated and user.is_superuser if is_admin: pass @@ -104,5 +104,5 @@ def download(self, request, pk=None, path=None, *args, **kwargs): methods=["get"], ) def link(self, request, pk=None, path=None, *args, **kwargs): - logger.warning(f"REQUESTED ASSET LINK FOR PK:{pk} PATH:{path}") + logger.debug(f"REQUESTED ASSET LINK FOR PK:{pk} PATH:{path}") return self._get_file(request, pk, attachment=False, path=path) From ed36af4786ee00a0a40c419237db94b5a771f871 Mon Sep 17 00:00:00 2001 From: "G.Allegri" Date: Mon, 29 Jun 2026 17:20:38 +0200 Subject: [PATCH 39/98] Fix dynamic version resolution in pyproject.toml setuptools resolves the dynamic version attr by reading geonode.__version__ directly. That attribute is a tuple (e.g. (5, 1, 0, 'final', 0)); setuptools normalises a non-string attr by joining it with dots, producing an invalid PEP 440 version like '5.1.0.final.0' and breaking the wheel build. Expose a plain string attribute __version_str__ = get_version() in geonode/__init__.py and point the dynamic version at it, so setuptools reads an already-normalised version string (e.g. '5.1.0'). (cherry picked from commit a2dda2e3f76d6804f2f91ab0a02c994b4edca39a) --- geonode/__init__.py | 6 ++++++ pyproject.toml | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/geonode/__init__.py b/geonode/__init__.py index a5ad6613ff7..e18c1b7350c 100644 --- a/geonode/__init__.py +++ b/geonode/__init__.py @@ -28,6 +28,12 @@ def get_version(): return geonode.version.get_version(__version__) +# PEP 440 compliant version string, referenced by pyproject.toml's dynamic +# version (a plain string attribute, so setuptools doesn't have to stringify the +# __version__ tuple, which would produce an invalid version like "5.1.0.final.0"). +__version_str__ = get_version() + + def main(_, **settings): from django.core.wsgi import get_wsgi_application diff --git a/pyproject.toml b/pyproject.toml index 7661d2878de..e34b8db2ff9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -191,7 +191,7 @@ exclude = ["tests*"] include = ["geonode*"] [tool.setuptools.dynamic] -version = {attr = "geonode.__version__"} +version = {attr = "geonode.__version_str__"} [tool.black] line-length = 120 From 8d79b7beca68871fbfe0ed03b14fd36fb2248cd3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 1 Jul 2026 10:24:59 +0300 Subject: [PATCH 40/98] =?UTF-8?q?[Fixes=20#14386]=20Replace=20Dataset=20do?= =?UTF-8?q?es=20not=20keep=20in=20account=20the=20user=20perm=E2=80=A6=20(?= =?UTF-8?q?#14387)=20(#14397)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [Fixes #14386] Replace Dataset does not keep in account the user permissions --------- (cherry picked from commit 79d213c4b868349127e56a9cd306ceeae41b16d9) Co-authored-by: mattiagiupponi <51856725+mattiagiupponi@users.noreply.github.com> Co-authored-by: sijandh35 --- geonode/upload/handlers/common/raster.py | 44 ++++- .../upload/handlers/common/tests_raster.py | 80 ++++++++ .../upload/handlers/common/tests_vector.py | 182 +++++++++++++++++- geonode/upload/handlers/common/vector.py | 112 ++++++++--- 4 files changed, 378 insertions(+), 40 deletions(-) diff --git a/geonode/upload/handlers/common/raster.py b/geonode/upload/handlers/common/raster.py index 029662415ca..f3e374bdb06 100644 --- a/geonode/upload/handlers/common/raster.py +++ b/geonode/upload/handlers/common/raster.py @@ -37,6 +37,8 @@ from geonode.upload.handlers.utils import create_alternate, should_be_imported from geonode.upload.models import ResourceHandlerInfo from geonode.upload.orchestrator import orchestrator +from geonode.security.permissions import _to_compact_perms +from geonode.security.registry import permissions_registry from geonode.upload.utils import find_key_recursively, ImporterRequestAction as ira from osgeo import gdal from geonode.upload.celery_app import importer_app @@ -181,9 +183,22 @@ def create_asset_and_link(self, resource, files, action=None): response.raise_for_status() def overwrite_geoserver_resource(self, resource: List[str], catalog, store, workspace): - # we need to delete the resource before recreating it - self._delete_resource(resource, catalog, workspace) - self._delete_store(resource, catalog, workspace) + try: + self._delete_resource(resource, catalog, workspace) + except Exception as e: + logger.warning( + f"Could not delete existing resource '{resource.get('name')}' from GeoServer " + f"before replace. GeoServer returned: {e}. " + f"Proceeding — the resource will be overwritten by publish." + ) + try: + self._delete_store(resource, catalog, workspace) + except Exception as e: + logger.warning( + f"Could not delete existing store '{resource.get('name')}' from GeoServer " + f"before replace. GeoServer returned: {e}. " + f"Proceeding — the store will be overwritten by publish." + ) return self.publish_resources([resource], catalog, store, workspace) def _delete_store(self, resource, catalog, workspace): @@ -380,14 +395,23 @@ def overwrite_geonode_resource( _exec = self._get_execution_request_object(execution_id) - dataset = resource_type.objects.filter(alternate__icontains=alternate, owner=_exec.user) + dataset = resource_type.objects.filter(pk=_exec.input_params.get("resource_pk")).first() _overwrite = _exec.action == ira.REPLACE.value - # if the layer exists, we just update the information of the dataset by - # let it recreate the catalogue - if dataset.exists() and _overwrite: - dataset = dataset.first() + if dataset and _overwrite: + perms = _to_compact_perms( + permissions_registry.get_perms( + instance=dataset, + user=_exec.user, + ) + ) + if not any(p in perms for p in ("manage", "edit")): + raise ImportException( + f"User does not have permission to replace dataset '{layer_name}'. " + f"'edit' or 'manage' permission is required." + ) + resolved_resource_manager = resource_manager_registry.get_for_instance(dataset) dataset = resolved_resource_manager.update( dataset.uuid, @@ -410,12 +434,12 @@ def overwrite_geonode_resource( resolved_resource_manager.set_thumbnail(dataset.uuid, instance=dataset, overwrite=True) dataset.refresh_from_db() return dataset - elif not dataset.exists() and _overwrite: + elif not dataset and _overwrite: logger.warning( f"The dataset required {alternate} does not exists, but an overwrite is required, the resource will be created" ) return self.create_geonode_resource(layer_name, alternate, execution_id, resource_type, asset) - elif not dataset.exists() and not _overwrite: + elif not dataset and not _overwrite: logger.warning("The resource does not exists, please use 'create_geonode_resource' to create one") return diff --git a/geonode/upload/handlers/common/tests_raster.py b/geonode/upload/handlers/common/tests_raster.py index 6169e9b0ee5..66699a18d0a 100644 --- a/geonode/upload/handlers/common/tests_raster.py +++ b/geonode/upload/handlers/common/tests_raster.py @@ -16,6 +16,7 @@ # along with this program. If not, see . # ######################################################################### +from unittest.mock import MagicMock from django.test import TestCase from mock import patch from geonode.upload.handlers.common.raster import BaseRasterFileHandler @@ -93,3 +94,82 @@ def test_import_resource_should_work(self, import_orchestrator): finally: if exec_id: ExecutionRequest.objects.filter(exec_id=exec_id).delete() + + def test_overwrite_geoserver_resource_delete_resource_failure_is_non_fatal(self): + """ + If _delete_resource raises during a replace, overwrite_geoserver_resource + must log a warning and continue to publish_resources rather than failing. + """ + mock_catalog = MagicMock() + mock_store = MagicMock() + mock_workspace = MagicMock() + resource = {"name": "test_raster"} + + with patch.object(self.handler, "_delete_resource", side_effect=Exception("GeoServer 500")): + with patch.object(self.handler, "_delete_store") as mock_delete_store: + with patch.object(self.handler, "publish_resources", return_value=True) as mock_publish: + result = self.handler.overwrite_geoserver_resource( + resource, mock_catalog, mock_store, mock_workspace + ) + # _delete_store must still be attempted even if _delete_resource failed + mock_delete_store.assert_called_once() + mock_publish.assert_called_once() + self.assertTrue(result) + + def test_overwrite_geoserver_resource_delete_store_failure_is_non_fatal(self): + """ + If _delete_store raises during a replace, overwrite_geoserver_resource + must log a warning and continue to publish_resources rather than failing. + """ + mock_catalog = MagicMock() + mock_store = MagicMock() + mock_workspace = MagicMock() + resource = {"name": "test_raster"} + + with patch.object(self.handler, "_delete_resource"): + with patch.object(self.handler, "_delete_store", side_effect=Exception("GeoServer 500")): + with patch.object(self.handler, "publish_resources", return_value=True) as mock_publish: + result = self.handler.overwrite_geoserver_resource( + resource, mock_catalog, mock_store, mock_workspace + ) + mock_publish.assert_called_once() + self.assertTrue(result) + + def test_overwrite_geoserver_resource_both_deletes_fail_still_publishes(self): + """ + If both _delete_resource and _delete_store raise, publish_resources + must still be called — the store will be overwritten by publish. + """ + mock_catalog = MagicMock() + mock_store = MagicMock() + mock_workspace = MagicMock() + resource = {"name": "test_raster"} + + with patch.object(self.handler, "_delete_resource", side_effect=Exception("GeoServer 500")): + with patch.object(self.handler, "_delete_store", side_effect=Exception("GeoServer 500")): + with patch.object(self.handler, "publish_resources", return_value=True) as mock_publish: + result = self.handler.overwrite_geoserver_resource( + resource, mock_catalog, mock_store, mock_workspace + ) + mock_publish.assert_called_once() + self.assertTrue(result) + + def test_overwrite_geoserver_resource_success_calls_all_steps(self): + """ + When everything works, all three steps must be called in order. + """ + mock_catalog = MagicMock() + mock_store = MagicMock() + mock_workspace = MagicMock() + resource = {"name": "test_raster"} + + with patch.object(self.handler, "_delete_resource") as mock_delete_resource: + with patch.object(self.handler, "_delete_store") as mock_delete_store: + with patch.object(self.handler, "publish_resources", return_value=True) as mock_publish: + result = self.handler.overwrite_geoserver_resource( + resource, mock_catalog, mock_store, mock_workspace + ) + mock_delete_resource.assert_called_once_with(resource, mock_catalog, mock_workspace) + mock_delete_store.assert_called_once_with(resource, mock_catalog, mock_workspace) + mock_publish.assert_called_once_with([resource], mock_catalog, mock_store, mock_workspace) + self.assertTrue(result) diff --git a/geonode/upload/handlers/common/tests_vector.py b/geonode/upload/handlers/common/tests_vector.py index 939d0773408..d544f0f2908 100644 --- a/geonode/upload/handlers/common/tests_vector.py +++ b/geonode/upload/handlers/common/tests_vector.py @@ -25,7 +25,7 @@ from django.test import TestCase from django.urls import reverse from mock import MagicMock, patch -from geonode.upload.api.exceptions import UpsertException +from geonode.upload.api.exceptions import ImportException, UpsertException from geonode.upload.handlers.common.vector import BaseVectorFileHandler, import_with_ogr2ogr from django.contrib.auth import get_user_model from geonode.upload import project_dir @@ -38,8 +38,8 @@ from dynamic_models.models import ModelSchema from osgeo import ogr from django.test.utils import override_settings +from geonode.upload.utils import ImporterRequestAction as ira from geoserver.catalog import Catalog - from geonode.upload.tests.utils import TransactionImporterBaseTestSupport from geonode.utils import OGC_Servers_Handler from geonode.upload.utils import create_vrt_file, has_incompatible_field_names @@ -598,6 +598,184 @@ def test_copy_table_with_ogr2ogr(self, mock_popen): self.assertIn("new_table", call_args) self.assertIn("original_table", call_args) + def test_setup_dynamic_model_non_owner_with_edit_permission_can_replace(self): + """ + A non-owner user with 'edit' permission should be allowed to replace + the dataset. The dataset lookup must not be scoped by owner. + """ + exec_id = None + try: + non_owner, _ = get_user_model().objects.get_or_create(username="non_owner_editor") + exec_id = orchestrator.create_execution_request( + user=non_owner, + func_name="funct1", + step="step", + input_params={"files": self.valid_files, "skip_existing_layer": False}, + ) + resource = self.handler.create_geonode_resource( + "stazioni_metropolitana", + "stazioni_metropolitana", + str(exec_id), + ) + ExecutionRequest.objects.filter(exec_id=exec_id).update( + input_params={"files": self.valid_files, "skip_existing_layer": False, "resource_pk": resource.pk} + ) + + with patch("geonode.upload.handlers.common.vector._to_compact_perms", return_value=["edit"]): + with patch("geonode.upload.handlers.common.vector.permissions_registry") as mock_registry: + mock_registry.get_perms.return_value = MagicMock() + layers = ogr.Open(self.valid_gpkg) + dynamic_model, layer_name, celery_group = self.handler.setup_dynamic_model( + layer=[x for x in layers][0], + execution_id=str(exec_id), + should_be_overwritten=True, + username=non_owner, + ) + self.assertIsNotNone(dynamic_model) + self.assertIsNotNone(layer_name) + finally: + if exec_id: + ExecutionRequest.objects.filter(exec_id=exec_id).delete() + + def test_setup_dynamic_model_user_without_permission_cannot_replace(self): + """ + A user without 'edit' or 'manage' permission should get an ImportException + when trying to replace a dataset they don't own. + """ + exec_id = None + try: + stranger, _ = get_user_model().objects.get_or_create(username="stranger_user") + exec_id = orchestrator.create_execution_request( + user=stranger, + func_name="funct1", + step="step", + input_params={"files": self.valid_files, "skip_existing_layer": False}, + ) + resource = self.handler.create_geonode_resource( + "stazioni_metropolitana", + "stazioni_metropolitana", + str(exec_id), + ) + ExecutionRequest.objects.filter(exec_id=exec_id).update( + input_params={"files": self.valid_files, "skip_existing_layer": False, "resource_pk": resource.pk} + ) + + with patch("geonode.upload.handlers.common.vector._to_compact_perms", return_value=["view"]): + with patch("geonode.upload.handlers.common.vector.permissions_registry") as mock_registry: + mock_registry.get_perms.return_value = MagicMock() + layers = ogr.Open(self.valid_gpkg) + with self.assertRaises(ImportException): + self.handler.setup_dynamic_model( + layer=[x for x in layers][0], + execution_id=str(exec_id), + should_be_overwritten=True, + username=stranger, + ) + finally: + if exec_id: + ExecutionRequest.objects.filter(exec_id=exec_id).delete() + + def test_setup_dynamic_model_get_or_create_prevents_unique_constraint_crash(self): + """ + If a ModelSchema already exists but is not found by the iexact lookup + (e.g. name normalization mismatch between .shp and .zip), the second + branch must not crash with a unique constraint violation. + """ + exec_id = None + schema = None + try: + exec_id = orchestrator.create_execution_request( + user=self.user, + func_name="funct1", + step="step", + input_params={"files": self.valid_files, "skip_existing_layer": False}, + ) + # Pre-create the schema to simulate a stale/orphaned entry + schema, _ = ModelSchema.objects.get_or_create(name="stazioni_metropolitana", db_name="datastore") + layers = ogr.Open(self.valid_gpkg) + # Should not raise IntegrityError — get_or_create absorbs the collision + dynamic_model, layer_name, celery_group = self.handler.setup_dynamic_model( + layer=[x for x in layers][0], + execution_id=str(exec_id), + should_be_overwritten=False, + username=self.user, + ) + self.assertIsNotNone(dynamic_model) + finally: + if exec_id: + ExecutionRequest.objects.filter(exec_id=exec_id).delete() + if schema: + schema.delete() + + def test_overwrite_geonode_resource_non_owner_with_manage_permission_can_overwrite(self): + """ + A non-owner with 'manage' permission should be able to overwrite + a dataset via overwrite_geonode_resource. + """ + exec_id = None + try: + non_owner, _ = get_user_model().objects.get_or_create(username="non_owner_manager") + exec_id = orchestrator.create_execution_request( + user=non_owner, + func_name="funct1", + step="step", + action=ira.REPLACE.value, + input_params={ + "files": self.valid_files, + "resource_pk": self.layer.pk, + }, + ) + + with patch("geonode.upload.handlers.common.vector._to_compact_perms", return_value=["manage"]): + with patch("geonode.upload.handlers.common.vector.permissions_registry") as mock_registry: + mock_registry.get_perms.return_value = MagicMock() + with patch.object( + self.handler, "refresh_geonode_resource", return_value=self.layer + ) as mock_refresh: + result = self.handler.overwrite_geonode_resource( + layer_name="stazioni_metropolitana", + alternate="stazioni_metropolitana", + execution_id=str(exec_id), + ) + mock_refresh.assert_called_once() + self.assertIsNotNone(result) + finally: + if exec_id: + ExecutionRequest.objects.filter(exec_id=exec_id).delete() + + def test_overwrite_geonode_resource_user_without_permission_returns_none(self): + """ + A user without 'edit' or 'manage' permission should get None returned + from overwrite_geonode_resource without touching the dataset. + """ + exec_id = None + try: + stranger, _ = get_user_model().objects.get_or_create(username="stranger_overwrite") + exec_id = orchestrator.create_execution_request( + user=stranger, + func_name="funct1", + step="step", + action=ira.REPLACE.value, + input_params={ + "files": self.valid_files, + "resource_pk": self.layer.pk, + }, + ) + + with patch("geonode.upload.handlers.common.vector._to_compact_perms", return_value=["view"]): + with patch("geonode.upload.handlers.common.vector.permissions_registry") as mock_registry: + mock_registry.get_perms.return_value = MagicMock() + with patch.object(self.handler, "refresh_geonode_resource"): + with self.assertRaises(ImportException): + self.handler.overwrite_geonode_resource( + layer_name="stazioni_metropolitana", + alternate="stazioni_metropolitana", + execution_id=str(exec_id), + ) + finally: + if exec_id: + ExecutionRequest.objects.filter(exec_id=exec_id).delete() + class TestUpsertBaseVectorHandler(TransactionImporterBaseTestSupport): """ diff --git a/geonode/upload/handlers/common/vector.py b/geonode/upload/handlers/common/vector.py index ad81ca983ba..8b66f0e964a 100644 --- a/geonode/upload/handlers/common/vector.py +++ b/geonode/upload/handlers/common/vector.py @@ -681,47 +681,72 @@ def setup_dynamic_model( is_dynamic_model_managed = _exec_obj.input_params.get("is_dynamic_model_managed", False) workspace = DataPublisher(None).workspace + if resource_pk := _exec_obj.input_params.get("resource_pk", None): - user_datasets = Dataset.objects.filter(owner=username, pk=resource_pk) - user_dataset = user_datasets.first() + available_dataset = Dataset.objects.filter(pk=resource_pk) + user_dataset = available_dataset.first() if user_dataset: dynamic_schema = ModelSchema.objects.filter(name__iexact=user_dataset.name) else: dynamic_schema = ModelSchema.objects.none() else: - user_datasets = Dataset.objects.filter(owner=username, alternate__iexact=f"{workspace.name}:{layer_name}") + available_dataset = Dataset.objects.filter(alternate__iexact=f"{workspace.name}:{layer_name}") dynamic_schema = ModelSchema.objects.filter(name__iexact=layer_name) dynamic_schema_exists = dynamic_schema.exists() - dataset_exists = user_datasets.exists() + dataset_exists = available_dataset.exists() + + # evaluate if the user has the correct permissions for the resource. + # if can "edit" / manage the resource, it should be able to replace it + if should_be_overwritten and dataset_exists: + perms = _to_compact_perms( + permissions_registry.get_perms( + instance=available_dataset.first(), + user=username, + ) + ) + if not any(p in perms for p in ("manage", "edit")): + raise ImportException( + f"User '{username}' does not have permission to replace dataset '{layer_name}'. " + f"'edit' or 'manage' permission is required." + ) if dataset_exists and dynamic_schema_exists and should_be_overwritten: """ - If the user have a dataset, the dynamic model has already been created and is in overwrite mode, + If the user has a dataset, the dynamic model has already been created and is in overwrite mode, we just take the dynamic_model to overwrite the existing one """ dynamic_schema = dynamic_schema.get() - layer_name = user_datasets.first().alternate.split(":")[-1] + layer_name = available_dataset.first().alternate.split(":")[-1] + elif not dataset_exists and not dynamic_schema_exists: """ - cames here when is a new brand upload or when (for any reasons) the dataset exists but the - dynamic model has not been created before + Comes here when is a brand new upload or when (for any reasons) the dataset exists but the + dynamic model has not been created before. """ - # layer_name = create_alternate(layer_name, execution_id) - dynamic_schema = ModelSchema.objects.create( + dynamic_schema, created = ModelSchema.objects.get_or_create( name=layer_name, - db_name="datastore", - managed=is_dynamic_model_managed, - db_table_name=layer_name, + defaults={ + "db_name": "datastore", + "managed": is_dynamic_model_managed, + "db_table_name": layer_name, + }, ) + if not created: + logger.warning( + f"ModelSchema for '{layer_name}' already exists but was not found by the " + f"iexact lookup — possible name normalization mismatch between file formats " + f"(e.g. .shp vs .zip). Reusing the existing schema for execution {execution_id}." + ) + elif ( (not dataset_exists and dynamic_schema_exists) or (dataset_exists and dynamic_schema_exists and not should_be_overwritten) or (dataset_exists and not dynamic_schema_exists) ): """ - it comes here when the layer should not be overrided so we append the UUID - to the layer to let it proceed to the next steps + The layer should not be overwritten so we append the UUID + to the layer name to let it proceed to the next steps """ layer_name = create_alternate(layer_name, execution_id) dynamic_schema, _ = ModelSchema.objects.get_or_create( @@ -920,24 +945,33 @@ def overwrite_geonode_resource( ): _exec = self._get_execution_request_object(execution_id) - dataset = resource_type.objects.filter(pk=_exec.input_params.get("resource_pk"), owner=_exec.user) + dataset = resource_type.objects.filter(pk=_exec.input_params.get("resource_pk")).first() _overwrite = _exec.action == ira.REPLACE.value # if the layer exists, we just update the information of the dataset by # let it recreate the catalogue - if dataset.exists() and _overwrite: - dataset = dataset.first() + if dataset and _overwrite: + perms = _to_compact_perms( + permissions_registry.get_perms( + instance=dataset, + user=_exec.user, + ) + ) + if not any(p in perms for p in ("manage", "edit")): + logger.error( + f"User '{_exec.user}' does not have permission to overwrite dataset " + f"'{dataset.alternate}'. 'edit' or 'manage' permission is required." + ) + raise ImportException( + f"User does not have permission to replace dataset '{layer_name}'. " + f"'edit' or 'manage' permission is required." + ) dataset = self.refresh_geonode_resource( str(_exec.exec_id), asset, dataset, create_asset=False, layer_name=layer_name ) return dataset - elif not dataset.exists() and _overwrite: - logger.warning( - f"The dataset required {alternate} does not exists, but an overwrite is required, the resource will be created" - ) - return self.create_geonode_resource(layer_name, alternate, execution_id, resource_type, asset) - elif not dataset.exists() and not _overwrite: + elif not dataset and not _overwrite: logger.warning("The resource does not exists, please use 'create_geonode_resource' to create one") return @@ -1109,10 +1143,32 @@ def _import_resource_rollback(self, exec_id, instance_name=None, *args, **kwargs schema = None if settings.IMPORTER_ENABLE_DYN_MODELS: schema = ModelSchema.objects.filter(name=instance_name).first() + if schema is not None: - _model_editor = ModelSchemaEditor(initial_model=instance_name, db_name=schema.db_name) - _model_editor.drop_table(schema.as_model()) - ModelSchema.objects.filter(name=instance_name).delete() + # before destroying the schema, check whether a dataset that + # predates this execution references it. If so, this execution did not + # create the schema and must not delete it. + dataset_alternate = instance_name + if ":" not in dataset_alternate: + workspace = DataPublisher(None).workspace.name + dataset_alternate = f"{workspace}:{dataset_alternate}" + pre_existing_dataset = ( + Dataset.objects.filter(alternate__iexact=dataset_alternate) + .exclude(resourcehandlerinfo__execution_request__exec_id=exec_id) + .exists() + ) + + if pre_existing_dataset: + logger.warning( + f"Rollback skipping schema deletion for '{instance_name}': " + f"a dataset not created by execution {exec_id} references this schema. " + f"The schema will be preserved to avoid data loss." + ) + else: + _model_editor = ModelSchemaEditor(initial_model=instance_name, db_name=schema.db_name) + _model_editor.drop_table(schema.as_model()) + ModelSchema.objects.filter(name=instance_name).delete() + elif schema is None: try: logger.warning("Dynamic model does not exists, removing ogr2ogr table in progress") @@ -1121,7 +1177,7 @@ def _import_resource_rollback(self, exec_id, instance_name=None, *args, **kwargs return db_name = os.getenv("DEFAULT_BACKEND_DATASTORE", "datastore") with connections[db_name].cursor() as cursor: - cursor.execute(f"DROP TABLE {instance_name}") + cursor.execute(f"DROP TABLE IF EXISTS {instance_name}") except Exception as e: logger.warning(e) pass From c64575748896295b78086b04e19d38013d56819f Mon Sep 17 00:00:00 2001 From: Simone Giannecchini Date: Fri, 3 Jul 2026 15:36:15 +0200 Subject: [PATCH 41/98] Revise security vulnerability reporting instructions (#14403) (cherry picked from commit 80d1ada3a0bf983cc0a9ab368909bf1cc01431d8) --- SECURITY.md | 35 ++++++++++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/SECURITY.md b/SECURITY.md index f24bd36a3ab..273f5de3c0f 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -2,17 +2,42 @@ ## Supported Versions -GeoNode versions supported with security updates. +GeoNode [versions](https://github.com/GeoNode/geonode/wiki/Releases) supported with security updates. | Version | Supported | | ------- | ------------------ | -| 4.0.x | :white_check_mark: | +| 5.1.x | :white_check_mark: | | 5.0.x | :white_check_mark: | -| < 4.0 | :x: | +| < 5 | :x: | + +This approach provides ample time for upgrading ensuring you are always working with a supported GeoNode release. + +If your organization is making use of a GeoNode version that is no longer in use by the community all is not lost. +You can volunteer on the developer list to make additional releases, or engage with one of our +[Commercial Support](https://geonode.org/providers/) providers. ## Reporting a Vulnerability -- **DO NOT** send a security alert on the public mailing list or any other public channel -- **SEND** the report to geonode-psc@lists.osgeo.org and be prapred to collaborate with the GeoNode PSC +1. **DO NOT** report vulnerabilities on the public mailing list or any other public channel +2. There are **two options** to report a security vulnerability: + - Send the report by email to geonode-psc@lists.osgeo.org and be prepared to collaborate with the GeoNode PSC + - Navigate to [Private vulnerability reporting](https://github.com/geonode/geonode/security/advisories/new) and create a new vulnerability report. For more information see [GitHub documentation](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/privately-reporting-a-security-vulnerability#privately-reporting-a-security-vulnerability). + +3. There is no expected response time. +4. Keep in mind participants are volunteering their time, an extensive fix may require fundraising/resources. Thanks for you support and help! + +## Coordinated vulnerability disclosure + +Disclosure policy: + +1. The reported vulnerability has been verified by working with the GeoNode PSC +2. GitHub [security advisory](https://github.com/geonode/geonode/security) is used to reserve a CVE number by the GeoNode Organization +3. A fix or documentation clarification is accepted and backported to active branches +4. A fix is included for the active branches release downloads ([reelases](https://github.com/GeoNode/geonode/releases), or issued via emergency update) +6. The CVE vulnerability is published by the GeoNode Organization with mitigation and patch instructions + +This represents a balance between transparency and participation that does not overwhelm participants. +Those seeking greater visibility are encouraged to volunteer with the [geonode-devel](https://lists.osgeo.org/cgi-bin/mailman/listinfo/geonode-devel) list; +or work with one of the [commercial support providers](https://geonode.org/providers/) who participate on behalf of their customers. From 6487c646f2d15b3b69beb44fb4fbdd1f418f6b73 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Jul 2026 00:04:35 +0000 Subject: [PATCH 42/98] build(deps): bump django from 5.2.15 to 5.2.16 Bumps [django](https://github.com/django/django) from 5.2.15 to 5.2.16. - [Commits](https://github.com/django/django/compare/5.2.15...5.2.16) --- updated-dependencies: - dependency-name: django dependency-version: 5.2.16 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] (cherry picked from commit 22874b9984b03af12f3b9565bf6a500aba0a1d82) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index e34b8db2ff9..bcc1b86059f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,7 +27,7 @@ dependencies = [ "Pillow==12.2.0", "lxml==6.1.1", "psycopg2==2.9.12", - "Django==5.2.15", + "Django==5.2.16", # Other "beautifulsoup4==4.12.3", From 5ab5aa5f22cda9d865ed5620f1e8fe622c36eb59 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Jul 2026 00:03:18 +0000 Subject: [PATCH 43/98] build(deps): bump boto3 from 1.39.3 to 1.43.46 Bumps [boto3](https://github.com/boto/boto3) from 1.39.3 to 1.43.46. - [Release notes](https://github.com/boto/boto3/releases) - [Commits](https://github.com/boto/boto3/compare/1.39.3...1.43.46) --- updated-dependencies: - dependency-name: boto3 dependency-version: 1.43.46 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] (cherry picked from commit 6100a24cb22ba1b98ac93b1284e4d49b62bbb6b9) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index bcc1b86059f..ac945c0496e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -117,7 +117,7 @@ dependencies = [ # storage manager dependencies "django-storages==1.14.3", "google-cloud-storage==2.16.0", - "boto3==1.39.3", + "boto3==1.43.46", # Django Caches "whitenoise==6.6.0", # Contribs From 930acd8bc0f62955c5a03ba87bc9a95c99f5e45b Mon Sep 17 00:00:00 2001 From: George Petrakis Date: Thu, 9 Jul 2026 11:19:46 +0300 Subject: [PATCH 44/98] [Fixes #14406] Introducing a new setting for the URL validation (#14407) * introducing a new setting for the URL validation * adding tests and docs for the new setting (cherry picked from commit c3f53cc63e2b8a51c0a93f51976cbb190c202820) --- .env.sample | 5 +++- docs/src/setup/configuration/settings.md | 19 +++++++++++++ docs/src/user-guide/datasets/remote.md | 3 ++ geonode/settings.py | 3 ++ geonode/tests/test_utils.py | 35 ++++++++++++++++++++++++ geonode/utils.py | 5 ++++ 6 files changed, 69 insertions(+), 1 deletion(-) diff --git a/.env.sample b/.env.sample index 21422dc8bd9..7858837882e 100644 --- a/.env.sample +++ b/.env.sample @@ -248,4 +248,7 @@ DEFAULT_MAX_PARALLEL_UPLOADS_PER_USER=5 # FORCE_READ_ONLY_MODE=False Override the read-only value saved in the configuration # Enable or not the XLSX / XLS upload -XLSX_UPLOAD_ENABLED=False \ No newline at end of file +XLSX_UPLOAD_ENABLED=False + +# List of trusted hosts (format: domain:port) allowed to bypass URL safety validation. +# SAFE_URL_TRUSTED_HOSTS=[] diff --git a/docs/src/setup/configuration/settings.md b/docs/src/setup/configuration/settings.md index a2ec0ae7e32..b62a5615f2f 100644 --- a/docs/src/setup/configuration/settings.md +++ b/docs/src/setup/configuration/settings.md @@ -1391,6 +1391,25 @@ If ``RESOURCE_OWNERSHIP_ADMIN_USERNAME`` is not set, GeoNode defaults to ``admin ## S +**SAFE_URL_TRUSTED_HOSTS** + + +: - Default: ``[]`` + - Env: ``SAFE_URL_TRUSTED_HOSTS`` + +A list of trusted hosts that bypass GeoNode's URL safety validation. The most common use case is allowing connections to remote services hosted on private networks (e.g. corporate intranet, VPN). Hosts resolving to loopback, link-local, multicast or reserved IP addresses, as well as hosts not resolvable via public DNS, are also blocked by default and can be allowed through this setting. + +Hosts must be specified in ``domain:port`` format. Standard ports (``80`` for HTTP, ``443`` for HTTPS) are inferred automatically from the URL scheme, so they must still be listed explicitly in this setting. + +Example: + +```python +SAFE_URL_TRUSTED_HOSTS = ['internal.geoserver.example.com:8443', 'internal.geoserver.example.com:443'] +``` + +!!! warning + Only add hosts you fully trust. + **SEARCH_FILTERS** diff --git a/docs/src/user-guide/datasets/remote.md b/docs/src/user-guide/datasets/remote.md index 6787e607f4a..4b6abbba08a 100644 --- a/docs/src/user-guide/datasets/remote.md +++ b/docs/src/user-guide/datasets/remote.md @@ -29,6 +29,9 @@ Once the service has been configured, you can select the resources you are inter From the page where the services are listed, it is possible to click on the Title of a service, which will open the Service Details page. If you want to import more resources from that service, you can click on the **Import Service Resources** button. +!!! note + To connect to a remote service hosted on a private network, see [`SAFE_URL_TRUSTED_HOSTS`](../../setup/configuration/settings.md#safe_url_trusted_hosts). + ### Remote 3D Tiles The GeoNode client supports visualization of [3D Tiles](https://docs.mapstore.geosolutionsgroup.com/en/latest/user-guide/catalog/#3d-tiles-catalog) thanks to the capabilities fo the MapStore framework on which it is based. 3D Tiles tilesets can be published either from a file upload (.zip file containing the tileset) or by reference of a remotwly published tileset, served over HTTP(S). diff --git a/geonode/settings.py b/geonode/settings.py index ead2bc9cf6c..1aecb4c5d44 100644 --- a/geonode/settings.py +++ b/geonode/settings.py @@ -2280,3 +2280,6 @@ def get_geonode_catalogue_service(): # Enable or not the XLSX / XLS upload XLSX_UPLOAD_ENABLED = ast.literal_eval(os.getenv("XLSX_UPLOAD_ENABLED", "False")) + +# Trusted hosts for bypassing the URL validation e.g internal networks. Use the format : +SAFE_URL_TRUSTED_HOSTS = ast.literal_eval(os.getenv("SAFE_URL_TRUSTED_HOSTS", "[]")) diff --git a/geonode/tests/test_utils.py b/geonode/tests/test_utils.py index 487b21a3390..72711f6f34c 100644 --- a/geonode/tests/test_utils.py +++ b/geonode/tests/test_utils.py @@ -380,3 +380,38 @@ def test_allows_redirect_to_public_domain(self, mock_requests_session): self.assertTrue(result) self.assertIsNone(blocked_url) + + @override_settings(SAFE_URL_TRUSTED_HOSTS=["internal.private.host:9090"]) + def test_trusted_host_with_private_ip_is_allowed(self): + """Hosts in SAFE_URL_TRUSTED_HOSTS should bypass IP validation even if they resolve to a private IP.""" + with patch("geonode.utils.socket.getaddrinfo") as mock_dns: + mock_dns.return_value = [(None, None, None, None, ("192.168.1.100", 0))] + self.assertTrue(is_safe_url("https://internal.private.host:9090/geoserver/ows")) + + @override_settings(SAFE_URL_TRUSTED_HOSTS=["internal.private.host:9090"]) + def test_non_trusted_host_with_private_ip_is_rejected(self): + """Hosts not in SAFE_URL_TRUSTED_HOSTS should still be rejected when resolving to a private IP.""" + with patch("geonode.utils.socket.getaddrinfo") as mock_dns: + mock_dns.return_value = [(None, None, None, None, ("192.168.1.100", 0))] + self.assertFalse(is_safe_url("https://other.private.host:9090/geoserver/ows")) + + @override_settings(SAFE_URL_TRUSTED_HOSTS=["internal.private.host:9090"]) + def test_trusted_host_without_port_is_not_matched(self): + """A trusted host entry with port should not match the same host without a port.""" + with patch("geonode.utils.socket.getaddrinfo") as mock_dns: + mock_dns.return_value = [(None, None, None, None, ("192.168.1.100", 0))] + self.assertFalse(is_safe_url("https://internal.private.host/geoserver/ows")) + + @override_settings(SAFE_URL_TRUSTED_HOSTS=["internal.private.host:443"]) + def test_trusted_host_implicit_default_port_is_matched(self): + """A URL without an explicit port should match a trusted host entry with the default port.""" + with patch("geonode.utils.socket.getaddrinfo") as mock_dns: + mock_dns.return_value = [(None, None, None, None, ("192.168.1.100", 0))] + self.assertTrue(is_safe_url("https://internal.private.host/geoserver/ows")) + + @override_settings(SAFE_URL_TRUSTED_HOSTS=["INTERNAL.PRIVATE.HOST:9090"]) + def test_trusted_host_case_insensitive(self): + """Trusted host matching should be case-insensitive.""" + with patch("geonode.utils.socket.getaddrinfo") as mock_dns: + mock_dns.return_value = [(None, None, None, None, ("192.168.1.100", 0))] + self.assertTrue(is_safe_url("https://internal.private.host:9090/geoserver/ows")) diff --git a/geonode/utils.py b/geonode/utils.py index 572bdfd3e2c..0fa4094e021 100755 --- a/geonode/utils.py +++ b/geonode/utils.py @@ -1817,6 +1817,11 @@ def _resolve_hostname(hostname: str) -> set: return False if not parsed.hostname: return False + _default_ports = {"http": 80, "https": 443} + _port = parsed.port or _default_ports.get(parsed.scheme) + _trusted_hosts = [h.lower() for h in getattr(settings, "SAFE_URL_TRUSTED_HOSTS", [])] + if _port and f"{parsed.hostname}:{_port}" in _trusted_hosts: + return True ips = _resolve_hostname(parsed.hostname) if not ips: return False From 6f239ad4169e9ea37b0e91e4fdfeb6eed1314deb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Jul 2026 00:02:59 +0000 Subject: [PATCH 45/98] build(deps): bump pillow from 12.2.0 to 12.3.0 Bumps [pillow](https://github.com/python-pillow/Pillow) from 12.2.0 to 12.3.0. - [Release notes](https://github.com/python-pillow/Pillow/releases) - [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst) - [Commits](https://github.com/python-pillow/Pillow/compare/12.2.0...12.3.0) --- updated-dependencies: - dependency-name: pillow dependency-version: 12.3.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] (cherry picked from commit df22b7089534c9c4939c581d9da307a93f4e9f68) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index ac945c0496e..89f5fe9d26b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ classifiers = [ # Core dependencies (consolidated from requirements.txt) dependencies = [ - "Pillow==12.2.0", + "Pillow==12.3.0", "lxml==6.1.1", "psycopg2==2.9.12", "Django==5.2.16", From 4d762842d2bd6a0456870c5e5c653d326b0c4bd5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Jul 2026 00:03:08 +0000 Subject: [PATCH 46/98] build(deps): bump pymupdf from 1.27.2.3 to 1.28.0 Bumps [pymupdf](https://github.com/pymupdf/pymupdf) from 1.27.2.3 to 1.28.0. - [Release notes](https://github.com/pymupdf/pymupdf/releases) - [Changelog](https://github.com/pymupdf/PyMuPDF/blob/main/changes.txt) - [Commits](https://github.com/pymupdf/pymupdf/compare/1.27.2.3...1.28.0) --- updated-dependencies: - dependency-name: pymupdf dependency-version: 1.28.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] (cherry picked from commit c457b6f2fe9e60872f4dca4d6c2114f400d2cd31) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 89f5fe9d26b..0de1b9989b8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,7 +43,7 @@ dependencies = [ "schema==0.7.7", "rdflib==6.3.2", "smart_open==7.0.4", - "PyMuPDF==1.27.2.3", + "PyMuPDF==1.28.0", "defusedxml==0.7.1", "zipstream-ng==1.8.0", From 83f74214fa0b850ff99a0577f3c7ec11dc5cf67a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Jun 2026 00:02:32 +0000 Subject: [PATCH 47/98] build(deps): update jwcrypto requirement from >=1.5.7 to >=1.5.8 Updates the requirements on [jwcrypto](https://github.com/latchset/jwcrypto) to permit the latest version. - [Release notes](https://github.com/latchset/jwcrypto/releases) - [Commits](https://github.com/latchset/jwcrypto/compare/v1.5.7...v1.5.8) --- updated-dependencies: - dependency-name: jwcrypto dependency-version: 1.5.8 dependency-type: direct:production ... Signed-off-by: dependabot[bot] (cherry picked from commit 085b8c518a9b98511d451332a12f6cbe72262e72) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 0de1b9989b8..3146630de0a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -156,7 +156,7 @@ dependencies = [ "webdriver_manager==4.0.1", # Security and audit "cryptography==49.0.0", - "jwcrypto>=1.5.7", + "jwcrypto>=1.5.8", # dependency for XLSX handler "python-calamine==0.6.2", "python-magic==0.4.27", From 1987a3b326387eeaff7e2ab37face6706d5185ac Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 15 Jul 2026 09:59:11 +0200 Subject: [PATCH 48/98] [Fixes #14321] Copy with dump, invalid error variable name (#14420) (#14442) (cherry picked from commit 4f0a4b6261174a30821cf80a76a88fd33b25b9fa) Co-authored-by: mattiagiupponi <51856725+mattiagiupponi@users.noreply.github.com> --- geonode/upload/handlers/common/vector.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/geonode/upload/handlers/common/vector.py b/geonode/upload/handlers/common/vector.py index 8b66f0e964a..42bdecdd37e 100644 --- a/geonode/upload/handlers/common/vector.py +++ b/geonode/upload/handlers/common/vector.py @@ -340,7 +340,8 @@ def copy_table_with_ogr2ogr(original_table_name: str, new_table_name: str, db_na env = {**os.environ, "PGPASSWORD": _datastore["PASSWORD"]} psql_cmd = [ "psql", - "-v ON_ERROR_STOP=1", + "-v", + "ON_ERROR_STOP=1", "-d", _datastore["NAME"], "-h", @@ -1790,7 +1791,8 @@ def import_with_ogr2ogr( _datastore = settings.DATABASES["datastore"] psql_cmd = [ "psql", - "-v ON_ERROR_STOP=1", + "-v", + "ON_ERROR_STOP=1", "-d", _datastore["NAME"], "-h", From eb1438a7cd18b2d0bfde8859d5c41f9061896859 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 15 Jul 2026 10:15:15 +0200 Subject: [PATCH 49/98] Improve error handling in upsert (#14432) (#14443) * Log to err upsert exception * fixed msg formatting --------- (cherry picked from commit 8c6a987fab800ab4c3aca41a7d1e05fd20e679f8) Co-authored-by: Giovanni Allegri Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- geonode/upload/handlers/common/vector.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/geonode/upload/handlers/common/vector.py b/geonode/upload/handlers/common/vector.py index 42bdecdd37e..4f9f5c60419 100644 --- a/geonode/upload/handlers/common/vector.py +++ b/geonode/upload/handlers/common/vector.py @@ -1447,10 +1447,11 @@ def _commit_upsert(self, model_obj, OriginalResource, upsert_key, layer_iterator ) chunk_index += 1 except Exception as e: - msg = f"Error occurred during feature save in Batch {chunk_index} with error: {str(e)}" + logger.exception(f"Error occurred during feature save in Batch {chunk_index} ({str(e)})") + msg = f"Error occurred during feature save in Batch {chunk_index}" if exec_obj and layers: self._create_error_log(exec_obj, layers, [{"error": msg}]) - raise UpsertException(msg) + raise UpsertException("An internal error occurred while processing the upsert operation.") return valid_create, valid_update From ebd18b13429231a3faaed8c1ac3bbd466a7472b3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 15 Jul 2026 15:15:47 +0200 Subject: [PATCH 50/98] [Fixes #14437] Fixes keywords lost on style updates (#14441) (cherry picked from commit e8195d354ee28d83124f3a9eea2cc6d9999e199e) Co-authored-by: sijandh35 --- geonode/geoserver/helpers.py | 12 +++++++-- .../commands/sync_geonode_datasets.py | 2 +- .../management/commands/updatelayers.py | 8 ++++++ geonode/geoserver/tests/test_helpers.py | 27 +++++++++++++++++++ 4 files changed, 46 insertions(+), 3 deletions(-) diff --git a/geonode/geoserver/helpers.py b/geonode/geoserver/helpers.py index a165d90bbc7..e2295107faf 100755 --- a/geonode/geoserver/helpers.py +++ b/geonode/geoserver/helpers.py @@ -111,7 +111,8 @@ def _sync_geoserver_keywords_to_instance(instance, keywords): if not keywords: return try: - KeywordHandler(instance=instance, keywords=list(keywords)).set_keywords() + merged_keywords = set(keywords) | set(instance.keyword_list()) + KeywordHandler(instance=instance, keywords=list(merged_keywords)).set_keywords() except Exception: logger.exception(f"Error while importing keywords from GeoServer for dataset {instance.name}") @@ -605,6 +606,7 @@ def gs_slurp( filter=None, skip_unadvertised=False, skip_geonode_registered=False, + skip_keywords=False, remove_deleted=False, permissions=None, execute_signals=False, @@ -763,6 +765,10 @@ def gs_slurp( # recalculate the layer statistics set_attributes_from_geoserver(layer, overwrite=True) + if not skip_keywords: + # import the keywords from GeoServer, merging them with the existing ones + _sync_geoserver_keywords_to_instance(layer, resource.keywords) + # in some cases we need to explicitily save the resource to execute the signals # (for sure when running updatelayers) resolved_resource_manager.update(layer.uuid, instance=layer, notify=execute_signals) @@ -1932,6 +1938,7 @@ def sync_instance_with_geoserver(instance_id, *args, **kwargs): """ updatebbox = kwargs.get("updatebbox", True) updatemetadata = kwargs.get("updatemetadata", True) + importgskeywords = kwargs.get("importgskeywords", False) instance = None try: @@ -1979,7 +1986,8 @@ def sync_instance_with_geoserver(instance_id, *args, **kwargs): instance = instance.fixup_store_type(["alternate", "store", "subtype"], values) if updatemetadata: - _sync_geoserver_keywords_to_instance(instance, gs_resource.keywords) + if importgskeywords: + _sync_geoserver_keywords_to_instance(instance, gs_resource.keywords) # Get metadata links metadata_links = [] diff --git a/geonode/geoserver/management/commands/sync_geonode_datasets.py b/geonode/geoserver/management/commands/sync_geonode_datasets.py index b9f16561de8..c75b2d23ef4 100644 --- a/geonode/geoserver/management/commands/sync_geonode_datasets.py +++ b/geonode/geoserver/management/commands/sync_geonode_datasets.py @@ -75,7 +75,7 @@ def sync_geonode_datasets( sync_instance_with_geoserver(layer.id, updatemetadata=False, updatebbox=True) if updatemetadata: logger.info("Updating metadata...") - sync_instance_with_geoserver(layer.id, updatemetadata=True, updatebbox=False) + sync_instance_with_geoserver(layer.id, updatemetadata=True, updatebbox=False, importgskeywords=True) if removeduplicates: # remove duplicates logger.info("Removing duplicate links...") diff --git a/geonode/geoserver/management/commands/updatelayers.py b/geonode/geoserver/management/commands/updatelayers.py index a00d03bb4ec..ee38e4de1ce 100644 --- a/geonode/geoserver/management/commands/updatelayers.py +++ b/geonode/geoserver/management/commands/updatelayers.py @@ -48,6 +48,12 @@ def add_arguments(self, parser): dest='skip_geonode_registered', default=False, help='Just processing GeoServer layers still not registered in GeoNode.'), + parser.add_argument( + '--skip-keywords', + action='store_true', + dest='skip_keywords', + default=False, + help='Do not import GeoServer keywords into the GeoNode datasets.'), parser.add_argument( '--remove-deleted', action='store_true', @@ -89,6 +95,7 @@ def handle(self, **options): ignore_errors = options.get('ignore_errors') skip_unadvertised = options.get('skip_unadvertised') skip_geonode_registered = options.get('skip_geonode_registered') + skip_keywords = options.get('skip_keywords') remove_deleted = options.get('remove_deleted') verbosity = int(options.get('verbosity')) user = options.get('user') @@ -116,6 +123,7 @@ def handle(self, **options): filter=filter, skip_unadvertised=skip_unadvertised, skip_geonode_registered=skip_geonode_registered, + skip_keywords=skip_keywords, remove_deleted=remove_deleted, permissions=permissions, execute_signals=True) diff --git a/geonode/geoserver/tests/test_helpers.py b/geonode/geoserver/tests/test_helpers.py index 1c32898d6b1..f5a4ac5f879 100644 --- a/geonode/geoserver/tests/test_helpers.py +++ b/geonode/geoserver/tests/test_helpers.py @@ -43,6 +43,7 @@ get_dataset_capabilities_url, get_layer_ows_url, get_time_info, + _sync_geoserver_keywords_to_instance, ) from geonode.geoserver.ows import _wcs_link, _wfs_link, _wms_link from unittest.mock import patch, Mock @@ -360,3 +361,29 @@ def test_get_time_info_no_layer(self, mock_gs_catalog): result = get_time_info(mock_layer) self.assertIsNone(result) + + @on_ogc_backend(geoserver.BACKEND_PACKAGE) + def test_sync_geoserver_keywords_merges_and_does_not_replace(self): + dataset = Dataset.objects.create( + uuid=str(uuid4()), + owner=get_user_model().objects.get(username=self.user), + name="keywords_merge_test", + store="httpfooremoteservce", + subtype="remote", + alternate="geonode:keywords_merge_test", + ) + dataset.keywords.add("keyword_from_geonode", "shared_keyword") + + _sync_geoserver_keywords_to_instance(dataset, ["keyword_from_geoserver", "shared_keyword"]) + + self.assertSetEqual( + {"keyword_from_geonode", "keyword_from_geoserver", "shared_keyword"}, + set(dataset.keyword_list()), + ) + + # an empty keyword list from GeoServer must leave the existing keywords untouched + _sync_geoserver_keywords_to_instance(dataset, []) + self.assertSetEqual( + {"keyword_from_geonode", "keyword_from_geoserver", "shared_keyword"}, + set(dataset.keyword_list()), + ) From d97519a453a781994096c02b5b1311788343ed18 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 23 Jul 2026 10:18:33 +0200 Subject: [PATCH 51/98] [Fixes #14455] XML metadata is not regenerated after updating metadata (#14456) (#14462) (cherry picked from commit ad4d7bfd922ff047bb96da9e2cbe3ae7b7434abb) Co-authored-by: Emanuele Tajariol --- geonode/catalogue/models.py | 60 +++++++++++++++++++++-------------- geonode/metadata/api/views.py | 5 ++- 2 files changed, 41 insertions(+), 24 deletions(-) diff --git a/geonode/catalogue/models.py b/geonode/catalogue/models.py index e575be179e6..c268b82c315 100644 --- a/geonode/catalogue/models.py +++ b/geonode/catalogue/models.py @@ -23,6 +23,7 @@ from django.db.models import signals from lxml import etree from owslib.etree import etree as dlxml +from polymorphic.models import PolymorphicTypeInvalid from geonode.layers.models import Dataset from geonode.documents.models import Document from geonode.catalogue import get_catalogue @@ -41,7 +42,6 @@ def catalogue_pre_delete(instance, sender, **kwargs): def catalogue_post_save(instance, sender, **kwargs): """Get information from catalogue""" _id = instance.resourcebase_ptr.id if hasattr(instance, "resourcebase_ptr") else instance.id - resources = ResourceBase.objects.filter(id=_id) # Update the Catalog try: @@ -49,40 +49,54 @@ def catalogue_post_save(instance, sender, **kwargs): catalogue.create_record(instance) record = catalogue.get_record(instance.uuid) except OSError as err: - msg = f'Could not connect to catalogue to save information for layer "{instance.name}"' if err.errno == errno.ECONNREFUSED: - LOGGER.warn(msg, err) + LOGGER.warning( + f'Could not connect to catalogue to save information for layer "{instance.name}": {err}', exc_info=err + ) return else: - raise err + raise - if not record: - msg = f"Metadata record for {instance.title} does not exist, check the catalogue signals." - LOGGER.warning(msg) - return + if record: + _recreate_links(instance, record) + else: + LOGGER.warning(f"Metadata record for {instance.title} does not exist, check the catalogue signals.") + + ResourceBase.objects.filter(id=_id).update(csw_wkt_geometry=instance.geographic_bounding_box) + update_csw_metadata(instance, catalogue) + +def _recreate_links(instance, record): if not hasattr(record, "links"): msg = f"Metadata record for {instance.title} should contain links." raise Exception(msg) # Create the different metadata links with the available formats - if resources.exists(): - for mime, name, metadata_url in record.links["metadata"]: - try: - Link.objects.get_or_create( - resource=resources.get(), - url=metadata_url, - defaults=dict(name=name, extension="xml", mime=mime, link_type="metadata"), - ) - except Exception: - _d = dict(name=name, extension="xml", mime=mime, link_type="metadata") - Link.objects.filter( - resource=resources.get(), url=metadata_url, extension="xml", link_type="metadata" - ).update(**_d) + for mime, name, metadata_url in record.links["metadata"]: + try: + Link.objects.get_or_create( + resource_id=instance.id, + url=metadata_url, + defaults=dict(name=name, extension="xml", mime=mime, link_type="metadata"), + ) + except Exception: + _d = dict(name=name, extension="xml", mime=mime, link_type="metadata") + Link.objects.filter( + resource_id=instance.id, url=metadata_url, extension="xml", link_type="metadata" + ).update(**_d) + + +def update_csw_metadata(instance, catalogue=None): + if not catalogue: + catalogue = get_catalogue() if instance.metadata_uploaded and instance.metadata_uploaded_preserve: md_doc = etree.tostring(dlxml.fromstring(instance.metadata_xml)) else: + try: + instance = instance.get_real_instance() # get as many attrs as possible from the instance + except PolymorphicTypeInvalid as e: + LOGGER.warning(f"Could not resolve real instance for {instance.title}, using it as-is: {e}") # generate an XML document (GeoNode's default is ISO) raw_xml = catalogue.catalogue.csw_gen_xml(instance, settings.CATALOG_METADATA_TEMPLATE) md_obj = dlxml.fromstring(raw_xml, parser=etree.XMLParser(remove_blank_text=True)) @@ -91,10 +105,10 @@ def catalogue_post_save(instance, sender, **kwargs): try: csw_anytext = catalogue.catalogue.csw_gen_anytext(md_doc) except Exception as e: - LOGGER.exception(e) + LOGGER.exception(f"Error while generating ANYTEXT: {e}", exc_info=e) csw_anytext = "" - resources.update(metadata_xml=md_doc, csw_wkt_geometry=instance.geographic_bounding_box, csw_anytext=csw_anytext) + ResourceBase.objects.filter(pk=instance.id).update(metadata_xml=md_doc, csw_anytext=csw_anytext) if "geonode.catalogue" in settings.INSTALLED_APPS: diff --git a/geonode/metadata/api/views.py b/geonode/metadata/api/views.py index 2c5ae6e8bd0..1b77576b8cf 100644 --- a/geonode/metadata/api/views.py +++ b/geonode/metadata/api/views.py @@ -35,6 +35,7 @@ from geonode.base.models import ResourceBase, ThesaurusKeyword, ThesaurusKeywordLabel, TopicCategory, License from geonode.base.utils import remove_country_from_languagecode from geonode.base.views import LinkedResourcesAutocomplete, RegionAutocomplete, HierarchicalKeywordAutocomplete +from geonode.catalogue.models import update_csw_metadata from geonode.groups.models import GroupProfile from geonode.metadata.handlers.abstract import MetadataHandler from geonode.base.i18n import get_localized_label @@ -122,7 +123,9 @@ def schema_instance(self, request, pk=None): else metadata_manager.update_schema_instance_partial(resource, request.data, request.user, lang) ) resource.refresh_from_db() - resource.save() # we want to trigger all the post_save signals + resource.save() # this is a ResourceBase and won't trigger the catalogue post_save + update_csw_metadata(resource) + except Exception as e: logger.warning(f"Error while updating schema instance: {e}") MetadataHandler._set_error( From 5c3f187861dba94e7df6705b47e444614e993b93 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 23 Jul 2026 10:21:06 +0200 Subject: [PATCH 52/98] [Fixes #13646] Multilang: use multilang entries in ISO19115 (#14421) (#14463) * [Fixes #13646] : Use multilang entries in ISO19115 * Update multilang tags against malformed mappings and duplicate locales * Updated language_info tag to handle different cases * Update multilang template tags * Update LANGUAGE_MAPPINGS settings * Fix catalogue tests * Skip unsupported languages in multilang_values tag * Used single string for a log message (cherry picked from commit c883cbc7b40ddc2a63d3b1f4c73c5c12937c0a80) Co-authored-by: Niraj Adhikari <41701707+nrjadkry@users.noreply.github.com> --- .../templates/catalogue/full_metadata.xml | 43 ++++- geonode/catalogue/templatetags/__init__.py | 0 geonode/catalogue/templatetags/multilang.py | 158 ++++++++++++++++++ geonode/catalogue/tests.py | 123 +++++++++++++- geonode/metadata/multilang/utils.py | 29 ++++ geonode/settings.py | 9 + 6 files changed, 357 insertions(+), 5 deletions(-) create mode 100644 geonode/catalogue/templatetags/__init__.py create mode 100644 geonode/catalogue/templatetags/multilang.py diff --git a/geonode/catalogue/templates/catalogue/full_metadata.xml b/geonode/catalogue/templates/catalogue/full_metadata.xml index d54c45a59a7..0cb7cbd65f5 100644 --- a/geonode/catalogue/templates/catalogue/full_metadata.xml +++ b/geonode/catalogue/templates/catalogue/full_metadata.xml @@ -1,11 +1,14 @@ {% load thesaurus %} {% load l10n %} +{% load multilang %} {{layer.uuid}} + {% language_info layer.language as resource_language %} + {% languages_info metadata as locales %} - {{layer.language}} + {{ resource_language.label }} utf8 @@ -91,6 +94,18 @@ ISO 19115:2003 + {% for locale in locales %} + + + + {{ locale.label }} + + + {{ locale.encoding }} + + + + {% endfor %} @@ -114,8 +129,18 @@ - + {% multilang_values "title" metadata as title_translations %} + {{layer.title}} + {% if title_translations %} + + {% for t in title_translations %} + + {{ t.text }} + + {% endfor %} + + {% endif %} {% if layer.alternate %} @@ -149,8 +174,18 @@ - + {% multilang_values "abstract" metadata as abstract_translations %} + {{layer.raw_abstract}} + {% if abstract_translations %} + + {% for t in abstract_translations %} + + {{ t.text }} + + {% endfor %} + + {% endif %} {% if layer.raw_purpose %}{{layer.raw_purpose}}{% endif %} @@ -370,7 +405,7 @@ {% endif %} - {{layer.language}} + {{ resource_language.label }} utf8 diff --git a/geonode/catalogue/templatetags/__init__.py b/geonode/catalogue/templatetags/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/geonode/catalogue/templatetags/multilang.py b/geonode/catalogue/templatetags/multilang.py new file mode 100644 index 00000000000..ba471ee79a4 --- /dev/null +++ b/geonode/catalogue/templatetags/multilang.py @@ -0,0 +1,158 @@ +######################################################################### +# +# Copyright (C) 2026 OSGeo +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +######################################################################### + +import logging + +from django import template +from django.conf import settings + +from geonode.metadata.multilang.utils import ( + get_default_language, + get_3_from_2, + get_2_from_3, + get_multilang_field_names, + get_all_multilang_fields, +) + +register = template.Library() +logger = logging.getLogger(__name__) + + +def _language_label(language_code_2, fallback): + """ + Return the display label for a configured language. + """ + language_labels = {code.split("-")[0]: label for code, label in settings.LANGUAGES} + + return language_labels.get(language_code_2, fallback) + + +def _language_descriptor(lang_2, lang_3): + """Build the language descriptor used by the ISO template.""" + return { + "id": f"locale-{lang_2}", + "iso639_2": lang_3, + "label": _language_label(lang_2, lang_3), + "encoding": "utf8", + } + + +@register.filter(name="is_multilang") +def is_multilang(field_name): + """Return whether the field is configured as multilingual.""" + return field_name in getattr(settings, "MULTILANG_FIELDS", ()) + + +@register.simple_tag(name="multilang_values") +def multilang_values(field_name, metadata): + """ + Return all localized values for a multilingual field. + """ + if not isinstance(metadata, dict) or not is_multilang(field_name): + return [] + + values = [] + + for language_code, translated_field in get_multilang_field_names(field_name): + translated_text = metadata.get(translated_field) + if not translated_text: + continue + + if get_3_from_2(language_code) is None: + logger.warning( + "No entry in LANGUAGE_MAPPINGS for language '%s'; skipping localized value for field '%s'.", + language_code, + field_name, + ) + continue + + values.append( + { + "locale": language_code, + "text": translated_text, + } + ) + + return values + + +@register.simple_tag(name="language_info") +def language_info(lang_3): + """ + Return information about a single ISO639-2 language. + """ + if not lang_3: + lang_2 = get_default_language() + lang_3 = get_3_from_2(lang_2) + if lang_3 is None: + logger.warning( + "No ISO639-2 language found for '%s'; using 'eng' as the default.", + lang_2, + ) + lang_3 = "und" + elif len(lang_3) == 2: + lang_2 = lang_3 + lang_3 = get_3_from_2(lang_2) or "und" + else: + lang_2 = get_2_from_3(lang_3) + + if lang_2 is None: + logger.warning( + "No ISO639-1 language found for '%s'; using the ISO639-2 code as the label.", + lang_3, + ) + + return _language_descriptor( + lang_2, + lang_3, + ) + + +@register.simple_tag(name="languages_info") +def languages_info(metadata, fields=None): + """ + Return descriptors for all translated languages present in the metadata. + """ + if not isinstance(metadata, dict): + return [] + + multilingual_fields = set(fields or settings.MULTILANG_FIELDS) + + languages = set() + + for (field_name, language_code), multilang_field_name in get_all_multilang_fields().items(): + if field_name not in multilingual_fields or not metadata.get(multilang_field_name): + continue + + languages.add(language_code) + + descriptors = [] + for language_code in sorted(languages): + lang_3 = get_3_from_2(language_code) + + if lang_3 is None: + logger.warning( + "No entry in LANGUAGE_MAPPINGS for language '%s'; skipping this entry.", + language_code, + ) + continue + + descriptors.append(_language_descriptor(language_code, lang_3)) + + return descriptors diff --git a/geonode/catalogue/tests.py b/geonode/catalogue/tests.py index 139b2104553..b0a3e69e4de 100644 --- a/geonode/catalogue/tests.py +++ b/geonode/catalogue/tests.py @@ -21,11 +21,12 @@ from django.db.models import Q from django.template.loader import get_template -from django.test import RequestFactory +from django.test import RequestFactory, override_settings from django.http.response import Http404 from django.core.exceptions import PermissionDenied from geonode.layers.models import Dataset from geonode.catalogue import get_catalogue +from geonode.catalogue.templatetags import multilang as tags from django.contrib.auth import get_user_model from django.contrib.auth.models import AnonymousUser @@ -208,3 +209,123 @@ def test_localized_keyword_in_metadata(self): rendered_xml = template.render(context) self.assertIn(self.keyword_label.label, rendered_xml) self.assertNotIn(self.keyword.alt_label, rendered_xml) + + +@override_settings(MULTILANG_FIELDS=["title", "abstract"]) +class IsMultilangFilterTest(GeoNodeBaseTestSupport): + + def test_configured_field(self): + self.assertTrue(tags.is_multilang("title")) + self.assertTrue(tags.is_multilang("abstract")) + + def test_unconfigured_field(self): + self.assertFalse(tags.is_multilang("purpose")) + + @override_settings(MULTILANG_FIELDS=()) + def test_feature_off(self): + self.assertFalse(tags.is_multilang("title")) + + +@override_settings(MULTILANG_FIELDS=["title", "abstract"]) +class MultilangValuesTagTests(GeoNodeBaseTestSupport): + + def test_field_not_configured(self): + metadata = {"purpose": "x", "purpose_multilang_it": "scopo"} + self.assertEqual([], tags.multilang_values("purpose", metadata)) + + def test_configured_field_no_translations(self): + self.assertEqual([], tags.multilang_values("title", {"title": "Hello"})) + + def test_excludes_empty_string_translation(self): + metadata = { + "title": "Hello", + "title_multilang_de": "", + "title_multilang_it": "Ciao", + } + self.assertEqual([{"locale": "it", "text": "Ciao"}], tags.multilang_values("title", metadata)) + + def test_multiple_translations(self): + metadata = { + "title": "Hello", + "title_multilang_de": "Hallo", + "title_multilang_es": "Hola", + "title_multilang_fr": "Bonjour", + "title_multilang_it": "Ciao", + } + result = {t["locale"]: t["text"] for t in tags.multilang_values("title", metadata)} + self.assertEqual({"de": "Hallo", "es": "Hola", "fr": "Bonjour", "it": "Ciao"}, result) + + def test_empty_or_missing_metadata(self): + self.assertEqual([], tags.multilang_values("title", {})) + self.assertEqual([], tags.multilang_values("title", None)) + + +@override_settings(MULTILANG_FIELDS=["title", "abstract"]) +class LanguageTemplateTagTests(GeoNodeBaseTestSupport): + + def test_language_info_known_code(self): + result = tags.language_info("ita") + self.assertEqual( + { + "id": "locale-it", + "iso639_2": "ita", + "label": "Italiano", + "encoding": "utf8", + }, + result, + ) + + def test_language_info_code_with_bibliographic_terminological_variant(self): + result = tags.language_info("fra") + self.assertEqual("locale-fr", result["id"]) + self.assertEqual("fra", result["iso639_2"]) + self.assertEqual("Français", result["label"]) + + def test_languages_info_empty_or_missing_metadata(self): + self.assertEqual([], tags.languages_info({})) + self.assertEqual([], tags.languages_info(None)) + + def test_languages_info_no_translations(self): + metadata = {"title": "Hello", "abstract": "An abstract"} + self.assertEqual([], tags.languages_info(metadata)) + + def test_languages_info_across_fields(self): + metadata = { + "title": "Hello", + "title_multilang_it": "Ciao", + "title_multilang_de": "Hallo", + "abstract": "An abstract", + "abstract_multilang_fr": "Un résumé", + "abstract_multilang_de": "Eine Zusammenfassung", + } + result = {d["id"]: d for d in tags.languages_info(metadata)} + self.assertEqual({"locale-it", "locale-de", "locale-fr"}, set(result.keys())) + self.assertEqual( + { + "id": "locale-it", + "iso639_2": "ita", + "label": "Italiano", + "encoding": "utf8", + }, + result["locale-it"], + ) + + def test_languages_info_respects_explicit_fields_argument(self): + metadata = { + "title": "Hello", + "title_multilang_it": "Ciao", + "abstract": "x", + "abstract_multilang_fr": "Un résumé", + } + result = tags.languages_info(metadata, fields=["title"]) + self.assertEqual(["locale-it"], [d["id"] for d in result]) + + @override_settings(LANGUAGE_MAPPINGS=(("en", "eng"), ("it", "ita"))) + def test_languages_info_missing_iso_mapping_is_skipped_and_warns(self): + metadata = { + "title": "Hello", + "title_multilang_it": "Ciao", + "title_multilang_de": "Hallo", + } + result = tags.languages_info(metadata) + self.assertEqual(["locale-it"], [d["id"] for d in result]) diff --git a/geonode/metadata/multilang/utils.py b/geonode/metadata/multilang/utils.py index d92b05d7490..61f1b25cd86 100644 --- a/geonode/metadata/multilang/utils.py +++ b/geonode/metadata/multilang/utils.py @@ -59,3 +59,32 @@ def get_all_multilang_fields(): for field in settings.MULTILANG_FIELDS for lang in get_2letters_languages() } + + +def get_3_from_2(lang_2): + """ + Return the preferred ISO 639-2 (3-letter) code for an ISO 639-1 code. + """ + mappings = getattr(settings, "LANGUAGE_MAPPINGS", ()) + if not isinstance(mappings, dict): + mappings = dict(mappings) + + code = mappings.get(lang_2) + if isinstance(code, (list, tuple)): + return code[0] if code else None + return code + + +def get_2_from_3(lang_3): + """ + Return the ISO 639-1 (2-letter) code for a given ISO 639-2 (3-letter) code. + """ + mappings = getattr(settings, "LANGUAGE_MAPPINGS", ()) + if isinstance(mappings, dict): + mappings = mappings.items() + + for lang_2, lang_3_codes in mappings: + codes = lang_3_codes if isinstance(lang_3_codes, (list, tuple)) else (lang_3_codes,) + if lang_3 in codes: + return lang_2 + return None diff --git a/geonode/settings.py b/geonode/settings.py index 1aecb4c5d44..5d9cef9b746 100644 --- a/geonode/settings.py +++ b/geonode/settings.py @@ -2148,6 +2148,15 @@ def get_geonode_catalogue_service(): "all": ["title", "abstract", "supplemental_information"], } +# Maps ISO 639-1 (2-letter) codes to ISO 639-2 (3-letter) code(s) +LANGUAGE_MAPPINGS = ( + ("en", "eng"), + ("de", ["ger", "deu"]), + ("es", "spa"), + ("fr", ["fre", "fra"]), + ("it", "ita"), +) + # you can get the language names in psql using "\dF" MULTILANG_POSTGRES_LANGS = { None: "simple", From f8e70773417cde9d7368dcb54ab002b01bb0ca3b Mon Sep 17 00:00:00 2001 From: Sijan Dhungana Date: Thu, 30 Jul 2026 20:50:50 +0545 Subject: [PATCH 53/98] [Backport PR #14402 to 5.1.x] Moving the logic of PROFILE_LANGUAGE_CHOICES to a helper function (#14478) * moving the logic of PROFILE_LANGUAGE_CHOICES to a helper function (cherry picked from commit 79f1616c49793dee01bd6d97c1956e7a0b976a4c) * cover the case of duplicated languages (cherry picked from commit 6858982d9cef264a1b5ddad889bd926aea3dce0d) --------- Co-authored-by: gpetrak --- geonode/people/forms/__init__.py | 5 +++-- geonode/people/tests.py | 4 ---- geonode/people/utils.py | 8 ++++++++ geonode/people/views.py | 4 ++-- geonode/settings.py | 3 --- 5 files changed, 13 insertions(+), 11 deletions(-) diff --git a/geonode/people/forms/__init__.py b/geonode/people/forms/__init__.py index 9e7fefb9e14..8b9a17306d7 100644 --- a/geonode/people/forms/__init__.py +++ b/geonode/people/forms/__init__.py @@ -23,7 +23,8 @@ from django.contrib.auth import get_user_model from django.contrib.auth.forms import UserCreationForm, UserChangeForm from django.utils.translation import gettext_lazy as _ -from django.conf import settings + +from geonode.people.utils import get_profile_language_choices # Ported in from django-registration attrs_dict = {"class": "required"} @@ -52,7 +53,7 @@ class ProfileForm(forms.ModelForm): language = forms.ChoiceField( label=_("Language"), - choices=settings.PROFILE_LANGUAGE_CHOICES, + choices=get_profile_language_choices, ) class Meta: diff --git a/geonode/people/tests.py b/geonode/people/tests.py index 9de1027e568..6fc29ae6068 100644 --- a/geonode/people/tests.py +++ b/geonode/people/tests.py @@ -1361,10 +1361,6 @@ def test_authenticated_user_language_switch_updates_session_and_db(self): ("en-us", "English"), ("it-it", "Italiano"), ), - PROFILE_LANGUAGE_CHOICES=( - ("en", "English"), - ("it", "Italiano"), - ), ) def test_authenticated_user_language_switch_stores_profile_code_from_runtime_code(self): user = get_user_model().objects.get(username="bobby") diff --git a/geonode/people/utils.py b/geonode/people/utils.py index 8a4ed64a07f..449b4ca5bec 100644 --- a/geonode/people/utils.py +++ b/geonode/people/utils.py @@ -246,3 +246,11 @@ def runtime_to_profile_lang(lang): return None return lang.lower().split("-")[0] + + +def get_profile_language_choices(): + """ + Build the list of DB-based (2-char) language choices from the currently + configured settings.LANGUAGES + """ + return tuple({code.split("-")[0].lower(): label for code, label in settings.LANGUAGES}.items()) diff --git a/geonode/people/views.py b/geonode/people/views.py index 476d2606473..6500c009fe8 100644 --- a/geonode/people/views.py +++ b/geonode/people/views.py @@ -33,7 +33,7 @@ from geonode.tasks.tasks import send_email from geonode.people.forms import ProfileForm -from geonode.people.utils import get_available_users, runtime_to_profile_lang +from geonode.people.utils import get_available_users, get_profile_language_choices, runtime_to_profile_lang from geonode.base.auth import get_or_create_token from geonode.people.forms import ForgotUsernameForm from geonode.base.views import user_and_group_permission @@ -178,7 +178,7 @@ def set_session_language(request): raw_lang = request.POST.get("language") profile_lang = runtime_to_profile_lang(raw_lang) - if profile_lang in {code for code, _label in settings.PROFILE_LANGUAGE_CHOICES}: + if profile_lang in {code for code, _label in get_profile_language_choices()}: request.session[SESSION_LANG_KEY] = profile_lang is_read_only = Configuration.load().read_only diff --git a/geonode/settings.py b/geonode/settings.py index 5d9cef9b746..1c61d67699a 100644 --- a/geonode/settings.py +++ b/geonode/settings.py @@ -1436,9 +1436,6 @@ def get_geonode_catalogue_service(): else: LANGUAGES = MAPSTORE_DEFAULT_LANGUAGES - # This setting includes supported Maptstore language choices in a DB-based format - PROFILE_LANGUAGE_CHOICES = tuple((code.split("-")[0].lower(), label) for code, label in LANGUAGES) - # The default mapstore client compiles the translations json files in the /static/mapstore directory # gn-translations are the custom translations for the client and ms-translations are the translations from the core framework MAPSTORE_TRANSLATIONS_PATH = os.environ.get( From 0086d549a3ab9eaa180293e8b0d5f78f58f8bc7a Mon Sep 17 00:00:00 2001 From: Niraj Adhikari <41701707+nrjadkry@users.noreply.github.com> Date: Fri, 7 Aug 2026 14:31:51 +0545 Subject: [PATCH 54/98] [Fixes #14484] Created a m2m signal to sync group members (#14485) (#14486) (cherry picked from commit 327dffef37777c440d2cb54efc14c911e2d3637d) --- geonode/groups/models.py | 8 +++++--- geonode/groups/tests.py | 38 ++++++++++++++++++++++++++++++++++++++ geonode/people/models.py | 2 ++ geonode/people/signals.py | 21 +++++++++++++++++++++ 4 files changed, 66 insertions(+), 3 deletions(-) diff --git a/geonode/groups/models.py b/geonode/groups/models.py index a4cddca47db..8c66ca61bbc 100644 --- a/geonode/groups/models.py +++ b/geonode/groups/models.py @@ -276,14 +276,16 @@ class GroupMember(models.Model): joined = models.DateTimeField(default=now) def save(self, *args, **kwargs): - # add django.contrib.auth.group to user - self.user.groups.add(self.group.group) super().save(*args, **kwargs) + # add django.contrib.auth.group to user + if not self.user.groups.filter(id=self.group.group.id).exists(): + self.user.groups.add(self.group.group) self._handle_perms(role=self.role) def delete(self, *args, **kwargs): - self.user.groups.remove(self.group.group) super().delete(*args, **kwargs) + if self.user.groups.filter(id=self.group.group.id).exists(): + self.user.groups.remove(self.group.group) self._handle_perms() def promote(self, *args, **kwargs): diff --git a/geonode/groups/tests.py b/geonode/groups/tests.py index 9e58f6bf3ab..337c188055c 100644 --- a/geonode/groups/tests.py +++ b/geonode/groups/tests.py @@ -807,6 +807,44 @@ def test_group_categories_add(self): self.assertEqual(q.count(), 1) self.assertTrue(q.get().slug) + def test_adding_user_to_auth_group_creates_member(self): + + # Ensure they are not a member initially + self.assertFalse(self.bar.user_is_member(self.test_user)) + + # Add via the standard Django auth.Group + self.test_user.groups.add(self.bar.group) + + member_exists = GroupMember.objects.filter(user=self.test_user, group=self.bar).exists() + self.assertTrue(member_exists) + + def test_removing_user_from_auth_group_deletes_member(self): + self.test_user.groups.add(self.bar.group) + self.assertTrue(self.bar.user_is_member(self.test_user)) + + # Remove via the standard Django auth.Group + self.test_user.groups.remove(self.bar.group) + member_exists = GroupMember.objects.filter(user=self.test_user, group=self.bar).exists() + self.assertFalse(member_exists) + + def test_join_syncs_to_auth_group(self): + self.bar.join(self.test_user, role=GroupMember.MEMBER) + + # Verify the user was added to standard Django auth.Group + in_auth_group = self.test_user.groups.filter(id=self.bar.group.id).exists() + self.assertTrue(in_auth_group) + + def test_leave_syncs_to_auth_group(self): + + self.bar.join(self.test_user, role=GroupMember.MEMBER) + self.assertTrue(self.test_user.groups.filter(id=self.bar.group.id).exists()) + + self.bar.leave(self.test_user) + + # Verify the user was removed from standard Django auth.Group + in_auth_group = self.test_user.groups.filter(id=self.bar.group.id).exists() + self.assertFalse(in_auth_group) + class UserAutocompleteTest(GeoNodeBaseTestSupport): def setUp(self): diff --git a/geonode/people/models.py b/geonode/people/models.py index 029dc4e704d..b3123b20995 100644 --- a/geonode/people/models.py +++ b/geonode/people/models.py @@ -48,6 +48,7 @@ do_login, do_logout, profile_post_save, + sync_group_members, update_user_email_addresses, notify_admins_new_signup, clear_user_resource_permissions_cache_on_delete, @@ -286,3 +287,4 @@ def get_anonymous_user_instance(user_model): user_signed_up.connect(notify_admins_new_signup, dispatch_uid=str(uuid4()), weak=False) signals.post_save.connect(profile_post_save, sender=settings.AUTH_USER_MODEL) signals.pre_delete.connect(clear_user_resource_permissions_cache_on_delete, sender=settings.AUTH_USER_MODEL) +signals.m2m_changed.connect(sync_group_members, sender=Profile.groups.through) diff --git a/geonode/people/signals.py b/geonode/people/signals.py index cffae8a1fa5..dca049ffcfb 100644 --- a/geonode/people/signals.py +++ b/geonode/people/signals.py @@ -137,3 +137,24 @@ def clear_user_resource_permissions_cache_on_delete(sender, instance, **kwargs): instance=instance, group_clear_cache=False, ) + + +def sync_group_members(sender, instance, action, reverse, **kwargs): + if reverse or action not in ("post_add", "post_remove", "post_clear"): + return + + from geonode.groups.models import GroupProfile, GroupMember + + user_groups_ids = set(instance.groups.values_list("id", flat=True)) + group_profiles = GroupProfile.objects.filter(group_id__in=user_groups_ids) + + for group_profile in group_profiles: + GroupMember.objects.get_or_create( + group=group_profile, + user=instance, + defaults={"role": GroupMember.MEMBER}, + ) + + stale_members = GroupMember.objects.filter(user=instance).exclude(group__group_id__in=user_groups_ids) + for member in stale_members: + member.delete() From 058283f7db435bddea0a309b2446a02dfa05445d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 12 Aug 2026 15:01:50 +0200 Subject: [PATCH 55/98] Removed redundant code for merging files from assets and links. (#14500) (#14502) Removed redundant code for merging files from assets and links. (cherry picked from commit 365bb46ee95376bc83caa86612e4c098f14939eb) Co-authored-by: mattiagiupponi <51856725+mattiagiupponi@users.noreply.github.com> --- geonode/resource/manager.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/geonode/resource/manager.py b/geonode/resource/manager.py index 120d77e94d5..2e6fa8e095b 100644 --- a/geonode/resource/manager.py +++ b/geonode/resource/manager.py @@ -547,15 +547,6 @@ def copy( if files: to_update = {"files": files} - assets_and_links = copy_assets_and_links(instance, target=_resource) - # we're just merging all the files together: it won't work once we have multiple assets per resource - # TODO: get the files from the proper Asset, or make the _concrete_resource_manager.copy use assets - to_update = {} - - files = list(itertools.chain.from_iterable([asset.location for asset, _ in assets_and_links])) - if files: - to_update = {"files": files} - except Exception as e: logger.exception(e) _resource = None From e017772d958239964e3cb3043a807dee17cd61ad Mon Sep 17 00:00:00 2001 From: Giovanni Allegri Date: Wed, 12 Aug 2026 17:51:45 +0200 Subject: [PATCH 56/98] Upload size validation (#14507) (cherry picked from commit 6de48d6923207bacb0456372d84d526285cbc034) --- geonode/upload/api/tests.py | 24 +++++++++++++ geonode/upload/api/views.py | 71 ++++++++++++++++++++----------------- geonode/upload/utils.py | 10 ++++-- 3 files changed, 71 insertions(+), 34 deletions(-) diff --git a/geonode/upload/api/tests.py b/geonode/upload/api/tests.py index 81b7701aad0..109b21fcaae 100644 --- a/geonode/upload/api/tests.py +++ b/geonode/upload/api/tests.py @@ -33,6 +33,7 @@ from geonode.upload.models import ResourceHandlerInfo from geonode.upload.tests.utils import ImporterBaseTestSupport +from geonode.upload.utils import UploadLimitValidator def _build_gpkg_header(): @@ -329,6 +330,29 @@ def test_zip_file_is_unzip_and_the_handler_is_found(self, patch_upload): self.assertEqual(201, response.status_code) + def test_total_upload_size_counts_zip_file_of_different_size(self): + validator = UploadLimitValidator(user=None) + files = {"base_file": MagicMock(size=10), "zip_file": MagicMock(size=1000)} + self.assertEqual(1010, validator._get_uploaded_files_total_size(files)) + + def test_total_upload_size_ignores_zip_file_matching_base_file(self): + validator = UploadLimitValidator(user=None) + files = {"base_file": MagicMock(size=500), "zip_file": MagicMock(size=500)} + self.assertEqual(500, validator._get_uploaded_files_total_size(files)) + + @patch("geonode.storage.manager.StorageManager.delete_retrieved_paths") + def test_cloned_files_removed_when_no_handler_is_found(self, mock_delete): + self.client.force_login(get_user_model().objects.get(username="admin")) + payload = { + "base_file": open(f"{project_dir}/tests/fixture/valid.zip", "rb"), + "action": "upload", + } + + response = self.client.post(self.url, data=payload) + + self.assertNotEqual(201, response.status_code) + mock_delete.assert_called_with(force=True) + @override_settings(SAFE_URL_CHECK_ENABLED=True) def test_remote_upload_rejects_unsafe_url(self): self.client.force_login(get_user_model().objects.get(username="admin")) diff --git a/geonode/upload/api/views.py b/geonode/upload/api/views.py index 6bc62b0f11c..a6e143a3a51 100644 --- a/geonode/upload/api/views.py +++ b/geonode/upload/api/views.py @@ -32,7 +32,7 @@ ResourceBasePermissionsFilter, UserHasPerms, ) -from rest_framework.exceptions import ValidationError +from rest_framework.exceptions import ValidationError, APIException from rest_framework import status from geonode.base.api.serializers import ResourceBaseSerializer from geonode.base.api.views import ResourceBaseViewSet @@ -155,24 +155,24 @@ def create(self, request, *args, **kwargs): if "url" in _data: check_add_remote_resource_perm(request.user) - # clone the memory files into local file system - if "url" not in _data and not _data.get("is_empty", False): - storage_manager = StorageManager( - remote_files={k: v for k, v in _data.items() if k.endswith("_file")}, - concrete_storage_manager=FileSystemStorageManager(), - ) - storage_manager.clone_remote_files(create_tempdir=True, unzip=False) - # validate the upload - self.validate_upload(request, storage_manager) - # merging the new local path with the input payload - _data = _data | storage_manager.get_retrieved_paths() - # checking the correct handler for the uploaded files - handler = orchestrator.get_handler(_data) - # not file but handler means that is a remote resource - action = _data.get("action") - - if handler and handler.can_do(action): - try: + try: + # clone the memory files into local file system + if "url" not in _data and not _data.get("is_empty", False): + storage_manager = StorageManager( + remote_files={k: v for k, v in _data.items() if k.endswith("_file")}, + concrete_storage_manager=FileSystemStorageManager(), + ) + storage_manager.clone_remote_files(create_tempdir=True, unzip=False) + # validate the upload + self.validate_upload(request, storage_manager) + # merging the new local path with the input payload + _data = _data | storage_manager.get_retrieved_paths() + # checking the correct handler for the uploaded files + handler = orchestrator.get_handler(_data) + # not file but handler means that is a remote resource + action = _data.get("action") + + if handler and handler.can_do(action): extracted_params, _files = handler.extract_params_from_data(_data) if "url" in extracted_params: # we should register the hosts for the proxy @@ -198,21 +198,28 @@ def create(self, request, *args, **kwargs): sig = import_orchestrator.s(_data, str(execution_id), handler=str(handler), action=action) sig.apply_async() return Response(data={"execution_id": execution_id}, status=201) - except Exception as e: - # in case of any exception, is better to delete the - # cloned files to keep the storage under control - if storage_manager: - try: - storage_manager.delete_retrieved_paths(force=True) - except Exception as _exc: - logger.warning(_exc) - if execution_id: - orchestrator.set_as_failed(execution_id=str(execution_id), reason=e) - logger.exception(e) - raise ImportException(detail=e.args[0] if len(e.args) > 0 else e) - + except Exception as e: + # in case of any exception, is better to delete the + # cloned files to keep the storage under control + self._cleanup_cloned_files(storage_manager) + if execution_id: + orchestrator.set_as_failed(execution_id=str(execution_id), reason=e) + logger.exception(e) + if isinstance(e, APIException): + raise + raise ImportException(detail=_("An internal error occurred while processing the upload.")) + + self._cleanup_cloned_files(storage_manager) raise ImportException(detail="No handlers found for this dataset type/action") + def _cleanup_cloned_files(self, storage_manager): + """Remove the locally cloned upload files""" + if storage_manager: + try: + storage_manager.delete_retrieved_paths(force=True) + except Exception as _exc: + logger.warning(_exc) + def validate_upload(self, request, storage_manager): upload_validator = UploadLimitValidator(request.user) upload_validator.validate_parallelism_limit_per_user() diff --git a/geonode/upload/utils.py b/geonode/upload/utils.py index 62679a8edd9..ce8f26f3c00 100644 --- a/geonode/upload/utils.py +++ b/geonode/upload/utils.py @@ -254,14 +254,20 @@ def _get_uploaded_files(self): return [django_file for field_name, django_file in self.files.items() if field_name != "base_file"] def _get_uploaded_files_total_size(self, file_dict): - """Return a list with all of the uploaded files""" + """Return the total size of the uploaded files.""" + # excluded from the count only when their size matches base_file, to + # avoid counting the same file twice. excluded_files = ( "zip_file", "shp_file", ) _iterate_files = file_dict.data_items if hasattr(file_dict, "data_items") else file_dict + base_file = _iterate_files.get("base_file") + base_size = base_file.size if base_file else None uploaded_files_sizes = [ - file_obj.size for field_name, file_obj in _iterate_files.items() if field_name not in excluded_files + file_obj.size + for field_name, file_obj in _iterate_files.items() + if field_name not in excluded_files or file_obj.size != base_size ] total_size = sum(uploaded_files_sizes) return total_size From 4bdd1266878237f12a3fd7d0ea2a517b78c0fc62 Mon Sep 17 00:00:00 2001 From: sijandh35 Date: Wed, 22 Jul 2026 09:48:51 +0000 Subject: [PATCH 57/98] correct timeseries endpoint check (cherry picked from commit 99defd24082b52ad1d7adfe67e6e18c561f7214d) --- geonode/layers/api/tests.py | 14 ++++++++++++++ geonode/layers/api/views.py | 7 +++++++ 2 files changed, 21 insertions(+) diff --git a/geonode/layers/api/tests.py b/geonode/layers/api/tests.py index 27c6992c3bf..3c71f320c06 100644 --- a/geonode/layers/api/tests.py +++ b/geonode/layers/api/tests.py @@ -651,6 +651,20 @@ def test_permissions_for_permitted_user(self): response = self.client.put(url, data=put_data) self.assertEqual(200, response.status_code) + def test_timeseries_update_blocked_for_view_only_user(self): + """A user with only view rights must not be able to edit the time configuration.""" + view_only_user = get_user_model().objects.create_user( + username="timeseries_viewer", + password="timeseries_viewer_password", + ) + layer = Dataset.objects.first() + assign_perm("view_resourcebase", view_only_user, layer.get_self_resource()) + url = reverse("datasets-timeseries", args=(layer.id,)) + self.client.login(username="timeseries_viewer", password="timeseries_viewer_password") + + response = self.client.put(url, data={"has_time": False}, format="json") + self.assertEqual(403, response.status_code) + def test_valid_metadata_file(self): layer = Dataset.objects.first() url = reverse("datasets-replace-metadata", args=(layer.id,)) diff --git a/geonode/layers/api/views.py b/geonode/layers/api/views.py index 31b3e44fd5e..d04d86d103a 100644 --- a/geonode/layers/api/views.py +++ b/geonode/layers/api/views.py @@ -207,6 +207,13 @@ def timeseries_info(self, request, pk, *args, **kwards): """ layer = get_object_or_404(Dataset, id=pk) + access = _to_compact_perms(permissions_registry.get_perms(instance=layer, user=request.user)) + if request.method == "GET": + if access not in ("owner", "edit", "manage", "view", "download"): + raise PermissionDenied("You do not have permission to view this dataset") + else: # PUT + if access not in ("owner", "edit", "manage"): + raise PermissionDenied("You do not have permission to edit this dataset") if layer.supports_time is False: return JsonResponse({"message": "The time dimension is not supported for this dataset."}, status=200) From 0f1bf9cff359f89ae21fe1b919ab6e886bac2175 Mon Sep 17 00:00:00 2001 From: Giovanni Allegri Date: Thu, 13 Aug 2026 09:35:51 +0200 Subject: [PATCH 58/98] Xml upload download improvements (#14505) (cherry picked from commit cf5922a404cd61a65b59fc97e622b45681e6529e) --- geonode/assets/local.py | 15 +++++- geonode/base/api/views.py | 12 +++++ geonode/documents/forms.py | 16 ++++++ geonode/documents/tests.py | 8 +++ geonode/layers/metadata.py | 7 ++- geonode/tests/test_utils.py | 28 +++++++++- geonode/upload/api/serializer.py | 23 ++++++++ geonode/upload/api/tests.py | 57 ++++++++++++++++++++ geonode/upload/handlers/sld/handler.py | 9 +++- geonode/upload/handlers/xml/handler.py | 9 +++- geonode/utils.py | 74 ++++++++++++++++++++++++++ 11 files changed, 249 insertions(+), 9 deletions(-) diff --git a/geonode/assets/local.py b/geonode/assets/local.py index 6bfbb347e4e..d8e62deb3ce 100644 --- a/geonode/assets/local.py +++ b/geonode/assets/local.py @@ -22,6 +22,9 @@ concrete_storage_manager=FileSystemStorageManager(location=os.path.dirname(settings.ASSETS_ROOT)) ) +# extensions served as an attachment (download) instead of rendered inline +FORCE_DOWNLOAD_EXTENSIONS = {"xml", "sld"} + class DefaultLocalLinkUrlHandler: def get_link_url(self, asset: LocalAsset): @@ -303,9 +306,17 @@ def create_response( ) case False: logger.info(f"Returning file '{localfile}' with name '{outname}'") - return DownloadResponse( - _asset_storage_manager.open(localfile).file, basename=f"{outname}", attachment=False + force_download = ext.lower().lstrip(".") in FORCE_DOWNLOAD_EXTENSIONS + response = DownloadResponse( + _asset_storage_manager.open(localfile).file, + basename=f"{outname}", + attachment=force_download, + ) + response.headers["X-Content-Type-Options"] = "nosniff" + response.headers["Content-Security-Policy"] = ( + "default-src 'none'; style-src 'unsafe-inline'; sandbox" ) + return response else: logger.warning(f"Internal file {localfile} not found for asset {asset.id}") return HttpResponse(f"Internal file not found for asset {asset.id}", status=404 if path else 500) diff --git a/geonode/base/api/views.py b/geonode/base/api/views.py index cf6c6b839da..d334bb4df42 100644 --- a/geonode/base/api/views.py +++ b/geonode/base/api/views.py @@ -112,6 +112,7 @@ from geonode.assets.utils import create_asset_and_link, unlink_asset from geonode.assets.handlers import asset_handler_registry from geonode.utils import get_supported_datasets_file_types +from geonode.utils import assert_safe_xml, UnsafeXMLError logger = logging.getLogger(__name__) @@ -1356,6 +1357,17 @@ def asset(self, request, pk=None, *args, **kwargs): {"message": f"The uploaded file type {file_ext} is not allowed."}, status=status.HTTP_400_BAD_REQUEST, ) + if file_ext in ("xml", "sld"): + try: + assert_safe_xml(file.read()) + except UnsafeXMLError: + logger.warning("XML validation failed for uploaded asset.", exc_info=True) + return Response( + {"message": f"The uploaded {file_ext} file is invalid or unsafe."}, + status=status.HTTP_400_BAD_REQUEST, + ) + finally: + file.seek(0) try: handler = asset_handler_registry.get_default_handler() asset, link = create_asset_and_link( diff --git a/geonode/documents/forms.py b/geonode/documents/forms.py index 155e9cce6ae..fe39b8646c1 100644 --- a/geonode/documents/forms.py +++ b/geonode/documents/forms.py @@ -31,9 +31,12 @@ from geonode.upload.models import UploadSizeLimit from geonode.upload.api.exceptions import FileUploadLimitException from geonode.upload.zip_validation import ZipValidationError, is_zip_extension, validate_safe_zip +from geonode.utils import assert_safe_xml, UnsafeXMLError logger = logging.getLogger(__name__) +XML_LIKE_DOCUMENT_EXTENSIONS = {"xml", "sld"} + class SizeRestrictedFileField(forms.FileField): """ @@ -160,4 +163,17 @@ def clean_doc_file(self): except (OSError, ValueError): pass + if doc_file and os.path.splitext(doc_file.name)[1].lower()[1:] in XML_LIKE_DOCUMENT_EXTENSIONS: + try: + assert_safe_xml(doc_file.read()) + except UnsafeXMLError as err: + logger.warning("Unsafe XML content rejected on document upload: %s", err) + raise forms.ValidationError(_("Uploaded XML contains unsafe content.")) + finally: + if hasattr(doc_file, "seek"): + try: + doc_file.seek(0) + except (OSError, ValueError): + pass + return doc_file diff --git a/geonode/documents/tests.py b/geonode/documents/tests.py index 177d4fd6ff7..1f3da1ac791 100644 --- a/geonode/documents/tests.py +++ b/geonode/documents/tests.py @@ -704,6 +704,14 @@ def setUp(self): self.perm_spec = self.__class__.perm_spec self.doc_link_url = self.__class__.doc_link_url + def test_document_link_sets_anti_xss_headers(self): + self.test_doc.set_permissions(self.perm_spec) + self.client.login(username=self.not_admin.username, password="very-secret") + response = self.client.get(self.doc_link_url) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.headers.get("X-Content-Type-Options"), "nosniff") + self.assertIn("sandbox", response.headers.get("Content-Security-Policy", "")) + def test_document_link_with_permissions(self): self.test_doc.set_permissions(self.perm_spec) # Get link as Anonymous user diff --git a/geonode/layers/metadata.py b/geonode/layers/metadata.py index 91b1443fbf2..068d8aac9df 100644 --- a/geonode/layers/metadata.py +++ b/geonode/layers/metadata.py @@ -26,7 +26,6 @@ # OWSLib functionality from owslib import iso, util from owslib.csw import CswRecord -from owslib.etree import etree as dlxml from owslib.fgdc import Metadata from django.conf import settings @@ -40,10 +39,14 @@ def set_metadata(xml, identifier="", vals={}, regions=[], keywords=[], custom={}): """Generate dict of model properties based on XML metadata""" + from geonode.utils import assert_safe_xml, UnsafeXMLError # check if document is XML try: - exml = dlxml.fromstring(xml.encode()) + exml = assert_safe_xml(xml) + except UnsafeXMLError as err: + LOGGER.warning("Unsafe XML content rejected in metadata: %s", err) + raise GeoNodeException("Uploaded XML document contains unsafe content") except Exception as err: raise GeoNodeException(f"Uploaded XML document is not XML: {str(err)}") diff --git a/geonode/tests/test_utils.py b/geonode/tests/test_utils.py index 72711f6f34c..31432252769 100644 --- a/geonode/tests/test_utils.py +++ b/geonode/tests/test_utils.py @@ -32,7 +32,14 @@ from geonode.geoserver.helpers import set_attributes from geonode.tests.base import GeoNodeBaseTestSupport from geonode.br.management.commands.utils.utils import ignore_time -from geonode.utils import copy_tree, bbox_to_wkt, is_safe_url, is_safe_url_with_redirects +from geonode.utils import ( + copy_tree, + bbox_to_wkt, + is_safe_url, + is_safe_url_with_redirects, + assert_safe_xml, + UnsafeXMLError, +) from unittest.mock import MagicMock @@ -192,6 +199,25 @@ def test_set_attributes_creates_attributes(self): self.assertIn([a.attribute, a.attribute_type], expected_results) +class TestAssertSafeXml(TestCase): + def test_rejects_xslt_stylesheet(self): + payload = ( + b'' + b'' + b'' + ) + with self.assertRaises(UnsafeXMLError): + assert_safe_xml(payload) + + def test_rejects_script_element(self): + with self.assertRaises(UnsafeXMLError): + assert_safe_xml(b"") + + def test_accepts_safe_xml(self): + payload = b"safe content" + self.assertIsNotNone(assert_safe_xml(payload)) + + class TestSupportedTypes(TestCase): def setUp(self): self.replaced = [ diff --git a/geonode/upload/api/serializer.py b/geonode/upload/api/serializer.py index ddf838761d6..033aadf12e5 100644 --- a/geonode/upload/api/serializer.py +++ b/geonode/upload/api/serializer.py @@ -24,6 +24,7 @@ from geonode.upload.models import UploadParallelismLimit, UploadSizeLimit from geonode.resource.enumerator import ExecutionRequestAction as exa from geonode.upload.zip_validation import ZipValidationError, is_zip_extension, validate_safe_zip +from geonode.utils import assert_safe_xml, UnsafeXMLError logger = logging.getLogger(__name__) @@ -65,6 +66,28 @@ def validate_base_file(self, f): pass return f + def _validate_xml_payload(self, f, label): + if f is None: + return f + try: + assert_safe_xml(f.read()) + except UnsafeXMLError: + logger.warning("%s validation failed for uploaded file.", label, exc_info=True) + raise serializers.ValidationError(f"Invalid or unsafe {label} file.") + finally: + if hasattr(f, "seek"): + try: + f.seek(0) + except (OSError, ValueError): + pass + return f + + def validate_xml_file(self, value): + return self._validate_xml_payload(value, "XML") + + def validate_sld_file(self, value): + return self._validate_xml_payload(value, "SLD") + class ImporterSerializer(BaseImporterSerializer): class Meta: diff --git a/geonode/upload/api/tests.py b/geonode/upload/api/tests.py index 109b21fcaae..45d509b7a3e 100644 --- a/geonode/upload/api/tests.py +++ b/geonode/upload/api/tests.py @@ -156,6 +156,63 @@ def test_importer_upload_rejects_geojson_with_binary_content(self): self.assertEqual(400, response.status_code) self.assertFalse(Dataset.objects.filter(name="fake").exists()) + def test_importer_upload_rejects_unsafe_xml_file(self): + self.client.force_login(get_user_model().objects.get(username="admin")) + payload = { + "base_file": SimpleUploadedFile(name="test.gpkg", content=GPKG_VALID_HEADER), + "xml_file": SimpleUploadedFile( + name="meta.xml", + content=b'', + content_type="application/xml", + ), + "action": "upload", + } + + response = self.client.post(self.url, data=payload) + + self.assertEqual(400, response.status_code) + self.assertIn(b"Invalid or unsafe XML file", response.content) + + def test_importer_upload_rejects_unsafe_sld_file(self): + self.client.force_login(get_user_model().objects.get(username="admin")) + payload = { + "base_file": SimpleUploadedFile(name="test.gpkg", content=GPKG_VALID_HEADER), + "sld_file": SimpleUploadedFile( + name="style.sld", + content=b'' + b"", + content_type="application/xml", + ), + "action": "upload", + } + + response = self.client.post(self.url, data=payload) + + self.assertEqual(400, response.status_code) + self.assertIn(b"Invalid or unsafe SLD file", response.content) + + @patch("geonode.upload.api.views.import_orchestrator") + def test_importer_upload_accepts_safe_xml_and_sld(self, mock_orchestrator): + self.client.force_login(get_user_model().objects.get(username="admin")) + payload = { + "base_file": SimpleUploadedFile(name="test.gpkg", content=GPKG_VALID_HEADER), + "xml_file": SimpleUploadedFile( + name="meta.xml", + content=b'ok', + content_type="application/xml", + ), + "sld_file": SimpleUploadedFile( + name="style.sld", + content=b'', + content_type="application/xml", + ), + "action": "upload", + } + + response = self.client.post(self.url, data=payload) + + self.assertEqual(201, response.status_code) + def test_importer_upload_rejects_zip_with_path_traversal(self): self.client.force_login(get_user_model().objects.get(username="admin")) buf = io.BytesIO() diff --git a/geonode/upload/handlers/sld/handler.py b/geonode/upload/handlers/sld/handler.py index a5c60cd4483..c1a328244c5 100644 --- a/geonode/upload/handlers/sld/handler.py +++ b/geonode/upload/handlers/sld/handler.py @@ -21,7 +21,6 @@ from geonode.resource.registry import resource_manager_registry from geonode.upload.handlers.common.metadata import MetadataFileHandler from geonode.upload.handlers.sld.exceptions import InvalidSldException -from owslib.etree import etree as dlxml from geonode.upload.utils import ImporterRequestAction as ira logger = logging.getLogger("importer") @@ -89,10 +88,16 @@ def is_valid(files, user, **kwargs): """ Define basic validation steps """ + from geonode.utils import assert_safe_xml, UnsafeXMLError + # calling base validation checks try: with open(files.get("base_file")) as _xml: - dlxml.fromstring(_xml.read().encode()) + content = _xml.read() + assert_safe_xml(content) + except UnsafeXMLError as err: + logger.warning("Unsafe SLD content rejected: %s", err) + raise InvalidSldException("Uploaded document contains unsafe content") except Exception as err: raise InvalidSldException(f"Uploaded document is not SLD or is invalid: {str(err)}") return True diff --git a/geonode/upload/handlers/xml/handler.py b/geonode/upload/handlers/xml/handler.py index 182d9011277..cfc5382910c 100644 --- a/geonode/upload/handlers/xml/handler.py +++ b/geonode/upload/handlers/xml/handler.py @@ -21,7 +21,6 @@ from geonode.resource.registry import resource_manager_registry from geonode.upload.handlers.common.metadata import MetadataFileHandler from geonode.upload.handlers.xml.exceptions import InvalidXmlException -from owslib.etree import etree as dlxml from geonode.upload.utils import ImporterRequestAction as ira logger = logging.getLogger("importer") @@ -89,10 +88,16 @@ def is_valid(files, user=None, **kwargs): """ Define basic validation steps """ + from geonode.utils import assert_safe_xml, UnsafeXMLError + # calling base validation checks try: with open(files.get("base_file")) as _xml: - dlxml.fromstring(_xml.read().encode()) + content = _xml.read() + assert_safe_xml(content) + except UnsafeXMLError as err: + logger.warning("Unsafe XML content rejected: %s", err) + raise InvalidXmlException("Uploaded document contains unsafe content") except Exception as err: raise InvalidXmlException(f"Uploaded document is not XML or is invalid: {str(err)}") return True diff --git a/geonode/utils.py b/geonode/utils.py index 0fa4094e021..a7f143c0b29 100755 --- a/geonode/utils.py +++ b/geonode/utils.py @@ -97,6 +97,80 @@ # explicitly disable resolving XML entities in order to prevent malicious attacks XML_PARSER: typing.Final = etree.XMLParser(resolve_entities=False) + +class UnsafeXMLError(Exception): + """Raised when an uploaded XML carries active content (XSLT, scripts, PIs, DTD).""" + + +_XSLT_NAMESPACES: typing.Final = frozenset( + { + "http://www.w3.org/1999/XSL/Transform", + "http://www.w3.org/TR/WD-xsl", + } +) + +_DANGEROUS_LOCAL_NAMES: typing.Final = frozenset( + { + "script", + "iframe", + "object", + "embed", + "applet", + "foreignobject", + "handler", + } +) + +# only data:text/html is blocked, so legitimate data:image/... keeps working +_ACTIVE_URI_SCHEMES: typing.Final = ("javascript:", "vbscript:", "data:text/html") + + +def _safe_xml_parser() -> etree.XMLParser: + return etree.XMLParser( + resolve_entities=False, + no_network=True, + load_dtd=False, + dtd_validation=False, + huge_tree=False, + ) + + +def assert_safe_xml(data): + """Raise UnsafeXMLError if the XML carries active content; return the root element.""" + if isinstance(data, str): + data = data.encode("utf-8", "surrogatepass") + + try: + root = etree.fromstring(data, parser=_safe_xml_parser()) + except etree.XMLSyntaxError as err: + raise UnsafeXMLError(f"Document is not well-formed XML: {err}") + + docinfo = root.getroottree().docinfo + if docinfo is not None and docinfo.doctype: + raise UnsafeXMLError("DOCTYPE/DTD declarations are not allowed in uploaded XML") + + for proins in root.xpath("//processing-instruction()"): + raise UnsafeXMLError(f"Processing instruction '' is not allowed") + + for el in root.iter(): + if not isinstance(el.tag, str): + continue + qname = etree.QName(el) + if qname.namespace in _XSLT_NAMESPACES: + raise UnsafeXMLError(f"Embedded XSLT element '<{qname.localname}>' is not allowed") + if qname.localname.lower() in _DANGEROUS_LOCAL_NAMES: + raise UnsafeXMLError(f"Disallowed element '<{qname.localname}>' found") + for name, value in el.attrib.items(): + local = etree.QName(name).localname if name.startswith("{") else name + if len(local) > 2 and local.lower().startswith("on"): + raise UnsafeXMLError(f"Event-handler attribute '{local}' is not allowed") + v = "".join((value or "").split()).lower() + if v.startswith(_ACTIVE_URI_SCHEMES): + raise UnsafeXMLError("Active URI scheme (javascript:/vbscript:/data:text/html) is not allowed") + + return root + + requests.packages.urllib3.disable_warnings() From 56e279671fbff4c65c049e58c7adf2ce653f504a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 14 Aug 2026 10:26:17 +0200 Subject: [PATCH 59/98] Fix broken tests (#14516) (#14518) (cherry picked from commit d37a787c4e43369dd335f8b22348bc7480d1784b) Co-authored-by: mattiagiupponi <51856725+mattiagiupponi@users.noreply.github.com> --- geonode/upload/handlers/base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/geonode/upload/handlers/base.py b/geonode/upload/handlers/base.py index cceba710162..9e62815f045 100644 --- a/geonode/upload/handlers/base.py +++ b/geonode/upload/handlers/base.py @@ -32,7 +32,7 @@ from geonode.resource.enumerator import ExecutionRequestAction as exa from geonode.layers.models import Dataset from geonode.storage.utils import organize_files_by_ext -from geonode.upload.api.exceptions import ImportException +from geonode.upload.api.exceptions import GeneralUploadException, ImportException from geonode.upload.utils import ImporterRequestAction as ira, find_key_recursively from geonode.resource.models import ExecutionRequest from geonode.base.models import ResourceBase @@ -128,7 +128,7 @@ def merge_upload_validation_config(cls): @classmethod def get_task_list(cls, action) -> tuple: if action not in cls.TASKS: - raise Exception("The requested action is not implemented yet") + raise GeneralUploadException("The requested action is not implemented yet") return cls.TASKS.get(action) @property From c6a1ad4e0004a76900ba25966e42f8e6a27feafb Mon Sep 17 00:00:00 2001 From: "G.Allegri" Date: Wed, 19 Aug 2026 18:35:25 +0200 Subject: [PATCH 60/98] Aligned docker setup docs to the new goenode-project archtiecture --- .../docker/project-docker-installation.md | 28 +++---------------- 1 file changed, 4 insertions(+), 24 deletions(-) diff --git a/docs/src/setup/docker/project-docker-installation.md b/docs/src/setup/docker/project-docker-installation.md index 8c28d7132e1..3929aceb7b3 100644 --- a/docs/src/setup/docker/project-docker-installation.md +++ b/docs/src/setup/docker/project-docker-installation.md @@ -10,39 +10,19 @@ cd ~/geonode_projects git clone https://github.com/GeoNode/geonode-project.git ``` -This will clone the `master` branch. You will have to checkout the desidered branch or tag. As an example, if you want to generate a propject for GeoNode 4.4.3 you will docker. +This will clone the `master` branch. You will have to checkout the desidered branch or tag. As an example, if you want to generate a propject for GeoNode 5.1.0 you will do: ```bash cd geonode-project -git checkout -b 5.0.1 +git checkout -b 5.1.0 ``` !!! Note - You can replace the release number `5.0.1` with the latest one. You can find the releases [here](https://github.com/GeoNode/geonode-project/releases/) - -### Generate a custom GeoNode project - -This is the most important part for the GeoNode project installation. Before building the project, you have to generate custom GeoNode project intance, using the `GeoNode Template` - -!!! Note - We will call our instance my_geonode. You can change the name at your convenience. - -```bash -# Create and activate a Python environment called my_geonode_env -mkdir path/to/.venvs -python3 -m venv /path/to/.venvs/my_geonode_env -source /path/to/.venvs/my_geonode_env/bin/activate - -# Install Django in the activated Python environment -pip install Django==5.2.8 - -cd ~/geonode_projects -django-admin startproject --template=./geonode-project -e py,sh,md,rst,json,yml,ini,env,sample,properties -n monitoring-cron -n Dockerfile my_geonode -``` + You can replace the release number `5.1.0` with the latest one. You can find the releases [here](https://github.com/GeoNode/geonode-project/releases/) ### Prepare the .env file -Navigate to `my_geonode` folder and create the .env file by using the `create-envfile` script: +Go inside the `geonode-project` folder and create the .env file by using the `create-envfile` script: ```bash cd my_geonode From 5a6832a26f60b7b6fea0a06c0f8b386b02e46217 Mon Sep 17 00:00:00 2001 From: "G.Allegri" Date: Wed, 19 Aug 2026 18:43:22 +0200 Subject: [PATCH 61/98] Aligned bare docker setup docs --- .../setup/bare/project-bare-installation.md | 23 ++----------------- .../docker/project-docker-installation.md | 2 +- 2 files changed, 3 insertions(+), 22 deletions(-) diff --git a/docs/src/setup/bare/project-bare-installation.md b/docs/src/setup/bare/project-bare-installation.md index 68d3d517e0c..97dedb309fc 100644 --- a/docs/src/setup/bare/project-bare-installation.md +++ b/docs/src/setup/bare/project-bare-installation.md @@ -38,27 +38,8 @@ This will clone the ``master`` branch. You will have to checkout the desidered b As an example, if you want to generate a project for GeoNode 4.4.3 run the following: ```bash -git checkout -b 4.4.3 -``` - -### Generate a custom GeoNode project - -This is the most important part for the GeoNode project installation. Before building the project, you have to generate custom GeoNode project intance, using the `GeoNode Template` - -!!! Note - We will call our instance my_geonode. You can change the name at your convenience. - -```bash -# Create and activate a Python environment called my_geonode_env -mkdir path/to/.venvs -python3 -m venv /path/to/.venvs/my_geonode_env -source /path/to/.venvs/my_geonode_env/bin/activate - -# Install Django in the activated Python environment -pip install Django==5.2.8 - -cd /opt/geonode_projects -django-admin startproject --template=./geonode-project -e py,sh,md,rst,json,yml,ini,env,sample,properties -n monitoring-cron -n Dockerfile my_geonode +cd geonode-project +git checkout -b 5.1.0 ``` ### Install the Python requrements diff --git a/docs/src/setup/docker/project-docker-installation.md b/docs/src/setup/docker/project-docker-installation.md index 3929aceb7b3..d94909f4622 100644 --- a/docs/src/setup/docker/project-docker-installation.md +++ b/docs/src/setup/docker/project-docker-installation.md @@ -10,7 +10,7 @@ cd ~/geonode_projects git clone https://github.com/GeoNode/geonode-project.git ``` -This will clone the `master` branch. You will have to checkout the desidered branch or tag. As an example, if you want to generate a propject for GeoNode 5.1.0 you will do: +This will clone the `master` branch. You will have to checkout the desidered branch or tag. As an example, if you want to generate a propject for GeoNode 5.1.0 run the following: ```bash cd geonode-project From 119aea84b9e1794037dcf85143ac864170ad8cc8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 20 Aug 2026 11:27:45 +0200 Subject: [PATCH 62/98] [Fixes #14498] 3d tiles with multiple folders, raise error 21 (#14499) (#14537) * 3d tiles with multiple folders, raise error 21 (cherry picked from commit ebd13f472142191cef290e3450b100566720ecb5) Co-authored-by: mattiagiupponi <51856725+mattiagiupponi@users.noreply.github.com> --- geonode/upload/handlers/base.py | 9 ++- geonode/upload/handlers/tiles3d/handler.py | 29 +++++--- geonode/upload/handlers/tiles3d/tests.py | 79 +++++++++++++++++++++- 3 files changed, 103 insertions(+), 14 deletions(-) diff --git a/geonode/upload/handlers/base.py b/geonode/upload/handlers/base.py index 9e62815f045..4537ee9cfd1 100644 --- a/geonode/upload/handlers/base.py +++ b/geonode/upload/handlers/base.py @@ -257,8 +257,15 @@ def pre_processing(self, files, execution_id, **kwargs): # if a zipfile is provided, we need to unzip it before searching for an handler zipname = Path(files["base_file"]).stem # extract all the file content + extract_dir = os.path.dirname(files["base_file"]) with zipfile.ZipFile(files["base_file"], "r") as z: - z.extractall(path=os.path.dirname(files["base_file"])) + for member in z.namelist(): + # zip-slip: reject entries that would extract outside extract_dir + if not os.path.realpath(os.path.join(extract_dir, member)).startswith( + os.path.realpath(extract_dir) + ): + raise ImportException(f"Invalid zip entry: {member}") + z.extractall(path=extract_dir) # getting the path of the extracted files unzipped_path = [entry.path for entry in os.scandir(os.path.dirname(files["base_file"]))] # updating paths in the data paylad diff --git a/geonode/upload/handlers/tiles3d/handler.py b/geonode/upload/handlers/tiles3d/handler.py index 2c7b3929294..b4811563dfe 100755 --- a/geonode/upload/handlers/tiles3d/handler.py +++ b/geonode/upload/handlers/tiles3d/handler.py @@ -260,24 +260,33 @@ def create_geonode_resource( ): exec_obj = orchestrator.get_execution_object(execution_id) + files = exec_obj.input_params["files"] + # a ".json" file isn't necessarily the tileset (e.g. a stray metadata file), so validate + # each candidate against the 3dtiles schema + js_file = None + for candidate in sorted(x for x in files.values() if str(x).endswith(".json")): + try: + js_file = Tiles3DFileHandler.is_3dtiles_json(candidate) + break + except Invalid3DTilesException: + continue + if js_file is None: + raise Invalid3DTilesException("tileset.json file is missing") + resource = super().create_geonode_resource(layer_name, alternate, execution_id, ResourceBase, asset, **kwargs) + + if js_file: + files = {"base_file": files.get("base_file")} + asset = self.create_asset_and_link( resource, - files=exec_obj.input_params["files"], + files=files, action=exec_obj.action, asset_type="3dtiles", extension="3dtiles", ) - if isinstance(asset, LocalAsset): - # fixing-up bbox for the 3dtile object - js_file = None - with open(asset.location[0]) as _file: - js_file = json.loads(_file.read()) - - if not js_file: - return resource - + if isinstance(asset, LocalAsset) and js_file: if self._has_region(js_file): resource = self.set_bbox_from_region(js_file, resource=resource) elif self._has_sphere(js_file): diff --git a/geonode/upload/handlers/tiles3d/tests.py b/geonode/upload/handlers/tiles3d/tests.py index 844d68cd794..ab6a5a0088d 100755 --- a/geonode/upload/handlers/tiles3d/tests.py +++ b/geonode/upload/handlers/tiles3d/tests.py @@ -442,13 +442,86 @@ def test_set_bbox_from_bounding_volume_sphere_with_center_zero_without_transform ) self.assertTrue(resource.bbox == self.default_bbox) - def _generate_execid_asset(self): + def test_create_geonode_resource_should_raise_exception_if_tileset_json_is_missing(self): exec_id = orchestrator.create_execution_request( user=self.owner, func_name="funct1", step="step", input_params={ - "files": {"base_file": "/tmp/tileset.json"}, + "files": {"base_file": "/tmp/model.glb"}, + "skip_existing_layer": True, + }, + ) + + with self.assertRaises(Invalid3DTilesException) as _exc: + self.handler.create_geonode_resource( + "layername", + "layeralternate", + execution_id=exec_id, + resource_type="ResourceBase", + asset=None, + ) + + self.assertTrue("tileset.json file is missing" in str(_exc.exception)) + + def test_create_geonode_resource_should_skip_invalid_json_and_pick_the_actual_tileset(self): + # a stray/decoy ".json" file must not be mistaken for the tileset: the file is only + # accepted once its content passes the 3dtiles schema check, regardless of its name + # or of where it sorts among the other files + shutil.copy(self.valid_tileset_with_region, "/tmp/tileset.json") + with open("/tmp/aaa_decoy.json", "w") as _f: + _f.write(json.dumps({"not": "a tileset"})) + + exec_id, asset = self._generate_execid_asset( + files={"base_file": "/tmp/aaa_decoy.json", "json_file": "/tmp/tileset.json"}, + asset_files=["/tmp/aaa_decoy.json", "/tmp/tileset.json"], + ) + + try: + resource = self.handler.create_geonode_resource( + "layername", + "layeralternate", + execution_id=exec_id, + resource_type="ResourceBase", + asset=asset, + ) + self.assertFalse(resource.bbox == self.default_bbox) + finally: + os.remove("/tmp/aaa_decoy.json") + + def test_create_geonode_resource_should_read_bbox_from_tileset_regardless_of_asset_file_order(self): + # the bbox must be extracted from the tileset.json file explicitly, + # not from asset.location[0], which is not guaranteed to be the json file + shutil.copy(self.valid_tileset_with_region, "/tmp/tileset.json") + with open("/tmp/model.glb", "wb") as _f: + _f.write(b"\x00\x01\x02not-a-json-file") + + exec_id, asset = self._generate_execid_asset( + files={"base_file": "/tmp/tileset.json", "glb_file": "/tmp/model.glb"}, + asset_files=["/tmp/model.glb", "/tmp/tileset.json"], + ) + + try: + resource = self.handler.create_geonode_resource( + "layername", + "layeralternate", + execution_id=exec_id, + resource_type="ResourceBase", + asset=asset, + ) + self.assertFalse(resource.bbox == self.default_bbox) + finally: + os.remove("/tmp/model.glb") + + def _generate_execid_asset(self, files=None, asset_files=None): + files = files or {"base_file": "/tmp/tileset.json"} + asset_files = asset_files or ["/tmp/tileset.json"] + exec_id = orchestrator.create_execution_request( + user=self.owner, + func_name="funct1", + step="step", + input_params={ + "files": files, "skip_existing_layer": True, }, ) @@ -457,7 +530,7 @@ def _generate_execid_asset(self): owner=self.owner, description=None, type=str(self.handler), - files=["/tmp/tileset.json"], + files=asset_files, clone_files=False, ) From 5e1ae77731621811d80661813b2123a30c89b607 Mon Sep 17 00:00:00 2001 From: mattiagiupponi <51856725+mattiagiupponi@users.noreply.github.com> Date: Thu, 20 Aug 2026 11:28:05 +0200 Subject: [PATCH 63/98] [Backport #14519] GroupProfile.access default is public, not a valid choice (#14531) --- .../0036_fix_groupprofile_access_default.py | 38 +++++++++++++++++++ geonode/groups/models.py | 2 +- geonode/groups/tests.py | 11 ++++++ 3 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 geonode/groups/migrations/0036_fix_groupprofile_access_default.py diff --git a/geonode/groups/migrations/0036_fix_groupprofile_access_default.py b/geonode/groups/migrations/0036_fix_groupprofile_access_default.py new file mode 100644 index 00000000000..bb922454f11 --- /dev/null +++ b/geonode/groups/migrations/0036_fix_groupprofile_access_default.py @@ -0,0 +1,38 @@ +from django.db import migrations, models + +# The old default, with a stray apostrophe that matches none of the field's own +# GROUP_CHOICES. Every GroupProfile created without an explicit access — which is +# what geonode_ldap's group mirroring does — was stored with this value. +BAD_DEFAULT = "public'" + + +def repair_access(apps, schema_editor): + GroupProfile = apps.get_model("groups", "GroupProfile") + GroupProfile.objects.filter(access=BAD_DEFAULT).update(access="public") + + +class Migration(migrations.Migration): + dependencies = [ + ("groups", "0035_remove_modeltranslation"), + ] + + operations = [ + migrations.AlterField( + model_name="groupprofile", + name="access", + field=models.CharField( + choices=[ + ("public", "Public"), + ("public-invite", "Public (invite-only)"), + ("private", "Private"), + ], + default="public", + help_text="Public: Any registered user can view and join a public group.
Public (invite-only):Any registered user can view the group. Only invited users can join.
Private: Registered users cannot see any details about the group, including membership. Only invited users can join.", + max_length=15, + verbose_name="Access", + ), + ), + # Reversing would mean writing the invalid value back, so this only goes + # forward; the AlterField above is reversible on its own. + migrations.RunPython(repair_access, migrations.RunPython.noop), + ] diff --git a/geonode/groups/models.py b/geonode/groups/models.py index 8c66ca61bbc..b70511ef0e4 100644 --- a/geonode/groups/models.py +++ b/geonode/groups/models.py @@ -91,7 +91,7 @@ class GroupProfile(models.Model): email = models.EmailField(_("Email"), null=True, blank=True, help_text=email_help_text) keywords = TaggableManager(_("Keywords"), help_text=_("A space or comma-separated list of keywords"), blank=True) access = models.CharField( - _("Access"), max_length=15, default="public'", choices=GROUP_CHOICES, help_text=access_help_text + _("Access"), max_length=15, default="public", choices=GROUP_CHOICES, help_text=access_help_text ) categories = models.ManyToManyField(GroupCategory, verbose_name=_("Categories"), blank=True, related_name="groups") created = models.DateTimeField(auto_now_add=True, null=True, blank=True) diff --git a/geonode/groups/tests.py b/geonode/groups/tests.py index 337c188055c..b17993098e3 100644 --- a/geonode/groups/tests.py +++ b/geonode/groups/tests.py @@ -93,6 +93,17 @@ def test_registered_group_exists(self): group = Group.objects.filter(name=groups_settings.REGISTERED_MEMBERS_GROUP_NAME).first() self.assertTrue(group) + def test_default_access_is_a_valid_choice(self): + """ + Ensures a GroupProfile created without an explicit access gets one of the + field's own GROUP_CHOICES, so that code filtering on access="public" + (e.g. geonode.people.utils.get_available_users) can see it. + """ + group = GroupProfile.objects.create(slug="default_access_group", title="default_access_group") + self.assertEqual(group.access, "public") + self.assertIn(group.access, dict(GroupProfile.GROUP_CHOICES)) + self.assertIn(group, GroupProfile.objects.filter(access="public")) + def test_users_group_list_view(self): """ 1. Ensures that a superuser can see the whole group list. From 2e6c55503000678ff381894228714559f29cc812 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 20 Aug 2026 11:31:10 +0200 Subject: [PATCH 64/98] [Fixes #14533] Fixes XML File upload fails when trying to upload (#14534) (#14539) (cherry picked from commit ce64b7f7755558cb763fe6d0d912fa556bd2d94d) Co-authored-by: Sijan Dhungana --- geonode/documents/enumerations.py | 2 +- geonode/documents/tests.py | 16 ++++++++++++ geonode/upload/api/tests.py | 34 ++++++++++++++++++++++++++ geonode/upload/handlers/xml/handler.py | 2 +- geonode/upload/uploadhandler.py | 13 ++++++++++ 5 files changed, 65 insertions(+), 2 deletions(-) diff --git a/geonode/documents/enumerations.py b/geonode/documents/enumerations.py index 3c592bd3b54..589b370a746 100644 --- a/geonode/documents/enumerations.py +++ b/geonode/documents/enumerations.py @@ -93,7 +93,7 @@ { "csv": {"text/plain", "text/csv"}, "log": {"text/plain", "text/csv"}, - "xml": {"application/xml", "text/xml"}, + "xml": {"application/xml", "text/xml", "text/plain"}, "sld": {"text/plain", "application/xml", "text/xml"}, "dxf": {"image/vnd.dwg", "image/vnd.dxf"}, "doc": {"application/msword", "application/x-ole-storage"}, diff --git a/geonode/documents/tests.py b/geonode/documents/tests.py index 1f3da1ac791..0f387b54ee2 100644 --- a/geonode/documents/tests.py +++ b/geonode/documents/tests.py @@ -411,6 +411,22 @@ def test_upload_document_form_accepts_clean_zip(self): form = DocumentCreateForm(form_data, file_data) self.assertTrue(form.is_valid(), msg=form.errors) + def test_upload_document_form_accepts_valid_xml(self): + iso_xml_content = ( + b'' + b"" + b'123-abc' + b"" + b"" + ) + form_data = { + "title": "ISO Metadata XML", + "permissions": '{"anonymous":"document_readonly","authenticated":"resourcebase_readwrite","users":[]}', + } + file_data = {"doc_file": SimpleUploadedFile("metadata.xml", iso_xml_content, "text/plain")} + form = DocumentCreateForm(form_data, file_data) + self.assertTrue(form.is_valid(), msg=form.errors) + def test_document_embed(self): """/documents/1 -> Test accessing the embed view of a document""" d = Document.objects.all().first() diff --git a/geonode/upload/api/tests.py b/geonode/upload/api/tests.py index 45d509b7a3e..0b5182d2c00 100644 --- a/geonode/upload/api/tests.py +++ b/geonode/upload/api/tests.py @@ -213,6 +213,40 @@ def test_importer_upload_accepts_safe_xml_and_sld(self, mock_orchestrator): self.assertEqual(201, response.status_code) + @patch("geonode.upload.api.views.import_orchestrator") + def test_importer_upload_accepts_iso_xml_without_xml_declaration(self, _orc): + self.client.force_login(get_user_model().objects.get(username="admin")) + iso_xml_content = ( + b'\n' + b" test-iso-uuid\n" + b" \n" + b" \n" + b" Test abstract.\n" + b" \n" + b" \n" + b"" + ) + payload = { + "base_file": SimpleUploadedFile("metadata.xml", iso_xml_content, "application/xml"), + "action": "resource_metadata_upload", + "resource_pk": self.dataset.pk, + } + + response = self.client.post(self.url, data=payload) + self.assertEqual(201, response.status_code) + + def test_importer_upload_rejects_other_text_with_xml_extension(self): + self.client.force_login(get_user_model().objects.get(username="admin")) + payload = { + "base_file": SimpleUploadedFile("fake.xml", b"This is plain text, not XML.", "text/plain"), + "action": "resource_metadata_upload", + "resource_pk": self.dataset.pk, + } + + response = self.client.post(self.url, data=payload) + self.assertEqual(400, response.status_code) + def test_importer_upload_rejects_zip_with_path_traversal(self): self.client.force_login(get_user_model().objects.get(username="admin")) buf = io.BytesIO() diff --git a/geonode/upload/handlers/xml/handler.py b/geonode/upload/handlers/xml/handler.py index cfc5382910c..119ebbfb2f4 100644 --- a/geonode/upload/handlers/xml/handler.py +++ b/geonode/upload/handlers/xml/handler.py @@ -67,7 +67,7 @@ def supported_file_extension_config(self): @property def upload_validation_config(self): return { - "xml": {"mimes": {"application/xml", "text/xml"}}, + "xml": {"mimes": {"application/xml", "text/xml", "text/plain"}}, } @staticmethod diff --git a/geonode/upload/uploadhandler.py b/geonode/upload/uploadhandler.py index 70ac296797d..36e0d4bd6f1 100644 --- a/geonode/upload/uploadhandler.py +++ b/geonode/upload/uploadhandler.py @@ -117,6 +117,19 @@ def _validate_magic_mime(self): self._reject( f"Detected content type '{self.detected_mime}' does not match extension '.{self.extension}'." ) + if self.extension in ("xml", "sld") and self.detected_mime == "text/plain": + # Verify if sample contains valid XML syntax. + # Deep check (assert_safe_xml) is/should executed on the full file downstream. + from defusedxml.ElementTree import XMLParser + + parser = XMLParser(forbid_dtd=True, forbid_entities=True, forbid_external=True) + try: + # Verify that the text/plain sample has valid XML structure + parser.feed(sample) + except Exception: + self._reject( + f"Detected content type '{self.detected_mime}' does not match extension '.{self.extension}'." + ) if self.extension in self.magic_description_map: self.detected_description = self._detect_description(sample) From 328982d1734fa2c222985565872c32ec3940408d Mon Sep 17 00:00:00 2001 From: "G.Allegri" Date: Fri, 21 Aug 2026 16:31:35 +0200 Subject: [PATCH 65/98] Document the XLSX_UPLOAD_ENABLED configuration --- docs/src/setup/configuration/settings.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/src/setup/configuration/settings.md b/docs/src/setup/configuration/settings.md index b62a5615f2f..b5935ffca09 100644 --- a/docs/src/setup/configuration/settings.md +++ b/docs/src/setup/configuration/settings.md @@ -1656,3 +1656,10 @@ class MyObject(): : Default: ``'ALLOW-FROM %s' % SITEURL`` This is a [Django setting](https://docs.djangoproject.com/en/3.2/ref/clickjacking/#setting-x-frame-options-for-all-responses) + + +**XLSX_UPLOAD_ENABLED** + +: Default: `False` + + Enable or not the upload of XLSX / XLS files \ No newline at end of file From 760396860974fa66de073af228c85416c8a5f7d0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Aug 2026 00:02:49 +0000 Subject: [PATCH 66/98] build(deps): bump lxml from 6.1.1 to 6.1.2 Bumps [lxml](https://github.com/lxml/lxml) from 6.1.1 to 6.1.2. - [Release notes](https://github.com/lxml/lxml/releases) - [Changelog](https://github.com/lxml/lxml/blob/master/CHANGES.txt) - [Commits](https://github.com/lxml/lxml/compare/lxml-6.1.1...lxml-6.1.2) --- updated-dependencies: - dependency-name: lxml dependency-version: 6.1.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] (cherry picked from commit c26dd983f858060bb8fe01a6045c70059f951e44) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 3146630de0a..15840c8d9ab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,7 +25,7 @@ classifiers = [ # Core dependencies (consolidated from requirements.txt) dependencies = [ "Pillow==12.3.0", - "lxml==6.1.1", + "lxml==6.1.2", "psycopg2==2.9.12", "Django==5.2.16", From 2ddca6a24b3308b51b8c0db3a53cfb8c00c97144 Mon Sep 17 00:00:00 2001 From: Sijan Dhungana Date: Wed, 26 Aug 2026 13:23:15 +0545 Subject: [PATCH 67/98] [Backport #14522 to 5.1.x] Improvements to URLs parsing (#14557) Co-authored-by: Giovanni Allegri --- geonode/tests/test_utils.py | 43 +++++++++++++++++++ geonode/upload/handlers/common/remote.py | 3 +- geonode/upload/handlers/remote/cog.py | 10 +++-- geonode/upload/handlers/remote/flatgeobuf.py | 10 +++-- .../handlers/remote/tests/test_3dtiles.py | 3 +- .../upload/handlers/remote/tests/test_cog.py | 40 +++++++++++------ .../handlers/remote/tests/test_flatgeobuf.py | 40 +++++++++++------ .../upload/handlers/remote/tests/test_wms.py | 3 +- geonode/upload/handlers/remote/tiles3d.py | 16 +++---- geonode/utils.py | 21 +++++++++ 10 files changed, 143 insertions(+), 46 deletions(-) diff --git a/geonode/tests/test_utils.py b/geonode/tests/test_utils.py index 31432252769..a59d1691222 100644 --- a/geonode/tests/test_utils.py +++ b/geonode/tests/test_utils.py @@ -17,6 +17,8 @@ # ######################################################################### import copy +import requests +from requests.models import PreparedRequest, Response from unittest import TestCase from unittest.mock import patch @@ -38,6 +40,7 @@ is_safe_url, is_safe_url_with_redirects, assert_safe_xml, + safe_request_url, UnsafeXMLError, ) from unittest.mock import MagicMock @@ -441,3 +444,43 @@ def test_trusted_host_case_insensitive(self): with patch("geonode.utils.socket.getaddrinfo") as mock_dns: mock_dns.return_value = [(None, None, None, None, ("192.168.1.100", 0))] self.assertTrue(is_safe_url("https://internal.private.host:9090/geoserver/ows")) + + +def _fake_response(status_code, url, location=None): + """Build a minimal requests.Response usable by resolve_redirects without a socket.""" + response = Response() + request = PreparedRequest() + request.prepare(method="GET", url=url) + response.status_code = status_code + response.url = url + response.request = request + response.raw = None + response._content = b"" + response._content_consumed = True + response.history = [] + if location is not None: + response.headers["Location"] = location + return response + + +@override_settings(SAFE_URL_CHECK_ENABLED=True) +class TestSafeRequestUrl(djangoTestCase): + """safe_request_url must validate the initial URL and every redirect hop.""" + + @patch("geonode.utils.is_safe_url") + def test_rejects_disallowed_initial_url(self, mock_is_safe_url): + """An initial URL that is not allowed is rejected before any request is sent.""" + mock_is_safe_url.return_value = False + with self.assertRaises(requests.exceptions.InvalidURL): + safe_request_url("GET", "http://first.invalid/resource") + + @patch("requests.adapters.HTTPAdapter.send") + @patch("geonode.utils.is_safe_url") + def test_rejects_disallowed_redirect_target(self, mock_is_safe_url, mock_adapter_send): + """A redirect whose target is not allowed is rejected on the redirect hop.""" + mock_is_safe_url.side_effect = lambda candidate: "allowed.invalid" in candidate + mock_adapter_send.return_value = _fake_response( + 302, "http://allowed.invalid/resource", location="http://other.invalid/resource" + ) + with self.assertRaises(requests.exceptions.InvalidURL): + safe_request_url("GET", "http://allowed.invalid/resource") diff --git a/geonode/upload/handlers/common/remote.py b/geonode/upload/handlers/common/remote.py index 084cd71b54a..c5789362f38 100755 --- a/geonode/upload/handlers/common/remote.py +++ b/geonode/upload/handlers/common/remote.py @@ -35,6 +35,7 @@ from geonode.base.enumerations import SOURCE_TYPE_REMOTE from geonode.resource.registry import resource_manager_registry from geonode.resource.models import ExecutionRequest +from geonode.utils import safe_request_url logger = logging.getLogger("importer") @@ -91,7 +92,7 @@ def is_valid_url(url, **kwargs): and if the url is valid """ try: - r = requests.get(url, timeout=10) + r = safe_request_url("GET", url, timeout=10) r.raise_for_status() except requests.exceptions.Timeout: raise ImportException("Timed out") diff --git a/geonode/upload/handlers/remote/cog.py b/geonode/upload/handlers/remote/cog.py index 191a2f0ce57..c9f512ae07b 100644 --- a/geonode/upload/handlers/remote/cog.py +++ b/geonode/upload/handlers/remote/cog.py @@ -17,9 +17,9 @@ # ######################################################################### import logging -import requests from osgeo import gdal +from geonode.utils import is_safe_url_with_redirects, safe_request_url from geonode.security.utils import init_gdal_security from geonode.layers.models import Dataset from geonode.upload.handlers.common.remote import BaseRemoteResourceHandler @@ -58,9 +58,13 @@ def is_valid_url(url, **kwargs): Check if the URL is reachable and supports HTTP Range requests """ logger.debug(f"Checking COG URL validity (HEAD): {url}") + # Reject disallowed URLs early, before running the remaining validity checks. + is_safe, _ = is_safe_url_with_redirects(url) + if not is_safe: + raise ImportException("The requested URL is not allowed.") try: # Reachability check using HEAD - head_res = requests.head(url, timeout=10, allow_redirects=True) + head_res = safe_request_url("HEAD", url, timeout=10) logger.debug(f"HTTP HEAD status: {head_res.status_code}") head_res.raise_for_status() @@ -73,7 +77,7 @@ def is_valid_url(url, **kwargs): # Some servers might not return Accept-Ranges in HEAD, so we try a small range request logger.debug("Accept-Ranges header missing, trying a small Range GET...") - range_res = requests.get(url, headers={"Range": "bytes=0-1"}, timeout=10, stream=True) + range_res = safe_request_url("GET", url, headers={"Range": "bytes=0-1"}, timeout=10, stream=True) logger.debug(f"Range GET status: {range_res.status_code}") try: if range_res.status_code != 206: diff --git a/geonode/upload/handlers/remote/flatgeobuf.py b/geonode/upload/handlers/remote/flatgeobuf.py index f820834dc05..4579d39420e 100644 --- a/geonode/upload/handlers/remote/flatgeobuf.py +++ b/geonode/upload/handlers/remote/flatgeobuf.py @@ -17,9 +17,9 @@ # ######################################################################### import logging -import requests from osgeo import gdal +from geonode.utils import is_safe_url_with_redirects, safe_request_url from geonode.security.utils import init_gdal_security from geonode.layers.models import Dataset from geonode.upload.handlers.common.remote import BaseRemoteResourceHandler @@ -59,9 +59,13 @@ def is_valid_url(url, **kwargs): Check if the URL is reachable and supports HTTP Range requests """ logger.debug(f"Checking FlatGeobuf URL validity (HEAD): {url}") + # Reject disallowed URLs early, before running the remaining validity checks. + is_safe, _ = is_safe_url_with_redirects(url) + if not is_safe: + raise ImportException("The requested URL is not allowed.") try: # Reachability check using HEAD - head_res = requests.head(url, timeout=10, allow_redirects=True) + head_res = safe_request_url("HEAD", url, timeout=10) logger.debug(f"HTTP HEAD status: {head_res.status_code}") head_res.raise_for_status() @@ -74,7 +78,7 @@ def is_valid_url(url, **kwargs): # Some servers might not return Accept-Ranges in HEAD, so we try a small range request logger.debug("Accept-Ranges header missing, trying a small Range GET...") - range_res = requests.get(url, headers={"Range": "bytes=0-1"}, timeout=10, stream=True) + range_res = safe_request_url("GET", url, headers={"Range": "bytes=0-1"}, timeout=10, stream=True) logger.debug(f"Range GET status: {range_res.status_code}") try: if range_res.status_code != 206: diff --git a/geonode/upload/handlers/remote/tests/test_3dtiles.py b/geonode/upload/handlers/remote/tests/test_3dtiles.py index f5216ec9414..8623d507d99 100644 --- a/geonode/upload/handlers/remote/tests/test_3dtiles.py +++ b/geonode/upload/handlers/remote/tests/test_3dtiles.py @@ -18,7 +18,6 @@ ######################################################################### from django.test import TestCase from mock import MagicMock, patch -from geonode.upload.api.exceptions import ImportException from django.contrib.auth import get_user_model from geonode.upload.handlers.common.serializer import RemoteResourceSerializer from geonode.upload.handlers.remote.tiles3d import RemoteTiles3DResourceHandler @@ -95,7 +94,7 @@ def test_task_list_is_the_expected_one_geojson(self): self.assertTupleEqual(expected, self.handler.TASKS["copy"]) def test_is_valid_should_raise_exception_if_the_url_is_invalid(self): - with self.assertRaises(ImportException) as _exc: + with self.assertRaises(Invalid3DTilesException) as _exc: self.handler.is_valid_url(url=self.invalid_files["url"]) self.assertIsNotNone(_exc) diff --git a/geonode/upload/handlers/remote/tests/test_cog.py b/geonode/upload/handlers/remote/tests/test_cog.py index 87f8b086561..589e970389c 100644 --- a/geonode/upload/handlers/remote/tests/test_cog.py +++ b/geonode/upload/handlers/remote/tests/test_cog.py @@ -47,23 +47,35 @@ def test_can_handle_cog(self): self.assertTrue(self.handler.can_handle({"url": "http://example.com/y.tiff", "type": "cog"})) self.assertFalse(self.handler.can_handle({"url": "http://example.com/y.jpg", "type": "image"})) - @patch("geonode.upload.handlers.remote.cog.requests.head") - @patch("geonode.upload.handlers.remote.cog.requests.get") - @patch("geonode.upload.handlers.common.remote.requests.get") - def test_is_valid_url_success(self, mock_base_get, mock_get, mock_head): - mock_head.return_value.headers = {"Accept-Ranges": "bytes"} - mock_head.return_value.status_code = 200 - mock_base_get.return_value.status_code = 200 + @patch("geonode.upload.handlers.remote.cog.is_safe_url_with_redirects") + @patch("geonode.upload.handlers.remote.cog.safe_request_url") + def test_is_valid_url_success(self, mock_safe_request_url, mock_is_safe_url): + mock_is_safe_url.return_value = (True, None) + + mock_head_response = MagicMock() + mock_head_response.headers = {"Accept-Ranges": "bytes"} + mock_head_response.status_code = 200 + mock_head_response.raise_for_status.return_value = None + + mock_safe_request_url.return_value = mock_head_response self.assertTrue(self.handler.is_valid_url(self.valid_url)) - @patch("geonode.upload.handlers.remote.cog.requests.head") - @patch("geonode.upload.handlers.remote.cog.requests.get") - @patch("geonode.upload.handlers.common.remote.requests.get") - def test_is_valid_url_no_range_support(self, mock_base_get, mock_get, mock_head): - mock_head.return_value.headers = {} - mock_get.return_value.status_code = 404 # Not 206 - mock_base_get.return_value.status_code = 200 + @patch("geonode.upload.handlers.remote.cog.is_safe_url_with_redirects") + @patch("geonode.upload.handlers.remote.cog.safe_request_url") + def test_is_valid_url_no_range_support(self, mock_safe_request_url, mock_is_safe_url): + mock_is_safe_url.return_value = (True, None) + + mock_head_response = MagicMock() + mock_head_response.headers = {} + mock_head_response.status_code = 200 + mock_head_response.raise_for_status.return_value = None + + mock_get_response = MagicMock() + mock_get_response.status_code = 404 + mock_get_response.close.return_value = None + + mock_safe_request_url.side_effect = [mock_head_response, mock_get_response] with self.assertRaises(ImportException): self.handler.is_valid_url(self.valid_url) diff --git a/geonode/upload/handlers/remote/tests/test_flatgeobuf.py b/geonode/upload/handlers/remote/tests/test_flatgeobuf.py index 0776f0fa368..50863a8ef30 100644 --- a/geonode/upload/handlers/remote/tests/test_flatgeobuf.py +++ b/geonode/upload/handlers/remote/tests/test_flatgeobuf.py @@ -48,23 +48,35 @@ def test_can_handle_flatgeobuf(self): self.assertTrue(self.handler.can_handle({"url": "http://example.com/y.fgb", "type": "FlatGeobuf"})) self.assertFalse(self.handler.can_handle({"url": "http://example.com/y.jpg", "type": "image"})) - @patch("geonode.upload.handlers.remote.flatgeobuf.requests.head") - @patch("geonode.upload.handlers.remote.flatgeobuf.requests.get") - @patch("geonode.upload.handlers.common.remote.requests.get") - def test_is_valid_url_success(self, mock_base_get, mock_get, mock_head): - mock_head.return_value.headers = {"Accept-Ranges": "bytes"} - mock_head.return_value.status_code = 200 - mock_base_get.return_value.status_code = 200 + @patch("geonode.upload.handlers.remote.flatgeobuf.is_safe_url_with_redirects") + @patch("geonode.upload.handlers.remote.flatgeobuf.safe_request_url") + def test_is_valid_url_success(self, mock_safe_request_url, mock_is_safe_url): + mock_is_safe_url.return_value = (True, None) + + mock_head_response = MagicMock() + mock_head_response.headers = {"Accept-Ranges": "bytes"} + mock_head_response.status_code = 200 + mock_head_response.raise_for_status.return_value = None + + mock_safe_request_url.return_value = mock_head_response self.assertTrue(self.handler.is_valid_url(self.valid_url)) - @patch("geonode.upload.handlers.remote.flatgeobuf.requests.head") - @patch("geonode.upload.handlers.remote.flatgeobuf.requests.get") - @patch("geonode.upload.handlers.common.remote.requests.get") - def test_is_valid_url_no_range_support(self, mock_base_get, mock_get, mock_head): - mock_head.return_value.headers = {} - mock_get.return_value.status_code = 404 - mock_base_get.return_value.status_code = 200 + @patch("geonode.upload.handlers.remote.flatgeobuf.is_safe_url_with_redirects") + @patch("geonode.upload.handlers.remote.flatgeobuf.safe_request_url") + def test_is_valid_url_no_range_support(self, mock_safe_request_url, mock_is_safe_url): + mock_is_safe_url.return_value = (True, None) + + mock_head_response = MagicMock() + mock_head_response.headers = {} + mock_head_response.status_code = 200 + mock_head_response.raise_for_status.return_value = None + + mock_get_response = MagicMock() + mock_get_response.status_code = 404 + mock_get_response.close.return_value = None + + mock_safe_request_url.side_effect = [mock_head_response, mock_get_response] with self.assertRaises(ImportException): self.handler.is_valid_url(self.valid_url) diff --git a/geonode/upload/handlers/remote/tests/test_wms.py b/geonode/upload/handlers/remote/tests/test_wms.py index b21a243484b..277ee000616 100644 --- a/geonode/upload/handlers/remote/tests/test_wms.py +++ b/geonode/upload/handlers/remote/tests/test_wms.py @@ -18,7 +18,7 @@ ######################################################################### from collections import namedtuple from urllib.parse import ParseResult -from django.test import TestCase +from django.test import TestCase, override_settings from mock import MagicMock, patch from geonode.upload.api.exceptions import ImportException from django.contrib.auth import get_user_model @@ -113,6 +113,7 @@ def test_is_valid_should_raise_exception_if_the_url_is_invalid(self): self.assertIsNotNone(_exc) self.assertTrue("The provided url is not reachable") + @override_settings(SAFE_URL_TRUSTED_HOSTS=["geoserver:8080"]) def test_is_valid_should_pass_with_valid_url(self): self.handler.is_valid_url(url=self.valid_payload_with_parse_false["url"]) diff --git a/geonode/upload/handlers/remote/tiles3d.py b/geonode/upload/handlers/remote/tiles3d.py index 94d655a3716..293b2f9dfd9 100644 --- a/geonode/upload/handlers/remote/tiles3d.py +++ b/geonode/upload/handlers/remote/tiles3d.py @@ -18,7 +18,6 @@ ######################################################################### import logging -import requests from geonode.layers.models import Dataset from geonode.upload.handlers.common.remote import BaseRemoteResourceHandler from geonode.upload.handlers.common.serializer import RemoteResourceSerializer @@ -27,7 +26,7 @@ from geonode.upload.handlers.tiles3d.exceptions import Invalid3DTilesException from geonode.base.enumerations import SOURCE_TYPE_REMOTE from geonode.base.models import ResourceBase -from geonode.utils import is_safe_url_with_redirects +from geonode.utils import is_safe_url_with_redirects, safe_request_url logger = logging.getLogger("importer") @@ -60,9 +59,13 @@ def have_table(self): @staticmethod def is_valid_url(url, **kwargs): - BaseRemoteResourceHandler.is_valid_url(url) + # Reject disallowed URLs early, before running the remaining validity checks. + is_safe, _ = is_safe_url_with_redirects(url) + if not is_safe: + raise Invalid3DTilesException("The requested URL is not allowed.") + BaseRemoteResourceHandler.is_valid_url(url, **kwargs) try: - payload = requests.get(url, timeout=10).json() + payload = safe_request_url("GET", url, timeout=10).json() # required key described in the specification of 3dtiles # https://docs.ogc.org/cs/22-025r4/22-025r4.html#toc92 is_valid = all(key in payload.keys() for key in ("asset", "geometricError", "root")) @@ -92,12 +95,9 @@ def create_geonode_resource( _exec = orchestrator.get_execution_object(exec_id=execution_id) url = _exec.input_params.get("url") - is_safe, unsafe_url = is_safe_url_with_redirects(url) - if not is_safe: - raise Invalid3DTilesException("Invalid URL Provided") try: - js_file = requests.get(url, timeout=10).json() + js_file = safe_request_url("GET", url, timeout=10).json() except Exception as e: raise Invalid3DTilesException(e) diff --git a/geonode/utils.py b/geonode/utils.py index a7f143c0b29..df699653070 100755 --- a/geonode/utils.py +++ b/geonode/utils.py @@ -1935,3 +1935,24 @@ def is_safe_url_with_redirects(url: str, max_redirects: int = 3): return False, current_url return False, current_url + + +class SafeSession(requests.Session): + """requests Session that validates the target host of every request it + dispatches, including each redirect hop, before the socket is opened. + """ + + def send(self, request, **kwargs): + # request.url is the fully-resolved absolute URL of this hop. + if not is_safe_url(request.url): + # Keep the raised message generic: the URL may carry tokens or credentials. + logger.debug("Rejected request to disallowed host: %s", urlparse(request.url).hostname) + raise requests.exceptions.InvalidURL("The requested URL is not allowed.") + return super().send(request, **kwargs) + + +def safe_request_url(method, url, max_redirects=3, **kwargs): + kwargs.setdefault("timeout", 10) + with SafeSession() as session: + session.max_redirects = max_redirects + return session.request(method, url, **kwargs) From 54709bf7f56301a43c3505ba43c94f2e9afb778d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 31 Aug 2026 14:55:27 +0200 Subject: [PATCH 68/98] [Fixes #14544] Attribute fetch for ArcGIS REST remote datasets is broken (#14545) (#14570) * [Fixes #14544] Attribute fetch for ArcGIS REST remote datasets is broken (cherry picked from commit e277ee3067f5864aea60b2d036ce4426676af1d1) Co-authored-by: Emanuele Tajariol --- geonode/geoserver/helpers.py | 51 +++++++++---- geonode/geoserver/tests/test_helpers.py | 95 +++++++++++++++++++++++++ 2 files changed, 133 insertions(+), 13 deletions(-) diff --git a/geonode/geoserver/helpers.py b/geonode/geoserver/helpers.py index e2295107faf..8d2b8cd0502 100755 --- a/geonode/geoserver/helpers.py +++ b/geonode/geoserver/helpers.py @@ -31,7 +31,7 @@ from itertools import cycle from collections import defaultdict from os.path import basename, splitext, isfile -from urllib.parse import urlparse, urlencode, urlsplit, urljoin +from urllib.parse import urlparse, urlunparse, urlencode, urlsplit, urljoin, parse_qsl from bs4 import BeautifulSoup import xml.etree.ElementTree as ET @@ -76,6 +76,7 @@ is_monochromatic_image, set_resource_default_links, normalize_bbox_to_float_list, + safe_request_url, ) from .geofence import GeoFenceClient, GeoFenceUtils @@ -1003,6 +1004,10 @@ def set_attributes_from_geoserver(layer, overwrite=False): then store in GeoNode database using Attribute model """ attribute_map = [] + if layer.subtype == "remote" and not layer.remote_service: + # avoid falling through to the local-GeoServer branches, which would wipe existing attributes + logger.debug(f"Dataset '{layer.alternate or layer.typename}' is remote but has no remote_service yet") + return if getattr(layer, "remote_service") and layer.remote_service: server_url = layer.remote_service.service_url if layer.remote_service.operations.get("GetCapabilities", None) and layer.remote_service.operations.get( @@ -1014,20 +1019,40 @@ def set_attributes_from_geoserver(layer, overwrite=False): break else: server_url = ogc_server_settings.LOCATION - if layer.subtype in ["tileStore", "remote"] and layer.remote_service.ptype == "gxp_arcrestsource": - logger.info(f"Getting info for {layer.subtype} '{layer.alternate or layer.typename}'") - dft_url = f"{server_url}{(layer.alternate or layer.typename)}?f=json" + if layer.subtype == "remote" and layer.remote_service and layer.remote_service.ptype == "gxp_arcrestsource": + resource_identifier = layer.alternate or layer.typename + logger.info(f"Getting info for {layer.subtype} '{resource_identifier}'") + # `resource_identifier` is GeoNode's own "workspace:name" form (e.g. "remoteWorkspace:0"), + # not a real ArcGIS REST path: the actual layer id is whatever follows the last ':'. + arcgis_layer_id = resource_identifier.rsplit(":", 1)[-1] + # server_url may already carry a query string (e.g. an auth token via extra_queryparams): + # append the layer id to the path and merge f=json into the existing query, not the string. + parsed_server_url = urlparse(server_url) + query = dict(parse_qsl(parsed_server_url.query)) + query["f"] = "json" + dft_url = urlunparse( + parsed_server_url._replace( + path=f"{parsed_server_url.path.rstrip('/')}/{arcgis_layer_id}", + query=urlencode(query), + ) + ) try: - # The code below will fail if http_client cannot be imported - req, body = http_client.get(dft_url, user=_user) - body = json.loads(body) - attribute_map = [ - [n["name"], _esri_types[n["type"]]] for n in body["fields"] if n.get("name") and n.get("type") - ] + # Not http_client(user=...): that would send our own GeoServer OAuth token to this third party. + remote_auth = None + if layer.remote_service.needs_authentication: + from geonode.security.auth_registry import auth_handler_registry + + remote_auth = auth_handler_registry.build(layer.remote_service.auth_config).get_request_auth() + response = safe_request_url("GET", dft_url, auth=remote_auth, timeout=10) + response.raise_for_status() + body = response.json() + attribute_map = [] + for n in body["fields"]: + esri_type = _esri_types.get(n.get("type")) + if n.get("name") and esri_type: + attribute_map.append([n["name"], esri_type]) except Exception: - logger.warning( - f"Error while retrieving info for {layer.subtype} '{layer.alternate or layer.typename}'", exc_info=True - ) + logger.warning(f"Error while retrieving info for {layer.subtype} '{resource_identifier}'", exc_info=True) attribute_map = [] elif layer.can_have_wps_links: typename = layer.alternate if layer.alternate else layer.typename diff --git a/geonode/geoserver/tests/test_helpers.py b/geonode/geoserver/tests/test_helpers.py index f5a4ac5f879..54ad2f105ad 100644 --- a/geonode/geoserver/tests/test_helpers.py +++ b/geonode/geoserver/tests/test_helpers.py @@ -19,6 +19,7 @@ import re import time import logging +import requests from urllib.parse import urljoin @@ -43,9 +44,12 @@ get_dataset_capabilities_url, get_layer_ows_url, get_time_info, + set_attributes_from_geoserver, _sync_geoserver_keywords_to_instance, ) from geonode.geoserver.ows import _wcs_link, _wfs_link, _wms_link +from geonode.services.models import Service +from geonode.services.enumerations import REST_MAP, INDEXED from unittest.mock import patch, Mock @@ -208,6 +212,97 @@ def test_remoteStore_should_return_remote(self): el = get_dataset_storetype("remoteStore") self.assertEqual("remote", el) + def _create_arcgis_dataset(self): + service = Service.objects.create( + uuid=str(uuid4()), + owner=get_user_model().objects.get(username=self.user), + type=REST_MAP, + method=INDEXED, + base_url="https://sampleserver6.arcgisonline.com/arcgis/rest/services/USA/MapServer", + name="arcgis-test-service", + title="ArcGIS test service", + ) + return Dataset.objects.create( + uuid=str(uuid4()), + owner=get_user_model().objects.get(username=self.user), + name="usa_states", + store="arcgis-test-service", + subtype="remote", + workspace="remoteWorkspace", + typename="remoteWorkspace:0", + alternate="remoteWorkspace:0", + remote_service=service, + ) + + @patch("geonode.geoserver.helpers.safe_request_url") + def test_set_attributes_from_geoserver_arcgis_remote_dataset(self, mock_safe_request_url): + dataset = self._create_arcgis_dataset() + mock_response = Mock() + mock_response.json.return_value = { + "fields": [ + {"name": "STATE_NAME", "type": "esriFieldTypeString"}, + {"name": "POPULATION", "type": "esriFieldTypeDouble"}, + {"name": "SOME_FIELD", "type": "esriFieldTypeNotSupported"}, + ] + } + mock_safe_request_url.return_value = mock_response + + set_attributes_from_geoserver(dataset) + + method, called_url = mock_safe_request_url.call_args[0] + self.assertEqual(method, "GET") + self.assertTrue( + called_url.startswith("https://sampleserver6.arcgisonline.com/arcgis/rest/services/USA/MapServer/0") + ) + self.assertIn("f=json", called_url) + attributes = set(dataset.attribute_set.values_list("attribute", "attribute_type")) + self.assertIn(("STATE_NAME", "xsd:string"), attributes) + self.assertIn(("POPULATION", "xsd:double"), attributes) + self.assertFalse(dataset.attribute_set.filter(attribute="SOME_FIELD").exists()) + + @patch("geonode.geoserver.helpers.safe_request_url") + def test_set_attributes_from_geoserver_arcgis_preserves_existing_query_string(self, mock_safe_request_url): + dataset = self._create_arcgis_dataset() + dataset.remote_service.extra_queryparams = "token=abc123" + dataset.remote_service.save() + mock_response = Mock() + mock_response.json.return_value = {"fields": [{"name": "STATE_NAME", "type": "esriFieldTypeString"}]} + mock_safe_request_url.return_value = mock_response + + set_attributes_from_geoserver(dataset) + + called_url = mock_safe_request_url.call_args[0][1] + self.assertIn("token=abc123", called_url) + self.assertIn("f=json", called_url) + + @patch("geonode.geoserver.helpers.safe_request_url") + def test_set_attributes_from_geoserver_arcgis_http_error(self, mock_safe_request_url): + dataset = self._create_arcgis_dataset() + mock_response = Mock() + mock_response.raise_for_status.side_effect = requests.exceptions.HTTPError("500 error") + mock_safe_request_url.return_value = mock_response + + set_attributes_from_geoserver(dataset) + + self.assertFalse(dataset.attribute_set.exists()) + + def test_set_attributes_from_geoserver_arcgis_no_remote_service_yet(self): + dataset = Dataset.objects.create( + uuid=str(uuid4()), + owner=get_user_model().objects.get(username=self.user), + name="usa_states_pending", + store="arcgis-test-service", + subtype="remote", + workspace="remoteWorkspace", + typename="remoteWorkspace:1", + alternate="remoteWorkspace:1", + ) + Attribute.objects.create(dataset=dataset, attribute="STATE_NAME", attribute_type="xsd:string") + + set_attributes_from_geoserver(dataset, overwrite=True) + + self.assertTrue(dataset.attribute_set.filter(attribute="STATE_NAME").exists()) + @on_ogc_backend(geoserver.BACKEND_PACKAGE) def test_geoserver_proxy_strip_paths(self): response = self.client.get( From e35737c7ac39f870d0f86e14f36976fdace96559 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 2 Sep 2026 12:13:48 +0200 Subject: [PATCH 69/98] Improvements in resource thumbnail handling (#14571) (#14572) (cherry picked from commit d7c8fa5fcf9d014b7a4c2ebe6bb296294edc0b34) Co-authored-by: Sijan Dhungana --- geonode/base/api/tests.py | 23 +++++++++++++++++++++++ geonode/base/api/views.py | 7 +++++++ 2 files changed, 30 insertions(+) diff --git a/geonode/base/api/tests.py b/geonode/base/api/tests.py index 0d0be73cabd..4f2482755a1 100644 --- a/geonode/base/api/tests.py +++ b/geonode/base/api/tests.py @@ -2188,6 +2188,29 @@ def test_set_thumbnail_from_bbox_from_Anonymous_user_raise_permission_error(self self.assertEqual(response.status_code, 401) self.assertEqual(expected, response.json()) + def test_set_thumbnail_from_bbox_without_resource_change_permission_rejected(self): + bobby = get_user_model().objects.get(username="bobby") + resource = Dataset.objects.first() + resource.owner = bobby + resource.save() + + self.client.logout() + self.assertTrue(self.client.login(username="norman", password="norman")) + + url = reverse("base-resources-set-thumb-from-bbox", kwargs={"resource_id": resource.id}) + payload = { + "bbox": [-9072629.904175375, -9043966.018568434, 1491839.8773032012, 1507127.2829602365], + "srid": "EPSG:3857", + } + + response = self.client.post(url, data=payload, format="json") + + self.assertEqual(response.status_code, 403) + self.assertEqual( + response.json(), + {"message": "You do not have permission to set this thumbnail.", "success": False}, + ) + @patch("geonode.layers.manager.DatasetResourceManager.set_thumbnail") def test_set_thumbnail_from_bbox_from_logged_user_for_existing_dataset(self, mock_set_thumbnail): """ diff --git a/geonode/base/api/views.py b/geonode/base/api/views.py index d334bb4df42..3d4f62e95e4 100644 --- a/geonode/base/api/views.py +++ b/geonode/base/api/views.py @@ -653,6 +653,13 @@ def set_thumbnail_from_bbox(self, request, resource_id, *args, **kwargs): try: resource = ResourceBase.objects.get(id=ast.literal_eval(resource_id)) + # Check if the current user has the permissions to set the thumbnail + if not request.user.has_perm("change_resourcebase", resource.get_self_resource()): + return Response( + {"message": "You do not have permission to set this thumbnail.", "success": False}, + status=status.HTTP_403_FORBIDDEN, + ) + map_thumb_from_bbox = False if isinstance(resource.get_real_instance(), Map): map_thumb_from_bbox = True From 5d99c073a8cac33de6261f59573a81876b8ea41d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 3 Sep 2026 09:37:44 +0200 Subject: [PATCH 70/98] improvement in harvesters endpoint (#14567) (#14569) (cherry picked from commit f6e1959ba8679ff3677b917909378394ca1e0e76) Co-authored-by: Sijan Dhungana --- geonode/harvesting/api/views.py | 15 ++++++--------- geonode/harvesting/tests/test_api_views.py | 15 +++++++++++++-- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/geonode/harvesting/api/views.py b/geonode/harvesting/api/views.py index faef8473693..006b4fc0862 100644 --- a/geonode/harvesting/api/views.py +++ b/geonode/harvesting/api/views.py @@ -33,20 +33,14 @@ logger = logging.getLogger(__name__) -class IsAdminOrListOnly(rest_framework.permissions.BasePermission): +class IsSuperUser(rest_framework.permissions.BasePermission): def has_permission(self, request, view): - if request.user.is_superuser: - result = True - elif view.action == "list": - result = True - else: - result = False - return result + return request.user and request.user.is_superuser class HarvesterViewSet(NestedViewSetMixin, DynamicModelViewSet): permission_classes = [ - IsAdminOrListOnly, + IsSuperUser, ] queryset = models.Harvester.objects.all() serializer_class = serializers.HarvesterSerializer @@ -67,6 +61,9 @@ class HarvestableResourceViewSet( mixins.ListModelMixin, viewsets.GenericViewSet, ): + permission_classes = [ + IsSuperUser, + ] queryset = models.HarvestableResource.objects.all() pagination_class = GeoNodeApiPagination serializer_class = serializers.HarvestableResourceSerializer diff --git a/geonode/harvesting/tests/test_api_views.py b/geonode/harvesting/tests/test_api_views.py index c7bfac46ca3..2a9f21d2c8f 100644 --- a/geonode/harvesting/tests/test_api_views.py +++ b/geonode/harvesting/tests/test_api_views.py @@ -45,7 +45,12 @@ def setUpTestData(cls): ) cls.sessions = [session1, session2] - def test_get_harvester_list(self): + def test_get_harvester_list_for_non_admin(self): + response = self.client.get("/api/v2/harvesters/") + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_get_harvester_list_for_admin(self): + self.client.login(username="admin", password="admin") response = self.client.get("/api/v2/harvesters/") self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data["total"], len(self.harvesters)) @@ -73,7 +78,13 @@ def test_get_harvester_detail_for_admin(self): self.assertEqual(response.data["harvester"]["id"], harvester.pk) self.assertEqual(response.data["harvester"]["name"], harvester.name) - def test_get_harvester_resources(self): + def test_get_harvester_resources_for_non_admin(self): + harvester = self.harvesters[0] + response = self.client.get(f"/api/v2/harvesters/{harvester.id}/harvestable-resources/") + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_get_harvester_resources_for_admin(self): + self.client.login(username="admin", password="admin") for index, harvester in enumerate(self.harvesters): response = self.client.get(f"/api/v2/harvesters/{harvester.id}/harvestable-resources/") self.assertEqual(response.status_code, status.HTTP_200_OK) From 38fbf563c095d8865988951fa9e6ca465116d70a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 3 Sep 2026 09:38:19 +0200 Subject: [PATCH 71/98] document thumbnail issue fixed (#14560) (#14578) (cherry picked from commit 1a318e6ec26b5b7cb9589e2e6075fbd599973a50) Co-authored-by: Sijan Dhungana --- geonode/documents/tasks.py | 4 +++- geonode/resource/manager.py | 11 ++++++++--- geonode/resource/tests.py | 9 +++++++++ 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/geonode/documents/tasks.py b/geonode/documents/tasks.py index 316bdafdfe8..6a51faaae36 100644 --- a/geonode/documents/tasks.py +++ b/geonode/documents/tasks.py @@ -99,7 +99,9 @@ def create_document_thumbnail(self, object_id): except Document.DoesNotExist: logger.error(f"Document #{object_id} does not exist.") raise - + if not document.is_local: + logger.info(f"Skipping thumbnail generation for remote document: {document.doc_url}") + return image_file = None thumbnail_content = None remove_tmp_file = False diff --git a/geonode/resource/manager.py b/geonode/resource/manager.py index 2e6fa8e095b..9c8894e3105 100644 --- a/geonode/resource/manager.py +++ b/geonode/resource/manager.py @@ -958,9 +958,14 @@ def set_thumbnail( file_name = _generate_thumbnail_name(_resource.get_real_instance()) _resource.save_thumbnail(file_name, thumbnail, thumbnail_algorithm=thumbnail_algorithm) else: - if instance and isinstance(instance.get_real_instance(), Document): - if overwrite or not instance.thumbnail_url: - create_document_thumbnail.apply((instance.id,)) + if instance: + doc = instance.get_real_instance() + if isinstance(doc, Document): + if not doc.is_local: + logger.info(f"Skipping thumbnail generation for remote document: {doc.doc_url}") + return False + if overwrite or not instance.thumbnail_url: + create_document_thumbnail.apply((instance.id,)) self._concrete_resource_manager.set_thumbnail( uuid, instance=_resource, diff --git a/geonode/resource/tests.py b/geonode/resource/tests.py index 87eba22d9ce..8a2c671f409 100644 --- a/geonode/resource/tests.py +++ b/geonode/resource/tests.py @@ -455,6 +455,15 @@ def test_set_thumbnail(self): self.assertTrue(self.rm.set_thumbnail(dt.uuid, instance=dt)) self.assertTrue(self.rm.set_thumbnail(doc.uuid, instance=doc)) + @patch("geonode.documents.tasks.create_document_thumbnail.apply") + def test_set_thumbnail_skips_remote_document(self, create_thumb): + doc = create_single_doc("test_remote_thumb_doc") + doc.doc_url = "http://example.com/test.pdf" + doc.save() + + self.assertFalse(self.rm.set_thumbnail(doc.uuid, instance=doc)) + create_thumb.assert_not_called() + def test_set_thumbnail_algo(self): thumb_path = os.path.join(os.path.dirname(__file__), "../tests/data/thumb_sample.png") image = io.open(thumb_path, "rb").read() From 5454211b1ddf159562ac78373ac121cf838c8244 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 3 Sep 2026 10:56:02 +0200 Subject: [PATCH 72/98] Improvements of catalouge metadata (#14576) (#14579) (cherry picked from commit 62c095323503474fe1daa0761e8ff4ce0b8ce683) Co-authored-by: Sijan Dhungana --- geonode/catalogue/tests.py | 29 ++++++++++++++++++++++++++++- geonode/catalogue/views.py | 14 ++++++++++++-- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/geonode/catalogue/tests.py b/geonode/catalogue/tests.py index b0a3e69e4de..db6d2bd976f 100644 --- a/geonode/catalogue/tests.py +++ b/geonode/catalogue/tests.py @@ -34,7 +34,12 @@ from geonode.tests.base import GeoNodeBaseTestSupport from geonode.catalogue.models import catalogue_post_save -from geonode.catalogue.views import csw_global_dispatch, resolve_uuid +from geonode.catalogue.views import ( + csw_global_dispatch, + resolve_uuid, + csw_render_extra_format_html, + csw_render_extra_format_txt, +) from geonode.layers.populate_datasets_data import create_dataset_data from geonode.base.populate_test_data import all_public, create_models, remove_models, create_single_dataset @@ -210,6 +215,28 @@ def test_localized_keyword_in_metadata(self): self.assertIn(self.keyword_label.label, rendered_xml) self.assertNotIn(self.keyword.alt_label, rendered_xml) + def test_csw_extra_format_html_denies_anonymous_private_resource(self): + self.dataset.set_permissions( + {"groups": {"registered-members": ["base.view_resourcebase", "base.download_resourcebase"]}} + ) + + request = RequestFactory().get(f"/catalogue/csw_to_extra_format/{self.dataset.uuid}/{self.dataset.name}.html") + request.user = AnonymousUser() + + with self.assertRaises(PermissionDenied): + csw_render_extra_format_html(request, self.dataset.uuid, self.dataset.name) + + def test_csw_extra_format_txt_denies_anonymous_private_resource(self): + self.dataset.set_permissions( + {"groups": {"registered-members": ["base.view_resourcebase", "base.download_resourcebase"]}} + ) + + request = RequestFactory().get(f"/catalogue/csw_to_extra_format/{self.dataset.uuid}/{self.dataset.name}.txt") + request.user = AnonymousUser() + + with self.assertRaises(PermissionDenied): + csw_render_extra_format_txt(request, self.dataset.uuid, self.dataset.name) + @override_settings(MULTILANG_FIELDS=["title", "abstract"]) class IsMultilangFilterTest(GeoNodeBaseTestSupport): diff --git a/geonode/catalogue/views.py b/geonode/catalogue/views.py index 2d6b6dbae57..00da3ce4a9d 100644 --- a/geonode/catalogue/views.py +++ b/geonode/catalogue/views.py @@ -220,7 +220,12 @@ def get_keywords(resource): @csrf_exempt def csw_render_extra_format_txt(request, layeruuid, resname): """pycsw wrapper""" - resource = ResourceBase.objects.get(uuid=layeruuid) + resource = resolve_object( + request, + ResourceBase, + {"uuid": layeruuid}, + permission="base.view_resourcebase", + ) chrs = get_CSV_spec_char() s = chrs["separator"] c = chrs["carriage_return"] @@ -299,7 +304,12 @@ def __append_contact_role__(content, cr_attr_name, title_in_txt): def csw_render_extra_format_html(request, layeruuid, resname): - resource = ResourceBase.objects.get(uuid=layeruuid) + resource = resolve_object( + request, + ResourceBase, + {"uuid": layeruuid}, + permission="base.view_resourcebase", + ) extra_res_md = {} try: sprt = SpatialRepresentationType.objects.get(id=resource.spatial_representation_type_id) From 3d400a63c62f495b8e6fc51aaff7bc03158fed52 Mon Sep 17 00:00:00 2001 From: mattiagiupponi <51856725+mattiagiupponi@users.noreply.github.com> Date: Mon, 14 Sep 2026 16:58:03 +0200 Subject: [PATCH 73/98] BACKPORT Fixes #14574 Improvements on cloning (#14598) --- docs/src/user-guide/resource_info.md | 21 +++++ geonode/base/api/tests.py | 4 +- geonode/geoapps/tests.py | 62 ++++++++++++++- geonode/resource/manager.py | 49 ++++++++++-- geonode/resource/tests.py | 77 +++++++++++++++++- geonode/upload/celery_tasks.py | 37 ++++++--- geonode/upload/handlers/common/raster.py | 6 +- geonode/upload/handlers/common/remote.py | 26 +++++- .../upload/handlers/common/tests_vector.py | 79 +++++++++++++++++++ geonode/upload/handlers/common/vector.py | 31 +++++--- geonode/upload/handlers/shapefile/handler.py | 6 +- geonode/upload/handlers/tiles3d/handler.py | 24 +++++- 12 files changed, 380 insertions(+), 42 deletions(-) diff --git a/docs/src/user-guide/resource_info.md b/docs/src/user-guide/resource_info.md index 64367cb50bf..437173a18ee 100644 --- a/docs/src/user-guide/resource_info.md +++ b/docs/src/user-guide/resource_info.md @@ -71,6 +71,27 @@ From the lower right toolbar on the thumbnail part of the properties panel, it i - Copy the resource URL - Copy the OGC resource web services URL (in the case of a `Dataset`) +## Cloning a resource + +Cloning creates a new, fully independent resource. It gets its own UUID, its own permissions record, and, for a `Dataset`, its own copy of the data on the GIS backend. Ownership of the clone is transferred to whoever triggers it, regardless of who owned the source. + +What is carried over from the source: + +- Metadata: title, abstract, category, license, and every other descriptive field +- Keywords, regions, and thesaurus keywords +- Contacts and their roles (point of contact, metadata author, and so on) +- Geographic access limits (per-user and per-group) +- Permissions: the clone starts with the same permission spec as the source, not the default permissions a newly created resource would get +- Linked resources (e.g. a `Map`'s linked `Datasets`) +- Type-specific data: a `Dataset`'s attribute table, a `Map`'s layers, and the underlying files/assets + +What does not carry over: + +- The owner, which becomes the user who triggered the clone +- The `featured` flag, always reset to off on the clone + +Because the clone owns its own copy of everything above rather than sharing rows with the source, deleting the source resource afterward does not affect the clone. + You can access the resource details page by clicking the button on the right (`View dataset` in the case of a `dataset`) in the overview panel. That page looks like the one shown in the picture below. diff --git a/geonode/base/api/tests.py b/geonode/base/api/tests.py index 4f2482755a1..25a9d81beee 100644 --- a/geonode/base/api/tests.py +++ b/geonode/base/api/tests.py @@ -2932,7 +2932,7 @@ def test_resource_service_copy_with_perms_dataset(self): @patch.dict(os.environ, {"ASYNC_SIGNALS": "False"}) @override_settings(ASYNC_SIGNALS=False) - def test_resource_service_copy_with_perms_dataset_set_default_perms(self): + def test_resource_service_copy_with_perms_dataset_keep_original_perms(self): with self.settings(ASYNC_SIGNALS=False): files = os.path.join(gisdata.GOOD_DATA, "vector/single_point.shp") files_as_dict, _ = get_files(files) @@ -2973,7 +2973,7 @@ def test_resource_service_copy_with_perms_dataset_set_default_perms(self): self.assertEqual("finished", self.client.get(response.json().get("status_url")).json().get("status")) _resource = Dataset.objects.filter(title__icontains="test_copy_with_perms").last() self.assertIsNotNone(_resource) - self.assertNotIn( + self.assertIn( "bobby", [x.username for x in permissions_registry.get_perms(instance=_resource).get("users", [])], ) diff --git a/geonode/geoapps/tests.py b/geonode/geoapps/tests.py index 903055016d6..685d5b69db3 100644 --- a/geonode/geoapps/tests.py +++ b/geonode/geoapps/tests.py @@ -21,8 +21,10 @@ from django.contrib.auth import get_user_model from geonode.geoapps.models import GeoApp -from geonode.base.models import TopicCategory +from geonode.base.models import TopicCategory, Region, Thesaurus, ThesaurusKeyword +from geonode.groups.models import GroupProfile from geonode.resource.registry import resource_manager_registry, geoapp_manager +from geonode.security.registry import permissions_registry from geonode.tests.base import GeoNodeBaseTestSupport from geonode.metadata.manager import metadata_manager from geonode.base.populate_test_data import all_public, create_models, remove_models @@ -82,11 +84,67 @@ def test_geoapp_copy(self): self.client.login(username="admin", password="admin") geoapp_copy = None try: + # owner must be whoever triggers the clone (self.bobby), not self.geoapp's own owner (self.user) geoapp_copy = resource_manager_registry.get_for_instance(self.geoapp).copy( - self.geoapp, defaults=dict(title="Testing GeoApp 2") + self.geoapp, owner=self.bobby, defaults=dict(title="Testing GeoApp 2") ) self.assertIsNotNone(geoapp_copy) self.assertEqual(geoapp_copy.title, "Testing GeoApp 2") + self.assertEqual(geoapp_copy.owner, self.bobby) + finally: + if geoapp_copy: + geoapp_copy.delete() + self.assertIsNotNone(self.geoapp) + + def test_geoapp_copy_carries_over_metadata_and_permissions(self): + """M2M metadata and perm_spec must survive a GeoApp copy too, same as Dataset/Map.""" + self.client.login(username="admin", password="admin") + # update() drops keywords/regions kwargs, set M2M fields directly instead + region = Region.objects.first() + self.geoapp = geoapp_manager.update( + self.geoapp.uuid, instance=self.geoapp, vals={"abstract": "test abstract", "purpose": "test purpose"} + ) + self.geoapp.keywords.add("foo", "bar") + self.geoapp.regions.add(region) + thesaurus = Thesaurus.objects.create(identifier="test_thesaurus_geoapp_copy", title="Test Thesaurus") + tkeyword = ThesaurusKeyword.objects.create(thesaurus=thesaurus, alt_label="test_tkeyword") + self.geoapp.tkeywords.add(tkeyword) + + custom_group, _ = GroupProfile.objects.get_or_create( + slug="geoapp_copy_group", title="geoapp_copy_group", access="private" + ) + custom_perms = { + "users": {self.bobby.username: ["view_resourcebase", "change_resourcebase"]}, + "groups": {custom_group.slug: ["view_resourcebase"]}, + } + geoapp_manager.set_permissions(self.geoapp.uuid, instance=self.geoapp, permissions=custom_perms) + + geoapp_copy = None + try: + geoapp_copy = resource_manager_registry.get_for_instance(self.geoapp).copy( + self.geoapp, owner=self.bobby, defaults=dict(title="Testing GeoApp Metadata Copy") + ) + self.assertIsNotNone(geoapp_copy) + self.assertEqual(geoapp_copy.owner, self.bobby) + self.assertEqual(self.geoapp.abstract, geoapp_copy.abstract) + self.assertEqual(self.geoapp.purpose, geoapp_copy.purpose) + self.assertCountEqual( + [k.name for k in self.geoapp.keywords.all()], [k.name for k in geoapp_copy.keywords.all()] + ) + self.assertCountEqual(list(self.geoapp.regions.all()), list(geoapp_copy.regions.all())) + self.assertCountEqual(list(self.geoapp.tkeywords.all()), list(geoapp_copy.tkeywords.all())) + + source_perms = permissions_registry.get_perms(instance=self.geoapp, include_virtual=False) + copy_perms = permissions_registry.get_perms(instance=geoapp_copy, include_virtual=False) + for perm_key in ("users", "groups"): + source_entries = {profile.pk: set(perms) for profile, perms in source_perms.get(perm_key, {}).items()} + copy_entries = {profile.pk: set(perms) for profile, perms in copy_perms.get(perm_key, {}).items()} + # bobby is also the clone's new owner, who always gets full owner perms on top + # of whatever perm_spec is passed, regardless of what the source spec granted him + source_entries.pop(self.bobby.pk, None) + copy_entries.pop(self.bobby.pk, None) + self.assertEqual(source_entries, copy_entries) + self.assertIn("change_resourcebase_permissions", copy_perms["users"][self.bobby]) finally: if geoapp_copy: geoapp_copy.delete() diff --git a/geonode/resource/manager.py b/geonode/resource/manager.py index 9c8894e3105..baadfb1f460 100644 --- a/geonode/resource/manager.py +++ b/geonode/resource/manager.py @@ -41,7 +41,7 @@ from django.core.exceptions import ValidationError, FieldDoesNotExist from geonode.assets.utils import create_asset_and_link_dict, rollback_asset_and_link, copy_assets_and_links, create_link -from geonode.base.models import ResourceBase, LinkedResource +from geonode.base.models import ResourceBase, LinkedResource, ContactRole, UserGeoLimit, GroupGeoLimit from geonode.documents.tasks import create_document_thumbnail from geonode.metadata.manager import metadata_manager from geonode.thumbs.thumbnails import _generate_thumbnail_name @@ -496,26 +496,60 @@ def update( def copy( self, instance: ResourceBase, /, uuid: str = None, owner: settings.AUTH_USER_MODEL = None, defaults: dict = {} ) -> ResourceBase: + """Clone a resource under a new pk/uuid, owned by `owner`. + + Scalar/FK fields (title, abstract, license, ...) come along via copy.copy(). + Everything else lives in a join table keyed by the old pk and needs re-linking by + hand: keywords/regions/tkeywords, contacts (ContactRole), geolimits (own new rows, + not shared with the source), linked resources, dataset attributes/maplayers, assets, + and the source's permission spec (re-applied after update(), which resets to defaults). + """ + if owner is None: + raise ValueError("copy() requires an explicit 'owner': the user who triggered the clone") _resource = None if instance: try: instance.set_processing_state(enumerations.STATE_RUNNING) with transaction.atomic(): _resource = copy.copy(instance.get_real_instance()) - _resource.owner = owner or instance.get_real_instance().owner + _resource.owner = owner _resource.pk = _resource.id = None _resource.uuid = uuid or str(uuid4()) # Ensure that the featured flag is set to False _resource.featured = False try: - # Avoid Integrity errors... _resource.get_real_instance()._meta.get_field("name") - _name = defaults.get("name", _resource.get_real_instance().name) - _resource.get_real_instance().name = defaults["name"] = f"{_name}_{uuid1().hex[:8]}" + if "name" in defaults: + _resource.get_real_instance().name = defaults["name"] + else: + _name = _resource.get_real_instance().name + _resource.get_real_instance().name = defaults["name"] = f"{_name}_{uuid1().hex[:8]}" except FieldDoesNotExist: if "name" in defaults: defaults.pop("name") _resource.save() + _src = instance.get_real_instance() + _dst = _resource.get_real_instance() + # M2M fields live in join tables keyed by pk, not carried by copy.copy() + for _field in ("keywords", "regions", "tkeywords"): + getattr(_dst, _field).set(getattr(_src, _field).all()) + # contacts goes through ContactRole (has a "role" field), plain .set() doesn't work + for _contact_role in ContactRole.objects.filter(resource=instance): + ContactRole.objects.get_or_create( + resource=_dst, contact=_contact_role.contact, role=_contact_role.role + ) + # UserGeoLimit/GroupGeoLimit own a `resource` FK (CASCADE), reusing the source's + # rows via .set() would tie the clone's limits to the source resource's lifetime + for _geolimit in UserGeoLimit.objects.filter(resource=instance): + _new_geolimit = UserGeoLimit.objects.create( + user=_geolimit.user, resource=_dst, wkt=_geolimit.wkt + ) + _dst.users_geolimits.add(_new_geolimit) + for _geolimit in GroupGeoLimit.objects.filter(resource=instance): + _new_geolimit = GroupGeoLimit.objects.create( + group=_geolimit.group, resource=_dst, wkt=_geolimit.wkt + ) + _dst.groups_geolimits.add(_new_geolimit) for lr in LinkedResource.get_linked_resources(source=instance.pk, is_internal=False): LinkedResource.objects.get_or_create( source_id=_resource.pk, target_id=lr.target.pk, internal=False @@ -558,7 +592,10 @@ def copy( to_update.update(defaults) # Refresh from DB _resource.refresh_from_db() - return self.update(_resource.uuid, _resource, vals=to_update) + _resource = self.update(_resource.uuid, _resource, vals=to_update) + _source_perms = permissions_registry.get_perms(instance=instance, include_virtual=False) + self.set_permissions(_resource.uuid, instance=_resource, permissions=_source_perms) + return _resource except Exception as e: logger.exception(e) finally: diff --git a/geonode/resource/tests.py b/geonode/resource/tests.py index 8a2c671f409..c89adbac7c8 100644 --- a/geonode/resource/tests.py +++ b/geonode/resource/tests.py @@ -29,7 +29,7 @@ from geonode.base.populate_test_data import create_models from geonode.tests.base import GeoNodeBaseTestSupport from geonode.resource.manager import BaseResourceManager -from geonode.base.models import LinkedResource, ResourceBase +from geonode.base.models import LinkedResource, ResourceBase, Region, Thesaurus, ThesaurusKeyword from geonode.layers.models import Dataset from geonode.services.models import Service from geonode.documents.models import Document @@ -262,12 +262,15 @@ def test_ingest(self): res.delete() def test_dataset_copy(self): + trigger_user = self.User.objects.create_user(username="test_dataset_copy_trigger", email="trigger@test.com") + def _copy_assert_resource(res, title): dataset_copy = None try: - dataset_copy = self.rm.copy(res, defaults=dict(title=title)) + dataset_copy = self.rm.copy(res, owner=trigger_user, defaults=dict(title=title)) self.assertIsNotNone(dataset_copy) self.assertEqual(dataset_copy.title, title) + self.assertEqual(dataset_copy.owner, trigger_user) finally: if dataset_copy: dataset_copy.delete() @@ -319,7 +322,7 @@ def test_resource_copy_with_linked_resources(self): def _copy_assert_resource(res, title): dataset_copy = None try: - dataset_copy = self.rm.copy(res, defaults=dict(title=title)) + dataset_copy = self.rm.copy(res, owner=self.user, defaults=dict(title=title)) self.assertIsNotNone(dataset_copy) self.assertEqual(dataset_copy.title, title) finally: @@ -335,6 +338,74 @@ def _copy_assert_resource(res, title): self.assertTrue(isinstance(res, Map)) _copy_assert_resource(res, "A Test Map 2") + def test_resource_copy_carries_over_metadata_and_permissions(self): + res = create_single_map("A Test Map With Metadata") + try: + region = Region.objects.first() + vals = { + "abstract": "test abstract", + "purpose": "test purpose", + "supplemental_information": "test supplemental information", + "data_quality_statement": "test data quality statement", + } + res = self.rm.update(res.uuid, instance=res, vals=vals, keywords=["foo", "bar"], regions=[region.name]) + + thesaurus = Thesaurus.objects.create(identifier="test_thesaurus_copy", title="Test Thesaurus") + tkeyword = ThesaurusKeyword.objects.create(thesaurus=thesaurus, alt_label="test_tkeyword") + res.tkeywords.add(tkeyword) + + custom_group, _ = GroupProfile.objects.get_or_create( + slug="test_copy_group", title="test_copy_group", access="private" + ) + custom_perms = { + "users": {self.user.username: ["view_resourcebase", "change_resourcebase"]}, + "groups": {custom_group.slug: ["view_resourcebase"]}, + } + self.rm.set_permissions(res.uuid, instance=res, permissions=custom_perms) + + # owner must be whoever triggers the clone (self.user), not res's own owner + self.assertNotEqual(res.owner, self.user) + dataset_copy = self.rm.copy(res, owner=self.user, defaults=dict(title="A Test Map With Metadata Copy")) + try: + self.assertIsNotNone(dataset_copy) + self.assertEqual(dataset_copy.owner, self.user) + self.assertEqual(res.abstract, dataset_copy.abstract) + self.assertEqual(res.purpose, dataset_copy.purpose) + self.assertEqual(res.supplemental_information, dataset_copy.supplemental_information) + self.assertEqual(res.data_quality_statement, dataset_copy.data_quality_statement) + self.assertCountEqual( + [k.name for k in res.keywords.all()], [k.name for k in dataset_copy.keywords.all()] + ) + self.assertCountEqual(list(res.regions.all()), list(dataset_copy.regions.all())) + self.assertCountEqual(list(res.tkeywords.all()), list(dataset_copy.tkeywords.all())) + + source_perms = permissions_registry.get_perms(instance=res, include_virtual=False) + copy_perms = permissions_registry.get_perms(instance=dataset_copy, include_virtual=False) + # self.user is the copy's new owner, so the permission engine promotes it to full + # owner perms on top of whatever custom perms it already had (AdvancedSecurityWorkflowManager + # always grants the owner admin+view perms) - it's the one entry expected to differ. + for perm_key in ("users", "groups"): + source_entries = { + profile.pk: set(perms) for profile, perms in source_perms.get(perm_key, {}).items() + } + copy_entries = {profile.pk: set(perms) for profile, perms in copy_perms.get(perm_key, {}).items()} + if perm_key == "users": + source_entries.pop(self.user.pk, None) + copy_entries.pop(self.user.pk, None) + self.assertEqual(source_entries, copy_entries) + # sanity: our own custom entries actually landed (owner promotion is a superset, not a replacement) + copy_user_perms = {profile.username: set(perms) for profile, perms in copy_perms["users"].items()} + self.assertTrue( + set(custom_perms["users"][self.user.username]).issubset(copy_user_perms[self.user.username]) + ) + copy_group_perms = {group.name: perms for group, perms in copy_perms["groups"].items()} + self.assertCountEqual(copy_group_perms[custom_group.slug], custom_perms["groups"][custom_group.slug]) + finally: + if dataset_copy: + dataset_copy.delete() + finally: + res.delete() + def test_exec(self): map = create_single_map("test_exec_map") self.assertIsNone(self.rm.exec("set_style", None, instance=None)) diff --git a/geonode/upload/celery_tasks.py b/geonode/upload/celery_tasks.py index f9ef1299c8e..8e47691c44d 100644 --- a/geonode/upload/celery_tasks.py +++ b/geonode/upload/celery_tasks.py @@ -592,15 +592,28 @@ def copy_geonode_resource(self, exec_id, actual_step, layer_name, alternate, han new_alternate = kwargs.get("kwargs").get("new_dataset_alternate") from geonode.upload.celery_tasks import import_orchestrator + resource = None try: - resource = ResourceBase.objects.filter(alternate=original_dataset_alternate) - if not resource.exists(): + _exec = orchestrator.get_execution_object(exec_id) + + resource = _exec.geonode_resource + if not original_dataset_alternate: + original_dataset_alternate = resource.alternate if resource else alternate + kwargs["kwargs"]["original_dataset_alternate"] = original_dataset_alternate + if not resource: + resource = ResourceBase.objects.filter(alternate=original_dataset_alternate).first() + + if not resource: raise Exception("The resource requested does not exists") - resource = resource.first() # setting the original resource in dirty_state resource.set_dirty_state() - _exec = orchestrator.get_execution_object(exec_id) + if not new_alternate: + new_alternate = create_alternate( + _exec.input_params.get("title") or resource.title or resource.alternate, + exec_id, + ) + kwargs["kwargs"]["new_dataset_alternate"] = new_alternate # Update input_params with original resource's uuid immediately so the API # can find this execution request for original the dirty resource @@ -609,12 +622,14 @@ def copy_geonode_resource(self, exec_id, actual_step, layer_name, alternate, han input_params={**_exec.input_params, **{"uuid": resource.uuid}}, ) - workspace = resource.alternate.split(":")[0] - - data_to_update = { - "alternate": f"{workspace}:{new_alternate}", - "name": new_alternate, - } + if resource.alternate and ":" in resource.alternate: + workspace = resource.alternate.split(":")[0] + data_to_update = { + "alternate": f"{workspace}:{new_alternate}", + "name": new_alternate, + } + else: + data_to_update = {"alternate": new_alternate, "name": new_alternate} if _exec.input_params.get("title"): data_to_update["title"] = _exec.input_params.get("title") @@ -640,7 +655,7 @@ def copy_geonode_resource(self, exec_id, actual_step, layer_name, alternate, han execution_id=_exec, ) - assert f"{workspace}:{new_alternate}" == new_resource.alternate + assert data_to_update["alternate"] == new_resource.alternate orchestrator.update_execution_request_status( execution_id=str(_exec.exec_id), diff --git a/geonode/upload/handlers/common/raster.py b/geonode/upload/handlers/common/raster.py index f3e374bdb06..4d0ded253ee 100644 --- a/geonode/upload/handlers/common/raster.py +++ b/geonode/upload/handlers/common/raster.py @@ -119,8 +119,10 @@ def extract_params_from_data(_data, action=None): all the other are returned """ if action == exa.COPY.value: - title = json.loads(_data.get("defaults")) - return {"title": title.pop("title"), "store_spatial_file": True}, _data + data = _data.get("defaults") + if isinstance(data, str): + data = json.loads(data) + return {"title": data.pop("title"), "store_spatial_file": True}, _data return { "skip_existing_layers": _data.pop("skip_existing_layers", "False"), diff --git a/geonode/upload/handlers/common/remote.py b/geonode/upload/handlers/common/remote.py index c5789362f38..7b643b3701c 100755 --- a/geonode/upload/handlers/common/remote.py +++ b/geonode/upload/handlers/common/remote.py @@ -108,8 +108,10 @@ def extract_params_from_data(_data, action=None): all the other are returned """ if action == exa.COPY.value: - title = json.loads(_data.get("defaults")) - return {"title": title.pop("title"), "store_spatial_file": True}, _data + data = _data.get("defaults") + if isinstance(data, str): + data = json.loads(data) + return {"title": data.pop("title"), "store_spatial_file": True}, _data return { "action": _data.pop("action", "upload"), @@ -230,6 +232,26 @@ def create_geonode_resource( return resource + def copy_geonode_resource(self, alternate, resource, _exec, data_to_update, new_alternate, **kwargs): + defaults = { + "alternate": new_alternate, + } + + if data_to_update.get("title"): + defaults["title"] = data_to_update["title"] + + if resource.subtype: + defaults["subtype"] = resource.subtype + + if resource.sourcetype: + defaults["sourcetype"] = resource.sourcetype + + return resource_manager_registry.get_for_instance(resource).copy( + resource, + owner=_exec.user, + defaults=defaults, + ) + def create_link(self, resource, params: dict, name): link = Link( resource=resource, diff --git a/geonode/upload/handlers/common/tests_vector.py b/geonode/upload/handlers/common/tests_vector.py index d544f0f2908..23134f044af 100644 --- a/geonode/upload/handlers/common/tests_vector.py +++ b/geonode/upload/handlers/common/tests_vector.py @@ -43,6 +43,11 @@ from geonode.upload.tests.utils import TransactionImporterBaseTestSupport from geonode.utils import OGC_Servers_Handler from geonode.upload.utils import create_vrt_file, has_incompatible_field_names +from geonode.resource.enumerator import ExecutionRequestAction as exa +from geonode.resource.registry import dataset_manager +from geonode.security.registry import permissions_registry +from geonode.groups.models import GroupProfile +from geonode.base.models import ResourceBase, Region, Thesaurus, ThesaurusKeyword class TestBaseVectorFileHandler(TestCase): @@ -65,6 +70,80 @@ def setUp(self) -> None: shutil.copy(self.valid_gpkg, "/tmp") super().setUp() + def test_copy_geonode_resource_should_copy_metadata_and_permissions(self): + dataset = create_single_dataset(name="dataset_to_clone_metadata", owner=self.owner) + region, _ = Region.objects.get_or_create(code="test_vector_copy_region", defaults={"name": "Test Region"}) + dataset = dataset_manager.update( + dataset.uuid, + instance=dataset, + vals={"abstract": "test abstract", "purpose": "test purpose"}, + keywords=["foo", "bar"], + regions=[region.name], + ) + thesaurus = Thesaurus.objects.create(identifier="test_thesaurus_vector_copy", title="Test Thesaurus") + tkeyword = ThesaurusKeyword.objects.create(thesaurus=thesaurus, alt_label="test_tkeyword") + dataset.tkeywords.add(tkeyword) + + other_user, _ = get_user_model().objects.get_or_create(username="vector_copy_other_user") + custom_group, _ = GroupProfile.objects.get_or_create( + slug="vector_copy_group", title="vector_copy_group", access="private" + ) + custom_perms = { + "users": {other_user.username: ["view_resourcebase", "change_resourcebase"]}, + "groups": {custom_group.slug: ["view_resourcebase"]}, + } + dataset_manager.set_permissions(dataset.uuid, instance=dataset, permissions=custom_perms) + + resource = ResourceBase.objects.get(pk=dataset.pk) + # distinct from self.owner (the dataset's owner) so the owner assertion below actually + # proves the clone follows the trigger, not just falls back to the source's owner + trigger_user, _ = get_user_model().objects.get_or_create(username="vector_copy_trigger") + exec_id = orchestrator.create_execution_request( + user=trigger_user, + func_name="start_import", + step="start_import", + action=exa.COPY.value, + input_params={"title": "cloned dataset metadata"}, + ) + _exec = orchestrator.get_execution_object(str(exec_id)) + + new_resource = self.handler.copy_geonode_resource( + alternate=dataset.alternate, + resource=resource, + _exec=_exec, + data_to_update={"title": "cloned dataset metadata"}, + new_alternate="dataset_to_clone_metadata_copy", + kwargs={"original_dataset_alternate": dataset.alternate}, + ) + + try: + self.assertNotEqual(dataset.pk, new_resource.pk) + self.assertEqual(trigger_user, new_resource.owner) + self.assertEqual(dataset.abstract, new_resource.abstract) + self.assertEqual(dataset.purpose, new_resource.purpose) + self.assertCountEqual( + [k.name for k in dataset.keywords.all()], [k.name for k in new_resource.keywords.all()] + ) + self.assertCountEqual(list(dataset.regions.all()), list(new_resource.regions.all())) + self.assertCountEqual(list(dataset.tkeywords.all()), list(new_resource.tkeywords.all())) + + # read via the plain ResourceBase, like production does (resource, not the downcast + # dataset) - get_all_level_info() only merges in the dataset-specific perms when + # called on a ResourceBase with a '.dataset' accessor, not on an already-downcast Dataset + source_perms = permissions_registry.get_perms(instance=resource, include_virtual=False) + copy_perms = permissions_registry.get_perms(instance=new_resource, include_virtual=False) + for perm_key in ("users", "groups"): + source_entries = {profile.pk: set(perms) for profile, perms in source_perms.get(perm_key, {}).items()} + copy_entries = {profile.pk: set(perms) for profile, perms in copy_perms.get(perm_key, {}).items()} + # trigger_user is the clone's new owner, always granted full owner perms on top + # of the reapplied source perm_spec, on top of whatever the source spec granted + copy_entries.pop(trigger_user.pk, None) + self.assertEqual(source_entries, copy_entries) + self.assertIn("change_resourcebase_permissions", copy_perms["users"][trigger_user]) + finally: + new_resource.delete() + dataset.delete() + def test_create_error_log(self): """ Should return the formatted way for the log of the handler diff --git a/geonode/upload/handlers/common/vector.py b/geonode/upload/handlers/common/vector.py index 4f9f5c60419..e134ee06d47 100644 --- a/geonode/upload/handlers/common/vector.py +++ b/geonode/upload/handlers/common/vector.py @@ -62,8 +62,9 @@ from osgeo import ogr, gdal from geonode.upload.api.exceptions import ImportException, UpsertException from geonode.upload.celery_app import importer_app -from geonode.assets.utils import copy_assets_and_links, get_default_asset +from geonode.assets.utils import get_default_asset +from geonode.resource.registry import dataset_manager from geonode.upload.handlers.utils import create_alternate, should_be_imported from geonode.upload.models import ResourceHandlerInfo from geonode.upload.orchestrator import orchestrator @@ -188,8 +189,10 @@ def extract_params_from_data(_data, action=None): all the other are returned """ if action == exa.COPY.value: - title = json.loads(_data.get("defaults")) - return {"title": title.pop("title"), "store_spatial_file": True}, _data + data = _data.get("defaults") + if isinstance(data, str): + data = json.loads(data) + return {"title": data.pop("title"), "store_spatial_file": True}, _data return { "skip_existing_layers": _data.pop("skip_existing_layers", "False"), @@ -1053,14 +1056,22 @@ def copy_geonode_resource( ).first() if previous_resource: subtype = previous_resource.subtype - new_resource = self.create_geonode_resource( - layer_name=data_to_update.get("title"), - alternate=new_alternate, - execution_id=str(_exec.exec_id), - subtype=subtype, - ) - copy_assets_and_links(resource, target=new_resource) + workspace = getattr(settings, "DEFAULT_WORKSPACE", getattr(settings, "CASCADE_WORKSPACE", "geonode")) + defaults = { + "name": new_alternate, + "alternate": f"{workspace}:{new_alternate}", + "workspace": workspace, + "store": os.environ.get("GEONODE_GEODATABASE", "geonode_data"), + } + if data_to_update.get("title"): + defaults["title"] = data_to_update["title"] + if subtype: + defaults["subtype"] = subtype + + new_resource = dataset_manager.copy(resource, owner=_exec.user, defaults=defaults) + # copy() only relinks GeoNode-side data, the GIS backend still needs its own style + self.handle_sld_file(new_resource, _exec) if resource.dataset.has_time is True: diff --git a/geonode/upload/handlers/shapefile/handler.py b/geonode/upload/handlers/shapefile/handler.py index e376ae16dcb..009ba91d24f 100644 --- a/geonode/upload/handlers/shapefile/handler.py +++ b/geonode/upload/handlers/shapefile/handler.py @@ -107,8 +107,10 @@ def extract_params_from_data(_data, action=None): all the other are returned """ if action == exa.COPY.value: - title = json.loads(_data.get("defaults")) - return {"title": title.pop("title"), "store_spatial_file": True}, _data + data = _data.get("defaults") + if isinstance(data, str): + data = json.loads(data) + return {"title": data.pop("title"), "store_spatial_file": True}, _data additional_params = { "skip_existing_layers": _data.pop("skip_existing_layers", "False"), diff --git a/geonode/upload/handlers/tiles3d/handler.py b/geonode/upload/handlers/tiles3d/handler.py index b4811563dfe..40a2be7eeff 100755 --- a/geonode/upload/handlers/tiles3d/handler.py +++ b/geonode/upload/handlers/tiles3d/handler.py @@ -34,6 +34,7 @@ from geonode.upload.utils import ImporterRequestAction as ira from geonode.base.models import ResourceBase from geonode.upload.handlers.tiles3d.exceptions import Invalid3DTilesException +from geonode.resource.registry import resource_manager_registry logger = logging.getLogger("importer") @@ -184,8 +185,10 @@ def extract_params_from_data(_data, action=None): all the other are returned """ if action == exa.COPY.value: - title = json.loads(_data.get("defaults")) - return {"title": title.pop("title"), "store_spatial_file": True}, _data + data = _data.get("defaults") + if isinstance(data, str): + data = json.loads(data) + return {"title": data.pop("title"), "store_spatial_file": True}, _data return { "skip_existing_layers": _data.pop("skip_existing_layers", "False"), @@ -296,6 +299,23 @@ def create_geonode_resource( return resource + def copy_geonode_resource(self, alternate, resource, _exec, data_to_update, new_alternate, **kwargs): + defaults = { + "alternate": new_alternate, + } + + if data_to_update.get("title"): + defaults["title"] = data_to_update["title"] + + if resource.subtype: + defaults["subtype"] = resource.subtype + + return resource_manager_registry.get_for_instance(resource).copy( + resource, + owner=_exec.user, + defaults=defaults, + ) + def create_asset_and_link(self, resource, files, action=None, asset_name=None, asset_type=None, **kwargs): """ Overrides the base handler to prevent double asset creation for 3D tiles. From 87831c7d6c5a7caa34e4336b4552305f0b70328f Mon Sep 17 00:00:00 2001 From: sijandh35 Date: Mon, 14 Sep 2026 15:23:55 +0000 Subject: [PATCH 74/98] Profile page fixes --- geonode/base/api/serializers.py | 13 ++++++++ geonode/groups/forms.py | 17 ++++++++++ .../groups/templates/groups/group_detail.html | 6 ++-- .../templates/groups/group_members.html | 8 ++--- .../groups/groupcategory_detail.html | 4 +-- geonode/people/api/serializers.py | 9 ++++++ geonode/people/forms/__init__.py | 15 ++++++++- .../templates/people/_profile_about_item.html | 2 +- .../templates/people/profile_detail.html | 10 +++--- .../people/templates/people/profile_edit.html | 7 ++-- geonode/people/tests.py | 32 +++++++++++++++++++ geonode/people/utils.py | 8 +++++ geonode/templates/base.html | 2 +- 13 files changed, 114 insertions(+), 19 deletions(-) diff --git a/geonode/base/api/serializers.py b/geonode/base/api/serializers.py index 6d8094518d0..442fe244dc7 100644 --- a/geonode/base/api/serializers.py +++ b/geonode/base/api/serializers.py @@ -71,6 +71,7 @@ from geonode.resource.models import ExecutionRequest from django.contrib.gis.geos import Polygon from geonode.security.registry import permissions_registry +from geonode.people.utils import contains_disallowed_template_tokens logger = logging.getLogger(__name__) @@ -178,6 +179,18 @@ class Meta: keywords = serializers.SlugRelatedField(many=True, slug_field="slug", read_only=True) categories = serializers.SlugRelatedField(many=True, slug_field="slug", queryset=GroupCategory.objects.all()) + def validate(self, data): + field_errors = { + field_name: "This field contains characters that are not allowed." + for field_name, value in data.items() + if contains_disallowed_template_tokens(value) + } + + if field_errors: + raise serializers.ValidationError(field_errors) + + return data + class SimpleHierarchicalKeywordSerializer(DynamicModelSerializer): class Meta: diff --git a/geonode/groups/forms.py b/geonode/groups/forms.py index 376dd4d464e..bd3737aa7e9 100644 --- a/geonode/groups/forms.py +++ b/geonode/groups/forms.py @@ -25,6 +25,7 @@ from geonode.groups.models import GroupProfile from geonode.base.widgets import TaggitSelect2Custom +from geonode.people.utils import contains_disallowed_template_tokens class GroupForm(forms.ModelForm): @@ -46,6 +47,12 @@ def clean_title(self): def clean(self): cleaned_data = self.cleaned_data + for field_name, value in list(cleaned_data.items()): + if contains_disallowed_template_tokens(value): + raise forms.ValidationError( + _("Field %(field_name)s contains characters that are not allowed."), + params={"field_name": field_name}, + ) name = cleaned_data.get("title") if not name or GroupProfile.objects.filter(title__iexact=self.cleaned_data["title"]).exists(): @@ -71,6 +78,16 @@ def clean_name(self): raise forms.ValidationError(_("A group already exists with that name.")) return self.cleaned_data["title"] + def clean(self): + cleaned_data = self.cleaned_data + for field_name, value in list(cleaned_data.items()): + if contains_disallowed_template_tokens(value): + raise forms.ValidationError( + _("Field %(field_name)s contains characters that are not allowed."), + params={"field_name": field_name}, + ) + return cleaned_data + class Meta: model = GroupProfile exclude = ["group"] diff --git a/geonode/groups/templates/groups/group_detail.html b/geonode/groups/templates/groups/group_detail.html index ca14b7d1bbd..884af946702 100644 --- a/geonode/groups/templates/groups/group_detail.html +++ b/geonode/groups/templates/groups/group_detail.html @@ -16,7 +16,7 @@ {% block body_class %}groups{% endblock %} {% block body_outer %} -