-
Notifications
You must be signed in to change notification settings - Fork 3.2k
[webpubsub] support AAD, Api management proxy #20533
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from 3 commits
Commits
Show all changes
33 commits
Select commit
Hold shift + click to select a range
a21afb3
codegen
msyyc a4d3d15
update test
msyyc c3995d5
update readme.md
msyyc 95d910a
add test
msyyc 090c8c1
from connection string
msyyc a4fdbbb
example
msyyc 156eaa6
Update _version.py
msyyc c374338
api management proxy
msyyc d25309a
Update _web_pub_sub_service_client.py
msyyc ec2ddb6
review
msyyc bebb486
api management
msyyc 859ccc6
fix dependency
msyyc 18ee661
fix dependency
msyyc fe01cff
apim proxy
msyyc 3983c03
edit code in _patch.py instead of generated code
msyyc 9cc207e
review
msyyc b4bfd53
fix broken link
msyyc be9026e
Update setup.py
msyyc e296dbe
Update dev_requirements.txt
msyyc a367a37
fix ci
msyyc 9f954b2
fix ci
msyyc 0fd5b47
fix ci failure of windows_python27
msyyc b95cbee
Update sdk/webpubsub/azure-messaging-webpubsubservice/examples/send_m…
msyyc 1ce7f49
Update sdk/webpubsub/azure-messaging-webpubsubservice/examples/send_m…
msyyc d34c3a0
Update README.md
msyyc 3dd7bea
review
msyyc 8dcfcb9
regenerate code
msyyc 5fa9d25
fix ci
msyyc 55537f7
fix ci
msyyc f49f435
move operation onto client
msyyc 132f1bc
review
msyyc 9534e09
Update send_messages_connection_string_apim_proxy.py
msyyc 3bf7b82
Update send_messages_aad_apim_proxy.py
msyyc File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -43,14 +43,26 @@ python -m pip install azure-messaging-webpubsubservice | |
|
|
||
| ### Authenticating the client | ||
|
|
||
| In order to interact with the Azure WebPubSub service, you'll need to create an instance of the [WebPubSubServiceClient][webpubsubservice_client_class] class. In order to authenticate against the service, you need to pass in an AzureKeyCredential instance with endpoint and api key. The endpoint and api key can be found on the azure portal. | ||
| To use an [Azure Active Directory (AAD) token credential][authenticate_with_token], | ||
msyyc marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| provide an instance of the desired credential type obtained from the | ||
| [azure-identity][azure_identity_credentials] library. | ||
|
|
||
| To authenticate with AAD, you must first [pip][pip] install [`azure-identity`][azure_identity_pip] and | ||
| [enable AAD authentication on your Webpubsub resource][enable_aad] | ||
|
|
||
| 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.messaging.webpubsubservice import WebPubSubServiceClient | ||
| >>> from azure.core.credentials import AzureKeyCredential | ||
| >>> client = WebPubSubServiceClient(endpoint='<endpoint>', credential=AzureKeyCredential('somesecret')) | ||
| >>> client | ||
| <WebPubSubServiceClient endpoint:'<endpoint>'> | ||
| >>> from azure.identity import DefaultAzureCredential | ||
| >>> client = WebPubSubServiceClient(endpoint='https://myname.webpubsub.azure.com', credential=DefaultAzureCredential()) | ||
| ``` | ||
|
|
||
| ## Examples | ||
|
|
@@ -59,22 +71,16 @@ In order to interact with the Azure WebPubSub service, you'll need to create an | |
|
|
||
| ```python | ||
| >>> from azure.messaging.webpubsubservice import WebPubSubServiceClient | ||
| >>> from azure.core.credentials import AzureKeyCredential | ||
| >>> from azure.messaging.webpubsubservice.rest import build_send_to_all_request | ||
| >>> client = WebPubSubServiceClient(endpoint='<endpoint>', credential=AzureKeyCredential('somesecret')) | ||
| >>> request = build_send_to_all_request('default', json={ 'Hello': 'webpubsub!' }) | ||
| >>> request | ||
| <HttpRequest [POST], url: '/api/hubs/default/:send?api-version=2020-10-01'> | ||
| >>> response = client.send_request(request) | ||
| >>> response | ||
| <RequestsTransportResponse: 202 Accepted> | ||
| >>> response.status_code | ||
| 202 | ||
| >>> from azure.identity import DefaultAzureCredential | ||
| >>> from azure.core.exceptions import HttpResponseError | ||
|
|
||
| >>> client = WebPubSubServiceClient(endpoint='https://myname.webpubsub.azure.com', credential=DefaultAzureCredential()) | ||
| >>> with open('file.json', 'r') as f: | ||
| >>> request = build_send_to_all_request('ahub', content=f, content_type='application/json') | ||
| >>> response = client.send_request(request) | ||
| >>> print(response) | ||
| <RequestsTransportResponse: 202 Accepted> | ||
| try: | ||
msyyc marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| client.web_pub_sub.send_to_all('ahub', content=f, content_type='application/json') | ||
msyyc marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| except HttpResponseError as e: | ||
| print('service responds error') | ||
|
|
||
| ``` | ||
|
|
||
| ## Key concepts | ||
|
|
@@ -107,10 +113,32 @@ This SDK uses Python standard logging library. | |
| You can configure logging print out debugging information to the stdout or anywhere you want. | ||
|
|
||
| ```python | ||
| import sys | ||
| import logging | ||
| from azure.identity import DefaultAzureCredential | ||
| >>> from azure.messaging.webpubsubservice import WebPubSubServiceClient | ||
|
|
||
| # Create a logger for the 'azure' SDK | ||
| logger = logging.getLogger('azure') | ||
| logger.setLevel(logging.DEBUG) | ||
|
|
||
| logging.basicConfig(level=logging.DEBUG) | ||
| ```` | ||
| # Configure a console output | ||
| handler = logging.StreamHandler(stream=sys.stdout) | ||
| logger.addHandler(handler) | ||
|
|
||
| endpoint = "https://myname.webpubsub.azure.com" | ||
| credential = DefaultAzureCredential() | ||
|
|
||
| # This client will log detailed information about its HTTP sessions, at DEBUG level | ||
| client = WebPubSubServiceClient(endpoint=endpoint, credential=credential, logging_enable=True) | ||
| ``` | ||
|
|
||
| Similarly, `logging_enable` can enable detailed logging for a single call, | ||
| even when it isn't enabled for the client: | ||
|
|
||
| ```python | ||
| result = client.web_pub_sub.send_to_all(logging_enable=True) | ||
msyyc marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| ``` | ||
|
|
||
| Http request and response details are printed to stdout with this logging config. | ||
|
|
||
|
|
@@ -146,3 +174,9 @@ additional questions or comments. | |
| [code_of_conduct]: https://opensource.microsoft.com/codeofconduct/ | ||
| [coc_faq]: https://opensource.microsoft.com/codeofconduct/faq/ | ||
| [coc_contact]: mailto:[email protected] | ||
| [authenticate_with_token]: https://docs.microsoft.com/azure/cognitive-services/authentication?tabs=powershell#authenticate-with-an-authentication-token | ||
| [azure_identity_credentials]: https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/identity/azure-identity#credentials | ||
| [azure_identity_pip]: https://pypi.org/project/azure-identity/ | ||
| [default_azure_credential]: https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/identity/azure-identity#defaultazurecredential | ||
| [pip]: https://pypi.org/project/pip/ | ||
| [enable_aad]: https://docs.microsoft.com/en-us/azure/azure-web-pubsub/howto-develop-create-instance | ||
235 changes: 11 additions & 224 deletions
235
sdk/webpubsub/azure-messaging-webpubsubservice/azure/messaging/webpubsubservice/__init__.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,232 +1,19 @@ | ||
| # coding=utf-8 | ||
| # -------------------------------------------------------------------------- | ||
| # Copyright (c) Microsoft Corporation. All rights reserved. | ||
| # Licensed under the MIT License. See License.txt in the project root for | ||
| # license information. | ||
| # | ||
| # Licensed under the MIT License. See License.txt in the project root for license information. | ||
| # Code generated by Microsoft (R) AutoRest Code Generator. | ||
| # Changes may cause incorrect behavior and will be lost if the code is | ||
| # regenerated. | ||
| # Changes may cause incorrect behavior and will be lost if the code is regenerated. | ||
| # -------------------------------------------------------------------------- | ||
|
|
||
| __all__ = ["build_authentication_token", "WebPubSubServiceClient"] | ||
| from ._web_pub_sub_service_client import WebPubSubServiceClient | ||
| from ._version import VERSION | ||
|
|
||
| from copy import deepcopy | ||
| from datetime import datetime, timedelta | ||
| from typing import TYPE_CHECKING | ||
| __version__ = VERSION | ||
| __all__ = ['WebPubSubServiceClient'] | ||
|
|
||
| import jwt | ||
| import six | ||
|
|
||
| import azure.core.credentials as corecredentials | ||
| import azure.core.pipeline as corepipeline | ||
| import azure.core.pipeline.policies as corepolicies | ||
| import azure.core.pipeline.transport as coretransport | ||
|
|
||
|
|
||
| # Temporary location for types that eventually graduate to Azure Core | ||
| from .core import rest as corerest | ||
| from ._version import VERSION as _VERSION | ||
| from ._policies import JwtCredentialPolicy | ||
| from ._utils import UTC as _UTC | ||
|
|
||
| if TYPE_CHECKING: | ||
| from azure.core.pipeline.policies import HTTPPolicy, SansIOHTTPPolicy | ||
| from typing import Any, List, cast, Type, TypeVar | ||
|
|
||
| ClientType = TypeVar("ClientType", bound="WebPubSubServiceClient") | ||
|
|
||
|
|
||
| def _parse_connection_string(connection_string, **kwargs): | ||
| for segment in connection_string.split(";"): | ||
| if "=" in segment: | ||
| key, value = segment.split("=", maxsplit=1) | ||
| key = key.lower() | ||
| if key not in ("version", ): | ||
| kwargs.setdefault(key, value) | ||
| elif segment: | ||
| raise ValueError( | ||
| "Malformed connection string - expected 'key=value', found segment '{}' in '{}'".format( | ||
| segment, connection_string | ||
| ) | ||
| ) | ||
|
|
||
| if "endpoint" not in kwargs: | ||
| raise ValueError("connection_string missing 'endpoint' field") | ||
|
|
||
| if "accesskey" not in kwargs: | ||
| raise ValueError("connection_string missing 'accesskey' field") | ||
|
|
||
| return kwargs | ||
|
|
||
| def build_authentication_token(endpoint, hub, **kwargs): | ||
| """Build an authentication token for the given endpoint, hub using the provided key. | ||
| :keyword endpoint: connetion string or HTTP or HTTPS endpoint for the WebPubSub service instance. | ||
| :type endpoint: ~str | ||
| :keyword hub: The hub to give access to. | ||
| :type hub: ~str | ||
| :keyword accesskey: Key to sign the token with. Required if endpoint is not a connection string | ||
| :type accesskey: ~str | ||
| :keyword ttl: Optional ttl timedelta for the token. Default is 1 hour. | ||
msyyc marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| :type ttl: ~datetime.timedelta | ||
| :keyword user: Optional user name (subject) for the token. Default is no user. | ||
| :type user: ~str | ||
| :keyword roles: Roles for the token. | ||
| :type roles: typing.List[str]. Default is no roles. | ||
| :returns: ~dict containing the web socket endpoint, the token and a url with the generated access token. | ||
| :rtype: ~dict | ||
| Example: | ||
| >>> build_authentication_token(endpoint='https://contoso.com/api/webpubsub', hub='theHub', key='123') | ||
| { | ||
| 'baseUrl': 'wss://contoso.com/api/webpubsub/client/hubs/theHub', | ||
| 'token': 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ...', | ||
| 'url': 'wss://contoso.com/api/webpubsub/client/hubs/theHub?access_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ...' | ||
| } | ||
| """ | ||
| if 'accesskey' not in kwargs: | ||
| kwargs = _parse_connection_string(endpoint, **kwargs) | ||
| endpoint = kwargs.pop('endpoint') | ||
|
|
||
| user = kwargs.pop("user", None) | ||
| key = kwargs.pop("accesskey") | ||
| ttl = kwargs.pop("ttl", timedelta(hours=1)) | ||
| roles = kwargs.pop("roles", []) | ||
| endpoint = endpoint.lower() | ||
| if not endpoint.startswith("http://") and not endpoint.startswith("https://"): | ||
| raise ValueError( | ||
| "Invalid endpoint: '{}' has unknown scheme - expected 'http://' or 'https://'".format( | ||
| endpoint | ||
| ) | ||
| ) | ||
|
|
||
| # Ensure endpoint has no trailing slash | ||
| endpoint = endpoint.rstrip("/") | ||
|
|
||
| # Switch from http(s) to ws(s) scheme | ||
| client_endpoint = "ws" + endpoint[4:] | ||
| client_url = "{}/client/hubs/{}".format(client_endpoint, hub) | ||
| audience = "{}/client/hubs/{}".format(endpoint, hub) | ||
|
|
||
| payload = { | ||
| "aud": audience, | ||
| "iat": datetime.now(tz=_UTC), | ||
| "exp": datetime.now(tz=_UTC) + ttl, | ||
| } | ||
| if user: | ||
| payload["sub"] = user | ||
| if roles: | ||
| payload["role"] = roles | ||
|
|
||
| token = six.ensure_str(jwt.encode(payload, key, algorithm="HS256")) | ||
| return { | ||
| "baseUrl": client_url, | ||
| "token": token, | ||
| "url": "{}?access_token={}".format(client_url, token), | ||
| } | ||
|
|
||
|
|
||
| class WebPubSubServiceClient(object): | ||
| def __init__(self, endpoint, credential, **kwargs): | ||
| # type: (str, corecredentials.AzureKeyCredential, Any) -> None | ||
| """Create a new WebPubSubServiceClient instance | ||
| :param endpoint: Endpoint to connect to. | ||
| :type endpoint: ~str | ||
| :param credential: Credentials to use to connect to endpoint. | ||
| :type credential: ~azure.core.credentials.AzureKeyCredential | ||
| :keyword api_version: Api version to use when communicating with the service. | ||
| :type api_version: str | ||
| :keyword user: User to connect as. Optional. | ||
| :type user: ~str | ||
| """ | ||
| self.endpoint = endpoint.rstrip("/") | ||
| transport = kwargs.pop("transport", None) or coretransport.RequestsTransport( | ||
| **kwargs | ||
| ) | ||
| kwargs.setdefault( | ||
| "sdk_moniker", "messaging-webpubsubservice/{}".format(_VERSION) | ||
| ) | ||
| policies = [ | ||
| corepolicies.HeadersPolicy(**kwargs), | ||
| corepolicies.UserAgentPolicy(**kwargs), | ||
| corepolicies.RetryPolicy(**kwargs), | ||
| corepolicies.ProxyPolicy(**kwargs), | ||
| corepolicies.CustomHookPolicy(**kwargs), | ||
| corepolicies.RedirectPolicy(**kwargs), | ||
| JwtCredentialPolicy(credential, kwargs.get("user", None)), | ||
| corepolicies.NetworkTraceLoggingPolicy(**kwargs), | ||
| ] # type: Any | ||
| self._pipeline = corepipeline.Pipeline( | ||
| transport, | ||
| policies, | ||
| ) # type: corepipeline.Pipeline | ||
|
|
||
| @classmethod | ||
| def from_connection_string(cls, connection_string, **kwargs): | ||
| # type: (Type[ClientType], str, Any) -> ClientType | ||
| """Create a new WebPubSubServiceClient from a connection string. | ||
| :param connection_string: Connection string | ||
| :type connection_string: ~str | ||
| :rtype: WebPubSubServiceClient | ||
| """ | ||
| kwargs = _parse_connection_string(connection_string, **kwargs) | ||
|
|
||
| kwargs["credential"] = corecredentials.AzureKeyCredential( | ||
| kwargs.pop("accesskey") | ||
| ) | ||
| return cls(**kwargs) | ||
|
|
||
| def __repr__(self): | ||
| return "<WebPubSubServiceClient> endpoint:'{}'".format(self.endpoint) | ||
|
|
||
| def _format_url(self, url): | ||
| # type: (str) -> str | ||
| assert self.endpoint[-1] != "/", "My endpoint should not have a trailing slash" | ||
| return "/".join([self.endpoint, url.lstrip("/")]) | ||
|
|
||
| def send_request(self, http_request, **kwargs): | ||
| # type: (corerest.HttpRequest, Any) -> corerest.HttpResponse | ||
| """Runs the network request through the client's chained policies. | ||
| We have helper methods to create requests specific to this service in `azure.messaging.webpubsub.rest`. | ||
| Use these helper methods to create the request you pass to this method. See our example below: | ||
| >>> from azure.messaging.webpubsub.rest import build_healthapi_get_health_status_request | ||
| >>> request = build_healthapi_get_health_status_request(api_version) | ||
| <HttpRequest [HEAD], url: '/api/health'> | ||
| >>> response = client.send_request(request) | ||
| <HttpResponse: 200 OK> | ||
| For more information on this code flow, see https://aka.ms/azsdk/python/llcwiki | ||
| For advanced cases, you can also create your own :class:`~azure.messaging.webpubsub.core.rest.HttpRequest` | ||
| and pass it in. | ||
| :param http_request: The network request you want to make. Required. | ||
| :type http_request: ~azure.messaging.webpubsub.core.rest.HttpRequest | ||
| :keyword bool stream_response: Whether the response payload will be streamed. Defaults to False. | ||
| :return: The response of your network call. Does not do error handling on your response. | ||
| :rtype: ~azure.messaging.webpubsub.core.rest.HttpResponse | ||
| """ | ||
| request_copy = deepcopy(http_request) | ||
| request_copy.url = self._format_url(request_copy.url) | ||
|
|
||
| # can't do StreamCOntextManager yet. This client doesn't have a pipeline client, | ||
| # StreamContextManager requires a pipeline client. WIll look more into it | ||
| # if kwargs.pop("stream_response", False): | ||
| # return corerest._StreamContextManager( | ||
| # client=self._client, | ||
| # request=request_copy, | ||
| # ) | ||
| pipeline_response = self._pipeline.run(request_copy._internal_request, **kwargs) # pylint: disable=protected-access | ||
| response = corerest.HttpResponse( | ||
| status_code=pipeline_response.http_response.status_code, | ||
| request=request_copy, | ||
| _internal_response=pipeline_response.http_response, | ||
| ) | ||
| response.read() | ||
| return response | ||
| try: | ||
| from ._patch import patch_sdk # type: ignore | ||
| patch_sdk() | ||
| except ImportError: | ||
| pass | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.