(domains) get domain expected config for DNS

Call dimail to get DNS configuration values
to make an external domain work and save it in our db.
Add values to serializer for displaying.
This commit is contained in:
Sabrina Demagny
2025-02-12 23:11:10 +01:00
parent d29b5141b1
commit 3893fdf4d7
8 changed files with 169 additions and 2 deletions

View File

@@ -10,6 +10,7 @@ and this project adheres to
### Added
- ✨(domains) display DNS config expected for domain with required actions
- ✨(domains) check status after creation
- ✨(domains) display required actions to do on domain
- ✨(plugin) add CommuneCreation plugin with domain provisioning #658

View File

@@ -74,6 +74,7 @@ class MailDomainSerializer(serializers.ModelSerializer):
"support_email",
"last_check_details",
"action_required_details",
"expected_config",
]
read_only_fields = [
"id",
@@ -85,6 +86,7 @@ class MailDomainSerializer(serializers.ModelSerializer):
"count_mailboxes",
"last_check_details",
"action_required_details",
"expected_config",
]
def get_action_required_details(self, domain) -> dict:
@@ -122,6 +124,7 @@ class MailDomainSerializer(serializers.ModelSerializer):
# check domain status and update it
try:
client.fetch_domain_status(domain)
client.fetch_domain_expected_config(domain)
except HTTPError as e:
logger.exception(
"[DIMAIL] domain status fetch after creation failed %s with error %s",

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.1.6 on 2025-02-14 17:47
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('mailbox_manager', '0020_alter_mailbox_options_alter_maildomain_options_and_more'),
]
operations = [
migrations.AddField(
model_name='maildomain',
name='expected_config',
field=models.JSONField(blank=True, help_text='A JSON object containing the expected config', null=True, verbose_name='expected config'),
),
]

View File

@@ -36,6 +36,12 @@ class MailDomain(BaseModel):
verbose_name=_("last check details"),
help_text=_("A JSON object containing the last health check details"),
)
expected_config = models.JSONField(
null=True,
blank=True,
verbose_name=_("expected config"),
help_text=_("A JSON object containing the expected config"),
)
class Meta:
db_table = "people_mail_domain"

View File

@@ -15,7 +15,11 @@ from rest_framework.test import APIClient
from core import factories as core_factories
from mailbox_manager import enums, factories, models
from mailbox_manager.tests.fixtures.dimail import CHECK_DOMAIN_BROKEN, CHECK_DOMAIN_OK
from mailbox_manager.tests.fixtures.dimail import (
CHECK_DOMAIN_BROKEN,
CHECK_DOMAIN_OK,
DOMAIN_SPEC,
)
pytestmark = pytest.mark.django_db
@@ -108,6 +112,13 @@ def test_api_mail_domains__create_authenticated():
status=status.HTTP_200_OK,
content_type="application/json",
)
responses.add(
responses.GET,
re.compile(rf".*/domains/{domain_name}/spec/"),
body=json.dumps(DOMAIN_SPEC),
status=status.HTTP_200_OK,
content_type="application/json",
)
response = client.post(
"/api/v1.0/mail-domains/",
{
@@ -145,6 +156,7 @@ def test_api_mail_domains__create_authenticated():
"or je trouve example-fr.mail.protection.outlook.com.",
"spf": "Le SPF record ne contient pas include:_spf.ox.numerique.gouv.fr",
},
"expected_config": DOMAIN_SPEC,
}
# a new domain with status "action required" is created and authenticated user is the owner
@@ -212,6 +224,7 @@ def test_api_mail_domains__create_authenticated__dimail_failure(caplog):
status=dimail_error["status_code"],
content_type="application/json",
)
response = client.post(
"/api/v1.0/mail-domains/",
{
@@ -237,6 +250,7 @@ def test_api_mail_domains__create_authenticated__dimail_failure(caplog):
"support_email": domain.support_email,
"last_check_details": None,
"action_required_details": {},
"expected_config": None,
}
# a new domain with status "failed" is created and authenticated user is the owner
@@ -302,6 +316,13 @@ def test_api_mail_domains__create_dimail_domain(caplog):
status=status.HTTP_200_OK,
content_type="application/json",
)
responses.add(
responses.GET,
re.compile(rf".*/domains/{domain_name}/spec/"),
body=json.dumps(DOMAIN_SPEC),
status=status.HTTP_200_OK,
content_type="application/json",
)
response = client.post(
"/api/v1.0/mail-domains/",
{

View File

@@ -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
@@ -14,6 +18,7 @@ from mailbox_manager.tests.fixtures import dimail as dimail_fixtures
pytestmark = pytest.mark.django_db
@responses.activate
def test_api_mail_domains__retrieve_anonymous():
"""Anonymous users should not be allowed to retrieve a domain."""
@@ -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
@@ -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
@@ -60,6 +71,7 @@ def test_api_mail_domains__retrieve_authenticated_unrelated():
assert response.json() == {"detail": "No MailDomain matches the given query."}
@responses.activate
def test_api_mail_domains__retrieve_authenticated_related():
"""
Authenticated users should be allowed to retrieve a domain
@@ -91,9 +103,11 @@ def test_api_mail_domains__retrieve_authenticated_related():
"support_email": domain.support_email,
"last_check_details": None,
"action_required_details": {},
"expected_config": None,
}
@responses.activate
def test_api_mail_domains__retrieve_authenticated_related_with_action_required():
"""
Authenticated users should be allowed to retrieve a domain
@@ -113,7 +127,6 @@ def test_api_mail_domains__retrieve_authenticated_related_with_action_required()
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),
@@ -130,9 +143,11 @@ 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": None,
}
@responses.activate
def test_api_mail_domains__retrieve_authenticated_related_with_ok_status():
"""
Authenticated users should be allowed to retrieve a domain
@@ -165,4 +180,43 @@ 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": None,
}
@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": None,
}

View File

@@ -255,6 +255,25 @@ CHECK_DOMAIN_OK = {
"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"})

View File

@@ -519,3 +519,48 @@ class DimailAPIClient:
if isinstance(value, dict) and value.get("internal") is internal
}
return {key: value.get("ok", False) for key, value in checks.items()}
def fetch_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:
dimail_expected_config = [
{
"target": item["target"],
"type": item["type"],
"value": item["value"],
}
for item in response.json()
]
domain.expected_config = dimail_expected_config
domain.save()
return dimail_expected_config
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 []