Skip to content

Commit

Permalink
[Text Translation] Fix the Entra ID authentication on custom endpoint (
Browse files Browse the repository at this point in the history
…#35835)

* [Text Translation] Fix the Entra ID authentication on custom endpoint

* Fix build issues

* fix md links

* Fix documentation

* Fixing PR comments

* Fix changelog

* Update sdk/translation/azure-ai-translation-text/azure/ai/translation/text/_patch.py

Co-authored-by: Anna Tisch <[email protected]>

* Added comment

* Adding release info

---------

Co-authored-by: Michal Materna <[email protected]>
Co-authored-by: Anna Tisch <[email protected]>
  • Loading branch information
3 people authored Jun 22, 2024
1 parent b9e0b17 commit f792549
Show file tree
Hide file tree
Showing 9 changed files with 186 additions and 35 deletions.
9 changes: 2 additions & 7 deletions sdk/translation/azure-ai-translation-text/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,9 @@
# Release History

## 1.0.1 (Unreleased)

### Features Added

### Breaking Changes
## 1.0.1 (2024-06-24)

### Bugs Fixed

### Other Changes
- Fixed a bug where Entra Id authentication couldn't be used with custom endpoint.

## 1.0.0 (2024-05-23)

Expand Down
2 changes: 1 addition & 1 deletion sdk/translation/azure-ai-translation-text/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ With the value of the `endpoint`, `credential` and a `region`, you can create th

```python
credential = AzureKeyCredential(apikey)
text_translator = TextTranslationClient(credential=credential, endpoint=endpoint, region=region)
text_translator = TextTranslationClient(credential=credential, region=region)
```

<!-- END SNIPPET -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@

from ._client import TextTranslationClient as ServiceClientGenerated

DEFAULT_TOKEN_SCOPE = "https://api.microsofttranslator.com/"
DEFAULT_ENTRA_ID_SCOPE = "https://cognitiveservices.azure.com"
DEFAULT_SCOPE = "/.default"

Expand Down Expand Up @@ -79,6 +78,12 @@ def get_translation_endpoint(endpoint, api_version):

return translator_endpoint

def is_cognitive_services_scope(audience: str) -> bool:
if "microsofttranslator" in audience:
return True

return False


def set_authentication_policy(credential, kwargs):
if isinstance(credential, AzureKeyCredential):
Expand All @@ -92,7 +97,7 @@ def set_authentication_policy(credential, kwargs):
elif hasattr(credential, "get_token"):
if not kwargs.get("authentication_policy"):
if kwargs.get("region") and kwargs.get("resource_id"):
scope = kwargs.pop("audience", DEFAULT_ENTRA_ID_SCOPE).rstrip("/") + DEFAULT_SCOPE
scope = kwargs.pop("audience", DEFAULT_ENTRA_ID_SCOPE).rstrip("/").rstrip(DEFAULT_SCOPE) + DEFAULT_SCOPE
kwargs["authentication_policy"] = TranslatorEntraIdAuthenticationPolicy(
credential,
kwargs["resource_id"],
Expand All @@ -105,8 +110,12 @@ def set_authentication_policy(credential, kwargs):
"""Both 'resource_id' and 'region' must be provided with a TokenCredential for
regional resource authentication."""
)
scope: str = kwargs.pop("audience", DEFAULT_ENTRA_ID_SCOPE)
if not is_cognitive_services_scope(scope):
scope = scope.rstrip("/").rstrip(DEFAULT_SCOPE) + DEFAULT_SCOPE

kwargs["authentication_policy"] = BearerTokenCredentialPolicy(
credential, *[kwargs.pop("audience", DEFAULT_TOKEN_SCOPE)], kwargs
credential, scope
)


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@
from azure.core.credentials_async import AsyncTokenCredential

from .._patch import (
DEFAULT_TOKEN_SCOPE,
DEFAULT_ENTRA_ID_SCOPE,
DEFAULT_SCOPE,
get_translation_endpoint,
TranslatorAuthenticationPolicy,
is_cognitive_services_scope,
)

from ._client import TextTranslationClient as ServiceClientGenerated
Expand Down Expand Up @@ -72,7 +72,7 @@ def set_authentication_policy(credential, kwargs):
elif hasattr(credential, "get_token"):
if not kwargs.get("authentication_policy"):
if kwargs.get("region") and kwargs.get("resource_id"):
scope = kwargs.pop("audience", DEFAULT_ENTRA_ID_SCOPE).rstrip("/") + DEFAULT_SCOPE
scope = kwargs.pop("audience", DEFAULT_ENTRA_ID_SCOPE).rstrip("/").rstrip(DEFAULT_SCOPE) + DEFAULT_SCOPE
kwargs["authentication_policy"] = AsyncTranslatorEntraIdAuthenticationPolicy(
credential,
kwargs["resource_id"],
Expand All @@ -85,8 +85,11 @@ def set_authentication_policy(credential, kwargs):
"""Both 'resource_id' and 'region' must be provided with a TokenCredential
for regional resource authentication."""
)
scope: str = kwargs.pop("audience", DEFAULT_ENTRA_ID_SCOPE)
if not is_cognitive_services_scope(scope):
scope = scope.rstrip("/").rstrip(DEFAULT_SCOPE) + DEFAULT_SCOPE
kwargs["authentication_policy"] = AsyncBearerTokenCredentialPolicy(
credential, *[kwargs.pop("audience", DEFAULT_TOKEN_SCOPE)], kwargs
credential, scope
)


Expand Down
93 changes: 83 additions & 10 deletions sdk/translation/azure-ai-translation-text/samples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,36 +24,104 @@ Translator Service is a cloud-based neural machine translation service that is p

See the [README][README] of the Text Translator client library for more information, including useful links and instructions.

## Common scenarios samples
# Create Client

Text Translation service is using two types of endpoints - Global and Custom. You can find more information in the [v3 Translator reference][TranslatorReference].

# Create Client
## Global Endpoint

For some of these operations you can create a new `TextTranslationClient` without any authentication. You will only need your endpoint:
When using Text Translation service with global endpoint, the endpoint doesn't need to be provided when creating `TextTranslationClient`. When `endpoint` is
not provided, global `api.cognitive.microsofttranslator.com` is used. For [sovereign clouds][SovereignClouds], the endpoint is always required.

<!-- SNIPPET: sample_text_translation_client.create_text_translation_client_with_endpoint -->
### Using the Subscription Key

When using `cognitive services key` and `region` with global endpoint, you can create `TextTranslationClient`:

<!-- SNIPPET: sample_text_translation_client.create_text_translation_client_with_credential -->

```python
text_translator = TextTranslationClient(endpoint=endpoint)
credential = AzureKeyCredential(apikey)
text_translator = TextTranslationClient(credential=credential, region=region)
```

<!-- END SNIPPET -->

The values of the `endpoint` variable can be retrieved from environment variables, configuration settings, or any other secure approach that works for your application.
### Using the Cognitive Services Token

For other samples an overloaded constructor is provided that uses a TextTranslationCredential. In addition to `endpoint`, this function requires configuring an `apikey` and `region` to create the credential. The values of the `endpoint`, `apiKey` and `region` variables can be retrieved from environment variables, configuration settings, or any other secure approach that works for your application.
For the Cognitive Services Token authentication, there is currently no implementation provided in the SDK. You can implement the solution
according to the [Token Authentication documentation][TranslatorReference]. For a simple example implementation, you can refer: [StaticAccessTokenCredential][static_access_token_credential].
When the token is created, you can create the `TextTranslationClient`:

The appropriate constructor is invoked in each sample to create a `TextTranslationClient` instance.
<!-- SNIPPET: sample_text_translation_client.create_text_translation_client_with_cognitive_services_token -->

<!-- SNIPPET: sample_text_translation_client.create_text_translation_client_with_credential -->
```python
client = TextTranslationClient(credential=credential, audience="https://api.microsofttranslator.com/")
```

<!-- END SNIPPET -->

### Using the Entra Id Authentication

The Authentication with Microsoft Entra ID on global endpoint requires bearer token generated by Azure AD, Region of the translator resource
and Resource ID for your Translator resource instance. For prerequisites and more information refer to [Authentication with Microsoft Entra ID][TranslatorReference].

<!-- SNIPPET: sample_text_translation_client.create_text_translation_client_with_entra_id_token -->

```python
credential = DefaultAzureCredential()
client = TextTranslationClient(credential=credential, region=region, resource_id=resource_id)
```

<!-- END SNIPPET -->

## Custom Endpoint

When using some selected features (for example Virtual Network) you need to use custom endpoints. More information can be found in [Virtual Network support][TranslatorReference].

### Using the Subscription Key

For Custom endpoint and cognitive services key combination, you don't need to provide the region:

<!-- SNIPPET: sample_text_translation_client.create_text_translation_client_custom_with_credential -->

```python
credential = AzureKeyCredential(apikey)
text_translator = TextTranslationClient(credential=credential, endpoint=endpoint, region=region)
text_translator = TextTranslationClient(credential=credential, endpoint=endpoint)
```

<!-- END SNIPPET -->

### Using the Cognitive Services Token

The Cognitive Services Token is not supported when using the Custom Endpoint.

### Using the Entra Id Authentication

The Authentication with Microsoft Entra ID on custom endpoint requires you to provide only bearer token generated by Azure AD:

<!-- SNIPPET: sample_text_translation_client.create_text_translation_client_custom_with_entra_id_token -->

```python
credential = DefaultAzureCredential()
client = TextTranslationClient(credential=credential, endpoint=endpoint)
```

<!-- END SNIPPET -->

## Azure AI Translator Container

When using `TextTranslationClient` with the [Azure AI Translator Container][Container] without any authentication. You will only need your endpoint:

<!-- SNIPPET: sample_text_translation_client.create_text_translation_client_with_endpoint -->

```python
text_translator = TextTranslationClient(endpoint=endpoint)
```

<!-- END SNIPPET -->

The values of the `endpoint` variable can be retrieved from environment variables, configuration settings, or any other secure approach that works for your application.

# Get Languages

This sample demonstrates how to get languages that are supported by other operations.
Expand Down Expand Up @@ -803,3 +871,8 @@ raise
[breaksentence_sample]: https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/translation/azure-ai-translation-text/samples/sample_text_translation_break_sentence.py
[dictionarylookup_sample]: https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/translation/azure-ai-translation-text/samples/sample_text_translation_dictionary_lookup.py
[dictionaryexamples_sample]: https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/translation/azure-ai-translation-text/samples/sample_text_translation_dictionary_examples.py
[static_access_token_credential]: https://github.com/Azure/azure-sdk-for-python/blob/main/sdk/translation/azure-ai-translation-text/tests/static_access_token_credential.py

[Container]: https://learn.microsoft.com/azure/ai-services/translator/containers/overview
[TranslatorReference]: https://learn.microsoft.com/azure/ai-services/translator/reference/v3-0-reference
[SovereignClouds]: https://learn.microsoft.com/azure/ai-services/translator/sovereign-clouds
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,24 @@
1) AZURE_TEXT_TRANSLATION_ENDPOINT - the endpoint to your Text Translation resource.
Note: the endpoint must be formatted to use the custom domain name for your resource:
https:\\<NAME-OF-YOUR-RESOURCE>.cognitiveservices.azure.com\
The create_text_translation_client_with_credential call requires additional variables:
2) AZURE_TEXT_TRANSLATION_APIKEY - the API key to your Text Translation resource.
3) AZURE_TEXT_TRANSLATION_REGION - the Azure Region of your Text Translation resource.
4) AZURE_TEXT_TRANSLATION_RESOURCE_ID - the Azure Resource Id path
"""

import os

from static_access_token_credential import StaticAccessTokenCredential


# -------------------------------------------------------------------------
# Text translation client
# -------------------------------------------------------------------------
def create_text_translation_client_with_endpoint():
from azure.ai.translation.text import TextTranslationClient

endpoint = os.environ["AZURE_TEXT_TRANSLATION_ENDPOINT"]
endpoint = "http://localhost"
# [START create_text_translation_client_with_endpoint]
text_translator = TextTranslationClient(endpoint=endpoint)
# [END create_text_translation_client_with_endpoint]
Expand All @@ -44,11 +46,58 @@ def create_text_translation_client_with_credential():
from azure.ai.translation.text import TextTranslationClient
from azure.core.credentials import AzureKeyCredential

endpoint = os.environ["AZURE_TEXT_TRANSLATION_ENDPOINT"]
apikey = os.environ["AZURE_TEXT_TRANSLATION_APIKEY"]
region = os.environ["AZURE_TEXT_TRANSLATION_REGION"]
# [START create_text_translation_client_with_credential]
credential = AzureKeyCredential(apikey)
text_translator = TextTranslationClient(credential=credential, endpoint=endpoint, region=region)
text_translator = TextTranslationClient(credential=credential, region=region)
# [END create_text_translation_client_with_credential]
return text_translator

def create_text_translation_client_custom_with_credential():
from azure.ai.translation.text import TextTranslationClient
from azure.core.credentials import AzureKeyCredential

endpoint = os.environ["AZURE_TEXT_TRANSLATION_ENDPOINT"]
apikey = os.environ["AZURE_TEXT_TRANSLATION_APIKEY"]
# [START create_text_translation_client_custom_with_credential]
credential = AzureKeyCredential(apikey)
text_translator = TextTranslationClient(credential=credential, endpoint=endpoint)
# [END create_text_translation_client_custom_with_credential]
return text_translator

def create_text_translation_client_with_cognitive_services_token():
from azure.ai.translation.text import TextTranslationClient
from azure.core.credentials import TokenCredential

apikey = os.environ["AZURE_TEXT_TRANSLATION_APIKEY"]
region = os.environ["AZURE_TEXT_TRANSLATION_REGION"]

credential: TokenCredential = StaticAccessTokenCredential(apikey, region)

# [START create_text_translation_client_with_cognitive_services_token]
client = TextTranslationClient(credential=credential, audience="https://api.microsofttranslator.com/")
# [END create_text_translation_client_with_cognitive_services_token]

def create_text_translation_client_with_entra_id_token():
from azure.ai.translation.text import TextTranslationClient
from azure.identity import DefaultAzureCredential

region = os.environ["AZURE_TEXT_TRANSLATION_REGION"]
resource_id = os.environ["AZURE_TEXT_TRANSLATION_RESOURCE_ID"]

# [START create_text_translation_client_with_entra_id_token]
credential = DefaultAzureCredential()
client = TextTranslationClient(credential=credential, region=region, resource_id=resource_id)
# [END create_text_translation_client_with_entra_id_token]

def create_text_translation_client_custom_with_entra_id_token():
from azure.ai.translation.text import TextTranslationClient
from azure.identity import DefaultAzureCredential

endpoint = os.environ["AZURE_TEXT_TRANSLATION_ENDPOINT"]

# [START create_text_translation_client_custom_with_entra_id_token]
credential = DefaultAzureCredential()
client = TextTranslationClient(credential=credential, endpoint=endpoint)
# [END create_text_translation_client_custom_with_entra_id_token]
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ def test_token(self, **kwargs):
endpoint = kwargs.get("translation_text_endpoint")
apikey = kwargs.get("translation_text_apikey")
region = kwargs.get("translation_text_region")
client = self.create_client_token(endpoint, apikey, region)
client = self.create_client_token(endpoint, apikey, region, "https://api.microsofttranslator.com/")

to_language = ["cs"]
input_text_elements = ["This is a test."]
Expand Down Expand Up @@ -319,3 +319,21 @@ def test_translate_aad(self, **kwargs):
assert len(response[0].translations) == 1
assert response[0].translations[0].to == "cs"
assert response[0].translations[0].text is not None

@pytest.mark.skip
@TextTranslationPreparer()
@recorded_by_proxy
def test_translate_aad_custom(self, **kwargs):
endpoint = kwargs.get("translation_text_custom_endpoint")
token_credential = self.get_mt_credential(False)
client = self.create_text_translation_client_custom_with_aad(token_credential, endpoint=endpoint)

from_language = "es"
to_language = ["cs"]
input_text_elements = ["Hola mundo"]
response = client.translate(body=input_text_elements, to_language=to_language, from_language=from_language)

assert len(response) == 1
assert len(response[0].translations) == 1
assert response[0].translations[0].to == "cs"
assert response[0].translations[0].text is not None
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,7 @@ async def test_token(self, **kwargs):
endpoint = kwargs.get("translation_text_endpoint")
apikey = kwargs.get("translation_text_apikey")
region = kwargs.get("translation_text_region")
client = self.create_async_client_token(endpoint, apikey, region)
client = self.create_async_client_token(endpoint, apikey, region, "https://api.microsofttranslator.com/")

to_language = ["cs"]
input_text_elements = ["This is a test."]
Expand Down
12 changes: 8 additions & 4 deletions sdk/translation/azure-ai-translation-text/tests/testcase.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,19 @@ def create_client(self, endpoint, apikey, region):
client = TextTranslationClient(endpoint=endpoint, credential=credential, region=region)
return client

def create_client_token(self, endpoint, apikey, region):
def create_client_token(self, endpoint, apikey, region, audience):
credential = StaticAccessTokenCredential(apikey, region)
client = TextTranslationClient(endpoint=endpoint, credential=credential)
client = TextTranslationClient(endpoint=endpoint, credential=credential, audience=audience)
return client

def create_text_translation_client_with_aad(self, innerCredential, aadRegion, aadResourceId):
text_translator = TextTranslationClient(credential=innerCredential, resource_id=aadResourceId, region=aadRegion)
return text_translator

def create_text_translation_client_custom_with_aad(self, innerCredential, endpoint):
text_translator = TextTranslationClient(credential=innerCredential, endpoint=endpoint)
return text_translator

def create_async_getlanguage_client(self, endpoint):
from azure.ai.translation.text.aio import TextTranslationClient as TextTranslationClientAsync

Expand All @@ -44,11 +48,11 @@ def create_async_client(self, endpoint, apikey, region):
client = TextTranslationClientAsync(endpoint=endpoint, credential=credential, region=region)
return client

def create_async_client_token(self, endpoint, apikey, region):
def create_async_client_token(self, endpoint, apikey, region, audience):
credential = StaticAccessTokenCredential(apikey, region)
from azure.ai.translation.text.aio import TextTranslationClient as TextTranslationClientAsync

client = TextTranslationClientAsync(endpoint=endpoint, credential=credential)
client = TextTranslationClientAsync(endpoint=endpoint, credential=credential, audience=audience)
return client

def create_async_text_translation_client_with_aad(self, innerCredential, aadRegion, aadResourceId):
Expand Down

0 comments on commit f792549

Please sign in to comment.