Skip to content

Commit

Permalink
✨(backend) domain accesses update API
Browse files Browse the repository at this point in the history
Allow to update (PUT, PATCH) an access.
Role can be change only to a role available
depending to the authenticated user.
  • Loading branch information
sdemagny committed Sep 23, 2024
1 parent 34341e6 commit a73a981
Show file tree
Hide file tree
Showing 4 changed files with 369 additions and 22 deletions.
47 changes: 28 additions & 19 deletions src/backend/mailbox_manager/api/serializers.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
"""Client serializers for People's mailbox manager app."""

from rest_framework import serializers
from rest_framework import exceptions, serializers

from core.api.serializers import UserSerializer

from mailbox_manager import enums, models
from mailbox_manager import models
from mailbox_manager.utils.dimail import DimailAPIClient


Expand Down Expand Up @@ -77,23 +77,32 @@ class Meta:
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)
"""Return roles available to set for the authenticated user"""
request = self.context.get("request")
if request:
return access.get_can_set_role_to(request.user)
return {}

def validate(self, attrs):
"""
Check access rights specific to writing (update)
"""
request = self.context.get("request")
user = getattr(request, "user", None)
role = attrs.get("role")

# Update
if self.instance:
can_set_role_to = self.instance.get_can_set_role_to(user)

if role and role not in can_set_role_to:
message = (
f"You are only allowed to set role to {', '.join(can_set_role_to)}"
if can_set_role_to
else "You are not allowed to set this role for this domain."
)
raise exceptions.PermissionDenied(message)
return attrs


class MailDomainAccessReadOnlySerializer(MailDomainAccessSerializer):
Expand Down
36 changes: 34 additions & 2 deletions src/backend/mailbox_manager/api/viewsets.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@

from django.db.models import Subquery

from rest_framework import filters, mixins, viewsets
from rest_framework import exceptions, filters, mixins, viewsets
from rest_framework import permissions as drf_permissions

from core import models as core_models

from mailbox_manager import models
from mailbox_manager import enums, models
from mailbox_manager.api import permissions, serializers


Expand Down Expand Up @@ -58,6 +58,7 @@ def perform_create(self, serializer):
class MailDomainAccessViewSet(
viewsets.GenericViewSet,
mixins.ListModelMixin,
mixins.UpdateModelMixin,
mixins.RetrieveModelMixin,
):
"""
Expand All @@ -66,6 +67,14 @@ class MailDomainAccessViewSet(
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.
PUT /api/v1.0/mail-domains/<domain_slug>/accesses/<domain_access_id>/ with expected data:
- role: str [owner|admin|viewer]
Return updated domain access
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
"""

permission_classes = [drf_permissions.IsAuthenticated]
Expand All @@ -90,6 +99,7 @@ def get_serializer_context(self):
"""Extra context provided to the serializer class."""
context = super().get_serializer_context()
context["domain_slug"] = self.kwargs["domain_slug"]
context["authenticated_user"] = self.request.user
return context

def get_queryset(self):
Expand Down Expand Up @@ -118,6 +128,28 @@ def get_queryset(self):
)
return queryset

def perform_update(self, serializer):
"""Check that we don't change the role if it leads to losing the last owner."""
instance = serializer.instance

# Check if the role is being updated and the new role is not "owner"
if (
"role" in self.request.data
and self.request.data["role"] != enums.MailDomainRoleChoices.OWNER
):
domain = instance.domain
# Check if the access being updated 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 change the role to a non-owner role for the last owner access."
raise exceptions.PermissionDenied({"role": message})
serializer.save()


class MailBoxViewSet(
mixins.CreateModelMixin,
Expand Down
35 changes: 34 additions & 1 deletion src/backend/mailbox_manager/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ def get_abilities(self, user):
"""
Compute and return abilities for a given user on the domain.
"""
is_owner_or_admin = False
role = None

if user.is_authenticated:
Expand Down Expand Up @@ -103,6 +102,40 @@ class Meta:
def __str__(self):
return f"Access of user {self.user} on domain {self.domain}."

def get_can_set_role_to(self, user):
"""Return roles available to set"""
if not user.is_authenticated:
return []
roles = list(MailDomainRoleChoices)
authenticated_user_role = None

# get role of authenticated user
if hasattr(self, "user_role"):
authenticated_user_role = self.user_role
else:
try:
authenticated_user_role = user.mail_domain_accesses.get(
domain=self.domain
).role
except (MailDomainAccess.DoesNotExist, IndexError):
return []

# only an owner can set an owner role
if authenticated_user_role != MailDomainRoleChoices.OWNER:
roles.remove(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 == MailDomainRoleChoices.VIEWER or (
authenticated_user_role != MailDomainRoleChoices.OWNER
and self.role == MailDomainRoleChoices.OWNER
):
return []
# we only want to return other roles available to change,
# so we remove the current role of current access.
roles.remove(self.role)
return sorted(roles)


class Mailbox(BaseModel):
"""Mailboxes for users from mail domain."""
Expand Down
Loading

0 comments on commit a73a981

Please sign in to comment.