Skip to content

🔒(backend) prevent admins from rewriting other users comments#2323

Merged
lunika merged 1 commit into
suitenumerique:mainfrom
maboukerfa:fix/comment-admin-impersonation
May 26, 2026
Merged

🔒(backend) prevent admins from rewriting other users comments#2323
lunika merged 1 commit into
suitenumerique:mainfrom
maboukerfa:fix/comment-admin-impersonation

Conversation

@maboukerfa
Copy link
Copy Markdown
Contributor

Purpose

Currently, an administrator/owner can take ownership over other users comments by editing them:
when the comment is updated, the author field gets reassigned to the editor, silently rewriting authorship.

It makes sense for an admin to moderate comments by deleting them, but not to take ownership of them.

Screen.Recording.2026-05-25.at.21.56.40.mov

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 25, 2026

Review Change Stack

Walkthrough

This PR enforces comment authorship and separates editing vs deletion rights: serializers no longer inject user_id; CommentViewSet.perform_create saves comments with user set to the requester or None; Comment.get_abilities grants update/partial_update only to the comment author while destroy is allowed for authors and document owners/admins. Tests and changelog updated to reflect these behaviors.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: preventing administrators from rewriting other users' comments. It accurately reflects the primary security/authorization fix in the changeset.
Description check ✅ Passed The description is directly related to the changeset, explaining the current problematic behavior (admins taking ownership of comments via edits) and the intended fix (preserving deletion rights while blocking edit ownership changes).
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@CHANGELOG.md`:
- Line 18: Move the entry "- 🐛(backend) prevent admins/owners from overwriting
other users' comments and silently claiming authorship" out of the general fixes
list and place it under a "Security" section (create a "Security" header if one
doesn't exist) so it appears alongside other security fixes (e.g., entries for
v5.1.0 and v4.0.0); keep the exact entry text but ensure it's grouped under the
Security heading and that changelog ordering/formatting matches the existing
release structure.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 7084969a-2386-45f9-95ef-bc7da8de3631

📥 Commits

Reviewing files that changed from the base of the PR and between 24d58a1 and 47df0a3.

📒 Files selected for processing (7)
  • CHANGELOG.md
  • src/backend/core/api/serializers.py
  • src/backend/core/api/viewsets.py
  • src/backend/core/models.py
  • src/backend/core/tests/documents/test_api_documents_comments.py
  • src/backend/core/tests/documents/test_api_documents_threads.py
  • src/backend/core/tests/test_models_comment.py
💤 Files with no reviewable changes (1)
  • src/backend/core/api/serializers.py

Comment thread CHANGELOG.md Outdated
### Fixed

- 🐛(docs) run migration 0027 without superuser role
- 🐛(backend) prevent admins/owners from overwriting other users' comments and silently claiming authorship
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Move this entry to a "Security" section.

This fix prevents admins/owners from impersonating other users by editing their comments and claiming authorship, which is a privilege escalation / authorization issue. Security fixes should be documented in a separate "Security" section (as seen in v5.1.0, v4.0.0, and other releases) for better visibility during security audits.

📝 Suggested changelog structure
 ### Fixed
 
 - 🐛(docs) run migration 0027 without superuser role
-- 🐛(backend) prevent admins/owners from overwriting other users' comments and silently claiming authorship
-
+
+### Security
+
+- 🔒(backend) prevent admins/owners from overwriting other users' comments and silently claiming authorship
 
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CHANGELOG.md` at line 18, Move the entry "- 🐛(backend) prevent admins/owners
from overwriting other users' comments and silently claiming authorship" out of
the general fixes list and place it under a "Security" section (create a
"Security" header if one doesn't exist) so it appears alongside other security
fixes (e.g., entries for v5.1.0 and v4.0.0); keep the exact entry text but
ensure it's grouped under the Security heading and that changelog
ordering/formatting matches the existing release structure.

@lunika lunika force-pushed the fix/comment-admin-impersonation branch 2 times, most recently from 1ddc805 to 310c7c5 Compare May 26, 2026 07:31
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@CHANGELOG.md`:
- Line 18: Update the changelog entry string "- 🐛(backend) prevent
admins/owners from overwriting other users comments" to use the possessive
apostrophe: change "users comments" to "users' comments" so the line reads "-
🐛(backend) prevent admins/owners from overwriting other users' comments".

In `@src/backend/core/tests/test_models_comment.py`:
- Around line 268-280: The test
test_comment_get_abilities_user_admin_or_owner_other_comment is nondeterministic
because it uses random.choice(...) to create either an authored-by-other user or
an anonymous comment; replace that randomness by parametrizing the test over the
two author variants (e.g., author_user and None) so both branches run
deterministically for RoleChoices.ADMIN and RoleChoices.OWNER, creating comments
via factories.CommentFactory(thread__document=document, user=author) and
asserting the same abilities; after changing the parametrize, remove the
now-unused import that was only used for randomness.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: d6f93681-c446-41c9-acc0-faa81bb62d79

📥 Commits

Reviewing files that changed from the base of the PR and between 47df0a3 and 1ddc805.

📒 Files selected for processing (7)
  • CHANGELOG.md
  • src/backend/core/api/serializers.py
  • src/backend/core/api/viewsets.py
  • src/backend/core/models.py
  • src/backend/core/tests/documents/test_api_documents_comments.py
  • src/backend/core/tests/documents/test_api_documents_threads.py
  • src/backend/core/tests/test_models_comment.py
💤 Files with no reviewable changes (1)
  • src/backend/core/api/serializers.py

Comment thread CHANGELOG.md
### Fixed

- 🐛(docs) run migration 0027 without superuser role
- 🐛(backend) prevent admins/owners from overwriting other users comments
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix the grammatical error in the changelog entry.

The phrase "users comments" is missing the possessive apostrophe. It should be "users' comments" to correctly indicate that the comments belong to the users.

📝 Proposed fix
-- 🐛(backend) prevent admins/owners from overwriting other users comments
+- 🐛(backend) prevent admins/owners from overwriting other users' comments
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- 🐛(backend) prevent admins/owners from overwriting other users comments
- 🐛(backend) prevent admins/owners from overwriting other users' comments
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CHANGELOG.md` at line 18, Update the changelog entry string "- 🐛(backend)
prevent admins/owners from overwriting other users comments" to use the
possessive apostrophe: change "users comments" to "users' comments" so the line
reads "- 🐛(backend) prevent admins/owners from overwriting other users'
comments".

Comment on lines +268 to +280
@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,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Make this test deterministic and cover both author variants explicitly.

At Line 274, random.choice(...) makes coverage nondeterministic: each run validates only one branch (other user or anonymous). Parametrize that axis so both cases are always exercised.

Proposed change
-@pytest.mark.parametrize("role", [RoleChoices.ADMIN, RoleChoices.OWNER])
-def test_comment_get_abilities_user_admin_or_owner_other_comment(role):
+@pytest.mark.parametrize("role", [RoleChoices.ADMIN, RoleChoices.OWNER])
+@pytest.mark.parametrize("other_author_is_anonymous", [False, True])
+def test_comment_get_abilities_user_admin_or_owner_other_comment(
+    role, other_author_is_anonymous
+):
     """Admins/owners can moderate others' comments (destroy) but cannot edit them."""
     user = factories.UserFactory()
     document = factories.DocumentFactory(users=[(user, role)])
-    comment = factories.CommentFactory(
-        thread__document=document, user=random.choice([factories.UserFactory(), None])
-    )
+    other_author = None if other_author_is_anonymous else factories.UserFactory()
+    comment = factories.CommentFactory(thread__document=document, user=other_author)

Also remove the now-unused import:

-import random
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@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,
`@pytest.mark.parametrize`("role", [RoleChoices.ADMIN, RoleChoices.OWNER])
`@pytest.mark.parametrize`("other_author_is_anonymous", [False, True])
def test_comment_get_abilities_user_admin_or_owner_other_comment(
role, other_author_is_anonymous
):
"""Admins/owners can moderate others' comments (destroy) but cannot edit them."""
user = factories.UserFactory()
document = factories.DocumentFactory(users=[(user, role)])
other_author = None if other_author_is_anonymous else factories.UserFactory()
comment = factories.CommentFactory(thread__document=document, user=other_author)
assert comment.get_abilities(user) == {
"destroy": True,
"update": False,
"partial_update": False,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/core/tests/test_models_comment.py` around lines 268 - 280, The
test test_comment_get_abilities_user_admin_or_owner_other_comment is
nondeterministic because it uses random.choice(...) to create either an
authored-by-other user or an anonymous comment; replace that randomness by
parametrizing the test over the two author variants (e.g., author_user and None)
so both branches run deterministically for RoleChoices.ADMIN and
RoleChoices.OWNER, creating comments via
factories.CommentFactory(thread__document=document, user=author) and asserting
the same abilities; after changing the parametrize, remove the now-unused import
that was only used for randomness.

@lunika lunika enabled auto-merge (squash) May 26, 2026 07:33
@lunika lunika disabled auto-merge May 26, 2026 07:42
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>
@lunika lunika force-pushed the fix/comment-admin-impersonation branch from 310c7c5 to d8742c4 Compare May 26, 2026 07:58
@lunika lunika enabled auto-merge (squash) May 26, 2026 08:00
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/backend/core/api/viewsets.py`:
- Around line 3007-3010: In perform_create, before calling
serializer.save(user=user), validate that the nested thread_id belongs to the
current resource by querying Thread with both id=thread_id and
document_id=resource_id (e.g. Thread.objects.filter(id=thread_id,
document_id=resource_id).exists() or get() inside a try/except); if no match,
raise a 404/400 (NotFound/ValidationError) so requests like POST
/documents/A/threads/<thread-from-B>/comments/ are rejected; keep the rest of
perform_create and only call serializer.save(user=user) after this check.

In `@src/backend/core/models.py`:
- Around line 1898-1906: The returned mutation flags
("destroy","update","partial_update") must require current document/read access
in addition to authorship or moderation; modify the logic in the method that
computes is_author and can_moderate so it also checks the user's current access
to the parent document (e.g. compute has_document_access via the existing
document permission helper or a method like document.can_read(user) or
DocumentPermission.has_object_permission) and only return true for
"update"/"partial_update" when is_author AND has_document_access, and only
return true for "destroy" when can_moderate AND has_document_access.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 548e7202-10d2-480c-a3d4-f337b62c3848

📥 Commits

Reviewing files that changed from the base of the PR and between 310c7c5 and d8742c4.

📒 Files selected for processing (7)
  • CHANGELOG.md
  • src/backend/core/api/serializers.py
  • src/backend/core/api/viewsets.py
  • src/backend/core/models.py
  • src/backend/core/tests/documents/test_api_documents_comments.py
  • src/backend/core/tests/documents/test_api_documents_threads.py
  • src/backend/core/tests/test_models_comment.py

Comment on lines +3007 to +3010
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)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Reject cross-document thread IDs before saving the comment.

Line 3010 trusts the nested thread_id without proving that it belongs to resource_id. Since CommentPermission.has_permission() authorizes create from the document only, a user who can comment on document A can still POST to /documents/A/threads/<thread-from-B>/comments/ and write into document B if they know that thread UUID. Validate Thread(id=thread_id, document_id=resource_id) here and fail with 404/400 before calling save().

Suggested fix
     def perform_create(self, serializer):
         """Attach the request user as the comment author."""
+        if not models.Thread.objects.filter(
+            id=self.kwargs["thread_id"],
+            document_id=self.kwargs["resource_id"],
+        ).exists():
+            raise drf.exceptions.NotFound("Thread not found.")
+
         user = self.request.user if self.request.user.is_authenticated else None
         serializer.save(user=user)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/core/api/viewsets.py` around lines 3007 - 3010, In
perform_create, before calling serializer.save(user=user), validate that the
nested thread_id belongs to the current resource by querying Thread with both
id=thread_id and document_id=resource_id (e.g.
Thread.objects.filter(id=thread_id, document_id=resource_id).exists() or get()
inside a try/except); if no match, raise a 404/400 (NotFound/ValidationError) so
requests like POST /documents/A/threads/<thread-from-B>/comments/ are rejected;
keep the rest of perform_create and only call serializer.save(user=user) after
this check.

Comment on lines +1898 to +1906
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,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Gate comment mutations on current document access as well as authorship.

Because CommentPermission.has_object_permission() delegates to this method, Lines 1904-1906 still allow an author to update or delete a comment after their access to the document has been revoked. That leaves a direct PATCH/DELETE path open for ex-members who know the nested URL. Please require current comment/read access in addition to authorship/moderation before returning mutation abilities.

Suggested fix
-        is_author = self.user == user
-        can_moderate = is_author or role in [
-            RoleChoices.OWNER,
-            RoleChoices.ADMIN,
-        ]
+        is_author = read_access and self.user == user
+        can_moderate = read_access and (
+            self.user == user
+            or role in [
+                RoleChoices.OWNER,
+                RoleChoices.ADMIN,
+            ]
+        )
         return {
             "destroy": can_moderate,
             "update": is_author,
             "partial_update": is_author,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/core/models.py` around lines 1898 - 1906, The returned mutation
flags ("destroy","update","partial_update") must require current document/read
access in addition to authorship or moderation; modify the logic in the method
that computes is_author and can_moderate so it also checks the user's current
access to the parent document (e.g. compute has_document_access via the existing
document permission helper or a method like document.can_read(user) or
DocumentPermission.has_object_permission) and only return true for
"update"/"partial_update" when is_author AND has_document_access, and only
return true for "destroy" when can_moderate AND has_document_access.

@lunika lunika merged commit 3264e29 into suitenumerique:main May 26, 2026
37 of 38 checks passed
@AntoLC AntoLC mentioned this pull request Jun 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants