2024-04-17 11:19:22 +02:00
|
|
|
"""Client serializers for People's mailbox manager app."""
|
2024-04-16 17:06:43 +02:00
|
|
|
|
2024-12-16 22:28:42 +01:00
|
|
|
from logging import getLogger
|
2024-09-20 18:08:02 +02:00
|
|
|
|
2025-01-14 11:43:42 +01:00
|
|
|
from django.contrib.auth.hashers import make_password
|
|
|
|
|
|
2024-12-16 22:28:42 +01:00
|
|
|
from requests.exceptions import HTTPError
|
2024-09-27 15:54:44 +02:00
|
|
|
from rest_framework import exceptions, serializers
|
2024-04-16 17:06:43 +02:00
|
|
|
|
2024-11-25 14:44:34 +01:00
|
|
|
from core.api.client.serializers import UserSerializer
|
2024-09-25 00:43:02 +02:00
|
|
|
from core.models import User
|
2024-09-14 00:59:38 +02:00
|
|
|
|
2024-09-25 00:43:02 +02:00
|
|
|
from mailbox_manager import enums, models
|
2024-09-19 18:47:08 +02:00
|
|
|
from mailbox_manager.utils.dimail import DimailAPIClient
|
2024-04-16 17:06:43 +02:00
|
|
|
|
2024-12-16 22:28:42 +01:00
|
|
|
logger = getLogger(__name__)
|
|
|
|
|
|
2024-04-16 17:06:43 +02:00
|
|
|
|
|
|
|
|
class MailboxSerializer(serializers.ModelSerializer):
|
|
|
|
|
"""Serialize mailbox."""
|
|
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
|
model = models.Mailbox
|
2024-11-15 18:14:10 +01:00
|
|
|
fields = [
|
|
|
|
|
"id",
|
|
|
|
|
"first_name",
|
|
|
|
|
"last_name",
|
|
|
|
|
"local_part",
|
|
|
|
|
"secondary_email",
|
|
|
|
|
"status",
|
|
|
|
|
]
|
2024-08-05 12:20:44 +02:00
|
|
|
# everything is actually read-only as we do not allow update for now
|
2024-11-15 18:14:10 +01:00
|
|
|
read_only_fields = ["id", "status"]
|
2024-04-16 17:06:43 +02:00
|
|
|
|
2024-09-19 18:47:08 +02:00
|
|
|
def create(self, validated_data):
|
|
|
|
|
"""
|
|
|
|
|
Override create function to fire a request on mailbox creation.
|
2025-01-14 11:43:42 +01:00
|
|
|
|
|
|
|
|
By default, we generate an unusable password for the mailbox, meaning that the mailbox
|
|
|
|
|
will not be able to be used as a login credential until the password is set.
|
2024-09-19 18:47:08 +02:00
|
|
|
"""
|
2025-01-14 11:43:42 +01:00
|
|
|
mailbox = super().create(
|
|
|
|
|
validated_data
|
|
|
|
|
| {
|
|
|
|
|
"password": make_password(None), # generate an unusable password
|
|
|
|
|
}
|
|
|
|
|
)
|
2025-01-10 18:25:07 +01:00
|
|
|
if mailbox.domain.status == enums.MailDomainStatusChoices.ENABLED:
|
2024-11-15 18:14:10 +01:00
|
|
|
client = DimailAPIClient()
|
|
|
|
|
# send new mailbox request to dimail
|
2025-01-10 18:25:07 +01:00
|
|
|
response = client.create_mailbox(mailbox, self.context["request"].user.sub)
|
2024-09-20 18:08:02 +02:00
|
|
|
|
2024-12-26 19:15:36 +01:00
|
|
|
mailbox.status = enums.MailDomainStatusChoices.ENABLED
|
|
|
|
|
mailbox.save()
|
2024-09-20 18:08:02 +02:00
|
|
|
|
2024-11-15 18:14:10 +01:00
|
|
|
# send confirmation email
|
2024-11-20 17:56:27 +01:00
|
|
|
client.notify_mailbox_creation(
|
2025-01-16 18:53:01 +01:00
|
|
|
recipient=mailbox.secondary_email, mailbox_data=response.json()
|
2024-11-15 18:14:10 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# actually save mailbox on our database
|
2024-12-26 19:15:36 +01:00
|
|
|
return mailbox
|
2024-09-19 18:47:08 +02:00
|
|
|
|
2024-04-16 17:06:43 +02:00
|
|
|
|
|
|
|
|
class MailDomainSerializer(serializers.ModelSerializer):
|
|
|
|
|
"""Serialize mail domain."""
|
|
|
|
|
|
2024-08-06 00:04:51 +02:00
|
|
|
abilities = serializers.SerializerMethodField(read_only=True)
|
2025-02-04 14:50:35 +01:00
|
|
|
count_mailboxes = serializers.SerializerMethodField(read_only=True)
|
2025-02-11 13:00:04 +01:00
|
|
|
action_required_details = serializers.SerializerMethodField(read_only=True)
|
2024-08-06 00:04:51 +02:00
|
|
|
|
2024-04-16 17:06:43 +02:00
|
|
|
class Meta:
|
|
|
|
|
model = models.MailDomain
|
2024-06-03 16:59:55 +02:00
|
|
|
lookup_field = "slug"
|
2024-04-17 11:19:22 +02:00
|
|
|
fields = [
|
|
|
|
|
"id",
|
|
|
|
|
"name",
|
2024-06-18 15:10:15 +02:00
|
|
|
"slug",
|
2024-08-08 15:55:16 +02:00
|
|
|
"status",
|
2024-08-06 00:04:51 +02:00
|
|
|
"abilities",
|
2024-04-17 11:19:22 +02:00
|
|
|
"created_at",
|
|
|
|
|
"updated_at",
|
2025-02-04 14:50:35 +01:00
|
|
|
"count_mailboxes",
|
2025-02-02 23:14:04 +01:00
|
|
|
"support_email",
|
2025-02-11 13:00:04 +01:00
|
|
|
"last_check_details",
|
|
|
|
|
"action_required_details",
|
2025-02-12 23:11:10 +01:00
|
|
|
"expected_config",
|
2024-04-17 11:19:22 +02:00
|
|
|
]
|
2024-06-19 16:48:07 +02:00
|
|
|
read_only_fields = [
|
|
|
|
|
"id",
|
|
|
|
|
"slug",
|
2024-08-08 15:55:16 +02:00
|
|
|
"status",
|
2024-08-06 00:04:51 +02:00
|
|
|
"abilities",
|
2024-06-19 16:48:07 +02:00
|
|
|
"created_at",
|
|
|
|
|
"updated_at",
|
2025-02-04 14:50:35 +01:00
|
|
|
"count_mailboxes",
|
2025-02-11 13:00:04 +01:00
|
|
|
"last_check_details",
|
|
|
|
|
"action_required_details",
|
2025-02-12 23:11:10 +01:00
|
|
|
"expected_config",
|
2024-06-19 16:48:07 +02:00
|
|
|
]
|
2024-04-17 11:19:22 +02:00
|
|
|
|
2025-02-11 13:00:04 +01:00
|
|
|
def get_action_required_details(self, domain) -> dict:
|
|
|
|
|
"""Return last check details of the domain."""
|
|
|
|
|
details = {}
|
|
|
|
|
if domain.last_check_details:
|
|
|
|
|
for check, value in domain.last_check_details.items():
|
|
|
|
|
if (
|
|
|
|
|
isinstance(value, dict)
|
|
|
|
|
and value.get("ok") is False
|
|
|
|
|
and value.get("internal") is False
|
|
|
|
|
):
|
|
|
|
|
details[check] = value["errors"][0].get("detail")
|
|
|
|
|
return details
|
|
|
|
|
|
2024-08-06 00:04:51 +02:00
|
|
|
def get_abilities(self, domain) -> dict:
|
|
|
|
|
"""Return abilities of the logged-in user on the instance."""
|
|
|
|
|
request = self.context.get("request")
|
|
|
|
|
if request:
|
|
|
|
|
return domain.get_abilities(request.user)
|
|
|
|
|
return {}
|
|
|
|
|
|
2025-02-04 14:50:35 +01:00
|
|
|
def get_count_mailboxes(self, domain) -> int:
|
|
|
|
|
"""Return count of mailboxes for the domain."""
|
|
|
|
|
return domain.mailboxes.count()
|
|
|
|
|
|
2024-10-23 18:44:14 +02:00
|
|
|
def create(self, validated_data):
|
|
|
|
|
"""
|
|
|
|
|
Override create function to fire a request to dimail upon domain creation.
|
|
|
|
|
"""
|
|
|
|
|
# send new domain request to dimail
|
|
|
|
|
client = DimailAPIClient()
|
2024-11-20 17:56:27 +01:00
|
|
|
client.create_domain(validated_data["name"], self.context["request"].user.sub)
|
2025-02-12 18:48:22 +01:00
|
|
|
domain = super().create(validated_data)
|
|
|
|
|
# check domain status and update it
|
|
|
|
|
try:
|
|
|
|
|
client.fetch_domain_status(domain)
|
2025-02-12 23:11:10 +01:00
|
|
|
client.fetch_domain_expected_config(domain)
|
2025-02-12 18:48:22 +01:00
|
|
|
except HTTPError as e:
|
|
|
|
|
logger.exception(
|
|
|
|
|
"[DIMAIL] domain status fetch after creation failed %s with error %s",
|
|
|
|
|
domain.name,
|
|
|
|
|
e,
|
|
|
|
|
)
|
|
|
|
|
return domain
|
2024-10-23 18:44:14 +02:00
|
|
|
|
2024-04-17 11:19:22 +02:00
|
|
|
|
|
|
|
|
class MailDomainAccessSerializer(serializers.ModelSerializer):
|
2024-09-14 00:59:38 +02:00
|
|
|
"""Serialize mail domain access."""
|
|
|
|
|
|
|
|
|
|
user = UserSerializer(read_only=True, fields=["id", "name", "email"])
|
|
|
|
|
can_set_role_to = serializers.SerializerMethodField(read_only=True)
|
2024-04-17 11:19:22 +02:00
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
|
model = models.MailDomainAccess
|
2024-09-25 00:43:02 +02:00
|
|
|
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)
|
2024-09-14 00:59:38 +02:00
|
|
|
|
|
|
|
|
def get_can_set_role_to(self, access):
|
2024-09-27 15:54:44 +02:00
|
|
|
"""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):
|
|
|
|
|
"""
|
2024-09-25 00:43:02 +02:00
|
|
|
Check access rights specific to writing (update/create)
|
2024-09-27 15:54:44 +02:00
|
|
|
"""
|
2024-09-25 00:43:02 +02:00
|
|
|
|
2024-09-27 15:54:44 +02:00
|
|
|
request = self.context.get("request")
|
|
|
|
|
authenticated_user = getattr(request, "user", None)
|
|
|
|
|
role = attrs.get("role")
|
|
|
|
|
|
|
|
|
|
# Update
|
|
|
|
|
if self.instance:
|
|
|
|
|
can_set_role_to = self.instance.get_can_set_role_to(authenticated_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 modify role for this user."
|
|
|
|
|
)
|
|
|
|
|
raise exceptions.PermissionDenied(message)
|
2024-09-25 00:43:02 +02:00
|
|
|
# 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."
|
|
|
|
|
)
|
2024-11-20 19:14:48 +01:00
|
|
|
|
2024-09-25 00:43:02 +02:00
|
|
|
# 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"]
|
|
|
|
|
)
|
2024-09-27 15:54:44 +02:00
|
|
|
return attrs
|
2024-09-14 00:59:38 +02:00
|
|
|
|
2024-11-20 19:14:48 +01:00
|
|
|
def create(self, validated_data):
|
|
|
|
|
"""
|
|
|
|
|
Override create function to fire requests to dimail on access creation.
|
|
|
|
|
"""
|
|
|
|
|
dimail = DimailAPIClient()
|
|
|
|
|
|
2024-12-16 22:28:42 +01:00
|
|
|
user = validated_data["user"]
|
|
|
|
|
domain = validated_data["domain"]
|
|
|
|
|
|
2024-11-20 19:14:48 +01:00
|
|
|
if validated_data["role"] in [
|
|
|
|
|
enums.MailDomainRoleChoices.ADMIN,
|
|
|
|
|
enums.MailDomainRoleChoices.OWNER,
|
|
|
|
|
]:
|
2024-12-16 22:28:42 +01:00
|
|
|
try:
|
|
|
|
|
dimail.create_user(user.sub)
|
|
|
|
|
dimail.create_allow(user.sub, domain.name)
|
|
|
|
|
except HTTPError:
|
|
|
|
|
logger.exception("[DIMAIL] access creation failed %s")
|
|
|
|
|
domain.status = enums.MailDomainStatusChoices.FAILED
|
|
|
|
|
domain.save()
|
2024-11-20 19:14:48 +01:00
|
|
|
|
|
|
|
|
return super().create(validated_data)
|
|
|
|
|
|
2024-09-14 00:59:38 +02:00
|
|
|
|
|
|
|
|
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",
|
2024-04-17 11:19:22 +02:00
|
|
|
]
|
2025-02-07 19:01:57 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class DomainInvitationSerializer(serializers.ModelSerializer):
|
|
|
|
|
"""Serialize invitations."""
|
|
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
|
model = models.DomainInvitation
|
|
|
|
|
fields = ["id", "created_at", "email", "domain", "role", "issuer", "is_expired"]
|
|
|
|
|
read_only_fields = ["id", "created_at", "domain", "issuer", "is_expired"]
|
|
|
|
|
|
|
|
|
|
def validate(self, attrs):
|
|
|
|
|
"""Validate and restrict invitation to new user based on email."""
|
|
|
|
|
|
|
|
|
|
request = self.context.get("request")
|
|
|
|
|
user = getattr(request, "user", None)
|
|
|
|
|
|
|
|
|
|
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 management invitation."
|
|
|
|
|
) from exc
|
|
|
|
|
|
|
|
|
|
domain = models.MailDomain.objects.get(slug=domain_slug)
|
|
|
|
|
if not domain.get_abilities(user)["manage_accesses"]:
|
|
|
|
|
raise exceptions.PermissionDenied(
|
|
|
|
|
"You are not allowed to manage invitations for this domain."
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
attrs["domain"] = domain
|
|
|
|
|
attrs["issuer"] = user
|
|
|
|
|
return attrs
|