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: