|
2 | 2 |
|
3 | 3 | # pylint: disable=too-many-lines |
4 | 4 | import random |
| 5 | +import uuid |
5 | 6 | from unittest import mock |
6 | 7 |
|
7 | 8 | from django.contrib.auth.models import AnonymousUser |
@@ -302,6 +303,118 @@ def test_create_comment_authenticated_user_not_enough_access(): |
302 | 303 | assert response.status_code == 403 |
303 | 304 |
|
304 | 305 |
|
| 306 | +def test_create_comment_thread_from_another_document(): |
| 307 | + """ |
| 308 | + A user allowed to comment on a document should not be able to store a |
| 309 | + comment into a thread belonging to another document by naming their own |
| 310 | + document in the URL path. |
| 311 | + """ |
| 312 | + user = factories.UserFactory() |
| 313 | + other_document = factories.DocumentFactory( |
| 314 | + link_reach="restricted", users=[(user, models.LinkRoleChoices.COMMENTER)] |
| 315 | + ) |
| 316 | + thread = factories.ThreadFactory() # belongs to another document |
| 317 | + |
| 318 | + client = APIClient() |
| 319 | + client.force_login(user) |
| 320 | + with mock.patch("core.api.viewsets.posthog_capture") as mock_capture: |
| 321 | + response = client.post( |
| 322 | + f"/api/v1.0/documents/{other_document.id!s}/threads/{thread.id!s}/comments/", |
| 323 | + {"body": "test"}, |
| 324 | + ) |
| 325 | + |
| 326 | + assert response.status_code == 400 |
| 327 | + assert response.json() == { |
| 328 | + "thread_id": ["This thread does not belong to this document."] |
| 329 | + } |
| 330 | + # No comment must have been written anywhere |
| 331 | + assert models.Comment.objects.count() == 0 |
| 332 | + mock_capture.assert_not_called() |
| 333 | + |
| 334 | + |
| 335 | +def test_create_comment_nonexistent_thread(): |
| 336 | + """ |
| 337 | + A nonexistent thread id should be refused with a 400, not crash with a 500 |
| 338 | + as it did before the thread-to-document binding was enforced. |
| 339 | + """ |
| 340 | + user = factories.UserFactory() |
| 341 | + document = factories.DocumentFactory( |
| 342 | + link_reach="restricted", users=[(user, models.LinkRoleChoices.COMMENTER)] |
| 343 | + ) |
| 344 | + |
| 345 | + client = APIClient() |
| 346 | + client.force_login(user) |
| 347 | + response = client.post( |
| 348 | + f"/api/v1.0/documents/{document.id!s}/threads/{uuid.uuid4()!s}/comments/", |
| 349 | + {"body": "test"}, |
| 350 | + ) |
| 351 | + |
| 352 | + assert response.status_code == 400 |
| 353 | + assert response.json() == { |
| 354 | + "thread_id": ["This thread does not belong to this document."] |
| 355 | + } |
| 356 | + assert models.Comment.objects.count() == 0 |
| 357 | + |
| 358 | + |
| 359 | +@pytest.mark.parametrize( |
| 360 | + "link_reach", |
| 361 | + [models.LinkReachChoices.RESTRICTED, models.LinkReachChoices.AUTHENTICATED], |
| 362 | +) |
| 363 | +def test_create_comment_anonymous_user_cross_document_thread(link_reach): |
| 364 | + """ |
| 365 | + An anonymous user should not be able to write into a thread of a private |
| 366 | + document by naming a public document they are allowed to comment on in the |
| 367 | + URL path. |
| 368 | + """ |
| 369 | + public_document = factories.DocumentFactory( |
| 370 | + link_reach="public", link_role=models.LinkRoleChoices.EDITOR |
| 371 | + ) |
| 372 | + thread = factories.ThreadFactory( |
| 373 | + document__link_reach=link_reach |
| 374 | + ) # belongs to another document |
| 375 | + |
| 376 | + client = APIClient() |
| 377 | + response = client.post( |
| 378 | + f"/api/v1.0/documents/{public_document.id!s}/threads/{thread.id!s}/comments/", |
| 379 | + {"body": "test"}, |
| 380 | + ) |
| 381 | + |
| 382 | + assert response.status_code == 400 |
| 383 | + assert models.Comment.objects.count() == 0 |
| 384 | + |
| 385 | + |
| 386 | +def test_create_comment_reader_cross_document_thread(): |
| 387 | + """ |
| 388 | + A reader of a document should not be able to escalate to writing comments |
| 389 | + by naming another document they can comment on in the URL path. |
| 390 | + """ |
| 391 | + reader = factories.UserFactory() |
| 392 | + owner = factories.UserFactory() |
| 393 | + victim_document = factories.DocumentFactory(users=[(owner, "owner")]) |
| 394 | + factories.UserDocumentAccessFactory( |
| 395 | + document=victim_document, user=reader, role=models.RoleChoices.READER |
| 396 | + ) |
| 397 | + thread = factories.ThreadFactory(document=victim_document) |
| 398 | + |
| 399 | + attacker_document = factories.DocumentFactory( |
| 400 | + link_reach="restricted", users=[(reader, models.LinkRoleChoices.COMMENTER)] |
| 401 | + ) |
| 402 | + |
| 403 | + client = APIClient() |
| 404 | + client.force_login(reader) |
| 405 | + response = client.post( |
| 406 | + f"/api/v1.0/documents/{attacker_document.id!s}/threads/{thread.id!s}/comments/", |
| 407 | + {"body": "test"}, |
| 408 | + ) |
| 409 | + |
| 410 | + assert response.status_code == 400 |
| 411 | + assert response.json() == { |
| 412 | + "thread_id": ["This thread does not belong to this document."] |
| 413 | + } |
| 414 | + # The victim's thread was not polluted |
| 415 | + assert models.Comment.objects.count() == 0 |
| 416 | + |
| 417 | + |
305 | 418 | # Retrieve comment |
306 | 419 |
|
307 | 420 |
|
|
0 commit comments