diff --git a/geonode/resource/manager.py b/geonode/resource/manager.py index f06879eec91..da7a39c9410 100644 --- a/geonode/resource/manager.py +++ b/geonode/resource/manager.py @@ -246,7 +246,6 @@ def finalize_creation_permissions( self, instance: ResourceBase, owner: settings.AUTH_USER_MODEL = None, - initial_user: settings.AUTH_USER_MODEL = None, ) -> bool: """ Finalize default permissions for newly created resources, @@ -254,9 +253,12 @@ def finalize_creation_permissions( """ if not instance: return False - if not getattr(settings, "AUTO_ASSIGN_RESOURCE_OWNERSHIP_TO_ADMIN", False): + enabled = getattr(settings, "AUTO_ASSIGN_RESOURCE_OWNERSHIP_TO_ADMIN", False) or getattr( + settings, "AUTO_ASSIGN_RESOURCE_CREATOR_GROUPS_PERMISSIONS", False + ) + if not enabled: return False - instance.set_default_permissions(owner=owner or instance.owner, created=True, initial_user=initial_user) + instance.set_default_permissions(owner=owner or instance.owner, created=True) return True def search(self, filter: dict, /, resource_type: typing.Optional[object]) -> QuerySet: @@ -345,8 +347,8 @@ def create(self, uuid: str, /, resource_type: typing.Optional[object] = None, de if resource_type.objects.filter(uuid=uuid).exists(): return resource_type.objects.filter(uuid=uuid).get() uuid = uuid or str(uuid4()) - initial_user = defaults.get("owner", None) - resolved_owner = self.resolve_creation_owner(initial_user) + originator = defaults.get("owner", None) + resolved_owner = self.resolve_creation_owner(originator) resource_dict = { # TODO: cleanup params and dicts k: v for k, v in defaults.items() @@ -373,13 +375,20 @@ def create(self, uuid: str, /, resource_type: typing.Optional[object] = None, de uuid, resource_type=resource_type, defaults=resource_dict ) _resource.save() + metadata_dict = infer_default_metadata(_resource.get_real_instance()) + # Store the resource creator as metadata Originator contact + if originator: + metadata_dict.setdefault("contacts", {}).setdefault( + "originator", + [{"id": str(originator.id), "label": originator.username}], + ) metadata_manager.update_schema_instance_partial( _resource, - infer_default_metadata(_resource.get_real_instance()), + metadata_dict, user=None, ) resourcebase_post_save(_resource.get_real_instance()) - self.finalize_creation_permissions(_resource, owner=resolved_owner, initial_user=initial_user) + self.finalize_creation_permissions(_resource, owner=resolved_owner) _resource.set_processing_state(enumerations.STATE_PROCESSED) except Exception as e: logger.exception(e) diff --git a/geonode/security/handlers.py b/geonode/security/handlers.py index 2610458147b..c281feefcc6 100644 --- a/geonode/security/handlers.py +++ b/geonode/security/handlers.py @@ -17,8 +17,11 @@ # ######################################################################### from abc import ABC +import logging from django.conf import settings -from geonode.security.permissions import _to_extended_perms, MANAGE_RIGHTS +from geonode.security.permissions import _to_extended_perms, VIEW_RIGHTS, DOWNLOAD_RIGHTS, EDIT_RIGHTS, MANAGE_RIGHTS + +logger = logging.getLogger(__name__) class BasePermissionsHandler(ABC): @@ -128,12 +131,13 @@ def fixup_perms(instance, perms_payload, include_virtual=True, *args, **kwargs): if not kwargs.get("created", False): return perms_payload - initial_user = kwargs.get("initial_user", None) - if not initial_user: + originators = getattr(instance, "originator", None) or [] + originator = originators[0] if originators else None + if not originator: return perms_payload - initial_username = initial_user if isinstance(initial_user, str) else getattr(initial_user, "username", None) - if not initial_username or initial_username == getattr(instance.owner, "username", None): + originator_username = originator if isinstance(originator, str) else getattr(originator, "username", None) + if not originator_username or originator_username == getattr(instance.owner, "username", None): return perms_payload _resource_type = getattr(instance, "resource_type", None) or instance.polymorphic_ctype.name @@ -143,7 +147,68 @@ def fixup_perms(instance, perms_payload, include_virtual=True, *args, **kwargs): payload = perms_payload or {} if "users" not in payload: payload["users"] = {} - payload["users"][initial_username] = sorted(manage_perms) + payload["users"][originator_username] = sorted(manage_perms) + return payload + + +class ResourceCreatorGroupsPermissionsHandler(BasePermissionsHandler): + """ + Auto-assign configured permissions to all groups of the resource creator on creation. + """ + + VALID_COMPACT_PERMISSIONS = {VIEW_RIGHTS, DOWNLOAD_RIGHTS, EDIT_RIGHTS, MANAGE_RIGHTS} + + @staticmethod + def _get_valid_resource_creator_groups_permission(raw_value): + default_value = VIEW_RIGHTS + normalized_value = str(raw_value or default_value).strip().lower() or default_value + + if normalized_value not in ResourceCreatorGroupsPermissionsHandler.VALID_COMPACT_PERMISSIONS: + logger.warning( + "RESOURCE_CREATOR_GROUPS_PERMISSIONS contains unsupported value '%s'. Defaulting to 'view'.", + normalized_value, + ) + return default_value + + return normalized_value + + @staticmethod + def fixup_perms(instance, perms_payload, include_virtual=True, *args, **kwargs): + from geonode.security.utils import get_user_groups + + if not kwargs.get("created", False): + return perms_payload + + if not getattr(settings, "AUTO_ASSIGN_RESOURCE_CREATOR_GROUPS_PERMISSIONS", False): + return perms_payload + + originators = getattr(instance, "originator", None) or [] + originator = originators[0] if originators else None + if not originator: + return perms_payload + + # Skip when uploader is a superuser to avoid granting permissions to all admin groups. + # Superusers can belong to every groups by default, which could assign this resource to every group. + if originator.is_superuser: + return perms_payload + + payload = perms_payload or {} + payload.setdefault("users", {}) + payload.setdefault("groups", {}) + + _resource_type = getattr(instance, "resource_type", None) or instance.polymorphic_ctype.name + _resource_subtype = (getattr(instance, "subtype", None) or "").lower() + + compact_permission = ResourceCreatorGroupsPermissionsHandler._get_valid_resource_creator_groups_permission( + getattr(settings, "RESOURCE_CREATOR_GROUPS_PERMISSIONS", VIEW_RIGHTS) + ) + extended_permissions = set(_to_extended_perms(compact_permission, _resource_type, _resource_subtype) or []) + + if not extended_permissions: + extended_permissions = set(_to_extended_perms("view", _resource_type, _resource_subtype) or []) + + for user_group in get_user_groups(originator): + payload["groups"][user_group] = sorted(extended_permissions) return payload diff --git a/geonode/security/tests.py b/geonode/security/tests.py index 3d1c3057af7..a3b7ea00063 100644 --- a/geonode/security/tests.py +++ b/geonode/security/tests.py @@ -49,7 +49,11 @@ from geonode.layers.models import Dataset from geonode.documents.models import Document from geonode.compat import ensure_string -from geonode.security.handlers import BasePermissionsHandler, GroupManagersPermissionsHandler +from geonode.security.handlers import ( + BasePermissionsHandler, + GroupManagersPermissionsHandler, + ResourceCreatorGroupsPermissionsHandler, +) from geonode.upload.models import ResourceHandlerInfo from geonode.utils import check_ogc_backend, build_absolute_uri from geonode.tests.utils import check_dataset @@ -60,6 +64,7 @@ from geonode.layers.populate_datasets_data import create_dataset_data from geonode.base.auth import create_auth_token, get_or_create_token from geonode.security.registry import permissions_registry +from geonode.metadata.manager import metadata_manager from geonode.base.models import Configuration, UserGeoLimit, GroupGeoLimit from geonode.base.populate_test_data import ( @@ -3085,6 +3090,66 @@ def test_group_managers_permissons_handler(self): # Still empty, since user had no base perms self.assertListEqual(updated_perms_empty["users"][self.group_manager], []) + @override_settings( + AUTO_ASSIGN_RESOURCE_CREATOR_GROUPS_PERMISSIONS=True, RESOURCE_CREATOR_GROUPS_PERMISSIONS="download" + ) + def test_resource_creator_groups_permissions_handler_on_create(self): + from geonode.security.permissions import _to_extended_perms + + owner = get_user_model().objects.create_user( + "creator_owner", "creator_owner@fakemail.com", "creator_owner_password", is_active=True + ) + group_profile_1 = GroupProfile.objects.create(title="creator group 1", slug="creator_group_1") + group_profile_2 = GroupProfile.objects.create(title="creator group 2", slug="creator_group_2") + GroupMember.objects.create(user=owner, group=group_profile_1, role=GroupMember.MEMBER) + GroupMember.objects.create(user=owner, group=group_profile_2, role=GroupMember.MEMBER) + + resource = create_single_dataset("creator_groups_dataset") + resource.owner = owner + resource.save() + + metadata_manager.update_schema_instance_partial( + resource, + {"contacts": {"originator": [{"id": str(owner.id), "label": owner.username}]}}, + user=None, + ) + + payload = {"users": {}, "groups": {}} + handler = ResourceCreatorGroupsPermissionsHandler() + updated_perms = handler.fixup_perms(resource, payload, created=True, include_virtual=False) + + resource_type = getattr(resource, "resource_type", None) or resource.polymorphic_ctype.name + resource_subtype = (getattr(resource, "subtype", None) or "").lower() + expected_perms = sorted(_to_extended_perms("download", resource_type, resource_subtype)) + + self.assertIn(group_profile_1.group, updated_perms["groups"]) + self.assertIn(group_profile_2.group, updated_perms["groups"]) + self.assertListEqual(sorted(updated_perms["groups"][group_profile_1.group]), expected_perms) + self.assertListEqual(sorted(updated_perms["groups"][group_profile_2.group]), expected_perms) + + @override_settings( + AUTO_ASSIGN_RESOURCE_CREATOR_GROUPS_PERMISSIONS=True, RESOURCE_CREATOR_GROUPS_PERMISSIONS="download" + ) + def test_resource_creator_groups_permissions_handler_skips_when_not_created(self): + owner = get_user_model().objects.create_user( + "creator_owner_no_create", + "creator_owner_no_create@fakemail.com", + "creator_owner_no_create_password", + is_active=True, + ) + group_profile = GroupProfile.objects.create(title="creator group no create", slug="creator_group_no_create") + GroupMember.objects.create(user=owner, group=group_profile, role=GroupMember.MEMBER) + + resource = create_single_dataset("creator_groups_dataset_no_create") + resource.owner = owner + resource.save() + + payload = {"users": {}, "groups": {}} + handler = ResourceCreatorGroupsPermissionsHandler() + updated_perms = handler.fixup_perms(resource, payload, created=False, include_virtual=False) + + self.assertDictEqual({"users": {}, "groups": {}}, updated_perms) + @override_settings( CACHES={ diff --git a/geonode/settings.py b/geonode/settings.py index 84a17ca31b7..14dc0b31107 100644 --- a/geonode/settings.py +++ b/geonode/settings.py @@ -1851,7 +1851,10 @@ def get_geonode_catalogue_service(): AUTO_ASSIGN_REGISTERED_MEMBERS_TO_CONTRIBUTORS = ast.literal_eval( os.getenv("AUTO_ASSIGN_REGISTERED_MEMBERS_TO_CONTRIBUTORS", "True") ) - +AUTO_ASSIGN_RESOURCE_CREATOR_GROUPS_PERMISSIONS = ast.literal_eval( + os.getenv("AUTO_ASSIGN_RESOURCE_CREATOR_GROUPS_PERMISSIONS", "False") +) +RESOURCE_CREATOR_GROUPS_PERMISSIONS = os.getenv("RESOURCE_CREATOR_GROUPS_PERMISSIONS", "view") AUTO_ASSIGN_RESOURCE_OWNERSHIP_TO_ADMIN = ast.literal_eval( os.getenv("AUTO_ASSIGN_RESOURCE_OWNERSHIP_TO_ADMIN", "False") ) @@ -1879,6 +1882,7 @@ def get_geonode_catalogue_service(): "geonode.security.handlers.GroupManagersPermissionsHandler", "geonode.security.handlers.SpecialGroupsPermissionsHandler", "geonode.security.handlers.AdvancedWorkflowPermissionsHandler", + "geonode.security.handlers.ResourceCreatorGroupsPermissionsHandler", "geonode.security.handlers.AutoAssignResourceOwnershipHandler", ]