Skip to content

Commit f25e550

Browse files
authored
[Fixes #11995] Implement the DELETE method for the User API (#12028)
* [Fixes #11995] Implement the DELETE method for the User API * [Fixes #11995] Implement the DELETE method for the User API refactor and docstrings added
1 parent a411232 commit f25e550

File tree

5 files changed

+211
-5
lines changed

5 files changed

+211
-5
lines changed

geonode/base/api/tests.py

+10-4
Original file line numberDiff line numberDiff line change
@@ -464,15 +464,21 @@ def test_delete_user_profile(self):
464464
# Bob can't delete user
465465
self.assertTrue(self.client.login(username="bobby", password="bob"))
466466
response = self.client.delete(url, format="json")
467-
self.assertEqual(response.status_code, 405)
468-
# User can not delete self profile
467+
self.assertEqual(response.status_code, 403)
468+
# User can delete self profile
469469
self.assertTrue(self.client.login(username="user_test_delete", password="user"))
470470
response = self.client.delete(url, format="json")
471-
self.assertEqual(response.status_code, 405)
471+
self.assertEqual(response.status_code, 200)
472+
self.assertEqual(get_user_model().objects.filter(username="user_test_delete").first(), None)
473+
# recreate user that was deleted
474+
user = get_user_model().objects.create_user(
475+
username="user_test_delete", email="[email protected]", password="user"
476+
)
477+
url = reverse("users-detail", kwargs={"pk": user.pk})
472478
# Admin can delete user
473479
self.assertTrue(self.client.login(username="admin", password="admin"))
474480
response = self.client.delete(url, format="json")
475-
self.assertEqual(response.status_code, 405)
481+
self.assertEqual(response.status_code, 200)
476482
finally:
477483
user.delete()
478484

geonode/people/tests.py

+125
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
import django
2020
from django.test.utils import override_settings
2121
from mock import MagicMock, PropertyMock, patch
22+
from geonode.base.models import ResourceBase
23+
from geonode.groups.models import GroupMember, GroupProfile
2224
from geonode.tests.base import GeoNodeBaseTestSupport
2325

2426
from django.core import mail
@@ -58,6 +60,7 @@ def setUp(self):
5860
self.permission_type = ("view", "download", "edit")
5961
self.groups = Group.objects.all()[:3]
6062
self.group_ids = ",".join(str(element.pk) for element in self.groups)
63+
self.bar = GroupProfile.objects.get(slug="bar")
6164

6265
def test_redirect_on_get_request(self):
6366
"""
@@ -786,3 +789,125 @@ def test_users_api_patch_username(self):
786789
# username cannot be updated
787790
self.assertEqual(response.status_code, 400)
788791
self.assertTrue("username cannot be updated" in response.json()["errors"])
792+
793+
@override_settings(
794+
USER_DELETION_RULES=["geonode.people.utils.user_has_resources", "geonode.people.utils.user_is_manager"]
795+
)
796+
def test_valid_delete(self):
797+
# create a new user
798+
tim = get_user_model().objects.create(username="tim")
799+
800+
admin = get_user_model().objects.get(username="admin")
801+
802+
self.assertTrue(self.client.login(username="admin", password="admin"))
803+
804+
# admin wants to delete tim
805+
# Admin is superuser or staff
806+
self.assertTrue(admin.is_superuser or admin.is_staff)
807+
# check that tim is not manager
808+
# nor has any resources
809+
self.assertFalse(ResourceBase.objects.filter(owner_id=tim.pk).exists())
810+
self.assertFalse(GroupMember.objects.filter(user_id=tim.pk, role="manager").exists())
811+
812+
url = f"{reverse('users-list')}/{tim.pk}"
813+
response = self.client.delete(url, content_type="application/json")
814+
815+
# admin is permitted to delete
816+
self.assertEqual(response.status_code, 200)
817+
# tim has been deleted
818+
self.assertEqual(get_user_model().objects.filter(username="tim").first(), None)
819+
820+
@override_settings(USER_DELETION_RULES=[])
821+
@patch("geonode.people.utils.user_deletion_modules", [])
822+
def test_delete_without_validators(self):
823+
824+
norman = get_user_model().objects.get(username="norman")
825+
admin = get_user_model().objects.get(username="admin")
826+
827+
self.assertTrue(self.client.login(username="admin", password="admin"))
828+
829+
# admin wants to delete norman but norman is already promoted
830+
# Admin is superuser or staff
831+
self.assertTrue(admin.is_superuser or admin.is_staff)
832+
833+
# Make sure norman is not a member
834+
self.assertFalse(self.bar.user_is_member(norman))
835+
836+
# Add norman to the self.bar group
837+
self.bar.join(norman)
838+
839+
# Ensure norman is now a member
840+
self.assertTrue(self.bar.user_is_member(norman))
841+
842+
# promote norman to a manager
843+
self.bar.promote(norman)
844+
# Ensure norman is in the managers queryset
845+
self.assertTrue(norman in self.bar.get_managers())
846+
847+
url = f"{reverse('users-list')}/{norman.pk}"
848+
response = self.client.delete(url, content_type="application/json")
849+
850+
# norman can be deleted because validator rules are not applied
851+
self.assertEqual(response.status_code, 200)
852+
self.assertEqual(get_user_model().objects.filter(username="norman").first(), None)
853+
854+
@override_settings(
855+
USER_DELETION_RULES=["geonode.people.utils.user_has_resources", "geonode.people.utils.user_is_manager"]
856+
)
857+
def test_delete_a_manger(self):
858+
norman = get_user_model().objects.get(username="norman")
859+
admin = get_user_model().objects.get(username="admin")
860+
861+
self.assertTrue(self.client.login(username="admin", password="admin"))
862+
863+
# admin wants to delete norman but norman is already promoted
864+
# Admin is superuser or staff
865+
self.assertTrue(admin.is_superuser or admin.is_staff)
866+
867+
# Make sure norman is not a member
868+
self.assertFalse(self.bar.user_is_member(norman))
869+
870+
# Add norman to the self.bar group
871+
self.bar.join(norman)
872+
873+
# Ensure norman is now a member
874+
self.assertTrue(self.bar.user_is_member(norman))
875+
876+
# promote norman to a manager
877+
self.bar.promote(norman)
878+
# Ensure norman is in the managers queryset
879+
self.assertTrue(norman in self.bar.get_managers())
880+
881+
url = f"{reverse('users-list')}/{norman.pk}"
882+
response = self.client.delete(url, content_type="application/json")
883+
884+
# norman cant be deleted
885+
self.assertEqual(response.status_code, 403)
886+
self.assertNotEqual(get_user_model().objects.filter(username="norman").first(), None)
887+
#
888+
self.assertTrue("user_is_manager" in response.json()["errors"][0])
889+
890+
@override_settings(USER_DELETION_RULES=["geonode.people.utils.user_has_resources"])
891+
def test_delete_a_user_with_resource(self):
892+
# create a new user
893+
bobby = get_user_model().objects.get(username="bobby")
894+
admin = get_user_model().objects.get(username="admin")
895+
896+
self.assertTrue(self.client.login(username="admin", password="admin"))
897+
898+
# admin wants to delete bobby
899+
# Admin is superuser or staff
900+
self.assertTrue(admin.is_superuser or admin.is_staff)
901+
# check that bobby is not manager
902+
# but he has resources already assigned
903+
self.assertTrue(ResourceBase.objects.filter(owner_id=bobby.pk).exists())
904+
self.assertFalse(GroupMember.objects.filter(user_id=bobby.pk, role="manager").exists())
905+
906+
url = f"{reverse('users-list')}/{bobby.pk}"
907+
response = self.client.delete(url, content_type="application/json")
908+
909+
# admin is permitted to delete
910+
self.assertEqual(response.status_code, 403)
911+
# bobby cant be deleted
912+
self.assertNotEqual(get_user_model().objects.filter(username="bobby").first(), None)
913+
self.assertTrue("user_has_resources" in response.json()["errors"][0])

geonode/people/utils.py

+56
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,14 @@
2222
from django.contrib.auth.models import Group
2323

2424
from geonode import GeoNodeException
25+
from geonode.base.models import ResourceBase
2526
from geonode.groups.models import GroupProfile, GroupMember
2627
from geonode.groups.conf import settings as groups_settings
28+
from rest_framework.exceptions import PermissionDenied
29+
from django.conf import settings
30+
from django.utils.module_loading import import_string
31+
32+
# from geonode.people.models import Profile
2733

2834

2935
def get_default_user():
@@ -140,3 +146,53 @@ def get_available_users(user):
140146
member_ids.extend(users_ids)
141147

142148
return get_user_model().objects.filter(id__in=member_ids)
149+
150+
151+
def user_has_resources(profile) -> bool:
152+
"""
153+
checks if user has any resource in ownership
154+
155+
Args:
156+
profile (Profile) : accepts a userprofile instance.
157+
158+
Returns:
159+
bool: profile is the owner of any resources
160+
"""
161+
return ResourceBase.objects.filter(owner_id=profile.pk).exists()
162+
163+
164+
def user_is_manager(profile) -> bool:
165+
"""
166+
Checks if user is the manager of any group
167+
168+
Args:
169+
profile (Profile) : accepts a userprofile instance.
170+
171+
Returns:
172+
bool: profile is mangager or not
173+
174+
"""
175+
return GroupMember.objects.filter(user_id=profile.pk, role=GroupMember.MANAGER).exists()
176+
177+
178+
def call_user_deletion_rules(profile) -> None:
179+
"""
180+
calls a set of defined rules specific to the deletion of a user
181+
which are read from settings.USER_DELETION_RULES
182+
new rules can be added as long as they take as parameter the userprofile
183+
and return a boolean
184+
Args:
185+
profile (Profile) : accepts a userprofile instance.
186+
187+
Returns:
188+
None : Raises PermissionDenied Exception in case any of the deletion rule Fail
189+
"""
190+
if not globals().get("user_deletion_modules"):
191+
rule_path = settings.USER_DELETION_RULES if hasattr(settings, "USER_DELETION_RULES") else []
192+
globals()["user_deletion_modules"] = [import_string(deletion_rule) for deletion_rule in rule_path]
193+
error_list = []
194+
for not_deletable in globals().get("user_deletion_modules", []):
195+
if not_deletable(profile):
196+
error_list.append(not_deletable.__name__)
197+
if error_list:
198+
raise PermissionDenied(f"Deletion rule Violated: {', '.join(error_list)}")

geonode/people/views.py

+11-1
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
from geonode.security.utils import get_visible_resources
5555
from guardian.shortcuts import get_objects_for_user
5656
from rest_framework.exceptions import PermissionDenied
57+
from geonode.people.utils import call_user_deletion_rules
5758

5859

5960
class SetUserLayerPermission(View):
@@ -166,7 +167,7 @@ class UserViewSet(DynamicModelViewSet):
166167
API endpoint that allows users to be viewed or edited.
167168
"""
168169

169-
http_method_names = ["get", "post", "patch"]
170+
http_method_names = ["get", "post", "patch", "delete"]
170171
authentication_classes = [SessionAuthentication, BasicAuthentication, OAuth2Authentication]
171172
permission_classes = [
172173
IsAuthenticated,
@@ -204,6 +205,15 @@ def update(self, request, *args, **kwargs):
204205
serializer.save()
205206
return Response(serializer.data)
206207

208+
def destroy(self, request, *args, **kwargs):
209+
instance = self.get_object()
210+
self.perform_destroy(instance)
211+
return Response("User deleted sucessfully", status=200)
212+
213+
def perform_destroy(self, instance):
214+
call_user_deletion_rules(instance)
215+
instance.delete()
216+
207217
@extend_schema(
208218
methods=["get"],
209219
responses={200: ResourceBaseSerializer(many=True)},

geonode/settings.py

+9
Original file line numberDiff line numberDiff line change
@@ -2220,6 +2220,15 @@ def get_geonode_catalogue_service():
22202220
]
22212221

22222222

2223+
"""
2224+
List of modules that implement the deletion rules for a user
2225+
"""
2226+
USER_DELETION_RULES = [
2227+
"geonode.people.utils.user_has_resources"
2228+
# ,"geonode.people.utils.user_is_manager"
2229+
]
2230+
2231+
22232232
"""
22242233
Define the URLs patterns used by the SizeRestrictedFileUploadHandler
22252234
to evaluate if the file is greater than the limit size defined

0 commit comments

Comments
 (0)