Repository: ckan/ckan

Issue:
Some patch actions raise `ckan.logic.NotAuthorized` even though `context['ignore_auth'] = True`
## CKAN version

2.9+

## Describe the bug

The patch action functions in [ckan/logic/action/patch.py](https://github.com/ckan/ckan/tree/master/ckan/logic/action/patch.py) create a separate `show_context: Context` object that is used with a show action to retrieve the resource that is being patched. For almost all of these patch functions, the `'ignore_auth'` value from the patch action's input `context: Context` argument is not propagated to the `show_context` object. As a result, patching some resource types with `'ignore_auth': True` in the patch action's `Context` unexpectedly fails with a `ckan.logic.NotAuthorized` error.

Only [`package_patch()`](https://github.com/ckan/ckan/blob/master/ckan/logic/action/patch.py#L14) correctly propagates this value. The other four patch action functions are affected:

* [`resource_patch()`](https://github.com/ckan/ckan/blob/master/ckan/logic/action/patch.py#L57)
* [`group_patch()`](https://github.com/ckan/ckan/blob/master/ckan/logic/action/patch.py#L88)
* [`organization_patch()`](https://github.com/ckan/ckan/blob/master/ckan/logic/action/patch.py#L122)
* [`user_patch()`](https://github.com/ckan/ckan/blob/master/ckan/logic/action/patch.py#L157)

## Example

The following code snippet uses the Plugin Toolkit to access the [`user_patch()` function](https://github.com/ckan/ckan/blob/master/ckan/logic/action/patch.py#L157). This will fail if `user` is not authorized to perform `'user_show'`, because `'ignore_auth'` [is not propagated to `show_context`](https://github.com/ckan/ckan/blob/master/ckan/logic/action/patch.py#L171) in `user_patch()`.

```python
toolkit.get_action('user_patch')(
    context={
        'user': user,
        'ignore_auth': True,
    },
)
```

A problem like this showed up while I was modifying some code in the `ckanext-ldap` plugin. I believe the reason is that at the time this is being called, a currently not-logged-in user is being passed, and such a user cannot perform `'user_show'`. Regardless, I would have expected that with `'ignore_auth'` being passed into the patch function, the action would succeed, or at least would not return an authorization error.

## Suggested fix

### Easy

The easiest thing to do is just add `'ignore_auth': context.get('ignore_auth', False)` to each of the `show_context` definitions that are missing them.

### Robust

A more robust fix would be to introduce a helper function, `_create_show_context()` (defined below), that each function can call to create the `show_context` object. That way, future changes to the `show_context` will be propagated to all of the patch functions.

It is worth noting that I have absolutely no clue what the `'for_update'` key does. I couldn't find any documentation about it. It seems to be used in the database interaction code, but I'm not really familiar with working with databases. In any case: this key is not set consistently in the `show_context` objects across the various patch functions, so in the code below, it is an optional parameter that can be passed into the new function.

```python
def _create_show_context(context: Context, for_update: bool = False) -> Context:
    '''Create a Context that can be used with a user_show action call.

    This method is internal. It is meant to be used by the patch action
    functions to generate a Context that can be used with a show action
    corresponding to the type of the patch action. The show action is
    used to retrieve the item that will be patched.

    The ``show_context`` is derived from the original patch Context,
    which is the ``context`` input argument. Certain values are propagated
    from the input ``context`` to the returned ``show_context``.

    :param context: Context from the original patch request
    :type context: Context

    :param for_update: if ``True``, then ``show_context['for_update'] = True``.
        If ``False`` (the default), then ``'for_update'`` will not be
        explicitly set in ``show_context``.
    :type for_update: bool

    :returns: A Context, ``show_context``, with the appropriate settings.
    '''
    show_context: Context = {
        'model': context['model'],
        'session': context['session'],
        'user': context['user'],
        'auth_user_obj': context['auth_user_obj'],
        'ignore_auth': context.get('ignore_auth', False),
    }

    if for_update:
        show_context['for_update'] = True

    return show_context
```


Discussion:
Good investigation, it should be a small fix. Better to use the existing `fresh_context` function than add another: 
https://github.com/ckan/ckan/blob/d61908bf4f2cf33e5fb306150b3c5b2893268f01/ckan/logic/__init__.py#L861-L874

`for_update` in the context makes `package_show` use `SELECT FOR UPDATE` on the database so that parallel updates won't cause data loss. This isn't maintained by `fresh_context` so it will need to be applied afterwards.

Resolving patch:
diff --git a/ckan/logic/action/patch.py b/ckan/logic/action/patch.py
--- a/ckan/logic/action/patch.py
+++ b/ckan/logic/action/patch.py
@@ -6,6 +6,7 @@
     get_action as _get_action,
     check_access as _check_access,
     get_or_bust as _get_or_bust,
+    fresh_context as _fresh_context
 )
 from ckan.types import Context, DataDict
 from ckan.types.logic import ActionResult
@@ -68,13 +69,8 @@ def resource_patch(context: Context,
     '''
     _check_access('resource_patch', context, data_dict)
 
-    show_context: Context = {
-        'model': context['model'],
-        'session': context['session'],
-        'user': context['user'],
-        'auth_user_obj': context['auth_user_obj'],
-        'for_update': True
-    }
+    show_context: Context = _fresh_context(context)
+    show_context.update({'for_update': True})
 
     resource_dict = _get_action('resource_show')(
         show_context,
@@ -99,12 +95,7 @@ def group_patch(context: Context,
     '''
     _check_access('group_patch', context, data_dict)
 
-    show_context: Context = {
-        'model': context['model'],
-        'session': context['session'],
-        'user': context['user'],
-        'auth_user_obj': context['auth_user_obj'],
-    }
+    show_context: Context = _fresh_context(context)
 
     group_dict = _get_action('group_show')(
         show_context,
@@ -134,12 +125,7 @@ def organization_patch(
     '''
     _check_access('organization_patch', context, data_dict)
 
-    show_context: Context = {
-        'model': context['model'],
-        'session': context['session'],
-        'user': context['user'],
-        'auth_user_obj': context['auth_user_obj'],
-    }
+    show_context: Context = _fresh_context(context)
 
     organization_dict = _get_action('organization_show')(
         show_context,
@@ -168,12 +154,7 @@ def user_patch(context: Context,
     '''
     _check_access('user_patch', context, data_dict)
 
-    show_context: Context = {
-        'model': context['model'],
-        'session': context['session'],
-        'user': context['user'],
-        'auth_user_obj': context['auth_user_obj'],
-    }
+    show_context: Context = _fresh_context(context)
 
     user_dict = _get_action('user_show')(
         show_context,


Test patch: