Skip to content

Commit 93fd77c

Browse files
authored
Support AAD in soverign clouds (#39379)
1 parent f2f1e98 commit 93fd77c

File tree

6 files changed

+56
-10
lines changed

6 files changed

+56
-10
lines changed

sdk/monitor/azure-monitor-opentelemetry-exporter/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
### Features Added
66

7+
- Support AAD for sovereign clouds
8+
([#39379](https://github.com/Azure/azure-sdk-for-python/pull/39379))
79
- Support stable http semantic conventions for breeze exporter - REQUESTS
810
([#39208](https://github.com/Azure/azure-sdk-for-python/pull/39208))
911

sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/_connection_string_parser.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
LIVE_ENDPOINT = "liveendpoint"
88
INGESTION_ENDPOINT = "ingestionendpoint"
99
INSTRUMENTATION_KEY = "instrumentationkey"
10+
# cspell:disable-next-line
11+
AAD_AUDIENCE = "aadaudience"
1012

1113
# Validate UUID format
1214
# Specs taken from https://tools.ietf.org/html/rfc4122
@@ -26,6 +28,7 @@ def __init__(self, connection_string: typing.Optional[str] = None) -> None:
2628
self.endpoint = ""
2729
self.live_endpoint = ""
2830
self._connection_string = connection_string
31+
self.aad_audience = ""
2932
self._initialize()
3033
self._validate_instrumentation_key()
3134

@@ -42,17 +45,25 @@ def _initialize(self) -> None:
4245
# 3. Key from connection string in environment variable
4346
# 4. Key from instrumentation key in environment variable
4447
self.instrumentation_key = (
45-
code_cs.get(INSTRUMENTATION_KEY) or code_ikey or env_cs.get(INSTRUMENTATION_KEY) or env_ikey # type: ignore
48+
code_cs.get(INSTRUMENTATION_KEY) or code_ikey or \
49+
env_cs.get(INSTRUMENTATION_KEY) or env_ikey # type: ignore
4650
)
4751
# The priority of the endpoints is as follows:
4852
# 1. The endpoint explicitly passed in connection string
4953
# 2. The endpoint from the connection string in environment variable
5054
# 3. The default breeze endpoint
5155
self.endpoint = (
52-
code_cs.get(INGESTION_ENDPOINT) or env_cs.get(INGESTION_ENDPOINT) or "https://dc.services.visualstudio.com"
56+
code_cs.get(INGESTION_ENDPOINT) or env_cs.get(INGESTION_ENDPOINT) or \
57+
"https://dc.services.visualstudio.com"
5358
)
5459
self.live_endpoint = (
55-
code_cs.get(LIVE_ENDPOINT) or env_cs.get(LIVE_ENDPOINT) or "https://rt.services.visualstudio.com"
60+
code_cs.get(LIVE_ENDPOINT) or env_cs.get(LIVE_ENDPOINT) or \
61+
"https://rt.services.visualstudio.com"
62+
)
63+
# The AUDIENCE is a url that identifies Azure Monitor in a specific cloud
64+
# (For example: "https://monitor.azure.com/").
65+
self.aad_audience = (
66+
code_cs.get(AAD_AUDIENCE) or env_cs.get(AAD_AUDIENCE) # type: ignore
5667
)
5768

5869
def _validate_instrumentation_key(self) -> None:

sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/_constants.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,6 @@
204204

205205
# AAD Auth
206206

207-
_APPLICATION_INSIGHTS_RESOURCE_SCOPE = "https://monitor.azure.com//.default"
207+
_DEFAULT_AAD_SCOPE = "https://monitor.azure.com//.default"
208208

209209
# cSpell:disable

sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/_utils.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
from azure.monitor.opentelemetry.exporter._version import VERSION as ext_version
2323
from azure.monitor.opentelemetry.exporter._constants import (
2424
_AKS_ARM_NAMESPACE_ID,
25-
_APPLICATION_INSIGHTS_RESOURCE_SCOPE,
25+
_DEFAULT_AAD_SCOPE,
2626
_PYTHON_ENABLE_OPENTELEMETRY,
2727
_INSTRUMENTATIONS_BIT_MAP,
2828
_FUNCTIONS_WORKER_RUNTIME,
@@ -274,17 +274,25 @@ def _filter_custom_properties(properties: Attributes, filter=None) -> Dict[str,
274274
return truncated_properties
275275

276276

277-
def _get_auth_policy(credential, default_auth_policy):
277+
def _get_auth_policy(credential, default_auth_policy, aad_audience=None):
278278
if credential:
279279
if hasattr(credential, "get_token"):
280280
return BearerTokenCredentialPolicy(
281281
credential,
282-
_APPLICATION_INSIGHTS_RESOURCE_SCOPE,
282+
_get_scope(aad_audience),
283283
)
284284
raise ValueError("Must pass in valid TokenCredential.")
285285
return default_auth_policy
286286

287287

288+
def _get_scope(aad_audience=None):
289+
# The AUDIENCE is a url that identifies Azure Monitor in a specific cloud
290+
# (For example: "https://monitor.azure.com/").
291+
# The SCOPE is the audience + the permission
292+
# (For example: "https://monitor.azure.com//.default").
293+
return _DEFAULT_AAD_SCOPE if not aad_audience else "{}/.default".format(aad_audience)
294+
295+
288296
class Singleton(type):
289297
_instance = None
290298

sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/_base.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ def __init__(self, **kwargs: Any) -> None:
9090
self._disable_offline_storage = kwargs.get("disable_offline_storage", False)
9191
self._endpoint = parsed_connection_string.endpoint
9292
self._instrumentation_key = parsed_connection_string.instrumentation_key
93+
self._aad_audience = parsed_connection_string.aad_audience
9394
self._storage_maintenance_period = kwargs.get(
9495
"storage_maintenance_period", 60
9596
) # Maintenance interval in seconds.
@@ -126,7 +127,7 @@ def __init__(self, **kwargs: Any) -> None:
126127
# Handle redirects in exporter, set new endpoint if redirected
127128
RedirectPolicy(permit_redirects=False),
128129
config.retry_policy,
129-
_get_auth_policy(self._credential, config.authentication_policy),
130+
_get_auth_policy(self._credential, config.authentication_policy, self._aad_audience),
130131
config.custom_hook_policy,
131132
config.logging_policy,
132133
# Explicitly disabling to avoid infinite loop of Span creation when data is exported

sdk/monitor/azure-monitor-opentelemetry-exporter/tests/test_base_exporter.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
)
1919
from azure.monitor.opentelemetry.exporter.statsbeat._state import _REQUESTS_MAP
2020
from azure.monitor.opentelemetry.exporter._constants import (
21+
_DEFAULT_AAD_SCOPE,
2122
_REQ_DURATION_NAME,
2223
_REQ_EXCEPTION_NAME,
2324
_REQ_FAILURE_NAME,
@@ -941,16 +942,29 @@ def test_exporter_credential(self, mock_add_credential_policy):
941942
TEST_CREDENTIAL = "TEST_CREDENTIAL"
942943
base = BaseExporter(credential=TEST_CREDENTIAL, authentication_policy=TEST_AUTH_POLICY)
943944
self.assertEqual(base._credential, TEST_CREDENTIAL)
944-
mock_add_credential_policy.assert_called_once_with(TEST_CREDENTIAL, TEST_AUTH_POLICY)
945+
mock_add_credential_policy.assert_called_once_with(TEST_CREDENTIAL, TEST_AUTH_POLICY, None)
946+
947+
@mock.patch("azure.monitor.opentelemetry.exporter.export._base._get_auth_policy")
948+
def test_exporter_credential_audience(self, mock_add_credential_policy):
949+
test_cs = "AadAudience=test-aad"
950+
TEST_CREDENTIAL = "TEST_CREDENTIAL"
951+
base = BaseExporter(
952+
connection_string=test_cs,
953+
credential=TEST_CREDENTIAL,
954+
authentication_policy=TEST_AUTH_POLICY,
955+
)
956+
self.assertEqual(base._credential, TEST_CREDENTIAL)
957+
mock_add_credential_policy.assert_called_once_with(TEST_CREDENTIAL, TEST_AUTH_POLICY, "test-aad")
945958

946959
def test_get_auth_policy(self):
947960
class TestCredential:
948-
def get_token():
961+
def get_token(self):
949962
return "TEST_TOKEN"
950963

951964
credential = TestCredential()
952965
result = _get_auth_policy(credential, TEST_AUTH_POLICY)
953966
self.assertEqual(result._credential, credential)
967+
self.assertEqual(result._scopes, (_DEFAULT_AAD_SCOPE,))
954968

955969
def test_get_auth_policy_no_credential(self):
956970
self.assertEqual(_get_auth_policy(credential=None, default_auth_policy=TEST_AUTH_POLICY), TEST_AUTH_POLICY)
@@ -964,6 +978,16 @@ def invalid_get_token():
964978
ValueError, _get_auth_policy, credential=InvalidTestCredential(), default_auth_policy=TEST_AUTH_POLICY
965979
)
966980

981+
def test_get_auth_policy_audience(self):
982+
class TestCredential:
983+
def get_token():
984+
return "TEST_TOKEN"
985+
986+
credential = TestCredential()
987+
result = _get_auth_policy(credential, TEST_AUTH_POLICY, aad_audience="test_audience")
988+
self.assertEqual(result._credential, credential)
989+
self.assertEqual(result._scopes, ("test_audience/.default",))
990+
967991

968992
def validate_telemetry_item(item1, item2):
969993
return (

0 commit comments

Comments
 (0)