Skip to content

Commit 7e16561

Browse files
pvanecklmazuel
andauthored
[Tables] Add audience keyword argument support (Azure#40487)
This enables sovereign cloud support. Signed-off-by: Paul Van Eck <[email protected]> Co-authored-by: Laurent Mazuel <[email protected]>
1 parent aeeb1f0 commit 7e16561

File tree

10 files changed

+154
-28
lines changed

10 files changed

+154
-28
lines changed

sdk/tables/azure-data-tables/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
* Added to support customized encoding and decoding in entity CRUD operations.
77
* Added to support Entity property in Tuple and Enum types.
88
* Added to support flatten Entity metadata in entity deserialization by passing kwarg `flatten_result_entity` when creating clients.
9+
* Added support for configuring custom audiences for `TokenCredential` authentication when initializing a `TableClient` or `TableServiceClient`. ([#40487](https://github.com/Azure/azure-sdk-for-python/pull/40487))
910

1011
### Bugs Fixed
1112
* Fixed duplicate odata tag bug in encoder when Entity property has "@odata.type" provided.

sdk/tables/azure-data-tables/README.md

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ The `credential` parameter may be provided in a number of different forms, depen
5555
* Shared Key
5656
* Connection String
5757
* Shared Access Signature Token
58-
* TokenCredential(AAD)(Supported on Storage)
58+
* TokenCredential (Microsoft Entra ID)(Supported on Storage)
5959

6060
##### Creating the client from a shared key
6161
To use an account [shared key][azure_shared_key] (aka account key or access key), provide the key as a string. This can be found in your storage account in the [Azure Portal][azure_portal_account_url] under the "Access Keys" section or by running the following Azure CLI command:
@@ -79,7 +79,7 @@ with TableServiceClient(
7979
```
8080

8181
##### Creating the client from a connection string
82-
Depending on your use case and authorization method, you may prefer to initialize a client instance with a connection string instead of providing the account URL and credential separately. To do this, pass the connection string to the client's `from_connection_string` class method. If the connection string does not specify a fully qualified endpoint URL (`"TableEndpoint"`), or URL suffix (`"EndpointSuffix"`), the endpoint will be assumed to be an Azure Storage account, and the URL automatically formatted accordingly.
82+
Depending on your use case and authorization method, you may prefer to initialize a client instance with a connection string instead of providing the account URL and credential separately. To do this, pass the connection string to the client's `from_connection_string` class method. If the connection string does not specify a fully qualified endpoint URL (`"TableEndpoint"`), or URL suffix (`"EndpointSuffix"`), the endpoint will be assumed to be an Azure Storage account, and the URL automatically formatted accordingly.
8383

8484
For Tables Storage, the connection string can be found in your storage account in the [Azure Portal][azure_portal_account_url] under the "Access Keys" section or with the following Azure CLI command:
8585

@@ -129,11 +129,12 @@ with TableServiceClient(
129129
```
130130

131131
##### Creating the client from a TokenCredential
132-
Azure Tables provides integration with Azure Active Directory(Azure AD) for identity-based authentication of requests to the Table service when targeting a Storage endpoint. With Azure AD, you can use role-based access control(RBAC) to grant access to your Azure Table resources to users, groups, or applications.
132+
133+
Azure Tables provides integration with Microsoft Entra ID for identity-based authentication of requests to the Table service when targeting a Storage endpoint. With Microsoft Entra ID, you can use role-based access control (RBAC) to grant access to your Azure Table resources to users, groups, or applications.
133134

134135
To access a table resource with a TokenCredential, the authenticated identity should have either the "Storage Table Data Contributor" or "Storage Table Data Reader" role.
135136

136-
With the `azure-identity` package, you can seamlessly authorize requests in both development and production environments. To learn more about Azure AD integration in Azure Storage, see the [azure-identity README](https://github.com/Azure/azure-sdk-for-python/blob/main/sdk/identity/azure-identity/README.md)
137+
With the `azure-identity` package, you can seamlessly authorize requests in both development and production environments. To learn more about Microsoft Entra ID integration in Azure Storage, see the [azure-identity README](https://github.com/Azure/azure-sdk-for-python/blob/main/sdk/identity/azure-identity/README.md)
137138

138139
```python
139140
from azure.data.tables import TableServiceClient
@@ -146,6 +147,33 @@ with TableServiceClient(
146147
print(f"{properties}")
147148
```
148149

150+
###### Configure client for an Azure sovereign cloud
151+
152+
When TokenCredential authentication is used, all clients are configured to use the Azure public cloud by default. To configure a client for a sovereign cloud, you should provide the correct `audience` keyword argument when creating the client. The following table lists some known audiences:
153+
154+
| Cloud | Audience |
155+
|-------|----------|
156+
| Azure Public | https://storage.azure.com / https://cosmos.azure.com |
157+
| Azure US Government | https://storage.azure.us / https://cosmos.azure.us |
158+
| Azure China | https://storage.chinacloudapi.cn / https://cosmos.chinacloudapi.cn |
159+
160+
The following example shows how to configure the `TableServiceClient` to connect to Azure US Government:
161+
162+
```python
163+
from azure.data.tables import TableServiceClient
164+
from azure.identity import AzureAuthorityHosts, DefaultAzureCredential
165+
166+
# Authority can also be set via the AZURE_AUTHORITY_HOST environment variable.
167+
credential = DefaultAzureCredential(authority=AzureAuthorityHosts.AZURE_GOVERNMENT)
168+
169+
table_service_client = TableServiceClient(
170+
endpoint="https://<my_account_name>.table.core.usgovcloudapi.net",
171+
credential=credential,
172+
audience="https://storage.azure.us"
173+
)
174+
```
175+
176+
149177
## Key concepts
150178
Common uses of the Table service included:
151179
* Storing TBs of structured data capable of serving web scale applications

sdk/tables/azure-data-tables/azure/data/tables/_authentication.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -222,34 +222,46 @@ def on_challenge(self, request: PipelineRequest, response: PipelineResponse) ->
222222

223223

224224
@overload
225-
def _configure_credential(credential: AzureNamedKeyCredential) -> SharedKeyCredentialPolicy: ...
225+
def _configure_credential(
226+
credential: AzureNamedKeyCredential, cosmos_endpoint: bool = False, audience: Optional[str] = None
227+
) -> SharedKeyCredentialPolicy: ...
226228

227229

228230
@overload
229-
def _configure_credential(credential: SharedKeyCredentialPolicy) -> SharedKeyCredentialPolicy: ...
231+
def _configure_credential(
232+
credential: SharedKeyCredentialPolicy, cosmos_endpoint: bool = False, audience: Optional[str] = None
233+
) -> SharedKeyCredentialPolicy: ...
230234

231235

232236
@overload
233-
def _configure_credential(credential: AzureSasCredential) -> AzureSasCredentialPolicy: ...
237+
def _configure_credential(
238+
credential: AzureSasCredential, cosmos_endpoint: bool = False, audience: Optional[str] = None
239+
) -> AzureSasCredentialPolicy: ...
234240

235241

236242
@overload
237-
def _configure_credential(credential: TokenCredential) -> BearerTokenChallengePolicy: ...
243+
def _configure_credential(
244+
credential: TokenCredential, cosmos_endpoint: bool = False, audience: Optional[str] = None
245+
) -> BearerTokenChallengePolicy: ...
238246

239247

240248
@overload
241-
def _configure_credential(credential: None) -> None: ...
249+
def _configure_credential(credential: None, cosmos_endpoint: bool = False, audience: Optional[str] = None) -> None: ...
242250

243251

244252
def _configure_credential(
245253
credential: Optional[
246254
Union[AzureNamedKeyCredential, AzureSasCredential, TokenCredential, SharedKeyCredentialPolicy]
247255
],
248256
cosmos_endpoint: bool = False,
257+
audience: Optional[str] = None,
249258
) -> Optional[Union[BearerTokenChallengePolicy, AzureSasCredentialPolicy, SharedKeyCredentialPolicy]]:
250259
if hasattr(credential, "get_token"):
251260
credential = cast(TokenCredential, credential)
252-
scope = COSMOS_OAUTH_SCOPE if cosmos_endpoint else STORAGE_OAUTH_SCOPE
261+
if audience:
262+
scope = audience.rstrip("/") + "/.default"
263+
else:
264+
scope = COSMOS_OAUTH_SCOPE if cosmos_endpoint else STORAGE_OAUTH_SCOPE
253265
return BearerTokenChallengePolicy(credential, scope)
254266
if isinstance(credential, SharedKeyCredentialPolicy):
255267
return credential

sdk/tables/azure-data-tables/azure/data/tables/_base_client.py

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import os
88
from uuid import uuid4
99
from urllib.parse import parse_qs, quote, urlparse
10-
from typing import Any, List, Optional, Mapping, Union
10+
from typing import Any, List, Optional, Mapping, Union, Literal
1111
from typing_extensions import Self
1212

1313
from azure.core.credentials import AzureSasCredential, AzureNamedKeyCredential, TokenCredential
@@ -48,6 +48,18 @@
4848
# cspell:disable-next-line
4949
_DEV_CONN_STRING = "DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;TableEndpoint=http://127.0.0.1:10002/devstoreaccount1" # pylint: disable=line-too-long
5050

51+
AudienceType = Union[
52+
str,
53+
Literal[
54+
"https://storage.azure.com",
55+
"https://storage.azure.us",
56+
"https://storage.azure.cn",
57+
"https://cosmos.azure.com",
58+
"https://cosmos.azure.us",
59+
"https://cosmos.azure.cn",
60+
],
61+
]
62+
5163

5264
def get_api_version(api_version: Optional[str], default: str) -> str:
5365
if api_version and api_version not in _SUPPORTED_API_VERSIONS:
@@ -71,6 +83,7 @@ def __init__( # pylint: disable=missing-client-constructor-parameter-credential
7183
*,
7284
credential: Optional[Union[AzureSasCredential, AzureNamedKeyCredential, TokenCredential]] = None,
7385
api_version: Optional[str] = None,
86+
audience: Optional[AudienceType] = None,
7487
**kwargs: Any,
7588
) -> None:
7689
"""
@@ -83,6 +96,9 @@ def __init__( # pylint: disable=missing-client-constructor-parameter-credential
8396
~azure.core.credentials.AzureNamedKeyCredential or
8497
~azure.core.credentials.AzureSasCredential or
8598
~azure.core.credentials.TokenCredential or None
99+
:keyword audience: Optional audience to use for Microsoft Entra ID authentication. If not specified,
100+
the public cloud audience will be used.
101+
:paramtype audience: str or None
86102
:keyword api_version: Specifies the version of the operation to use for this request. Default value
87103
is "2019-02-02".
88104
:paramtype api_version: str or None
@@ -129,7 +145,7 @@ def __init__( # pylint: disable=missing-client-constructor-parameter-credential
129145
}
130146
self._hosts = _hosts
131147

132-
self._policies = self._configure_policies(hosts=self._hosts, **kwargs)
148+
self._policies = self._configure_policies(audience=audience, hosts=self._hosts, **kwargs)
133149
if self._cosmos_endpoint:
134150
self._policies.insert(0, CosmosPatchTransformPolicy())
135151

@@ -222,8 +238,10 @@ def _format_url(self, hostname):
222238
"""
223239
return f"{self.scheme}://{hostname}{self._query_str}"
224240

225-
def _configure_policies(self, **kwargs):
226-
credential_policy = _configure_credential(self.credential, self._cosmos_endpoint)
241+
def _configure_policies(self, *, audience: Optional[str] = None, **kwargs: Any) -> List[Any]:
242+
credential_policy = _configure_credential(
243+
self.credential, cosmos_endpoint=self._cosmos_endpoint, audience=audience
244+
)
227245
return [
228246
RequestIdPolicy(**kwargs),
229247
StorageHeadersPolicy(**kwargs),

sdk/tables/azure-data-tables/azure/data/tables/_table_client.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from ._common_conversion import _prepare_key, _return_headers_and_deserialized, _trim_service_metadata
1818
from ._encoder import TableEntityEncoder, EncoderMapType
1919
from ._decoder import TableEntityDecoder, deserialize_iso, DecoderMapType
20-
from ._base_client import parse_connection_str, TablesBaseClient
20+
from ._base_client import parse_connection_str, TablesBaseClient, AudienceType
2121
from ._entity import TableEntity
2222
from ._error import (
2323
_decode_error,
@@ -73,6 +73,7 @@ def __init__( # pylint: disable=missing-client-constructor-parameter-credential
7373
table_name: str,
7474
*,
7575
credential: Optional[Union[AzureSasCredential, AzureNamedKeyCredential, TokenCredential]] = None,
76+
audience: Optional[AudienceType] = None,
7677
api_version: Optional[str] = None,
7778
encoder_map: Optional[EncoderMapType] = None,
7879
decoder_map: Optional[DecoderMapType] = None,
@@ -91,6 +92,9 @@ def __init__( # pylint: disable=missing-client-constructor-parameter-credential
9192
~azure.core.credentials.AzureNamedKeyCredential or
9293
~azure.core.credentials.AzureSasCredential or
9394
~azure.core.credentials.TokenCredential or None
95+
:keyword audience: Optional audience to use for Microsoft Entra ID authentication. If not specified,
96+
the public cloud audience will be used.
97+
:paramtype audience: str or None
9498
:keyword api_version: Specifies the version of the operation to use for this request. Default value
9599
is "2019-02-02".
96100
:paramtype api_version: str or None
@@ -112,7 +116,9 @@ def __init__( # pylint: disable=missing-client-constructor-parameter-credential
112116
self.table_name: str = table_name
113117
self.encoder = TableEntityEncoder(convert_map=encoder_map)
114118
self.decoder = TableEntityDecoder(convert_map=decoder_map, flatten_result_entity=flatten_result_entity)
115-
super(TableClient, self).__init__(endpoint, credential=credential, api_version=api_version, **kwargs)
119+
super(TableClient, self).__init__(
120+
endpoint, credential=credential, api_version=api_version, audience=audience, **kwargs
121+
)
116122

117123
@classmethod
118124
def from_connection_string(cls, conn_str: str, table_name: str, **kwargs: Any) -> "TableClient":

sdk/tables/azure-data-tables/azure/data/tables/aio/_authentication_async.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -78,34 +78,46 @@ async def on_challenge(self, request: PipelineRequest, response: PipelineRespons
7878

7979

8080
@overload
81-
def _configure_credential(credential: AzureNamedKeyCredential) -> SharedKeyCredentialPolicy: ...
81+
def _configure_credential(
82+
credential: AzureNamedKeyCredential, cosmos_endpoint: bool = False, audience: Optional[str] = None
83+
) -> SharedKeyCredentialPolicy: ...
8284

8385

8486
@overload
85-
def _configure_credential(credential: SharedKeyCredentialPolicy) -> SharedKeyCredentialPolicy: ...
87+
def _configure_credential(
88+
credential: SharedKeyCredentialPolicy, cosmos_endpoint: bool = False, audience: Optional[str] = None
89+
) -> SharedKeyCredentialPolicy: ...
8690

8791

8892
@overload
89-
def _configure_credential(credential: AzureSasCredential) -> AzureSasCredentialPolicy: ...
93+
def _configure_credential(
94+
credential: AzureSasCredential, cosmos_endpoint: bool = False, audience: Optional[str] = None
95+
) -> AzureSasCredentialPolicy: ...
9096

9197

9298
@overload
93-
def _configure_credential(credential: AsyncTokenCredential) -> AsyncBearerTokenChallengePolicy: ...
99+
def _configure_credential(
100+
credential: AsyncTokenCredential, cosmos_endpoint: bool = False, audience: Optional[str] = None
101+
) -> AsyncBearerTokenChallengePolicy: ...
94102

95103

96104
@overload
97-
def _configure_credential(credential: None) -> None: ...
105+
def _configure_credential(credential: None, cosmos_endpoint: bool = False, audience: Optional[str] = None) -> None: ...
98106

99107

100108
def _configure_credential(
101109
credential: Optional[
102110
Union[AzureNamedKeyCredential, AzureSasCredential, AsyncTokenCredential, SharedKeyCredentialPolicy]
103111
],
104112
cosmos_endpoint: bool = False,
113+
audience: Optional[str] = None,
105114
) -> Optional[Union[AsyncBearerTokenChallengePolicy, AzureSasCredentialPolicy, SharedKeyCredentialPolicy]]:
106115
if hasattr(credential, "get_token"):
107116
credential = cast(AsyncTokenCredential, credential)
108-
scope = COSMOS_OAUTH_SCOPE if cosmos_endpoint else STORAGE_OAUTH_SCOPE
117+
if audience:
118+
scope = audience.rstrip("/") + "/.default"
119+
else:
120+
scope = COSMOS_OAUTH_SCOPE if cosmos_endpoint else STORAGE_OAUTH_SCOPE
109121
return AsyncBearerTokenChallengePolicy(credential, scope)
110122
if isinstance(credential, SharedKeyCredentialPolicy):
111123
return credential

sdk/tables/azure-data-tables/azure/data/tables/aio/_base_client_async.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
from .._common_conversion import _is_cosmos_endpoint, _get_account
3131
from .._constants import DEFAULT_STORAGE_ENDPOINT_SUFFIX
3232
from .._generated.aio import AzureTable
33-
from .._base_client import extract_batch_part_metadata, parse_query, format_query_string, get_api_version
33+
from .._base_client import extract_batch_part_metadata, parse_query, format_query_string, get_api_version, AudienceType
3434
from .._error import (
3535
RequestTooLargeError,
3636
TableTransactionError,
@@ -57,6 +57,7 @@ def __init__( # pylint: disable=missing-client-constructor-parameter-credential
5757
endpoint: str,
5858
*,
5959
credential: Optional[Union[AzureSasCredential, AzureNamedKeyCredential, AsyncTokenCredential]] = None,
60+
audience: Optional[AudienceType] = None,
6061
api_version: Optional[str] = None,
6162
**kwargs: Any,
6263
) -> None:
@@ -70,6 +71,9 @@ def __init__( # pylint: disable=missing-client-constructor-parameter-credential
7071
~azure.core.credentials.AzureNamedKeyCredential or
7172
~azure.core.credentials.AzureSasCredential or
7273
~azure.core.credentials_async.AsyncTokenCredential or None
74+
:keyword audience: Optional audience to use for Microsoft Entra ID authentication. If not specified,
75+
the public cloud audience will be used.
76+
:paramtype audience: str or None
7377
:keyword api_version: Specifies the version of the operation to use for this request. Default value
7478
is "2019-02-02".
7579
:paramtype api_version: str or None
@@ -116,7 +120,7 @@ def __init__( # pylint: disable=missing-client-constructor-parameter-credential
116120
}
117121
self._hosts = _hosts
118122

119-
self._policies = self._configure_policies(hosts=self._hosts, **kwargs)
123+
self._policies = self._configure_policies(audience=audience, hosts=self._hosts, **kwargs)
120124
if self._cosmos_endpoint:
121125
self._policies.insert(0, CosmosPatchTransformPolicy())
122126

@@ -215,8 +219,8 @@ def _format_url(self, hostname):
215219
"""
216220
return f"{self.scheme}://{hostname}{self._query_str}"
217221

218-
def _configure_policies(self, **kwargs):
219-
credential_policy = _configure_credential(self.credential, self._cosmos_endpoint)
222+
def _configure_policies(self, *, audience: Optional[str] = None, **kwargs: Any) -> List[Any]:
223+
credential_policy = _configure_credential(self.credential, self._cosmos_endpoint, audience=audience)
220224
return [
221225
RequestIdPolicy(**kwargs),
222226
StorageHeadersPolicy(**kwargs),

0 commit comments

Comments
 (0)