From d33286019cfd4d73fe23a395151824117262adb6 Mon Sep 17 00:00:00 2001 From: Manuel Raynaud Date: Wed, 18 Jun 2025 15:50:12 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8(back)=20accept=20for=20a=20owner=20th?= =?UTF-8?q?e=20request=20to=20access=20a=20document?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add the action accepting a request to access a document. It is possible to override the role from the request and also update an existing DocumentAccess --- src/backend/core/api/serializers.py | 17 +- src/backend/core/api/viewsets.py | 11 ++ src/backend/core/models.py | 14 ++ .../test_api_documents_ask_for_access.py | 172 ++++++++++++++++++ 4 files changed, 212 insertions(+), 2 deletions(-) diff --git a/src/backend/core/api/serializers.py b/src/backend/core/api/serializers.py index e08bae1c..8a0d3d9e 100644 --- a/src/backend/core/api/serializers.py +++ b/src/backend/core/api/serializers.py @@ -665,10 +665,22 @@ class InvitationSerializer(serializers.ModelSerializer): return role +class RoleSerializer(serializers.Serializer): + """Serializer validating role choices.""" + + role = serializers.ChoiceField( + choices=models.RoleChoices.choices, required=False, allow_null=True + ) + + class DocumentAskForAccessCreateSerializer(serializers.Serializer): """Serializer for creating a document ask for access.""" - role = serializers.ChoiceField(choices=models.RoleChoices.choices, required=False, default=models.RoleChoices.READER) + role = serializers.ChoiceField( + choices=models.RoleChoices.choices, + required=False, + default=models.RoleChoices.READER, + ) class DocumentAskForAccessSerializer(serializers.ModelSerializer): @@ -695,7 +707,8 @@ class DocumentAskForAccessSerializer(serializers.ModelSerializer): if request: return invitation.get_abilities(request.user) return {} - + + class VersionFilterSerializer(serializers.Serializer): """Validate version filters applied to the list endpoint.""" diff --git a/src/backend/core/api/viewsets.py b/src/backend/core/api/viewsets.py index 5ba51e4d..093cf14b 100644 --- a/src/backend/core/api/viewsets.py +++ b/src/backend/core/api/viewsets.py @@ -1837,6 +1837,17 @@ class DocumentAskForAccessViewSet( return drf.response.Response(status=drf.status.HTTP_201_CREATED) + @drf.decorators.action(detail=True, methods=["post"]) + def accept(self, request, *args, **kwargs): + """Accept a document ask for access resource.""" + document_ask_for_access = self.get_object() + + serializer = serializers.RoleSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + + document_ask_for_access.accept(role=serializer.validated_data.get("role")) + return drf.response.Response(status=drf.status.HTTP_204_NO_CONTENT) + class ConfigView(drf.views.APIView): """API ViewSet for sharing some public settings.""" diff --git a/src/backend/core/models.py b/src/backend/core/models.py index ca9f4b19..f137802d 100644 --- a/src/backend/core/models.py +++ b/src/backend/core/models.py @@ -1205,8 +1205,22 @@ class DocumentAskForAccess(BaseModel): "update": is_admin_or_owner, "partial_update": is_admin_or_owner, "retrieve": is_admin_or_owner, + "accept": is_admin_or_owner, } + def accept(self, role=None): + """Accept a document ask for access resource.""" + if role is None: + role = self.role + + DocumentAccess.objects.update_or_create( + document=self.document, + user=self.user, + defaults={"role": role}, + create_defaults={"role": role}, + ) + self.delete() + class Template(BaseModel): """HTML and CSS code used for formatting the print around the MarkDown body.""" diff --git a/src/backend/core/tests/documents/test_api_documents_ask_for_access.py b/src/backend/core/tests/documents/test_api_documents_ask_for_access.py index facaab2e..f8f03189 100644 --- a/src/backend/core/tests/documents/test_api_documents_ask_for_access.py +++ b/src/backend/core/tests/documents/test_api_documents_ask_for_access.py @@ -190,6 +190,7 @@ def test_api_documents_ask_for_access_list_authenticated_own_request(): "+00:00", "Z" ), "abilities": { + "accept": False, "destroy": False, "update": False, "partial_update": False, @@ -277,6 +278,7 @@ def test_api_documents_ask_for_access_list_owner_or_admin(role): "+00:00", "Z" ), "abilities": { + "accept": True, "destroy": True, "update": True, "partial_update": True, @@ -365,6 +367,7 @@ def test_api_documents_ask_for_access_retrieve_owner_or_admin(role): "+00:00", "Z" ), "abilities": { + "accept": True, "destroy": True, "update": True, "partial_update": True, @@ -443,3 +446,172 @@ def test_api_documents_ask_for_access_delete_owner_or_admin(role): assert not DocumentAskForAccess.objects.filter( id=document_ask_for_access.id ).exists() + + +## Accept + + +def test_api_documents_ask_for_access_accept_anonymous(): + """Anonymous users should not be able to accept document ask for access.""" + document = DocumentFactory() + document_ask_for_access = DocumentAskForAccessFactory( + document=document, role=RoleChoices.READER + ) + + client = APIClient() + response = client.post( + f"/api/v1.0/documents/{document.id}/ask-for-access/{document_ask_for_access.id}/accept/" + ) + assert response.status_code == 401 + + +def test_api_documents_ask_for_access_accept_authenticated(): + """Authenticated users should not be able to accept document ask for access.""" + document = DocumentFactory() + document_ask_for_access = DocumentAskForAccessFactory( + document=document, role=RoleChoices.READER + ) + + client = APIClient() + client.force_login(UserFactory()) + response = client.post( + f"/api/v1.0/documents/{document.id}/ask-for-access/{document_ask_for_access.id}/accept/" + ) + assert response.status_code == 404 + + +@pytest.mark.parametrize("role", [RoleChoices.READER, RoleChoices.EDITOR]) +def test_api_documents_ask_for_access_accept_authenticated_non_owner_or_admin(role): + """Non owner or admin users should not be able to accept document ask for access.""" + user = UserFactory() + document = DocumentFactory(users=[(user, role)]) + document_ask_for_access = DocumentAskForAccessFactory( + document=document, role=RoleChoices.READER + ) + + client = APIClient() + client.force_login(user) + + response = client.post( + f"/api/v1.0/documents/{document.id}/ask-for-access/{document_ask_for_access.id}/accept/" + ) + assert response.status_code == 404 + + +@pytest.mark.parametrize("role", [RoleChoices.OWNER, RoleChoices.ADMIN]) +def test_api_documents_ask_for_access_accept_owner_or_admin(role): + """Owner or admin users should be able to accept document ask for access.""" + user = UserFactory() + document = DocumentFactory(users=[(user, role)]) + document_ask_for_access = DocumentAskForAccessFactory( + document=document, role=RoleChoices.READER + ) + + client = APIClient() + client.force_login(user) + + response = client.post( + f"/api/v1.0/documents/{document.id}/ask-for-access/{document_ask_for_access.id}/accept/" + ) + assert response.status_code == 204 + + assert not DocumentAskForAccess.objects.filter( + id=document_ask_for_access.id + ).exists() + assert DocumentAccess.objects.filter( + document=document, user=document_ask_for_access.user, role=RoleChoices.READER + ).exists() + + +@pytest.mark.parametrize("role", [RoleChoices.OWNER, RoleChoices.ADMIN]) +def test_api_documents_ask_for_access_accept_authenticated_specific_role(role): + """ + Owner or admin users should be able to accept document ask for access with a specific role. + """ + user = UserFactory() + document = DocumentFactory(users=[(user, role)]) + document_ask_for_access = DocumentAskForAccessFactory( + document=document, role=RoleChoices.READER + ) + + client = APIClient() + client.force_login(user) + + response = client.post( + f"/api/v1.0/documents/{document.id}/ask-for-access/{document_ask_for_access.id}/accept/", + data={"role": RoleChoices.EDITOR}, + ) + assert response.status_code == 204 + + assert not DocumentAskForAccess.objects.filter( + id=document_ask_for_access.id + ).exists() + assert DocumentAccess.objects.filter( + document=document, user=document_ask_for_access.user, role=RoleChoices.EDITOR + ).exists() + + +@pytest.mark.parametrize("role", [RoleChoices.OWNER, RoleChoices.ADMIN]) +def test_api_documents_ask_for_access_accept_authenticated_owner_or_admin_update_access( + role, +): + """ + Owner or admin users should be able to accept document ask for access and update the access. + """ + user = UserFactory() + document = DocumentFactory(users=[(user, role)]) + document_access = UserDocumentAccessFactory( + document=document, role=RoleChoices.READER + ) + document_ask_for_access = DocumentAskForAccessFactory( + document=document, user=document_access.user, role=RoleChoices.EDITOR + ) + + client = APIClient() + client.force_login(user) + + response = client.post( + f"/api/v1.0/documents/{document.id}/ask-for-access/{document_ask_for_access.id}/accept/", + data={"role": RoleChoices.EDITOR}, + ) + assert response.status_code == 204 + + assert not DocumentAskForAccess.objects.filter( + id=document_ask_for_access.id + ).exists() + document_access.refresh_from_db() + assert document_access.role == RoleChoices.EDITOR + + +@pytest.mark.parametrize("role", [RoleChoices.OWNER, RoleChoices.ADMIN]) +# pylint: disable=line-too-long +def test_api_documents_ask_for_access_accept_authenticated_owner_or_admin_update_access_with_specific_role( + role, +): + """ + Owner or admin users should be able to accept document ask for access and update the access + with a specific role. + """ + user = UserFactory() + document = DocumentFactory(users=[(user, role)]) + document_access = UserDocumentAccessFactory( + document=document, role=RoleChoices.READER + ) + document_ask_for_access = DocumentAskForAccessFactory( + document=document, user=document_access.user, role=RoleChoices.EDITOR + ) + + client = APIClient() + client.force_login(user) + + response = client.post( + f"/api/v1.0/documents/{document.id}/ask-for-access/{document_ask_for_access.id}/accept/", + data={"role": RoleChoices.ADMIN}, + ) + assert response.status_code == 204 + + assert not DocumentAskForAccess.objects.filter( + id=document_ask_for_access.id + ).exists() + document_access.refresh_from_db() + assert document_access.role == RoleChoices.ADMIN