Skip to content

Commit 3b4568d

Browse files
authored
Route53: change_tags_for_resource(): Validate ZoneId parameter (#8984)
1 parent e12d7ed commit 3b4568d

File tree

4 files changed

+135
-7
lines changed

4 files changed

+135
-7
lines changed

moto/route53/models.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ def create_route53_zone_id( # type: ignore
6868
tags: Tags = None,
6969
) -> str:
7070
# New ID's look like this Z1RWWTK7Y8UDDQ
71-
return "".join([random.choice(ROUTE53_ID_CHOICE) for _ in range(0, 15)])
71+
return "".join([random.choice(ROUTE53_ID_CHOICE) for _ in range(0, 22)])
7272

7373

7474
def create_route53_caller_reference() -> str:
@@ -656,7 +656,12 @@ def disassociate_vpc_from_hosted_zone(self, zone_id: str, vpcid: str) -> FakeZon
656656
zone.delete_vpc(vpcid)
657657
return zone
658658

659-
def change_tags_for_resource(self, resource_id: str, tags: Any) -> None:
659+
def change_tags_for_resource(
660+
self, resource_type: str, resource_id: str, tags: Any
661+
) -> None:
662+
if resource_type == "hostedzone" and resource_id not in self.zones:
663+
raise NoSuchHostedZone(host_zone_id=resource_id)
664+
660665
if "Tag" in tags:
661666
if isinstance(tags["Tag"], list):
662667
for tag in tags["Tag"]:

moto/route53/responses.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from moto.core.common_types import TYPE_RESPONSE
1111
from moto.core.responses import BaseResponse
1212
from moto.core.utils import iso_8601_datetime_with_milliseconds
13-
from moto.route53.exceptions import InvalidChangeBatch
13+
from moto.route53.exceptions import InvalidChangeBatch, InvalidInput
1414
from moto.route53.models import Route53Backend, route53_backends
1515

1616
XMLNS = "https://route53.amazonaws.com/doc/2013-04-01/"
@@ -350,6 +350,14 @@ def list_or_change_tags_for_resource_request( # type: ignore[return]
350350
assert id_matcher
351351
type_ = id_matcher.group(1)
352352
id_ = unquote(id_matcher.group(2))
353+
if type_ == "hostedzone" and len(id_) > 32:
354+
# From testing, it looks like Route53 creates ID's that are either 21 or 22 characters long
355+
# In practice, this error will typically appear when passing in the full ID: `/hostedzone/{id}`
356+
# Users should pass in {id} instead
357+
# NOTE: we don't know (yet) what kind of validation (if any) is in place for type_==healthcheck.
358+
raise InvalidInput(
359+
f"1 validation error detected: Value '{id_}' at 'resourceId' failed to satisfy constraint: Member must have length less than or equal to 32"
360+
)
353361

354362
if request.method == "GET":
355363
tags = self.backend.list_tags_for_resource(id_)
@@ -368,7 +376,7 @@ def list_or_change_tags_for_resource_request( # type: ignore[return]
368376
elif "RemoveTagKeys" in tags:
369377
tags = tags["RemoveTagKeys"] # type: ignore
370378

371-
self.backend.change_tags_for_resource(id_, tags)
379+
self.backend.change_tags_for_resource(type_, id_, tags)
372380
template = Template(CHANGE_TAGS_FOR_RESOURCE_RESPONSE)
373381
return 200, headers, template.render()
374382

tests/test_route53/test_route53.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -478,7 +478,7 @@ def test_list_or_change_tags_for_resource_request():
478478
"HealthThreshold": 123,
479479
},
480480
)
481-
healthcheck_id = health_check["HealthCheck"]["Id"]
481+
healthcheck_id = health_check["HealthCheck"]["Id"].replace("/hostedzone/", "")
482482

483483
# confirm this works for resources with zero tags
484484
response = conn.list_tags_for_resource(
@@ -558,11 +558,11 @@ def test_list_tags_for_resources():
558558
zone1 = conn.create_hosted_zone(
559559
Name="testdns1.aws.com", CallerReference=str(hash("foo"))
560560
)
561-
zone1_id = zone1["HostedZone"]["Id"]
561+
zone1_id = zone1["HostedZone"]["Id"].replace("/hostedzone/", "")
562562
zone2 = conn.create_hosted_zone(
563563
Name="testdns2.aws.com", CallerReference=str(hash("bar"))
564564
)
565-
zone2_id = zone2["HostedZone"]["Id"]
565+
zone2_id = zone2["HostedZone"]["Id"].replace("/hostedzone/", "")
566566

567567
# Create two healthchecks
568568
health_check1 = conn.create_health_check(
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
"""
2+
Route53 has some idiosyncrasies related to HostedZone ids
3+
Some operations return `/hosted_zone/{id}`
4+
Other operations return `{id}`
5+
Some operations expect `/hosted_zone/{id}`
6+
Other operations expect `{id}`
7+
Some operations allow both (probably..)
8+
9+
The tests in this file are purely to test the different scenarios.
10+
11+
Note that all tests are verified against AWS.
12+
If you want to run the tests against AWS yourself, make sure `TEST_DOMAIN_NAME` is changed to a domain name that's known to AWS.
13+
"""
14+
15+
from functools import wraps
16+
from uuid import uuid4
17+
18+
import boto3
19+
import pytest
20+
from botocore.exceptions import ClientError
21+
22+
from moto import mock_aws
23+
from tests import allow_aws_request
24+
25+
TEST_DOMAIN_NAME = "mototests.click"
26+
TEST_HOSTED_ZONE_NAME = f"test.{TEST_DOMAIN_NAME}"
27+
28+
29+
def route53_aws_verified(func):
30+
@wraps(func)
31+
def pagination_wrapper(**kwargs):
32+
def create_hosted_zone():
33+
client = boto3.client("route53", "us-east-1")
34+
35+
hosted_zone = client.create_hosted_zone(
36+
Name=TEST_HOSTED_ZONE_NAME, CallerReference=str(uuid4())
37+
)["HostedZone"]
38+
39+
kwargs["hosted_zone"] = hosted_zone
40+
41+
try:
42+
return func(**kwargs)
43+
finally:
44+
client.delete_hosted_zone(Id=hosted_zone["Id"])
45+
46+
if allow_aws_request():
47+
return create_hosted_zone()
48+
else:
49+
with mock_aws():
50+
return create_hosted_zone()
51+
52+
return pagination_wrapper
53+
54+
55+
@pytest.mark.aws_verified
56+
@route53_aws_verified
57+
def test_hosted_zone_id_in_change_tags(hosted_zone=None):
58+
client = boto3.client("route53", "us-east-1")
59+
60+
full_zone_id = hosted_zone["Id"]
61+
assert full_zone_id.startswith("/hostedzone/")
62+
63+
# Can't use full ID here
64+
with pytest.raises(ClientError) as exc:
65+
client.change_tags_for_resource(
66+
ResourceType="hostedzone",
67+
ResourceId=full_zone_id,
68+
AddTags=[{"Key": "foo", "Value": "bar"}],
69+
)
70+
err = exc.value.response["Error"]
71+
assert err["Code"] == "InvalidInput"
72+
assert (
73+
err["Message"]
74+
== f"1 validation error detected: Value '{full_zone_id}' at 'resourceId' failed to satisfy constraint: Member must have length less than or equal to 32"
75+
)
76+
77+
# if we naively limit the id-length to 32, we get a NoSuchHostedZone-exception (as expected)
78+
with pytest.raises(ClientError) as exc:
79+
client.change_tags_for_resource(
80+
ResourceType="hostedzone",
81+
ResourceId=full_zone_id[0:32],
82+
AddTags=[{"Key": "foo", "Value": "bar"}],
83+
)
84+
err = exc.value.response["Error"]
85+
assert err["Code"] == "NoSuchHostedZone"
86+
87+
# Need to strip the '/hosted_zone/'-prefix
88+
id_without_prefix = full_zone_id.replace("/hostedzone/", "")
89+
client.change_tags_for_resource(
90+
ResourceType="hostedzone",
91+
ResourceId=id_without_prefix,
92+
AddTags=[{"Key": "foo", "Value": "bar"}],
93+
)
94+
95+
# Test retrieval of tags with full ID
96+
with pytest.raises(ClientError) as exc:
97+
client.list_tags_for_resource(
98+
ResourceType="hostedzone", ResourceId=full_zone_id
99+
)
100+
err = exc.value.response["Error"]
101+
assert err["Code"] == "InvalidInput"
102+
assert (
103+
err["Message"]
104+
== f"1 validation error detected: Value '{full_zone_id}' at 'resourceId' failed to satisfy constraint: Member must have length less than or equal to 32"
105+
)
106+
107+
# Test retrieval of tags with stripped ID
108+
tags = client.list_tags_for_resource(
109+
ResourceType="hostedzone", ResourceId=id_without_prefix
110+
)["ResourceTagSet"]
111+
assert tags == {
112+
"ResourceId": id_without_prefix,
113+
"ResourceType": "hostedzone",
114+
"Tags": [{"Key": "foo", "Value": "bar"}],
115+
}

0 commit comments

Comments
 (0)