From b637774179d94cecb0ef2454d4762750a6a5e8c0 Mon Sep 17 00:00:00 2001 From: Sabrina Demagny Date: Tue, 6 Aug 2024 00:04:51 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8(mail)=20manage=20mailboxes=20permissi?= =?UTF-8?q?ons?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Manage create and list permissions for all roles. --- .../mailbox_manager/api/permissions.py | 11 ++ .../mailbox_manager/api/serializers.py | 11 ++ src/backend/mailbox_manager/api/viewsets.py | 4 +- src/backend/mailbox_manager/models.py | 1 + .../test_api_mail_domains_retrieve.py | 1 + .../tests/test_api_mailboxes_create.py | 151 ++++++++++++------ .../tests/test_api_mailboxes_list.py | 42 +++-- .../tests/test_models_maildomain.py | 78 ++++++++- 8 files changed, 236 insertions(+), 63 deletions(-) diff --git a/src/backend/mailbox_manager/api/permissions.py b/src/backend/mailbox_manager/api/permissions.py index bc534ac..597754e 100644 --- a/src/backend/mailbox_manager/api/permissions.py +++ b/src/backend/mailbox_manager/api/permissions.py @@ -2,6 +2,8 @@ from core.api import permissions as core_permissions +from mailbox_manager import models + class AccessPermission(core_permissions.IsAuthenticated): """Permission class for access objects.""" @@ -10,3 +12,12 @@ class AccessPermission(core_permissions.IsAuthenticated): """Check permission for a given object.""" abilities = obj.get_abilities(request.user) return abilities.get(request.method.lower(), False) + + +class MailBoxPermission(core_permissions.IsAuthenticated): + """Permission class to manage mailboxes for a mail domain""" + + 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) diff --git a/src/backend/mailbox_manager/api/serializers.py b/src/backend/mailbox_manager/api/serializers.py index 0e62f7e..13d2101 100644 --- a/src/backend/mailbox_manager/api/serializers.py +++ b/src/backend/mailbox_manager/api/serializers.py @@ -16,6 +16,8 @@ class MailboxSerializer(serializers.ModelSerializer): class MailDomainSerializer(serializers.ModelSerializer): """Serialize mail domain.""" + abilities = serializers.SerializerMethodField(read_only=True) + class Meta: model = models.MailDomain lookup_field = "slug" @@ -23,16 +25,25 @@ class MailDomainSerializer(serializers.ModelSerializer): "id", "name", "slug", + "abilities", "created_at", "updated_at", ] read_only_fields = [ "id", "slug", + "abilities", "created_at", "updated_at", ] + def get_abilities(self, domain) -> dict: + """Return abilities of the logged-in user on the instance.""" + request = self.context.get("request") + if request: + return domain.get_abilities(request.user) + return {} + class MailDomainAccessSerializer(serializers.ModelSerializer): """Serialize mail domain accesses.""" diff --git a/src/backend/mailbox_manager/api/viewsets.py b/src/backend/mailbox_manager/api/viewsets.py index 62c4d00..716ed8b 100644 --- a/src/backend/mailbox_manager/api/viewsets.py +++ b/src/backend/mailbox_manager/api/viewsets.py @@ -76,8 +76,10 @@ class MailBoxViewSet( ): """MailBox ViewSet""" - permission_classes = [drf_permissions.IsAuthenticated] + permission_classes = [permissions.MailBoxPermission] serializer_class = serializers.MailboxSerializer + filter_backends = [filters.OrderingFilter] + ordering = ["-created_at"] queryset = models.Mailbox.objects.all() def get_queryset(self): diff --git a/src/backend/mailbox_manager/models.py b/src/backend/mailbox_manager/models.py index de31c8f..750b14e 100644 --- a/src/backend/mailbox_manager/models.py +++ b/src/backend/mailbox_manager/models.py @@ -66,6 +66,7 @@ class MailDomain(BaseModel): "get": bool(role), "patch": is_owner_or_admin, "put": is_owner_or_admin, + "post": is_owner_or_admin, "delete": role == MailDomainRoleChoices.OWNER, "manage_accesses": is_owner_or_admin, } diff --git a/src/backend/mailbox_manager/tests/api/mail_domain/test_api_mail_domains_retrieve.py b/src/backend/mailbox_manager/tests/api/mail_domain/test_api_mail_domains_retrieve.py index 4a0a4fd..30cbb04 100644 --- a/src/backend/mailbox_manager/tests/api/mail_domain/test_api_mail_domains_retrieve.py +++ b/src/backend/mailbox_manager/tests/api/mail_domain/test_api_mail_domains_retrieve.py @@ -68,4 +68,5 @@ def test_api_mail_domains__retrieve_authenticated_related(): "slug": domain.slug, "created_at": domain.created_at.isoformat().replace("+00:00", "Z"), "updated_at": domain.updated_at.isoformat().replace("+00:00", "Z"), + "abilities": domain.get_abilities(user), } diff --git a/src/backend/mailbox_manager/tests/test_api_mailboxes_create.py b/src/backend/mailbox_manager/tests/test_api_mailboxes_create.py index 0c28a93..0c6dae9 100644 --- a/src/backend/mailbox_manager/tests/test_api_mailboxes_create.py +++ b/src/backend/mailbox_manager/tests/test_api_mailboxes_create.py @@ -8,7 +8,8 @@ from rest_framework.test import APIClient from core import factories as core_factories -from mailbox_manager import factories, models +from mailbox_manager import enums, factories, models +from mailbox_manager.api import serializers pytestmark = pytest.mark.django_db @@ -16,86 +17,132 @@ pytestmark = pytest.mark.django_db def test_api_mailboxes__create_anonymous_forbidden(): """Anonymous users should not be able to create a new mailbox via the API.""" mail_domain = factories.MailDomainEnabledFactory() - + mailbox_values = serializers.MailboxSerializer( + factories.MailboxFactory.build() + ).data response = APIClient().post( f"/api/v1.0/mail-domains/{mail_domain.slug}/mailboxes/", - { - "first_name": "jean", - "last_name": "doe", - "local_part": "jean.doe", - "secondary_email": "jean.doe@gmail.com", - "phone_number": "+33150142700", - }, + mailbox_values, ) assert response.status_code == status.HTTP_401_UNAUTHORIZED assert not models.Mailbox.objects.exists() -def test_api_mailboxes__create_authenticated_missing_fields(): - """ - Authenticated users should not be able to create mailboxes - without local part or secondary mail. - """ - user = core_factories.UserFactory(email="tester@ministry.fr", name="john doe") +def test_api_mailboxes__create_authenticated_failure(): + """Authenticated users should not be able to create mailbox + without specific role on mail domain.""" + user = core_factories.UserFactory() client = APIClient() client.force_login(user) + mailbox_values = serializers.MailboxSerializer( + factories.MailboxFactory.build() + ).data mail_domain = factories.MailDomainEnabledFactory() response = client.post( f"/api/v1.0/mail-domains/{mail_domain.slug}/mailboxes/", - { - "first_name": "jean", - "last_name": "doe", - "secondary_email": "jean.doe@gmail.com", - "phone_number": "+33150142700", - }, + mailbox_values, format="json", ) - assert response.status_code == status.HTTP_400_BAD_REQUEST - assert models.Mailbox.objects.exists() is False - assert response.json() == {"local_part": ["This field is required."]} - response = client.post( - f"/api/v1.0/mail-domains/{mail_domain.slug}/mailboxes/", - { - "first_name": "jean", - "last_name": "doe", - "local_part": "jean.doe", - "phone_number": "+33150142700", - }, - format="json", + assert response.status_code == status.HTTP_403_FORBIDDEN + assert not models.Mailbox.objects.exists() + + +def test_api_mailboxes__create_viewer_failure(): + """Users with viewer role should not be able to create mailbox on the mail domain.""" + mail_domain = factories.MailDomainEnabledFactory() + access = factories.MailDomainAccessFactory( + role=enums.MailDomainRoleChoices.VIEWER, domain=mail_domain ) - assert response.status_code == status.HTTP_400_BAD_REQUEST - assert models.Mailbox.objects.exists() is False - assert response.json() == {"secondary_email": ["This field is required."]} - - -def test_api_mailboxes__create_authenticated_successful(): - """Authenticated users should be able to create mailbox.""" - user = core_factories.UserFactory(email="tester@ministry.fr", name="john doe") client = APIClient() - client.force_login(user) + client.force_login(access.user) - mail_domain = factories.MailDomainEnabledFactory(name="saint-jean.collectivite.fr") + mailbox_values = serializers.MailboxSerializer( + factories.MailboxFactory.build() + ).data response = client.post( f"/api/v1.0/mail-domains/{mail_domain.slug}/mailboxes/", - { - "first_name": "jean", - "last_name": "doe", - "local_part": "jean.doe", - "secondary_email": "jean.doe@gmail.com", - "phone_number": "+33150142700", - }, + mailbox_values, format="json", ) + + assert response.status_code == status.HTTP_403_FORBIDDEN + assert not models.Mailbox.objects.exists() + + +@pytest.mark.parametrize( + "role", + [ + enums.MailDomainRoleChoices.OWNER, + enums.MailDomainRoleChoices.ADMIN, + ], +) +def test_api_mailboxes__create_roles_success(role): + """Users with owner or admin role should be able to create mailbox on the mail domain.""" + mail_domain = factories.MailDomainEnabledFactory() + access = factories.MailDomainAccessFactory(role=role, domain=mail_domain) + + client = APIClient() + client.force_login(access.user) + + mailbox_values = serializers.MailboxSerializer( + factories.MailboxFactory.build() + ).data + response = client.post( + f"/api/v1.0/mail-domains/{mail_domain.slug}/mailboxes/", + mailbox_values, + format="json", + ) + assert response.status_code == status.HTTP_201_CREATED mailbox = models.Mailbox.objects.get() - assert mailbox.local_part == "jean.doe" - assert mailbox.secondary_email == "jean.doe@gmail.com" + + assert mailbox.local_part == mailbox_values["local_part"] + assert mailbox.secondary_email == mailbox_values["secondary_email"] assert response.json() == { "id": str(mailbox.id), "local_part": str(mailbox.local_part), "secondary_email": str(mailbox.secondary_email), } + + +def test_api_mailboxes__create_administrator_missing_fields(): + """ + Administrator users should not be able to create mailboxes + without local part or secondary mail. + """ + mail_domain = factories.MailDomainEnabledFactory() + access = factories.MailDomainAccessFactory( + role=enums.MailDomainRoleChoices.ADMIN, domain=mail_domain + ) + client = APIClient() + client.force_login(access.user) + + mailbox_values = serializers.MailboxSerializer( + factories.MailboxFactory.build() + ).data + del mailbox_values["local_part"] + response = client.post( + f"/api/v1.0/mail-domains/{mail_domain.slug}/mailboxes/", + mailbox_values, + format="json", + ) + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert not models.Mailbox.objects.exists() + assert response.json() == {"local_part": ["This field is required."]} + + mailbox_values = serializers.MailboxSerializer( + factories.MailboxFactory.build() + ).data + del mailbox_values["secondary_email"] + response = client.post( + f"/api/v1.0/mail-domains/{mail_domain.slug}/mailboxes/", + mailbox_values, + format="json", + ) + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert not models.Mailbox.objects.exists() + assert response.json() == {"secondary_email": ["This field is required."]} diff --git a/src/backend/mailbox_manager/tests/test_api_mailboxes_list.py b/src/backend/mailbox_manager/tests/test_api_mailboxes_list.py index c8bdd5c..6aec390 100644 --- a/src/backend/mailbox_manager/tests/test_api_mailboxes_list.py +++ b/src/backend/mailbox_manager/tests/test_api_mailboxes_list.py @@ -8,7 +8,7 @@ from rest_framework.test import APIClient from core import factories as core_factories -from mailbox_manager import factories +from mailbox_manager import enums, factories pytestmark = pytest.mark.django_db @@ -25,28 +25,52 @@ def test_api_mailboxes__list_anonymous(): } -def test_api_mailboxes__list_authenticated_no_query(): - """Authenticated users should be able to list mailboxes without applying a query.""" - user = core_factories.UserFactory(email="tester@ministry.fr", name="john doe") +def test_api_mailboxes__list_authenticated(): + """Authenticated users should not be able to list mailboxes""" + user = core_factories.UserFactory() client = APIClient() client.force_login(user) + mail_domain = factories.MailDomainEnabledFactory() + factories.MailboxFactory.create_batch(2, domain=mail_domain) + + response = client.get(f"/api/v1.0/mail-domains/{mail_domain.slug}/mailboxes/") + assert response.status_code == status.HTTP_403_FORBIDDEN + assert response.json() == { + "detail": "You do not have permission to perform this action." + } + + +@pytest.mark.parametrize( + "role", + [ + enums.MailDomainRoleChoices.OWNER, + enums.MailDomainRoleChoices.ADMIN, + enums.MailDomainRoleChoices.VIEWER, + ], +) +def test_api_mailboxes__list_roles(role): + """Owner, admin and viewer users should be able to list mailboxes""" mail_domain = factories.MailDomainEnabledFactory() mailbox1 = factories.MailboxFactory(domain=mail_domain) mailbox2 = factories.MailboxFactory(domain=mail_domain) + access = factories.MailDomainAccessFactory(role=role, domain=mail_domain) + client = APIClient() + client.force_login(access.user) + response = client.get(f"/api/v1.0/mail-domains/{mail_domain.slug}/mailboxes/") assert response.status_code == status.HTTP_200_OK assert response.json()["results"] == [ - { - "id": str(mailbox1.id), - "local_part": str(mailbox1.local_part), - "secondary_email": str(mailbox1.secondary_email), - }, { "id": str(mailbox2.id), "local_part": str(mailbox2.local_part), "secondary_email": str(mailbox2.secondary_email), }, + { + "id": str(mailbox1.id), + "local_part": str(mailbox1.local_part), + "secondary_email": str(mailbox1.secondary_email), + }, ] diff --git a/src/backend/mailbox_manager/tests/test_models_maildomain.py b/src/backend/mailbox_manager/tests/test_models_maildomain.py index eb89e14..f143e69 100644 --- a/src/backend/mailbox_manager/tests/test_models_maildomain.py +++ b/src/backend/mailbox_manager/tests/test_models_maildomain.py @@ -2,12 +2,15 @@ Unit tests for the MailDomain model """ +from django.contrib.auth.models import AnonymousUser from django.core.exceptions import ValidationError from django.utils.text import slugify import pytest -from mailbox_manager import factories +from core import factories as core_factories + +from mailbox_manager import enums, factories pytestmark = pytest.mark.django_db @@ -32,3 +35,76 @@ def test_models_mail_domain__slug_inferred_from_name(): name = "N3w_D0main-Name$.com" domain = factories.MailDomainFactory(name=name, slug="something else") assert domain.slug == slugify(name) + + +# get_abilities + + +def test_models_maildomains_get_abilities_anonymous(): + """Check abilities returned for an anonymous user.""" + maildomain = factories.MailDomainFactory() + abilities = maildomain.get_abilities(AnonymousUser()) + assert abilities == { + "delete": False, + "get": False, + "patch": False, + "put": False, + "post": False, + "manage_accesses": False, + } + + +def test_models_maildomains_get_abilities_authenticated(): + """Check abilities returned for an authenticated user.""" + maildomain = factories.MailDomainFactory() + abilities = maildomain.get_abilities(core_factories.UserFactory()) + assert abilities == { + "delete": False, + "get": False, + "patch": False, + "put": False, + "post": False, + "manage_accesses": False, + } + + +def test_models_maildomains_get_abilities_owner(): + """Check abilities returned for the owner of a maildomain.""" + access = factories.MailDomainAccessFactory(role=enums.MailDomainRoleChoices.OWNER) + abilities = access.domain.get_abilities(access.user) + assert abilities == { + "delete": True, + "get": True, + "patch": True, + "put": True, + "post": True, + "manage_accesses": True, + } + + +def test_models_maildomains_get_abilities_administrator(): + """Check abilities returned for the administrator of a maildomain.""" + access = factories.MailDomainAccessFactory(role=enums.MailDomainRoleChoices.ADMIN) + abilities = access.domain.get_abilities(access.user) + assert abilities == { + "delete": False, + "get": True, + "patch": True, + "put": True, + "post": True, + "manage_accesses": True, + } + + +def test_models_maildomains_get_abilities_viewer(): + """Check abilities returned for the member of a mail domain. It's a viewer role.""" + access = factories.MailDomainAccessFactory(role=enums.MailDomainRoleChoices.VIEWER) + abilities = access.domain.get_abilities(access.user) + assert abilities == { + "delete": False, + "get": True, + "patch": False, + "put": False, + "post": False, + "manage_accesses": False, + }