Skip to content

Commit

Permalink
✨(backend) domain accesses delete API
Browse files Browse the repository at this point in the history
  • Loading branch information
sdemagny committed Sep 30, 2024
1 parent fbb2acc commit c95164a
Show file tree
Hide file tree
Showing 4 changed files with 207 additions and 2 deletions.
10 changes: 10 additions & 0 deletions src/backend/mailbox_manager/api/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,13 @@ 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"""

# todo: add check on POST def has_permission check role on domain and rights !?
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)
23 changes: 21 additions & 2 deletions src/backend/mailbox_manager/api/viewsets.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -61,6 +60,7 @@ class MailDomainAccessViewSet(
mixins.CreateModelMixin,
mixins.UpdateModelMixin,
mixins.RetrieveModelMixin,
mixins.DestroyModelMixin,
):
"""
API ViewSet for all interactions with mail domain accesses.
Expand All @@ -81,9 +81,12 @@ class MailDomainAccessViewSet(
PATCH /api/v1.0/mail-domains/<domain_slug>/accesses/<domain_access_id>/ with expected data:
- role: str [owner|admin|viewer]
Return partially updated domain access
DELETE /api/v1.0/mail-domains/<domain_slug>/accesses/<domain_access_id>/
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"]
Expand Down Expand Up @@ -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,
Expand Down
25 changes: 25 additions & 0 deletions src/backend/mailbox_manager/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
"""
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."""
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
"""
Test for mail_domain accesses API endpoints in People's core app : delete
"""

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_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)

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_403_FORBIDDEN
assert models.MailDomainAccess.objects.count() == 2


def test_api_mail_domain__accesses_delete_administrators():
"""
Users who are administrators in a mail_domain should be allowed to delete an access
from the mail_domain provided it is not ownership.
"""
authenticated_user = core_factories.UserFactory()
mail_domain = factories.MailDomainFactory(
users=[(authenticated_user, enums.MailDomainRoleChoices.ADMIN)]
)
access = factories.MailDomainAccessFactory(
domain=mail_domain,
role=random.choice(
[enums.MailDomainRoleChoices.VIEWER, enums.MailDomainRoleChoices.ADMIN]
),
)

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_except_owners():
"""
Users should be able to delete the mail_domain access of another user
for a mail_domain of which they are owner provided it is not an owner access.
"""
authenticated_user = core_factories.UserFactory()
mail_domain = factories.MailDomainFactory(
users=[(authenticated_user, enums.MailDomainRoleChoices.OWNER)]
)
access = factories.MailDomainAccessFactory(
domain=mail_domain,
role=random.choice(
[enums.MailDomainRoleChoices.VIEWER, enums.MailDomainRoleChoices.ADMIN]
),
)

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,
)
assert models.MailDomainAccess.objects.count() == 1

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() == 1

0 comments on commit c95164a

Please sign in to comment.