Repository: ckan/ckan

Issue:
Refactor old _get_object logic
The authorization layer provides a _get_object method that can generate side effects if context objects are used in sequential action calls. (See #7348)

https://github.com/ckan/ckan/blob/e21141098f33c54e1e3fdf0a6d19922f5d54a3f7/ckan/logic/auth/__init__.py#L46-L66

This method is part of an old implementation of a "cache like" layer that could be modernized. Newer versions of sqlalchemy and databases provide their own cache logic when doing selects queries, therefore there would be no need to implement custom logic.

### Some ideas of implementation:

- `_get_object` should directly call to the database using the object id.
- There would be no need to store and pass the object in the context for "later reuse".


Discussion:
So a good implementation for this would be to remove the logic or retrieving the object from the context and directly call the database. I'm expecting this to cause some errors elsewhere since this "cache logic" has been miss used sometimes.

## To Do:
 - [ ] Remove the use of `context["name"]`
 - [ ] Check if this also fixes #7348 
 - [ ] Make an overall review of the code for places set `context[name]` before calling the authorization and clean up that line since it is no longer needed.
@shashigharti this is a necessary and good ticket to work if you have time!
As a suggestion for the last point @shashigharti I would do a quick round of manual testing with a print statement to spot where is this being used in the system:
```python
...
try: 
    x = context[name] 
    print(x)
    return x
except KeyError:
    ...
```

Resolving patch:
diff --git a/ckan/logic/auth/__init__.py b/ckan/logic/auth/__init__.py
--- a/ckan/logic/auth/__init__.py
+++ b/ckan/logic/auth/__init__.py
@@ -46,24 +46,19 @@ def _get_object(context: Context,
 def _get_object(context: Context,
                 data_dict: Optional[DataDict], name: str,
                 class_name: str) -> Any:
-    # return the named item if in the context, or get it from model.class_name
-    try:
-        return context[name]
-    except KeyError:
-        model = context['model']
-        if not data_dict:
-            data_dict = {}
-        id = data_dict.get('id', None)
-        if not id:
-            raise logic.ValidationError({
-                "message": 'Missing id, can not get {0} object'.format(
-                    class_name)})
-        obj = getattr(model, class_name).get(id)
-        if not obj:
-            raise logic.NotFound
-        # Save in case we need this again during the request
-        context[name] = obj
-        return obj
+    # return the named item from model.class_name
+    model = context['model']
+    if not data_dict:
+        data_dict = {}
+    id = data_dict.get('id', None)
+    if not id:
+        raise logic.ValidationError({
+            "message": 'Missing id, can not get {0} object'.format(
+                class_name)})
+    obj = getattr(model, class_name).get(id)
+    if not obj:
+        raise logic.NotFound
+    return obj
 
 
 def get_package_object(
diff --git a/ckan/logic/validators.py b/ckan/logic/validators.py
--- a/ckan/logic/validators.py
+++ b/ckan/logic/validators.py
@@ -500,7 +500,7 @@ def ignore_not_package_admin(key: FlattenKey, data: FlattenDataDict,
     pkg = context.get('package')
     if pkg:
         try:
-            logic.check_access('package_change_state',context)
+            logic.check_access('package_change_state',context, {"id": pkg.id})
             authorized = True
         except logic.NotAuthorized:
             authorized = False
@@ -540,7 +540,7 @@ def ignore_not_group_admin(key: FlattenKey, data: FlattenDataDict,
     group = context.get('group')
     if group:
         try:
-            logic.check_access('group_change_state',context)
+            logic.check_access('group_change_state',context, {"id": group.id})
             authorized = True
         except logic.NotAuthorized:
             authorized = False
diff --git a/ckan/views/dataset.py b/ckan/views/dataset.py
--- a/ckan/views/dataset.py
+++ b/ckan/views/dataset.py
@@ -805,7 +805,11 @@ def get(self,
         resources_json = h.json.dumps(data.get(u'resources', []))
         user = current_user.name
         try:
-            check_access(u'package_update', context)
+            check_access(
+                'package_update',
+                context,
+                {"id": pkg_dict.get('id')}
+            )
         except NotAuthorized:
             return base.abort(
                 403,


Test patch:
diff --git a/ckan/tests/logic/auth/test_init.py b/ckan/tests/logic/auth/test_init.py
--- a/ckan/tests/logic/auth/test_init.py
+++ b/ckan/tests/logic/auth/test_init.py
@@ -19,18 +19,6 @@ def _get_function(obj_type):
     return _get_object_functions[obj_type]
 
 
-def _get_object_in_context(obj_type):
-
-    if obj_type == "user":
-        context = {"user_obj": "a_fake_object"}
-    else:
-        context = {obj_type: "a_fake_object"}
-
-    obj = _get_function(obj_type)(context)
-
-    assert obj == "a_fake_object"
-
-
 def _get_object_id_not_found(obj_type):
 
     with pytest.raises(logic.NotFound):
@@ -43,22 +31,6 @@ def _get_object_id_none(obj_type):
         _get_function(obj_type)({"model": core_model}, {})
 
 
-def test_get_package_object_in_context():
-    _get_object_in_context("package")
-
-
-def test_get_resource_object_in_context():
-    _get_object_in_context("resource")
-
-
-def test_get_user_object_in_context():
-    _get_object_in_context("user")
-
-
-def test_get_group_object_in_context():
-    _get_object_in_context("group")
-
-
 def test_get_package_object_id_not_found():
     _get_object_id_not_found("package")
 
@@ -105,7 +77,6 @@ def test_get_package_object_with_id(self):
         obj = logic_auth.get_package_object(context, {"id": dataset["id"]})
 
         assert obj.id == dataset["id"]
-        assert context["package"] == obj
 
     def test_get_resource_object_with_id(self):
 
@@ -126,7 +97,6 @@ def test_get_resource_object_with_id(self):
         obj = logic_auth.get_resource_object(context, {"id": resource["id"]})
 
         assert obj.id == resource["id"]
-        assert context["resource"] == obj
 
     def test_get_user_object_with_id(self):
 
@@ -143,7 +113,6 @@ def test_get_user_object_with_id(self):
         obj = logic_auth.get_user_object(context, {"id": user["id"]})
 
         assert obj.id == user["id"]
-        assert context["user_obj"] == obj
 
     def test_get_group_object_with_id(self):
 
@@ -157,4 +126,3 @@ def test_get_group_object_with_id(self):
         obj = logic_auth.get_group_object(context, {"id": group["id"]})
 
         assert obj.id == group["id"]
-        assert context["group"] == obj
diff --git a/ckan/tests/logic/test_logic.py b/ckan/tests/logic/test_logic.py
--- a/ckan/tests/logic/test_logic.py
+++ b/ckan/tests/logic/test_logic.py
@@ -90,3 +90,26 @@ def test_fresh_context():
     assert "model" not in cleaned_context
     assert "session" not in cleaned_context
     assert "auth_user_obj" not in cleaned_context
+
+
+def test_check_access_auth_user_for_different_objects():
+    user1 = factories.User()
+    user2 = factories.User()
+    context = {"user": user1["name"]}
+
+    organization1 = factories.Organization(user=user1)
+    organization2 = factories.Organization(user=user2)
+
+    datasets1 = [
+        factories.Dataset(owner_org=organization1["id"], private=True)
+        for _ in range(0, 1)
+    ]
+    datasets2 = [
+        factories.Dataset(owner_org=organization2["id"], private=True)
+        for _ in range(0, 1)
+    ]
+    dataset3 = datasets1 + datasets2
+
+    with pytest.raises(logic.NotAuthorized):
+        for dataset in dataset3:
+            logic.check_access("package_show", context, {'id': dataset["id"]})