From 6e30e55ea51f8ece954dbe216cd0adfa35e3309a Mon Sep 17 00:00:00 2001
From: Calum Mackervoy <c.mackervoy@gmail.com>
Date: Thu, 2 Mar 2023 18:08:34 +0100
Subject: [PATCH] feature (permissions): owner_field supports nested fields

---
 djangoldp/models.py                       | 19 ++++++++++++++++---
 djangoldp/tests/models.py                 | 14 ++++++++++++++
 djangoldp/tests/tests_user_permissions.py | 21 ++++++++++++++++++++-
 docs/create_model.md                      |  2 +-
 4 files changed, 51 insertions(+), 5 deletions(-)

diff --git a/djangoldp/models.py b/djangoldp/models.py
index b9c8c3da..f5d3d1a7 100644
--- a/djangoldp/models.py
+++ b/djangoldp/models.py
@@ -326,9 +326,22 @@ class Model(models.Model):
         if owner_field is None:
             return False
 
-        return (getattr(obj, owner_field) == user
-                or (hasattr(user, 'urlid') and getattr(obj, owner_field) == user.urlid)
-                or getattr(obj, owner_field) == user.id)
+        # owner fields might be nested (e.g. "collection__author")
+        owner_field_nesting = owner_field.split("__")
+        if len(owner_field_nesting) > 1:
+            obj_copy = obj
+
+            for level in owner_field_nesting:
+                owner_value = getattr(obj_copy, level)
+                obj_copy = owner_value
+
+        # or they might not be (e.g. "author")
+        else:
+            owner_value = getattr(obj, owner_field)
+        
+        return (owner_value == user
+                or (hasattr(user, 'urlid') and owner_value == user.urlid)
+                or owner_value == user.id)
 
     @classmethod
     def is_external(cls, value):
diff --git a/djangoldp/tests/models.py b/djangoldp/tests/models.py
index b5acbf26..a5fe54eb 100644
--- a/djangoldp/tests/models.py
+++ b/djangoldp/tests/models.py
@@ -114,6 +114,20 @@ class OwnedResourceVariant(Model):
         depth = 1
 
 
+class OwnedResourceNestedOwnership(Model):
+    description = models.CharField(max_length=255, blank=True, null=True)
+    parent = models.ForeignKey(OwnedResource, blank=True, null=True, related_name="owned_resources",
+                               on_delete=models.CASCADE)
+
+    class Meta(Model.Meta):
+        anonymous_perms = []
+        authenticated_perms = []
+        owner_perms = ['view', 'delete', 'add', 'change', 'control']
+        owner_field = 'parent__user__urlid'
+        serializer_fields = ['@id', 'description', 'parent']
+        depth = 1
+
+
 class UserProfile(Model):
     description = models.CharField(max_length=255, blank=True, null=True)
     user = models.OneToOneField(settings.AUTH_USER_MODEL, related_name='userprofile', on_delete=models.CASCADE)
diff --git a/djangoldp/tests/tests_user_permissions.py b/djangoldp/tests/tests_user_permissions.py
index 78cef0f7..ed1b2c56 100644
--- a/djangoldp/tests/tests_user_permissions.py
+++ b/djangoldp/tests/tests_user_permissions.py
@@ -4,7 +4,7 @@ from django.conf import settings
 from django.test import override_settings
 from rest_framework.test import APIClient, APITestCase
 from djangoldp.tests.models import JobOffer, LDPDummy, PermissionlessDummy, UserProfile, OwnedResource, \
-    NoSuperUsersAllowedModel, ComplexPermissionClassesModel
+    NoSuperUsersAllowedModel, ComplexPermissionClassesModel, OwnedResourceNestedOwnership
 
 import json
 
@@ -229,6 +229,25 @@ class TestUserPermissions(APITestCase):
         self.assertEqual(response.status_code, 200)
         self.assertEqual(len(response.data['ldp:contains']), 1)
         self.assertEqual(response.data['ldp:contains'][0]['@id'], my_resource.urlid)
+    
+    # a repeat of the previous test but using a model where the owner_field is nested
+    def test_list_owned_resources_nested(self):
+        my_resource = OwnedResource.objects.create(description='test', user=self.user)
+        my_second_resource = OwnedResource.objects.create(description='test', user=self.user)
+        another_user = get_user_model().objects.create_user(username='test', email='test@test.com', password='test')
+        their_resource = OwnedResource.objects.create(description='another test', user=another_user)
+
+        my_nested = OwnedResourceNestedOwnership.objects.create(description="test", parent=my_resource)
+        my_second_nested = OwnedResourceNestedOwnership.objects.create(description="test", parent=my_second_resource)
+        their_nested = OwnedResourceNestedOwnership.objects.create(description="test", parent=their_resource)
+
+        response = self.client.get('/ownedresourcenestedownerships/')
+        self.assertEqual(response.status_code, 200)
+        self.assertEqual(len(response.data['ldp:contains']), 2)
+        ids = [r['@id'] for r in response.data['ldp:contains']]
+        self.assertIn(my_nested.urlid, ids)
+        self.assertIn(my_second_nested.urlid, ids)
+        self.assertNotIn(their_nested.urlid, ids)
 
     # I do not have model permissions as an authenticated user, but I am the resources' owner
     def test_get_owned_resource(self):
diff --git a/docs/create_model.md b/docs/create_model.md
index 08035c77..7c01d381 100644
--- a/docs/create_model.md
+++ b/docs/create_model.md
@@ -354,7 +354,7 @@ class Todo(Model):
         authenticated_perms = ['inherit', 'add'] # inherits from anonymous
         owner_perms = ['inherit', 'change', 'control', 'delete'] # inherits from authenticated
         superuser_perms = ['inherit'] # inherits from owner
-        owner_field = 'user'
+        owner_field = 'user' # can be nested, e.g. user__parent
 ```
 
 
-- 
GitLab