✨(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:
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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'),
|
||||
),
|
||||
]
|
||||
@@ -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"
|
||||
|
||||
@@ -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/",
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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"})
|
||||
|
||||
@@ -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 []
|
||||
|
||||
Reference in New Issue
Block a user