Skip to content

Commit 68dda72

Browse files
authored
add app service api 2017 backward compatibility support (Azure#23626)
* add app service api 2017 backward compatibility support * add tests
1 parent fc295e7 commit 68dda72

File tree

3 files changed

+151
-0
lines changed

3 files changed

+151
-0
lines changed

sdk/identity/azure-identity/azure/identity/_credentials/azure_ml.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ def _get_client_args(**kwargs):
4444

4545
return dict(
4646
kwargs,
47+
_content_callback=_parse_expires_on,
4748
identity_config=identity_config,
4849
base_headers={"secret": secret},
4950
request_factory=functools.partial(_get_request, url),
@@ -55,3 +56,36 @@ def _get_request(url, scope, identity_config):
5556
request = HttpRequest("GET", url)
5657
request.format_parameters(dict({"api-version": "2017-09-01", "resource": scope}, **identity_config))
5758
return request
59+
60+
def _parse_expires_on(content):
61+
# type: (dict) -> None
62+
"""Parse an App Service MSI version 2017-09-01 expires_on value to epoch seconds.
63+
This version of the API returns expires_on as a UTC datetime string rather than epoch seconds. The string's
64+
format depends on the OS. Responses on Windows include AM/PM, for example "1/16/2020 5:24:12 AM +00:00".
65+
Responses on Linux do not, for example "06/20/2019 02:57:58 +00:00".
66+
:raises ValueError: ``expires_on`` didn't match an expected format
67+
"""
68+
69+
# Azure ML sets the same environment variables as App Service but returns expires_on as an integer.
70+
# That means we could have an Azure ML response here, so let's first try to parse expires_on as an int.
71+
try:
72+
content["expires_on"] = int(content["expires_on"])
73+
return
74+
except ValueError:
75+
pass
76+
77+
import calendar
78+
import time
79+
80+
expires_on = content["expires_on"]
81+
if expires_on.endswith(" +00:00"):
82+
date_string = expires_on[: -len(" +00:00")]
83+
for format_string in ("%m/%d/%Y %H:%M:%S", "%m/%d/%Y %I:%M:%S %p"): # (Linux, Windows)
84+
try:
85+
t = time.strptime(date_string, format_string)
86+
content["expires_on"] = calendar.timegm(t)
87+
return
88+
except ValueError:
89+
pass
90+
91+
raise ValueError("'{}' doesn't match the expected format".format(expires_on))

sdk/identity/azure-identity/tests/test_managed_identity.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,63 @@ def test_cloud_shell_user_assigned_identity():
378378
assert token.expires_on == expires_on
379379

380380

381+
def test_app_service_2017_09_01():
382+
"""When the environment for 2019-08-01 is not configured, 2017-09-01 should be used."""
383+
384+
access_token = "****"
385+
expires_on = 42
386+
expected_token = AccessToken(access_token, expires_on)
387+
url = "http://localhost:42/token"
388+
secret = "expected-secret"
389+
scope = "scope"
390+
391+
transport = validating_transport(
392+
requests=[
393+
Request(
394+
url,
395+
method="GET",
396+
required_headers={"secret": secret, "User-Agent": USER_AGENT},
397+
required_params={"api-version": "2017-09-01", "resource": scope},
398+
)
399+
]
400+
* 2,
401+
responses=[
402+
mock_response(
403+
json_payload={
404+
"access_token": access_token,
405+
"expires_on": "01/01/1970 00:00:{} +00:00".format(expires_on), # linux format
406+
"resource": scope,
407+
"token_type": "Bearer",
408+
}
409+
),
410+
mock_response(
411+
json_payload={
412+
"access_token": access_token,
413+
"expires_on": "1/1/1970 12:00:{} AM +00:00".format(expires_on), # windows format
414+
"resource": scope,
415+
"token_type": "Bearer",
416+
}
417+
),
418+
],
419+
)
420+
421+
with mock.patch.dict(
422+
MANAGED_IDENTITY_ENVIRON,
423+
{
424+
EnvironmentVariables.MSI_ENDPOINT: url,
425+
EnvironmentVariables.MSI_SECRET: secret,
426+
},
427+
clear=True,
428+
):
429+
token = ManagedIdentityCredential(transport=transport).get_token(scope)
430+
assert token == expected_token
431+
assert token.expires_on == expires_on
432+
433+
token = ManagedIdentityCredential(transport=transport).get_token(scope)
434+
assert token == expected_token
435+
assert token.expires_on == expires_on
436+
437+
381438
def test_prefers_app_service_2019_08_01():
382439
"""When the environment is configured for both App Service versions, the credential should prefer the most recent"""
383440

sdk/identity/azure-identity/tests/test_managed_identity_async.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,66 @@ async def test_cloud_shell_user_assigned_identity():
379379
assert token.expires_on == expires_on
380380

381381

382+
@pytest.mark.asyncio
383+
async def test_app_service_2017_09_01():
384+
"""When the environment for 2019-08-01 is not configured, 2017-09-01 should be used."""
385+
386+
access_token = "****"
387+
expires_on = 42
388+
expected_token = AccessToken(access_token, expires_on)
389+
url = "http://localhost:42/token"
390+
secret = "expected-secret"
391+
scope = "scope"
392+
393+
transport = async_validating_transport(
394+
requests=[
395+
Request(
396+
url,
397+
method="GET",
398+
required_headers={"secret": secret, "User-Agent": USER_AGENT},
399+
required_params={"api-version": "2017-09-01", "resource": scope},
400+
)
401+
]
402+
* 2,
403+
responses=[
404+
mock_response(
405+
json_payload={
406+
"access_token": access_token,
407+
"expires_on": "01/01/1970 00:00:{} +00:00".format(expires_on), # linux format
408+
"resource": scope,
409+
"token_type": "Bearer",
410+
}
411+
),
412+
mock_response(
413+
json_payload={
414+
"access_token": access_token,
415+
"expires_on": "1/1/1970 12:00:{} AM +00:00".format(expires_on), # windows format
416+
"resource": scope,
417+
"token_type": "Bearer",
418+
}
419+
),
420+
],
421+
)
422+
423+
with mock.patch.dict(
424+
MANAGED_IDENTITY_ENVIRON,
425+
{
426+
EnvironmentVariables.MSI_ENDPOINT: url,
427+
EnvironmentVariables.MSI_SECRET: secret,
428+
},
429+
clear=True,
430+
):
431+
credential = ManagedIdentityCredential(transport=transport)
432+
token = await credential.get_token(scope)
433+
assert token == expected_token
434+
assert token.expires_on == expires_on
435+
436+
credential = ManagedIdentityCredential(transport=transport)
437+
token = await credential.get_token(scope)
438+
assert token == expected_token
439+
assert token.expires_on == expires_on
440+
441+
382442
@pytest.mark.asyncio
383443
async def test_app_service_2019_08_01():
384444
"""App Service 2019-08-01: IDENTITY_ENDPOINT, IDENTITY_HEADER set"""

0 commit comments

Comments
 (0)