From 5d84e226b7447659cbd7c962b76a7e90fecf7c4c Mon Sep 17 00:00:00 2001 From: Sabrina Demagny Date: Tue, 26 Nov 2024 12:03:50 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8(maildomain=5Faccess)=20add=20API=20en?= =?UTF-8?q?dpoint=20to=20search=20users?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add new API endpoint to search for new users to whom we can assign new roles. --- CHANGELOG.md | 4 + .../mailbox_manager/api/client/viewsets.py | 30 ++- ...il_domains_accesses_get_available_users.py | 203 ++++++++++++++++++ 3 files changed, 236 insertions(+), 1 deletion(-) create mode 100644 src/backend/mailbox_manager/tests/api/mail_domain_accesses/test_api_mail_domains_accesses_get_available_users.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 6553d38..570a590 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ and this project adheres to ## [Unreleased] +### Added + +- ✨(maildomain_access) add API endpoint to search users #508 + ## [1.8.0] - 2024-12-12 ### Added diff --git a/src/backend/mailbox_manager/api/client/viewsets.py b/src/backend/mailbox_manager/api/client/viewsets.py index 1537ae0..9514bba 100644 --- a/src/backend/mailbox_manager/api/client/viewsets.py +++ b/src/backend/mailbox_manager/api/client/viewsets.py @@ -1,12 +1,13 @@ """API endpoints""" -from django.db.models import Subquery +from django.db.models import Q, Subquery from rest_framework import exceptions, filters, mixins, viewsets from rest_framework.decorators import action from rest_framework.response import Response from core import models as core_models +from core.api.client.serializers import UserSerializer from mailbox_manager import enums, models from mailbox_manager.api import permissions @@ -76,6 +77,9 @@ class MailDomainAccessViewSet( Return list of all domain accesses related to the logged-in user and one domain access if an id is provided. + GET /api/v1.0/mail-domains//accesses/users/ + Return list of all users who can have an access to the domain + POST /api/v1.0/mail-domains//accesses/ with expected data: - user: str - role: str [owner|admin|viewer] @@ -183,6 +187,30 @@ class MailDomainAccessViewSet( return super().destroy(request, *args, **kwargs) + @action(detail=False, url_path="users", methods=["get"]) + def get_available_users(self, request, domain_slug): + """API endpoint to search user to give them new access. + More filters and permission will be added soon. + """ + domain = models.MailDomain.objects.get(slug=domain_slug) + abilities = domain.get_abilities(request.user) + if not abilities["manage_accesses"]: + raise exceptions.PermissionDenied() + + queryset = ( + core_models.User.objects.order_by("-created_at") + # exclude inactive users and get users from identified user's organization + .filter(is_active=True, organization_id=request.user.organization_id) + # exclude all users with already an access config + .exclude(mail_domain_accesses__domain__slug=domain_slug) + ) + # Search by case-insensitive and accent-insensitive + if query := request.GET.get("q", ""): + queryset = queryset.filter( + Q(name__unaccent__icontains=query) | Q(email__unaccent__icontains=query) + ) + return Response(UserSerializer(queryset.all(), many=True).data) + class MailBoxViewSet( mixins.CreateModelMixin, diff --git a/src/backend/mailbox_manager/tests/api/mail_domain_accesses/test_api_mail_domains_accesses_get_available_users.py b/src/backend/mailbox_manager/tests/api/mail_domain_accesses/test_api_mail_domains_accesses_get_available_users.py new file mode 100644 index 0000000..1005b78 --- /dev/null +++ b/src/backend/mailbox_manager/tests/api/mail_domain_accesses/test_api_mail_domains_accesses_get_available_users.py @@ -0,0 +1,203 @@ +""" +Tests for MailDomainAccess API endpoint in People's mailbox manager app. +Focus on get available users. +""" + +import pytest +from rest_framework import status +from rest_framework.test import APIClient + +from core import factories as core_factories +from core import models as core_models + +from mailbox_manager import enums, factories + +pytestmark = pytest.mark.django_db + + +def test_api_mail_domain__available_users_anonymous(): + """Anonymous users should not be allowed to list users.""" + maildomain = factories.MailDomainFactory() + + response = APIClient().get( + f"/api/v1.0/mail-domains/{maildomain.slug}/accesses/users/" + ) + + assert response.status_code == status.HTTP_401_UNAUTHORIZED + assert response.json() == { + "detail": "Authentication credentials were not provided." + } + + +def test_api_mail_domain__available_users_forbidden(): + """Authenticated user without accesses on maildomain should not be able to see available + users. + """ + authenticated_user = core_factories.UserFactory() + client = APIClient() + client.force_login(authenticated_user) + maildomain = factories.MailDomainFactory() + + response = client.get(f"/api/v1.0/mail-domains/{maildomain.slug}/accesses/users/") + + assert response.status_code == status.HTTP_403_FORBIDDEN + + +@pytest.mark.parametrize( + "role", + [ + enums.MailDomainRoleChoices.OWNER, + enums.MailDomainRoleChoices.ADMIN, + ], +) +def test_api_mail_domain__list_available_users__with_abilities(role): + """Authenticated users with roles owner and admin should be allowed to list available users + for a domain. + """ + dave = core_factories.UserFactory(email="bowbow@example.com", name="David Bowman") + nicole = core_factories.UserFactory( + email="nicole_foole@example.com", name="Nicole Foole" + ) + frank = core_factories.UserFactory( + email="frank_poole@example.com", name="Frank Poole" + ) + mary = core_factories.UserFactory(email="mary_pol@example.com", name="Mary Pol") + + expected_ids = {str(user.id) for user in core_models.User.objects.all()} + + authenticated_user = core_factories.UserFactory(name="Owen Rights") + client = APIClient() + client.force_login(authenticated_user) + + maildomain = factories.MailDomainFactory(name="example.com") + factories.MailDomainAccessFactory( + user=authenticated_user, + domain=maildomain, + role=role, + ) + + response = client.get(f"/api/v1.0/mail-domains/{maildomain.slug}/accesses/users/") + assert response.status_code == status.HTTP_200_OK + results = response.json() + assert len(results) == 4 + results_id = {result["id"] for result in results} + assert expected_ids == results_id + + # now test filter user + response = client.get( + f"/api/v1.0/mail-domains/{maildomain.slug}/accesses/users/?q=OL" + ) + assert response.status_code == status.HTTP_200_OK + expected_ids = {str(user.id) for user in [nicole, frank, mary]} + results = response.json() + assert len(results) == 3 + results_id = {result["id"] for result in results} + assert expected_ids == results_id + + # filter on email info + response = client.get( + f"/api/v1.0/mail-domains/{maildomain.slug}/accesses/users/?q=bowbow" + ) + assert response.status_code == status.HTTP_200_OK + results = response.json() + assert len(results) == 1 + assert results[0]["id"] == str(dave.id) + + +def test_api_mail_domain__list_available_users__viewer(): + """A viewer should not be allowed to list available users for a domain.""" + core_factories.UserFactory.create_batch(10) + + authenticated_user = core_factories.UserFactory() + client = APIClient() + client.force_login(authenticated_user) + + maildomain = factories.MailDomainFactory(name="example.com") + factories.MailDomainAccessFactory( + user=authenticated_user, + domain=maildomain, + role=enums.MailDomainRoleChoices.VIEWER, + ) + + response = client.get(f"/api/v1.0/mail-domains/{maildomain.slug}/accesses/users/") + assert response.status_code == status.HTTP_403_FORBIDDEN + + +@pytest.mark.parametrize( + "role", + [ + enums.MailDomainRoleChoices.OWNER, + enums.MailDomainRoleChoices.ADMIN, + ], +) +def test_api_mail_domain__list_available_users__organization(role): + """If an authenticated owner or admin of a domain has an organization, + only users from the same organization are available. + """ + organization = core_factories.OrganizationFactory(with_registration_id=True) + other_organization = core_factories.OrganizationFactory(with_registration_id=True) + authenticated_user = core_factories.UserFactory( + name="Owen Rights", organization=organization + ) + + client = APIClient() + client.force_login(authenticated_user) + + maildomain = factories.MailDomainFactory(name="example.com") + factories.MailDomainAccessFactory( + user=authenticated_user, + domain=maildomain, + role=role, + ) + + dave = core_factories.UserFactory( + email="bowbow@example.com", + name="David Bowman", + organization=organization, + ) + nicole = core_factories.UserFactory( + email="nicole_foole@example.com", + name="Nicole Foole", + organization=organization, + ) + core_factories.UserFactory( + email="frank_poole@example.com", + name="Frank Poole", + organization=other_organization, + ) + core_factories.UserFactory( + email="mary_pol@example.com", + name="Mary Pol", + organization=other_organization, + ) + + expected_ids = sorted([str(nicole.id), str(dave.id)]) + response = client.get(f"/api/v1.0/mail-domains/{maildomain.slug}/accesses/users/") + assert response.status_code == status.HTTP_200_OK + results = response.json() + assert len(results) == 2 + results_id = sorted({result["id"] for result in results}) + assert expected_ids == results_id + + +def test_api_mail_domain__list_available_users__organization_viewer(): + """A viewer should not be allowed to list available users for a domain.""" + organization = core_factories.OrganizationFactory(with_registration_id=True) + other_organization = core_factories.OrganizationFactory(with_registration_id=True) + authenticated_user = core_factories.UserFactory(organization=organization) + + client = APIClient() + client.force_login(authenticated_user) + + maildomain = factories.MailDomainFactory() + factories.MailDomainAccessFactory( + user=authenticated_user, + domain=maildomain, + role=enums.MailDomainRoleChoices.VIEWER, + ) + + core_factories.UserFactory.create_batch(10, organization=organization) + core_factories.UserFactory.create_batch(5, organization=other_organization) + + response = client.get(f"/api/v1.0/mail-domains/{maildomain.slug}/accesses/users/") + assert response.status_code == status.HTTP_403_FORBIDDEN