Skip to content

Commit

Permalink
✨(backend) domain accesses create API
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
sdemagny committed Sep 27, 2024
1 parent 00816e0 commit c4c3e9d
Show file tree
Hide file tree
Showing 4 changed files with 230 additions and 9 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
59 changes: 50 additions & 9 deletions src/backend/mailbox_manager/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -85,22 +86,23 @@ 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"""
return access.get_can_set_role_to(self.context.get("request").user)

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")
Expand All @@ -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


Expand Down
6 changes: 6 additions & 0 deletions src/backend/mailbox_manager/api/viewsets.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ def perform_create(self, serializer):
class MailDomainAccessViewSet(
viewsets.GenericViewSet,
mixins.ListModelMixin,
mixins.CreateModelMixin,
mixins.UpdateModelMixin,
mixins.RetrieveModelMixin,
):
Expand All @@ -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/<domain_slug>/accesses/ with expected data:
- user: str
- role: str [owner|admin|viewer]
Return newly created mail domain access
PUT /api/v1.0/mail-domains/<domain_slug>/accesses/<domain_access_id>/ with expected data:
- role: str [owner|admin|viewer]
Return updated domain access
Expand Down
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit c4c3e9d

Please sign in to comment.