Skip to content

Commit 0bab17c

Browse files
committed
Drop region validation, add URL normalization and configurable timeout
1 parent 9883332 commit 0bab17c

2 files changed

Lines changed: 108 additions & 114 deletions

File tree

awscli/customizations/sso/resolve.py

Lines changed: 32 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,21 @@
4040
),
4141
)
4242

43-
_ALL_PARTITIONS = ('aws', 'aws-cn', 'aws-us-gov')
44-
4543
_MAX_REDIRECTS = 1
4644

45+
_DEFAULT_PORTS = {'https': 443, 'http': 80}
46+
47+
_DEFAULT_TIMEOUT = 10
48+
49+
50+
def _normalize_url(url):
51+
parsed = urllib.parse.urlparse(url)
52+
netloc = parsed.hostname or ''
53+
if parsed.port and parsed.port != _DEFAULT_PORTS.get(parsed.scheme):
54+
netloc = f'{netloc}:{parsed.port}'
55+
path = parsed.path.rstrip('/')
56+
return urllib.parse.urlunparse((parsed.scheme, netloc, path, '', '', ''))
57+
4758

4859
def is_aws_owned_domain(hostname):
4960
hostname = hostname.lower().rstrip('.')
@@ -66,38 +77,25 @@ def _extract_region_from_hostname(hostname):
6677
return None
6778

6879

69-
def _validate_region(region, session):
70-
available = set()
71-
for partition in _ALL_PARTITIONS:
72-
available.update(
73-
session.get_available_regions('sso-oidc', partition_name=partition)
74-
)
75-
if region not in available:
76-
raise ConfigurationError(
77-
f"Region '{region}' parsed from the resolved start URL is not "
78-
f"a known AWS region. Verify the start URL is correct."
79-
)
80-
81-
82-
def _follow_redirect(url):
80+
def _follow_redirect(url, timeout=_DEFAULT_TIMEOUT):
8381
class _NoRedirectHandler(urllib.request.HTTPRedirectHandler):
8482
def redirect_request(self, req, fp, code, msg, headers, newurl):
8583
return None
8684

8785
opener = urllib.request.build_opener(_NoRedirectHandler)
8886
redirect_codes = (301, 302, 303, 307, 308)
8987

90-
for _attempt in range(_MAX_REDIRECTS):
88+
for _ in range(_MAX_REDIRECTS):
9189
try:
9290
req = urllib.request.Request(url, method='HEAD')
93-
resp = opener.open(req, timeout=10)
91+
resp = opener.open(req, timeout=timeout)
9492
resp.close()
9593
return url
9694
except urllib.error.HTTPError as e:
9795
if e.code in (405, 501):
9896
try:
9997
req = urllib.request.Request(url, method='GET')
100-
resp = opener.open(req, timeout=10)
98+
resp = opener.open(req, timeout=timeout)
10199
resp.close()
102100
return url
103101
except urllib.error.HTTPError as e2:
@@ -135,7 +133,9 @@ def redirect_request(self, req, fp, code, msg, headers, newurl):
135133
return url
136134

137135

138-
def resolve_start_url(start_url, session, configured_region=None):
136+
def resolve_start_url(
137+
start_url, session, configured_region=None, timeout=None
138+
):
139139
parsed = urllib.parse.urlparse(start_url)
140140

141141
if parsed.scheme != 'https':
@@ -158,7 +158,10 @@ def resolve_start_url(start_url, session, configured_region=None):
158158
LOG.debug(
159159
"Start URL '%s' is not AWS-owned, following redirects", start_url
160160
)
161-
resolved_url = _follow_redirect(start_url)
161+
effective_timeout = timeout if timeout is not None else _DEFAULT_TIMEOUT
162+
resolved_url = _normalize_url(
163+
_follow_redirect(start_url, timeout=effective_timeout)
164+
)
162165

163166
resolved_hostname = urllib.parse.urlparse(resolved_url).hostname
164167
if not resolved_hostname or not is_aws_owned_domain(resolved_hostname):
@@ -174,14 +177,13 @@ def resolve_start_url(start_url, session, configured_region=None):
174177

175178
region = _extract_region_from_hostname(resolved_hostname)
176179

177-
if region:
178-
_validate_region(region, session)
179-
elif configured_region:
180-
region = configured_region
181-
else:
182-
raise ConfigurationError(
183-
f"Cannot determine region from start URL '{start_url}'. "
184-
f"Please provide sso_region in your configuration."
185-
)
180+
if not region:
181+
if configured_region:
182+
region = configured_region
183+
else:
184+
raise ConfigurationError(
185+
f"Cannot determine region from start URL '{start_url}'. "
186+
f"Please provide sso_region in your configuration."
187+
)
186188

187189
return resolved_url, region

tests/unit/customizations/sso/test_resolve.py

Lines changed: 76 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from awscli.customizations.sso.resolve import (
1919
_extract_region_from_hostname,
2020
_follow_redirect,
21+
_normalize_url,
2122
is_aws_owned_domain,
2223
resolve_start_url,
2324
)
@@ -87,19 +88,25 @@ def test_returns_none_for_region_less_hostnames(self, hostname):
8788
assert _extract_region_from_hostname(hostname) is None
8889

8990

91+
class TestNormalizeUrl:
92+
@pytest.mark.parametrize(
93+
'url, expected',
94+
[
95+
('https://example.com:443/', 'https://example.com'),
96+
('https://example.com:443/path/', 'https://example.com/path'),
97+
('https://example.com/', 'https://example.com'),
98+
('https://example.com:8443/path', 'https://example.com:8443/path'),
99+
('http://example.com:80/', 'http://example.com'),
100+
('https://example.com', 'https://example.com'),
101+
],
102+
)
103+
def test_normalize_url(self, url, expected):
104+
assert _normalize_url(url) == expected
105+
106+
90107
class TestResolveStartUrl:
91-
def _mock_session(self, regions=None):
92-
if regions is None:
93-
regions = [
94-
'us-east-1',
95-
'us-west-2',
96-
'eu-west-1',
97-
'us-gov-west-1',
98-
'cn-north-1',
99-
]
100-
session = mock.Mock()
101-
session.get_available_regions.return_value = regions
102-
return session
108+
def _mock_session(self):
109+
return mock.Mock()
103110

104111
def test_aws_owned_url_returns_configured_region(self):
105112
session = self._mock_session()
@@ -139,6 +146,7 @@ def test_aws_owned_url_uses_configured_region_not_hostname(self):
139146
session=session,
140147
configured_region='us-east-1',
141148
)
149+
assert resolved_url == 'https://ssoins-abc.portal.us-west-2.app.aws'
142150
assert region == 'us-east-1'
143151

144152
def test_awsapps_url_requires_configured_region(self):
@@ -172,96 +180,39 @@ def test_invalid_url_raises_error(self):
172180
with pytest.raises(ConfigurationError, match='Invalid sso_start_url'):
173181
resolve_start_url('https://', session=session)
174182

175-
def test_vanity_url_invalid_region_raises_error(self):
176-
session = self._mock_session(regions=['us-east-1', 'us-west-2'])
177-
with mock.patch(
178-
'awscli.customizations.sso.resolve._follow_redirect'
179-
) as mock_follow:
180-
mock_follow.return_value = (
181-
'https://ssoins-abc.portal.fake-region-1.app.aws'
182-
)
183-
with pytest.raises(
184-
ConfigurationError, match='not a known AWS region'
185-
):
186-
resolve_start_url(
187-
'https://aws.mycompany.com',
188-
session=session,
189-
)
183+
def test_vanity_url_follows_redirect(self):
184+
session = self._mock_session()
185+
redirect_url = 'https://ssoins-abc.portal.us-east-1.app.aws:443/'
186+
normalized_url = 'https://ssoins-abc.portal.us-east-1.app.aws'
190187

191-
def test_vanity_url_invalid_region_does_not_fall_back_to_configured(self):
192-
session = self._mock_session(regions=['us-east-1', 'us-west-2'])
193-
with mock.patch(
194-
'awscli.customizations.sso.resolve._follow_redirect'
195-
) as mock_follow:
196-
mock_follow.return_value = (
197-
'https://ssoins-abc.portal.fake-region-1.app.aws'
198-
)
199-
with pytest.raises(
200-
ConfigurationError, match='not a known AWS region'
201-
):
202-
resolve_start_url(
203-
'https://aws.mycompany.com',
204-
session=session,
205-
configured_region='us-east-1',
206-
)
207-
208-
def test_vanity_url_govcloud_region_accepted(self):
209-
session = mock.Mock()
210-
session.get_available_regions.side_effect = (
211-
lambda service, partition_name='aws': {
212-
'aws': ['us-east-1', 'us-west-2'],
213-
'aws-us-gov': ['us-gov-west-1'],
214-
'aws-cn': ['cn-north-1'],
215-
}.get(partition_name, [])
216-
)
217188
with mock.patch(
218189
'awscli.customizations.sso.resolve._follow_redirect'
219190
) as mock_follow:
220-
mock_follow.return_value = (
221-
'https://ssoins-abc.portal.us-gov-west-1.app.aws'
222-
)
191+
mock_follow.return_value = redirect_url
223192
resolved_url, region = resolve_start_url(
224193
'https://aws.mycompany.com',
225194
session=session,
226195
)
227-
assert region == 'us-gov-west-1'
228-
229-
def test_vanity_url_china_region_accepted(self):
230-
session = mock.Mock()
231-
session.get_available_regions.side_effect = (
232-
lambda service, partition_name='aws': {
233-
'aws': ['us-east-1', 'us-west-2'],
234-
'aws-us-gov': ['us-gov-west-1'],
235-
'aws-cn': ['cn-north-1'],
236-
}.get(partition_name, [])
237-
)
238-
with mock.patch(
239-
'awscli.customizations.sso.resolve._follow_redirect'
240-
) as mock_follow:
241-
mock_follow.return_value = (
242-
'https://ssoins-abc.portal.cn-north-1.app.aws'
243-
)
244-
resolved_url, region = resolve_start_url(
245-
'https://aws.mycompany.com',
246-
session=session,
196+
assert resolved_url == normalized_url
197+
assert region == 'us-east-1'
198+
mock_follow.assert_called_once_with(
199+
'https://aws.mycompany.com', timeout=10
247200
)
248-
assert region == 'cn-north-1'
249201

250-
def test_vanity_url_follows_redirect(self):
202+
def test_vanity_url_uses_parsed_region_not_configured(self):
251203
session = self._mock_session()
252-
redirect_url = 'https://ssoins-abc.portal.us-east-1.app.aws:443/'
253-
254204
with mock.patch(
255205
'awscli.customizations.sso.resolve._follow_redirect'
256206
) as mock_follow:
257-
mock_follow.return_value = redirect_url
207+
mock_follow.return_value = (
208+
'https://ssoins-abc.portal.us-west-2.app.aws'
209+
)
258210
resolved_url, region = resolve_start_url(
259211
'https://aws.mycompany.com',
260212
session=session,
213+
configured_region='eu-west-1',
261214
)
262-
assert resolved_url == redirect_url
263-
assert region == 'us-east-1'
264-
mock_follow.assert_called_once_with('https://aws.mycompany.com')
215+
assert region == 'us-west-2'
265216

266217
def test_vanity_url_resolves_to_non_aws_domain_raises_error(self):
267218
session = self._mock_session()
@@ -283,6 +234,47 @@ def test_vanity_url_resolves_to_http_raises_error(self):
283234
with pytest.raises(ConfigurationError, match='must use https'):
284235
resolve_start_url('https://aws.mycompany.com', session=session)
285236

237+
def test_vanity_url_region_less_requires_configured_region(self):
238+
session = self._mock_session()
239+
with mock.patch(
240+
'awscli.customizations.sso.resolve._follow_redirect'
241+
) as mock_follow:
242+
mock_follow.return_value = 'https://d-abc123.awsapps.com/start'
243+
with pytest.raises(
244+
ConfigurationError, match='Cannot determine region'
245+
):
246+
resolve_start_url('https://aws.mycompany.com', session=session)
247+
248+
def test_vanity_url_region_less_uses_configured_region(self):
249+
session = self._mock_session()
250+
with mock.patch(
251+
'awscli.customizations.sso.resolve._follow_redirect'
252+
) as mock_follow:
253+
mock_follow.return_value = 'https://d-abc123.awsapps.com/start'
254+
resolved_url, region = resolve_start_url(
255+
'https://aws.mycompany.com',
256+
session=session,
257+
configured_region='us-east-1',
258+
)
259+
assert region == 'us-east-1'
260+
261+
def test_custom_timeout_passed_to_follow_redirect(self):
262+
session = self._mock_session()
263+
with mock.patch(
264+
'awscli.customizations.sso.resolve._follow_redirect'
265+
) as mock_follow:
266+
mock_follow.return_value = (
267+
'https://ssoins-abc.portal.us-east-1.app.aws'
268+
)
269+
resolve_start_url(
270+
'https://aws.mycompany.com',
271+
session=session,
272+
timeout=5,
273+
)
274+
mock_follow.assert_called_once_with(
275+
'https://aws.mycompany.com', timeout=5
276+
)
277+
286278

287279
class TestFollowRedirect:
288280
def _make_http_error(self, code, headers=None):

0 commit comments

Comments
 (0)