diff --git a/CHANGELOG.md b/CHANGELOG.md index 44206ea4..0b08f365 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to ## Changed - ♻️(frontend) Integrate UI kit #783 +- 🏗️(y-provider) manage auth in y-provider app ## Fixed diff --git a/src/backend/core/api/viewsets.py b/src/backend/core/api/viewsets.py index 432ca02c..ea8d6be2 100644 --- a/src/backend/core/api/viewsets.py +++ b/src/backend/core/api/viewsets.py @@ -380,10 +380,7 @@ class DocumentViewSet( 9. **Media Auth**: Authorize access to document media. Example: GET /documents/media-auth/ - 10. **Collaboration Auth**: Authorize access to the collaboration server for a document. - Example: GET /documents/collaboration-auth/ - - 11. **AI Transform**: Apply a transformation action on a piece of text with AI. + 10. **AI Transform**: Apply a transformation action on a piece of text with AI. Example: POST /documents/{id}/ai-transform/ Expected data: - text (str): The input text. @@ -391,7 +388,7 @@ class DocumentViewSet( Returns: JSON response with the processed text. Throttled by: AIDocumentRateThrottle, AIUserRateThrottle. - 12. **AI Translate**: Translate a piece of text with AI. + 11. **AI Translate**: Translate a piece of text with AI. Example: POST /documents/{id}/ai-translate/ Expected data: - text (str): The input text. @@ -1207,17 +1204,6 @@ class DocumentViewSet( logger.debug("Failed to extract parameters from subrequest URL: %s", exc) raise drf.exceptions.PermissionDenied() from exc - def _auth_get_document(self, pk): - """ - Retrieves the document corresponding to the given primary key (pk). - Raises PermissionDenied if the document is not found. - """ - try: - return models.Document.objects.get(pk=pk) - except models.Document.DoesNotExist as exc: - logger.debug("Document with ID '%s' does not exist", pk) - raise drf.exceptions.PermissionDenied() from exc - @drf.decorators.action(detail=False, methods=["get"], url_path="media-auth") def media_auth(self, request, *args, **kwargs): """ @@ -1265,42 +1251,6 @@ class DocumentViewSet( return drf.response.Response("authorized", headers=request.headers, status=200) - @drf.decorators.action(detail=False, methods=["get"], url_path="collaboration-auth") - def collaboration_auth(self, request, *args, **kwargs): - """ - This view is used by an Nginx subrequest to control access to a document's - collaboration server. - """ - parsed_url = self._auth_get_original_url(request) - url_params = self._auth_get_url_params( - enums.COLLABORATION_WS_URL_PATTERN, parsed_url.query - ) - document = self._auth_get_document(url_params["pk"]) - - abilities = document.get_abilities(request.user) - if not abilities.get(self.action, False): - logger.debug( - "User '%s' lacks permission for document '%s'", - request.user, - document.pk, - ) - raise drf.exceptions.PermissionDenied() - - if not settings.COLLABORATION_SERVER_SECRET: - logger.debug("Collaboration server secret is not defined") - raise drf.exceptions.PermissionDenied() - - # Add the collaboration server secret token to the headers - headers = { - "Authorization": settings.COLLABORATION_SERVER_SECRET, - "X-Can-Edit": str(abilities["partial_update"]), - } - - if request.user.is_authenticated: - headers["X-User-Id"] = str(request.user.id) - - return drf.response.Response("authorized", headers=headers, status=200) - @drf.decorators.action( detail=True, methods=["post"], diff --git a/src/backend/core/enums.py b/src/backend/core/enums.py index a9bdd898..78cac40a 100644 --- a/src/backend/core/enums.py +++ b/src/backend/core/enums.py @@ -20,7 +20,6 @@ MEDIA_STORAGE_URL_PATTERN = re.compile( MEDIA_STORAGE_URL_EXTRACT = re.compile( f"{settings.MEDIA_URL:s}({UUID_REGEX}/{ATTACHMENTS_FOLDER}/{UUID_REGEX}{FILE_EXT_REGEX})" ) -COLLABORATION_WS_URL_PATTERN = re.compile(rf"(?:^|&)room=(?P{UUID_REGEX})(?:&|$)") # In Django's code base, `LANGUAGES` is set by default with all supported languages. diff --git a/src/backend/core/tests/documents/test_api_documents_collaboration_auth.py b/src/backend/core/tests/documents/test_api_documents_collaboration_auth.py deleted file mode 100644 index de7ec6a7..00000000 --- a/src/backend/core/tests/documents/test_api_documents_collaboration_auth.py +++ /dev/null @@ -1,185 +0,0 @@ -""" -Test collaboration websocket access API endpoint for users in impress's core app. -""" - -from uuid import uuid4 - -from django.test import override_settings - -import pytest -from rest_framework.test import APIClient - -from core import factories, models -from core.tests.conftest import TEAM, USER, VIA - -pytestmark = pytest.mark.django_db - - -def test_api_documents_collaboration_auth_unkown_document(): - """ - Trying to connect to the collaboration server on a document ID that does not exist - should not have the side effect to create it (no regression test). - """ - original_url = f"http://localhost/collaboration/ws/?room={uuid4()!s}" - - response = APIClient().get( - "/api/v1.0/documents/collaboration-auth/", HTTP_X_ORIGINAL_URL=original_url - ) - - assert response.status_code == 403 - assert models.Document.objects.exists() is False - - -def test_api_documents_collaboration_auth_original_url_not_matching(): - """ - Trying to authenticate on the collaboration server with an invalid - original url should return a 403. - """ - document = factories.DocumentFactory(link_reach="public") - - response = APIClient().get( - "/api/v1.0/documents/collaboration-auth/", - HTTP_X_ORIGINAL_URL=f"http://localhost/ws/?invalid={document.pk}", - ) - - assert response.status_code == 403 - assert "Authorization" not in response - assert "X-Can-Edit" not in response - assert "X-User-Id" not in response - - -def test_api_documents_collaboration_auth_secret_not_defined(settings): - """ - Trying to authenticate on the collaboration server when the secret is not defined - should return a 403. - """ - settings.COLLABORATION_SERVER_SECRET = None - - document = factories.DocumentFactory(link_reach="public") - - response = APIClient().get( - "/api/v1.0/documents/collaboration-auth/", - HTTP_X_ORIGINAL_URL=f"http://localhost/ws/?room={document.pk}", - ) - - assert response.status_code == 403 - assert "Authorization" not in response - assert "X-Can-Edit" not in response - assert "X-User-Id" not in response - - -@override_settings(COLLABORATION_SERVER_SECRET="123") -@pytest.mark.parametrize("reach", ["authenticated", "restricted"]) -def test_api_documents_collaboration_auth_anonymous_authenticated_or_restricted(reach): - """ - Anonymous users should not be allowed to connect to the collaboration server for a document - with link reach set to authenticated or restricted. - """ - document = factories.DocumentFactory(link_reach=reach) - - response = APIClient().get( - "/api/v1.0/documents/collaboration-auth/", - HTTP_X_ORIGINAL_URL=f"http://localhost/ws/?room={document.pk}", - ) - - assert response.status_code == 403 - assert "Authorization" not in response - assert "X-Can-Edit" not in response - assert "X-User-Id" not in response - - -@override_settings(COLLABORATION_SERVER_SECRET="123") -def test_api_documents_collaboration_auth_anonymous_public(): - """ - Anonymous users should be able to connect to the collaboration server for a public document. - """ - document = factories.DocumentFactory(link_reach="public") - - response = APIClient().get( - "/api/v1.0/documents/collaboration-auth/", - HTTP_X_ORIGINAL_URL=f"http://localhost/ws/?room={document.pk}", - ) - - assert response.status_code == 200 - assert response["Authorization"] == "123" - assert response["X-Can-Edit"] == str(document.link_role == "editor") - assert "X-User-Id" not in response - - -@override_settings(COLLABORATION_SERVER_SECRET="123") -@pytest.mark.parametrize("reach", ["public", "authenticated"]) -def test_api_documents_collaboration_auth_authenticated_public_or_authenticated(reach): - """ - Authenticated users who are not related to a document should be able to connect to the - collaboration server if this document's link reach is set to public or authenticated. - """ - user = factories.UserFactory() - client = APIClient() - client.force_login(user) - - document = factories.DocumentFactory(link_reach=reach) - - response = client.get( - "/api/v1.0/documents/collaboration-auth/", - HTTP_X_ORIGINAL_URL=f"http://localhost/ws/?room={document.pk}", - ) - - assert response.status_code == 200 - assert response["Authorization"] == "123" - assert response["X-Can-Edit"] == str(document.link_role == "editor") - assert response["X-User-Id"] == str(user.id) - - -@override_settings(COLLABORATION_SERVER_SECRET="123") -def test_api_documents_collaboration_auth_authenticated_restricted(): - """ - Authenticated users who are not related to a document should not be allowed to connect to the - collaboration server if this document's link reach is set to restricted. - """ - user = factories.UserFactory() - client = APIClient() - client.force_login(user) - - document = factories.DocumentFactory(link_reach="restricted") - - response = client.get( - "/api/v1.0/documents/collaboration-auth/", - HTTP_X_ORIGINAL_URL=f"http://localhost/ws/?room={document.pk}", - ) - - assert response.status_code == 403 - assert "Authorization" not in response - assert "X-Can-Edit" not in response - assert "X-User-Id" not in response - - -@override_settings(COLLABORATION_SERVER_SECRET="123") -@pytest.mark.parametrize("role", models.RoleChoices.values) -@pytest.mark.parametrize("via", VIA) -def test_api_documents_collaboration_auth_related(via, role, mock_user_teams): - """ - Users who have a specific access to a document, whatever the role, should be able to - connect to the collaboration server for this document. - """ - user = factories.UserFactory() - client = APIClient() - client.force_login(user) - - document = factories.DocumentFactory(link_reach="restricted") - if via == USER: - factories.UserDocumentAccessFactory(document=document, user=user, role=role) - elif via == TEAM: - mock_user_teams.return_value = ["lasuite", "unknown"] - factories.TeamDocumentAccessFactory( - document=document, team="lasuite", role=role - ) - - response = client.get( - "/api/v1.0/documents/collaboration-auth/", - HTTP_X_ORIGINAL_URL=f"http://localhost/ws/?room={document.pk}", - ) - - assert response.status_code == 200 - assert response["Authorization"] == "123" - assert response["X-Can-Edit"] == str(role != "reader") - assert response["X-User-Id"] == str(user.id) diff --git a/src/helm/impress/values.yaml b/src/helm/impress/values.yaml index fcf09730..eeb06224 100644 --- a/src/helm/impress/values.yaml +++ b/src/helm/impress/values.yaml @@ -75,15 +75,11 @@ ingressCollaborationWS: ## @param ingressCollaborationWS.customBackends Add custom backends to ingress customBackends: [] - ## @param ingressCollaborationWS.annotations.nginx.ingress.kubernetes.io/auth-response-headers - ## @param ingressCollaborationWS.annotations.nginx.ingress.kubernetes.io/auth-url ## @param ingressCollaborationWS.annotations.nginx.ingress.kubernetes.io/enable-websocket ## @param ingressCollaborationWS.annotations.nginx.ingress.kubernetes.io/proxy-read-timeout ## @param ingressCollaborationWS.annotations.nginx.ingress.kubernetes.io/proxy-send-timeout ## @param ingressCollaborationWS.annotations.nginx.ingress.kubernetes.io/upstream-hash-by annotations: - nginx.ingress.kubernetes.io/auth-response-headers: "Authorization, X-Can-Edit, X-User-Id" - nginx.ingress.kubernetes.io/auth-url: https://impress.example.com/api/v1.0/documents/collaboration-auth/ nginx.ingress.kubernetes.io/enable-websocket: "true" nginx.ingress.kubernetes.io/proxy-read-timeout: "86400" nginx.ingress.kubernetes.io/proxy-send-timeout: "86400"