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), - ), ] ), ),