Repository: ckan/ckan

Issue:
Unable to add resource as a collaborator
**CKAN version**
2.9

**Describe the bug**
If a user is a collaborator on a dataset they cannot add resources to it

**Steps to reproduce**
As user A:
  Create an organization owned dataset as user A
  Add User B as a collaborator with editor access
As user B:
  Try to add a resource to the dataset

**Expected behavior**
User B should be able to add / upload a resource to the dataset

**Observed behavior**
The action fails with message:
Owner org: You cannot add a dataset to this organization

**Additional details**
I dug into the error a bit and it appears to come from the owner_org_validator function. Looking at it, there is logic for collaborators in the function, but it only covers changing organizations. I also wonder if there is a reason much of this logic duplicates the package auth functions, and if by doing so it encumbers extensions that might desire to override auth functionality. 





Discussion:

Resolving patch:
diff --git a/ckan/logic/validators.py b/ckan/logic/validators.py
--- a/ckan/logic/validators.py
+++ b/ckan/logic/validators.py
@@ -44,6 +44,8 @@ def owner_org_validator(key, data, errors, context):
     model = context['model']
     user = context['user']
     user = model.User.get(user)
+    package = context.get('package')
+
     if value == '':
         if not authz.check_config_permission('create_unowned_dataset'):
             raise Invalid(_('An organization must be provided'))
@@ -52,7 +54,6 @@ def owner_org_validator(key, data, errors, context):
     if (authz.check_config_permission('allow_dataset_collaborators')
             and not authz.check_config_permission('allow_collaborators_to_change_owner_org')):
 
-        package = context.get('package')
         if package and user and not user.sysadmin:
             is_collaborator = authz.user_is_collaborator_on_dataset(
                 user.id, package.id, ['admin', 'editor'])
@@ -70,10 +71,14 @@ def owner_org_validator(key, data, errors, context):
     if not group:
         raise Invalid(_('Organization does not exist'))
     group_id = group.id
-    if not context.get(u'ignore_auth', False) and not(user.sysadmin or
-           authz.has_user_permission_for_group_or_org(
-               group_id, user.name, 'create_dataset')):
-        raise Invalid(_('You cannot add a dataset to this organization'))
+
+    if not package or (package and package.owner_org != group_id):
+        # This is a new dataset or we are changing the organization
+        if not context.get(u'ignore_auth', False) and not(user.sysadmin or
+               authz.has_user_permission_for_group_or_org(
+                   group_id, user.name, 'create_dataset')):
+            raise Invalid(_('You cannot add a dataset to this organization'))
+
     data[key] = group_id
 
 


Test patch:
diff --git a/ckan/tests/logic/action/test_create.py b/ckan/tests/logic/action/test_create.py
--- a/ckan/tests/logic/action/test_create.py
+++ b/ckan/tests/logic/action/test_create.py
@@ -588,6 +588,35 @@ def test_metadata_modified_is_set_to_utcnow_when_created(self):
         assert (result['metadata_modified'] ==
                 datetime.datetime.utcnow().isoformat())
 
+    @pytest.mark.ckan_config('ckan.auth.allow_dataset_collaborators', True)
+    @pytest.mark.ckan_config('ckan.auth.allow_admin_collaborators', True)
+    @pytest.mark.parametrize('role', ['admin', 'editor'])
+    def test_collaborators_can_create_resources(self, role):
+
+        org1 = factories.Organization()
+        dataset = factories.Dataset(owner_org=org1['id'])
+
+        user = factories.User()
+
+        helpers.call_action(
+            'package_collaborator_create',
+            id=dataset['id'], user_id=user['id'], capacity=role)
+
+        context = {
+            'user': user['name'],
+            'ignore_auth': False,
+
+        }
+
+        created_resource = helpers.call_action(
+            'resource_create',
+            context=context,
+            package_id=dataset['id'],
+            name='created by collaborator',
+            url='https://example.com')
+
+        assert created_resource['name'] == 'created by collaborator'
+
 
 @pytest.mark.usefixtures("clean_db", "with_request_context")
 class TestMemberCreate(object):
diff --git a/ckan/tests/logic/action/test_delete.py b/ckan/tests/logic/action/test_delete.py
--- a/ckan/tests/logic/action/test_delete.py
+++ b/ckan/tests/logic/action/test_delete.py
@@ -35,6 +35,32 @@ def test_resource_delete(self):
         res_obj = model.Resource.get(resource["id"])
         assert res_obj.state == "deleted"
 
+    @pytest.mark.ckan_config('ckan.auth.allow_dataset_collaborators', True)
+    @pytest.mark.ckan_config('ckan.auth.allow_admin_collaborators', True)
+    @pytest.mark.parametrize('role', ['admin', 'editor'])
+    def test_collaborators_can_delete_resources(self, role):
+
+        org1 = factories.Organization()
+        dataset = factories.Dataset(owner_org=org1['id'])
+        resource = factories.Resource(package_id=dataset['id'])
+
+        user = factories.User()
+
+        helpers.call_action(
+            'package_collaborator_create',
+            id=dataset['id'], user_id=user['id'], capacity=role)
+
+        context = {
+            'user': user['name'],
+            'ignore_auth': False,
+
+        }
+
+        created_resource = helpers.call_action(
+            'resource_delete',
+            context=context,
+            id=resource['id'])
+
 
 @pytest.mark.ckan_config("ckan.plugins", "image_view")
 @pytest.mark.usefixtures("clean_db", "with_plugins", "with_request_context")
diff --git a/ckan/tests/logic/action/test_update.py b/ckan/tests/logic/action/test_update.py
--- a/ckan/tests/logic/action/test_update.py
+++ b/ckan/tests/logic/action/test_update.py
@@ -1694,6 +1694,34 @@ def test_mark_as_old_some_activities_by_a_followed_user(self):
 @pytest.mark.ckan_config('ckan.auth.allow_dataset_collaborators', True)
 class TestCollaboratorsUpdate(object):
 
+    @pytest.mark.ckan_config('ckan.auth.allow_admin_collaborators', True)
+    @pytest.mark.parametrize('role', ['admin', 'editor'])
+    def test_collaborators_can_update_resources(self, role):
+
+        org1 = factories.Organization()
+        dataset = factories.Dataset(owner_org=org1['id'])
+        resource = factories.Resource(package_id=dataset['id'])
+
+        user = factories.User()
+
+        helpers.call_action(
+            'package_collaborator_create',
+            id=dataset['id'], user_id=user['id'], capacity=role)
+
+        context = {
+            'user': user['name'],
+            'ignore_auth': False,
+
+        }
+
+        updated_resource = helpers.call_action(
+            'resource_update',
+            context=context,
+            id=resource['id'],
+            description='updated')
+
+        assert updated_resource['description'] == 'updated'
+
     def test_collaborators_can_not_change_owner_org_by_default(self):
 
         org1 = factories.Organization()