From fd1e300aefb186ed7cf4cb3efa2c6608c392a3c9 Mon Sep 17 00:00:00 2001 From: sijandh35 Date: Tue, 4 Aug 2026 07:56:25 +0000 Subject: [PATCH 1/6] [Fixes #14481] Enhancement of Resource cloning logic --- geonode/base/api/serializers.py | 6 ++++- geonode/base/api/tests.py | 41 +++++++++++++++++++++++++++++++++ geonode/base/api/views.py | 2 +- geonode/base/models.py | 41 +++++++++++++++++++++++++++++---- geonode/upload/api/views.py | 2 ++ 5 files changed, 85 insertions(+), 7 deletions(-) diff --git a/geonode/base/api/serializers.py b/geonode/base/api/serializers.py index 6d8094518d0..bcf2a934fe6 100644 --- a/geonode/base/api/serializers.py +++ b/geonode/base/api/serializers.py @@ -647,7 +647,7 @@ class ResourceBaseSerializer(MultiLangOutputMixin, DynamicModelSerializer): license = ComplexDynamicRelationField(LicenseSerializer, embed=True) spatial_representation_type = ComplexDynamicRelationField(SpatialRepresentationTypeSerializer, embed=True) blob = serializers.JSONField(required=False, write_only=True) - is_copyable = serializers.BooleanField(read_only=True) + is_copyable = serializers.SerializerMethodField(read_only=True) download_url = DownloadLinkField(read_only=True) favorite = FavoriteField(read_only=True) download_urls = DownloadArrayLinkField(read_only=True) @@ -791,6 +791,10 @@ def get_perms(self, instance): ) return permissions + def get_is_copyable(self, instance): + request = self.context.get("request") + return instance.is_copyable_by(request.user if request else None) + def save(self, **kwargs): extent = self.validated_data.pop("extent", None) instance = super().save(**kwargs) diff --git a/geonode/base/api/tests.py b/geonode/base/api/tests.py index 0d0be73cabd..ce0d2475373 100644 --- a/geonode/base/api/tests.py +++ b/geonode/base/api/tests.py @@ -2907,6 +2907,47 @@ def test_resource_service_copy_with_perms_dataset(self): ) self._assertCloningWithPerms(resource) + def test_is_copyable_by_remote_dataset(self): + from geonode.base import enumerations + from geonode.security.models import AuthConfig + + owner = get_user_model().objects.get(username="bobby") + other = get_user_model().objects.get(username="norman") + admin = get_user_model().objects.get(username="admin") + + resource = Dataset.objects.create( + owner=owner, + name="test_remote_copy", + store="geonode_data", + subtype="remote", + alternate="geonode:test_remote_copy", + resource_type="dataset", + sourcetype=enumerations.SOURCE_TYPE_REMOTE, + uuid=str(uuid4()), + ) + try: + # no AuthConfig: owner and administrators may clone, nobody else + self.assertTrue(resource.is_copyable_by(owner)) + self.assertFalse(resource.is_copyable_by(other)) + self.assertTrue(resource.is_copyable_by(admin)) + + # an AuthConfig restricts cloning to the owner, admins included + resource.auth_config = AuthConfig.objects.create(type="basic") + resource.save() + resource.refresh_from_db() + self.assertTrue(resource.is_copyable_by(owner)) + self.assertFalse(resource.is_copyable_by(other)) + self.assertFalse(resource.is_copyable_by(admin)) + + # attaching an arbitrary file must not make it copyable + create_asset_and_link(resource, other, [os.path.join(gisdata.GOOD_DATA, "vector/single_point.shp")]) + self.assertFalse(resource.is_copyable_by(other)) + finally: + try: + resource.delete() + except Exception as e: + logger.warning(f"Can't delete test resource {resource}", exc_info=e) + @patch.dict(os.environ, {"ASYNC_SIGNALS": "False"}) @override_settings(ASYNC_SIGNALS=False) def test_resource_service_copy_with_perms_dataset_set_default_perms(self): diff --git a/geonode/base/api/views.py b/geonode/base/api/views.py index cf6c6b839da..9a019189441 100644 --- a/geonode/base/api/views.py +++ b/geonode/base/api/views.py @@ -1127,7 +1127,7 @@ def resource_service_copy(self, request, pk, *args, **kwargs): or not request.user.has_perm("view_resourcebase", resource.get_self_resource()) ): return Response(status=status.HTTP_403_FORBIDDEN) - if not resource.is_copyable: + if not resource.is_copyable_by(request.user): return Response({"message": "Resource can not be cloned."}, status=400) try: request_params = self._get_request_params(request) diff --git a/geonode/base/models.py b/geonode/base/models.py index ac874721f9f..6853ba8c552 100644 --- a/geonode/base/models.py +++ b/geonode/base/models.py @@ -1309,14 +1309,45 @@ def instance_is_processed(self): except Exception: return False - @property - def is_copyable(self): + def is_copyable_by(self, user): + """ + Whether ``user`` is allowed to clone this resource. + """ if self.resource_type == "dataset": - from geonode.assets.utils import get_default_asset + instance = self.get_real_instance() + if self.sourcetype == enumerations.SOURCE_TYPE_REMOTE: + if user is None: + return False + if user == self.owner: + return True + remote_service = getattr(instance, "remote_service", None) + has_auth_config = self.auth_config_id or (remote_service and remote_service.auth_config_id) + if not has_auth_config and user.is_superuser: + return True + else: + return False + if instance.is_vector(): + return True + if instance.is_raster: + from geonode.assets.handlers import asset_handler_registry + from geonode.assets.models import Asset + + asset = Asset.objects.filter(link__resource=self, title="Original").last() + asset = asset.get_real_instance() if asset else None + location = getattr(asset, "location", None) + if not location: + return False + storage = asset_handler_registry.get_handler(asset).get_storage_manager(asset) + return all(storage.exists(_file) for _file in location) + + # this is fallback + from geonode.assets.models import Asset from geonode.geoserver.helpers import select_relevant_files - asset = get_default_asset(self) # TODO: maybe we need to filter by original files - allowed_file = select_relevant_files(get_allowed_extensions(), asset.location) if asset else [] + asset = Asset.objects.filter(link__resource=self, title="Original").last() + asset = asset.get_real_instance() if asset else None + location = getattr(asset, "location", None) + allowed_file = select_relevant_files(get_allowed_extensions(), location) if location else [] return len(allowed_file) != 0 return True diff --git a/geonode/upload/api/views.py b/geonode/upload/api/views.py index 6bc62b0f11c..45655999dbc 100644 --- a/geonode/upload/api/views.py +++ b/geonode/upload/api/views.py @@ -251,6 +251,8 @@ class ResourceImporter(DynamicModelViewSet): def copy(self, request, *args, **kwargs): try: resource = self.get_object() + if not resource.is_copyable_by(request.user): + return Response({"message": "Resource can not be cloned."}, status=400) if resource.resourcehandlerinfo_set.exists(): handler_module_path = resource.resourcehandlerinfo_set.first().handler_module_path From 3c0809f95b07fbf70952684aba8f4b2a80cba3e7 Mon Sep 17 00:00:00 2001 From: sijandh35 Date: Tue, 4 Aug 2026 10:04:12 +0000 Subject: [PATCH 2/6] [Fixes #14481] update test case and handle edge case for error --- geonode/base/api/tests.py | 53 ++++++++++++++++++++++++++------------- geonode/base/models.py | 6 ++++- 2 files changed, 41 insertions(+), 18 deletions(-) diff --git a/geonode/base/api/tests.py b/geonode/base/api/tests.py index ce0d2475373..b63844ce579 100644 --- a/geonode/base/api/tests.py +++ b/geonode/base/api/tests.py @@ -2868,28 +2868,47 @@ def test_resource_service_copy(self): second_cloned = Dataset.objects.exclude(pk__in=[resource.pk, cloned_resource.pk]).latest("id") self.assertFalse(second_cloned.featured, msg="Cloned resource should have featured=False") - # clone dataset with invalid file - # resource.files = ["/path/invalid_file.wrong"] - # resource.save() - asset.location = ["/path/invalid_file.wrong"] - asset.save() - response = self.client.put(copy_url) - - self.assertEqual(response.status_code, 400) - self.assertEqual(response.json()["message"], "Resource can not be cloned.") - # clone dataset with no files - link.delete() - asset.delete() - response = self.client.put(copy_url) - - self.assertEqual(response.status_code, 400) - self.assertEqual(response.json()["message"], "Resource can not be cloned.") - # clean + # the file/asset based checks do not apply to non remote vector datasets anymore, + # as the data can also be only stored on db level and cloning takes care of cloning db table. try: resource.delete() except Exception as e: logger.warning(f"Can't delete test resource {resource}", exc_info=e) + def test_is_copyable_by_raster_dataset(self): + owner = get_user_model().objects.get(username="admin") + raster_file = os.path.join(gisdata.GOOD_DATA, "raster", "relief_san_andres.tif") + resource = Dataset.objects.create( + owner=owner, + name="test_raster_copy", + store="geonode_data", + subtype="raster", + alternate="geonode:test_raster_copy", + resource_type="dataset", + uuid=str(uuid4()), + ) + try: + asset, _ = create_asset_and_link(resource, owner, [raster_file], title="Original") + self.assertTrue(resource.is_copyable_by(owner)) + + location = asset.location + asset.location = ["/path/invalid_file.wrong"] + asset.save() + self.assertFalse(resource.is_copyable_by(owner)) + + asset.location = location + asset.title = "not_the_original" + asset.save() + self.assertFalse(resource.is_copyable_by(owner)) + + asset.delete() + self.assertFalse(resource.is_copyable_by(owner)) + finally: + try: + resource.delete() + except Exception as e: + logger.warning(f"Can't delete test resource {resource}", exc_info=e) + def test_resource_service_copy_with_perms_dataset(self): files = os.path.join(gisdata.GOOD_DATA, "vector/single_point.shp") files_as_dict, _ = get_files(files) diff --git a/geonode/base/models.py b/geonode/base/models.py index 6853ba8c552..90fcd9e0854 100644 --- a/geonode/base/models.py +++ b/geonode/base/models.py @@ -1338,7 +1338,11 @@ def is_copyable_by(self, user): if not location: return False storage = asset_handler_registry.get_handler(asset).get_storage_manager(asset) - return all(storage.exists(_file) for _file in location) + try: + return all(storage.exists(_file) for _file in location) + except Exception as e: + logger.warning(f"Cannot check the files of asset {asset.pk} for resource {self.pk}: {e}") + return False # this is fallback from geonode.assets.models import Asset From fe249ce64439a195b0d1becce889aa33d677c3b6 Mon Sep 17 00:00:00 2001 From: sijandh35 Date: Wed, 5 Aug 2026 11:29:26 +0000 Subject: [PATCH 3/6] [Fixes #14481] update the condition to handle more case --- geonode/base/api/tests.py | 26 ++++++++++++ geonode/base/models.py | 83 +++++++++++++++++++++------------------ 2 files changed, 70 insertions(+), 39 deletions(-) diff --git a/geonode/base/api/tests.py b/geonode/base/api/tests.py index b63844ce579..53bc071f8c5 100644 --- a/geonode/base/api/tests.py +++ b/geonode/base/api/tests.py @@ -2875,6 +2875,32 @@ def test_resource_service_copy(self): except Exception as e: logger.warning(f"Can't delete test resource {resource}", exc_info=e) + def test_is_copyable_by_document(self): + owner = get_user_model().objects.get(username="admin") + raster_file = os.path.join(gisdata.GOOD_DATA, "raster", "relief_san_andres.tif") + resource = Document.objects.create( + owner=get_user_model().objects.get(username="admin"), + subtype="image", + alternate="geonode:test_copy", + resource_type="document", + uuid=str(uuid4()), + ) + try: + asset, _ = create_asset_and_link(resource, owner, [raster_file]) + self.assertTrue(resource.is_copyable_by(owner)) + + asset.location = ["/path/invalid_file.wrong"] + asset.save() + self.assertFalse(resource.is_copyable_by(owner)) + + asset.delete() + self.assertFalse(resource.is_copyable_by(owner)) + finally: + try: + resource.delete() + except Exception as e: + logger.warning(f"Can't delete test resource {resource}", exc_info=e) + def test_is_copyable_by_raster_dataset(self): owner = get_user_model().objects.get(username="admin") raster_file = os.path.join(gisdata.GOOD_DATA, "raster", "relief_san_andres.tif") diff --git a/geonode/base/models.py b/geonode/base/models.py index 90fcd9e0854..146d81570da 100644 --- a/geonode/base/models.py +++ b/geonode/base/models.py @@ -73,7 +73,6 @@ find_by_attr, bbox_to_projection, bbox_swap, - get_allowed_extensions, is_monochromatic_image, ) from geonode.thumbs.utils import thumb_size, remove_thumbs, get_unique_upload_path, ThumbnailAlgorithms @@ -1309,50 +1308,56 @@ def instance_is_processed(self): except Exception: return False + def has_actual_asset(self, original_title=False): + from geonode.assets.handlers import asset_handler_registry + from geonode.assets.models import Asset + + filters = {"link__resource": self} + if original_title: + filters["title"] = "Original" + + asset = Asset.objects.filter(**filters).order_by("created").first() + asset = asset.get_real_instance() if asset else None + location = getattr(asset, "location", None) + if not location: + return False + try: + handler = asset_handler_registry.get_handler(asset) + if handler is None: + return False + storage = handler.get_storage_manager(asset) + return all(storage.exists(_file) for _file in location) + except Exception as e: + logger.warning(f"Cannot check the files of asset {asset.pk} for resource {self.pk}: {e}") + return False + def is_copyable_by(self, user): """ Whether ``user`` is allowed to clone this resource. """ + if self.sourcetype == enumerations.SOURCE_TYPE_REMOTE: + if user is None: + return False + if user == self.owner: + return True + remote_service = getattr(self.get_real_instance(), "remote_service", None) + has_auth_config = self.auth_config_id or (remote_service and remote_service.auth_config_id) + if not has_auth_config and user.is_superuser: + return True + else: + return False if self.resource_type == "dataset": - instance = self.get_real_instance() - if self.sourcetype == enumerations.SOURCE_TYPE_REMOTE: - if user is None: - return False - if user == self.owner: - return True - remote_service = getattr(instance, "remote_service", None) - has_auth_config = self.auth_config_id or (remote_service and remote_service.auth_config_id) - if not has_auth_config and user.is_superuser: - return True - else: - return False - if instance.is_vector(): + if self.subtype in ["vector", "vector_time"]: return True - if instance.is_raster: - from geonode.assets.handlers import asset_handler_registry - from geonode.assets.models import Asset - - asset = Asset.objects.filter(link__resource=self, title="Original").last() - asset = asset.get_real_instance() if asset else None - location = getattr(asset, "location", None) - if not location: - return False - storage = asset_handler_registry.get_handler(asset).get_storage_manager(asset) - try: - return all(storage.exists(_file) for _file in location) - except Exception as e: - logger.warning(f"Cannot check the files of asset {asset.pk} for resource {self.pk}: {e}") - return False - - # this is fallback - from geonode.assets.models import Asset - from geonode.geoserver.helpers import select_relevant_files - - asset = Asset.objects.filter(link__resource=self, title="Original").last() - asset = asset.get_real_instance() if asset else None - location = getattr(asset, "location", None) - allowed_file = select_relevant_files(get_allowed_extensions(), location) if location else [] - return len(allowed_file) != 0 + if self.subtype == "raster": + return self.has_actual_asset(original_title=True) + # for tabular/local 3dtiles etc. + return self.has_actual_asset() + + if self.resource_type == "document": + return self.has_actual_asset() + + # maps/geostory/dashboard goes here return True def keyword_list(self): From fb9ccafbd7ebabd6cbbadb3505c3cfb8ae07fe4d Mon Sep 17 00:00:00 2001 From: sijandh35 Date: Fri, 7 Aug 2026 07:35:55 +0000 Subject: [PATCH 4/6] [Fixes #14481] move is_copyable in resource manager --- geonode/base/api/serializers.py | 10 ++++++- geonode/base/api/tests.py | 34 ++++++++++----------- geonode/base/api/views.py | 2 +- geonode/base/models.py | 52 --------------------------------- geonode/geoapps/manager.py | 3 ++ geonode/layers/manager.py | 7 +++++ geonode/maps/manager.py | 3 ++ geonode/resource/manager.py | 44 ++++++++++++++++++++++++++++ geonode/upload/api/views.py | 4 ++- 9 files changed, 87 insertions(+), 72 deletions(-) diff --git a/geonode/base/api/serializers.py b/geonode/base/api/serializers.py index bcf2a934fe6..4d5ec010fda 100644 --- a/geonode/base/api/serializers.py +++ b/geonode/base/api/serializers.py @@ -792,8 +792,16 @@ def get_perms(self, instance): return permissions def get_is_copyable(self, instance): + from geonode.resource.registry import resource_manager_registry + request = self.context.get("request") - return instance.is_copyable_by(request.user if request else None) + try: + return resource_manager_registry.get_for_instance(instance).is_copyable( + instance, user=request.user if request else None + ) + except Exception as e: + logger.warning(f"Cannot evaluate is_copyable for resource {instance.pk}: {e}") + return False def save(self, **kwargs): extent = self.validated_data.pop("extent", None) diff --git a/geonode/base/api/tests.py b/geonode/base/api/tests.py index 53bc071f8c5..af636193957 100644 --- a/geonode/base/api/tests.py +++ b/geonode/base/api/tests.py @@ -2875,7 +2875,7 @@ def test_resource_service_copy(self): except Exception as e: logger.warning(f"Can't delete test resource {resource}", exc_info=e) - def test_is_copyable_by_document(self): + def test_is_copyable_document(self): owner = get_user_model().objects.get(username="admin") raster_file = os.path.join(gisdata.GOOD_DATA, "raster", "relief_san_andres.tif") resource = Document.objects.create( @@ -2887,21 +2887,21 @@ def test_is_copyable_by_document(self): ) try: asset, _ = create_asset_and_link(resource, owner, [raster_file]) - self.assertTrue(resource.is_copyable_by(owner)) + self.assertTrue(resource_manager_registry.get_for_instance(resource).is_copyable(resource, user=owner)) asset.location = ["/path/invalid_file.wrong"] asset.save() - self.assertFalse(resource.is_copyable_by(owner)) + self.assertFalse(resource_manager_registry.get_for_instance(resource).is_copyable(resource, user=owner)) asset.delete() - self.assertFalse(resource.is_copyable_by(owner)) + self.assertFalse(resource_manager_registry.get_for_instance(resource).is_copyable(resource, user=owner)) finally: try: resource.delete() except Exception as e: logger.warning(f"Can't delete test resource {resource}", exc_info=e) - def test_is_copyable_by_raster_dataset(self): + def test_is_copyable_raster_dataset(self): owner = get_user_model().objects.get(username="admin") raster_file = os.path.join(gisdata.GOOD_DATA, "raster", "relief_san_andres.tif") resource = Dataset.objects.create( @@ -2915,20 +2915,20 @@ def test_is_copyable_by_raster_dataset(self): ) try: asset, _ = create_asset_and_link(resource, owner, [raster_file], title="Original") - self.assertTrue(resource.is_copyable_by(owner)) + self.assertTrue(resource_manager_registry.get_for_instance(resource).is_copyable(resource, user=owner)) location = asset.location asset.location = ["/path/invalid_file.wrong"] asset.save() - self.assertFalse(resource.is_copyable_by(owner)) + self.assertFalse(resource_manager_registry.get_for_instance(resource).is_copyable(resource, user=owner)) asset.location = location asset.title = "not_the_original" asset.save() - self.assertFalse(resource.is_copyable_by(owner)) + self.assertFalse(resource_manager_registry.get_for_instance(resource).is_copyable(resource, user=owner)) asset.delete() - self.assertFalse(resource.is_copyable_by(owner)) + self.assertFalse(resource_manager_registry.get_for_instance(resource).is_copyable(resource, user=owner)) finally: try: resource.delete() @@ -2952,7 +2952,7 @@ def test_resource_service_copy_with_perms_dataset(self): ) self._assertCloningWithPerms(resource) - def test_is_copyable_by_remote_dataset(self): + def test_is_copyable_remote_dataset(self): from geonode.base import enumerations from geonode.security.models import AuthConfig @@ -2972,21 +2972,21 @@ def test_is_copyable_by_remote_dataset(self): ) try: # no AuthConfig: owner and administrators may clone, nobody else - self.assertTrue(resource.is_copyable_by(owner)) - self.assertFalse(resource.is_copyable_by(other)) - self.assertTrue(resource.is_copyable_by(admin)) + self.assertTrue(resource_manager_registry.get_for_instance(resource).is_copyable(resource, user=owner)) + self.assertFalse(resource_manager_registry.get_for_instance(resource).is_copyable(resource, user=other)) + self.assertTrue(resource_manager_registry.get_for_instance(resource).is_copyable(resource, user=admin)) # an AuthConfig restricts cloning to the owner, admins included resource.auth_config = AuthConfig.objects.create(type="basic") resource.save() resource.refresh_from_db() - self.assertTrue(resource.is_copyable_by(owner)) - self.assertFalse(resource.is_copyable_by(other)) - self.assertFalse(resource.is_copyable_by(admin)) + self.assertTrue(resource_manager_registry.get_for_instance(resource).is_copyable(resource, user=owner)) + self.assertFalse(resource_manager_registry.get_for_instance(resource).is_copyable(resource, user=other)) + self.assertFalse(resource_manager_registry.get_for_instance(resource).is_copyable(resource, user=admin)) # attaching an arbitrary file must not make it copyable create_asset_and_link(resource, other, [os.path.join(gisdata.GOOD_DATA, "vector/single_point.shp")]) - self.assertFalse(resource.is_copyable_by(other)) + self.assertFalse(resource_manager_registry.get_for_instance(resource).is_copyable(resource, user=other)) finally: try: resource.delete() diff --git a/geonode/base/api/views.py b/geonode/base/api/views.py index 9a019189441..30811cc2dda 100644 --- a/geonode/base/api/views.py +++ b/geonode/base/api/views.py @@ -1127,7 +1127,7 @@ def resource_service_copy(self, request, pk, *args, **kwargs): or not request.user.has_perm("view_resourcebase", resource.get_self_resource()) ): return Response(status=status.HTTP_403_FORBIDDEN) - if not resource.is_copyable_by(request.user): + if not resource_manager_registry.get_for_instance(resource).is_copyable(resource, user=request.user): return Response({"message": "Resource can not be cloned."}, status=400) try: request_params = self._get_request_params(request) diff --git a/geonode/base/models.py b/geonode/base/models.py index 146d81570da..02d9a95a597 100644 --- a/geonode/base/models.py +++ b/geonode/base/models.py @@ -1308,58 +1308,6 @@ def instance_is_processed(self): except Exception: return False - def has_actual_asset(self, original_title=False): - from geonode.assets.handlers import asset_handler_registry - from geonode.assets.models import Asset - - filters = {"link__resource": self} - if original_title: - filters["title"] = "Original" - - asset = Asset.objects.filter(**filters).order_by("created").first() - asset = asset.get_real_instance() if asset else None - location = getattr(asset, "location", None) - if not location: - return False - try: - handler = asset_handler_registry.get_handler(asset) - if handler is None: - return False - storage = handler.get_storage_manager(asset) - return all(storage.exists(_file) for _file in location) - except Exception as e: - logger.warning(f"Cannot check the files of asset {asset.pk} for resource {self.pk}: {e}") - return False - - def is_copyable_by(self, user): - """ - Whether ``user`` is allowed to clone this resource. - """ - if self.sourcetype == enumerations.SOURCE_TYPE_REMOTE: - if user is None: - return False - if user == self.owner: - return True - remote_service = getattr(self.get_real_instance(), "remote_service", None) - has_auth_config = self.auth_config_id or (remote_service and remote_service.auth_config_id) - if not has_auth_config and user.is_superuser: - return True - else: - return False - if self.resource_type == "dataset": - if self.subtype in ["vector", "vector_time"]: - return True - if self.subtype == "raster": - return self.has_actual_asset(original_title=True) - # for tabular/local 3dtiles etc. - return self.has_actual_asset() - - if self.resource_type == "document": - return self.has_actual_asset() - - # maps/geostory/dashboard goes here - return True - def keyword_list(self): return [kw.name for kw in self.keywords.all()] diff --git a/geonode/geoapps/manager.py b/geonode/geoapps/manager.py index a6186c61177..3283bbdfb90 100644 --- a/geonode/geoapps/manager.py +++ b/geonode/geoapps/manager.py @@ -76,3 +76,6 @@ def update(self, uuid: str, /, instance: ResourceBase = None, vals: dict = {}, * if payload: return self._create_and_update(payload, instance=instance, notify=notify, request_user=request_user) return super().update(uuid, instance=instance, vals=vals, notify=notify, **kwargs) + + def _is_local_resource_copyable(self, instance: GeoApp, user=None) -> bool: + return True diff --git a/geonode/layers/manager.py b/geonode/layers/manager.py index 63557dd96c5..18916edf891 100644 --- a/geonode/layers/manager.py +++ b/geonode/layers/manager.py @@ -28,3 +28,10 @@ class DatasetResourceManager(BaseResourceManager): """ handled_model = Dataset + + def _is_local_resource_copyable(self, instance: Dataset, user=None) -> bool: + if instance.subtype in ("vector", "vector_time"): + return True + if instance.subtype == "raster": + return self._has_actual_asset(instance, original_title=True) + return super()._is_local_resource_copyable(instance, user=user) diff --git a/geonode/maps/manager.py b/geonode/maps/manager.py index df2d4073162..41a4afa94ec 100644 --- a/geonode/maps/manager.py +++ b/geonode/maps/manager.py @@ -101,3 +101,6 @@ def update(self, uuid: str, /, instance: ResourceBase = None, vals: dict = {}, * notify=notify, ) return instance + + def _is_local_resource_copyable(self, instance: Map, user=None) -> bool: + return True diff --git a/geonode/resource/manager.py b/geonode/resource/manager.py index 120d77e94d5..c51d266c0c8 100644 --- a/geonode/resource/manager.py +++ b/geonode/resource/manager.py @@ -986,6 +986,50 @@ def set_thumbnail( logger.exception(e) return False + def _has_actual_asset(self, instance: ResourceBase, original_title=False) -> bool: + from geonode.assets.handlers import asset_handler_registry + from geonode.assets.models import Asset + + filters = {"link__resource": instance} + if original_title: + filters["title"] = "Original" + + asset = Asset.objects.filter(**filters).order_by("created").first() + asset = asset.get_real_instance() if asset else None + location = getattr(asset, "location", None) + if not location: + return False + try: + handler = asset_handler_registry.get_handler(asset) + if handler is None: + return False + storage = handler.get_storage_manager(asset) + return all(storage.exists(_file) for _file in location) + except Exception as e: + logger.warning(f"Cannot check the files of asset {asset.pk} for resource {instance.pk}: {e}") + return False + + def _is_local_resource_copyable(self, instance: ResourceBase, user=None) -> bool: + return self._has_actual_asset(instance) + + def is_copyable(self, instance: ResourceBase, /, user=None) -> bool: + """ + Whether ``user`` is allowed to clone ``instance``. + """ + _resource = instance.get_real_instance() + if _resource.sourcetype == enumerations.SOURCE_TYPE_REMOTE: + if user is None: + return False + if user == _resource.owner: + return True + remote_service = getattr(_resource, "remote_service", None) + has_auth_config = _resource.auth_config_id or (remote_service and remote_service.auth_config_id) + if not has_auth_config and user.is_superuser: + return True + else: + return False + return self._is_local_resource_copyable(_resource, user=user) + def _apply_extent_and_role_defaults( self, instance: ResourceBase, extent: dict = None, user: settings.AUTH_USER_MODEL = None ) -> None: diff --git a/geonode/upload/api/views.py b/geonode/upload/api/views.py index 45655999dbc..2885cde122a 100644 --- a/geonode/upload/api/views.py +++ b/geonode/upload/api/views.py @@ -249,9 +249,11 @@ class ResourceImporter(DynamicModelViewSet): pagination_class = GeoNodeApiPagination def copy(self, request, *args, **kwargs): + from geonode.resource.registry import resource_manager_registry + try: resource = self.get_object() - if not resource.is_copyable_by(request.user): + if not resource_manager_registry.get_for_instance(resource).is_copyable(resource, user=request.user): return Response({"message": "Resource can not be cloned."}, status=400) if resource.resourcehandlerinfo_set.exists(): handler_module_path = resource.resourcehandlerinfo_set.first().handler_module_path From fc3aaecd18af4007dc91cfd323990d88520e1bf2 Mon Sep 17 00:00:00 2001 From: sijandh35 Date: Fri, 7 Aug 2026 11:38:48 +0000 Subject: [PATCH 5/6] [Fixes #14481] update asset check --- geonode/base/api/tests.py | 10 ---------- geonode/layers/manager.py | 2 +- geonode/resource/manager.py | 18 +++--------------- 3 files changed, 4 insertions(+), 26 deletions(-) diff --git a/geonode/base/api/tests.py b/geonode/base/api/tests.py index af636193957..31435b71ffd 100644 --- a/geonode/base/api/tests.py +++ b/geonode/base/api/tests.py @@ -2889,10 +2889,6 @@ def test_is_copyable_document(self): asset, _ = create_asset_and_link(resource, owner, [raster_file]) self.assertTrue(resource_manager_registry.get_for_instance(resource).is_copyable(resource, user=owner)) - asset.location = ["/path/invalid_file.wrong"] - asset.save() - self.assertFalse(resource_manager_registry.get_for_instance(resource).is_copyable(resource, user=owner)) - asset.delete() self.assertFalse(resource_manager_registry.get_for_instance(resource).is_copyable(resource, user=owner)) finally: @@ -2917,12 +2913,6 @@ def test_is_copyable_raster_dataset(self): asset, _ = create_asset_and_link(resource, owner, [raster_file], title="Original") self.assertTrue(resource_manager_registry.get_for_instance(resource).is_copyable(resource, user=owner)) - location = asset.location - asset.location = ["/path/invalid_file.wrong"] - asset.save() - self.assertFalse(resource_manager_registry.get_for_instance(resource).is_copyable(resource, user=owner)) - - asset.location = location asset.title = "not_the_original" asset.save() self.assertFalse(resource_manager_registry.get_for_instance(resource).is_copyable(resource, user=owner)) diff --git a/geonode/layers/manager.py b/geonode/layers/manager.py index 18916edf891..cc67b0c67f8 100644 --- a/geonode/layers/manager.py +++ b/geonode/layers/manager.py @@ -33,5 +33,5 @@ def _is_local_resource_copyable(self, instance: Dataset, user=None) -> bool: if instance.subtype in ("vector", "vector_time"): return True if instance.subtype == "raster": - return self._has_actual_asset(instance, original_title=True) + return self._has_asset(instance, original_title=True) return super()._is_local_resource_copyable(instance, user=user) diff --git a/geonode/resource/manager.py b/geonode/resource/manager.py index c51d266c0c8..4d71109a5fc 100644 --- a/geonode/resource/manager.py +++ b/geonode/resource/manager.py @@ -986,8 +986,7 @@ def set_thumbnail( logger.exception(e) return False - def _has_actual_asset(self, instance: ResourceBase, original_title=False) -> bool: - from geonode.assets.handlers import asset_handler_registry + def _has_asset(self, instance: ResourceBase, original_title=False) -> bool: from geonode.assets.models import Asset filters = {"link__resource": instance} @@ -996,21 +995,10 @@ def _has_actual_asset(self, instance: ResourceBase, original_title=False) -> boo asset = Asset.objects.filter(**filters).order_by("created").first() asset = asset.get_real_instance() if asset else None - location = getattr(asset, "location", None) - if not location: - return False - try: - handler = asset_handler_registry.get_handler(asset) - if handler is None: - return False - storage = handler.get_storage_manager(asset) - return all(storage.exists(_file) for _file in location) - except Exception as e: - logger.warning(f"Cannot check the files of asset {asset.pk} for resource {instance.pk}: {e}") - return False + return bool(getattr(asset, "location", None)) def _is_local_resource_copyable(self, instance: ResourceBase, user=None) -> bool: - return self._has_actual_asset(instance) + return self._has_asset(instance) def is_copyable(self, instance: ResourceBase, /, user=None) -> bool: """ From f68db8a4a6a3390316408d7656b8c58c7dbae848 Mon Sep 17 00:00:00 2001 From: sijandh35 Date: Fri, 7 Aug 2026 12:38:02 +0000 Subject: [PATCH 6/6] [Fixes #14481] update asset check --- geonode/resource/manager.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/geonode/resource/manager.py b/geonode/resource/manager.py index 4d71109a5fc..3e68bac6886 100644 --- a/geonode/resource/manager.py +++ b/geonode/resource/manager.py @@ -993,9 +993,7 @@ def _has_asset(self, instance: ResourceBase, original_title=False) -> bool: if original_title: filters["title"] = "Original" - asset = Asset.objects.filter(**filters).order_by("created").first() - asset = asset.get_real_instance() if asset else None - return bool(getattr(asset, "location", None)) + return Asset.objects.filter(**filters).exists() def _is_local_resource_copyable(self, instance: ResourceBase, user=None) -> bool: return self._has_asset(instance)