Skip to content

Commit 57bc5f5

Browse files
authored
[Identity] Log user-assigned MI IDs (Azure#39621)
Signed-off-by: Paul Van Eck <[email protected]>
1 parent 7f21254 commit 57bc5f5

File tree

5 files changed

+163
-31
lines changed

5 files changed

+163
-31
lines changed

sdk/identity/azure-identity/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
### Other Changes
2626

2727
- `AzureCliCredential` and `AzureDeveloperCliCredential` will now call their corresponding executables directly instead of going through the shell. ([#38606](https://github.com/Azure/azure-sdk-for-python/pull/38606))
28+
- `ManagedIdentityCredential` will now log the configured user-assigned identity if one is set. ([#39621](https://github.com/Azure/azure-sdk-for-python/pull/39621))
2829

2930
## 1.19.0 (2024-10-08)
3031

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

Lines changed: 47 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
# ------------------------------------
55
import logging
66
import os
7-
from typing import Optional, Any, Mapping, cast
7+
from typing import Optional, Any, Mapping, cast, Tuple
88

99
from azure.core.credentials import AccessToken, AccessTokenInfo, TokenRequestOptions, TokenCredential, SupportsTokenInfo
1010
from .. import CredentialUnavailableError
@@ -15,20 +15,34 @@
1515
_LOGGER = logging.getLogger(__name__)
1616

1717

18-
def validate_identity_config(client_id: Optional[str], identity_config: Optional[Mapping[str, str]]) -> None:
18+
def validate_identity_config(
19+
client_id: Optional[str], identity_config: Optional[Mapping[str, str]]
20+
) -> Optional[Tuple[str, str]]:
1921
if identity_config:
22+
valid_keys = {"object_id", "resource_id", "client_id"}
2023
if client_id:
21-
if any(key in identity_config for key in ("object_id", "resource_id", "client_id")):
24+
if any(key in identity_config for key in valid_keys):
2225
raise ValueError(
23-
"identity_config must not contain 'object_id', 'resource_id', or 'client_id' when 'client_id' is "
24-
"provided as a keyword argument."
26+
"When 'client_id' is provided as a keyword argument, 'identity_config' must not contain any of the "
27+
f"following keys: {', '.join(valid_keys)}"
2528
)
26-
# Only one of these keys should be present if one is present.
27-
valid_keys = {"object_id", "resource_id", "client_id"}
28-
if len(identity_config.keys() & valid_keys) > 1:
29-
raise ValueError(
30-
f"identity_config must not contain more than one of the following keys: {', '.join(valid_keys)}"
31-
)
29+
return "client_id", client_id
30+
31+
# Only one of the valid keys should be present if one is present.
32+
result = None
33+
for key in valid_keys:
34+
if key in identity_config:
35+
if result:
36+
raise ValueError(
37+
"identity_config must not contain more than one of the following keys: "
38+
f"{', '.join(valid_keys)}"
39+
)
40+
result = key, identity_config[key]
41+
return result
42+
43+
if client_id:
44+
return "client_id", client_id
45+
return None
3246

3347

3448
class ManagedIdentityCredential:
@@ -59,51 +73,58 @@ class ManagedIdentityCredential:
5973
def __init__(
6074
self, *, client_id: Optional[str] = None, identity_config: Optional[Mapping[str, str]] = None, **kwargs: Any
6175
) -> None:
62-
validate_identity_config(client_id, identity_config)
76+
user_identity_info = validate_identity_config(client_id, identity_config)
6377
self._credential: Optional[SupportsTokenInfo] = None
6478
exclude_workload_identity = kwargs.pop("_exclude_workload_identity_credential", False)
79+
managed_identity_type = None
80+
6581
if os.environ.get(EnvironmentVariables.IDENTITY_ENDPOINT):
6682
if os.environ.get(EnvironmentVariables.IDENTITY_HEADER):
6783
if os.environ.get(EnvironmentVariables.IDENTITY_SERVER_THUMBPRINT):
68-
_LOGGER.info("%s will use Service Fabric managed identity", self.__class__.__name__)
84+
managed_identity_type = "Service Fabric managed identity"
6985
from .service_fabric import ServiceFabricCredential
7086

7187
self._credential = ServiceFabricCredential(
7288
client_id=client_id, identity_config=identity_config, **kwargs
7389
)
7490
else:
75-
_LOGGER.info("%s will use App Service managed identity", self.__class__.__name__)
91+
managed_identity_type = "App Service managed identity"
7692
from .app_service import AppServiceCredential
7793

7894
self._credential = AppServiceCredential(
7995
client_id=client_id, identity_config=identity_config, **kwargs
8096
)
8197
elif os.environ.get(EnvironmentVariables.IMDS_ENDPOINT):
82-
_LOGGER.info("%s will use Azure Arc managed identity", self.__class__.__name__)
98+
managed_identity_type = "Azure Arc managed identity"
8399
from .azure_arc import AzureArcCredential
84100

85101
self._credential = AzureArcCredential(client_id=client_id, identity_config=identity_config, **kwargs)
86102
elif os.environ.get(EnvironmentVariables.MSI_ENDPOINT):
87103
if os.environ.get(EnvironmentVariables.MSI_SECRET):
88-
_LOGGER.info("%s will use Azure ML managed identity", self.__class__.__name__)
104+
managed_identity_type = "Azure ML managed identity"
89105
from .azure_ml import AzureMLCredential
90106

91107
self._credential = AzureMLCredential(client_id=client_id, identity_config=identity_config, **kwargs)
92108
else:
93-
_LOGGER.info("%s will use Cloud Shell managed identity", self.__class__.__name__)
109+
managed_identity_type = "Cloud Shell managed identity"
94110
from .cloud_shell import CloudShellCredential
95111

96112
self._credential = CloudShellCredential(client_id=client_id, identity_config=identity_config, **kwargs)
97113
elif (
98114
all(os.environ.get(var) for var in EnvironmentVariables.WORKLOAD_IDENTITY_VARS)
99115
and not exclude_workload_identity
100116
):
101-
_LOGGER.info("%s will use workload identity", self.__class__.__name__)
102117
from .workload_identity import WorkloadIdentityCredential
103118

104119
workload_client_id = client_id or os.environ.get(EnvironmentVariables.AZURE_CLIENT_ID)
105120
if not workload_client_id:
106-
raise ValueError('Configure the environment with a client ID or pass a value for "client_id" argument')
121+
raise ValueError(
122+
"Workload identity was selected but no client ID was provided. "
123+
'Configure the environment with a client ID or pass a value for "client_id" argument'
124+
)
125+
126+
managed_identity_type = "workload identity"
127+
user_identity_info = ("client_id", workload_client_id)
107128

108129
self._credential = WorkloadIdentityCredential(
109130
tenant_id=os.environ[EnvironmentVariables.AZURE_TENANT_ID],
@@ -112,11 +133,17 @@ def __init__(
112133
**kwargs,
113134
)
114135
else:
136+
managed_identity_type = "IMDS"
115137
from .imds import ImdsCredential
116138

117-
_LOGGER.info("%s will use IMDS", self.__class__.__name__)
118139
self._credential = ImdsCredential(client_id=client_id, identity_config=identity_config, **kwargs)
119140

141+
if managed_identity_type:
142+
log_msg = f"{self.__class__.__name__} will use {managed_identity_type}"
143+
if user_identity_info:
144+
log_msg += f" with {user_identity_info[0]}: {user_identity_info[1]}"
145+
_LOGGER.info(log_msg)
146+
120147
def __enter__(self) -> "ManagedIdentityCredential":
121148
if self._credential:
122149
self._credential.__enter__() # type: ignore

sdk/identity/azure-identity/azure/identity/aio/_credentials/managed_identity.py

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -46,65 +46,76 @@ class ManagedIdentityCredential(AsyncContextManager):
4646
def __init__(
4747
self, *, client_id: Optional[str] = None, identity_config: Optional[Mapping[str, str]] = None, **kwargs: Any
4848
) -> None:
49-
validate_identity_config(client_id, identity_config)
49+
user_identity_info = validate_identity_config(client_id, identity_config)
5050
self._credential: Optional[AsyncSupportsTokenInfo] = None
5151
exclude_workload_identity = kwargs.pop("_exclude_workload_identity_credential", False)
52-
52+
managed_identity_type = None
5353
if os.environ.get(EnvironmentVariables.IDENTITY_ENDPOINT):
5454
if os.environ.get(EnvironmentVariables.IDENTITY_HEADER):
5555
if os.environ.get(EnvironmentVariables.IDENTITY_SERVER_THUMBPRINT):
56-
_LOGGER.info("%s will use Service Fabric managed identity", self.__class__.__name__)
56+
managed_identity_type = "Service Fabric managed identity"
5757
from .service_fabric import ServiceFabricCredential
5858

5959
self._credential = ServiceFabricCredential(
6060
client_id=client_id, identity_config=identity_config, **kwargs
6161
)
6262
else:
63-
_LOGGER.info("%s will use App Service managed identity", self.__class__.__name__)
63+
managed_identity_type = "App Service managed identity"
6464
from .app_service import AppServiceCredential
6565

6666
self._credential = AppServiceCredential(
6767
client_id=client_id, identity_config=identity_config, **kwargs
6868
)
6969
elif os.environ.get(EnvironmentVariables.IMDS_ENDPOINT):
70-
_LOGGER.info("%s will use Azure Arc managed identity", self.__class__.__name__)
70+
managed_identity_type = "Azure Arc managed identity"
7171
from .azure_arc import AzureArcCredential
7272

7373
self._credential = AzureArcCredential(client_id=client_id, identity_config=identity_config, **kwargs)
7474
elif os.environ.get(EnvironmentVariables.MSI_ENDPOINT):
7575
if os.environ.get(EnvironmentVariables.MSI_SECRET):
76-
_LOGGER.info("%s will use Azure ML managed identity", self.__class__.__name__)
76+
managed_identity_type = "Azure ML managed identity"
7777
from .azure_ml import AzureMLCredential
7878

7979
self._credential = AzureMLCredential(client_id=client_id, identity_config=identity_config, **kwargs)
8080
else:
81-
_LOGGER.info("%s will use Cloud Shell managed identity", self.__class__.__name__)
81+
managed_identity_type = "Cloud Shell managed identity"
8282
from .cloud_shell import CloudShellCredential
8383

8484
self._credential = CloudShellCredential(client_id=client_id, identity_config=identity_config, **kwargs)
8585
elif (
8686
all(os.environ.get(var) for var in EnvironmentVariables.WORKLOAD_IDENTITY_VARS)
8787
and not exclude_workload_identity
8888
):
89-
_LOGGER.info("%s will use workload identity", self.__class__.__name__)
9089
from .workload_identity import WorkloadIdentityCredential
9190

9291
workload_client_id = client_id or os.environ.get(EnvironmentVariables.AZURE_CLIENT_ID)
9392
if not workload_client_id:
94-
raise ValueError('Configure the environment with a client ID or pass a value for "client_id" argument')
93+
raise ValueError(
94+
"Workload identity was selected but no client ID was provided. "
95+
'Configure the environment with a client ID or pass a value for "client_id" argument'
96+
)
97+
98+
managed_identity_type = "workload identity"
99+
user_identity_info = ("client_id", workload_client_id)
95100

96101
self._credential = WorkloadIdentityCredential(
97102
tenant_id=os.environ[EnvironmentVariables.AZURE_TENANT_ID],
98103
client_id=workload_client_id,
99104
token_file_path=os.environ[EnvironmentVariables.AZURE_FEDERATED_TOKEN_FILE],
100-
**kwargs
105+
**kwargs,
101106
)
102107
else:
108+
managed_identity_type = "IMDS"
103109
from .imds import ImdsCredential
104110

105-
_LOGGER.info("%s will use IMDS", self.__class__.__name__)
106111
self._credential = ImdsCredential(client_id=client_id, identity_config=identity_config, **kwargs)
107112

113+
if managed_identity_type:
114+
log_msg = f"{self.__class__.__name__} will use {managed_identity_type}"
115+
if user_identity_info:
116+
log_msg += f" with {user_identity_info[0]}: {user_identity_info[1]}"
117+
_LOGGER.info(log_msg)
118+
108119
async def __aenter__(self) -> "ManagedIdentityCredential":
109120
if self._credential:
110121
await self._credential.__aenter__()

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

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@
44
# ------------------------------------
55
from itertools import product
66
import time
7+
import logging
78
from unittest import mock
89

910
from azure.identity import ManagedIdentityCredential, CredentialUnavailableError
1011
from azure.identity._constants import EnvironmentVariables
1112
from azure.identity._credentials.imds import IMDS_AUTHORITY, IMDS_TOKEN_PATH
13+
from azure.identity._credentials.managed_identity import validate_identity_config
1214
from azure.identity._internal.user_agent import USER_AGENT
1315
from azure.identity._internal import within_credential_chain
1416
import pytest
@@ -1003,6 +1005,20 @@ def test_validate_identity_config():
10031005
ManagedIdentityCredential(identity_config={"object_id": "bar", "client_id": "foo"})
10041006

10051007

1008+
def test_validate_identity_config_output():
1009+
output = validate_identity_config(None, {"client_id": "foo"})
1010+
assert output == ("client_id", "foo")
1011+
1012+
output = validate_identity_config("foo", None)
1013+
assert output == ("client_id", "foo")
1014+
1015+
output = validate_identity_config(None, {"object_id": "bar"})
1016+
assert output == ("object_id", "bar")
1017+
1018+
output = validate_identity_config(None, {"resource_id": "biz"})
1019+
assert output == ("resource_id", "biz")
1020+
1021+
10061022
def test_validate_cloud_shell_credential():
10071023
with mock.patch.dict(
10081024
MANAGED_IDENTITY_ENVIRON, {EnvironmentVariables.MSI_ENDPOINT: "https://localhost"}, clear=True
@@ -1016,3 +1032,41 @@ def test_validate_cloud_shell_credential():
10161032
ManagedIdentityCredential(identity_config={"object_id": "foo"})
10171033
with pytest.raises(ValueError):
10181034
ManagedIdentityCredential(identity_config={"resource_id": "foo"})
1035+
1036+
1037+
def test_log(caplog):
1038+
with caplog.at_level(logging.INFO, logger="azure.identity._credentials.managed_identity"):
1039+
ManagedIdentityCredential()
1040+
assert "ManagedIdentityCredential will use IMDS" in caplog.text
1041+
1042+
caplog.clear()
1043+
with mock.patch.dict(
1044+
MANAGED_IDENTITY_ENVIRON,
1045+
{
1046+
EnvironmentVariables.IDENTITY_ENDPOINT: "new_endpoint",
1047+
EnvironmentVariables.IDENTITY_HEADER: "new_secret",
1048+
EnvironmentVariables.MSI_ENDPOINT: "endpoint",
1049+
EnvironmentVariables.MSI_SECRET: "secret",
1050+
},
1051+
clear=True,
1052+
):
1053+
ManagedIdentityCredential()
1054+
assert "App Service managed identity" in caplog.text
1055+
1056+
caplog.clear()
1057+
ManagedIdentityCredential(client_id="foo")
1058+
assert "App Service managed identity with client_id: foo" in caplog.text
1059+
1060+
caplog.clear()
1061+
ManagedIdentityCredential(identity_config={"object_id": "bar"})
1062+
assert "App Service managed identity with object_id: bar" in caplog.text
1063+
1064+
caplog.clear()
1065+
mock_environ = {
1066+
EnvironmentVariables.AZURE_AUTHORITY_HOST: "authority",
1067+
EnvironmentVariables.AZURE_TENANT_ID: "tenant",
1068+
EnvironmentVariables.AZURE_FEDERATED_TOKEN_FILE: "token_file",
1069+
}
1070+
with mock.patch.dict("os.environ", mock_environ, clear=True):
1071+
ManagedIdentityCredential(client_id="foo")
1072+
assert "workload identity with client_id: foo" in caplog.text

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

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from itertools import product
66
import os
77
import time
8+
import logging
89
from unittest import mock
910

1011
from azure.core.exceptions import ClientAuthenticationError
@@ -1301,3 +1302,41 @@ def test_validate_cloud_shell_credential():
13011302
ManagedIdentityCredential(identity_config={"object_id": "foo"})
13021303
with pytest.raises(ValueError):
13031304
ManagedIdentityCredential(identity_config={"resource_id": "foo"})
1305+
1306+
1307+
def test_log(caplog):
1308+
with caplog.at_level(logging.INFO, logger="azure.identity.aio._credentials.managed_identity"):
1309+
ManagedIdentityCredential()
1310+
assert "ManagedIdentityCredential will use IMDS" in caplog.text
1311+
1312+
caplog.clear()
1313+
with mock.patch.dict(
1314+
MANAGED_IDENTITY_ENVIRON,
1315+
{
1316+
EnvironmentVariables.IDENTITY_ENDPOINT: "new_endpoint",
1317+
EnvironmentVariables.IDENTITY_HEADER: "new_secret",
1318+
EnvironmentVariables.MSI_ENDPOINT: "endpoint",
1319+
EnvironmentVariables.MSI_SECRET: "secret",
1320+
},
1321+
clear=True,
1322+
):
1323+
ManagedIdentityCredential()
1324+
assert "App Service managed identity" in caplog.text
1325+
1326+
caplog.clear()
1327+
ManagedIdentityCredential(client_id="foo")
1328+
assert "App Service managed identity with client_id: foo" in caplog.text
1329+
1330+
caplog.clear()
1331+
ManagedIdentityCredential(identity_config={"object_id": "bar"})
1332+
assert "App Service managed identity with object_id: bar" in caplog.text
1333+
1334+
caplog.clear()
1335+
mock_environ = {
1336+
EnvironmentVariables.AZURE_AUTHORITY_HOST: "authority",
1337+
EnvironmentVariables.AZURE_TENANT_ID: "tenant",
1338+
EnvironmentVariables.AZURE_FEDERATED_TOKEN_FILE: "token_file",
1339+
}
1340+
with mock.patch.dict("os.environ", mock_environ, clear=True):
1341+
ManagedIdentityCredential(client_id="foo")
1342+
assert "workload identity with client_id: foo" in caplog.text

0 commit comments

Comments
 (0)