From 3d55ed841db590f9a532372b684a3df2d30dd2f6 Mon Sep 17 00:00:00 2001 From: Manuel Raynaud Date: Thu, 13 Nov 2025 11:29:44 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=A5(backend)=20remove=20api=20managing?= =?UTF-8?q?=20templates?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A complete API was able to manage templates lifecycle, from the creation to the deletion and managing accesses on them. This API is not used by the frontend application, is not finished. A connected user can interact with this API and lead to unwanted behavior in the interface. Refering ot issue #1222 templates can maybe totaly remove in the future. While it's here and used, we only keep list and retrive endpoints. The template management can still be done in the admin interface. --- CHANGELOG.md | 4 + src/backend/core/api/viewsets.py | 97 --- .../templates/test_api_template_accesses.py | 799 ------------------ .../test_api_template_accesses_create.py | 206 ----- .../templates/test_api_templates_create.py | 6 +- .../templates/test_api_templates_delete.py | 68 +- .../templates/test_api_templates_update.py | 190 +---- src/backend/core/urls.py | 13 - 8 files changed, 16 insertions(+), 1367 deletions(-) delete mode 100644 src/backend/core/tests/templates/test_api_template_accesses.py delete mode 100644 src/backend/core/tests/templates/test_api_template_accesses_create.py diff --git a/CHANGELOG.md b/CHANGELOG.md index d6a5009531..c62ecf60a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,10 @@ and this project adheres to - 🐛(frontend) preserve @ character when esc is pressed after typing it #1512 - 🐛(frontend) fix pdf embed to use full width #1526 +### Removed + +- 🔥(backend) remove api managing templates + ## [3.9.0] - 2025-11-10 ### Added diff --git a/src/backend/core/api/viewsets.py b/src/backend/core/api/viewsets.py index 84402ceaae..43ca89ae3d 100644 --- a/src/backend/core/api/viewsets.py +++ b/src/backend/core/api/viewsets.py @@ -1831,10 +1831,7 @@ def perform_destroy(self, instance): class TemplateViewSet( - drf.mixins.CreateModelMixin, - drf.mixins.DestroyModelMixin, drf.mixins.RetrieveModelMixin, - drf.mixins.UpdateModelMixin, viewsets.GenericViewSet, ): """Template ViewSet""" @@ -1890,100 +1887,6 @@ def list(self, request, *args, **kwargs): serializer = self.get_serializer(queryset, many=True) return drf.response.Response(serializer.data) - @transaction.atomic - def perform_create(self, serializer): - """Set the current user as owner of the newly created object.""" - obj = serializer.save() - models.TemplateAccess.objects.create( - template=obj, - user=self.request.user, - role=models.RoleChoices.OWNER, - ) - - -class TemplateAccessViewSet( - ResourceAccessViewsetMixin, - drf.mixins.CreateModelMixin, - drf.mixins.DestroyModelMixin, - drf.mixins.RetrieveModelMixin, - drf.mixins.UpdateModelMixin, - viewsets.GenericViewSet, -): - """ - API ViewSet for all interactions with template accesses. - - GET /api/v1.0/templates//accesses/: - Return list of all template accesses related to the logged-in user or one - template access if an id is provided. - - POST /api/v1.0/templates//accesses/ with expected data: - - user: str - - role: str [administrator|editor|reader] - Return newly created template access - - PUT /api/v1.0/templates//accesses// with expected data: - - role: str [owner|admin|editor|reader] - Return updated template access - - PATCH /api/v1.0/templates//accesses// with expected data: - - role: str [owner|admin|editor|reader] - Return partially updated template access - - DELETE /api/v1.0/templates//accesses// - Delete targeted template access - """ - - lookup_field = "pk" - permission_classes = [permissions.ResourceAccessPermission] - throttle_scope = "template_access" - queryset = models.TemplateAccess.objects.select_related("user").all() - resource_field_name = "template" - serializer_class = serializers.TemplateAccessSerializer - - @cached_property - def template(self): - """Get related template from resource ID in url.""" - try: - return models.Template.objects.get(pk=self.kwargs["resource_id"]) - except models.Template.DoesNotExist as excpt: - raise drf.exceptions.NotFound() from excpt - - def list(self, request, *args, **kwargs): - """Restrict templates returned by the list endpoint""" - user = self.request.user - teams = user.teams - queryset = self.filter_queryset(self.get_queryset()) - - # Limit to resource access instances related to a resource THAT also has - # a resource access instance for the logged-in user (we don't want to list - # only the resource access instances pointing to the logged-in user) - queryset = queryset.filter( - db.Q(template__accesses__user=user) - | db.Q(template__accesses__team__in=teams), - ).distinct() - - serializer = self.get_serializer(queryset, many=True) - return drf.response.Response(serializer.data) - - def perform_create(self, serializer): - """ - Actually create the new template access: - - Ensures the `template_id` is explicitly set from the URL. - - If the assigned role is `OWNER`, checks that the requesting user is an owner - of the document. This is the only permission check deferred until this step; - all other access checks are handled earlier in the permission lifecycle. - """ - role = serializer.validated_data.get("role") - if ( - role == choices.RoleChoices.OWNER - and self.template.get_role(self.request.user) != choices.RoleChoices.OWNER - ): - raise drf.exceptions.PermissionDenied( - "Only owners of a template can assign other users as owners." - ) - - serializer.save(template_id=self.kwargs["resource_id"]) - class InvitationViewset( drf.mixins.CreateModelMixin, diff --git a/src/backend/core/tests/templates/test_api_template_accesses.py b/src/backend/core/tests/templates/test_api_template_accesses.py deleted file mode 100644 index 01bb6a121c..0000000000 --- a/src/backend/core/tests/templates/test_api_template_accesses.py +++ /dev/null @@ -1,799 +0,0 @@ -""" -Test template accesses API endpoints for users in impress's core app. -""" - -import random -from unittest import mock -from uuid import uuid4 - -import pytest -from rest_framework.test import APIClient - -from core import factories, models -from core.api import serializers -from core.tests.conftest import TEAM, USER, VIA - -pytestmark = pytest.mark.django_db - - -def test_api_template_accesses_list_anonymous(): - """Anonymous users should not be allowed to list template accesses.""" - template = factories.TemplateFactory() - factories.UserTemplateAccessFactory.create_batch(2, template=template) - - response = APIClient().get(f"/api/v1.0/templates/{template.id!s}/accesses/") - assert response.status_code == 401 - assert response.json() == { - "detail": "Authentication credentials were not provided." - } - - -def test_api_template_accesses_list_authenticated_unrelated(): - """ - Authenticated users should not be allowed to list template accesses for a template - to which they are not related. - """ - user = factories.UserFactory(with_owned_template=True) - - client = APIClient() - client.force_login(user) - - template = factories.TemplateFactory() - factories.UserTemplateAccessFactory.create_batch(3, template=template) - - # Accesses for other templates to which the user is related should not be listed either - other_access = factories.UserTemplateAccessFactory(user=user) - factories.UserTemplateAccessFactory(template=other_access.template) - - response = client.get( - f"/api/v1.0/templates/{template.id!s}/accesses/", - ) - assert response.status_code == 200 - assert response.json() == [] - - -@pytest.mark.parametrize("via", VIA) -def test_api_template_accesses_list_authenticated_related(via, mock_user_teams): - """ - Authenticated users should be able to list template accesses for a template - to which they are directly related, whatever their role in the template. - """ - user = factories.UserFactory(with_owned_template=True) - - client = APIClient() - client.force_login(user) - - template = factories.TemplateFactory() - user_access = None - if via == USER: - user_access = models.TemplateAccess.objects.create( - template=template, - user=user, - role=random.choice(models.RoleChoices.values), - ) - elif via == TEAM: - mock_user_teams.return_value = ["lasuite", "unknown"] - user_access = models.TemplateAccess.objects.create( - template=template, - team="lasuite", - role=random.choice(models.RoleChoices.values), - ) - - access1 = factories.TeamTemplateAccessFactory(template=template) - access2 = factories.UserTemplateAccessFactory(template=template) - - # Accesses for other templates to which the user is related should not be listed either - other_access = factories.UserTemplateAccessFactory(user=user) - factories.UserTemplateAccessFactory(template=other_access.template) - - response = client.get( - f"/api/v1.0/templates/{template.id!s}/accesses/", - ) - - assert response.status_code == 200 - content = response.json() - assert len(content) == 3 - assert sorted(content, key=lambda x: x["id"]) == sorted( - [ - { - "id": str(user_access.id), - "user": str(user.id) if via == "user" else None, - "team": "lasuite" if via == "team" else "", - "role": user_access.role, - "abilities": user_access.get_abilities(user), - }, - { - "id": str(access1.id), - "user": None, - "team": access1.team, - "role": access1.role, - "abilities": access1.get_abilities(user), - }, - { - "id": str(access2.id), - "user": str(access2.user.id), - "team": "", - "role": access2.role, - "abilities": access2.get_abilities(user), - }, - ], - key=lambda x: x["id"], - ) - - -def test_api_template_accesses_retrieve_anonymous(): - """ - Anonymous users should not be allowed to retrieve a template access. - """ - access = factories.UserTemplateAccessFactory() - - response = APIClient().get( - f"/api/v1.0/templates/{access.template_id!s}/accesses/{access.id!s}/", - ) - - assert response.status_code == 401 - assert response.json() == { - "detail": "Authentication credentials were not provided." - } - - -def test_api_template_accesses_retrieve_authenticated_unrelated(): - """ - Authenticated users should not be allowed to retrieve a template access for - a template to which they are not related. - """ - user = factories.UserFactory(with_owned_template=True) - - client = APIClient() - client.force_login(user) - - template = factories.TemplateFactory() - access = factories.UserTemplateAccessFactory(template=template) - - response = client.get( - f"/api/v1.0/templates/{template.id!s}/accesses/{access.id!s}/", - ) - assert response.status_code == 403 - assert response.json() == { - "detail": "You do not have permission to perform this action." - } - - # Accesses related to another template should be excluded even if the user is related to it - for access in [ - factories.UserTemplateAccessFactory(), - factories.UserTemplateAccessFactory(user=user), - ]: - response = client.get( - f"/api/v1.0/templates/{template.id!s}/accesses/{access.id!s}/", - ) - - assert response.status_code == 404 - assert response.json() == { - "detail": "No TemplateAccess matches the given query." - } - - -@pytest.mark.parametrize("via", VIA) -def test_api_template_accesses_retrieve_authenticated_related(via, mock_user_teams): - """ - A user who is related to a template should be allowed to retrieve the - associated template user accesses. - """ - user = factories.UserFactory(with_owned_template=True) - - client = APIClient() - client.force_login(user) - - template = factories.TemplateFactory() - if via == USER: - factories.UserTemplateAccessFactory(template=template, user=user) - elif via == TEAM: - mock_user_teams.return_value = ["lasuite", "unknown"] - factories.TeamTemplateAccessFactory(template=template, team="lasuite") - - access = factories.UserTemplateAccessFactory(template=template) - - response = client.get( - f"/api/v1.0/templates/{template.id!s}/accesses/{access.id!s}/", - ) - - assert response.status_code == 200 - assert response.json() == { - "id": str(access.id), - "user": str(access.user.id), - "team": "", - "role": access.role, - "abilities": access.get_abilities(user), - } - - -def test_api_template_accesses_update_anonymous(): - """Anonymous users should not be allowed to update a template access.""" - access = factories.UserTemplateAccessFactory() - old_values = serializers.TemplateAccessSerializer(instance=access).data - - new_values = { - "id": uuid4(), - "user": factories.UserFactory().id, - "role": random.choice(models.RoleChoices.values), - } - - api_client = APIClient() - for field, value in new_values.items(): - response = api_client.put( - f"/api/v1.0/templates/{access.template_id!s}/accesses/{access.id!s}/", - {**old_values, field: value}, - format="json", - ) - assert response.status_code == 401 - - access.refresh_from_db() - updated_values = serializers.TemplateAccessSerializer(instance=access).data - assert updated_values == old_values - - -def test_api_template_accesses_update_authenticated_unrelated(): - """ - Authenticated users should not be allowed to update a template access for a template to which - they are not related. - """ - user = factories.UserFactory(with_owned_template=True) - - client = APIClient() - client.force_login(user) - - access = factories.UserTemplateAccessFactory() - - old_values = serializers.TemplateAccessSerializer(instance=access).data - new_values = { - "id": uuid4(), - "user": factories.UserFactory().id, - "role": random.choice(models.RoleChoices.values), - } - - for field, value in new_values.items(): - response = client.put( - f"/api/v1.0/templates/{access.template_id!s}/accesses/{access.id!s}/", - {**old_values, field: value}, - format="json", - ) - assert response.status_code == 403 - - access.refresh_from_db() - updated_values = serializers.TemplateAccessSerializer(instance=access).data - assert updated_values == old_values - - -@pytest.mark.parametrize("role", ["reader", "editor"]) -@pytest.mark.parametrize("via", VIA) -def test_api_template_accesses_update_authenticated_editor_or_reader( - via, role, mock_user_teams -): - """Editors or readers of a template should not be allowed to update its accesses.""" - user = factories.UserFactory(with_owned_template=True) - - client = APIClient() - client.force_login(user) - - template = factories.TemplateFactory() - if via == USER: - factories.UserTemplateAccessFactory(template=template, user=user, role=role) - elif via == TEAM: - mock_user_teams.return_value = ["lasuite", "unknown"] - factories.TeamTemplateAccessFactory( - template=template, team="lasuite", role=role - ) - - access = factories.UserTemplateAccessFactory(template=template) - old_values = serializers.TemplateAccessSerializer(instance=access).data - - new_values = { - "id": uuid4(), - "user": factories.UserFactory().id, - "role": random.choice(models.RoleChoices.values), - } - - for field, value in new_values.items(): - response = client.put( - f"/api/v1.0/templates/{access.template_id!s}/accesses/{access.id!s}/", - {**old_values, field: value}, - format="json", - ) - assert response.status_code == 403 - - access.refresh_from_db() - updated_values = serializers.TemplateAccessSerializer(instance=access).data - assert updated_values == old_values - - -@pytest.mark.parametrize("via", VIA) -def test_api_template_accesses_update_administrator_except_owner(via, mock_user_teams): - """ - A user who is a direct administrator in a template should be allowed to update a user - access for this template, as long as they don't try to set the role to owner. - """ - user = factories.UserFactory(with_owned_template=True) - - client = APIClient() - client.force_login(user) - - template = factories.TemplateFactory() - if via == USER: - factories.UserTemplateAccessFactory( - template=template, user=user, role="administrator" - ) - elif via == TEAM: - mock_user_teams.return_value = ["lasuite", "unknown"] - factories.TeamTemplateAccessFactory( - template=template, team="lasuite", role="administrator" - ) - - access = factories.UserTemplateAccessFactory( - template=template, - role=random.choice(["administrator", "editor", "reader"]), - ) - - old_values = serializers.TemplateAccessSerializer(instance=access).data - new_values = { - "id": uuid4(), - "user_id": factories.UserFactory().id, - "role": random.choice(["administrator", "editor", "reader"]), - } - - for field, value in new_values.items(): - new_data = {**old_values, field: value} - response = client.put( - f"/api/v1.0/templates/{template.id!s}/accesses/{access.id!s}/", - data=new_data, - format="json", - ) - - if ( - new_data["role"] == old_values["role"] - ): # we are not really updating the role - assert response.status_code == 403 - else: - assert response.status_code == 200 - - access.refresh_from_db() - updated_values = serializers.TemplateAccessSerializer(instance=access).data - if field == "role": - assert updated_values == {**old_values, "role": new_values["role"]} - else: - assert updated_values == old_values - - -@pytest.mark.parametrize("via", VIA) -def test_api_template_accesses_update_administrator_from_owner(via, mock_user_teams): - """ - A user who is an administrator in a template, should not be allowed to update - the user access of an "owner" for this template. - """ - user = factories.UserFactory(with_owned_template=True) - - client = APIClient() - client.force_login(user) - - template = factories.TemplateFactory() - if via == USER: - factories.UserTemplateAccessFactory( - template=template, user=user, role="administrator" - ) - elif via == TEAM: - mock_user_teams.return_value = ["lasuite", "unknown"] - factories.TeamTemplateAccessFactory( - template=template, team="lasuite", role="administrator" - ) - - other_user = factories.UserFactory() - access = factories.UserTemplateAccessFactory( - template=template, user=other_user, role="owner" - ) - - old_values = serializers.TemplateAccessSerializer(instance=access).data - new_values = { - "id": uuid4(), - "user_id": factories.UserFactory().id, - "role": random.choice(models.RoleChoices.values), - } - - for field, value in new_values.items(): - response = client.put( - f"/api/v1.0/templates/{template.id!s}/accesses/{access.id!s}/", - data={**old_values, field: value}, - format="json", - ) - - assert response.status_code == 403 - access.refresh_from_db() - updated_values = serializers.TemplateAccessSerializer(instance=access).data - assert updated_values == old_values - - -@pytest.mark.parametrize("via", VIA) -def test_api_template_accesses_update_administrator_to_owner(via, mock_user_teams): - """ - A user who is an administrator in a template, should not be allowed to update - the user access of another user to grant template ownership. - """ - user = factories.UserFactory(with_owned_template=True) - - client = APIClient() - client.force_login(user) - - template = factories.TemplateFactory() - if via == USER: - factories.UserTemplateAccessFactory( - template=template, user=user, role="administrator" - ) - elif via == TEAM: - mock_user_teams.return_value = ["lasuite", "unknown"] - factories.TeamTemplateAccessFactory( - template=template, team="lasuite", role="administrator" - ) - - other_user = factories.UserFactory() - access = factories.UserTemplateAccessFactory( - template=template, - user=other_user, - role=random.choice(["administrator", "editor", "reader"]), - ) - - old_values = serializers.TemplateAccessSerializer(instance=access).data - new_values = { - "id": uuid4(), - "user_id": factories.UserFactory().id, - "role": "owner", - } - - for field, value in new_values.items(): - new_data = {**old_values, field: value} - response = client.put( - f"/api/v1.0/templates/{template.id!s}/accesses/{access.id!s}/", - data=new_data, - format="json", - ) - # We are not allowed or not really updating the role - if field == "role" or new_data["role"] == old_values["role"]: - assert response.status_code == 403 - else: - assert response.status_code == 200 - - access.refresh_from_db() - updated_values = serializers.TemplateAccessSerializer(instance=access).data - assert updated_values == old_values - - -@pytest.mark.parametrize("via", VIA) -def test_api_template_accesses_update_owner(via, mock_user_teams): - """ - A user who is an owner in a template should be allowed to update - a user access for this template whatever the role. - """ - user = factories.UserFactory(with_owned_template=True) - - client = APIClient() - client.force_login(user) - - template = factories.TemplateFactory() - if via == USER: - factories.UserTemplateAccessFactory(template=template, user=user, role="owner") - elif via == TEAM: - mock_user_teams.return_value = ["lasuite", "unknown"] - factories.TeamTemplateAccessFactory( - template=template, team="lasuite", role="owner" - ) - - factories.UserFactory() - access = factories.UserTemplateAccessFactory( - template=template, - ) - - old_values = serializers.TemplateAccessSerializer(instance=access).data - new_values = { - "id": uuid4(), - "user_id": factories.UserFactory().id, - "role": random.choice(models.RoleChoices.values), - } - - for field, value in new_values.items(): - new_data = {**old_values, field: value} - response = client.put( - f"/api/v1.0/templates/{template.id!s}/accesses/{access.id!s}/", - data=new_data, - format="json", - ) - - if ( - new_data["role"] == old_values["role"] - ): # we are not really updating the role - assert response.status_code == 403 - else: - assert response.status_code == 200 - - access.refresh_from_db() - updated_values = serializers.TemplateAccessSerializer(instance=access).data - - if field == "role": - assert updated_values == {**old_values, "role": new_values["role"]} - else: - assert updated_values == old_values - - -@pytest.mark.parametrize("via", VIA) -def test_api_template_accesses_update_owner_self(via, mock_user_teams): - """ - A user who is owner of a template should be allowed to update - their own user access provided there are other owners in the template. - """ - user = factories.UserFactory(with_owned_template=True) - - client = APIClient() - client.force_login(user) - - template = factories.TemplateFactory() - if via == TEAM: - mock_user_teams.return_value = ["lasuite", "unknown"] - access = factories.TeamTemplateAccessFactory( - template=template, team="lasuite", role="owner" - ) - else: - access = factories.UserTemplateAccessFactory( - template=template, user=user, role="owner" - ) - - old_values = serializers.TemplateAccessSerializer(instance=access).data - new_role = random.choice(["administrator", "editor", "reader"]) - - response = client.put( - f"/api/v1.0/templates/{template.id!s}/accesses/{access.id!s}/", - data={**old_values, "role": new_role}, - format="json", - ) - - assert response.status_code == 403 - access.refresh_from_db() - assert access.role == "owner" - - # Add another owner and it should now work - factories.UserTemplateAccessFactory(template=template, role="owner") - - response = client.put( - f"/api/v1.0/templates/{template.id!s}/accesses/{access.id!s}/", - data={**old_values, "role": new_role}, - format="json", - ) - - assert response.status_code == 200 - access.refresh_from_db() - assert access.role == new_role - - -# Delete - - -def test_api_template_accesses_delete_anonymous(): - """Anonymous users should not be allowed to destroy a template access.""" - access = factories.UserTemplateAccessFactory() - - response = APIClient().delete( - f"/api/v1.0/templates/{access.template_id!s}/accesses/{access.id!s}/", - ) - - assert response.status_code == 401 - assert models.TemplateAccess.objects.count() == 1 - - -def test_api_template_accesses_delete_authenticated(): - """ - Authenticated users should not be allowed to delete a template access for a - template to which they are not related. - """ - user = factories.UserFactory(with_owned_template=True) - - client = APIClient() - client.force_login(user) - - access = factories.UserTemplateAccessFactory() - - response = client.delete( - f"/api/v1.0/templates/{access.template_id!s}/accesses/{access.id!s}/", - ) - - assert response.status_code == 403 - assert models.TemplateAccess.objects.count() == 2 - - -@pytest.mark.parametrize("role", ["reader", "editor"]) -@pytest.mark.parametrize("via", VIA) -def test_api_template_accesses_delete_editor_or_reader(via, role, mock_user_teams): - """ - Authenticated users should not be allowed to delete a template access for a - template in which they are a simple editor or reader. - """ - user = factories.UserFactory(with_owned_template=True) - - client = APIClient() - client.force_login(user) - - template = factories.TemplateFactory() - if via == USER: - factories.UserTemplateAccessFactory(template=template, user=user, role=role) - elif via == TEAM: - mock_user_teams.return_value = ["lasuite", "unknown"] - factories.TeamTemplateAccessFactory( - template=template, team="lasuite", role=role - ) - - access = factories.UserTemplateAccessFactory(template=template) - - assert models.TemplateAccess.objects.count() == 3 - assert models.TemplateAccess.objects.filter(user=access.user).exists() - - response = client.delete( - f"/api/v1.0/templates/{template.id!s}/accesses/{access.id!s}/", - ) - - assert response.status_code == 403 - assert models.TemplateAccess.objects.count() == 3 - - -@pytest.mark.parametrize("via", VIA) -def test_api_template_accesses_delete_administrators_except_owners( - via, mock_user_teams -): - """ - Users who are administrators in a template should be allowed to delete an access - from the template provided it is not ownership. - """ - user = factories.UserFactory() - - client = APIClient() - client.force_login(user) - - template = factories.TemplateFactory() - if via == USER: - factories.UserTemplateAccessFactory( - template=template, user=user, role="administrator" - ) - elif via == TEAM: - mock_user_teams.return_value = ["lasuite", "unknown"] - factories.TeamTemplateAccessFactory( - template=template, team="lasuite", role="administrator" - ) - - access = factories.UserTemplateAccessFactory( - template=template, role=random.choice(["reader", "editor", "administrator"]) - ) - - assert models.TemplateAccess.objects.count() == 2 - assert models.TemplateAccess.objects.filter(user=access.user).exists() - - response = client.delete( - f"/api/v1.0/templates/{template.id!s}/accesses/{access.id!s}/", - ) - - assert response.status_code == 204 - assert models.TemplateAccess.objects.count() == 1 - - -@pytest.mark.parametrize("via", VIA) -def test_api_template_accesses_delete_administrator_on_owners(via, mock_user_teams): - """ - Users who are administrators in a template should not be allowed to delete an ownership - access from the template. - """ - user = factories.UserFactory(with_owned_template=True) - - client = APIClient() - client.force_login(user) - - template = factories.TemplateFactory() - if via == USER: - factories.UserTemplateAccessFactory( - template=template, user=user, role="administrator" - ) - elif via == TEAM: - mock_user_teams.return_value = ["lasuite", "unknown"] - factories.TeamTemplateAccessFactory( - template=template, team="lasuite", role="administrator" - ) - - access = factories.UserTemplateAccessFactory(template=template, role="owner") - - assert models.TemplateAccess.objects.count() == 3 - assert models.TemplateAccess.objects.filter(user=access.user).exists() - - response = client.delete( - f"/api/v1.0/templates/{template.id!s}/accesses/{access.id!s}/", - ) - - assert response.status_code == 403 - assert models.TemplateAccess.objects.count() == 3 - - -@pytest.mark.parametrize("via", VIA) -def test_api_template_accesses_delete_owners(via, mock_user_teams): - """ - Users should be able to delete the template access of another user - for a template of which they are owner. - """ - user = factories.UserFactory() - - client = APIClient() - client.force_login(user) - - template = factories.TemplateFactory() - if via == USER: - factories.UserTemplateAccessFactory(template=template, user=user, role="owner") - elif via == TEAM: - mock_user_teams.return_value = ["lasuite", "unknown"] - factories.TeamTemplateAccessFactory( - template=template, team="lasuite", role="owner" - ) - - access = factories.UserTemplateAccessFactory(template=template) - - assert models.TemplateAccess.objects.count() == 2 - assert models.TemplateAccess.objects.filter(user=access.user).exists() - - response = client.delete( - f"/api/v1.0/templates/{template.id!s}/accesses/{access.id!s}/", - ) - - assert response.status_code == 204 - assert models.TemplateAccess.objects.count() == 1 - - -@pytest.mark.parametrize("via", VIA) -def test_api_template_accesses_delete_owners_last_owner(via, mock_user_teams): - """ - It should not be possible to delete the last owner access from a template - """ - user = factories.UserFactory(with_owned_template=True) - - client = APIClient() - client.force_login(user) - - template = factories.TemplateFactory() - access = None - if via == USER: - access = factories.UserTemplateAccessFactory( - template=template, user=user, role="owner" - ) - elif via == TEAM: - mock_user_teams.return_value = ["lasuite", "unknown"] - access = factories.TeamTemplateAccessFactory( - template=template, team="lasuite", role="owner" - ) - - assert models.TemplateAccess.objects.count() == 2 - response = client.delete( - f"/api/v1.0/templates/{template.id!s}/accesses/{access.id!s}/", - ) - - assert response.status_code == 403 - assert models.TemplateAccess.objects.count() == 2 - - -def test_api_template_accesses_throttling(settings): - """Test api template accesses throttling.""" - current_rate = settings.REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"]["template_access"] - settings.REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"]["template_access"] = "2/minute" - template = factories.TemplateFactory() - user = factories.UserFactory() - factories.UserTemplateAccessFactory( - template=template, user=user, role="administrator" - ) - client = APIClient() - client.force_login(user) - for _i in range(2): - response = client.get(f"/api/v1.0/templates/{template.id!s}/accesses/") - assert response.status_code == 200 - with mock.patch("core.api.throttling.capture_message") as mock_capture_message: - response = client.get(f"/api/v1.0/templates/{template.id!s}/accesses/") - assert response.status_code == 429 - mock_capture_message.assert_called_once_with( - "Rate limit exceeded for scope template_access", "warning" - ) - settings.REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"]["template_access"] = current_rate diff --git a/src/backend/core/tests/templates/test_api_template_accesses_create.py b/src/backend/core/tests/templates/test_api_template_accesses_create.py deleted file mode 100644 index a33cd470a3..0000000000 --- a/src/backend/core/tests/templates/test_api_template_accesses_create.py +++ /dev/null @@ -1,206 +0,0 @@ -""" -Test template accesses create API endpoint for users in impress's core app. -""" - -import random - -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_template_accesses_create_anonymous(): - """Anonymous users should not be allowed to create template accesses.""" - template = factories.TemplateFactory() - - other_user = factories.UserFactory() - response = APIClient().post( - f"/api/v1.0/templates/{template.id!s}/accesses/", - { - "user": str(other_user.id), - "template": str(template.id), - "role": random.choice(models.RoleChoices.values), - }, - format="json", - ) - - assert response.status_code == 401 - assert response.json() == { - "detail": "Authentication credentials were not provided." - } - assert models.TemplateAccess.objects.exists() is False - - -def test_api_template_accesses_create_authenticated_unrelated(): - """ - Authenticated users should not be allowed to create template accesses for a template to - which they are not related. - """ - user = factories.UserFactory(with_owned_template=True) - - client = APIClient() - client.force_login(user) - - other_user = factories.UserFactory() - template = factories.TemplateFactory() - - response = client.post( - f"/api/v1.0/templates/{template.id!s}/accesses/", - { - "user": str(other_user.id), - }, - format="json", - ) - - assert response.status_code == 403 - assert not models.TemplateAccess.objects.filter(user=other_user).exists() - - -@pytest.mark.parametrize("role", ["reader", "editor"]) -@pytest.mark.parametrize("via", VIA) -def test_api_template_accesses_create_authenticated_editor_or_reader( - via, role, mock_user_teams -): - """Editors or readers of a template should not be allowed to create template accesses.""" - user = factories.UserFactory(with_owned_template=True) - - client = APIClient() - client.force_login(user) - - template = factories.TemplateFactory() - if via == USER: - factories.UserTemplateAccessFactory(template=template, user=user, role=role) - elif via == TEAM: - mock_user_teams.return_value = ["lasuite", "unknown"] - factories.TeamTemplateAccessFactory( - template=template, team="lasuite", role=role - ) - - other_user = factories.UserFactory() - - for new_role in [role[0] for role in models.RoleChoices.choices]: - response = client.post( - f"/api/v1.0/templates/{template.id!s}/accesses/", - { - "user": str(other_user.id), - "role": new_role, - }, - format="json", - ) - - assert response.status_code == 403 - - assert not models.TemplateAccess.objects.filter(user=other_user).exists() - - -@pytest.mark.parametrize("via", VIA) -def test_api_template_accesses_create_authenticated_administrator(via, mock_user_teams): - """ - Administrators of a template should be able to create template accesses - except for the "owner" role. - """ - user = factories.UserFactory(with_owned_template=True) - - client = APIClient() - client.force_login(user) - - template = factories.TemplateFactory() - if via == USER: - factories.UserTemplateAccessFactory( - template=template, user=user, role="administrator" - ) - elif via == TEAM: - mock_user_teams.return_value = ["lasuite", "unknown"] - factories.TeamTemplateAccessFactory( - template=template, team="lasuite", role="administrator" - ) - - other_user = factories.UserFactory() - - # It should not be allowed to create an owner access - response = client.post( - f"/api/v1.0/templates/{template.id!s}/accesses/", - { - "user": str(other_user.id), - "role": "owner", - }, - format="json", - ) - - assert response.status_code == 403 - assert response.json() == { - "detail": "Only owners of a template can assign other users as owners." - } - - # It should be allowed to create a lower access - role = random.choice( - [role[0] for role in models.RoleChoices.choices if role[0] != "owner"] - ) - - response = client.post( - f"/api/v1.0/templates/{template.id!s}/accesses/", - { - "user": str(other_user.id), - "role": role, - }, - format="json", - ) - - assert response.status_code == 201 - assert models.TemplateAccess.objects.filter(user=other_user).count() == 1 - new_template_access = models.TemplateAccess.objects.filter(user=other_user).get() - assert response.json() == { - "abilities": new_template_access.get_abilities(user), - "id": str(new_template_access.id), - "team": "", - "role": role, - "user": str(other_user.id), - } - - -@pytest.mark.parametrize("via", VIA) -def test_api_template_accesses_create_authenticated_owner(via, mock_user_teams): - """ - Owners of a template should be able to create template accesses whatever the role. - """ - user = factories.UserFactory(with_owned_template=True) - - client = APIClient() - client.force_login(user) - - template = factories.TemplateFactory() - if via == USER: - factories.UserTemplateAccessFactory(template=template, user=user, role="owner") - elif via == TEAM: - mock_user_teams.return_value = ["lasuite", "unknown"] - factories.TeamTemplateAccessFactory( - template=template, team="lasuite", role="owner" - ) - - other_user = factories.UserFactory() - - role = random.choice([role[0] for role in models.RoleChoices.choices]) - - response = client.post( - f"/api/v1.0/templates/{template.id!s}/accesses/", - { - "user": str(other_user.id), - "role": role, - }, - format="json", - ) - - assert response.status_code == 201 - assert models.TemplateAccess.objects.filter(user=other_user).count() == 1 - new_template_access = models.TemplateAccess.objects.filter(user=other_user).get() - assert response.json() == { - "id": str(new_template_access.id), - "user": str(other_user.id), - "team": "", - "role": role, - "abilities": new_template_access.get_abilities(user), - } diff --git a/src/backend/core/tests/templates/test_api_templates_create.py b/src/backend/core/tests/templates/test_api_templates_create.py index 75dddc828d..3935d0acce 100644 --- a/src/backend/core/tests/templates/test_api_templates_create.py +++ b/src/backend/core/tests/templates/test_api_templates_create.py @@ -42,7 +42,5 @@ def test_api_templates_create_authenticated(): format="json", ) - assert response.status_code == 201 - template = Template.objects.get() - assert template.title == "my template" - assert template.accesses.filter(role="owner", user=user).exists() + assert response.status_code == 405 + assert not Template.objects.exists() diff --git a/src/backend/core/tests/templates/test_api_templates_delete.py b/src/backend/core/tests/templates/test_api_templates_delete.py index 5c4005e422..8db834de36 100644 --- a/src/backend/core/tests/templates/test_api_templates_delete.py +++ b/src/backend/core/tests/templates/test_api_templates_delete.py @@ -8,7 +8,6 @@ from rest_framework.test import APIClient from core import factories, models -from core.tests.conftest import TEAM, USER, VIA pytestmark = pytest.mark.django_db @@ -25,7 +24,7 @@ def test_api_templates_delete_anonymous(): assert models.Template.objects.count() == 1 -def test_api_templates_delete_authenticated_unrelated(): +def test_api_templates_delete_not_implemented(): """ Authenticated users should not be allowed to delete a template to which they are not related. @@ -36,72 +35,11 @@ def test_api_templates_delete_authenticated_unrelated(): client.force_login(user) is_public = random.choice([True, False]) - template = factories.TemplateFactory(is_public=is_public) + template = factories.TemplateFactory(is_public=is_public, users=[(user, "owner")]) response = client.delete( f"/api/v1.0/templates/{template.id!s}/", ) - assert response.status_code == 403 if is_public else 404 + assert response.status_code == 405 assert models.Template.objects.count() == 1 - - -@pytest.mark.parametrize("role", ["reader", "editor", "administrator"]) -@pytest.mark.parametrize("via", VIA) -def test_api_templates_delete_authenticated_member_or_administrator( - via, role, mock_user_teams -): - """ - Authenticated users should not be allowed to delete a template for which they are - only a member or administrator. - """ - user = factories.UserFactory() - - client = APIClient() - client.force_login(user) - - template = factories.TemplateFactory() - if via == USER: - factories.UserTemplateAccessFactory(template=template, user=user, role=role) - elif via == TEAM: - mock_user_teams.return_value = ["lasuite", "unknown"] - factories.TeamTemplateAccessFactory( - template=template, team="lasuite", role=role - ) - - response = client.delete( - f"/api/v1.0/templates/{template.id}/", - ) - - assert response.status_code == 403 - assert response.json() == { - "detail": "You do not have permission to perform this action." - } - assert models.Template.objects.count() == 1 - - -@pytest.mark.parametrize("via", VIA) -def test_api_templates_delete_authenticated_owner(via, mock_user_teams): - """ - Authenticated users should be able to delete a template they own. - """ - user = factories.UserFactory() - - client = APIClient() - client.force_login(user) - - template = factories.TemplateFactory() - if via == USER: - factories.UserTemplateAccessFactory(template=template, user=user, role="owner") - elif via == TEAM: - mock_user_teams.return_value = ["lasuite", "unknown"] - factories.TeamTemplateAccessFactory( - template=template, team="lasuite", role="owner" - ) - - response = client.delete( - f"/api/v1.0/templates/{template.id}/", - ) - - assert response.status_code == 204 - assert models.Template.objects.exists() is False diff --git a/src/backend/core/tests/templates/test_api_templates_update.py b/src/backend/core/tests/templates/test_api_templates_update.py index 7c5a27c6bf..a20402937a 100644 --- a/src/backend/core/tests/templates/test_api_templates_update.py +++ b/src/backend/core/tests/templates/test_api_templates_update.py @@ -2,14 +2,11 @@ Tests for Templates API endpoint in impress's core app: update """ -import random - import pytest from rest_framework.test import APIClient from core import factories from core.api import serializers -from core.tests.conftest import TEAM, USER, VIA pytestmark = pytest.mark.django_db @@ -17,7 +14,6 @@ def test_api_templates_update_anonymous(): """Anonymous users should not be allowed to update a template.""" template = factories.TemplateFactory() - old_template_values = serializers.TemplateSerializer(instance=template).data new_template_values = serializers.TemplateSerializer( instance=factories.TemplateFactory() @@ -28,145 +24,18 @@ def test_api_templates_update_anonymous(): format="json", ) assert response.status_code == 401 - assert response.json() == { - "detail": "Authentication credentials were not provided." - } - - template.refresh_from_db() - template_values = serializers.TemplateSerializer(instance=template).data - assert template_values == old_template_values -def test_api_templates_update_authenticated_unrelated(): +def test_api_templates_update_not_implemented(): """ - Authenticated users should not be allowed to update a template to which they are not related. + Authenticated users should not be allowed to update a template. """ user = factories.UserFactory() client = APIClient() client.force_login(user) - template = factories.TemplateFactory(is_public=False) - old_template_values = serializers.TemplateSerializer(instance=template).data - - new_template_values = serializers.TemplateSerializer( - instance=factories.TemplateFactory() - ).data - response = client.put( - f"/api/v1.0/templates/{template.id!s}/", - new_template_values, - format="json", - ) - - assert response.status_code == 403 - assert response.json() == { - "detail": "You do not have permission to perform this action." - } - - template.refresh_from_db() - template_values = serializers.TemplateSerializer(instance=template).data - assert template_values == old_template_values - - -@pytest.mark.parametrize("via", VIA) -def test_api_templates_update_authenticated_readers(via, mock_user_teams): - """ - Users who are readers of a template should not be allowed to update it. - """ - user = factories.UserFactory() - - client = APIClient() - client.force_login(user) - - template = factories.TemplateFactory() - if via == USER: - factories.UserTemplateAccessFactory(template=template, user=user, role="reader") - elif via == TEAM: - mock_user_teams.return_value = ["lasuite", "unknown"] - factories.TeamTemplateAccessFactory( - template=template, team="lasuite", role="reader" - ) - - old_template_values = serializers.TemplateSerializer(instance=template).data - - new_template_values = serializers.TemplateSerializer( - instance=factories.TemplateFactory() - ).data - response = client.put( - f"/api/v1.0/templates/{template.id!s}/", - new_template_values, - format="json", - ) - - assert response.status_code == 403 - assert response.json() == { - "detail": "You do not have permission to perform this action." - } - - template.refresh_from_db() - template_values = serializers.TemplateSerializer(instance=template).data - assert template_values == old_template_values - - -@pytest.mark.parametrize("role", ["editor", "administrator", "owner"]) -@pytest.mark.parametrize("via", VIA) -def test_api_templates_update_authenticated_editor_or_administrator_or_owner( - via, role, mock_user_teams -): - """Administrator or owner of a template should be allowed to update it.""" - user = factories.UserFactory() - - client = APIClient() - client.force_login(user) - - template = factories.TemplateFactory() - if via == USER: - factories.UserTemplateAccessFactory(template=template, user=user, role=role) - elif via == TEAM: - mock_user_teams.return_value = ["lasuite", "unknown"] - factories.TeamTemplateAccessFactory( - template=template, team="lasuite", role=role - ) - - old_template_values = serializers.TemplateSerializer(instance=template).data - - new_template_values = serializers.TemplateSerializer( - instance=factories.TemplateFactory() - ).data - response = client.put( - f"/api/v1.0/templates/{template.id!s}/", - new_template_values, - format="json", - ) - assert response.status_code == 200 - - template.refresh_from_db() - template_values = serializers.TemplateSerializer(instance=template).data - for key, value in template_values.items(): - if key in ["id", "accesses"]: - assert value == old_template_values[key] - else: - assert value == new_template_values[key] - - -@pytest.mark.parametrize("via", VIA) -def test_api_templates_update_authenticated_owners(via, mock_user_teams): - """Administrators of a template should be allowed to update it.""" - user = factories.UserFactory() - - client = APIClient() - client.force_login(user) - - template = factories.TemplateFactory() - if via == USER: - factories.UserTemplateAccessFactory(template=template, user=user, role="owner") - elif via == TEAM: - mock_user_teams.return_value = ["lasuite", "unknown"] - factories.TeamTemplateAccessFactory( - template=template, team="lasuite", role="owner" - ) - - old_template_values = serializers.TemplateSerializer(instance=template).data + template = factories.TemplateFactory(users=[(user, "owner")]) new_template_values = serializers.TemplateSerializer( instance=factories.TemplateFactory() @@ -176,55 +45,10 @@ def test_api_templates_update_authenticated_owners(via, mock_user_teams): f"/api/v1.0/templates/{template.id!s}/", new_template_values, format="json" ) - assert response.status_code == 200 - template.refresh_from_db() - template_values = serializers.TemplateSerializer(instance=template).data - for key, value in template_values.items(): - if key in ["id", "accesses"]: - assert value == old_template_values[key] - else: - assert value == new_template_values[key] + assert response.status_code == 405 - -@pytest.mark.parametrize("via", VIA) -def test_api_templates_update_administrator_or_owner_of_another(via, mock_user_teams): - """ - Being administrator or owner of a template should not grant authorization to update - another template. - """ - user = factories.UserFactory() - - client = APIClient() - client.force_login(user) - - template = factories.TemplateFactory() - if via == USER: - factories.UserTemplateAccessFactory( - template=template, user=user, role=random.choice(["administrator", "owner"]) - ) - elif via == TEAM: - mock_user_teams.return_value = ["lasuite", "unknown"] - factories.TeamTemplateAccessFactory( - template=template, - team="lasuite", - role=random.choice(["administrator", "owner"]), - ) - - is_public = random.choice([True, False]) - template = factories.TemplateFactory(title="Old title", is_public=is_public) - old_template_values = serializers.TemplateSerializer(instance=template).data - - new_template_values = serializers.TemplateSerializer( - instance=factories.TemplateFactory() - ).data - response = client.put( - f"/api/v1.0/templates/{template.id!s}/", - new_template_values, - format="json", + response = client.patch( + f"/api/v1.0/templates/{template.id!s}/", new_template_values, format="json" ) - assert response.status_code == 403 if is_public else 404 - - template.refresh_from_db() - template_values = serializers.TemplateSerializer(instance=template).data - assert template_values == old_template_values + assert response.status_code == 405 diff --git a/src/backend/core/urls.py b/src/backend/core/urls.py index 2ad8b00395..a55795ae4e 100644 --- a/src/backend/core/urls.py +++ b/src/backend/core/urls.py @@ -34,15 +34,6 @@ ) -# - Routes nested under a template -template_related_router = DefaultRouter() -template_related_router.register( - "accesses", - viewsets.TemplateAccessViewSet, - basename="template_accesses", -) - - urlpatterns = [ path( f"api/{settings.API_VERSION}/", @@ -54,10 +45,6 @@ r"^documents/(?P[0-9a-z-]*)/", include(document_related_router.urls), ), - re_path( - r"^templates/(?P[0-9a-z-]*)/", - include(template_related_router.urls), - ), ] ), ),