Skip to content

Commit

Permalink
✨(domains) get domain specification for DNS config
Browse files Browse the repository at this point in the history
Call dimail to get all the DNS configuration values
to make an external domain work.
And give the value in domain serializer.
  • Loading branch information
sdemagny committed Feb 14, 2025
1 parent a811431 commit 88269ca
Show file tree
Hide file tree
Showing 6 changed files with 146 additions and 1 deletion.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to

### Added

- ✨(domains) display DNS config expected for domain with required actions
- ✨(domains) display required actions to do on domain
- ✨(plugin) add CommuneCreation plugin with domain provisioning #658
- ✨(frontend) display action required status on domain
Expand Down
10 changes: 10 additions & 0 deletions src/backend/mailbox_manager/api/client/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ class MailDomainSerializer(serializers.ModelSerializer):
abilities = serializers.SerializerMethodField(read_only=True)
count_mailboxes = serializers.SerializerMethodField(read_only=True)
action_required_details = serializers.SerializerMethodField(read_only=True)
expected_config = serializers.SerializerMethodField(read_only=True)

class Meta:
model = models.MailDomain
Expand All @@ -74,6 +75,7 @@ class Meta:
"support_email",
"last_check_details",
"action_required_details",
"expected_config",
]
read_only_fields = [
"id",
Expand All @@ -85,6 +87,7 @@ class Meta:
"count_mailboxes",
"last_check_details",
"action_required_details",
"expected_config",
]

def get_action_required_details(self, domain) -> dict:
Expand All @@ -111,6 +114,13 @@ def get_count_mailboxes(self, domain) -> int:
"""Return count of mailboxes for the domain."""
return domain.mailboxes.count()

def get_expected_config(self, domain) -> list:
"""Return expected config of the domain."""
if domain.status == enums.MailDomainStatusChoices.ACTION_REQUIRED:
client = DimailAPIClient()
return client.get_domain_expected_config(domain)
return []

def create(self, validated_data):
"""
Override create function to fire a request to dimail upon domain creation.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ def test_api_mail_domains__create_authenticated():
"support_email": domain.support_email,
"last_check_details": None,
"action_required_details": {},
"expected_config": [],
}

# a new domain with status "pending" is created and authenticated user is the owner
Expand Down Expand Up @@ -202,6 +203,7 @@ def test_api_mail_domains__create_authenticated__dimail_failure():
"support_email": domain.support_email,
"last_check_details": None,
"action_required_details": {},
"expected_config": [],
}

# a new domain with status "failed" is created and authenticated user is the owner
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@
Tests for MailDomains API endpoint in People's mailbox manager app. Focus on "retrieve" action.
"""

import json
import re

import pytest
import responses
from rest_framework import status
from rest_framework.test import APIClient

Expand All @@ -14,6 +18,7 @@
pytestmark = pytest.mark.django_db


@responses.activate
def test_api_mail_domains__retrieve_anonymous():
"""Anonymous users should not be allowed to retrieve a domain."""

Expand All @@ -24,8 +29,11 @@ def test_api_mail_domains__retrieve_anonymous():
assert response.json() == {
"detail": "Authentication credentials were not provided."
}
# Verify no calls were made to dimail API
assert len(responses.calls) == 0


@responses.activate
def test_api_domains__retrieve_non_existing():
"""
Authenticated users should have an explicit error when trying to retrive
Expand All @@ -39,8 +47,11 @@ def test_api_domains__retrieve_non_existing():
)
assert response.status_code == status.HTTP_404_NOT_FOUND
assert response.json() == {"detail": "Not found."}
# Verify no calls were made to dimail API
assert len(responses.calls) == 0


@responses.activate
def test_api_mail_domains__retrieve_authenticated_unrelated():
"""
Authenticated users should not be allowed to retrieve a domain
Expand All @@ -58,8 +69,11 @@ def test_api_mail_domains__retrieve_authenticated_unrelated():
)
assert response.status_code == status.HTTP_404_NOT_FOUND
assert response.json() == {"detail": "No MailDomain matches the given query."}
# Verify no calls were made to dimail API
assert len(responses.calls) == 0


@responses.activate
def test_api_mail_domains__retrieve_authenticated_related():
"""
Authenticated users should be allowed to retrieve a domain
Expand Down Expand Up @@ -91,9 +105,13 @@ def test_api_mail_domains__retrieve_authenticated_related():
"support_email": domain.support_email,
"last_check_details": None,
"action_required_details": {},
"expected_config": [],
}
# Verify no calls were made to dimail API
assert len(responses.calls) == 0


@responses.activate
def test_api_mail_domains__retrieve_authenticated_related_with_action_required():
"""
Authenticated users should be allowed to retrieve a domain
Expand All @@ -110,10 +128,16 @@ def test_api_mail_domains__retrieve_authenticated_related_with_action_required()
)
factories.MailDomainAccessFactory(domain=domain, user=user)

responses.add(
responses.GET,
re.compile(rf".*/domains/{domain.name}/spec/"),
body=json.dumps(dimail_fixtures.DOMAIN_SPEC),
status=status.HTTP_200_OK,
content_type="application/json",
)
response = client.get(
f"/api/v1.0/mail-domains/{domain.slug}/",
)

assert response.status_code == status.HTTP_200_OK
assert response.json() == {
"id": str(domain.id),
Expand All @@ -130,9 +154,13 @@ def test_api_mail_domains__retrieve_authenticated_related_with_action_required()
"mx": "Je veux que le MX du domaine soit mx.ox.numerique.gouv.fr., "
"or je trouve example-fr.mail.protection.outlook.com.",
},
"expected_config": dimail_fixtures.DOMAIN_SPEC,
}
# Verify one call was made to dimail API to get DNS configuration expected
assert len(responses.calls) == 1


@responses.activate
def test_api_mail_domains__retrieve_authenticated_related_with_ok_status():
"""
Authenticated users should be allowed to retrieve a domain
Expand Down Expand Up @@ -165,4 +193,47 @@ def test_api_mail_domains__retrieve_authenticated_related_with_ok_status():
"support_email": domain.support_email,
"last_check_details": dimail_fixtures.CHECK_DOMAIN_OK,
"action_required_details": {},
"expected_config": [],
}
# Verify no calls were made to dimail API
assert len(responses.calls) == 0


@responses.activate
def test_api_mail_domains__retrieve_authenticated_related_with_failed_status():
"""
Authenticated users should be allowed to retrieve a domain
to which they have access and which has failed status.
"""
user = core_factories.UserFactory()

client = APIClient()
client.force_login(user)

domain = factories.MailDomainFactory(
status=enums.MailDomainStatusChoices.FAILED,
last_check_details=dimail_fixtures.CHECK_DOMAIN_BROKEN_INTERNAL,
)
factories.MailDomainAccessFactory(domain=domain, user=user)

response = client.get(
f"/api/v1.0/mail-domains/{domain.slug}/",
)

assert response.status_code == status.HTTP_200_OK
assert response.json() == {
"id": str(domain.id),
"name": domain.name,
"slug": domain.slug,
"status": domain.status,
"created_at": domain.created_at.isoformat().replace("+00:00", "Z"),
"updated_at": domain.updated_at.isoformat().replace("+00:00", "Z"),
"abilities": domain.get_abilities(user),
"count_mailboxes": 0,
"support_email": domain.support_email,
"last_check_details": dimail_fixtures.CHECK_DOMAIN_BROKEN_INTERNAL,
"action_required_details": {},
"expected_config": [],
}
# Verify no calls were made to dimail API
assert len(responses.calls) == 0
19 changes: 19 additions & 0 deletions src/backend/mailbox_manager/tests/fixtures/dimail.py
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,25 @@
"cert": {"ok": True, "internal": True, "errors": []},
}

# pylint: disable=line-too-long
DOMAIN_SPEC = [
{"target": "", "type": "mx", "value": "mx.ox.numerique.gouv.fr."},
{
"target": "dimail._domainkey",
"type": "txt",
"value": "v=DKIM1; h=sha256; k=rsa; p=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
},
{"target": "imap", "type": "cname", "value": "imap.ox.numerique.gouv.fr."},
{"target": "smtp", "type": "cname", "value": "smtp.ox.numerique.gouv.fr."},
{
"target": "",
"type": "txt",
"value": "v=spf1 include:_spf.ox.numerique.gouv.fr -all",
},
{"target": "webmail", "type": "cname", "value": "webmail.ox.numerique.gouv.fr."},
]


## TOKEN

TOKEN_OK = json.dumps({"access_token": "token", "token_type": "bearer"})
Expand Down
42 changes: 42 additions & 0 deletions src/backend/mailbox_manager/utils/dimail.py
Original file line number Diff line number Diff line change
Expand Up @@ -504,3 +504,45 @@ def _get_dimail_checks(self, dimail_response, internal: bool):
if isinstance(value, dict) and value.get("internal") is internal
}
return {key: value.get("ok", False) for key, value in checks.items()}

def get_domain_expected_config(self, domain):
"""Send a request to dimail to get domain specification for DNS configuration."""
try:
response = session.get(
f"{self.API_URL}/domains/{domain.name}/spec/",
headers={"Authorization": f"Basic {self.API_CREDENTIALS}"},
verify=True,
timeout=10,
)
except requests.exceptions.ConnectionError as error:
logger.exception(
"Connection error while trying to reach %s.",
self.API_URL,
exc_info=error,
)
return []
if response.status_code == status.HTTP_200_OK:
# format the response to log an error if api response changed
try:
return [
{
"target": item["target"],
"type": item["type"],
"value": item["value"],
}
for item in response.json()
]
except KeyError as error:
logger.exception(
"[DIMAIL] spec expected response format changed: %s",
error,
)
return []
else:
logger.exception(
"[DIMAIL] unexpected error : %s %s",
response.status_code,
response.content,
exc_info=False,
)
return []

0 comments on commit 88269ca

Please sign in to comment.