Repository: ckan/ckan

Issue:
Can't change group type with organization/group update/patch operations
## CKAN version

CKAN 2.8, CKAN 2.9

## Describe the bug

I recently wanted to change the type of an organisation on a CKAN instance via the API. I made an API call like this:

```
curl -X POST http://my-ckan-server.com/api/3/action/organization_patch  -H "Authorization: <my-api-key>" -H "Content-Type: application/json" -H "Accept: application/json" --data '{"id": "<org-id>", "type": "new-type"}'
```

This returned a response:

```
200 OK

{
  "success": true,
  "result": {
    ...
    "type": "old-type",
  }
}
```

and did not update the type field.

This happens because the `type` field in the request body is ignored here:

https://github.com/ckan/ckan/blob/170556a97f38d42b94474943ccab4cef19fd9d49/ckan/logic/action/update.py#L645

It would be useful if it were possible to change the group type using the API for appropriately authorized users.
If it is impossible to do this by design, it would be much clearer what is going on if this request failed with a response like:

```
400 Bad Request

{
  "success": false,
  "error": {
    "message": "type can't be updated via the API",
  }
}
```

instead of reporting success but not making the change.

## Steps to reproduce
Steps to reproduce the behavior:

```
curl -X POST http://my-ckan-server.com/api/3/action/organization_patch  -H "Authorization: <my-api-key>" -H "Content-Type: application/json" -H "Accept: application/json" --data '{"id": "<org-id>", "type": "new-type"}'
```

## Expected behavior

Either the object should be updated, or a useful error response should be returned



Discussion:
Summary from our discussion in the dev meeting:

* If not allowed and the field value is always overridden the API call should fail with an error as suggested
* We could allow type changes on orgs/groups and datasets but:
  * we need to ensure that the schema for the new type is used for validation
  * it's up to the user to provide a data_dict that passes validation according to the new schema
* We need to check the effect of a `organization` -> `group` change

> Summary from our discussion in the dev meeting:
> 
>     * If not allowed and the field value is always overridden the API call should fail with an error as suggested
> 
>     * We could allow type changes on orgs/groups and datasets but:
>       
>       * we need to ensure that the schema for the new type is used for validation
>       * it's up to the user to provide a data_dict that passes validation according to the new schema
> 
>     * We need to check the effect of a `organization` -> `group` change

Hi @amercader , I agree with your first suggestion. I've create a error response when the `type` is different by replacing 
`data_dict['type'] = group.type` with:
```
if data_dict['type'] != group.type:
        raise ValidationError({"message": "type cannot be updated via API"})
``` 
The response is as below when the request is made for the type change:
```
{
    "help": "http://127.0.0.1:5000/api/3/action/help_show?name=organization_patch",
    "error": {
        "message": "type cannot be updated via API",
        "__type": "Validation Error"
    },
    "success": false
}
```
But getting the following errors when running the test cases:
```
def _group_or_org_update(
            context: Context, data_dict: DataDict, is_org: bool = False):
        model = context['model']
        session = context['session']
        id = _get_or_bust(data_dict, 'id')
    
        group = model.Group.get(id)
        if group is None:
            raise NotFound('Group was not found.')
        context["group"] = group
        print(group.type)
        # print(data_dict['type'])
        # data_dict['type'] = group.type
        # print(group.type)
        if data_dict['type'] != group.type:
>           raise ValidationError({"message": "type cannot be updated via API"})
E           ckan.logic.ValidationError: None - {'message': 'type cannot be updated via API'}

ckan/logic/action/update.py:663: ValidationError
----------------------------- Captured stdout call -----------------------------
2022-07-15 14:09:46,992 CRITI [ckan.lib.uploader] Please specify a ckan.storage_path in your config
                        for your uploads
2022-07-15 14:09:47,046 CRITI [ckan.lib.uploader] Please specify a ckan.storage_path in your config
                        for your uploads
organization
------------------------------ Captured log call -------------------------------
CRITICAL ckan.lib.uploader:uploader.py:85 Please specify a ckan.storage_path in your config
                        for your uploads
CRITICAL ckan.lib.uploader:uploader.py:85 Please specify a ckan.storage_path in your config
                        for your uploads
______________ TestGroupUpdate.test_group_update_cant_change_type ______________

self = <ckan.tests.logic.action.test_update.TestGroupUpdate object at 0x7f829b11e6a0>

    def test_group_update_cant_change_type(self):
        user = factories.User()
        context = {"user": user["name"]}
        group = factories.Group(type="group", user=user)
    
>       group = helpers.call_action(
            "group_update",
            context=context,
            name=group["name"],
            id=group["id"],
            type="favouritecolour",
        )

ckan/tests/logic/action/test_update.py:1475: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
ckan/tests/helpers.py:128: in call_action
    return logic.get_action(action_name)(context, kwargs)
ckan/logic/__init__.py:527: in wrapped
    result = _action(context, data_dict, **kw)
ckan/logic/action/update.py:753: in group_update
    return _group_or_org_update(context, data_dict)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

context = {'__auth_audit': [('group_update', 140198910762576)], 'group': <Group group-wefg-7245-ecux>, 'ignore_auth': True, 'model': <module 'ckan.model' from '/usr/lib/ckan/master/src/ckan/ckan/model/__init__.py'>, ...}
data_dict = {'id': 'e8b83f50-38c0-4c6a-8758-2328b12ea669', 'name': 'group-wefg-7245-ecux', 'type': 'favouritecolour'}
is_org = False

    def _group_or_org_update(
            context: Context, data_dict: DataDict, is_org: bool = False):
        model = context['model']
        session = context['session']
        id = _get_or_bust(data_dict, 'id')
    
        group = model.Group.get(id)
        if group is None:
            raise NotFound('Group was not found.')
        context["group"] = group
        print(group.type)
        # print(data_dict['type'])
        # data_dict['type'] = group.type
        # print(group.type)
        if data_dict['type'] != group.type:
>           raise ValidationError({"message": "type cannot be updated via API"})
E           ckan.logic.ValidationError: None - {'message': 'type cannot be updated via API'}

ckan/logic/action/update.py:663: ValidationError


```
Please let me know if my approach is correct or I should take a different approach for this problem:
Thanks.

Resolving patch:
diff --git a/ckan/logic/action/update.py b/ckan/logic/action/update.py
--- a/ckan/logic/action/update.py
+++ b/ckan/logic/action/update.py
@@ -652,7 +652,12 @@ def _group_or_org_update(
         raise NotFound('Group was not found.')
     context["group"] = group
 
-    data_dict['type'] = group.type
+    data_dict_type = data_dict.get('type') 
+    if data_dict_type is None:
+        data_dict['type'] = group.type
+    else:
+        if data_dict_type != group.type:
+            raise ValidationError({"message": "Type cannot be updated"})
 
     # get the schema
     group_plugin = lib_plugins.lookup_group_plugin(group.type)


Test patch:
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
@@ -379,19 +379,14 @@ def test_update_organization_cant_change_type(self):
         context = {"user": user["name"]}
         org = factories.Organization(type="organization", user=user)
 
-        org = helpers.call_action(
-            "organization_update",
-            context=context,
-            id=org["id"],
-            name=org["name"],
-            type="ragtagband",
-        )
-
-        assert org["type"] == "organization"
-        assert (
-            helpers.call_action("organization_show", id=org["name"])["type"]
-            == "organization"
-        )
+        with pytest.raises(logic.ValidationError):
+            helpers.call_action(
+                "organization_update",
+                context=context,
+                id=org["id"],
+                name=org["name"],
+                type="ragtagband",
+            )
 
 
 @pytest.mark.usefixtures("non_clean_db")
@@ -1472,19 +1467,14 @@ def test_group_update_cant_change_type(self):
         context = {"user": user["name"]}
         group = factories.Group(type="group", user=user)
 
-        group = helpers.call_action(
-            "group_update",
-            context=context,
-            name=group["name"],
-            id=group["id"],
-            type="favouritecolour",
-        )
-
-        assert group["type"] == "group"
-        assert (
-            helpers.call_action("group_show", id=group["name"])["type"]
-            == "group"
-        )
+        with pytest.raises(logic.ValidationError):
+            helpers.call_action(
+                "group_update",
+                context=context,
+                name=group["name"],
+                id=group["id"],
+                type="favouritecolour",
+            )
 
 
 @pytest.mark.usefixtures("non_clean_db")