diff --git a/CHANGELOG.md b/CHANGELOG.md index 38af7429f..e30d5aaea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ and this project adheres to - ✨(domains) domain accesses update API #423 - ✨(backend) domain accesses create API #428 - 🥅(frontend) catch new errors on mailbox creation #392 +- ✨(api) domain accesses delete API #433 ### Fixed diff --git a/src/backend/mailbox_manager/api/permissions.py b/src/backend/mailbox_manager/api/permissions.py index 597754e33..b5c1c9d60 100644 --- a/src/backend/mailbox_manager/api/permissions.py +++ b/src/backend/mailbox_manager/api/permissions.py @@ -21,3 +21,12 @@ def has_permission(self, request, view): domain = models.MailDomain.objects.get(slug=view.kwargs.get("domain_slug", "")) abilities = domain.get_abilities(request.user) return abilities.get(request.method.lower(), False) + + +class MailDomainAccessRolePermission(core_permissions.IsAuthenticated): + """Permission class to manage mailboxes for a mail domain""" + + def has_object_permission(self, request, view, obj): + """Check permission for a given object.""" + abilities = obj.get_abilities(request.user) + return abilities.get(request.method.lower(), False) diff --git a/src/backend/mailbox_manager/api/viewsets.py b/src/backend/mailbox_manager/api/viewsets.py index b2fcbb67f..c0e8a89cb 100644 --- a/src/backend/mailbox_manager/api/viewsets.py +++ b/src/backend/mailbox_manager/api/viewsets.py @@ -3,7 +3,6 @@ from django.db.models import Subquery from rest_framework import exceptions, filters, mixins, viewsets -from rest_framework import permissions as drf_permissions from core import models as core_models @@ -61,6 +60,7 @@ class MailDomainAccessViewSet( mixins.CreateModelMixin, mixins.UpdateModelMixin, mixins.RetrieveModelMixin, + mixins.DestroyModelMixin, ): """ API ViewSet for all interactions with mail domain accesses. @@ -81,9 +81,12 @@ class MailDomainAccessViewSet( PATCH /api/v1.0/mail-domains//accesses// with expected data: - role: str [owner|admin|viewer] Return partially updated domain access + + DELETE /api/v1.0/mail-domains//accesses// + Delete targeted domain access """ - permission_classes = [drf_permissions.IsAuthenticated] + permission_classes = [permissions.MailDomainAccessRolePermission] serializer_class = serializers.MailDomainAccessSerializer filter_backends = [filters.OrderingFilter] ordering_fields = ["role", "user__email", "user__name"] @@ -156,6 +159,22 @@ def perform_update(self, serializer): raise exceptions.PermissionDenied({"role": message}) serializer.save() + def destroy(self, request, *args, **kwargs): + """Forbid deleting the last owner access""" + instance = self.get_object() + domain = instance.domain + + # Check if the access being deleted is the last owner access for the domain + if ( + instance.role == enums.MailDomainRoleChoices.OWNER + and domain.accesses.filter(role=enums.MailDomainRoleChoices.OWNER).count() + == 1 + ): + message = "Cannot delete the last owner access for the domain." + raise exceptions.PermissionDenied({"detail": message}) + + return super().destroy(request, *args, **kwargs) + class MailBoxViewSet( mixins.CreateModelMixin, diff --git a/src/backend/mailbox_manager/models.py b/src/backend/mailbox_manager/models.py index d9af148ec..53e740326 100644 --- a/src/backend/mailbox_manager/models.py +++ b/src/backend/mailbox_manager/models.py @@ -136,6 +136,31 @@ def get_can_set_role_to(self, user): roles.remove(self.role) return sorted(roles) + def get_abilities(self, user): + """ + Compute and return abilities for a given user on the domain access. + """ + role = None + + if user.is_authenticated: + try: + role = user.mail_domain_accesses.filter(domain=self.domain).get().role + except (MailDomainAccess.DoesNotExist, IndexError): + role = None + + is_owner_or_admin = role in [ + MailDomainRoleChoices.OWNER, + MailDomainRoleChoices.ADMIN, + ] + + return { + "get": bool(role), + "patch": is_owner_or_admin, + "put": is_owner_or_admin, + "post": is_owner_or_admin, + "delete": is_owner_or_admin, + } + class Mailbox(BaseModel): """Mailboxes for users from mail domain.""" diff --git a/src/backend/mailbox_manager/tests/api/mail_domain_accesses/test_api_mail_domain_accesses_delete.py b/src/backend/mailbox_manager/tests/api/mail_domain_accesses/test_api_mail_domain_accesses_delete.py new file mode 100644 index 000000000..1c7dee3ff --- /dev/null +++ b/src/backend/mailbox_manager/tests/api/mail_domain_accesses/test_api_mail_domain_accesses_delete.py @@ -0,0 +1,139 @@ +""" +Test for mail_domain accesses API endpoints in People's core app : delete +""" + +import pytest +from rest_framework import status +from rest_framework.test import APIClient + +from core import factories as core_factories + +from mailbox_manager import enums, factories, models + +pytestmark = pytest.mark.django_db + + +def test_api_mail_domain__accesses_delete_anonymous(): + """Anonymous users should not be allowed to destroy a mail domain access.""" + access = factories.MailDomainAccessFactory() + + response = APIClient().delete( + f"/api/v1.0/mail-domains/{access.domain.slug}/accesses/{access.id!s}/", + ) + + assert response.status_code == status.HTTP_401_UNAUTHORIZED + assert models.MailDomainAccess.objects.count() == 1 + + +def test_api_mail_domain__accesses_delete_authenticated(): + """ + Authenticated users should not be allowed to delete a mail domain access for a + mail domain to which they are not related. + """ + authenticated_user = core_factories.UserFactory() + access = factories.MailDomainAccessFactory() + + client = APIClient() + client.force_login(authenticated_user) + response = client.delete( + f"/api/v1.0/mail-domains/{access.domain.slug}/accesses/{access.id!s}/", + ) + + assert response.status_code == status.HTTP_403_FORBIDDEN + assert models.MailDomainAccess.objects.count() == 1 + + +def test_api_mail_domain__accesses_delete_viewer(): + """ + Authenticated users should not be allowed to delete a mail domain access for a + mail domain in which they are a simple viewer. + """ + authenticated_user = core_factories.UserFactory() + mail_domain = factories.MailDomainFactory( + users=[(authenticated_user, enums.MailDomainRoleChoices.VIEWER)] + ) + access = factories.MailDomainAccessFactory(domain=mail_domain) + + client = APIClient() + client.force_login(authenticated_user) + response = client.delete( + f"/api/v1.0/mail-domains/{mail_domain.slug}/accesses/{access.id!s}/", + ) + + assert response.status_code == status.HTTP_403_FORBIDDEN + assert models.MailDomainAccess.objects.count() == 2 + assert models.MailDomainAccess.objects.filter(user=access.user).exists() + + +def test_api_mail_domain__accesses_delete_administrators(): + """ + Administrators of a mail domain should be allowed to delete accesses excepted owner accesses. + """ + authenticated_user = core_factories.UserFactory() + mail_domain = factories.MailDomainFactory( + users=[(authenticated_user, enums.MailDomainRoleChoices.ADMIN)] + ) + for role in [enums.MailDomainRoleChoices.VIEWER, enums.MailDomainRoleChoices.ADMIN]: + access = factories.MailDomainAccessFactory(domain=mail_domain, role=role) + + assert models.MailDomainAccess.objects.count() == 2 + assert models.MailDomainAccess.objects.filter(user=access.user).exists() + + client = APIClient() + client.force_login(authenticated_user) + response = client.delete( + f"/api/v1.0/mail-domains/{mail_domain.slug}/accesses/{access.id!s}/", + ) + + assert response.status_code == status.HTTP_204_NO_CONTENT + assert models.MailDomainAccess.objects.count() == 1 + + +def test_api_mail_domain__accesses_delete_owners(): + """ + An owner should be able to delete the mail domain access of another user including + a owner access. + """ + authenticated_user = core_factories.UserFactory() + mail_domain = factories.MailDomainFactory( + users=[(authenticated_user, enums.MailDomainRoleChoices.OWNER)] + ) + for role in [role[0] for role in enums.MailDomainRoleChoices.choices]: + access = factories.MailDomainAccessFactory(domain=mail_domain, role=role) + + assert models.MailDomainAccess.objects.count() == 2 + assert models.MailDomainAccess.objects.filter(user=access.user).exists() + + client = APIClient() + client.force_login(authenticated_user) + response = client.delete( + f"/api/v1.0/mail-domains/{mail_domain.slug}/accesses/{access.id!s}/", + ) + + assert response.status_code == status.HTTP_204_NO_CONTENT + assert models.MailDomainAccess.objects.count() == 1 + + +def test_api_mail_domain__accesses_delete_owners_last_owner(): + """ + It should not be possible to delete the last owner access from a mail domain + """ + authenticated_user = core_factories.UserFactory() + mail_domain = factories.MailDomainFactory() + access = factories.MailDomainAccessFactory( + domain=mail_domain, + user=authenticated_user, + role=enums.MailDomainRoleChoices.OWNER, + ) + factories.MailDomainAccessFactory.create_batch(9) + assert models.MailDomainAccess.objects.count() == 10 + + client = APIClient() + client.force_login(authenticated_user) + + response = client.delete( + f"/api/v1.0/mail-domains/{mail_domain.slug}/accesses/{access.id!s}/", + ) + + assert response.status_code == status.HTTP_403_FORBIDDEN + assert models.MailDomainAccess.objects.count() == 10