Skip to content

Commit b166e62

Browse files
committed
moderation: add self-action prevention
* The problem is that an admin could block his own account. With this change it is possible to prevent the admin from doing that. * Prevent self-action for: block, deactivate, restore, activate and approve. * Update tests for self-action prevention * introduce PreventSelf generator * introduce _check_manage_permissions in users service * add "PreventSelf" into "can_manage" permission
1 parent 1943393 commit b166e62

File tree

4 files changed

+51
-12
lines changed

4 files changed

+51
-12
lines changed

invenio_users_resources/services/generators.py

+13
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
# Copyright (C) 2022 TU Wien.
44
# Copyright (C) 2022 CERN.
55
# Copyright (C) 2023 Graz University of Technology.
6+
# Copyright (C) 2024 KTH Royal Institute of Technology.
67
#
78
# Invenio-Users-Resources is free software; you can redistribute it and/or
89
# modify it under the terms of the MIT License; see LICENSE file for more
@@ -94,6 +95,18 @@ def query_filter(self, identity=None, **kwargs):
9495
return []
9596

9697

98+
class PreventSelf(Generator):
99+
"""Prevents users from performing actions on themselves."""
100+
101+
def excludes(self, record=None, identity_id=None, **kwargs):
102+
"""Preventing Needs."""
103+
if record is not None and identity_id is not None:
104+
is_self_action = identity_id == str(record.id)
105+
if is_self_action:
106+
return [UserNeed(record.id)]
107+
return []
108+
109+
97110
class IfGroupNotManaged(ConditionalGenerator):
98111
"""Generator for managed group access."""
99112

invenio_users_resources/services/permissions.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# -*- coding: utf-8 -*-
22
#
33
# Copyright (C) 2022 TU Wien.
4+
# Copyright (C) 2024 KTH Royal Institute of Technology.
45
#
56
# Invenio-Users-Resources is free software; you can redistribute it and/or
67
# modify it under the terms of the MIT License; see LICENSE file for more
@@ -23,6 +24,7 @@
2324
IfGroupNotManaged,
2425
IfPublicEmail,
2526
IfPublicUser,
27+
PreventSelf,
2628
Self,
2729
)
2830

@@ -51,10 +53,10 @@ class UsersPermissionPolicy(BasePermissionPolicy):
5153
can_read_all = [UserManager, SystemProcess()]
5254

5355
# Moderation permissions
54-
can_manage = [UserManager, SystemProcess()]
56+
can_manage = [UserManager, PreventSelf(), SystemProcess()]
5557
can_search_all = [UserManager, SystemProcess()]
5658
can_read_system_details = [UserManager, SystemProcess()]
57-
can_impersonate = [UserManager, SystemProcess()]
59+
can_impersonate = [UserManager, PreventSelf(), SystemProcess()]
5860

5961

6062
class GroupsPermissionPolicy(BasePermissionPolicy):

invenio_users_resources/services/users/service.py

+16-10
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# -*- coding: utf-8 -*-
22
#
3-
# Copyright (C) 2022 KTH Royal Institute of Technology
3+
# Copyright (C) 2022-2024 KTH Royal Institute of Technology.
44
# Copyright (C) 2022 TU Wien.
55
# Copyright (C) 2022 European Union.
66
# Copyright (C) 2022 CERN.
@@ -129,15 +129,21 @@ def rebuild_index(self, identity, uow=None):
129129
self.indexer.bulk_index([u.id for u in users])
130130
return True
131131

132+
def _check_permission(self, identity, permission_type, user):
133+
"""Checks if given identity has the specified permission type on the user."""
134+
identity_id = str(identity.id)
135+
self.require_permission(
136+
identity, permission_type, record=user, identity_id=identity_id
137+
)
138+
132139
@unit_of_work()
133140
def block(self, identity, id_, uow=None):
134141
"""Blocks a user."""
135142
user = UserAggregate.get_record(id_)
136143
if user is None:
137144
# return 403 even on empty resource due to security implications
138145
raise PermissionDeniedError()
139-
140-
self.require_permission(identity, "manage", record=user)
146+
self._check_permission(identity, "manage", user)
141147

142148
if user.blocked:
143149
raise ValidationError("User is already blocked.")
@@ -160,8 +166,7 @@ def restore(self, identity, id_, uow=None):
160166
if user is None:
161167
# return 403 even on empty resource due to security implications
162168
raise PermissionDeniedError()
163-
164-
self.require_permission(identity, "manage", record=user)
169+
self._check_permission(identity, "manage", user)
165170

166171
if not user.blocked:
167172
raise ValidationError("User is not blocked.")
@@ -185,8 +190,7 @@ def approve(self, identity, id_, uow=None):
185190
if user is None:
186191
# return 403 even on empty resource due to security implications
187192
raise PermissionDeniedError()
188-
189-
self.require_permission(identity, "manage", record=user)
193+
self._check_permission(identity, "manage", user)
190194

191195
if user.verified:
192196
raise ValidationError("User is already verified.")
@@ -209,7 +213,7 @@ def deactivate(self, identity, id_, uow=None):
209213
if user is None:
210214
# return 403 even on empty resource due to security implications
211215
raise PermissionDeniedError()
212-
self.require_permission(identity, "manage", record=user)
216+
self._check_permission(identity, "manage", user)
213217

214218
if not user.active:
215219
raise ValidationError("User is already inactive.")
@@ -225,7 +229,8 @@ def activate(self, identity, id_, uow=None):
225229
if user is None:
226230
# return 403 even on empty resource due to security implications
227231
raise PermissionDeniedError()
228-
self.require_permission(identity, "manage", record=user)
232+
self._check_permission(identity, "manage", user)
233+
229234
if user.active and user.confirmed:
230235
raise ValidationError("User is already active.")
231236
user.activate()
@@ -238,5 +243,6 @@ def can_impersonate(self, identity, id_):
238243
if user is None:
239244
# return 403 even on empty resource due to security implications
240245
raise PermissionDeniedError()
241-
self.require_permission(identity, "impersonate", record=user)
246+
self._check_permission(identity, "impersonate", user)
247+
242248
return user.model.model_obj

tests/resources/test_resources_users.py

+18
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,10 @@ def test_approve_user(client, headers, user_pub, user_moderator, db):
127127
assert res.status_code == 200
128128
assert res.json["verified_at"] is not None
129129

130+
# Test user tries to approve themselves
131+
res = client.post(f"/users/{user_moderator.id}/approve", headers=headers)
132+
assert res.status_code == 403
133+
130134

131135
def test_block_user(client, headers, user_pub, user_moderator, db):
132136
"""Tests block user endpoint."""
@@ -138,6 +142,13 @@ def test_block_user(client, headers, user_pub, user_moderator, db):
138142
assert res.status_code == 200
139143
assert res.json["blocked_at"] is not None
140144

145+
# Test user tries to block themselves
146+
res = client.post(f"/users/{user_moderator.id}/block", headers=headers)
147+
assert res.status_code == 403
148+
149+
res = client.get(f"/users/{user_moderator.id}")
150+
assert res.status_code == 200
151+
141152

142153
def test_deactivate_user(client, headers, user_pub, user_moderator, db):
143154
"""Tests deactivate user endpoint."""
@@ -149,6 +160,13 @@ def test_deactivate_user(client, headers, user_pub, user_moderator, db):
149160
assert res.status_code == 200
150161
assert res.json["active"] == False
151162

163+
# Test user tries to deactivate themselves
164+
res = client.post(f"/users/{user_moderator.id}/deactivate", headers=headers)
165+
assert res.status_code == 403
166+
167+
res = client.get(f"/users/{user_moderator.id}")
168+
assert res.status_code == 200
169+
152170

153171
def test_management_permissions(client, headers, user_pub, db):
154172
"""Test permissions at the resource level."""

0 commit comments

Comments
 (0)