🔒(backend) prevent admins from rewriting other users comments

Admins/owners could overwrite another user's comment text and silently
take ownership of it.
Split write permissions: destroy stays broad (author + admin/owner) for
moderation.
    
Signed-off-by: Mohamed El Amine BOUKERFA <boukerfa.ma@gmail.com>
This commit is contained in:
Amine BOUKERFA
2026-05-26 10:26:31 +02:00
committed by GitHub
parent 8d42f814b7
commit 3264e29941
7 changed files with 33 additions and 28 deletions
+1
View File
@@ -15,6 +15,7 @@ and this project adheres to
### Fixed
- 🐛(docs) run migration 0027 without superuser role
- 🐛(backend) prevent admins/owners from overwriting other users comments
## [v5.1.0] - 2026-05-11
-5
View File
@@ -911,12 +911,7 @@ class CommentSerializer(serializers.ModelSerializer):
def validate(self, attrs):
"""Validate comment data."""
request = self.context.get("request")
user = getattr(request, "user", None)
attrs["thread_id"] = self.context["thread_id"]
attrs["user_id"] = user.id if user else None
return attrs
def get_abilities(self, obj):
+5
View File
@@ -3004,6 +3004,11 @@ class CommentViewSet(
context["thread_id"] = self.kwargs["thread_id"]
return context
def perform_create(self, serializer):
"""Attach the request user as the comment author."""
user = self.request.user if self.request.user.is_authenticated else None
serializer.save(user=user)
@drf.decorators.action(
detail=True,
methods=["post", "delete"],
+5 -4
View File
@@ -1895,14 +1895,15 @@ class Comment(BaseModel):
doc_abilities = self.thread.document.get_abilities(user)
read_access = doc_abilities.get("comment", False)
can_react = read_access and user.is_authenticated
write_access = self.user == user or role in [
is_author = self.user == user
can_moderate = is_author or role in [
RoleChoices.OWNER,
RoleChoices.ADMIN,
]
return {
"destroy": write_access,
"update": write_access,
"partial_update": write_access,
"destroy": can_moderate,
"update": is_author,
"partial_update": is_author,
"reactions": can_react,
"retrieve": read_access,
}
@@ -438,6 +438,7 @@ def test_update_comment_authenticated_user_own_comment():
comment.refresh_from_db()
assert comment.body == "other content"
assert comment.user == user
def test_update_comment_authenticated_user_not_enough_access():
@@ -479,14 +480,15 @@ def test_update_comment_authenticated_no_access():
@pytest.mark.parametrize("role", [models.RoleChoices.ADMIN, models.RoleChoices.OWNER])
def test_update_comment_authenticated_admin_or_owner_can_update_any_comment(role):
def test_update_comment_authenticated_admin_or_owner_cannot_update_other_comment(role):
"""
Authenticated users should be able to update comments on a document they don't have access to.
Admins and owners can moderate (delete) but must not edit other users' comments.
"""
user = factories.UserFactory()
document = factories.DocumentFactory(users=[(user, role)])
thread = factories.ThreadFactory(document=document)
comment = factories.CommentFactory(thread=thread, body="test")
original_author = factories.UserFactory()
comment = factories.CommentFactory(thread=thread, body="test", user=original_author)
client = APIClient()
client.force_login(user)
@@ -494,10 +496,11 @@ def test_update_comment_authenticated_admin_or_owner_can_update_any_comment(role
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/comments/{comment.id!s}/",
{"body": "other content"},
)
assert response.status_code == 200
assert response.status_code == 403
comment.refresh_from_db()
assert comment.body == "other content"
assert comment.body == "test"
assert comment.user == original_author
@pytest.mark.parametrize("role", [models.RoleChoices.ADMIN, models.RoleChoices.OWNER])
@@ -796,8 +796,8 @@ def test_api_documents_threads_retrieve_restricted_document_privileged_roles(rol
"reactions": [],
"abilities": {
"destroy": True,
"update": True,
"partial_update": True,
"update": False,
"partial_update": False,
"reactions": True,
"retrieve": True,
},
+12 -12
View File
@@ -249,13 +249,12 @@ def test_comment_get_abilities_user_editor_own_comment(link_role, link_reach):
}
def test_comment_get_abilities_user_admin():
"""Admins have all accesses to a comment."""
@pytest.mark.parametrize("role", [RoleChoices.ADMIN, RoleChoices.OWNER])
def test_comment_get_abilities_user_admin_or_owner_own_comment(role):
"""Admins/owners have full write access on their own comment."""
user = factories.UserFactory()
document = factories.DocumentFactory(users=[(user, RoleChoices.ADMIN)])
comment = factories.CommentFactory(
thread__document=document, user=random.choice([user, None])
)
document = factories.DocumentFactory(users=[(user, role)])
comment = factories.CommentFactory(thread__document=document, user=user)
assert comment.get_abilities(user) == {
"destroy": True,
@@ -266,18 +265,19 @@ def test_comment_get_abilities_user_admin():
}
def test_comment_get_abilities_user_owner():
"""Owners have all accesses to a comment."""
@pytest.mark.parametrize("role", [RoleChoices.ADMIN, RoleChoices.OWNER])
def test_comment_get_abilities_user_admin_or_owner_other_comment(role):
"""Admins/owners can moderate others' comments (destroy) but cannot edit them."""
user = factories.UserFactory()
document = factories.DocumentFactory(users=[(user, RoleChoices.OWNER)])
document = factories.DocumentFactory(users=[(user, role)])
comment = factories.CommentFactory(
thread__document=document, user=random.choice([user, None])
thread__document=document, user=random.choice([factories.UserFactory(), None])
)
assert comment.get_abilities(user) == {
"destroy": True,
"update": True,
"partial_update": True,
"update": False,
"partial_update": False,
"reactions": True,
"retrieve": True,
}