From 29f690091c70232a8c3e0192841c50a1ef190162 Mon Sep 17 00:00:00 2001 From: Krista Pratico Date: Wed, 26 May 2021 09:36:11 -0700 Subject: [PATCH] [translation] AAD support (#18892) * add support for aad authentication * add samples and snippets for aad auth * changelog, readme, dev requirements update * add basic test cases for aad auth * fix envar * review feedback --- .../CHANGELOG.md | 2 ++ .../azure-ai-translation-document/README.md | 31 +++++++++++++++++ .../azure/ai/translation/document/_client.py | 32 +++++++++--------- .../azure/ai/translation/document/_helpers.py | 20 +++++++++++ .../translation/document/aio/_client_async.py | 33 +++++++++++-------- .../dev_requirements.txt | 2 +- .../samples/README.md | 2 +- .../sample_authentication_async.py | 29 +++++++++++++++- .../samples/sample_authentication.py | 28 +++++++++++++++- .../tests/asynctestcase.py | 11 +++++++ .../tests/test_translation.py | 27 +++++++++++++++ .../tests/test_translation_async.py | 27 +++++++++++++++ .../tests/testcase.py | 12 +++++++ 13 files changed, 223 insertions(+), 33 deletions(-) diff --git a/sdk/translation/azure-ai-translation-document/CHANGELOG.md b/sdk/translation/azure-ai-translation-document/CHANGELOG.md index ee43f1a52da9..f8f2419f7c2c 100644 --- a/sdk/translation/azure-ai-translation-document/CHANGELOG.md +++ b/sdk/translation/azure-ai-translation-document/CHANGELOG.md @@ -6,6 +6,8 @@ This version of the SDK defaults to the latest supported service version, which **New features** +- Authentication using `azure-identity` credentials now supported. + - see the [Azure Identity documentation](https://github.com/Azure/azure-sdk-for-python/blob/master/sdk/identity/azure-identity/README.md) for more information. - Added paging and filtering options to `list_all_document_statuses` and `list_submitted_jobs`. **Dependency updates** diff --git a/sdk/translation/azure-ai-translation-document/README.md b/sdk/translation/azure-ai-translation-document/README.md index 3be6a2d528c1..d42e86a75f6f 100644 --- a/sdk/translation/azure-ai-translation-document/README.md +++ b/sdk/translation/azure-ai-translation-document/README.md @@ -86,6 +86,33 @@ credential = AzureKeyCredential("") document_translation_client = DocumentTranslationClient(endpoint, credential) ``` +#### Create the client with an Azure Active Directory credential + +`AzureKeyCredential` authentication is used in the examples in this getting started guide, but you can also +authenticate with Azure Active Directory using the [azure-identity][azure_identity] library. + +To use the [DefaultAzureCredential][default_azure_credential] type shown below, or other credential types provided +with the Azure SDK, please install the `azure-identity` package: + +```pip install azure-identity``` + +You will also need to [register a new AAD application and grant access][register_aad_app] to your +Translator resource by assigning the `"Cognitive Services User"` role to your service principal. + +Once completed, set the values of the client ID, tenant ID, and client secret of the AAD application as environment variables: +`AZURE_CLIENT_ID`, `AZURE_TENANT_ID`, `AZURE_CLIENT_SECRET`. + +```python +from azure.identity import DefaultAzureCredential +from azure.ai.translation.document import DocumentTranslationClient +credential = DefaultAzureCredential() + +document_translation_client = DocumentTranslationClient( + endpoint="https://.cognitiveservices.azure.com/", + credential=credential +) +``` + ## Key concepts The Document Translation service requires that you upload your files to an Azure Blob Storage source container and provide @@ -380,6 +407,10 @@ This project has adopted the [Microsoft Open Source Code of Conduct][code_of_con [azure_cli_endpoint_lookup]: https://docs.microsoft.com/cli/azure/cognitiveservices/account?view=azure-cli-latest#az-cognitiveservices-account-show [azure_portal_get_endpoint]: https://docs.microsoft.com/azure/cognitive-services/translator/document-translation/get-started-with-document-translation?tabs=csharp#get-your-custom-domain-name-and-subscription-key [cognitive_authentication_api_key]: https://docs.microsoft.com/azure/cognitive-services/translator/document-translation/get-started-with-document-translation?tabs=csharp#get-your-subscription-key +[register_aad_app]: https://docs.microsoft.com/azure/cognitive-services/authentication?tabs=powershell#authenticate-with-azure-active-directory +[custom_subdomain]: https://docs.microsoft.com/azure/cognitive-services/authentication#create-a-resource-with-a-custom-subdomain +[azure_identity]: https://github.com/Azure/azure-sdk-for-python/tree/master/sdk/identity/azure-identity +[default_azure_credential]: https://github.com/Azure/azure-sdk-for-python/tree/master/sdk/identity/azure-identity#defaultazurecredential [sdk_logging_docs]: https://docs.microsoft.com/azure/developer/python/azure-sdk-logging diff --git a/sdk/translation/azure-ai-translation-document/azure/ai/translation/document/_client.py b/sdk/translation/azure-ai-translation-document/azure/ai/translation/document/_client.py index 86cf31392dc3..6e71e548c513 100644 --- a/sdk/translation/azure-ai-translation-document/azure/ai/translation/document/_client.py +++ b/sdk/translation/azure-ai-translation-document/azure/ai/translation/document/_client.py @@ -4,12 +4,10 @@ # Licensed under the MIT License. # ------------------------------------ -from typing import Any, TYPE_CHECKING, List +from typing import Any, TYPE_CHECKING, List, Union from azure.core.tracing.decorator import distributed_trace from azure.core.polling import LROPoller from azure.core.polling.base_polling import LROBasePolling -from azure.core.credentials import AzureKeyCredential -from azure.core.pipeline.policies import AzureKeyCredentialPolicy from ._generated import BatchDocumentTranslationClient as _BatchDocumentTranslationClient from ._generated.models import TranslationStatus as _TranslationStatus from ._models import ( @@ -20,26 +18,27 @@ ) from ._user_agent import USER_AGENT from ._polling import TranslationPolling -from ._helpers import get_http_logging_policy, convert_datetime +from ._helpers import get_http_logging_policy, convert_datetime, get_authentication_policy if TYPE_CHECKING: from azure.core.paging import ItemPaged - -COGNITIVE_KEY_HEADER = "Ocp-Apim-Subscription-Key" + from azure.core.credentials import TokenCredential, AzureKeyCredential class DocumentTranslationClient(object): # pylint: disable=r0205 def __init__(self, endpoint, credential, **kwargs): - # type: (str, AzureKeyCredential, **Any) -> None + # type: (str, Union[AzureKeyCredential, TokenCredential], Any) -> None """DocumentTranslationClient is your interface to the Document Translation service. Use the client to translate whole documents while preserving source document structure and text formatting. :param str endpoint: Supported Document Translation endpoint (protocol and hostname, for example: https://.cognitiveservices.azure.com/). - :param credential: Credential needed for the client to connect to Azure. - Currently only API key authentication is supported. - :type credential: :class:`~azure.core.credentials.AzureKeyCredential` + :param credential: Credentials needed for the client to connect to Azure. + This is an instance of AzureKeyCredential if using an API key or a token + credential from :mod:`azure.identity`. + :type credential: :class:`~azure.core.credentials.AzureKeyCredential` or + :class:`~azure.core.credentials.TokenCredential` :keyword api_version: The API version of the service to use for requests. It defaults to the latest service version. Setting to an older version may result in reduced feature compatibility. @@ -53,16 +52,19 @@ def __init__(self, endpoint, credential, **kwargs): :language: python :dedent: 4 :caption: Creating the DocumentTranslationClient with an endpoint and API key. + + .. literalinclude:: ../samples/sample_authentication.py + :start-after: [START create_dt_client_with_aad] + :end-before: [END create_dt_client_with_aad] + :language: python + :dedent: 4 + :caption: Creating the DocumentTranslationClient with a token credential. """ self._endpoint = endpoint self._credential = credential self._api_version = kwargs.pop('api_version', None) - if credential is None: - raise ValueError("Parameter 'credential' must not be None.") - authentication_policy = AzureKeyCredentialPolicy( - name=COGNITIVE_KEY_HEADER, credential=credential - ) + authentication_policy = get_authentication_policy(credential) self._client = _BatchDocumentTranslationClient( endpoint=endpoint, diff --git a/sdk/translation/azure-ai-translation-document/azure/ai/translation/document/_helpers.py b/sdk/translation/azure-ai-translation-document/azure/ai/translation/document/_helpers.py index 02c6f8154dfa..586ba746585f 100644 --- a/sdk/translation/azure-ai-translation-document/azure/ai/translation/document/_helpers.py +++ b/sdk/translation/azure-ai-translation-document/azure/ai/translation/document/_helpers.py @@ -7,7 +7,27 @@ import datetime from typing import Union import six +from azure.core.credentials import AzureKeyCredential +from azure.core.pipeline.policies import AzureKeyCredentialPolicy from azure.core.pipeline.policies import HttpLoggingPolicy +COGNITIVE_KEY_HEADER = "Ocp-Apim-Subscription-Key" + + +def get_authentication_policy(credential): + authentication_policy = None + if credential is None: + raise ValueError("Parameter 'credential' must not be None.") + if isinstance(credential, AzureKeyCredential): + authentication_policy = AzureKeyCredentialPolicy( + name=COGNITIVE_KEY_HEADER, credential=credential + ) + elif credential is not None and not hasattr(credential, "get_token"): + raise TypeError( + "Unsupported credential: {}. Use an instance of AzureKeyCredential " + "or a token credential from azure.identity".format(type(credential)) + ) + + return authentication_policy def get_http_logging_policy(**kwargs): diff --git a/sdk/translation/azure-ai-translation-document/azure/ai/translation/document/aio/_client_async.py b/sdk/translation/azure-ai-translation-document/azure/ai/translation/document/aio/_client_async.py index 0e3f617990fb..a734de05689e 100644 --- a/sdk/translation/azure-ai-translation-document/azure/ai/translation/document/aio/_client_async.py +++ b/sdk/translation/azure-ai-translation-document/azure/ai/translation/document/aio/_client_async.py @@ -4,14 +4,12 @@ # Licensed under the MIT License. # ------------------------------------ -from typing import Any, List +from typing import Any, List, Union, TYPE_CHECKING from azure.core.tracing.decorator_async import distributed_trace_async from azure.core.tracing.decorator import distributed_trace from azure.core.polling import AsyncLROPoller from azure.core.polling.async_base_polling import AsyncLROBasePolling from azure.core.async_paging import AsyncItemPaged -from azure.core.credentials import AzureKeyCredential -from azure.core.pipeline.policies import AzureKeyCredentialPolicy from .._generated.aio import BatchDocumentTranslationClient as _BatchDocumentTranslationClient from .._user_agent import USER_AGENT from .._generated.models import ( @@ -23,15 +21,17 @@ FileFormat, DocumentStatusResult ) -from .._helpers import get_http_logging_policy, convert_datetime +from .._helpers import get_http_logging_policy, convert_datetime, get_authentication_policy from .._polling import TranslationPolling -COGNITIVE_KEY_HEADER = "Ocp-Apim-Subscription-Key" +if TYPE_CHECKING: + from azure.core.credentials import AzureKeyCredential + from azure.core.credentials_async import AsyncTokenCredential class DocumentTranslationClient(object): def __init__( - self, endpoint: str, credential: "AzureKeyCredential", **kwargs: Any + self, endpoint: str, credential: Union["AzureKeyCredential", "AsyncTokenCredential"], **kwargs: Any ) -> None: """DocumentTranslationClient is your interface to the Document Translation service. Use the client to translate whole documents while preserving source document @@ -39,9 +39,11 @@ def __init__( :param str endpoint: Supported Document Translation endpoint (protocol and hostname, for example: https://.cognitiveservices.azure.com/). - :param credential: Credential needed for the client to connect to Azure. - Currently only API key authentication is supported. - :type credential: :class:`~azure.core.credentials.AzureKeyCredential` + :param credential: Credentials needed for the client to connect to Azure. + This is an instance of AzureKeyCredential if using an API key or a token + credential from :mod:`azure.identity`. + :type credential: :class:`~azure.core.credentials.AzureKeyCredential` or + :class:`~azure.core.credentials.TokenCredential` :keyword api_version: The API version of the service to use for requests. It defaults to the latest service version. Setting to an older version may result in reduced feature compatibility. @@ -55,16 +57,19 @@ def __init__( :language: python :dedent: 4 :caption: Creating the DocumentTranslationClient with an endpoint and API key. + + .. literalinclude:: ../samples/async_samples/sample_authentication_async.py + :start-after: [START create_dt_client_with_aad_async] + :end-before: [END create_dt_client_with_aad_async] + :language: python + :dedent: 4 + :caption: Creating the DocumentTranslationClient with a token credential. """ self._endpoint = endpoint self._credential = credential self._api_version = kwargs.pop('api_version', None) - if credential is None: - raise ValueError("Parameter 'credential' must not be None.") - authentication_policy = AzureKeyCredentialPolicy( - name=COGNITIVE_KEY_HEADER, credential=credential - ) + authentication_policy = get_authentication_policy(credential) self._client = _BatchDocumentTranslationClient( endpoint=endpoint, credential=credential, # type: ignore diff --git a/sdk/translation/azure-ai-translation-document/dev_requirements.txt b/sdk/translation/azure-ai-translation-document/dev_requirements.txt index 21efdf5ae5a1..c983a704d5fe 100644 --- a/sdk/translation/azure-ai-translation-document/dev_requirements.txt +++ b/sdk/translation/azure-ai-translation-document/dev_requirements.txt @@ -2,6 +2,6 @@ -e ../../../tools/azure-devtools ../../core/azure-core ../../storage/azure-storage-blob -../../nspkg/azure-ai-translation-nspkg +-e ../../identity/azure-identity aiohttp>=3.0; python_version >= '3.5' pytz diff --git a/sdk/translation/azure-ai-translation-document/samples/README.md b/sdk/translation/azure-ai-translation-document/samples/README.md index 52380f344f6c..f88788183a24 100644 --- a/sdk/translation/azure-ai-translation-document/samples/README.md +++ b/sdk/translation/azure-ai-translation-document/samples/README.md @@ -14,7 +14,7 @@ urlFragment: documenttranslation-samples These code samples show common scenario operations with the Azure Document Translation client library. The async versions of the samples require Python 3.6 or later. -You can authenticate your client with a Document Translation API key: +You can authenticate your client with a Document Translation API key or through Azure Active Directory with a token credential from [azure-identity][azure_identity]: * See [sample_authentication.py][sample_authentication] and [sample_authentication_async.py][sample_authentication_async] for how to authenticate in the above cases. These sample programs show common scenarios for the Document Translation client's offerings. diff --git a/sdk/translation/azure-ai-translation-document/samples/async_samples/sample_authentication_async.py b/sdk/translation/azure-ai-translation-document/samples/async_samples/sample_authentication_async.py index f8614a5e18a2..df9964f79d4a 100644 --- a/sdk/translation/azure-ai-translation-document/samples/async_samples/sample_authentication_async.py +++ b/sdk/translation/azure-ai-translation-document/samples/async_samples/sample_authentication_async.py @@ -12,8 +12,12 @@ DESCRIPTION: This sample demonstrates how to authenticate to the Document Translation service. - There is currently only one supported method of authentication: + There are two supported methods of authentication: 1) Use a Document Translation API key with AzureKeyCredential from azure.core.credentials + 2) Use a token credential from azure-identity to authenticate with Azure Active Directory + + See more details about authentication here: + https://docs.microsoft.com/azure/cognitive-services/authentication Note: the endpoint must be formatted to use the custom domain name for your resource: https://.cognitiveservices.azure.com/ @@ -24,6 +28,9 @@ Set the environment variables with your own values before running the sample: 1) AZURE_DOCUMENT_TRANSLATION_ENDPOINT - the endpoint to your Document Translation resource. 2) AZURE_DOCUMENT_TRANSLATION_KEY - your Document Translation API key + 3) AZURE_CLIENT_ID - the client ID of your active directory application. + 4) AZURE_TENANT_ID - the tenant ID of your active directory application. + 5) AZURE_CLIENT_SECRET - the secret of your active directory application. """ import os @@ -46,8 +53,28 @@ async def sample_authentication_api_key_async(): result = await document_translation_client.get_document_formats() +def sample_authentication_with_azure_active_directory_async(): + # [START create_dt_client_with_aad_async] + """DefaultAzureCredential will use the values from these environment + variables: AZURE_CLIENT_ID, AZURE_TENANT_ID, AZURE_CLIENT_SECRET + """ + from azure.identity.aio import DefaultAzureCredential + from azure.ai.translation.document.aio import DocumentTranslationClient + + endpoint = os.environ["AZURE_DOCUMENT_TRANSLATION_ENDPOINT"] + credential = DefaultAzureCredential() + + document_translation_client = DocumentTranslationClient(endpoint, credential) + # [END create_dt_client_with_aad_async] + + # make calls with authenticated client + async with document_translation_client: + result = document_translation_client.get_document_formats() + + async def main(): await sample_authentication_api_key_async() + await sample_authentication_with_azure_active_directory_async() if __name__ == '__main__': loop = asyncio.get_event_loop() diff --git a/sdk/translation/azure-ai-translation-document/samples/sample_authentication.py b/sdk/translation/azure-ai-translation-document/samples/sample_authentication.py index febc7a89ffba..e66e891b6358 100644 --- a/sdk/translation/azure-ai-translation-document/samples/sample_authentication.py +++ b/sdk/translation/azure-ai-translation-document/samples/sample_authentication.py @@ -12,8 +12,12 @@ DESCRIPTION: This sample demonstrates how to authenticate to the Document Translation service. - There is currently only one supported method of authentication: + There are two supported methods of authentication: 1) Use a Document Translation API key with AzureKeyCredential from azure.core.credentials + 2) Use a token credential from azure-identity to authenticate with Azure Active Directory + + See more details about authentication here: + https://docs.microsoft.com/azure/cognitive-services/authentication Note: the endpoint must be formatted to use the custom domain name for your resource: https://.cognitiveservices.azure.com/ @@ -24,6 +28,9 @@ Set the environment variables with your own values before running the sample: 1) AZURE_DOCUMENT_TRANSLATION_ENDPOINT - the endpoint to your Document Translation resource. 2) AZURE_DOCUMENT_TRANSLATION_KEY - your Document Translation API key + 3) AZURE_CLIENT_ID - the client ID of your active directory application. + 4) AZURE_TENANT_ID - the tenant ID of your active directory application. + 5) AZURE_CLIENT_SECRET - the secret of your active directory application. """ import os @@ -44,5 +51,24 @@ def sample_authentication_api_key(): result = document_translation_client.get_document_formats() +def sample_authentication_with_azure_active_directory(): + # [START create_dt_client_with_aad] + """DefaultAzureCredential will use the values from these environment + variables: AZURE_CLIENT_ID, AZURE_TENANT_ID, AZURE_CLIENT_SECRET + """ + from azure.identity import DefaultAzureCredential + from azure.ai.translation.document import DocumentTranslationClient + + endpoint = os.environ["AZURE_DOCUMENT_TRANSLATION_ENDPOINT"] + credential = DefaultAzureCredential() + + document_translation_client = DocumentTranslationClient(endpoint, credential) + # [END create_dt_client_with_aad] + + # make calls with authenticated client + result = document_translation_client.get_document_formats() + + if __name__ == '__main__': sample_authentication_api_key() + sample_authentication_with_azure_active_directory() diff --git a/sdk/translation/azure-ai-translation-document/tests/asynctestcase.py b/sdk/translation/azure-ai-translation-document/tests/asynctestcase.py index 8d3ae9cf11c7..9207cfef14f9 100644 --- a/sdk/translation/azure-ai-translation-document/tests/asynctestcase.py +++ b/sdk/translation/azure-ai-translation-document/tests/asynctestcase.py @@ -3,6 +3,8 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. # ------------------------------------ + +import os from testcase import DocumentTranslationTest, Document from azure.ai.translation.document import DocumentTranslationInput, TranslationTarget @@ -11,6 +13,15 @@ class AsyncDocumentTranslationTest(DocumentTranslationTest): def __init__(self, method_name): super(AsyncDocumentTranslationTest, self).__init__(method_name) + def generate_oauth_token(self): + if self.is_live: + from azure.identity.aio import ClientSecretCredential + return ClientSecretCredential( + os.getenv("TRANSLATION_TENANT_ID"), + os.getenv("TRANSLATION_CLIENT_ID"), + os.getenv("TRANSLATION_CLIENT_SECRET"), + ) + async def _submit_and_validate_translation_job_async(self, async_client, translation_inputs, total_docs_count=None): # submit job job_details = await async_client.create_translation_job(translation_inputs) diff --git a/sdk/translation/azure-ai-translation-document/tests/test_translation.py b/sdk/translation/azure-ai-translation-document/tests/test_translation.py index 3ce65fee8994..29634a776526 100644 --- a/sdk/translation/azure-ai-translation-document/tests/test_translation.py +++ b/sdk/translation/azure-ai-translation-document/tests/test_translation.py @@ -18,6 +18,33 @@ class TestTranslation(DocumentTranslationTest): + @pytest.mark.live_test_only + @DocumentTranslationPreparer() + def test_active_directory_auth(self): + token = self.generate_oauth_token() + endpoint = self.get_oauth_endpoint() + client = DocumentTranslationClient(endpoint, token) + # prepare containers and test data + blob_data = b'This is some text' + source_container_sas_url = self.create_source_container(data=Document(data=blob_data)) + target_container_sas_url = self.create_target_container() + + # prepare translation inputs + translation_inputs = [ + DocumentTranslationInput( + source_url=source_container_sas_url, + targets=[ + TranslationTarget( + target_url=target_container_sas_url, + language_code="fr" + ) + ] + ) + ] + + # submit job and test + self._submit_and_validate_translation_job(client, translation_inputs, 1) + @DocumentTranslationPreparer() @DocumentTranslationClientPreparer() def test_single_source_single_target(self, client): diff --git a/sdk/translation/azure-ai-translation-document/tests/test_translation_async.py b/sdk/translation/azure-ai-translation-document/tests/test_translation_async.py index 330b078b0b63..e1688567aac6 100644 --- a/sdk/translation/azure-ai-translation-document/tests/test_translation_async.py +++ b/sdk/translation/azure-ai-translation-document/tests/test_translation_async.py @@ -19,6 +19,33 @@ class TestTranslation(AsyncDocumentTranslationTest): + @pytest.mark.live_test_only + @DocumentTranslationPreparer() + async def test_active_directory_auth_async(self): + token = self.generate_oauth_token() + endpoint = self.get_oauth_endpoint() + client = DocumentTranslationClient(endpoint, token) + # prepare containers and test data + blob_data = b'This is some text' + source_container_sas_url = self.create_source_container(data=Document(data=blob_data)) + target_container_sas_url = self.create_target_container() + + # prepare translation inputs + translation_inputs = [ + DocumentTranslationInput( + source_url=source_container_sas_url, + targets=[ + TranslationTarget( + target_url=target_container_sas_url, + language_code="fr" + ) + ] + ) + ] + + # submit job and test + await self._submit_and_validate_translation_job_async(client, translation_inputs, 1) + @DocumentTranslationPreparer() @DocumentTranslationClientPreparer() async def test_single_source_single_target(self, client): diff --git a/sdk/translation/azure-ai-translation-document/tests/testcase.py b/sdk/translation/azure-ai-translation-document/tests/testcase.py index 53e07673d3ff..eddc55867f8a 100644 --- a/sdk/translation/azure-ai-translation-document/tests/testcase.py +++ b/sdk/translation/azure-ai-translation-document/tests/testcase.py @@ -81,6 +81,18 @@ def __init__(self, method_name): self.storage_key, "fakeZmFrZV9hY29jdW50X2tleQ==" ) + def get_oauth_endpoint(self): + return os.getenv("TRANSLATION_DOCUMENT_TEST_ENDPOINT") + + def generate_oauth_token(self): + if self.is_live: + from azure.identity import ClientSecretCredential + return ClientSecretCredential( + os.getenv("TRANSLATION_TENANT_ID"), + os.getenv("TRANSLATION_CLIENT_ID"), + os.getenv("TRANSLATION_CLIENT_SECRET"), + ) + def upload_documents(self, data, container_client): if isinstance(data, list): for blob in data: