From ccb06b3abf62d64a2dcce77c02ac98dd657a0823 Mon Sep 17 00:00:00 2001 From: Sabrina Demagny Date: Fri, 22 Nov 2024 19:49:27 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8(mailbox)=20allow=20to=20disable=20mai?= =?UTF-8?q?lbox?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We send a request to dimail API and change mailbox status to disabled. A disabled mailbox can no longer be used thus access to webmail is disabled for user. --- CHANGELOG.md | 1 + .../locale/fr_FR/LC_MESSAGES/django.po | 4 +- .../mailbox_manager/api/client/viewsets.py | 20 +++- src/backend/mailbox_manager/models.py | 11 +-- .../mailboxes/test_api_mailboxes_create.py | 4 +- .../mailboxes/test_api_mailboxes_disable.py | 99 +++++++++++++++++++ .../tests/test_models_mailboxes.py | 2 +- src/backend/mailbox_manager/utils/dimail.py | 21 +++- 8 files changed, 147 insertions(+), 15 deletions(-) create mode 100644 src/backend/mailbox_manager/tests/api/mailboxes/test_api_mailboxes_disable.py diff --git a/CHANGELOG.md b/CHANGELOG.md index ae66868..b0a726d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to ### Added +- ✨(mailbox) allow to disable mailbox - ✨(backend) add ServiceProvider #522 - 💄(admin) allow header color customization #552 diff --git a/src/backend/locale/fr_FR/LC_MESSAGES/django.po b/src/backend/locale/fr_FR/LC_MESSAGES/django.po index 3d38ea9..8c9bc5b 100644 --- a/src/backend/locale/fr_FR/LC_MESSAGES/django.po +++ b/src/backend/locale/fr_FR/LC_MESSAGES/django.po @@ -519,8 +519,8 @@ msgid "Mailboxes" msgstr "" #: mailbox_manager/models.py:224 -msgid "You can't create a mailbox for a disabled domain." -msgstr "Vous ne pouvez pas créer de boîte mail pour un domain désactivé." +msgid "You can't create or update a mailbox for a disabled domain." +msgstr "Vous ne pouvez pas créer ou modifier une boîte mail pour un domain désactivé." #: mailbox_manager/utils/dimail.py:183 msgid "Your new mailbox information" diff --git a/src/backend/mailbox_manager/api/client/viewsets.py b/src/backend/mailbox_manager/api/client/viewsets.py index 5172e25..5b32c67 100644 --- a/src/backend/mailbox_manager/api/client/viewsets.py +++ b/src/backend/mailbox_manager/api/client/viewsets.py @@ -3,12 +3,15 @@ from django.db.models import Subquery from rest_framework import exceptions, filters, mixins, viewsets +from rest_framework.decorators import action +from rest_framework.response import Response from core import models as core_models from mailbox_manager import enums, models from mailbox_manager.api import permissions from mailbox_manager.api.client import serializers +from mailbox_manager.utils.dimail import DimailAPIClient # pylint: disable=too-many-ancestors @@ -186,15 +189,18 @@ class MailBoxViewSet( ): """MailBox ViewSet - GET /api//mail-domains//mailboxes/ + GET /api//mail-domains//mailboxes/ Return a list of mailboxes on the domain - POST /api//mail-domains//mailboxes/ with expected data: + POST /api//mail-domains//mailboxes/ with expected data: - first_name: str - last_name: str - local_part: str - secondary_email: str Sends request to email provisioning API and returns newly created mailbox + + POST /api//mail-domains//mailboxes//disable/ + Send a request to dimail to disable mailbox and change status of the mailbox in our DB """ permission_classes = [permissions.MailBoxPermission] @@ -218,3 +224,13 @@ class MailBoxViewSet( slug=domain_slug ) super().perform_create(serializer) + + @action(detail=True, methods=["post"]) + def disable(self, request, domain_slug, pk=None): # pylint: disable=unused-argument + """Disable mailbox. Send a request to dimail and change status in our DB""" + mailbox = self.get_object() + client = DimailAPIClient() + client.disable_mailbox(mailbox, request.user.sub) + mailbox.status = enums.MailboxStatusChoices.DISABLED + mailbox.save() + return Response(serializers.MailboxSerializer(mailbox).data) diff --git a/src/backend/mailbox_manager/models.py b/src/backend/mailbox_manager/models.py index 3985912..77ba8e5 100644 --- a/src/backend/mailbox_manager/models.py +++ b/src/backend/mailbox_manager/models.py @@ -215,17 +215,12 @@ class Mailbox(BaseModel): def save(self, *args, **kwargs): """ - Modification is forbidden for now. + Override save function to not allow to create or update mailbox of a disabled domain. """ self.full_clean() if self.domain.status == MailDomainStatusChoices.DISABLED: raise exceptions.ValidationError( - _("You can't create a mailbox for a disabled domain.") + _("You can't create or update a mailbox for a disabled domain.") ) - - if self._state.adding: - return super().save(*args, **kwargs) - - # Update is not implemented for now - raise NotImplementedError() + return super().save(*args, **kwargs) diff --git a/src/backend/mailbox_manager/tests/api/mailboxes/test_api_mailboxes_create.py b/src/backend/mailbox_manager/tests/api/mailboxes/test_api_mailboxes_create.py index ad9bb69..a9d1a8d 100644 --- a/src/backend/mailbox_manager/tests/api/mailboxes/test_api_mailboxes_create.py +++ b/src/backend/mailbox_manager/tests/api/mailboxes/test_api_mailboxes_create.py @@ -264,7 +264,9 @@ def test_api_mailboxes__cannot_create_on_disabled_domain(role): ) assert response.status_code == status.HTTP_400_BAD_REQUEST assert not models.Mailbox.objects.exists() - assert response.json() == ["You can't create a mailbox for a disabled domain."] + assert response.json() == [ + "You can't create or update a mailbox for a disabled domain." + ] @pytest.mark.parametrize( diff --git a/src/backend/mailbox_manager/tests/api/mailboxes/test_api_mailboxes_disable.py b/src/backend/mailbox_manager/tests/api/mailboxes/test_api_mailboxes_disable.py new file mode 100644 index 0000000..e81b800 --- /dev/null +++ b/src/backend/mailbox_manager/tests/api/mailboxes/test_api_mailboxes_disable.py @@ -0,0 +1,99 @@ +""" +Unit tests for the mailbox API +""" + +import re + +import pytest +import responses +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_mailboxes__disable_anonymous_forbidden(): + """Anonymous users should not be able to disable a mailbox via the API.""" + mailbox = factories.MailboxEnabledFactory() + response = APIClient().post( + f"/api/v1.0/mail-domains/{mailbox.domain.slug}/mailboxes/{mailbox.pk}/disable/", + ) + assert response.status_code == status.HTTP_401_UNAUTHORIZED + assert models.Mailbox.objects.get().status == enums.MailboxStatusChoices.ENABLED + + +def test_api_mailboxes__disable_authenticated_failure(): + """Authenticated users should not be able to disable mailbox + without specific role on mail domain.""" + user = core_factories.UserFactory() + + client = APIClient() + client.force_login(user) + + mailbox = factories.MailboxEnabledFactory() + response = client.post( + f"/api/v1.0/mail-domains/{mailbox.domain.slug}/mailboxes/{mailbox.pk}/disable/", + ) + + assert response.status_code == status.HTTP_403_FORBIDDEN + assert models.Mailbox.objects.get().status == enums.MailboxStatusChoices.ENABLED + + +def test_api_mailboxes__disable_viewer_failure(): + """Users with viewer role should not be able to disable mailbox on the mail domain.""" + mailbox = factories.MailboxEnabledFactory() + access = factories.MailDomainAccessFactory( + role=enums.MailDomainRoleChoices.VIEWER, domain=mailbox.domain + ) + + client = APIClient() + client.force_login(access.user) + + response = client.post( + f"/api/v1.0/mail-domains/{mailbox.domain.slug}/mailboxes/{mailbox.pk}/disable/", + ) + + assert response.status_code == status.HTTP_403_FORBIDDEN + assert models.Mailbox.objects.get().status == enums.MailboxStatusChoices.ENABLED + + +@pytest.mark.parametrize( + "role", + [enums.MailDomainRoleChoices.OWNER, enums.MailDomainRoleChoices.ADMIN], +) +def test_api_mailboxes__disable_roles_success(role): + """Users with owner or admin role should be able to disable mailbox on the mail domain.""" + mailbox = factories.MailboxEnabledFactory() + access = factories.MailDomainAccessFactory(role=role, domain=mailbox.domain) + + client = APIClient() + client.force_login(access.user) + + with responses.RequestsMock() as rsps: + # Ensure successful response using "responses": + rsps.add( + rsps.GET, + re.compile(r".*/token/"), + body='{"access_token": "domain_owner_token"}', + status=status.HTTP_200_OK, + content_type="application/json", + ) + rsps.add( + rsps.PATCH, + re.compile( + rf".*/domains/{mailbox.domain.name}/mailboxes/{mailbox.local_part}" + ), + status=status.HTTP_200_OK, + content_type="application/json", + ) + response = client.post( + f"/api/v1.0/mail-domains/{mailbox.domain.slug}/mailboxes/{mailbox.pk}/disable/", + ) + assert response.status_code == status.HTTP_200_OK + mailbox = models.Mailbox.objects.get() + + assert mailbox.status == enums.MailboxStatusChoices.DISABLED diff --git a/src/backend/mailbox_manager/tests/test_models_mailboxes.py b/src/backend/mailbox_manager/tests/test_models_mailboxes.py index 92be194..62fd3a1 100644 --- a/src/backend/mailbox_manager/tests/test_models_mailboxes.py +++ b/src/backend/mailbox_manager/tests/test_models_mailboxes.py @@ -116,7 +116,7 @@ def test_models_mailboxes__cannot_create_mailboxes_on_disabled_domain(): A disabled status for the mail domain raises an error.""" with pytest.raises( exceptions.ValidationError, - match="You can't create a mailbox for a disabled domain.", + match="You can't create or update a mailbox for a disabled domain.", ): factories.MailboxFactory( domain=factories.MailDomainFactory( diff --git a/src/backend/mailbox_manager/utils/dimail.py b/src/backend/mailbox_manager/utils/dimail.py index 8bf6984..3323684 100644 --- a/src/backend/mailbox_manager/utils/dimail.py +++ b/src/backend/mailbox_manager/utils/dimail.py @@ -211,7 +211,7 @@ class DimailAPIClient: ) def import_mailboxes(self, domain): - """Synchronize mailboxes from dimail - open xchange to our database. + """Import mailboxes from dimail - open xchange in our database. This is useful in case of acquisition of a pre-existing mail domain. Mailboxes created here are not new mailboxes and will not trigger mail notification.""" @@ -272,3 +272,22 @@ class DimailAPIClient: err, ) return imported_mailboxes + + def disable_mailbox(self, mailbox, user_sub=None): + """Send a request to disable a mailbox to dimail API""" + response = session.patch( + f"{self.API_URL}/domains/{mailbox.domain.name}/mailboxes/{mailbox.local_part}", + json={"active": "no"}, + headers=self.get_headers(user_sub), + verify=True, + timeout=10, + ) + if response.status_code == status.HTTP_200_OK: + logger.info( + "Mailbox %s successfully desactivated on domain %s by user %s", + str(mailbox), + str(mailbox.domain), + user_sub, + ) + return response + return self.raise_exception_for_unexpected_response(response)