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

API create mail domain access #428

Merged
merged 1 commit into from
Sep 27, 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 @@ -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
Loading