Skip to content

Commit

Permalink
[QA] support AAD auth (#25256)
Browse files Browse the repository at this point in the history
* aad support for analysis and projects client via patch

* add tests

* update changelog, docs, setup.py

* update version

* update shared reqs
  • Loading branch information
kristapratico authored Jul 19, 2022
1 parent 45a0432 commit 3b03626
Show file tree
Hide file tree
Showing 13 changed files with 373 additions and 26 deletions.
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
# Release History

## 1.1.0b2 (Unreleased)
## 1.1.0b2 (2022-07-19)

### Features Added

### Breaking Changes

### Bugs Fixed
* Added Azure Active Directory (AAD) authentication support

### Other Changes
* Python 2.7 is no longer supported. Please use Python version 3.6 or later.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,14 @@ _Azure SDK Python packages support for Python 2.7 ended 01 January 2022. For mor

- Python 3.6 or later is required to use this package.
- An [Azure subscription][azure_subscription]
- An existing Question Answering resource

> Note: the new unified Cognitive Language Services are not currently available for deployment.
- A Language Service resource

### Install the package

Install the Azure QuestionAnswering client library for Python with [pip][pip_link]:

```bash
pip install azure-ai-language-questionanswering
pip install azure-ai-language-questionanswering --pre
```

### Authenticate the client
Expand Down Expand Up @@ -69,6 +67,37 @@ credential = AzureKeyCredential("{api-key}")
client = QuestionAnsweringProjectsClient(endpoint, credential)
```

#### Create a client with an Azure Active Directory Credential

To use an [Azure Active Directory (AAD) token credential][cognitive_authentication_aad],
provide an instance of the desired credential type obtained from the
[azure-identity][azure_identity_credentials] library.
Note that regional endpoints do not support AAD authentication. Create a [custom subdomain][custom_subdomain]
name for your resource in order to use this type of authentication.

Authentication with AAD requires some initial setup:

- [Install azure-identity][install_azure_identity]
- [Register a new AAD application][register_aad_app]
- [Grant access][grant_role_access] to the Language service by assigning the "Cognitive Services Language Reader" role to your service principal.

After setup, you can choose which type of [credential][azure_identity_credentials] from azure.identity to use.
As an example, [DefaultAzureCredential][default_azure_credential]
can be used to authenticate the client:

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`

Use the returned token credential to authenticate the client:

```python
from azure.ai.textanalytics import QuestionAnsweringClient
from azure.identity import DefaultAzureCredential

credential = DefaultAzureCredential()
client = QuestionAnsweringClient(endpoint="https://<my-custom-subdomain>.cognitiveservices.azure.com/", credential=credential)
```

## Key concepts

### QuestionAnsweringClient
Expand Down Expand Up @@ -317,5 +346,12 @@ This project has adopted the [Microsoft Open Source Code of Conduct][code_of_con
[questionanswering_refdocs]: https://azuresdkdocs.blob.core.windows.net/$web/python/azure-ai-language-questionanswering/latest/azure.ai.language.questionanswering.html
[questionanswering_rest_docs]: https://docs.microsoft.com/rest/api/cognitiveservices-qnamaker/
[questionanswering_samples]: https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/cognitivelanguage/azure-ai-language-questionanswering/samples/README.md
[cognitive_authentication_aad]: https://docs.microsoft.com/azure/cognitive-services/authentication#authenticate-with-azure-active-directory
[azure_identity_credentials]: https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/identity/azure-identity#credentials
[custom_subdomain]: https://docs.microsoft.com/azure/cognitive-services/authentication#create-a-resource-with-a-custom-subdomain
[install_azure_identity]: https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/identity/azure-identity#install-the-package
[register_aad_app]: https://docs.microsoft.com/azure/cognitive-services/authentication#assign-a-role-to-a-service-principal
[grant_role_access]: https://docs.microsoft.com/azure/cognitive-services/authentication#assign-a-role-to-a-service-principal
[default_azure_credential]: https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/identity/azure-identity#defaultazurecredential

![Impressions](https://azure-sdk-impressions.azurewebsites.net/api/impressions/azure-sdk-for-python%2Fsdk%2Ftemplate%2Fazure-template%2FREADME.png)
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,32 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------
from typing import Any

from azure.core.credentials import AzureKeyCredential

from typing import Union, Any
from azure.core.credentials import AzureKeyCredential, TokenCredential
from azure.core.pipeline.policies import AzureKeyCredentialPolicy, BearerTokenCredentialPolicy
from ._question_answering_client import QuestionAnsweringClient as QuestionAnsweringClientGenerated


def _authentication_policy(credential, **kwargs):
if credential is None:
raise ValueError("Parameter 'credential' must not be None.")
if isinstance(credential, AzureKeyCredential):
authentication_policy = AzureKeyCredentialPolicy(
name="Ocp-Apim-Subscription-Key", credential=credential, **kwargs
)
elif hasattr(credential, "get_token"):
authentication_policy = BearerTokenCredentialPolicy(
credential, *kwargs.pop("credential_scopes", ["https://cognitiveservices.azure.com/.default"]), **kwargs
)
else:
raise TypeError(
"Unsupported credential: {}. Use an instance of AzureKeyCredential "
"or a token credential from azure.identity".format(type(credential))
)
return authentication_policy


class QuestionAnsweringClient(QuestionAnsweringClientGenerated):
"""The language service API is a suite of natural language processing (NLP) skills built with best-in-class Microsoft machine learning algorithms.
Expand All @@ -21,12 +40,19 @@ class QuestionAnsweringClient(QuestionAnsweringClientGenerated):
https://<resource-name>.api.cognitiveservices.azure.com).
:type endpoint: str
:param credential: Credential needed for the client to connect to Azure.
:type credential: ~azure.core.credentials.AzureKeyCredential
This can be the an instance of AzureKeyCredential if using a Language API key
or a token credential from :mod:`azure.identity`.
:type credential: ~azure.core.credentials.AzureKeyCredential or ~azure.core.credentials.TokenCredential
:keyword str default_language: Sets the default language to use for all operations.
"""

def __init__(self, endpoint: str, credential: AzureKeyCredential, **kwargs: Any) -> None:
super().__init__(endpoint, credential, **kwargs)
def __init__(self, endpoint: str, credential: Union[AzureKeyCredential, TokenCredential], **kwargs: Any) -> None:
super().__init__(
endpoint=endpoint,
credential=credential, # type: ignore
authentication_policy=kwargs.pop("authentication_policy", _authentication_policy(credential, **kwargs)),
**kwargs
)
self._default_language = kwargs.pop("default_language", None)


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,32 @@
Follow our quickstart for examples: https://aka.ms/azsdk/python/dpcodegen/python/customize
"""
from typing import Any, List
from typing import List, Union, Any
from azure.core.credentials import AzureKeyCredential
from azure.core.credentials_async import AsyncTokenCredential
from azure.core.pipeline.policies import AzureKeyCredentialPolicy, AsyncBearerTokenCredentialPolicy
from ._question_answering_client import QuestionAnsweringClient as QuestionAnsweringClientGenerated


def _authentication_policy(credential, **kwargs):
if credential is None:
raise ValueError("Parameter 'credential' must not be None.")
if isinstance(credential, AzureKeyCredential):
authentication_policy = AzureKeyCredentialPolicy(
name="Ocp-Apim-Subscription-Key", credential=credential, **kwargs
)
elif hasattr(credential, "get_token"):
authentication_policy = AsyncBearerTokenCredentialPolicy(
credential, *kwargs.pop("credential_scopes", ["https://cognitiveservices.azure.com/.default"]), **kwargs
)
else:
raise TypeError(
"Unsupported credential: {}. Use an instance of AzureKeyCredential "
"or a token credential from azure.identity".format(type(credential))
)
return authentication_policy


class QuestionAnsweringClient(QuestionAnsweringClientGenerated):
"""The language service API is a suite of natural language processing (NLP) skills built with best-in-class Microsoft machine learning algorithms.
Expand All @@ -22,12 +43,21 @@ class QuestionAnsweringClient(QuestionAnsweringClientGenerated):
https://<resource-name>.api.cognitiveservices.azure.com).
:type endpoint: str
:param credential: Credential needed for the client to connect to Azure.
:type credential: ~azure.core.credentials.AzureKeyCredential
This can be the an instance of AzureKeyCredential if using a Language API key
or a token credential from :mod:`azure.identity`.
:type credential: ~azure.core.credentials.AzureKeyCredential or ~azure.core.credentials_async.AsyncTokenCredential
:keyword str default_language: Sets the default language to use for all operations.
"""

def __init__(self, endpoint: str, credential: AzureKeyCredential, **kwargs: Any) -> None:
super().__init__(endpoint, credential, **kwargs)
def __init__(
self, endpoint: str, credential: Union[AzureKeyCredential, AsyncTokenCredential], **kwargs: Any
) -> None:
super().__init__(
endpoint=endpoint,
credential=credential, # type: ignore
authentication_policy=kwargs.pop("authentication_policy", _authentication_policy(credential)),
**kwargs
)
self._default_language = kwargs.pop("default_language", None)


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,64 @@

# This file is used for handwritten extensions to the generated code. Example:
# https://github.com/Azure/azure-sdk-for-python/blob/main/doc/dev/customize_code/how-to-patch-sdk-code.md

import importlib
from typing import Union, Any
from azure.core.credentials import AzureKeyCredential, TokenCredential
from azure.core.pipeline.policies import AzureKeyCredentialPolicy, BearerTokenCredentialPolicy
from ._question_answering_projects_client import QuestionAnsweringProjectsClient \
as QuestionAnsweringProjectsClientGenerated


def _authentication_policy(credential, **kwargs):
if credential is None:
raise ValueError("Parameter 'credential' must not be None.")
if isinstance(credential, AzureKeyCredential):
authentication_policy = AzureKeyCredentialPolicy(
name="Ocp-Apim-Subscription-Key", credential=credential, **kwargs
)
elif hasattr(credential, "get_token"):
authentication_policy = BearerTokenCredentialPolicy(
credential, *kwargs.pop("credential_scopes", ["https://cognitiveservices.azure.com/.default"]), **kwargs
)
else:
raise TypeError(
"Unsupported credential: {}. Use an instance of AzureKeyCredential "
"or a token credential from azure.identity".format(type(credential))
)
return authentication_policy


class QuestionAnsweringProjectsClient(QuestionAnsweringProjectsClientGenerated):
"""The language service API is a suite of natural language processing (NLP) skills built with
best-in-class Microsoft machine learning algorithms. The API can be used to analyze
unstructured text for tasks such as sentiment analysis, key phrase extraction, language
detection and question answering. Further documentation can be found in :code:`<a
href="https://docs.microsoft.com/en-us/azure/cognitive-services/text-analytics/overview">https://docs.microsoft.com/en-us/azure/cognitive-services/text-analytics/overview</a>`.
:param endpoint: Supported Cognitive Services endpoint (e.g.,
https://:code:`<resource-name>`.api.cognitiveservices.azure.com).
:type endpoint: str
:param credential: Credential needed for the client to connect to Azure.
This can be the an instance of AzureKeyCredential if using a Language API key
or a token credential from :mod:`azure.identity`.
:type credential: ~azure.core.credentials.AzureKeyCredential or ~azure.core.credentials.TokenCredential
:keyword api_version: Api Version. The default value is "2021-10-01". Note that overriding this
default value may result in unsupported behavior.
:paramtype api_version: str
:keyword int polling_interval: Default waiting time between two polls for LRO operations if no
Retry-After header is present.
"""

def __init__(self, endpoint: str, credential: Union[AzureKeyCredential, TokenCredential], **kwargs: Any) -> None:
super().__init__(
endpoint=endpoint,
credential=credential, # type: ignore
authentication_policy=kwargs.pop("authentication_policy", _authentication_policy(credential, **kwargs)),
**kwargs
)


def patch_sdk():
pass
curr_package = importlib.import_module("azure.ai.language.questionanswering.projects")
curr_package.QuestionAnsweringProjectsClient = QuestionAnsweringProjectsClient
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@
# Changes may cause incorrect behavior and will be lost if the code is regenerated.
# --------------------------------------------------------------------------

VERSION = "1.1.0b1"
VERSION = "1.1.0b2"
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,65 @@

# This file is used for handwritten extensions to the generated code. Example:
# https://github.com/Azure/azure-sdk-for-python/blob/main/doc/dev/customize_code/how-to-patch-sdk-code.md

import importlib
from typing import Union, Any
from azure.core.credentials import AzureKeyCredential
from azure.core.credentials_async import AsyncTokenCredential
from azure.core.pipeline.policies import AzureKeyCredentialPolicy, AsyncBearerTokenCredentialPolicy
from ._question_answering_projects_client import QuestionAnsweringProjectsClient \
as QuestionAnsweringProjectsClientGenerated


def _authentication_policy(credential, **kwargs):
if credential is None:
raise ValueError("Parameter 'credential' must not be None.")
if isinstance(credential, AzureKeyCredential):
authentication_policy = AzureKeyCredentialPolicy(
name="Ocp-Apim-Subscription-Key", credential=credential, **kwargs
)
elif hasattr(credential, "get_token"):
authentication_policy = AsyncBearerTokenCredentialPolicy(
credential, *kwargs.pop("credential_scopes", ["https://cognitiveservices.azure.com/.default"]), **kwargs
)
else:
raise TypeError(
"Unsupported credential: {}. Use an instance of AzureKeyCredential "
"or a token credential from azure.identity".format(type(credential))
)
return authentication_policy


class QuestionAnsweringProjectsClient(QuestionAnsweringProjectsClientGenerated):
"""The language service API is a suite of natural language processing (NLP) skills built with
best-in-class Microsoft machine learning algorithms. The API can be used to analyze
unstructured text for tasks such as sentiment analysis, key phrase extraction, language
detection and question answering. Further documentation can be found in :code:`<a
href="https://docs.microsoft.com/en-us/azure/cognitive-services/text-analytics/overview">https://docs.microsoft.com/en-us/azure/cognitive-services/text-analytics/overview</a>`.
:param endpoint: Supported Cognitive Services endpoint (e.g.,
https://:code:`<resource-name>`.api.cognitiveservices.azure.com).
:type endpoint: str
:param credential: Credential needed for the client to connect to Azure.
This can be the an instance of AzureKeyCredential if using a Language API key
or a token credential from :mod:`azure.identity`.
:type credential: ~azure.core.credentials.AzureKeyCredential or ~azure.core.credentials_async.AsyncTokenCredential
:keyword api_version: Api Version. The default value is "2021-10-01". Note that overriding this
default value may result in unsupported behavior.
:paramtype api_version: str
:keyword int polling_interval: Default waiting time between two polls for LRO operations if no
Retry-After header is present.
"""

def __init__(self, endpoint: str, credential: Union[AzureKeyCredential, AsyncTokenCredential], **kwargs: Any) -> None:
super().__init__(
endpoint=endpoint,
credential=credential, # type: ignore
authentication_policy=kwargs.pop("authentication_policy", _authentication_policy(credential, **kwargs)),
**kwargs
)


def patch_sdk():
pass
curr_package = importlib.import_module("azure.ai.language.questionanswering.projects.aio")
curr_package.QuestionAnsweringProjectsClient = QuestionAnsweringProjectsClient
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,10 @@
license='MIT License',
# ensure that the development status reflects the status of your package
classifiers=[
"Development Status :: 5 - Production/Stable",
"Development Status :: 4 - Beta",
"Programming Language :: Python",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
Expand All @@ -64,7 +63,7 @@
'pytyped': ['py.typed'],
},
install_requires=[
'azure-core<2.0.0,>=1.19.1',
"azure-core<2.0.0,>=1.24.0",
'msrest>=0.6.21',
],
project_urls={
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,33 @@

class CreateAndDeployTests(QuestionAnsweringTest):

@pytest.mark.live_test_only
@GlobalQuestionAnsweringAccountPreparer()
def test_create_project_aad(self, qna_account, qna_key):
token = self.get_credential(QuestionAnsweringProjectsClient)
client = QuestionAnsweringProjectsClient(qna_account, token)

# create project
project_name = "IssacNewton"
client.create_project(
project_name=project_name,
options={
"description": "biography of Sir Issac Newton",
"language": "en",
"multilingualResource": True,
"settings": {
"defaultAnswer": "no answer"
}
})

# list projects
qna_projects = client.list_projects()
found = False
for p in qna_projects:
if ("projectName" in p) and p["projectName"] == project_name:
found = True
assert found

@GlobalQuestionAnsweringAccountPreparer()
def test_create_project(self, qna_account, qna_key):
client = QuestionAnsweringProjectsClient(qna_account, AzureKeyCredential(qna_key))
Expand Down
Loading

0 comments on commit 3b03626

Please sign in to comment.