From 9237e68265af85a001c8a814c02677d13117c763 Mon Sep 17 00:00:00 2001 From: Benjamin Bayart Date: Fri, 26 Jul 2024 16:25:49 +0200 Subject: [PATCH] Check the domains when posting, new route to force check_domain, get the check informations in GET --- .../versions/a00d7feb5df9_add_errors.py | 66 +++++++++++++++++ src/conftest.py | 3 + src/dns/__init__.py | 4 +- src/dns/domain.py | 74 ++++++++++++++----- src/dns/test_basic.py | 3 +- src/routes/domains/__init__.py | 2 + src/routes/domains/check_domain.py | 31 ++++++++ src/routes/domains/post_domain.py | 9 ++- src/routes/test_admin.py | 21 +++--- src/routes/test_domains.py | 66 +++++++++++++++++ src/sql_api/__init__.py | 5 ++ src/sql_api/domain.py | 17 +++++ src/sql_api/models.py | 1 + src/sql_api/test_basic.py | 9 +++ src/web_models/__init__.py | 12 ++- src/web_models/admin.py | 47 +++++++++++- 16 files changed, 333 insertions(+), 37 deletions(-) create mode 100644 src/alembic/versions/a00d7feb5df9_add_errors.py create mode 100644 src/routes/domains/check_domain.py diff --git a/src/alembic/versions/a00d7feb5df9_add_errors.py b/src/alembic/versions/a00d7feb5df9_add_errors.py new file mode 100644 index 0000000..7616e42 --- /dev/null +++ b/src/alembic/versions/a00d7feb5df9_add_errors.py @@ -0,0 +1,66 @@ +"""add errors + +Revision ID: a00d7feb5df9 +Revises: 7d80d2028a7f +Create Date: 2024-07-26 10:13:59.099634 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'a00d7feb5df9' +down_revision: Union[str, None] = '7d80d2028a7f' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade(engine_name: str) -> None: + globals()["upgrade_%s" % engine_name]() + + +def downgrade(engine_name: str) -> None: + globals()["downgrade_%s" % engine_name]() + + + + + +def upgrade_api() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('domains', sa.Column('errors', sa.JSON(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade_api() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('domains', 'errors') + # ### end Alembic commands ### + + +def upgrade_dovecot() -> None: + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### + + +def downgrade_dovecot() -> None: + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### + + +def upgrade_postfix() -> None: + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### + + +def downgrade_postfix() -> None: + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### + diff --git a/src/conftest.py b/src/conftest.py index f8c016b..8fa7a89 100644 --- a/src/conftest.py +++ b/src/conftest.py @@ -536,6 +536,9 @@ def _make_domain( log.info("- creating the domain") res = client.post( "/domains/", + params = { + "no_check": "true", + }, json = { "name": name, "features": features, diff --git a/src/dns/__init__.py b/src/dns/__init__.py index 09d4e5f..c107b84 100644 --- a/src/dns/__init__.py +++ b/src/dns/__init__.py @@ -1,10 +1,12 @@ -from .domain import Domain +from .domain import background_check_new_domain, foreground_check_domain, Domain from .dkim import DkimInfo from .utils import get_ip_address, make_auth_resolver __all__ = [ + background_check_new_domain, DkimInfo, Domain, + foreground_check_domain, get_ip_address, make_auth_resolver, ] diff --git a/src/dns/domain.py b/src/dns/domain.py index ea0caae..180ecab 100644 --- a/src/dns/domain.py +++ b/src/dns/domain.py @@ -1,3 +1,5 @@ +import logging + import dns.name import dns.resolver import sqlalchemy.orm as orm @@ -23,7 +25,7 @@ targets = { "webmail": "webmail.ox.numerique.gouv.fr.", "imap": "imap.ox.numerique.gouv.fr.", - "mailbox": "mail.ox.numerique.gouv.fr.", + "mail": "mail.ox.numerique.gouv.fr.", "smtp": "smtp.ox.numerique.gouv.fr.", } #required_mx = "mx.fdn.fr." @@ -48,8 +50,8 @@ def __init__( self.dkim = dkim - def add_err(self, err: str, detail: str = ""): - self.errs.append({"code": err, "detail": detail}) + def add_err(self, test: str, err: str, detail: str = ""): + self.errs.append({"test": test, "code": err, "detail": detail}) self.valid = False def get_auth_resolver(self, domain: str, insist: bool = False) -> dns.resolver.Resolver: @@ -64,7 +66,7 @@ def get_auth_resolver(self, domain: str, insist: bool = False) -> dns.resolver.R def check_exists(self): resolver = self.get_auth_resolver(self.domain.name) if resolver is None: - self.add_err("must_exist", f"Le domaine {self.domain.name} n'existe pas") + self.add_err("domain_exist", "must_exist", f"Le domaine {self.domain.name} n'existe pas") return def try_cname_for_mx(self): @@ -75,10 +77,11 @@ def try_cname_for_mx(self): print(f"Je trouve un CNAME vers {self.dest_domain}, je le prend comme dest_domain") self.dest_name = dns.name.from_text(self.dest_domain) return self.check_mx() - except dns.resolver.NXDOMAIN: - self.add_err("no_mx", "Il n'y a pas d'enregistrement MX ou CNAME sur le domaine") + except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer): + self.add_err("mx", "no_mx", "Il n'y a pas d'enregistrement MX ou CNAME sur le domaine") return - except Exception: + except Exception as e: + print(f"Unexpected exception while searching for a CNAME for a MX : {e}") raise def check_mx(self): @@ -87,20 +90,23 @@ def check_mx(self): print(f"Je cherche un MX pour {self.dest_domain}") answer = resolver.resolve(self.dest_name, rdtype = "MX") except dns.resolver.NXDOMAIN: - self.add_err("no_mx", "Il n'y a pas d'enregistrement MX sur le domaine") + print("NXDOMAIN") + self.add_err("mx", "no_mx", "Il n'y a pas d'enregistrement MX sur le domaine") return except dns.resolver.NoAnswer: return self.try_cname_for_mx() - except Exception: + except Exception as e: + print(f"Unexpected exception while searching for MX {e}") raise nb_mx = len(answer) if nb_mx != 1 and False: - self.add_err("one_mx", f"Je veux un seul MX, et j'en trouve {nb_mx}") + self.add_err("mx", "one_mx", f"Je veux un seul MX, et j'en trouve {nb_mx}") return mx = str(answer[0].exchange) if not mx == required_mx: self.add_err( + "mx", "wrong_mx", f"Je veux que le MX du domaine soit {required_mx}, " f"or je trouve {mx}" @@ -124,18 +130,20 @@ def check_cname(self, name): for origin in origins: resolver = self.get_auth_resolver(origin) if resolver is None: - self.add_err(f"no_cname_{name}", f"Il faut un CNAME {origin} qui renvoie vers {target}") + self.add_err(f"cname_{name}", f"no_cname_{name}", f"Il faut un CNAME {origin} qui renvoie vers {target}") continue try: answer = resolver.resolve(origin, rdtype = "CNAME") except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer): - self.add_err(f"no_cname_{name}", f"Il n'y a pas de CNAME {origin} -> {target}") + self.add_err(f"cname_{name}", f"no_cname_{name}", f"Il n'y a pas de CNAME {origin} -> {target}") continue - except Exception: + except Exception as e: + print(f"Unexpected exception when searching for a CNAME : {e}") raise got_target = str(answer[0].target) if not got_target == target: self.add_err( + f"cname_{name}", f"wrong_cname_{name}", f"Le CNAME pour {origin} n'est pas bon, " f"il renvoie vers {got_target} et je veux {target}" @@ -147,7 +155,8 @@ def check_spf(self): answer = resolver.resolve(self.dest_name, rdtype="TXT") except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer): answer = [] - except Exception: + except Exception as e: + print(f"Unexpected exception when searching for the SPF record : {e}") raise found_spf = False valid_spf = False @@ -159,10 +168,10 @@ def check_spf(self): valid_spf = True return if not found_spf: - self.add_err("no_spf", f"Il faut un SPF record, et il doit contenir {required_spf}") + self.add_err("spf", "no_spf", f"Il faut un SPF record, et il doit contenir {required_spf}") return if not valid_spf: - self.add_err("wrong_spf", f"Le SPF record ne contient pas {required_spf}") + self.add_err("spf", "wrong_spf", f"Le SPF record ne contient pas {required_spf}") return def check_dkim(self): @@ -174,7 +183,8 @@ def check_dkim(self): answer = resolver.resolve(self.dkim_name, rdtype="TXT") except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer): answer = [] - except Exception: + except Exception as e: + print(f"Unexpected exception when searching for the DKIM record : {e}") raise found_dkim = False valid_dkim = False @@ -188,10 +198,10 @@ def check_dkim(self): valid_dkim = True return if not found_dkim: - self.add_err("no_dkim", "Il faut un DKIM record, et il doit contenir la bonne clef") + self.add_err("dkim", "no_dkim", "Il faut un DKIM record, et il doit contenir la bonne clef") return if not valid_dkim: - self.add_err("wrong_dkim", "Le DKIM record n'est pas valide (il ne contient pas la bonne clef)") + self.add_err("dkim", "wrong_dkim", "Le DKIM record n'est pas valide (il ne contient pas la bonne clef)") return def _check_domain(self) -> bool: @@ -205,7 +215,6 @@ def _check_domain(self) -> bool: self.check_cname("webmail") if self.domain.has_feature("mailbox"): self.check_cname("imap") - self.check_cname("mailbox") self.check_cname("smtp") self.check_spf() self.check_dkim() @@ -247,3 +256,28 @@ def db_setup(self, db: orm.Session): dom = None if dom is None: dom = sql_postfix.create_alias_domain(db, self.domain.name, self.domain.get_alias_domain()) + + +def foreground_check_domain(db: orm.Session, db_dom: sql_api.DBDomain) -> sql_api.DBDomain: + name = db_dom.name + ck_dom = Domain(db_dom) + ck_dom.check() + if ck_dom.valid: + sql_api.update_domain_state(db, name, "ok") + db_dom = sql_api.update_domain_errors(db, name, None) + else: + sql_api.update_domain_state(db, name, "broken") + db_dom = sql_api.update_domain_errors(db, name, ck_dom.errs) + return db_dom + +def background_check_new_domain(name: str): + log = logging.getLogger(__name__) + maker = sql_api.get_maker() + db = maker() + db_dom = sql_api.get_domain(db, name) + if db_dom is None: + log.error("Je ne sais pas vérifier un domaine qui n'existe pas en base") + db.close() + return + db_dom = foreground_check_domain(db, db_dom) + db.close() diff --git a/src/dns/test_basic.py b/src/dns/test_basic.py index b1b10b7..4a87f40 100644 --- a/src/dns/test_basic.py +++ b/src/dns/test_basic.py @@ -33,12 +33,11 @@ def test_domain_check(): ck_dom = domain.Domain(db_dom) ck_dom.check() assert ck_dom.valid is False - assert len(ck_dom.errs) == 6 + assert len(ck_dom.errs) == 5 codes = [ err["code"] for err in ck_dom.errs ] assert "wrong_mx" in codes assert "wrong_cname_webmail" in codes assert "wrong_cname_imap" in codes - assert "no_cname_mailbox" in codes assert "no_cname_smtp" in codes assert "wrong_spf" in codes diff --git a/src/routes/domains/__init__.py b/src/routes/domains/__init__.py index f38fcb0..ebb25d6 100644 --- a/src/routes/domains/__init__.py +++ b/src/routes/domains/__init__.py @@ -1,9 +1,11 @@ # ruff: noqa: E402 +from .check_domain import check_domain from .get_domain import get_domain from .get_domains import get_domains from .post_domain import post_domain __all__ = [ + check_domain, get_domain, get_domains, post_domain, diff --git a/src/routes/domains/check_domain.py b/src/routes/domains/check_domain.py new file mode 100644 index 0000000..f63dffb --- /dev/null +++ b/src/routes/domains/check_domain.py @@ -0,0 +1,31 @@ +import logging + +import fastapi + +from ... import auth, dns, sql_api, web_models +from .. import dependencies, routers + + +@routers.domains.get("/{domain_name}/check") +async def check_domain( + db: dependencies.DependsApiDb, + user: auth.DependsBasicAdmin, + domain_name: str, +) -> web_models.Domain: + log = logging.getLogger(__name__) + perms = user.get_creds() + + domain_db = sql_api.get_domain(db, domain_name) + if domain_db is None: + log.info(f"Domain {domain_name} not found.") + raise fastapi.HTTPException(status_code=404, detail="Domain not found") + + if not perms.can_read(domain_name): + log.info(f"Permission denied on domain {domain_name} for user.") + raise fastapi.HTTPException(status_code=401, detail="Not authorized.") + + domain_db = dns.foreground_check_domain(db, domain_db) + log.info(f"Domain state after check is {domain_db.state}") + assert domain_db.state in [ "ok", "broken" ] + + return web_models.Domain.from_db(domain_db) diff --git a/src/routes/domains/post_domain.py b/src/routes/domains/post_domain.py index 408bf40..275e89e 100644 --- a/src/routes/domains/post_domain.py +++ b/src/routes/domains/post_domain.py @@ -1,6 +1,6 @@ import fastapi -from ... import auth, oxcli, sql_api, web_models +from ... import auth, dns, oxcli, sql_api, web_models from .. import dependencies, routers @@ -8,7 +8,9 @@ async def post_domain( db: dependencies.DependsApiDb, user: auth.DependsBasicAdmin, - domain: web_models.Domain, + domain: web_models.CreateDomain, + bg: fastapi.BackgroundTasks, + no_check: str = "false", ) -> web_models.Domain: if "webmail" in domain.features and domain.context_name is None: raise fastapi.HTTPException( @@ -44,7 +46,8 @@ async def post_domain( imap_domains=domain.imap_domains, smtp_domains=domain.smtp_domains, ) - + if no_check == "false": + bg.add_task(dns.background_check_new_domain, domain.name) if "webmail" in domain.features: return web_models.Domain.from_db(domain_db, domain.context_name) else: diff --git a/src/routes/test_admin.py b/src/routes/test_admin.py index f171cac..d0f262d 100644 --- a/src/routes/test_admin.py +++ b/src/routes/test_admin.py @@ -313,12 +313,21 @@ def test_domains__create_successful(db_api_session, log, client, admin): assert response.status_code == fastapi.status.HTTP_201_CREATED assert response.json() == { "name": "domain", + "valid": False, + "state": "new", "features": ["mailbox", "webmail", "alias"], "mailbox_domain": None, "webmail_domain": None, "imap_domains": None, "smtp_domains": None, "context_name": "context", + "domain_exist": {"ok": True, "errors": []}, + "mx": {"ok": True, "errors": []}, + "cname_imap": {"ok": True, "errors": []}, + "cname_smtp": {"ok": True, "errors": []}, + "cname_webmail": {"ok": True, "errors": []}, + "spf": {"ok": True, "errors": []}, + "dkim": {"ok": True, "errors": []}, } # La creation d'un deuxieme domaine par un admin, dans le même contexte @@ -359,15 +368,9 @@ def test_allows__create_allows(db_api_session, log, client): # If we GET all the domains, we get the one newly created response = client.get("/domains/", auth=("admin", "admin_password")) assert response.status_code == fastapi.status.HTTP_200_OK - assert response.json() == [{ - 'name': 'domain', - 'features': [], - 'mailbox_domain': None, - 'webmail_domain': None, - 'imap_domains': None, - 'smtp_domains': None, - 'context_name': None, - }] + infos = response.json() + assert len(infos) == 1 + assert infos[0]["name"] == "domain" # Create allows for this user on this domain response = client.post( diff --git a/src/routes/test_domains.py b/src/routes/test_domains.py index 05705cf..714299d 100644 --- a/src/routes/test_domains.py +++ b/src/routes/test_domains.py @@ -23,12 +23,21 @@ def test_domains__get_domain_allowed_user(db_api, db_dovecot, log, client, norma assert response.status_code == fastapi.status.HTTP_200_OK assert response.json() == { "name": domain_name, + "valid": False, + "state": "new", "features": ["mailbox", "webmail"], "mailbox_domain": None, "webmail_domain": None, "imap_domains": None, "smtp_domains": None, "context_name": None, + "domain_exist": {"ok": True, "errors": []}, + "mx": {"ok": True, "errors": []}, + "cname_imap": {"ok": True, "errors": []}, + "cname_smtp": {"ok": True, "errors": []}, + "cname_webmail": {"ok": True, "errors": []}, + "spf": {"ok": True, "errors": []}, + "dkim": {"ok": True, "errors": []}, } @@ -73,12 +82,21 @@ def test_domains__get_domain_admin_always_authorized(db_api_session, domain, adm assert response.status_code == fastapi.status.HTTP_200_OK assert response.json() == { "name": domain_name, + "valid": False, + "state": "new", "features": domain_features, "mailbox_domain": None, "webmail_domain": None, "imap_domains": None, "smtp_domains": None, "context_name": None, + "domain_exist": {"ok": True, "errors": []}, + "mx": {"ok": True, "errors": []}, + "cname_imap": {"ok": True, "errors": []}, + "cname_smtp": {"ok": True, "errors": []}, + "cname_webmail": {"ok": True, "errors": []}, + "spf": {"ok": True, "errors": []}, + "dkim": {"ok": True, "errors": []}, } # If the domain does not exist -> not found @@ -112,3 +130,51 @@ def test_domains_create_failed(db_api_session, admin, log, client, domain): ) assert response.status_code == fastapi.status.HTTP_409_CONFLICT +@pytest.mark.parametrize( + "normal_user", + ["bidibule:toto"], + indirect=True, +) +@pytest.mark.parametrize( + "domain_web", + ["example.com:dimail"], + indirect=True +) +def test_domains_check_domain(db_api_session, admin, log, client, normal_user, domain_web): + auth=(admin["user"], admin["password"]) + + domain_name = domain_web["name"] + response = client.get(f"/domains/{domain_name}/check", auth=auth) + assert response.status_code == fastapi.status.HTTP_200_OK + infos = response.json() + assert infos["name"] == "example.com" + assert infos["state"] == "broken" + assert infos["valid"] == False + for key in [ "domain_exist", "mx", "cname_imap", "cname_smtp", "cname_webmail", "spf", "dkim" ]: + assert key in infos + assert "ok" in infos[key] + assert "errors" in infos[key] + if infos[key]["ok"]: + assert len(infos[key]["errors"]) == 0 + else: + assert len(infos[key]["errors"]) > 0 + assert infos["domain_exist"]["ok"] is True + assert infos["mx"]["ok"] is False + assert len(infos["mx"]["errors"]) == 1 + assert infos["mx"]["errors"][0]["code"] == "wrong_mx" + + assert infos["cname_imap"]["ok"] is False + assert len(infos["cname_imap"]["errors"]) == 1 + assert infos["cname_imap"]["errors"][0]["code"] == "no_cname_imap" + + assert infos["cname_smtp"]["ok"] is False + assert len(infos["cname_smtp"]["errors"]) == 1 + assert infos["cname_smtp"]["errors"][0]["code"] == "no_cname_smtp" + + assert infos["cname_webmail"]["ok"] is False + assert len(infos["cname_webmail"]["errors"]) == 1 + assert infos["cname_webmail"]["errors"][0]["code"] == "no_cname_webmail" + + assert infos["spf"]["ok"] is False + assert infos["dkim"]["ok"] is True + diff --git a/src/sql_api/__init__.py b/src/sql_api/__init__.py index ef529ef..c3552ac 100644 --- a/src/sql_api/__init__.py +++ b/src/sql_api/__init__.py @@ -13,6 +13,8 @@ get_domain, get_domains, update_domain_dtaction, + update_domain_errors, + update_domain_state, ) from .models import DBAllowed, DBDomain, DBUser from .user import ( @@ -46,6 +48,9 @@ delete_user, get_user, get_users, + update_domain_dtaction, + update_domain_errors, + update_domain_state, update_user_password, update_user_is_admin, ] diff --git a/src/sql_api/domain.py b/src/sql_api/domain.py index 26b513d..e63944b 100644 --- a/src/sql_api/domain.py +++ b/src/sql_api/domain.py @@ -53,6 +53,23 @@ def update_domain_state(db: orm.Session, name: str, state: str) -> models.DBDoma return db_domain +def update_domain_errors( + db: orm.Session, name: str, errors: list[str] | None +) -> models.DBDomain: + db_domain = get_domain(db, name) + if db_domain is None: + return None + try: + db_domain.errors = errors + db.flush() + db.commit() + except Exception: + db.rollback() + return None + db.refresh(db_domain) + return db_domain + + def update_domain_dtaction( db: orm.Session, name: str, dtaction: datetime.datetime ) -> models.DBDomain: diff --git a/src/sql_api/models.py b/src/sql_api/models.py index b46e0dd..ab82305 100644 --- a/src/sql_api/models.py +++ b/src/sql_api/models.py @@ -74,6 +74,7 @@ class DBDomain(Api): imap_domains = sa.Column(sa.JSON(), nullable=True) smtp_domains = sa.Column(sa.JSON(), nullable=True) state = sa.Column(sa.String(15), nullable=False, default="new") + errors = sa.Column(sa.JSON(), nullable=True) dtcreated = sa.Column(sa.DateTime(), nullable=False, server_default=sa.sql.func.now()) dtupdated = sa.Column(sa.DateTime(), nullable=False, server_default=sa.sql.func.now(), onupdate=sa.sql.func.now()) dtaction = sa.Column(sa.DateTime(), nullable=True, index=True) diff --git a/src/sql_api/test_basic.py b/src/sql_api/test_basic.py index f219a5f..65e3cc5 100644 --- a/src/sql_api/test_basic.py +++ b/src/sql_api/test_basic.py @@ -85,6 +85,7 @@ def test_create_domain(db_api_session, log): assert db_dom.smtp_domains is None assert db_dom.state == "new" assert db_dom.dtaction is None + assert db_dom.errors is None assert date_eq(db_dom.dtcreated, now) assert date_eq(db_dom.dtupdated, now) @@ -132,6 +133,14 @@ def test_create_domain(db_api_session, log): assert isinstance(db_dom, sql_api.DBDomain) assert db_dom.name == "domain_name" + db_dom = sql_api.update_domain_state(db_api_session, "domain_name", "broken") + assert isinstance(db_dom, sql_api.DBDomain) + assert db_dom.name == "domain_name" + assert db_dom.state == "broken" + + db_dom = sql_api.update_domain_errors(db_api_session, "domain_name", [ "coin", "pan", "kaï" ]) + assert isinstance(db_dom, sql_api.DBDomain) + assert db_dom.errors == [ "coin", "pan", "kaï" ] def test_create_user_bis(db_api_session): db_user = sql_api.create_user( diff --git a/src/web_models/__init__.py b/src/web_models/__init__.py index f36a2e7..acc4181 100644 --- a/src/web_models/__init__.py +++ b/src/web_models/__init__.py @@ -1,4 +1,13 @@ -from .admin import Allowed, CreateUser, UpdateUser, Domain, Feature, Token, User +from .admin import ( + Allowed, + CreateDomain, + CreateUser, + UpdateUser, + Domain, + Feature, + Token, + User, +) from .alias import Alias, CreateAlias from .mailbox import ( CreateMailbox, @@ -12,6 +21,7 @@ __all__ = [ Allowed, CreateAlias, + CreateDomain, CreateUser, Domain, Feature, diff --git a/src/web_models/admin.py b/src/web_models/admin.py index 569e34b..492bf7b 100644 --- a/src/web_models/admin.py +++ b/src/web_models/admin.py @@ -42,8 +42,32 @@ class UpdateUser(pydantic.BaseModel): is_admin: bool | None = None +class TestErr(pydantic.BaseModel): + code: str + detail: str + +class TestResult(pydantic.BaseModel): + ok: bool = True + errors: list[TestErr] = [] + + def add_err(self, err): + self.ok = False + self.errors.append(TestErr(code=err["code"], detail=err["detail"])) + +class CreateDomain(pydantic.BaseModel): + name: str + features: list[Feature] + mailbox_domain: str | None = None + webmail_domain: str | None = None + imap_domains: list[str] | None = None + smtp_domains: list[str] | None = None + context_name: str | None + class Domain(pydantic.BaseModel): name: str + state: str + valid: bool = True + features: list[Feature] mailbox_domain: str | None = None webmail_domain: str | None = None @@ -51,10 +75,26 @@ class Domain(pydantic.BaseModel): smtp_domains: list[str] | None = None context_name: str | None + domain_exist: TestResult = TestResult() + mx: TestResult = TestResult() + cname_imap: TestResult = TestResult() + cname_smtp: TestResult = TestResult() + cname_webmail: TestResult = TestResult() + spf: TestResult = TestResult() + dkim: TestResult = TestResult() + + def add_errors(self, errors: list): + if errors is None: + return + for err in errors: + self.valid = False + getattr(self, err["test"]).add_err(err) + @classmethod def from_db(cls, in_db: sql_api.DBDomain, ctx_name: str | None = None): - return cls( + res = cls( name=in_db.name, + state=in_db.state, features=in_db.features, mailbox_domain=in_db.mailbox_domain, webmail_domain=in_db.webmail_domain, @@ -62,6 +102,11 @@ def from_db(cls, in_db: sql_api.DBDomain, ctx_name: str | None = None): smtp_domains=in_db.smtp_domains, context_name=ctx_name, ) + if in_db.state == "new": + res.valid = False + if in_db.errors is not None: + res.add_errors(in_db.errors) + return res class Allowed(pydantic.BaseModel):