Skip to content

Commit 94a1ba7

Browse files
committed
✨(backend) notify collaboration server
When an access is updated or removed, the collaboration server is notified to reset the access connection; by being disconnected, the accesses will automatically reconnect by passing by the ngnix subrequest, and so get the good rights. We do the same system when the document link is updated, except here we reset every access connection.
1 parent bfecdbf commit 94a1ba7

File tree

11 files changed

+436
-75
lines changed

11 files changed

+436
-75
lines changed

docker/files/etc/nginx/conf.d/default.conf

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,12 @@ server {
4646
proxy_set_header X-Original-Method $request_method;
4747
}
4848

49+
location /collaboration/api/ {
50+
# Collaboration server
51+
proxy_pass http://y-provider:4444;
52+
proxy_set_header Host $host;
53+
}
54+
4955
# Proxy auth for media
5056
location /media/ {
5157
# Auth request configuration

env.d/development/common.dist

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,10 @@ AI_API_KEY=password
5353
AI_MODEL=llama
5454

5555
# Collaboration
56+
COLLABORATION_API_URL=http://nginx:8083/collaboration/api/
5657
COLLABORATION_SERVER_ORIGIN=http://localhost:3000
5758
COLLABORATION_SERVER_SECRET=my-secret
58-
COLLABORATION_WS_URL=ws://localhost:8083/collaboration/ws
59+
COLLABORATION_WS_URL=ws://localhost:8083/collaboration/ws/
5960

6061
# Frontend
6162
FRONTEND_THEME=dsfr

src/backend/core/api/viewsets.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030

3131
from core import enums, models
3232
from core.services.ai_services import AIService
33+
from core.services.collaboration_services import CollaborationService
3334

3435
from . import permissions, serializers, utils
3536
from .filters import DocumentFilter
@@ -520,6 +521,10 @@ def link_configuration(self, request, *args, **kwargs):
520521
serializer.is_valid(raise_exception=True)
521522

522523
serializer.save()
524+
525+
# Notify collaboration server about the link updated
526+
CollaborationService().reset_connections(str(document.id))
527+
523528
return drf.response.Response(serializer.data, status=drf.status.HTTP_200_OK)
524529

525530
@drf.decorators.action(detail=True, methods=["post", "delete"], url_path="favorite")
@@ -815,6 +820,28 @@ def perform_create(self, serializer):
815820
self.request.user,
816821
)
817822

823+
def perform_update(self, serializer):
824+
"""Update an access to the document and notify the collaboration server."""
825+
access = serializer.save()
826+
827+
access_user_id = None
828+
if access.user:
829+
access_user_id = str(access.user.id)
830+
831+
# Notify collaboration server about the access change
832+
CollaborationService().reset_connections(
833+
str(access.document.id), access_user_id
834+
)
835+
836+
def perform_destroy(self, instance):
837+
"""Delete an access to the document and notify the collaboration server."""
838+
instance.delete()
839+
840+
# Notify collaboration server about the access removed
841+
CollaborationService().reset_connections(
842+
str(instance.document.id), str(instance.user.id)
843+
)
844+
818845

819846
class TemplateViewSet(
820847
drf.mixins.CreateModelMixin,
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
"""Collaboration services."""
2+
3+
from django.conf import settings
4+
from django.core.exceptions import ImproperlyConfigured
5+
6+
import requests
7+
8+
9+
class CollaborationService:
10+
"""Service class for Collaboration related operations."""
11+
12+
def __init__(self):
13+
"""Ensure that the collaboration configuration is set properly."""
14+
if settings.COLLABORATION_API_URL is None:
15+
raise ImproperlyConfigured("Collaboration configuration not set")
16+
17+
def reset_connections(self, room, user_id=None):
18+
"""
19+
Reset connections of a room in the collaboration server.
20+
Reseting a connection means that the user will be disconnected and will
21+
have to reconnect to the collaboration server, with updated rights.
22+
"""
23+
endpoint = "reset-connections"
24+
25+
# room is necessary as a parameter, it is easier to stick to the
26+
# same pod thanks to a parameter
27+
endpoint_url = f"{settings.COLLABORATION_API_URL}{endpoint}/?room={room}"
28+
29+
headers = {"Authorization": settings.COLLABORATION_SERVER_SECRET}
30+
if user_id:
31+
headers["X-User-Id"] = user_id
32+
33+
try:
34+
response = requests.post(endpoint_url, headers=headers, timeout=10)
35+
except requests.RequestException as e:
36+
raise requests.HTTPError("Failed to notify WebSocket server.") from e
37+
38+
if response.status_code != 200:
39+
raise requests.HTTPError(
40+
f"Failed to notify WebSocket server. Status code: {response.status_code}, "
41+
f"Response: {response.text}"
42+
)

src/backend/core/tests/documents/test_api_document_accesses.py

Lines changed: 98 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111
from core import factories, models
1212
from core.api import serializers
1313
from core.tests.conftest import TEAM, USER, VIA
14+
from core.tests.test_services_collaboration_services import ( # pylint: disable=unused-import
15+
mock_reset_connections,
16+
)
1417

1518
pytestmark = pytest.mark.django_db
1619

@@ -316,7 +319,11 @@ def test_api_document_accesses_update_authenticated_reader_or_editor(
316319

317320

318321
@pytest.mark.parametrize("via", VIA)
319-
def test_api_document_accesses_update_administrator_except_owner(via, mock_user_teams):
322+
def test_api_document_accesses_update_administrator_except_owner(
323+
via,
324+
mock_user_teams,
325+
mock_reset_connections, # pylint: disable=redefined-outer-name
326+
):
320327
"""
321328
A user who is a direct administrator in a document should be allowed to update a user
322329
access for this document, as long as they don't try to set the role to owner.
@@ -351,18 +358,21 @@ def test_api_document_accesses_update_administrator_except_owner(via, mock_user_
351358

352359
for field, value in new_values.items():
353360
new_data = {**old_values, field: value}
354-
response = client.put(
355-
f"/api/v1.0/documents/{document.id!s}/accesses/{access.id!s}/",
356-
data=new_data,
357-
format="json",
358-
)
359-
360-
if (
361-
new_data["role"] == old_values["role"]
362-
): # we are not really updating the role
361+
if new_data["role"] == old_values["role"]:
362+
response = client.put(
363+
f"/api/v1.0/documents/{document.id!s}/accesses/{access.id!s}/",
364+
data=new_data,
365+
format="json",
366+
)
363367
assert response.status_code == 403
364368
else:
365-
assert response.status_code == 200
369+
with mock_reset_connections(document.id, str(access.user_id)):
370+
response = client.put(
371+
f"/api/v1.0/documents/{document.id!s}/accesses/{access.id!s}/",
372+
data=new_data,
373+
format="json",
374+
)
375+
assert response.status_code == 200
366376

367377
access.refresh_from_db()
368378
updated_values = serializers.DocumentAccessSerializer(instance=access).data
@@ -420,7 +430,11 @@ def test_api_document_accesses_update_administrator_from_owner(via, mock_user_te
420430

421431

422432
@pytest.mark.parametrize("via", VIA)
423-
def test_api_document_accesses_update_administrator_to_owner(via, mock_user_teams):
433+
def test_api_document_accesses_update_administrator_to_owner(
434+
via,
435+
mock_user_teams,
436+
mock_reset_connections, # pylint: disable=redefined-outer-name
437+
):
424438
"""
425439
A user who is an administrator in a document, should not be allowed to update
426440
the user access of another user to grant document ownership.
@@ -457,24 +471,35 @@ def test_api_document_accesses_update_administrator_to_owner(via, mock_user_team
457471

458472
for field, value in new_values.items():
459473
new_data = {**old_values, field: value}
460-
response = client.put(
461-
f"/api/v1.0/documents/{document.id!s}/accesses/{access.id!s}/",
462-
data=new_data,
463-
format="json",
464-
)
465474
# We are not allowed or not really updating the role
466475
if field == "role" or new_data["role"] == old_values["role"]:
476+
response = client.put(
477+
f"/api/v1.0/documents/{document.id!s}/accesses/{access.id!s}/",
478+
data=new_data,
479+
format="json",
480+
)
481+
467482
assert response.status_code == 403
468483
else:
469-
assert response.status_code == 200
484+
with mock_reset_connections(document.id, str(access.user_id)):
485+
response = client.put(
486+
f"/api/v1.0/documents/{document.id!s}/accesses/{access.id!s}/",
487+
data=new_data,
488+
format="json",
489+
)
490+
assert response.status_code == 200
470491

471492
access.refresh_from_db()
472493
updated_values = serializers.DocumentAccessSerializer(instance=access).data
473494
assert updated_values == old_values
474495

475496

476497
@pytest.mark.parametrize("via", VIA)
477-
def test_api_document_accesses_update_owner(via, mock_user_teams):
498+
def test_api_document_accesses_update_owner(
499+
via,
500+
mock_user_teams,
501+
mock_reset_connections, # pylint: disable=redefined-outer-name
502+
):
478503
"""
479504
A user who is an owner in a document should be allowed to update
480505
a user access for this document whatever the role.
@@ -507,18 +532,24 @@ def test_api_document_accesses_update_owner(via, mock_user_teams):
507532

508533
for field, value in new_values.items():
509534
new_data = {**old_values, field: value}
510-
response = client.put(
511-
f"/api/v1.0/documents/{document.id!s}/accesses/{access.id!s}/",
512-
data=new_data,
513-
format="json",
514-
)
515-
516535
if (
517536
new_data["role"] == old_values["role"]
518537
): # we are not really updating the role
538+
response = client.put(
539+
f"/api/v1.0/documents/{document.id!s}/accesses/{access.id!s}/",
540+
data=new_data,
541+
format="json",
542+
)
519543
assert response.status_code == 403
520544
else:
521-
assert response.status_code == 200
545+
with mock_reset_connections(document.id, str(access.user_id)):
546+
response = client.put(
547+
f"/api/v1.0/documents/{document.id!s}/accesses/{access.id!s}/",
548+
data=new_data,
549+
format="json",
550+
)
551+
552+
assert response.status_code == 200
522553

523554
access.refresh_from_db()
524555
updated_values = serializers.DocumentAccessSerializer(instance=access).data
@@ -530,7 +561,11 @@ def test_api_document_accesses_update_owner(via, mock_user_teams):
530561

531562

532563
@pytest.mark.parametrize("via", VIA)
533-
def test_api_document_accesses_update_owner_self(via, mock_user_teams):
564+
def test_api_document_accesses_update_owner_self(
565+
via,
566+
mock_user_teams,
567+
mock_reset_connections, # pylint: disable=redefined-outer-name
568+
):
534569
"""
535570
A user who is owner of a document should be allowed to update
536571
their own user access provided there are other owners in the document.
@@ -568,21 +603,23 @@ def test_api_document_accesses_update_owner_self(via, mock_user_teams):
568603
# Add another owner and it should now work
569604
factories.UserDocumentAccessFactory(document=document, role="owner")
570605

571-
response = client.put(
572-
f"/api/v1.0/documents/{document.id!s}/accesses/{access.id!s}/",
573-
data={
574-
**old_values,
575-
"role": new_role,
576-
"user_id": old_values.get("user", {}).get("id")
577-
if old_values.get("user") is not None
578-
else None,
579-
},
580-
format="json",
581-
)
606+
user_id = str(access.user_id) if via == USER else None
607+
with mock_reset_connections(document.id, user_id):
608+
response = client.put(
609+
f"/api/v1.0/documents/{document.id!s}/accesses/{access.id!s}/",
610+
data={
611+
**old_values,
612+
"role": new_role,
613+
"user_id": old_values.get("user", {}).get("id")
614+
if old_values.get("user") is not None
615+
else None,
616+
},
617+
format="json",
618+
)
582619

583-
assert response.status_code == 200
584-
access.refresh_from_db()
585-
assert access.role == new_role
620+
assert response.status_code == 200
621+
access.refresh_from_db()
622+
assert access.role == new_role
586623

587624

588625
# Delete
@@ -656,7 +693,9 @@ def test_api_document_accesses_delete_reader_or_editor(via, role, mock_user_team
656693

657694
@pytest.mark.parametrize("via", VIA)
658695
def test_api_document_accesses_delete_administrators_except_owners(
659-
via, mock_user_teams
696+
via,
697+
mock_user_teams,
698+
mock_reset_connections, # pylint: disable=redefined-outer-name
660699
):
661700
"""
662701
Users who are administrators in a document should be allowed to delete an access
@@ -685,12 +724,13 @@ def test_api_document_accesses_delete_administrators_except_owners(
685724
assert models.DocumentAccess.objects.count() == 2
686725
assert models.DocumentAccess.objects.filter(user=access.user).exists()
687726

688-
response = client.delete(
689-
f"/api/v1.0/documents/{document.id!s}/accesses/{access.id!s}/",
690-
)
727+
with mock_reset_connections(document.id, str(access.user_id)):
728+
response = client.delete(
729+
f"/api/v1.0/documents/{document.id!s}/accesses/{access.id!s}/",
730+
)
691731

692-
assert response.status_code == 204
693-
assert models.DocumentAccess.objects.count() == 1
732+
assert response.status_code == 204
733+
assert models.DocumentAccess.objects.count() == 1
694734

695735

696736
@pytest.mark.parametrize("via", VIA)
@@ -729,7 +769,11 @@ def test_api_document_accesses_delete_administrator_on_owners(via, mock_user_tea
729769

730770

731771
@pytest.mark.parametrize("via", VIA)
732-
def test_api_document_accesses_delete_owners(via, mock_user_teams):
772+
def test_api_document_accesses_delete_owners(
773+
via,
774+
mock_user_teams,
775+
mock_reset_connections, # pylint: disable=redefined-outer-name
776+
):
733777
"""
734778
Users should be able to delete the document access of another user
735779
for a document of which they are owner.
@@ -753,12 +797,13 @@ def test_api_document_accesses_delete_owners(via, mock_user_teams):
753797
assert models.DocumentAccess.objects.count() == 2
754798
assert models.DocumentAccess.objects.filter(user=access.user).exists()
755799

756-
response = client.delete(
757-
f"/api/v1.0/documents/{document.id!s}/accesses/{access.id!s}/",
758-
)
800+
with mock_reset_connections(document.id, str(access.user_id)):
801+
response = client.delete(
802+
f"/api/v1.0/documents/{document.id!s}/accesses/{access.id!s}/",
803+
)
759804

760-
assert response.status_code == 204
761-
assert models.DocumentAccess.objects.count() == 1
805+
assert response.status_code == 204
806+
assert models.DocumentAccess.objects.count() == 1
762807

763808

764809
@pytest.mark.parametrize("via", VIA)

0 commit comments

Comments
 (0)