Skip to content

Give priority to users connected to collaboration server (aka no websocket feature) #1093

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jul 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@ and this project adheres to
- 📝(project) add troubleshoot doc #1066
- 📝(project) add system-requirement doc #1066
- 🔧(front) configure x-frame-options to DENY in nginx conf #1084
- (doc) add documentation to install with compose #855
- ✨(backend) allow to disable checking unsafe mimetype on attachment upload
- ✨Ask for access #1081
- ✨(doc) add documentation to install with compose #855
- ✨ Give priority to users connected to collaboration server
(aka no websocket feature) #1093

### Changed

Expand Down
196 changes: 98 additions & 98 deletions docs/env.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions env.d/development/common.dist
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ COLLABORATION_API_URL=http://y-provider-development:4444/collaboration/api/
COLLABORATION_BACKEND_BASE_URL=http://app-dev:8000
COLLABORATION_SERVER_ORIGIN=http://localhost:3000
COLLABORATION_SERVER_SECRET=my-secret
COLLABORATION_WS_NOT_CONNECTED_READY_ONLY=true
COLLABORATION_WS_URL=ws://localhost:4444/collaboration/ws/

DJANGO_SERVER_TO_SERVER_API_TOKENS=server-api-token
Expand Down
2 changes: 2 additions & 0 deletions src/backend/core/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,7 @@ class DocumentSerializer(ListDocumentSerializer):
"""Serialize documents with all fields for display in detail views."""

content = serializers.CharField(required=False)
websocket = serializers.BooleanField(required=False, write_only=True)

class Meta:
model = models.Document
Expand All @@ -260,6 +261,7 @@ class Meta:
"title",
"updated_at",
"user_roles",
"websocket",
]
read_only_fields = [
"id",
Expand Down
77 changes: 77 additions & 0 deletions src/backend/core/api/viewsets.py
Original file line number Diff line number Diff line change
Expand Up @@ -634,6 +634,83 @@ def perform_destroy(self, instance):
"""Override to implement a soft delete instead of dumping the record in database."""
instance.soft_delete()

def _can_user_edit_document(self, document_id, set_cache=False):
"""Check if the user can edit the document."""
try:
count, exists = CollaborationService().get_document_connection_info(
document_id,
self.request.session.session_key,
)
except requests.HTTPError as e:
logger.exception("Failed to call collaboration server: %s", e)
count = 0
exists = False

if count == 0:
# Nobody is connected to the websocket server
logger.debug("update without connection found in the websocket server")
cache_key = f"docs:no-websocket:{document_id}"
current_editor = cache.get(cache_key)

if not current_editor:
if set_cache:
cache.set(
cache_key,
self.request.session.session_key,
settings.NO_WEBSOCKET_CACHE_TIMEOUT,
)
return True

if current_editor != self.request.session.session_key:
return False

if set_cache:
cache.touch(cache_key, settings.NO_WEBSOCKET_CACHE_TIMEOUT)
return True

if exists:
# Current user is connected to the websocket server
logger.debug("session key found in the websocket server")
return True

logger.debug(
"Users connected to the websocket but current editor not connected to it. Can not edit."
)

return False

def perform_update(self, serializer):
"""Check rules about collaboration."""
if (
serializer.validated_data.get("websocket", False)
or not settings.COLLABORATION_WS_NOT_CONNECTED_READY_ONLY
):
return super().perform_update(serializer)

if self._can_user_edit_document(serializer.instance.id, set_cache=True):
return super().perform_update(serializer)
Comment on lines +690 to +691
Copy link
Member

Choose a reason for hiding this comment

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

You may want to raise a specific error content to display a proper message to the user

Copy link
Member Author

Choose a reason for hiding this comment

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

Not sure to understand what you mean. I raise nothing here.


raise drf.exceptions.PermissionDenied(
"You are not allowed to edit this document."
)

@drf.decorators.action(
detail=True,
methods=["get"],
url_path="can-edit",
)
def can_edit(self, request, *args, **kwargs):
"""Check if the current user can edit the document."""
document = self.get_object()

can_edit = (
True
if not settings.COLLABORATION_WS_NOT_CONNECTED_READY_ONLY
else self._can_user_edit_document(document.id)
)

return drf.response.Response({"can_edit": can_edit})

@drf.decorators.action(
detail=False,
methods=["get"],
Expand Down
21 changes: 21 additions & 0 deletions src/backend/core/middleware.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"""Force session creation for all requests."""


class ForceSessionMiddleware:
"""
Force session creation for unauthenticated users.
Must be used after Authentication middleware.
"""

def __init__(self, get_response):
"""Initialize the middleware."""
self.get_response = get_response

def __call__(self, request):
"""Force session creation for unauthenticated users."""

if not request.user.is_authenticated and request.session.session_key is None:
request.session.create()

response = self.get_response(request)
return response
1 change: 1 addition & 0 deletions src/backend/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -836,6 +836,7 @@ def get_abilities(self, user, ancestors_links=None):
"ai_translate": ai_access,
"attachment_upload": can_update,
"media_check": can_get,
"can_edit": can_update,
"children_list": can_get,
"children_create": can_update and user.is_authenticated,
"collaboration_auth": can_get,
Expand Down
28 changes: 28 additions & 0 deletions src/backend/core/services/collaboration_services.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,31 @@ def reset_connections(self, room, user_id=None):
f"Failed to notify WebSocket server. Status code: {response.status_code}, "
f"Response: {response.text}"
)

def get_document_connection_info(self, room, session_key):
"""
Get the connection info for a document.
"""
endpoint = "get-connections"
querystring = {
"room": room,
"sessionKey": session_key,
}
endpoint_url = f"{settings.COLLABORATION_API_URL}{endpoint}/"

headers = {"Authorization": settings.COLLABORATION_SERVER_SECRET}

try:
response = requests.get(
endpoint_url, headers=headers, params=querystring, timeout=10
)
except requests.RequestException as e:
raise requests.HTTPError("Failed to get document connection info.") from e

if response.status_code != 200:
raise requests.HTTPError(
f"Failed to get document connection info. Status code: {response.status_code}, "
f"Response: {response.text}"
)
result = response.json()
return result.get("count", 0), result.get("exists", False)
Loading
Loading