diff --git a/geonode/security/auth_handlers.py b/geonode/security/auth_handlers.py
index cd4b5382c40..7c1b47593e7 100644
--- a/geonode/security/auth_handlers.py
+++ b/geonode/security/auth_handlers.py
@@ -16,6 +16,7 @@
# along with this program. If not, see .
#
#########################################################################
+import base64
from abc import ABC, abstractmethod
from django.core.exceptions import ValidationError
@@ -39,6 +40,9 @@ def _init_from_config(self):
def get_request_auth(self) -> AuthBase:
raise NotImplementedError
+ def get_gdal_config(self, url):
+ raise NotImplementedError
+
def auth_request(self, request, **kwargs):
raise NotImplementedError
@@ -50,8 +54,12 @@ def validate(cls, payload, instance=None):
raise NotImplementedError
@classmethod
- def create_auth_config(cls, **kwargs):
- raise NotImplementedError
+ def create_auth_config(cls, payload):
+ cls.validate(payload)
+ auth_config = AuthConfig(type=cls.handled_type)
+ auth_config.payload = payload
+ auth_config.save()
+ return auth_config
class HashableAuthBase(AuthBase):
@@ -90,17 +98,6 @@ def validate(cls, payload, instance=None):
raise ValidationError("Password is required for basic authentication.")
return payload
- @classmethod
- def create_auth_config(cls, username, password):
- if username is None and password is None:
- return None
- payload = {"username": username, "password": password}
- cls.validate(payload)
- auth_config = AuthConfig(type=cls.handled_type)
- auth_config.payload = payload
- auth_config.save()
- return auth_config
-
def _init_from_config(self):
payload = self.config.payload
self.username = payload.get("username")
@@ -109,6 +106,11 @@ def _init_from_config(self):
def get_request_auth(self) -> AuthBase:
return HashableAuthBase(HTTPBasicAuth(self.username, self.password))
+ def get_gdal_config(self, url):
+ credentials = f"{self.username}:{self.password}".encode()
+ token = base64.b64encode(credentials).decode()
+ return url, {"GDAL_HTTP_HEADERS": f"Authorization: Basic {token}"}
+
def auth_request(self, request, **kwargs):
request.auth = self.get_request_auth()
return request
diff --git a/geonode/security/tests.py b/geonode/security/tests.py
index e9fb8ab15a9..97b55082d31 100644
--- a/geonode/security/tests.py
+++ b/geonode/security/tests.py
@@ -4204,7 +4204,7 @@ def test_url_pattern_auth_config_string_representation(self):
self.assertEqual(str(url_pattern_auth_config), "https://example.com/*")
def test_basic_auth_payload_round_trip(self):
- auth_config = BasicAuthHandler.create_auth_config("test_user", "test_password")
+ auth_config = BasicAuthHandler.create_auth_config({"username": "test_user", "password": "test_password"})
self.assertEqual(auth_config.type, "basic")
self.assertNotIn("test_user", auth_config._payload)
@@ -4248,6 +4248,13 @@ def test_basic_auth_handler_auth_request_sets_request_auth(self):
self.assertEqual(request.auth.auth.username, "test_user")
self.assertEqual(request.auth.auth.password, "test_password")
+ def test_basic_auth_handler_get_gdal_config(self):
+ auth_handler = auth_handler_registry.build(self.auth_config)
+ expected_token = base64.b64encode(b"test_user:test_password").decode()
+ url, options = auth_handler.get_gdal_config("https://example.com/data.tif")
+ self.assertEqual("https://example.com/data.tif", url)
+ self.assertEqual({"GDAL_HTTP_HEADERS": f"Authorization: Basic {expected_token}"}, options)
+
class AuthHandlerRegistryTests(TestCase):
class SampleAuthHandler(AuthHandler):
diff --git a/geonode/services/apps.py b/geonode/services/apps.py
index bf2fb8db9d6..f44ad676906 100644
--- a/geonode/services/apps.py
+++ b/geonode/services/apps.py
@@ -63,6 +63,9 @@ def ready(self):
super().ready()
# Let's make sure the signals are connected to the App
from . import signals # noqa
+ from geonode.services.serviceprocessors.registry import service_type_registry
+
+ service_type_registry.init_registry()
post_migrate.connect(run_setup_hooks, sender=self)
# settings.CELERY_BEAT_SCHEDULE['probe_services'] = {
diff --git a/geonode/services/forms.py b/geonode/services/forms.py
index 4f1333a76ab..03255f53715 100644
--- a/geonode/services/forms.py
+++ b/geonode/services/forms.py
@@ -29,9 +29,8 @@
from geonode.security.models import AuthConfig
from . import enumerations
-from .models import Service
+from .models import Service, get_service_type_choices
from .serviceprocessors import get_service_handler
-from geonode.services.serviceprocessors import get_available_service_types
from geonode.utils import is_safe_url
logger = logging.getLogger(__name__)
@@ -48,7 +47,7 @@ class CreateServiceForm(forms.Form):
)
type = forms.ChoiceField(
label=_("Service Type"),
- choices=[(k, v["label"]) for k, v in get_available_service_types().items()], # from dictionary to tuple
+ choices=get_service_type_choices,
initial="AUTO",
)
diff --git a/geonode/services/migrations/0060_alter_service_type.py b/geonode/services/migrations/0060_alter_service_type.py
new file mode 100644
index 00000000000..2ae5f06e12f
--- /dev/null
+++ b/geonode/services/migrations/0060_alter_service_type.py
@@ -0,0 +1,19 @@
+# Generated by Django 5.2.15 on 2026-06-09 12:56
+
+import geonode.services.models
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("services", "0059_remove_service_password_remove_service_username"),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name="service",
+ name="type",
+ field=models.CharField(choices=geonode.services.models.get_service_type_choices, max_length=10),
+ ),
+ ]
diff --git a/geonode/services/models.py b/geonode/services/models.py
index f50ff51593f..f5276b97e0b 100644
--- a/geonode/services/models.py
+++ b/geonode/services/models.py
@@ -32,7 +32,10 @@
from geonode.services.serviceprocessors import get_available_service_types
from . import enumerations
-service_type_as_tuple = [(k, v["label"]) for k, v in get_available_service_types().items()]
+
+def get_service_type_choices():
+ return [(k, v["label"]) for k, v in get_available_service_types().items()]
+
logger = logging.getLogger("geonode.services")
@@ -40,7 +43,7 @@
class Service(ResourceBase):
"""Service Class to represent remote Geo Web Services"""
- type = models.CharField(max_length=10, choices=service_type_as_tuple)
+ type = models.CharField(max_length=10, choices=get_service_type_choices)
method = models.CharField(
max_length=1,
choices=(
@@ -102,12 +105,15 @@ def service_url(self):
@property
def ptype(self):
# Return the gxp ptype that should be used to display layers
- return GXP_PTYPES[self.type] if self.type else None
+ return GXP_PTYPES.get(self.type) if self.type else None
@property
def service_type(self):
# Return the gxp ptype that should be used to display layers
- return [x for x in service_type_as_tuple if x[0] == self.type][0][1]
+ service_type = get_available_service_types().get(self.type)
+ if service_type:
+ return service_type["label"]
+ return self.type
def get_absolute_url(self):
return "/services/%i" % self.id
diff --git a/geonode/services/serviceprocessors/__init__.py b/geonode/services/serviceprocessors/__init__.py
index a8601d05d5c..eba68ae7302 100644
--- a/geonode/services/serviceprocessors/__init__.py
+++ b/geonode/services/serviceprocessors/__init__.py
@@ -18,45 +18,17 @@
#########################################################################
import logging
-from collections import OrderedDict
-from django.utils.translation import gettext_lazy as _
from django.conf import settings
from geonode.services import enumerations
-from geonode.services.utils import parse_services_types
from django.core.cache import caches
+from geonode.services.serviceprocessors.registry import service_type_registry
service_cache = caches["services"]
logger = logging.getLogger(__name__)
-def get_available_service_types():
- # LGTM: Fixes - Module uses member of cyclically imported module, which can lead to failure at import time.
- from geonode.services.serviceprocessors.wms import GeoNodeServiceHandler, WmsServiceHandler
- from geonode.services.serviceprocessors.arcgis import ArcImageServiceHandler, ArcMapServiceHandler
-
- default = OrderedDict(
- {
- enumerations.WMS: {"OWS": True, "handler": WmsServiceHandler, "label": _("Web Map Service")},
- enumerations.GN_WMS: {
- "OWS": True,
- "handler": GeoNodeServiceHandler,
- "label": _("GeoNode (Web Map Service)"),
- },
- # enumerations.WFS: {"OWS": True, "handler": ServiceHandlerBase, "label": _('Paired WMS/WFS/WCS'},
- # enumerations.TMS: {"OWS": False, "handler": ServiceHandlerBase, "label": _('Paired WMS/WFS/WCS'},
- enumerations.REST_MAP: {"OWS": False, "handler": ArcMapServiceHandler, "label": _("ArcGIS REST MapServer")},
- enumerations.REST_IMG: {
- "OWS": False,
- "handler": ArcImageServiceHandler,
- "label": _("ArcGIS REST ImageServer"),
- },
- # enumerations.CSW: {"OWS": False, "handler": ServiceHandlerBase, "label": _('Catalogue Service')},
- # enumerations.OGP: {"OWS": True, "handler": ServiceHandlerBase, "label": _('OpenGeoPortal')}, # TODO: verify this
- # enumerations.HGL: {"OWS": False, "handler": ServiceHandlerBase, "label": _('Harvard Geospatial Library')}, # TODO: verify this
- }
- )
-
- return OrderedDict({**default, **parse_services_types()})
+def get_available_service_types():
+ return service_type_registry.get_available_service_types()
def get_service_handler(base_url, service_type=enumerations.AUTO, service_id=None, *args, **kwargs):
@@ -67,9 +39,7 @@ def get_service_handler(base_url, service_type=enumerations.AUTO, service_id=Non
if entry := service_cache.get(base_url):
return entry
- handlers = get_available_service_types()
-
- handler = handlers.get(service_type, {}).get("handler")
+ handler = service_type_registry.get_handler_class(service_type)
try:
service_handler = handler(base_url, service_id, *args, **kwargs)
service_cache.set(service_handler.url, service_handler, settings.SERVICE_CACHE_EXPIRATION_TIME)
diff --git a/geonode/services/serviceprocessors/registry.py b/geonode/services/serviceprocessors/registry.py
new file mode 100644
index 00000000000..6ed3c2786a5
--- /dev/null
+++ b/geonode/services/serviceprocessors/registry.py
@@ -0,0 +1,85 @@
+#########################################################################
+#
+# 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 collections import OrderedDict
+
+from django.conf import settings
+from django.utils.module_loading import import_string
+from django.utils.translation import gettext_lazy as _
+
+from geonode.services import enumerations
+
+
+class ServiceTypeRegistry:
+ def __init__(self):
+ self.registry = OrderedDict()
+ self._initialized = False
+
+ def register(self, service_type, handler, label, OWS=False, **kwargs):
+ self.registry[service_type] = {
+ "OWS": OWS,
+ "handler": handler,
+ "label": label,
+ **kwargs,
+ }
+
+ def unregister(self, service_type):
+ self.registry.pop(service_type, None)
+
+ def init_registry(self):
+ if self._initialized:
+ return
+
+ self.registry = OrderedDict()
+ self._register_default_service_types()
+ self._register_configured_service_types()
+ self._initialized = True
+
+ def reset(self):
+ self.registry = OrderedDict()
+ self._initialized = False
+
+ def get_available_service_types(self):
+ self.init_registry()
+ return OrderedDict(self.registry)
+
+ def get_handler_class(self, service_type):
+ service_type_config = self.get_available_service_types().get(service_type, {})
+ handler = service_type_config.get("handler")
+ if isinstance(handler, str):
+ return import_string(handler)
+ return handler
+
+ def _register_default_service_types(self):
+ # Keep imports lazy to avoid circular imports during Django app loading.
+ from geonode.services.serviceprocessors.arcgis import ArcImageServiceHandler, ArcMapServiceHandler
+ from geonode.services.serviceprocessors.wms import GeoNodeServiceHandler, WmsServiceHandler
+
+ self.register(enumerations.WMS, WmsServiceHandler, _("Web Map Service"), OWS=True)
+ self.register(enumerations.GN_WMS, GeoNodeServiceHandler, _("GeoNode (Web Map Service)"), OWS=True)
+ self.register(enumerations.REST_MAP, ArcMapServiceHandler, _("ArcGIS REST MapServer"))
+ self.register(enumerations.REST_IMG, ArcImageServiceHandler, _("ArcGIS REST ImageServer"))
+
+ def _register_configured_service_types(self):
+ for service_type_module_path in getattr(settings, "SERVICES_TYPE_MODULES", []):
+ custom_service_type_module = import_string(service_type_module_path)
+ for service_type, service_type_config in custom_service_type_module.services_type.items():
+ self.register(service_type, **service_type_config)
+
+
+service_type_registry = ServiceTypeRegistry()
diff --git a/geonode/services/tests.py b/geonode/services/tests.py
index 5250a899a9a..1e3506b7cd4 100644
--- a/geonode/services/tests.py
+++ b/geonode/services/tests.py
@@ -51,6 +51,7 @@
from .models import Service
from .serviceprocessors import base, wms, arcgis, get_service_handler, get_available_service_types
from .serviceprocessors.arcgis import ArcImageServiceHandler, ArcMapServiceHandler, MapLayer
+from .serviceprocessors.registry import ServiceTypeRegistry, service_type_registry
logger = logging.getLogger(__name__)
@@ -96,16 +97,14 @@ def test_get_cascading_workspace_creates_new_workspace(self, mock_settings, mock
mock_settings.CASCADE_WORKSPACE, f"http://www.geonode.org/{mock_settings.CASCADE_WORKSPACE}"
)
- @mock.patch("geonode.services.serviceprocessors.get_available_service_types", autospec=True)
+ @mock.patch("geonode.services.serviceprocessors.service_type_registry.get_handler_class", autospec=True)
def test_get_service_handler_wms(self, mock_wms_handler):
class PickableMagicMock(mock.MagicMock):
def __reduce__(self):
return (mock.MagicMock, ())
_handler = PickableMagicMock()
- mock_wms_handler.return_value = {
- enumerations.WMS: {"OWS": True, "handler": _handler, "label": "Web Map Service"}
- }
+ mock_wms_handler.return_value = _handler
phony_url = "http://fake"
get_service_handler(phony_url, service_type=enumerations.WMS)
_handler.assert_called_with(phony_url, None)
@@ -1025,38 +1024,82 @@ def test_will_use_multiple_service_types_defined(self):
@override_settings(SERVICES_TYPE_MODULES=SERVICES_TYPE_MODULES)
def test_will_use_multiple_service_types_defined_for_choices(self):
- elems = get_available_service_types()
- expected = {
- "WMS": {"OWS": True, "handler": wms.WmsServiceHandler, "label": "Web Map Service"},
- "GN_WMS": {"OWS": True, "handler": wms.GeoNodeServiceHandler, "label": "GeoNode (Web Map Service)"},
- "REST_MAP": {"OWS": False, "handler": ArcMapServiceHandler, "label": "ArcGIS REST MapServer"},
- "REST_IMG": {"OWS": False, "handler": ArcImageServiceHandler, "label": "ArcGIS REST ImageServer"},
- "test": {
- "OWS": True,
- "handler": "TestHandler",
- "label": "Test Number 1",
- "management_view": "path.to.view1",
- },
- "test2": {
- "OWS": False,
- "handler": "TestHandler2",
- "label": "Test Number 2",
- "management_view": "path.to.view2",
- },
- "test3": {
- "OWS": True,
- "handler": "TestHandler3",
- "label": "Test Number 3",
- "management_view": "path.to.view3",
- },
- "test4": {
+ service_type_registry.reset()
+ try:
+ elems = get_available_service_types()
+ expected = {
+ "WMS": {"OWS": True, "handler": wms.WmsServiceHandler, "label": "Web Map Service"},
+ "GN_WMS": {"OWS": True, "handler": wms.GeoNodeServiceHandler, "label": "GeoNode (Web Map Service)"},
+ "REST_MAP": {"OWS": False, "handler": ArcMapServiceHandler, "label": "ArcGIS REST MapServer"},
+ "REST_IMG": {"OWS": False, "handler": ArcImageServiceHandler, "label": "ArcGIS REST ImageServer"},
+ "test": {
+ "OWS": True,
+ "handler": "TestHandler",
+ "label": "Test Number 1",
+ "management_view": "path.to.view1",
+ },
+ "test2": {
+ "OWS": False,
+ "handler": "TestHandler2",
+ "label": "Test Number 2",
+ "management_view": "path.to.view2",
+ },
+ "test3": {
+ "OWS": True,
+ "handler": "TestHandler3",
+ "label": "Test Number 3",
+ "management_view": "path.to.view3",
+ },
+ "test4": {
+ "OWS": False,
+ "handler": "TestHandler4",
+ "label": "Test Number 4",
+ "management_view": "path.to.view4",
+ },
+ }
+ self.assertDictEqual(expected, elems)
+ finally:
+ service_type_registry.reset()
+
+ def test_service_type_registry_should_register_service_type(self):
+ registry = ServiceTypeRegistry()
+ registry.register(
+ "CUSTOM",
+ handler="path.to.CustomServiceHandler",
+ label="Custom Service",
+ OWS=False,
+ management_view="path.to.view",
+ )
+
+ self.assertEqual(
+ {
"OWS": False,
- "handler": "TestHandler4",
- "label": "Test Number 4",
- "management_view": "path.to.view4",
+ "handler": "path.to.CustomServiceHandler",
+ "label": "Custom Service",
+ "management_view": "path.to.view",
},
- }
- self.assertDictEqual(expected, elems)
+ registry.registry["CUSTOM"],
+ )
+
+ def test_service_type_registry_should_unregister_service_type(self):
+ registry = ServiceTypeRegistry()
+ registry.register("CUSTOM", handler="path.to.CustomServiceHandler", label="Custom Service")
+
+ registry.unregister("CUSTOM")
+
+ self.assertNotIn("CUSTOM", registry.registry)
+
+ @override_settings(SERVICES_TYPE_MODULES=["geonode.services.tests.dummy_services_type_handler"])
+ def test_service_type_registry_should_load_configured_service_types(self):
+ registry = ServiceTypeRegistry()
+
+ self.assertIn("test_handler", registry.get_available_service_types())
+
+ @override_settings(SERVICES_TYPE_MODULES=["geonode.services.tests.dummy_services_type_handler"])
+ def test_service_type_registry_should_return_handler_class(self):
+ registry = ServiceTypeRegistry()
+
+ self.assertEqual(wms.WmsServiceHandler, registry.get_handler_class("test_handler"))
"""
@@ -1086,3 +1129,13 @@ class dummy_services_type2:
"management_view": "path.to.view4",
},
}
+
+
+class dummy_services_type_handler:
+ services_type = {
+ "test_handler": {
+ "OWS": False,
+ "handler": "geonode.services.serviceprocessors.wms.WmsServiceHandler",
+ "label": "Test Handler",
+ },
+ }
diff --git a/geonode/thumbs/tests/test_unit.py b/geonode/thumbs/tests/test_unit.py
index 2c057b1455e..50023b52f59 100644
--- a/geonode/thumbs/tests/test_unit.py
+++ b/geonode/thumbs/tests/test_unit.py
@@ -207,7 +207,7 @@ def test_datasets_locations_dataset(self):
def test_get_auth_should_use_dataset_auth_config(self):
dataset = Dataset.objects.get(title="theaters_nyc")
- auth_config = BasicAuthHandler.create_auth_config("dataset_user", "dataset_password")
+ auth_config = BasicAuthHandler.create_auth_config({"username": "dataset_user", "password": "dataset_password"})
dataset.auth_config = auth_config
dataset.save()
diff --git a/geonode/upload/datastore.py b/geonode/upload/datastore.py
index c50a702021c..4bc82472e28 100644
--- a/geonode/upload/datastore.py
+++ b/geonode/upload/datastore.py
@@ -46,7 +46,7 @@ def input_is_valid(self):
"""
url = orchestrator.get_execution_object(exec_id=self.execution_id).input_params.get("url")
if url:
- return self.handler.is_valid_url(url)
+ return self.handler.is_valid_url(url, execution_id=self.execution_id)
return self.handler.is_valid(self.files, self.user, execution_id=self.execution_id)
def _import_and_register(self, execution_id, task_name, **kwargs):
diff --git a/geonode/upload/handlers/common/remote.py b/geonode/upload/handlers/common/remote.py
index 084cd71b54a..53514ca0bcf 100755
--- a/geonode/upload/handlers/common/remote.py
+++ b/geonode/upload/handlers/common/remote.py
@@ -19,6 +19,7 @@
import json
import logging
import os
+from contextlib import contextmanager
import requests
from geonode.layers.models import Dataset
@@ -35,6 +36,8 @@
from geonode.base.enumerations import SOURCE_TYPE_REMOTE
from geonode.resource.registry import resource_manager_registry
from geonode.resource.models import ExecutionRequest
+from geonode.security.auth_registry import auth_handler_registry
+from geonode.security.models import AuthConfig
logger = logging.getLogger("importer")
@@ -91,7 +94,8 @@ def is_valid_url(url, **kwargs):
and if the url is valid
"""
try:
- r = requests.get(url, timeout=10)
+ auth = BaseRemoteResourceHandler.get_request_auth_from_execution(kwargs.get("execution_id"))
+ r = requests.get(url, timeout=10, auth=auth)
r.raise_for_status()
except requests.exceptions.Timeout:
raise ImportException("Timed out")
@@ -110,18 +114,112 @@ def extract_params_from_data(_data, action=None):
title = json.loads(_data.get("defaults"))
return {"title": title.pop("title"), "store_spatial_file": True}, _data
- return {
+ payload = {
"action": _data.pop("action", "upload"),
"title": _data.pop("title", None),
"url": _data.pop("url", None),
"type": _data.pop("type", None),
- }, _data
+ }
+ BaseRemoteResourceHandler.extract_auth_config_from_data(_data, payload)
+ return payload, _data
+
+ @staticmethod
+ def extract_auth_config_from_data(_data, payload):
+ authentication = _data.pop("authentication", None)
+ if authentication:
+ auth_type = authentication.get("type")
+ auth_payload = authentication.get("payload") or {}
+ auth_handler_cls = auth_handler_registry.get_handler_class(auth_type)
+ if auth_handler_cls is None:
+ raise ValueError(f"Unsupported authentication type '{auth_type}'")
+ auth_config = auth_handler_cls.create_auth_config(auth_payload)
+ payload["auth_config_id"] = auth_config.pk
+
+ def _create_geonode_resource_rollback(self, exec_id, istance_name=None, *args, **kwargs):
+ super()._create_geonode_resource_rollback(exec_id, istance_name=istance_name)
+
+ _exec = orchestrator.get_execution_object(exec_id)
+ auth_config_id = _exec.input_params.get("auth_config_id")
+ if auth_config_id:
+ AuthConfig.objects.filter(
+ pk=auth_config_id,
+ authconfigresources__isnull=True,
+ url_patterns__isnull=True,
+ ).delete()
def pre_validation(self, files, execution_id, **kwargs):
"""
Hook for let the handler prepare the data before the validation.
Maybe a file rename, assign the resource to the execution_id
"""
+ _exec = orchestrator.get_execution_object(execution_id)
+ if _exec.input_params.get("auth_config_id") or not _exec.input_params.get("url"):
+ return
+
+ to_update = {}
+ service = self.find_matching_service(_exec.input_params["url"], _exec.user)
+ auth_config = self.get_auth_config_from_execution(_exec)
+ if not auth_config and service and service.auth_config:
+ auth_config = service.auth_config
+ to_update["auth_config_id"] = service.auth_config_id
+ if service and auth_config:
+ to_update["remote_service_id"] = service.id
+ _exec.input_params.update(to_update)
+ _exec.save()
+
+ @staticmethod
+ def find_matching_service(url, user):
+ if not user or not user.is_authenticated:
+ return None
+
+ from geonode.services.models import Service
+
+ services = Service.objects.filter(owner=user, auth_config__isnull=False).select_related("auth_config")
+ for service in services:
+ if not service.service_url:
+ continue
+ service_url = service.service_url.rstrip("/")
+ if url == service_url or url.startswith(f"{service_url}/") or url.startswith(f"{service_url}?"):
+ return service
+ return None
+
+ @staticmethod
+ def get_auth_config_from_execution(_exec):
+ if not _exec or not _exec.input_params:
+ return None
+
+ auth_config_id = _exec.input_params.get("auth_config_id")
+ if auth_config_id:
+ return AuthConfig.objects.filter(pk=auth_config_id).first()
+ return None
+
+ @staticmethod
+ def get_request_auth_from_execution(execution_id):
+ if not execution_id:
+ return None
+
+ _exec = orchestrator.get_execution_object(execution_id)
+ auth_config = BaseRemoteResourceHandler.get_auth_config_from_execution(_exec)
+ if not auth_config:
+ return None
+
+ return auth_handler_registry.build(auth_config).get_request_auth()
+
+ @staticmethod
+ @contextmanager
+ def gdal_config_options(options):
+ from osgeo import gdal
+
+ options = options or {}
+ try:
+ for option, value in options.items():
+ gdal.SetThreadLocalConfigOption(option, value)
+ yield
+ finally:
+ for option in options:
+ gdal.SetThreadLocalConfigOption(option, None)
+ if hasattr(gdal, "VSICurlClearCache"):
+ gdal.VSICurlClearCache()
def import_resource(self, files: dict, execution_id: str, **kwargs) -> str:
"""
@@ -260,7 +358,7 @@ def create_resourcehandlerinfo(
)
def generate_resource_payload(self, layer_name, alternate, asset, _exec, workspace, **kwargs):
- return dict(
+ payload = dict(
subtype=kwargs.get("type"),
sourcetype=SOURCE_TYPE_REMOTE,
alternate=alternate,
@@ -268,6 +366,9 @@ def generate_resource_payload(self, layer_name, alternate, asset, _exec, workspa
title=kwargs.get("title", layer_name),
owner=_exec.user,
)
+ if kwargs.get("auth_config_id"):
+ payload["auth_config_id"] = kwargs.get("auth_config_id")
+ return payload
def overwrite_geonode_resource(
self,
diff --git a/geonode/upload/handlers/common/serializer.py b/geonode/upload/handlers/common/serializer.py
index 05ead23e42a..64665768643 100644
--- a/geonode/upload/handlers/common/serializer.py
+++ b/geonode/upload/handlers/common/serializer.py
@@ -28,7 +28,7 @@ class Meta:
ref_name = "RemoteResourceSerializer"
model = ResourceBase
view_name = "importer_upload"
- fields = ("url", "title", "type", "action")
+ fields = ("url", "title", "type", "action", "authentication")
url = serializers.URLField(required=True, help_text="URL of the remote service / resource")
title = serializers.CharField(required=True, help_text="Title of the resource. Can be None or Empty")
@@ -37,8 +37,23 @@ class Meta:
help_text="Remote resource type, for example wms or 3dtiles. Is used by the handler to understand if can handle the resource",
)
action = serializers.CharField(required=False, default=exa.UPLOAD.value)
+ authentication = serializers.JSONField(required=False, allow_null=True)
def validate_url(self, value):
if not is_safe_url(value):
raise serializers.ValidationError("URL is not allowed.")
return value
+
+ def validate_authentication(self, value):
+ if value is None:
+ return value
+ if not isinstance(value, dict):
+ raise serializers.ValidationError("Authentication must be an object.")
+ if not value.get("type"):
+ raise serializers.ValidationError("Authentication type is required.")
+ payload = value.get("payload")
+ if payload is None:
+ raise serializers.ValidationError("Authentication payload is required.")
+ if not isinstance(payload, dict):
+ raise serializers.ValidationError("Authentication payload must be an object.")
+ return value
diff --git a/geonode/upload/handlers/common/test_remote.py b/geonode/upload/handlers/common/test_remote.py
index 5c78c138cbe..9f46e3086aa 100644
--- a/geonode/upload/handlers/common/test_remote.py
+++ b/geonode/upload/handlers/common/test_remote.py
@@ -25,6 +25,8 @@
from geonode.base.populate_test_data import create_single_dataset
from geonode.resource.models import ExecutionRequest
from geonode.base.models import ResourceBase
+from geonode.security.auth_handlers import BasicAuthHandler
+from geonode.security.models import AuthConfig
class TestBaseRemoteResourceHandler(TestCase):
@@ -111,6 +113,37 @@ def test_extract_params_from_data(self):
self.assertTrue("url" in actual)
self.assertTrue("type" in actual)
+ def test_extract_params_from_data_should_create_auth_config(self):
+ actual, _data = self.handler.extract_params_from_data(
+ _data={
+ "url": "http://abc123defsadsa.org",
+ "title": "Remote Title",
+ "type": "3dtiles",
+ "authentication": {
+ "type": BasicAuthHandler.handled_type,
+ "payload": {"username": "test_user", "password": "test_password"},
+ },
+ },
+ action="upload",
+ )
+
+ auth_config = AuthConfig.objects.get(pk=actual["auth_config_id"])
+ self.assertEqual(BasicAuthHandler.handled_type, auth_config.type)
+ self.assertEqual({"username": "test_user", "password": "test_password"}, auth_config.payload)
+
+ def test_create_geonode_resource_rollback_should_delete_created_auth_config(self):
+ auth_config = BasicAuthHandler.create_auth_config({"username": "test_user", "password": "test_password"})
+ exec_id = orchestrator.create_execution_request(
+ user=self.owner,
+ func_name="funct1",
+ step="step",
+ input_params={"auth_config_id": auth_config.pk},
+ )
+
+ self.handler._create_geonode_resource_rollback(exec_id, istance_name="missing-resource")
+
+ self.assertFalse(AuthConfig.objects.filter(pk=auth_config.pk).exists())
+
@patch("geonode.upload.handlers.common.remote.import_orchestrator")
def test_import_resource_should_work(self, patch_upload):
patch_upload.apply_async.side_effect = MagicMock()
diff --git a/geonode/upload/handlers/remote/cog.py b/geonode/upload/handlers/remote/cog.py
index 191a2f0ce57..816f3bb2228 100644
--- a/geonode/upload/handlers/remote/cog.py
+++ b/geonode/upload/handlers/remote/cog.py
@@ -21,6 +21,7 @@
from osgeo import gdal
from geonode.security.utils import init_gdal_security
+from geonode.security.auth_registry import auth_handler_registry
from geonode.layers.models import Dataset
from geonode.upload.handlers.common.remote import BaseRemoteResourceHandler
from geonode.upload.handlers.common.serializer import RemoteResourceSerializer
@@ -56,11 +57,12 @@ def can_handle(_data) -> bool:
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}")
- try:
- # Reachability check using HEAD
- head_res = requests.head(url, timeout=10, allow_redirects=True)
+ """
+ logger.debug(f"Checking COG URL validity (HEAD): {url}")
+ try:
+ auth = BaseRemoteResourceHandler.get_request_auth_from_execution(kwargs.get("execution_id"))
+ # Reachability check using HEAD
+ head_res = requests.head(url, timeout=10, allow_redirects=True, auth=auth)
logger.debug(f"HTTP HEAD status: {head_res.status_code}")
head_res.raise_for_status()
@@ -73,7 +75,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 = requests.get(url, headers={"Range": "bytes=0-1"}, timeout=10, stream=True, auth=auth)
logger.debug(f"Range GET status: {range_res.status_code}")
try:
if range_res.status_code != 206:
@@ -105,64 +107,69 @@ def create_geonode_resource(
logger.debug(f"Entering create_geonode_resource for {layer_name}")
_exec = orchestrator.get_execution_object(execution_id)
params = _exec.input_params.copy()
- url = params.get("url")
+ original_url = params.get("url")
+ url = original_url
+ gdal_config_options = {
+ "GDAL_HTTP_TIMEOUT": "15",
+ "GDAL_HTTP_MAX_RETRY": "1",
+ }
+ auth_config = self.get_auth_config_from_execution(_exec)
+ if auth_config:
+ auth_handler = auth_handler_registry.build(auth_config)
+ try:
+ url, auth_gdal_config_options = auth_handler.get_gdal_config(original_url)
+ gdal_config_options.update(auth_gdal_config_options)
+ except NotImplementedError:
+ pass
# Extract metadata via GDAL VSICURL
gdal.UseExceptions()
- logger.debug(f"Attempting to open COG with GDAL: /vsicurl/{url}")
+ logger.debug(f"Attempting to open COG with GDAL: /vsicurl/{original_url}")
try:
- # Set GDAL config options for faster failure
- gdal.SetThreadLocalConfigOption("GDAL_HTTP_TIMEOUT", "15")
- gdal.SetThreadLocalConfigOption("GDAL_HTTP_MAX_RETRY", "1")
init_gdal_security()
-
- vsiurl = f"/vsicurl/{url}"
- ds = gdal.OpenEx(vsiurl)
- if ds is None:
- logger.debug(f"GDAL failed to open dataset: {vsiurl}")
- raise ImportException(f"Could not open remote COG: {url}")
-
- if not ds.GetSpatialRef():
- raise ImportException(f"Could not extract spatial reference from COG: {url}")
-
- srid = self.identify_authority(ds)
-
- # Get BBox
- gt = ds.GetGeoTransform()
- width = ds.RasterXSize
- height = ds.RasterYSize
-
- # Check for rotation
- is_rotated = gt[2] != 0 or gt[4] != 0
-
- if is_rotated:
- logger.info("COG has rotation/skew - calculating envelope bbox")
- # Calculate all four corners
- corners = [
- (gt[0], gt[3]),
- (gt[0] + width * gt[1], gt[3] + width * gt[4]),
- (gt[0] + width * gt[1] + height * gt[2], gt[3] + width * gt[4] + height * gt[5]),
- (gt[0] + height * gt[2], gt[3] + height * gt[5]),
- ]
- xs = [x for x, y in corners]
- ys = [y for x, y in corners]
- bbox = [min(xs), min(ys), max(xs), max(ys)]
- else:
- # Simple calculation for north-up images
- minx = gt[0]
- maxy = gt[3]
- maxx = gt[0] + width * gt[1]
- miny = gt[3] + height * gt[5]
- bbox = [minx, miny, maxx, maxy]
-
- ds = None # close dataset
+
+ with self.gdal_config_options(gdal_config_options):
+ vsiurl = f"/vsicurl/{url}"
+ ds = gdal.OpenEx(vsiurl)
+ if ds is None:
+ logger.debug(f"GDAL failed to open dataset: {vsiurl}")
+ raise ImportException(f"Could not open remote COG: {url}")
+ if not ds.GetSpatialRef():
+ raise ImportException(f"Could not extract spatial reference from COG: {url}")
+ srid = self.identify_authority(ds)
+ # Get BBox
+ gt = ds.GetGeoTransform()
+ width = ds.RasterXSize
+ height = ds.RasterYSize
+ # Check for rotation
+ is_rotated = gt[2] != 0 or gt[4] != 0
+ if is_rotated:
+ logger.info("COG has rotation/skew - calculating envelope bbox")
+ # Calculate all four corners
+ corners = [
+ (gt[0], gt[3]),
+ (gt[0] + width * gt[1], gt[3] + width * gt[4]),
+ (gt[0] + width * gt[1] + height * gt[2], gt[3] + width * gt[4] + height * gt[5]),
+ (gt[0] + height * gt[2], gt[3] + height * gt[5]),
+ ]
+ xs = [x for x, y in corners]
+ ys = [y for x, y in corners]
+ bbox = [min(xs), min(ys), max(xs), max(ys)]
+ else:
+ # Simple calculation for north-up images
+ minx = gt[0]
+ maxy = gt[3]
+ maxx = gt[0] + width * gt[1]
+ miny = gt[3] + height * gt[5]
+ bbox = [minx, miny, maxx, maxy]
+ ds = None # close dataset
logger.debug("GDAL operations finished.")
except Exception as e:
logger.debug(f"GDAL ERROR: {str(e)}")
logger.exception(e)
if isinstance(e, ImportException):
raise e
- raise ImportException(f"Failed to extract metadata from COG: {url}")
+ raise ImportException(f"Failed to extract metadata from COG: {original_url}")
resource = super().create_geonode_resource(layer_name, alternate, execution_id, resource_type, asset)
resource.set_bbox_polygon(bbox, srid)
return resource
diff --git a/geonode/upload/handlers/remote/flatgeobuf.py b/geonode/upload/handlers/remote/flatgeobuf.py
index f820834dc05..e6d47936cb7 100644
--- a/geonode/upload/handlers/remote/flatgeobuf.py
+++ b/geonode/upload/handlers/remote/flatgeobuf.py
@@ -21,6 +21,7 @@
from osgeo import gdal
from geonode.security.utils import init_gdal_security
+from geonode.security.auth_registry import auth_handler_registry
from geonode.layers.models import Dataset
from geonode.upload.handlers.common.remote import BaseRemoteResourceHandler
from geonode.upload.handlers.common.serializer import RemoteResourceSerializer
@@ -60,8 +61,9 @@ def is_valid_url(url, **kwargs):
"""
logger.debug(f"Checking FlatGeobuf URL validity (HEAD): {url}")
try:
+ auth = BaseRemoteResourceHandler.get_request_auth_from_execution(kwargs.get("execution_id"))
# Reachability check using HEAD
- head_res = requests.head(url, timeout=10, allow_redirects=True)
+ head_res = requests.head(url, timeout=10, allow_redirects=True, auth=auth)
logger.debug(f"HTTP HEAD status: {head_res.status_code}")
head_res.raise_for_status()
@@ -74,7 +76,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 = requests.get(url, headers={"Range": "bytes=0-1"}, timeout=10, stream=True, auth=auth)
logger.debug(f"Range GET status: {range_res.status_code}")
try:
if range_res.status_code != 206:
@@ -107,59 +109,70 @@ def create_geonode_resource(
logger.debug(f"Entering create_geonode_resource for {layer_name}")
_exec = orchestrator.get_execution_object(execution_id)
params = _exec.input_params.copy()
- url = params.get("url")
+ original_url = params.get("url")
+ url = original_url
+ gdal_config_options = {
+ "GDAL_HTTP_TIMEOUT": "15",
+ "GDAL_HTTP_MAX_RETRY": "1",
+ }
+ auth_config = self.get_auth_config_from_execution(_exec)
+ if auth_config:
+ auth_handler = auth_handler_registry.build(auth_config)
+ try:
+ url, auth_gdal_config_options = auth_handler.get_gdal_config(original_url)
+ gdal_config_options.update(auth_gdal_config_options)
+ except NotImplementedError:
+ pass
# Extract metadata via GDAL VSICURL
gdal.UseExceptions()
- logger.debug(f"Attempting to open FlatGeobuf with GDAL: /vsicurl/{url}")
+ logger.debug(f"Attempting to open FlatGeobuf with GDAL: /vsicurl/{original_url}")
try:
- # Set GDAL config options for faster failure
- gdal.SetThreadLocalConfigOption("GDAL_HTTP_TIMEOUT", "15")
- gdal.SetThreadLocalConfigOption("GDAL_HTTP_MAX_RETRY", "1")
init_gdal_security()
- vsiurl = f"/vsicurl/{url}"
- ds = gdal.OpenEx(
- vsiurl,
- allowed_drivers=["FlatGeobuf"],
- )
- if ds is None:
- logger.debug(f"GDAL failed to open dataset: {vsiurl}")
- raise ImportException(f"Could not open remote FlatGeobuf: {url}")
+ with self.gdal_config_options(gdal_config_options):
+ vsiurl = f"/vsicurl/{url}"
+ ds = gdal.OpenEx(
+ vsiurl,
+ allowed_drivers=["FlatGeobuf"],
+ )
+ if ds is None:
+ logger.debug(f"GDAL failed to open dataset: /vsicurl/{original_url}")
+ raise ImportException(f"Could not open remote FlatGeobuf: {original_url}")
- logger.debug("GDAL opened dataset. Extracting metadata...")
+ logger.debug("GDAL opened dataset. Extracting metadata...")
- layer = ds.GetLayer(0)
- if layer is None:
- raise ImportException(f"No layers found in FlatGeobuf: {url}")
+ layer = ds.GetLayer(0)
+ if layer is None:
+ raise ImportException(f"No layers found in FlatGeobuf: {original_url}")
- if not layer.GetSpatialRef():
- raise ImportException(f"Could not extract spatial reference from Flatgeobuf: {url}")
+ if not layer.GetSpatialRef():
+ raise ImportException(f"Could not extract spatial reference from Flatgeobuf: {original_url}")
- srid = self.identify_authority(layer)
+ srid = self.identify_authority(layer)
- # Get BBox
- try:
- extent = layer.GetExtent()
- bbox = [extent[0], extent[2], extent[1], extent[3]]
- logger.debug(f"Extracted bounding box: {bbox}")
- except Exception as e:
- logger.error(f"Could not extract bounding box from FlatGeobuf: {url}. Error: {e}")
- raise ImportException(
- "Could not extract bounding box from FlatGeobuf. " "The file may be empty or corrupted."
- )
+ # Get BBox
+ try:
+ extent = layer.GetExtent()
+ bbox = [extent[0], extent[2], extent[1], extent[3]]
+ logger.debug(f"Extracted bounding box: {bbox}")
+ except Exception as e:
+ logger.error(f"Could not extract bounding box from FlatGeobuf: {original_url}. Error: {e}")
+ raise ImportException(
+ "Could not extract bounding box from FlatGeobuf. " "The file may be empty or corrupted."
+ )
- # Get feature attributes
- layer_defn = layer.GetLayerDefn()
- attribute_map = []
- for i in range(layer_defn.GetFieldCount()):
- field_defn = layer_defn.GetFieldDefn(i)
- attribute_map.append([field_defn.GetName(), field_defn.GetTypeName()])
+ # Get feature attributes
+ layer_defn = layer.GetLayerDefn()
+ attribute_map = []
+ for i in range(layer_defn.GetFieldCount()):
+ field_defn = layer_defn.GetFieldDefn(i)
+ attribute_map.append([field_defn.GetName(), field_defn.GetTypeName()])
- logger.debug(f"Extracted schema with {len(attribute_map)} fields")
- logger.debug("GDAL operations finished.")
+ logger.debug(f"Extracted schema with {len(attribute_map)} fields")
+ logger.debug("GDAL operations finished.")
- ds = None # close dataset
+ ds = None # close dataset
except ImportException as e:
raise e
except Exception as e:
diff --git a/geonode/upload/handlers/remote/serializers/wms.py b/geonode/upload/handlers/remote/serializers/wms.py
index 6642b42dd6c..a99add9a78e 100644
--- a/geonode/upload/handlers/remote/serializers/wms.py
+++ b/geonode/upload/handlers/remote/serializers/wms.py
@@ -29,28 +29,8 @@ class Meta:
"identifier",
"bbox",
"parse_remote_metadata",
- "authentication",
)
identifier = serializers.CharField(required=True)
bbox = serializers.ListField(required=False)
parse_remote_metadata = serializers.BooleanField(required=False, default=False)
- authentication = serializers.JSONField(required=False, allow_null=True)
-
- def validate_authentication(self, value):
- if value is None:
- return value
- if not isinstance(value, dict):
- raise serializers.ValidationError("Authentication must be an object.")
-
- auth_type = value.get("type")
- if not auth_type:
- raise serializers.ValidationError("Authentication type is required.")
-
- auth_payload = value.get("payload")
- if auth_payload is None:
- raise serializers.ValidationError("Authentication payload is required.")
- if not isinstance(auth_payload, dict):
- raise serializers.ValidationError("Authentication payload must be an object.")
-
- return value
diff --git a/geonode/upload/handlers/remote/tests/test_wms.py b/geonode/upload/handlers/remote/tests/test_wms.py
index 20061b92053..d4684b8a55d 100644
--- a/geonode/upload/handlers/remote/tests/test_wms.py
+++ b/geonode/upload/handlers/remote/tests/test_wms.py
@@ -152,7 +152,7 @@ def test_extract_params_from_data_should_create_basic_auth_config(self):
self.assertEqual({"username": "test_user", "password": "test_password"}, auth_config.payload)
def test_create_geonode_resource_rollback_should_delete_created_auth_config(self):
- auth_config = BasicAuthHandler.create_auth_config("test_user", "test_password")
+ auth_config = BasicAuthHandler.create_auth_config({"username": "test_user", "password": "test_password"})
exec_id = orchestrator.create_execution_request(
user=self.user,
func_name="funct1",
@@ -165,7 +165,7 @@ def test_create_geonode_resource_rollback_should_delete_created_auth_config(self
self.assertFalse(AuthConfig.objects.filter(pk=auth_config.pk).exists())
def test_create_geonode_resource_rollback_should_not_delete_attached_auth_config(self):
- auth_config = BasicAuthHandler.create_auth_config("test_user", "test_password")
+ auth_config = BasicAuthHandler.create_auth_config({"username": "test_user", "password": "test_password"})
self._create_wms_service(self.valid_url, auth_config=auth_config)
exec_id = orchestrator.create_execution_request(
user=self.user,
@@ -265,7 +265,7 @@ def test_prepare_import_should_update_the_execid(self, get_wms_resource, remote_
@patch("geonode.upload.handlers.remote.wms.WmsServiceHandler")
@patch("geonode.upload.handlers.remote.wms.WebMapService")
def test_prepare_import_should_use_matching_owned_service_auth_config(self, web_map_service, remote_wms):
- service_url = "http://fake/foo?bar"
+ service_url = self.valid_url
fake_url = ParseResult(scheme="http", netloc="fake", path="/foo", params="", query="bar", fragment="")
remote_wms.get_cleaned_url_params.return_value = fake_url, None, None, None
auth_config = AuthConfig.objects.create(
@@ -285,6 +285,7 @@ def test_prepare_import_should_use_matching_owned_service_auth_config(self, web_
input_params=self.valid_payload_with_parse_true,
)
+ self.handler.pre_validation(files=[], execution_id=str(exec_id))
self.handler.prepare_import(files=[], execution_id=str(exec_id))
expected_auth = auth_handler_registry.build(auth_config).get_request_auth()
@@ -295,7 +296,7 @@ def test_prepare_import_should_use_matching_owned_service_auth_config(self, web_
@patch("geonode.upload.handlers.remote.wms.WmsServiceHandler")
@patch("geonode.upload.handlers.remote.wms.WebMapService")
def test_prepare_import_should_not_use_service_auth_config_from_another_owner(self, web_map_service, remote_wms):
- service_url = "http://fake/foo?bar"
+ service_url = self.valid_url
fake_url = ParseResult(scheme="http", netloc="fake", path="/foo", params="", query="bar", fragment="")
remote_wms.get_cleaned_url_params.return_value = fake_url, None, None, None
auth_config = AuthConfig.objects.create(
@@ -316,6 +317,7 @@ def test_prepare_import_should_not_use_service_auth_config_from_another_owner(se
input_params=self.valid_payload_with_parse_true,
)
+ self.handler.pre_validation(files=[], execution_id=str(exec_id))
self.handler.prepare_import(files=[], execution_id=str(exec_id))
web_map_service.assert_called_once_with(self.valid_url, auth=None)
diff --git a/geonode/upload/handlers/remote/tiles3d.py b/geonode/upload/handlers/remote/tiles3d.py
index 94d655a3716..7862f621ab5 100644
--- a/geonode/upload/handlers/remote/tiles3d.py
+++ b/geonode/upload/handlers/remote/tiles3d.py
@@ -60,9 +60,10 @@ def have_table(self):
@staticmethod
def is_valid_url(url, **kwargs):
- BaseRemoteResourceHandler.is_valid_url(url)
+ BaseRemoteResourceHandler.is_valid_url(url, **kwargs)
try:
- payload = requests.get(url, timeout=10).json()
+ auth = BaseRemoteResourceHandler.get_request_auth_from_execution(kwargs.get("execution_id"))
+ payload = requests.get(url, timeout=10, auth=auth).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"))
@@ -97,7 +98,8 @@ def create_geonode_resource(
raise Invalid3DTilesException("Invalid URL Provided")
try:
- js_file = requests.get(url, timeout=10).json()
+ auth = self.get_request_auth_from_execution(execution_id)
+ js_file = requests.get(url, timeout=10, auth=auth).json()
except Exception as e:
raise Invalid3DTilesException(e)
@@ -114,7 +116,7 @@ def create_geonode_resource(
return resource
def generate_resource_payload(self, layer_name, alternate, asset, _exec, workspace, **kwargs):
- return dict(
+ payload = dict(
resource_type="dataset",
subtype=kwargs.get("type"),
sourcetype=SOURCE_TYPE_REMOTE,
@@ -123,3 +125,6 @@ def generate_resource_payload(self, layer_name, alternate, asset, _exec, workspa
title=kwargs.get("title", layer_name),
owner=_exec.user,
)
+ if kwargs.get("auth_config_id"):
+ payload["auth_config_id"] = kwargs.get("auth_config_id")
+ return payload
diff --git a/geonode/upload/handlers/remote/wms.py b/geonode/upload/handlers/remote/wms.py
index 82245d2a3f9..e13883dd8af 100644
--- a/geonode/upload/handlers/remote/wms.py
+++ b/geonode/upload/handlers/remote/wms.py
@@ -28,7 +28,6 @@
from geonode.upload.orchestrator import orchestrator
from geonode.harvesting.harvesters.wms import WebMapService
from geonode.security.auth_registry import auth_handler_registry
-from geonode.security.models import AuthConfig
from geonode.services.models import Service
from geonode.services.serviceprocessors.wms import WmsServiceHandler
from geonode.resource.registry import resource_manager_registry
@@ -68,35 +67,9 @@ def extract_params_from_data(_data, action=None):
payload["identifier"] = original_data.pop("identifier", None)
payload["bbox"] = original_data.pop("bbox", None)
payload["parse_remote_metadata"] = original_data.pop("parse_remote_metadata", None)
- authentication = original_data.pop("authentication", None)
- if authentication:
- # Resolve the auth handler from the payload type, then let the handler
- # validate the auth-specific payload before storing it.
- auth_type = authentication.get("type")
- auth_payload = authentication.get("payload") or {}
- auth_handler_cls = auth_handler_registry.get_handler_class(auth_type)
- if auth_handler_cls is None:
- raise ValueError(f"Unsupported authentication type '{auth_type}'")
- auth_handler_cls.validate(auth_payload)
- auth_config = AuthConfig(type=auth_type)
- auth_config.payload = auth_payload
- auth_config.save()
- payload["auth_config_id"] = auth_config.pk
return payload, original_data
- def _create_geonode_resource_rollback(self, exec_id, istance_name=None, *args, **kwargs):
- super()._create_geonode_resource_rollback(exec_id, istance_name=istance_name)
-
- _exec = orchestrator.get_execution_object(exec_id)
- auth_config_id = _exec.input_params.get("auth_config_id")
- if auth_config_id:
- AuthConfig.objects.filter(
- pk=auth_config_id,
- authconfigresources__isnull=True,
- url_patterns__isnull=True,
- ).delete()
-
def prepare_import(self, files, execution_id, **kwargs):
"""
If the title and bbox must be retrieved from the remote resource
@@ -115,32 +88,8 @@ def prepare_import(self, files, execution_id, **kwargs):
"remote_resource_id": _exec.input_params.get("identifier", None),
}
- auth = None
- auth_config_id = _exec.input_params.get("auth_config_id")
- if auth_config_id:
- # The import already has an AuthConfig assigned, for example from
- # credentials provided in the upload payload. Use it before looking
- # for credentials on a matching remote service.
- auth_config = AuthConfig.objects.filter(pk=auth_config_id).first()
- else:
- user = _exec.user
- service = None
- if user and user.is_authenticated:
- # Reuse credentials only from a matching service owned by the importing user.
- service = (
- Service.objects.filter(harvester__remote_url=ows_url, owner=user)
- .select_related("auth_config")
- .first()
- )
- if service and service.auth_config:
- # Assign the service AuthConfig on the new remote resource.
- auth_config = service.auth_config
- to_update["auth_config_id"] = auth_config.pk
- else:
- auth_config = None
- if auth_config:
- # Build runtime auth for the upstream WMS metadata request.
- auth = auth_handler_registry.build(auth_config).get_request_auth()
+ auth_config = self.get_auth_config_from_execution(_exec)
+ auth = auth_handler_registry.build(auth_config).get_request_auth() if auth_config else None
if _exec.input_params.get("parse_remote_metadata", False):
try: