From c4c3e9de969c2e1643b64def89d1c414b4259c6b Mon Sep 17 00:00:00 2001 From: Sabrina Demagny Date: Wed, 25 Sep 2024 00:43:02 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8(backend)=20domain=20accesses=20create?= =?UTF-8?q?=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Allow to create (POST) a new access for a domain. Role can be change only to a role available and depending to the authenticated user. --- CHANGELOG.md | 1 + .../mailbox_manager/api/serializers.py | 59 +++++- src/backend/mailbox_manager/api/viewsets.py | 6 + .../test_api_mail_domain_accesses_create.py | 173 ++++++++++++++++++ 4 files changed, 230 insertions(+), 9 deletions(-) create mode 100644 src/backend/mailbox_manager/tests/api/mail_domain_accesses/test_api_mail_domain_accesses_create.py diff --git a/CHANGELOG.md b/CHANGELOG.md index f236cca1f..d866f221e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ and this project adheres to - ✨(frontend) allow group members filtering #363 - ✨(mailbox) send new mailbox confirmation email #397 - ✨(domains) domain accesses update API #423 +- ✨(backend) domain accesses create API #428 ### Fixed diff --git a/src/backend/mailbox_manager/api/serializers.py b/src/backend/mailbox_manager/api/serializers.py index 76a784b3e..25fea76e8 100644 --- a/src/backend/mailbox_manager/api/serializers.py +++ b/src/backend/mailbox_manager/api/serializers.py @@ -5,8 +5,9 @@ from rest_framework import exceptions, serializers from core.api.serializers import UserSerializer +from core.models import User -from mailbox_manager import models +from mailbox_manager import enums, models from mailbox_manager.utils.dimail import DimailAPIClient @@ -85,13 +86,13 @@ class MailDomainAccessSerializer(serializers.ModelSerializer): class Meta: model = models.MailDomainAccess - fields = [ - "id", - "user", - "role", - "can_set_role_to", - ] - read_only_fields = ["id", "user", "can_set_role_to"] + fields = ["id", "user", "role", "can_set_role_to"] + read_only_fields = ["id", "can_set_role_to"] + + def update(self, instance, validated_data): + """Make "user" field is readonly but only on update.""" + validated_data.pop("user", None) + return super().update(instance, validated_data) def get_can_set_role_to(self, access): """Return roles available to set for the authenticated user""" @@ -99,8 +100,9 @@ def get_can_set_role_to(self, access): def validate(self, attrs): """ - Check access rights specific to writing (update) + Check access rights specific to writing (update/create) """ + request = self.context.get("request") authenticated_user = getattr(request, "user", None) role = attrs.get("role") @@ -116,6 +118,45 @@ def validate(self, attrs): else "You are not allowed to modify role for this user." ) raise exceptions.PermissionDenied(message) + # Create + else: + # A domain slug has to be set to create a new access + try: + domain_slug = self.context["domain_slug"] + except KeyError as exc: + raise exceptions.ValidationError( + "You must set a domain slug in kwargs to create a new domain access." + ) from exc + + try: + access = authenticated_user.mail_domain_accesses.get( + domain__slug=domain_slug + ) + except models.MailDomainAccess.DoesNotExist as exc: + raise exceptions.PermissionDenied( + "You are not allowed to manage accesses for this domain." + ) from exc + + # Authenticated user must be owner or admin of current domain to set new roles + if access.role not in [ + enums.MailDomainRoleChoices.OWNER, + enums.MailDomainRoleChoices.ADMIN, + ]: + raise exceptions.PermissionDenied( + "You are not allowed to manage accesses for this domain." + ) + # only an owner can set an owner role to another user + if ( + role == enums.MailDomainRoleChoices.OWNER + and access.role != enums.MailDomainRoleChoices.OWNER + ): + raise exceptions.PermissionDenied( + "Only owners of a domain can assign other users as owners." + ) + attrs["user"] = User.objects.get(pk=self.initial_data["user"]) + attrs["domain"] = models.MailDomain.objects.get( + slug=self.context["domain_slug"] + ) return attrs diff --git a/src/backend/mailbox_manager/api/viewsets.py b/src/backend/mailbox_manager/api/viewsets.py index 37399a886..b2fcbb67f 100644 --- a/src/backend/mailbox_manager/api/viewsets.py +++ b/src/backend/mailbox_manager/api/viewsets.py @@ -58,6 +58,7 @@ def perform_create(self, serializer): class MailDomainAccessViewSet( viewsets.GenericViewSet, mixins.ListModelMixin, + mixins.CreateModelMixin, mixins.UpdateModelMixin, mixins.RetrieveModelMixin, ): @@ -68,6 +69,11 @@ class MailDomainAccessViewSet( Return list of all domain accesses related to the logged-in user and one domain access if an id is provided. + POST /api/v1.0/mail-domains//accesses/ with expected data: + - user: str + - role: str [owner|admin|viewer] + Return newly created mail domain access + PUT /api/v1.0/mail-domains//accesses// with expected data: - role: str [owner|admin|viewer] Return updated domain access diff --git a/src/backend/mailbox_manager/tests/api/mail_domain_accesses/test_api_mail_domain_accesses_create.py b/src/backend/mailbox_manager/tests/api/mail_domain_accesses/test_api_mail_domain_accesses_create.py new file mode 100644 index 000000000..46a3bff5a --- /dev/null +++ b/src/backend/mailbox_manager/tests/api/mail_domain_accesses/test_api_mail_domain_accesses_create.py @@ -0,0 +1,173 @@ +""" +Test for mail domain accesses API endpoints in People's core app : create +""" + +import random + +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_create_anonymous(): + """Anonymous users should not be allowed to create mail domain accesses.""" + user = core_factories.UserFactory() + mail_domain = factories.MailDomainFactory() + for role in [role[0] for role in enums.MailDomainRoleChoices.choices]: + response = APIClient().post( + f"/api/v1.0/mail-domains/{mail_domain.slug}/accesses/", + { + "user": str(user.id), + "role": role, + }, + format="json", + ) + assert response.status_code == status.HTTP_401_UNAUTHORIZED + assert response.json() == { + "detail": "Authentication credentials were not provided." + } + assert models.MailDomainAccess.objects.exists() is False + + +def test_api_mail_domain__accesses_create_authenticated_unrelated(): + """ + Authenticated users should not be allowed to create domain accesses for a domain to + which they are not related. + """ + user = core_factories.UserFactory() + other_user = core_factories.UserFactory() + domain = factories.MailDomainFactory() + + client = APIClient() + client.force_login(user) + for role in [role[0] for role in enums.MailDomainRoleChoices.choices]: + response = client.post( + f"/api/v1.0/mail-domains/{domain.slug}/accesses/", + { + "user": str(other_user.id), + "role": role, + }, + format="json", + ) + + assert response.status_code == status.HTTP_403_FORBIDDEN + assert response.json() == { + "detail": "You are not allowed to manage accesses for this domain." + } + assert not models.MailDomainAccess.objects.filter(user=other_user).exists() + + +def test_api_mail_domain__accesses_create_authenticated_viewer(): + """Viewer of a mail domain should not be allowed to create mail domain accesses.""" + authenticated_user = core_factories.UserFactory() + mail_domain = factories.MailDomainFactory( + users=[(authenticated_user, enums.MailDomainRoleChoices.VIEWER)] + ) + other_user = core_factories.UserFactory() + + client = APIClient() + client.force_login(authenticated_user) + for role in [role[0] for role in enums.MailDomainRoleChoices.choices]: + response = client.post( + f"/api/v1.0/mail-domains/{mail_domain.slug}/accesses/", + { + "user": str(other_user.id), + "role": role, + }, + format="json", + ) + + assert response.status_code == status.HTTP_403_FORBIDDEN + assert response.json() == { + "detail": "You are not allowed to manage accesses for this domain." + } + + assert not models.MailDomainAccess.objects.filter(user=other_user).exists() + + +def test_api_mail_domain__accesses_create_authenticated_administrator(): + """ + Administrators of a domain should be able to create mail domain accesses + except for the "owner" role. + """ + authenticated_user = core_factories.UserFactory() + mail_domain = factories.MailDomainFactory( + users=[(authenticated_user, enums.MailDomainRoleChoices.ADMIN)] + ) + other_user = core_factories.UserFactory() + + client = APIClient() + client.force_login(authenticated_user) + + # It should not be allowed to create an owner access + response = client.post( + f"/api/v1.0/mail-domains/{mail_domain.slug}/accesses/", + { + "user": str(other_user.id), + "role": enums.MailDomainRoleChoices.OWNER, + }, + format="json", + ) + + assert response.status_code == status.HTTP_403_FORBIDDEN + assert response.json() == { + "detail": "Only owners of a domain can assign other users as owners." + } + + # It should be allowed to create a lower access + for role in [enums.MailDomainRoleChoices.ADMIN, enums.MailDomainRoleChoices.VIEWER]: + other_user = core_factories.UserFactory() + response = client.post( + f"/api/v1.0/mail-domains/{mail_domain.slug}/accesses/", + { + "user": str(other_user.id), + "role": role, + }, + format="json", + ) + assert response.status_code == status.HTTP_201_CREATED + new_mail_domain_access = models.MailDomainAccess.objects.filter( + user=other_user + ).last() + + assert response.json()["id"] == str(new_mail_domain_access.id) + assert response.json()["role"] == role + assert models.MailDomainAccess.objects.filter(domain=mail_domain).count() == 3 + + +def test_api_mail_domain__accesses_create_authenticated_owner(): + """ + Owners of a mail domain should be able to create mail domain accesses whatever the role. + """ + authenticated_user = core_factories.UserFactory() + mail_domain = factories.MailDomainFactory( + users=[(authenticated_user, enums.MailDomainRoleChoices.OWNER)] + ) + other_user = core_factories.UserFactory() + + role = random.choice([role[0] for role in enums.MailDomainRoleChoices.choices]) + + client = APIClient() + client.force_login(authenticated_user) + response = client.post( + f"/api/v1.0/mail-domains/{mail_domain.slug}/accesses/", + { + "user": str(other_user.id), + "role": role, + }, + format="json", + ) + + assert response.status_code == status.HTTP_201_CREATED + assert models.MailDomainAccess.objects.filter(user=other_user).count() == 1 + new_mail_domain_access = models.MailDomainAccess.objects.filter( + user=other_user + ).get() + assert response.json()["id"] == str(new_mail_domain_access.id) + assert response.json()["role"] == role