✨(backend) domain accesses list API
Add an endpoint to list all accesses created for a domain Return all roles available to set for each access depending to the authenticated user.
This commit is contained in:
@@ -8,6 +8,9 @@ and this project adheres to
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
✨(domains) add endpoint API to list all accesses created for a domain
|
||||
|
||||
|
||||
## [1.1.0] - 2024-09-10
|
||||
|
||||
### Added
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
from mailbox_manager import models
|
||||
from core.api.serializers import UserSerializer
|
||||
|
||||
from mailbox_manager import enums, models
|
||||
|
||||
|
||||
class MailboxSerializer(serializers.ModelSerializer):
|
||||
@@ -50,7 +52,10 @@ class MailDomainSerializer(serializers.ModelSerializer):
|
||||
|
||||
|
||||
class MailDomainAccessSerializer(serializers.ModelSerializer):
|
||||
"""Serialize mail domain accesses."""
|
||||
"""Serialize mail domain access."""
|
||||
|
||||
user = UserSerializer(read_only=True, fields=["id", "name", "email"])
|
||||
can_set_role_to = serializers.SerializerMethodField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.MailDomainAccess
|
||||
@@ -58,7 +63,44 @@ class MailDomainAccessSerializer(serializers.ModelSerializer):
|
||||
"id",
|
||||
"user",
|
||||
"role",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"can_set_role_to",
|
||||
]
|
||||
read_only_fields = ["id", "user", "can_set_role_to"]
|
||||
|
||||
def get_can_set_role_to(self, access):
|
||||
"""Return roles available to set"""
|
||||
roles = list(enums.MailDomainRoleChoices)
|
||||
# get role of authenticated user
|
||||
authenticated_user_role = access.user_role
|
||||
if authenticated_user_role != enums.MailDomainRoleChoices.OWNER:
|
||||
roles.remove(enums.MailDomainRoleChoices.OWNER)
|
||||
# if the user authenticated is a viewer, they can't modify role
|
||||
# and only an owner can change role of an owner
|
||||
if authenticated_user_role == enums.MailDomainRoleChoices.VIEWER or (
|
||||
authenticated_user_role != enums.MailDomainRoleChoices.OWNER
|
||||
and access.role == enums.MailDomainRoleChoices.OWNER
|
||||
):
|
||||
return []
|
||||
# we only want to return other roles available to change,
|
||||
# so we remove the current role of current access.
|
||||
roles.remove(access.role)
|
||||
return sorted(roles)
|
||||
|
||||
|
||||
class MailDomainAccessReadOnlySerializer(MailDomainAccessSerializer):
|
||||
"""Serialize mail domain access for list and retrieve actions."""
|
||||
|
||||
class Meta:
|
||||
model = models.MailDomainAccess
|
||||
fields = [
|
||||
"id",
|
||||
"user",
|
||||
"role",
|
||||
"can_set_role_to",
|
||||
]
|
||||
read_only_fields = [
|
||||
"id",
|
||||
"user",
|
||||
"role",
|
||||
"can_set_role_to",
|
||||
]
|
||||
read_only_fields = ["id"]
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""API endpoints"""
|
||||
|
||||
from django.db.models import Subquery
|
||||
|
||||
from rest_framework import filters, mixins, viewsets
|
||||
from rest_framework import permissions as drf_permissions
|
||||
|
||||
@@ -54,19 +56,67 @@ class MailDomainViewSet(
|
||||
|
||||
# pylint: disable=too-many-ancestors
|
||||
class MailDomainAccessViewSet(
|
||||
mixins.ListModelMixin,
|
||||
viewsets.GenericViewSet,
|
||||
mixins.ListModelMixin,
|
||||
mixins.RetrieveModelMixin,
|
||||
):
|
||||
"""
|
||||
MailDomainAccess viewset.
|
||||
API ViewSet for all interactions with mail domain accesses.
|
||||
|
||||
GET /api/v1.0/mail-domains/<domain_slug>/accesses/:<domain_access_id>
|
||||
Return list of all domain accesses related to the logged-in user and one
|
||||
domain access if an id is provided.
|
||||
"""
|
||||
|
||||
permission_classes = [drf_permissions.IsAuthenticated]
|
||||
serializer_class = serializers.MailDomainAccessSerializer
|
||||
filter_backends = [filters.OrderingFilter]
|
||||
ordering_fields = ["created_at", "user", "domain", "role"]
|
||||
ordering_fields = ["role", "user__email", "user__name"]
|
||||
ordering = ["-created_at"]
|
||||
queryset = models.MailDomainAccess.objects.all()
|
||||
queryset = (
|
||||
models.MailDomainAccess.objects.all()
|
||||
.select_related("user")
|
||||
.order_by("-created_at")
|
||||
)
|
||||
list_serializer_class = serializers.MailDomainAccessReadOnlySerializer
|
||||
detail_serializer_class = serializers.MailDomainAccessSerializer
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.action in {"list", "retrieve"}:
|
||||
return self.list_serializer_class
|
||||
return self.detail_serializer_class
|
||||
|
||||
def get_serializer_context(self):
|
||||
"""Extra context provided to the serializer class."""
|
||||
context = super().get_serializer_context()
|
||||
context["domain_slug"] = self.kwargs["domain_slug"]
|
||||
return context
|
||||
|
||||
def get_queryset(self):
|
||||
"""Return the queryset according to the action."""
|
||||
queryset = super().get_queryset()
|
||||
queryset = queryset.filter(domain__slug=self.kwargs["domain_slug"])
|
||||
|
||||
if self.action in {"list", "retrieve"}:
|
||||
# Determine which role the logged-in user has in the domain
|
||||
user_role_query = models.MailDomainAccess.objects.filter(
|
||||
user=self.request.user, domain__slug=self.kwargs["domain_slug"]
|
||||
).values("role")[:1]
|
||||
|
||||
queryset = (
|
||||
# The logged-in user should be part of a domain to see its accesses
|
||||
queryset.filter(
|
||||
domain__accesses__user=self.request.user,
|
||||
)
|
||||
# Abilities are computed based on logged-in user's role and
|
||||
# the user role on each domain access
|
||||
.annotate(
|
||||
user_role=Subquery(user_role_query),
|
||||
)
|
||||
.select_related("user")
|
||||
.distinct()
|
||||
)
|
||||
return queryset
|
||||
|
||||
|
||||
class MailBoxViewSet(
|
||||
|
||||
@@ -0,0 +1,260 @@
|
||||
"""
|
||||
Test for mail_domain accesses API endpoints in People's core app : list
|
||||
"""
|
||||
|
||||
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_list_anonymous():
|
||||
"""Anonymous users should not be allowed to list mail_domain accesses."""
|
||||
mail_domain = factories.MailDomainFactory()
|
||||
factories.MailDomainAccessFactory.create_batch(2, domain=mail_domain)
|
||||
|
||||
response = APIClient().get(f"/api/v1.0/mail-domains/{mail_domain.slug}/accesses/")
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
assert response.json() == {
|
||||
"detail": "Authentication credentials were not provided."
|
||||
}
|
||||
|
||||
|
||||
def test_api_mail_domain__accesses_list_authenticated_unrelated():
|
||||
"""
|
||||
Authenticated users should not be allowed to list mail_domain accesses for a mail_domain
|
||||
to which they are not related.
|
||||
"""
|
||||
user = core_factories.UserFactory()
|
||||
mail_domain = factories.MailDomainFactory()
|
||||
factories.MailDomainAccessFactory.create_batch(3, domain=mail_domain)
|
||||
|
||||
# Accesses for other mail_domains to which the user is related should not be listed either
|
||||
other_access = factories.MailDomainAccessFactory(user=user)
|
||||
factories.MailDomainAccessFactory(domain=other_access.domain)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
response = client.get(
|
||||
f"/api/v1.0/mail-domains/{mail_domain.slug}/accesses/",
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"count": 0,
|
||||
"next": None,
|
||||
"previous": None,
|
||||
"results": [],
|
||||
}
|
||||
|
||||
|
||||
def test_api_mail_domain__accesses_list_for_authenticated_user_related_to_domain():
|
||||
"""
|
||||
Authenticated users should be able to list mail_domain accesses for a mail_domain
|
||||
to which they are related, with a given role.
|
||||
"""
|
||||
viewer, administrator, owner = core_factories.UserFactory.create_batch(3)
|
||||
mail_domain = factories.MailDomainFactory()
|
||||
|
||||
owner_access = factories.MailDomainAccessFactory.create(
|
||||
domain=mail_domain, user=owner, role=enums.MailDomainRoleChoices.OWNER
|
||||
)
|
||||
admin_access = factories.MailDomainAccessFactory.create(
|
||||
domain=mail_domain, user=administrator, role=enums.MailDomainRoleChoices.ADMIN
|
||||
)
|
||||
viewer_access = models.MailDomainAccess.objects.create(
|
||||
domain=mail_domain, user=viewer, role=enums.MailDomainRoleChoices.VIEWER
|
||||
)
|
||||
|
||||
admin_expected_data = {
|
||||
"id": str(admin_access.id),
|
||||
"user": {
|
||||
"id": str(administrator.id),
|
||||
"email": str(administrator.email),
|
||||
"name": str(administrator.name),
|
||||
},
|
||||
"role": str(admin_access.role),
|
||||
}
|
||||
viewer_expected_data = {
|
||||
"id": str(viewer_access.id),
|
||||
"user": {
|
||||
"id": str(viewer.id),
|
||||
"email": str(viewer.email),
|
||||
"name": str(viewer.name),
|
||||
},
|
||||
"role": str(viewer_access.role),
|
||||
}
|
||||
owner_expected_data = {
|
||||
"id": str(owner_access.id),
|
||||
"user": {
|
||||
"id": str(owner.id),
|
||||
"email": str(owner.email),
|
||||
"name": str(owner.name),
|
||||
},
|
||||
"role": str(owner_access.role),
|
||||
}
|
||||
|
||||
# Grant other mail_domain accesses to the user, they should not be listed either
|
||||
other_access = factories.MailDomainAccessFactory(user=viewer)
|
||||
factories.MailDomainAccessFactory(domain=other_access.domain)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(viewer)
|
||||
# viewer can see accesses but no action is available
|
||||
admin_expected_data["can_set_role_to"] = []
|
||||
viewer_expected_data["can_set_role_to"] = []
|
||||
owner_expected_data["can_set_role_to"] = []
|
||||
response = client.get(
|
||||
f"/api/v1.0/mail-domains/{mail_domain.slug}/accesses/",
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["count"] == 3
|
||||
expected = sorted(
|
||||
[admin_expected_data, viewer_expected_data, owner_expected_data],
|
||||
key=lambda x: x["role"],
|
||||
)
|
||||
assert sorted(response.json()["results"], key=lambda x: x["role"]) == expected
|
||||
|
||||
client.force_login(administrator)
|
||||
# administrator can see and give new role but not an OWNER role
|
||||
admin_expected_data["can_set_role_to"] = [enums.MailDomainRoleChoices.VIEWER]
|
||||
viewer_expected_data["can_set_role_to"] = [enums.MailDomainRoleChoices.ADMIN]
|
||||
owner_expected_data["can_set_role_to"] = []
|
||||
response = client.get(
|
||||
f"/api/v1.0/mail-domains/{mail_domain.slug}/accesses/",
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["count"] == 3
|
||||
expected = sorted(
|
||||
[admin_expected_data, viewer_expected_data, owner_expected_data],
|
||||
key=lambda x: x["role"],
|
||||
)
|
||||
assert sorted(response.json()["results"], key=lambda x: x["role"]) == expected
|
||||
|
||||
client.force_login(owner)
|
||||
# owner can do everything
|
||||
admin_expected_data["can_set_role_to"] = [
|
||||
enums.MailDomainRoleChoices.OWNER,
|
||||
enums.MailDomainRoleChoices.VIEWER,
|
||||
]
|
||||
viewer_expected_data["can_set_role_to"] = [
|
||||
enums.MailDomainRoleChoices.ADMIN,
|
||||
enums.MailDomainRoleChoices.OWNER,
|
||||
]
|
||||
owner_expected_data["can_set_role_to"] = [
|
||||
enums.MailDomainRoleChoices.ADMIN,
|
||||
enums.MailDomainRoleChoices.VIEWER,
|
||||
]
|
||||
response = client.get(
|
||||
f"/api/v1.0/mail-domains/{mail_domain.slug}/accesses/",
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["count"] == 3
|
||||
|
||||
expected = sorted(
|
||||
[admin_expected_data, viewer_expected_data, owner_expected_data],
|
||||
key=lambda x: x["role"],
|
||||
)
|
||||
assert sorted(response.json()["results"], key=lambda x: x["role"]) == expected
|
||||
|
||||
|
||||
def test_api_mail_domain__accesses_list_authenticated_constant_numqueries(
|
||||
django_assert_num_queries,
|
||||
):
|
||||
"""
|
||||
The number of queries should not depend on the amount of fetched accesses.
|
||||
"""
|
||||
user = core_factories.UserFactory()
|
||||
mail_domain = factories.MailDomainFactory()
|
||||
models.MailDomainAccess.objects.create(domain=mail_domain, user=user) # random role
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
# Only 3 queries are needed to efficiently fetch mail_domain accesses,
|
||||
# related users :
|
||||
# - query retrieving logged-in user for user_role annotation
|
||||
# - count from pagination
|
||||
# - distinct from viewset
|
||||
with django_assert_num_queries(3):
|
||||
client.get(
|
||||
f"/api/v1.0/mail-domains/{mail_domain.slug}/accesses/",
|
||||
)
|
||||
|
||||
# create 20 new mail_domain accesses
|
||||
for _ in range(20):
|
||||
factories.MailDomainAccessFactory(domain=mail_domain)
|
||||
|
||||
# num queries should still be the same
|
||||
with django_assert_num_queries(3):
|
||||
response = client.get(
|
||||
f"/api/v1.0/mail-domains/{mail_domain.slug}/accesses/",
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["count"] == 21
|
||||
|
||||
|
||||
def test_api_mail_domain__accesses_list_authenticated_ordering():
|
||||
"""MailDomain accesses can be ordered by "role"."""
|
||||
|
||||
user = core_factories.UserFactory()
|
||||
mail_domain = factories.MailDomainFactory()
|
||||
models.MailDomainAccess.objects.create(domain=mail_domain, user=user)
|
||||
|
||||
# create 20 new mail_domain accesses
|
||||
for _ in range(20):
|
||||
factories.MailDomainAccessFactory(domain=mail_domain)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
response = client.get(
|
||||
f"/api/v1.0/mail-domains/{mail_domain.slug}/accesses/?ordering=role",
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json()["count"] == 21
|
||||
|
||||
results = [access["role"] for access in response.json()["results"]]
|
||||
assert sorted(results) == results
|
||||
|
||||
# check results when we change ordering
|
||||
response = client.get(
|
||||
f"/api/v1.0/mail-domains/{mail_domain.slug}/accesses/?ordering=-role",
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json()["count"] == 21
|
||||
|
||||
results = [access["role"] for access in response.json()["results"]]
|
||||
assert sorted(results, reverse=True) == results
|
||||
|
||||
|
||||
@pytest.mark.parametrize("ordering_field", ["email", "name"])
|
||||
def test_api_mail_domain__accesses_list_authenticated_ordering_user(ordering_field):
|
||||
"""Mail domain accesses can be ordered by user's fields."""
|
||||
|
||||
user = core_factories.UserFactory()
|
||||
mail_domain = factories.MailDomainFactory()
|
||||
models.MailDomainAccess.objects.create(domain=mail_domain, user=user)
|
||||
|
||||
for _ in range(20):
|
||||
factories.MailDomainAccessFactory(domain=mail_domain)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
response = client.get(
|
||||
f"/api/v1.0/mail-domains/{mail_domain.slug}/accesses/?ordering=user__{ordering_field}",
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json()["count"] == 21
|
||||
|
||||
def normalize(x):
|
||||
"""Mimic Django order_by, which is case-insensitive and space-insensitive"""
|
||||
return x.casefold().replace(" ", "")
|
||||
|
||||
results = [access["user"][ordering_field] for access in response.json()["results"]]
|
||||
assert sorted(results, key=normalize) == results
|
||||
@@ -0,0 +1,118 @@
|
||||
"""
|
||||
Test for mail_domain accesses API endpoints in People's core app : retrieve
|
||||
"""
|
||||
|
||||
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
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
def test_api_mail_domain__accesses_retrieve_anonymous():
|
||||
"""
|
||||
Anonymous users should not be allowed to retrieve a mail_domain access.
|
||||
"""
|
||||
access = factories.MailDomainAccessFactory()
|
||||
|
||||
response = APIClient().get(
|
||||
f"/api/v1.0/mail-domains/{access.domain.slug}/accesses/{access.id!s}/",
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
assert response.json() == {
|
||||
"detail": "Authentication credentials were not provided."
|
||||
}
|
||||
|
||||
|
||||
def test_api_mail_domain__accesses_retrieve_authenticated_unrelated():
|
||||
"""
|
||||
Authenticated users should not be allowed to retrieve a mail_domain access for
|
||||
a mail_domain to which they are not related.
|
||||
"""
|
||||
user = core_factories.UserFactory()
|
||||
access = factories.MailDomainAccessFactory(domain=factories.MailDomainFactory())
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
response = client.get(
|
||||
f"/api/v1.0/mail-domains/{access.domain.slug}/accesses/{access.id!s}/",
|
||||
)
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
assert response.json() == {"detail": "No MailDomainAccess matches the given query."}
|
||||
|
||||
# Accesses related to another mail_domain should be excluded even if the user is related to it
|
||||
for other_access in [
|
||||
factories.MailDomainAccessFactory(),
|
||||
factories.MailDomainAccessFactory(user=user),
|
||||
]:
|
||||
response = client.get(
|
||||
f"/api/v1.0/mail-domains/{access.domain.slug}/accesses/{other_access.id!s}/",
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
assert response.json() == {
|
||||
"detail": "No MailDomainAccess matches the given query."
|
||||
}
|
||||
|
||||
|
||||
def test_api_mail_domain__accesses_retrieve_authenticated_related():
|
||||
"""
|
||||
A user who is related to a mail_domain should be allowed to retrieve the
|
||||
associated mail_domain user accesses.
|
||||
"""
|
||||
owner = core_factories.UserFactory()
|
||||
mail_domain = factories.MailDomainFactory()
|
||||
access = factories.MailDomainAccessFactory(
|
||||
domain=mail_domain, user=owner, role=enums.MailDomainRoleChoices.OWNER
|
||||
)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(owner)
|
||||
response = client.get(
|
||||
f"/api/v1.0/mail-domains/{mail_domain.slug}/accesses/{access.id!s}/",
|
||||
)
|
||||
|
||||
results = {
|
||||
"id": str(access.id),
|
||||
"user": {
|
||||
"id": str(access.user.id),
|
||||
"email": str(owner.email),
|
||||
"name": str(owner.name),
|
||||
},
|
||||
"role": str(access.role),
|
||||
"can_set_role_to": [
|
||||
enums.MailDomainRoleChoices.ADMIN,
|
||||
enums.MailDomainRoleChoices.VIEWER,
|
||||
],
|
||||
}
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json() == results
|
||||
|
||||
admin = factories.MailDomainAccessFactory(
|
||||
domain=mail_domain, role=enums.MailDomainRoleChoices.ADMIN
|
||||
).user
|
||||
client.force_login(admin)
|
||||
response = client.get(
|
||||
f"/api/v1.0/mail-domains/{mail_domain.slug}/accesses/{access.id!s}/",
|
||||
)
|
||||
# admin can't change role of an owner
|
||||
results["can_set_role_to"] = []
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json() == results
|
||||
|
||||
viewer = factories.MailDomainAccessFactory(
|
||||
domain=mail_domain, role=enums.MailDomainRoleChoices.VIEWER
|
||||
).user
|
||||
client.force_login(viewer)
|
||||
response = client.get(
|
||||
f"/api/v1.0/mail-domains/{mail_domain.slug}/accesses/{access.id!s}/",
|
||||
)
|
||||
# viewer can't change anyone's role
|
||||
results["can_set_role_to"] = []
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json() == results
|
||||
Reference in New Issue
Block a user