Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨(backend) domain accesses delete API #433

Merged
merged 1 commit into from
Sep 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
9 changes: 9 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,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)
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 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."""
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Loading