Skip to content

Commit 55dd09f

Browse files
Migrate region discovery to IMDS /compute JSON endpoint (#929)
1 parent 62a8047 commit 55dd09f

2 files changed

Lines changed: 100 additions & 10 deletions

File tree

msal/region.py

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import json
12
import os
23
import logging
34
import re
@@ -6,6 +7,10 @@
67

78
_VALID_REGION_RE = re.compile(r"^[a-z][a-z0-9-]*$")
89

10+
# IMDS compute metadata API version used for region auto-discovery.
11+
# Bump this single constant when moving to a newer IMDS API version.
12+
_IMDS_API_VERSION = "2021-02-01"
13+
914

1015
def _validate_region(region, source="unknown"):
1116
"""Return *region* unchanged if it looks like a valid Azure region name,
@@ -30,15 +35,11 @@ def _detect_region(http_client=None):
3035

3136
def _detect_region_of_azure_vm(http_client):
3237
url = (
33-
"http://169.254.169.254/metadata/instance"
34-
35-
# Utilize the "route parameters" feature to obtain region as a string
36-
# https://docs.microsoft.com/en-us/azure/virtual-machines/windows/instance-metadata-service?tabs=linux#route-parameters
37-
"/compute/location?format=text"
38+
"http://169.254.169.254/metadata/instance/compute"
3839

39-
# Location info is available since API version 2017-04-02
40-
# https://docs.microsoft.com/en-us/azure/virtual-machines/windows/instance-metadata-service?tabs=linux#response-1
41-
"&api-version=2021-01-01"
40+
# The region is read from the "location" field of the compute metadata.
41+
# https://learn.microsoft.com/en-us/azure/virtual-machines/instance-metadata-service?tabs=linux#response-1
42+
"?api-version=" + _IMDS_API_VERSION
4243
)
4344
logger.info(
4445
"Connecting to IMDS {}. "
@@ -56,5 +57,16 @@ def _detect_region_of_azure_vm(http_client):
5657
"IMDS {} unavailable. Perhaps not running in Azure VM?".format(url))
5758
return None
5859
else:
59-
return _validate_region(resp.text.strip(), source="IMDS endpoint")
60+
try:
61+
location = json.loads(resp.text).get("location")
62+
except (ValueError, AttributeError, TypeError):
63+
# ValueError: body is not valid JSON;
64+
# AttributeError: body is valid JSON but not a JSON object;
65+
# TypeError: resp.text is not a string (e.g. a custom http_client).
66+
logger.info("IMDS {} returned a malformed response.".format(url))
67+
return None
68+
if location is not None and not isinstance(location, str):
69+
logger.info("IMDS {} returned a non-string location.".format(url))
70+
return None
71+
return _validate_region(location, source="IMDS endpoint")
6072

tests/test_region.py

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,32 @@
11
import os
22
import unittest
3+
from types import SimpleNamespace
34
from unittest.mock import patch
45

5-
from msal.region import _detect_region, _validate_region
6+
from msal.region import (
7+
_detect_region, _detect_region_of_azure_vm, _validate_region)
8+
9+
from tests.http_client import MinimalResponse
10+
11+
12+
class _StubHttpClient(object):
13+
"""Records the requested URL/headers and returns a preconfigured response.
14+
15+
If *response* is an exception instance, it is raised from ``get`` to
16+
simulate a network failure (e.g. not running in an Azure VM)."""
17+
18+
def __init__(self, response):
19+
self._response = response
20+
self.url = None
21+
self.headers = None
22+
23+
def get(self, url, params=None, headers=None, **kwargs):
24+
self.url = url
25+
self.headers = headers
26+
if isinstance(self._response, Exception):
27+
raise self._response
28+
return self._response
29+
630

731

832
class TestValidateRegion(unittest.TestCase):
@@ -55,5 +79,59 @@ def test_empty_env_returns_none(self):
5579
self.assertIsNone(_detect_region())
5680

5781

82+
class TestDetectRegionOfAzureVm(unittest.TestCase):
83+
84+
def test_valid_location_is_returned(self):
85+
client = _StubHttpClient(
86+
MinimalResponse(status_code=200, text='{"location": "westus2"}'))
87+
self.assertEqual(_detect_region_of_azure_vm(client), "westus2")
88+
89+
def test_request_uses_compute_json_endpoint(self):
90+
client = _StubHttpClient(
91+
MinimalResponse(status_code=200, text='{"location": "westus2"}'))
92+
_detect_region_of_azure_vm(client)
93+
self.assertEqual(
94+
client.url,
95+
"http://169.254.169.254/metadata/instance/compute"
96+
"?api-version=2021-02-01")
97+
self.assertNotIn("/location", client.url)
98+
self.assertNotIn("format=text", client.url)
99+
self.assertEqual(client.headers, {"Metadata": "true"})
100+
101+
def test_missing_location_returns_none(self):
102+
client = _StubHttpClient(MinimalResponse(status_code=200, text="{}"))
103+
self.assertIsNone(_detect_region_of_azure_vm(client))
104+
105+
def test_null_location_returns_none(self):
106+
client = _StubHttpClient(
107+
MinimalResponse(status_code=200, text='{"location": null}'))
108+
self.assertIsNone(_detect_region_of_azure_vm(client))
109+
110+
def test_malformed_json_returns_none(self):
111+
client = _StubHttpClient(
112+
MinimalResponse(status_code=200, text="not json"))
113+
self.assertIsNone(_detect_region_of_azure_vm(client))
114+
115+
def test_invalid_location_value_returns_none(self):
116+
client = _StubHttpClient(
117+
MinimalResponse(status_code=200, text='{"location": "evil.com/hijack"}'))
118+
self.assertIsNone(_detect_region_of_azure_vm(client))
119+
120+
def test_non_string_location_returns_none(self):
121+
client = _StubHttpClient(
122+
MinimalResponse(status_code=200, text='{"location": 123}'))
123+
self.assertIsNone(_detect_region_of_azure_vm(client))
124+
125+
def test_non_string_response_text_returns_none(self):
126+
# A custom http_client could yield a non-string resp.text; json.loads
127+
# would raise TypeError, which must be treated as a malformed response.
128+
client = _StubHttpClient(SimpleNamespace(status_code=200, text=None))
129+
self.assertIsNone(_detect_region_of_azure_vm(client))
130+
131+
def test_network_failure_returns_none(self):
132+
client = _StubHttpClient(IOError("IMDS unreachable"))
133+
self.assertIsNone(_detect_region_of_azure_vm(client))
134+
135+
58136
if __name__ == "__main__":
59137
unittest.main()

0 commit comments

Comments
 (0)