From 24b94fc192dccb311f07d2e8246d0ab27f3d38dc Mon Sep 17 00:00:00 2001 From: Joey Gracey Date: Sat, 27 Jan 2024 04:29:00 -0800 Subject: [PATCH] Support Route53 create_hosted_zone raising ClientError ConflictingDomainExists (#7249) --- moto/route53/exceptions.py | 15 ++++++++- moto/route53/models.py | 21 ++++++++++++ tests/test_route53/test_route53.py | 51 ++++++++++++++++++++++++++++++ 3 files changed, 86 insertions(+), 1 deletion(-) diff --git a/moto/route53/exceptions.py b/moto/route53/exceptions.py index c5d5d7459..9d236459a 100644 --- a/moto/route53/exceptions.py +++ b/moto/route53/exceptions.py @@ -1,5 +1,5 @@ """Exceptions raised by the Route53 service.""" -from typing import Any +from typing import Any, Optional from moto.core.exceptions import RESTError @@ -12,6 +12,19 @@ class Route53ClientError(RESTError): super().__init__(*args, **kwargs) +class ConflictingDomainExists(Route53ClientError): + """Domain already exists.""" + + code = 400 + + def __init__(self, domain_name: str, delegation_set_id: Optional[str]) -> None: + message = ( + f"Cannot create hosted zone with DelegationSetId DelegationSetId:{delegation_set_id} as the DNSName" + f"{domain_name} conflicts with existing ones sharing the delegation set" + ) + super().__init__("ConflictingDomainExists", message) + + class InvalidInput(Route53ClientError): """Malformed ARN for the CloudWatch log group.""" diff --git a/moto/route53/models.py b/moto/route53/models.py index ccfd5dc4d..195056cb0 100644 --- a/moto/route53/models.py +++ b/moto/route53/models.py @@ -12,6 +12,7 @@ from jinja2 import Template from moto.core import BackendDict, BaseBackend, BaseModel, CloudFormationModel from moto.moto_api._internal import mock_random as random from moto.route53.exceptions import ( + ConflictingDomainExists, DnsNameInvalidForZone, HostedZoneNotEmpty, InvalidActionValue, @@ -532,6 +533,24 @@ class Route53Backend(BaseBackend): self.query_logging_configs: Dict[str, QueryLoggingConfig] = {} self.delegation_sets: Dict[str, DelegationSet] = dict() + def _has_prev_conflicting_domain( + self, name: str, delegation_set_id: Optional[str] + ) -> bool: + """Check if a conflicting domain exists in the backend""" + if not delegation_set_id: + return False + for zone in self.zones.values(): + if not zone.delegation_set or zone.delegation_set.id != delegation_set_id: + # Delegation sets don't match, so can't possibly conflict + continue + if ( + zone.name == name + or zone.name.endswith(f".{name}") + or name.endswith(f".{zone.name}") + ): + return True + return False + def create_hosted_zone( self, name: str, @@ -542,6 +561,8 @@ class Route53Backend(BaseBackend): comment: Optional[str] = None, delegation_set_id: Optional[str] = None, ) -> FakeZone: + if self._has_prev_conflicting_domain(name, delegation_set_id): + raise ConflictingDomainExists(name, delegation_set_id) new_id = create_route53_zone_id() caller_reference = caller_reference or create_route53_caller_reference() delegation_set = self.create_reusable_delegation_set( diff --git a/tests/test_route53/test_route53.py b/tests/test_route53/test_route53.py index 149e34958..a40418523 100644 --- a/tests/test_route53/test_route53.py +++ b/tests/test_route53/test_route53.py @@ -1516,3 +1516,54 @@ def test_get_dns_sec(): )["HostedZone"]["Id"] dns_sec = client.get_dnssec(HostedZoneId=hosted_zone_id) assert dns_sec["Status"] == {"ServeSignature": "NOT_SIGNING"} + + +@mock_route53 +@pytest.mark.parametrize( + "domain1,domain2", + ( + ["a.com", "a.com"], + ["a.b.com", "b.com"], + ["b.com", "a.b.com"], + ["a.b.com", "a.b.com"], + ), +) +def test_conflicting_domain_exists(domain1, domain2): + delegation_set_id = "N10015061S366L6NMTRKQ" + conn = boto3.client("route53", region_name="us-east-1") + conn.create_hosted_zone( + Name=domain1, + CallerReference=str(hash("foo")), + DelegationSetId=delegation_set_id, + ) + with pytest.raises(ClientError) as exc_info: + conn.create_hosted_zone( + Name=domain2, + CallerReference=str(hash("bar")), + DelegationSetId=delegation_set_id, + ) + assert exc_info.value.response.get("Error").get("Code") == "ConflictingDomainExists" + for string in [delegation_set_id, domain2]: + assert string in exc_info.value.response.get("Error").get("Message") + + # Now test that these domains can be created with different delegation set ids + conn.create_hosted_zone( + Name=domain1, + CallerReference=str(hash("foo")), + ) + conn.create_hosted_zone( + Name=domain2, + CallerReference=str(hash("bar")), + ) + + # And, finally, test that these domains can be created with different named delegation sets + conn.create_hosted_zone( + Name=domain1, + CallerReference=str(hash("foo")), + DelegationSetId="1", + ) + conn.create_hosted_zone( + Name=domain2, + CallerReference=str(hash("bar")), + DelegationSetId="2", + )