Skip to content

Commit 7110c60

Browse files
committed
✨(back) check on document update if user can save it
When a document is updated, users not connected to the collaboration server can override work made by other people connected to the collaboration server. To avoid this, the priority is given to user connected to the collaboration server. If the websocket property in the request payload is missing or set to False, the backend fetch the collaboration server to now if the user can save or not. If users are already connected, the user can't save. Also, only one user without websocket can save a connect, the first user saving acquire a lock and all other users can't save. To implement this behavior, we need to track all users, connected and not, so a session is created for every user in the ForceSessionMiddleware.
1 parent 963d5f9 commit 7110c60

File tree

7 files changed

+388
-10
lines changed

7 files changed

+388
-10
lines changed

src/backend/core/api/serializers.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,7 @@ class DocumentSerializer(ListDocumentSerializer):
239239
"""Serialize documents with all fields for display in detail views."""
240240

241241
content = serializers.CharField(required=False)
242+
websocket = serializers.BooleanField(required=False, write_only=True)
242243

243244
class Meta:
244245
model = models.Document
@@ -260,6 +261,7 @@ class Meta:
260261
"title",
261262
"updated_at",
262263
"user_roles",
264+
"websocket",
263265
]
264266
read_only_fields = [
265267
"id",

src/backend/core/api/viewsets.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
from rest_framework import response as drf_response
3131
from rest_framework.permissions import AllowAny
3232
from rest_framework.throttling import UserRateThrottle
33+
from sentry_sdk import capture_exception
3334

3435
from core import authentication, enums, models
3536
from core.services.ai_services import AIService
@@ -631,6 +632,54 @@ def perform_destroy(self, instance):
631632
"""Override to implement a soft delete instead of dumping the record in database."""
632633
instance.soft_delete()
633634

635+
def perform_update(self, serializer):
636+
"""Check rules about collaboration."""
637+
if serializer.validated_data.get("websocket"):
638+
return super().perform_update(serializer)
639+
640+
try:
641+
connection_info = CollaborationService().get_document_connection_info(
642+
serializer.instance.id,
643+
self.request.session.session_key,
644+
)
645+
except requests.HTTPError as e:
646+
capture_exception(e)
647+
connection_info = {
648+
"count": 0,
649+
"exists": False,
650+
}
651+
652+
if connection_info["count"] == 0:
653+
# No websocket mode
654+
logger.debug("update without connection found in the websocket server")
655+
cache_key = f"docs:no-websocket:{serializer.instance.id}"
656+
current_editor = cache.get(cache_key)
657+
if not current_editor:
658+
cache.set(
659+
cache_key,
660+
self.request.session.session_key,
661+
settings.NO_WEBSOCKET_CACHE_TIMEOUT,
662+
)
663+
elif current_editor != self.request.session.session_key:
664+
raise drf.exceptions.PermissionDenied(
665+
"You are not allowed to edit this document."
666+
)
667+
cache.touch(cache_key, settings.NO_WEBSOCKET_CACHE_TIMEOUT)
668+
return super().perform_update(serializer)
669+
670+
if connection_info["exists"]:
671+
# Websocket mode
672+
logger.debug("session key found in the websocket server")
673+
return super().perform_update(serializer)
674+
675+
logger.debug(
676+
"Users connected to the websocket but current editor not connected to it. Can not edit."
677+
)
678+
679+
raise drf.exceptions.PermissionDenied(
680+
"You are not allowed to edit this document."
681+
)
682+
634683
@drf.decorators.action(
635684
detail=False,
636685
methods=["get"],

src/backend/core/middleware.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
"""Force session creation for all requests."""
2+
3+
4+
class ForceSessionMiddleware:
5+
"""
6+
Force session creation for unauthenticated users.
7+
Must be used after Authentication middleware.
8+
"""
9+
10+
def __init__(self, get_response):
11+
"""Initialize the middleware."""
12+
self.get_response = get_response
13+
14+
def __call__(self, request):
15+
"""Force session creation for unauthenticated users."""
16+
17+
if not request.user.is_authenticated and request.session.session_key is None:
18+
request.session.save()
19+
20+
response = self.get_response(request)
21+
return response

src/backend/core/services/collaboration_services.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,31 @@ def reset_connections(self, room, user_id=None):
4141
f"Failed to notify WebSocket server. Status code: {response.status_code}, "
4242
f"Response: {response.text}"
4343
)
44+
45+
def get_document_connection_info(self, room, session_key):
46+
"""
47+
Get the connection info for a document.
48+
"""
49+
endpoint = "get-connections"
50+
querystring = {
51+
"room": room,
52+
"sessionKey": session_key,
53+
}
54+
endpoint_url = f"{settings.COLLABORATION_API_URL}{endpoint}/"
55+
56+
headers = {"Authorization": settings.COLLABORATION_SERVER_SECRET}
57+
58+
try:
59+
response = requests.get(
60+
endpoint_url, headers=headers, params=querystring, timeout=10
61+
)
62+
except requests.RequestException as e:
63+
raise requests.HTTPError("Failed to get document connection info.") from e
64+
65+
if response.status_code != 200:
66+
raise requests.HTTPError(
67+
f"Failed to get document connection info. Status code: {response.status_code}, "
68+
f"Response: {response.text}"
69+
)
70+
71+
return response.json()

0 commit comments

Comments
 (0)