From e666f57a3e7a3f8859b197dc554ae6c848073e41 Mon Sep 17 00:00:00 2001 From: Kieran Brantner-Magee Date: Tue, 8 Sep 2020 00:27:37 -0700 Subject: [PATCH 1/8] - Remove public documentation and exports of ServiceBusSharedKeyCredential until we chose to release it across all languages. - Support for Sas Token connection strings (tests, etc) --- sdk/servicebus/azure-servicebus/CHANGELOG.md | 3 + .../azure/servicebus/__init__.py | 2 - .../azure/servicebus/_base_handler.py | 99 ++++++++++++++----- .../azure/servicebus/_common/utils.py | 22 ++++- .../azure/servicebus/_servicebus_client.py | 18 +++- .../azure/servicebus/_servicebus_receiver.py | 9 +- .../azure/servicebus/_servicebus_sender.py | 9 +- .../_servicebus_session_receiver.py | 4 +- .../azure/servicebus/aio/__init__.py | 2 - .../servicebus/aio/_base_handler_async.py | 33 ++++++- .../aio/_servicebus_client_async.py | 14 ++- .../aio/_servicebus_receiver_async.py | 8 +- .../aio/_servicebus_sender_async.py | 8 +- .../aio/_servicebus_session_receiver_async.py | 4 +- .../management/_management_client_async.py | 14 ++- .../management/_management_client.py | 14 ++- .../mgmt_tests/test_mgmt_queues_async.py | 2 +- .../tests/async_tests/test_sb_client_async.py | 30 ++++++ .../async_tests/test_subscriptions_async.py | 3 +- .../tests/async_tests/test_topic_async.py | 3 +- .../tests/mgmt_tests/test_mgmt_queues.py | 2 +- .../azure-servicebus/tests/test_sb_client.py | 30 +++++- .../tests/test_subscriptions.py | 3 +- .../azure-servicebus/tests/test_topic.py | 3 +- 24 files changed, 257 insertions(+), 82 deletions(-) diff --git a/sdk/servicebus/azure-servicebus/CHANGELOG.md b/sdk/servicebus/azure-servicebus/CHANGELOG.md index f1b832deb546..107d5d70cdf5 100644 --- a/sdk/servicebus/azure-servicebus/CHANGELOG.md +++ b/sdk/servicebus/azure-servicebus/CHANGELOG.md @@ -4,6 +4,8 @@ **New Features** * Messages can now be sent twice in succession. +* Connection strings now support +* Connection strings used with `from_connection_string` methods now support using the `SharedAccessSignature` key in leiu of `sharedaccesskey` and `sharedaccesskeyname`, taking the string of the properly constructed token as value. **Breaking Changes** @@ -11,6 +13,7 @@ * Sending a message twice will no longer result in a MessageAlreadySettled exception. * `ServiceBusClient.close()` now closes spawned senders and receivers. * Attempting to initialize a sender or receiver with a different connection string entity and specified entity (e.g. `queue_name`) will result in an AuthenticationError +* No longer export `ServiceBusSharedKeyCredential` ## 7.0.0b5 (2020-08-10) diff --git a/sdk/servicebus/azure-servicebus/azure/servicebus/__init__.py b/sdk/servicebus/azure-servicebus/azure/servicebus/__init__.py index 826e54f3e087..4da2ce7da233 100644 --- a/sdk/servicebus/azure-servicebus/azure/servicebus/__init__.py +++ b/sdk/servicebus/azure-servicebus/azure/servicebus/__init__.py @@ -13,7 +13,6 @@ from ._servicebus_receiver import ServiceBusReceiver from ._servicebus_session_receiver import ServiceBusSessionReceiver from ._servicebus_session import ServiceBusSession -from ._base_handler import ServiceBusSharedKeyCredential from ._common.message import Message, BatchMessage, PeekMessage, ReceivedMessage from ._common.constants import ReceiveSettleMode, NEXT_AVAILABLE from ._common.auto_lock_renewer import AutoLockRenew @@ -32,7 +31,6 @@ 'ServiceBusSessionReceiver', 'ServiceBusSession', 'ServiceBusSender', - 'ServiceBusSharedKeyCredential', 'TransportType', 'AutoLockRenew' ] diff --git a/sdk/servicebus/azure-servicebus/azure/servicebus/_base_handler.py b/sdk/servicebus/azure-servicebus/azure/servicebus/_base_handler.py index 80b181d116a9..97d82e0547c6 100644 --- a/sdk/servicebus/azure-servicebus/azure/servicebus/_base_handler.py +++ b/sdk/servicebus/azure-servicebus/azure/servicebus/_base_handler.py @@ -33,6 +33,7 @@ ASSOCIATEDLINKPROPERTYNAME ) +from azure.core.credentials import AccessToken if TYPE_CHECKING: from azure.core.credentials import TokenCredential @@ -46,6 +47,8 @@ def _parse_conn_str(conn_str): shared_access_key_name = None shared_access_key = None entity_path = None # type: Optional[str] + shared_access_signature = None # type: Optional[str] + shared_access_signature_expiry = None # type: Optional[int] for element in conn_str.split(";"): key, _, value = element.partition("=") if key.lower() == "endpoint": @@ -58,7 +61,16 @@ def _parse_conn_str(conn_str): shared_access_key = value elif key.lower() == "entitypath": entity_path = value - if not all([endpoint, shared_access_key_name, shared_access_key]): + elif key.lower() == "sharedaccesssignature": + shared_access_signature = value + try: + # Expiry can be stored in the "se=" clause of the token. ('&'-separated key-value pairs) + # type: ignore + shared_access_signature_expiry = int(shared_access_signature.split('se=')[1].split('&')[0]) + except (IndexError, TypeError, ValueError): # Fallback since technically expiry is optional. + # An arbitrary, absurdly large number, since you can't renew. + shared_access_signature_expiry = int(time.time() * 2) + if not (all((endpoint, shared_access_key_name, shared_access_key)) or all((endpoint, shared_access_signature))): raise ValueError( "Invalid connection string. Should be in the format: " "Endpoint=sb:///;SharedAccessKeyName=;SharedAccessKey=" @@ -69,7 +81,13 @@ def _parse_conn_str(conn_str): host = cast(str, endpoint)[left_slash_pos + 2:] else: host = str(endpoint) - return host, str(shared_access_key_name), str(shared_access_key), entity + + return (host, + str(shared_access_key_name) if shared_access_key_name else None, + str(shared_access_key) if shared_access_key else None, + entity, + str(shared_access_signature) if shared_access_signature else None, + shared_access_signature_expiry) def _generate_sas_token(uri, policy, key, expiry=None): @@ -90,29 +108,27 @@ def _generate_sas_token(uri, policy, key, expiry=None): return _AccessToken(token=token, expires_on=abs_expiry) -def _convert_connection_string_to_kwargs(conn_str, shared_key_credential_type, **kwargs): - # type: (str, Type, Any) -> Dict[str, Any] - host, policy, key, entity_in_conn_str = _parse_conn_str(conn_str) - queue_name = kwargs.get("queue_name") - topic_name = kwargs.get("topic_name") - if not (queue_name or topic_name or entity_in_conn_str): - raise ValueError("Entity name is missing. Please specify `queue_name` or `topic_name`" - " or use a connection string including the entity information.") - - if queue_name and topic_name: - raise ValueError("`queue_name` and `topic_name` can not be specified simultaneously.") - - entity_in_kwargs = queue_name or topic_name - if entity_in_conn_str and entity_in_kwargs and (entity_in_conn_str != entity_in_kwargs): - raise ServiceBusAuthenticationError( - "Entity names do not match, the entity name in connection string is {};" - " the entity name in parameter is {}.".format(entity_in_conn_str, entity_in_kwargs) - ) +class ServiceBusSASTokenCredential(object): + """The shared access token credential used for authentication. + :param str token: The shared access token string + :param int expiry: The epoch timestamp + """ + def __init__(self, token, expiry): + # type: (str, int) -> None + """ + :param str token: The shared access token string + :param float expiry: The epoch timestamp + """ + self.token = token + self.expiry = expiry + self.token_type = b"servicebus.windows.net:sastoken" - kwargs["fully_qualified_namespace"] = host - kwargs["entity_name"] = entity_in_conn_str or entity_in_kwargs - kwargs["credential"] = shared_key_credential_type(policy, key) - return kwargs + def get_token(self, *scopes, **kwargs): # pylint:disable=unused-argument + # type: (str, Any) -> AccessToken + """ + This method is automatically called when token is about to expire. + """ + return AccessToken(self.token, self.expiry) class ServiceBusSharedKeyCredential(object): @@ -158,6 +174,41 @@ def __init__( self._auth_uri = None self._properties = create_properties(self._config.user_agent) + + def _convert_connection_string_to_kwargs(self, conn_str, **kwargs): + # type: (str, Type, Any) -> Dict[str, Any] + host, policy, key, entity_in_conn_str, token, token_expiry = _parse_conn_str(conn_str) + queue_name = kwargs.get("queue_name") + topic_name = kwargs.get("topic_name") + if not (queue_name or topic_name or entity_in_conn_str): + raise ValueError("Entity name is missing. Please specify `queue_name` or `topic_name`" + " or use a connection string including the entity information.") + + if queue_name and topic_name: + raise ValueError("`queue_name` and `topic_name` can not be specified simultaneously.") + + entity_in_kwargs = queue_name or topic_name + if entity_in_conn_str and entity_in_kwargs and (entity_in_conn_str != entity_in_kwargs): + raise ServiceBusAuthenticationError( + "Entity names do not match, the entity name in connection string is {};" + " the entity name in parameter is {}.".format(entity_in_conn_str, entity_in_kwargs) + ) + + kwargs["fully_qualified_namespace"] = host + kwargs["entity_name"] = entity_in_conn_str or entity_in_kwargs + # This has to be defined seperately to support sync vs async credentials. + kwargs["credential"] = self._create_credential_from_connection_string_parameters(token, + token_expiry, + policy, + key) + return kwargs + + def _create_credential_from_connection_string_parameters(self, token, token_expiry, policy, key): + if token and token_expiry: + return ServiceBusSASTokenCredential(token, token_expiry) + elif policy and key: + return ServiceBusSharedKeyCredential(policy, key) + def __enter__(self): self._open_with_retry() return self diff --git a/sdk/servicebus/azure-servicebus/azure/servicebus/_common/utils.py b/sdk/servicebus/azure-servicebus/azure/servicebus/_common/utils.py index 65241798087b..8d8c0f2ea633 100644 --- a/sdk/servicebus/azure-servicebus/azure/servicebus/_common/utils.py +++ b/sdk/servicebus/azure-servicebus/azure/servicebus/_common/utils.py @@ -63,11 +63,15 @@ def utc_now(): return datetime.datetime.now(tz=TZ_UTC) +# This parse_conn_str is used for mgmt, the other in base_handler for handlers. Should be unified. def parse_conn_str(conn_str): + # type: (str) -> Tuple[str, Optional[str], Optional[str], str, Optional[str], Optional[int]] endpoint = None shared_access_key_name = None shared_access_key = None entity_path = None + shared_access_signature = None # type: Optional[str] + shared_access_signature_expiry = None # type: Optional[int] for element in conn_str.split(';'): key, _, value = element.partition('=') if key.lower() == 'endpoint': @@ -78,9 +82,23 @@ def parse_conn_str(conn_str): shared_access_key = value elif key.lower() == 'entitypath': entity_path = value - if not all([endpoint, shared_access_key_name, shared_access_key]): + elif key.lower() == "sharedaccesssignature": + shared_access_signature = value + try: + # Expiry can be stored in the "se=" clause of the token. ('&'-separated key-value pairs) + # type: ignore + shared_access_signature_expiry = int(shared_access_signature.split('se=')[1].split('&')[0]) + except (IndexError, TypeError, ValueError): # Fallback since technically expiry is optional. + # An arbitrary, absurdly large number, since you can't renew. + shared_access_signature_expiry = int(time.time() * 2) + if not (all((endpoint, shared_access_key_name, shared_access_key)) or all((endpoint, shared_access_signature))): raise ValueError("Invalid connection string") - return endpoint, shared_access_key_name, shared_access_key, entity_path + return (endpoint, + str(shared_access_key_name) if shared_access_key_name else None, + str(shared_access_key) if shared_access_key else None, + entity_path, + str(shared_access_signature) if shared_access_signature else None, + shared_access_signature_expiry) def build_uri(address, entity): diff --git a/sdk/servicebus/azure-servicebus/azure/servicebus/_servicebus_client.py b/sdk/servicebus/azure-servicebus/azure/servicebus/_servicebus_client.py index be27075ea2de..7ff7e271e8f7 100644 --- a/sdk/servicebus/azure-servicebus/azure/servicebus/_servicebus_client.py +++ b/sdk/servicebus/azure-servicebus/azure/servicebus/_servicebus_client.py @@ -7,7 +7,11 @@ import uamqp -from ._base_handler import _parse_conn_str, ServiceBusSharedKeyCredential, BaseHandler +from ._base_handler import ( + _parse_conn_str, + ServiceBusSharedKeyCredential, + ServiceBusSASTokenCredential, + BaseHandler) from ._servicebus_sender import ServiceBusSender from ._servicebus_receiver import ServiceBusReceiver from ._servicebus_session_receiver import ServiceBusSessionReceiver @@ -32,8 +36,8 @@ class ServiceBusClient(object): The namespace format is: `.servicebus.windows.net`. :param ~azure.core.credentials.TokenCredential credential: The credential object used for authentication which implements a particular interface for getting tokens. It accepts - :class:`ServiceBusSharedKeyCredential`, or credential objects - generated by the azure-identity library and objects that implement the `get_token(self, *scopes)` method. + :class: credential objects generated by the azure-identity library and objects that implement the + `get_token(self, *scopes)` method. :keyword str entity_name: Optional entity name, this can be the name of Queue or Topic. It must be specified if the credential is for specific Queue or Topic. :keyword bool logging_enable: Whether to output network trace logs to the logger. Default is `False`. @@ -145,11 +149,15 @@ def from_connection_string( :caption: Create a new instance of the ServiceBusClient from connection string. """ - host, policy, key, entity_in_conn_str = _parse_conn_str(conn_str) + host, policy, key, entity_in_conn_str, token, token_expiry = _parse_conn_str(conn_str) + if token and token_expiry: + credential = ServiceBusSASTokenCredential(token, token_expiry) + elif policy and key: + credential = ServiceBusSharedKeyCredential(policy, key) return cls( fully_qualified_namespace=host, entity_name=entity_in_conn_str or kwargs.pop("entity_name", None), - credential=ServiceBusSharedKeyCredential(policy, key), # type: ignore + credential=credential, # type: ignore **kwargs ) diff --git a/sdk/servicebus/azure-servicebus/azure/servicebus/_servicebus_receiver.py b/sdk/servicebus/azure-servicebus/azure/servicebus/_servicebus_receiver.py index 883a291581c3..5551c998be73 100644 --- a/sdk/servicebus/azure-servicebus/azure/servicebus/_servicebus_receiver.py +++ b/sdk/servicebus/azure-servicebus/azure/servicebus/_servicebus_receiver.py @@ -11,7 +11,7 @@ from uamqp.constants import SenderSettleMode from uamqp.authentication.common import AMQPAuth -from ._base_handler import BaseHandler, ServiceBusSharedKeyCredential, _convert_connection_string_to_kwargs +from ._base_handler import BaseHandler, ServiceBusSharedKeyCredential from ._common.utils import create_authentication from ._common.message import PeekMessage, ReceivedMessage from ._common.constants import ( @@ -54,8 +54,8 @@ class ServiceBusReceiver(BaseHandler, ReceiverMixin): # pylint: disable=too-man The namespace format is: `.servicebus.windows.net`. :param ~azure.core.credentials.TokenCredential credential: The credential object used for authentication which implements a particular interface for getting tokens. It accepts - :class:`ServiceBusSharedKeyCredential`, or credential objects - generated by the azure-identity library and objects that implement the `get_token(self, *scopes)` method. + :class: credential objects generated by the azure-identity library and objects that implement the + `get_token(self, *scopes)` method. :keyword str queue_name: The path of specific Service Bus Queue the client connects to. :keyword str topic_name: The path of specific Service Bus Topic which contains the Subscription the client connects to. @@ -365,9 +365,8 @@ def from_connection_string( :caption: Create a new instance of the ServiceBusReceiver from connection string. """ - constructor_args = _convert_connection_string_to_kwargs( + constructor_args = self._convert_connection_string_to_kwargs( conn_str, - ServiceBusSharedKeyCredential, **kwargs ) if kwargs.get("queue_name") and kwargs.get("subscription_name"): diff --git a/sdk/servicebus/azure-servicebus/azure/servicebus/_servicebus_sender.py b/sdk/servicebus/azure-servicebus/azure/servicebus/_servicebus_sender.py index a1c20ba6f942..102edba6ea4f 100644 --- a/sdk/servicebus/azure-servicebus/azure/servicebus/_servicebus_sender.py +++ b/sdk/servicebus/azure-servicebus/azure/servicebus/_servicebus_sender.py @@ -11,7 +11,7 @@ from uamqp import SendClient, types from uamqp.authentication.common import AMQPAuth -from ._base_handler import BaseHandler, ServiceBusSharedKeyCredential, _convert_connection_string_to_kwargs +from ._base_handler import BaseHandler, ServiceBusSharedKeyCredential from ._common import mgmt_handlers from ._common.message import Message, BatchMessage from .exceptions import ( @@ -97,8 +97,8 @@ class ServiceBusSender(BaseHandler, SenderMixin): The namespace format is: `.servicebus.windows.net`. :param ~azure.core.credentials.TokenCredential credential: The credential object used for authentication which implements a particular interface for getting tokens. It accepts - :class:`ServiceBusSharedKeyCredential`, or credential objects - generated by the azure-identity library and objects that implement the `get_token(self, *scopes)` method. + :class: credential objects generated by the azure-identity library and objects that implement the + `get_token(self, *scopes)` method. :keyword str queue_name: The path of specific Service Bus Queue the client connects to. :keyword str topic_name: The path of specific Service Bus Topic the client connects to. :keyword bool logging_enable: Whether to output network trace logs to the logger. Default is `False`. @@ -297,9 +297,8 @@ def from_connection_string( :caption: Create a new instance of the ServiceBusSender from connection string. """ - constructor_args = _convert_connection_string_to_kwargs( + constructor_args = self._convert_connection_string_to_kwargs( conn_str, - ServiceBusSharedKeyCredential, **kwargs ) return cls(**constructor_args) diff --git a/sdk/servicebus/azure-servicebus/azure/servicebus/_servicebus_session_receiver.py b/sdk/servicebus/azure-servicebus/azure/servicebus/_servicebus_session_receiver.py index c6a6bcc33d35..54c4756f593d 100644 --- a/sdk/servicebus/azure-servicebus/azure/servicebus/_servicebus_session_receiver.py +++ b/sdk/servicebus/azure-servicebus/azure/servicebus/_servicebus_session_receiver.py @@ -33,8 +33,8 @@ class ServiceBusSessionReceiver(ServiceBusReceiver, SessionReceiverMixin): The namespace format is: `.servicebus.windows.net`. :param ~azure.core.credentials.TokenCredential credential: The credential object used for authentication which implements a particular interface for getting tokens. It accepts - :class:`ServiceBusSharedKeyCredential`, or credential objects - generated by the azure-identity library and objects that implement the `get_token(self, *scopes)` method. + :class: credential objects generated by the azure-identity library and objects that implement the + `get_token(self, *scopes)` method. :keyword str queue_name: The path of specific Service Bus Queue the client connects to. :keyword str topic_name: The path of specific Service Bus Topic which contains the Subscription the client connects to. diff --git a/sdk/servicebus/azure-servicebus/azure/servicebus/aio/__init__.py b/sdk/servicebus/azure-servicebus/azure/servicebus/aio/__init__.py index 4e13bf070d49..0cf6aa9cdc25 100644 --- a/sdk/servicebus/azure-servicebus/azure/servicebus/aio/__init__.py +++ b/sdk/servicebus/azure-servicebus/azure/servicebus/aio/__init__.py @@ -4,7 +4,6 @@ # license information. # ------------------------------------------------------------------------- from ._async_message import ReceivedMessage -from ._base_handler_async import ServiceBusSharedKeyCredential from ._servicebus_sender_async import ServiceBusSender from ._servicebus_receiver_async import ServiceBusReceiver from ._servicebus_session_receiver_async import ServiceBusSessionReceiver @@ -19,6 +18,5 @@ 'ServiceBusReceiver', 'ServiceBusSessionReceiver', 'ServiceBusSession', - 'ServiceBusSharedKeyCredential', 'AutoLockRenew' ] diff --git a/sdk/servicebus/azure-servicebus/azure/servicebus/aio/_base_handler_async.py b/sdk/servicebus/azure-servicebus/azure/servicebus/aio/_base_handler_async.py index 125574319d79..cab977713df6 100644 --- a/sdk/servicebus/azure-servicebus/azure/servicebus/aio/_base_handler_async.py +++ b/sdk/servicebus/azure-servicebus/azure/servicebus/aio/_base_handler_async.py @@ -22,12 +22,34 @@ _create_servicebus_exception ) +from azure.core.credentials import AccessToken if TYPE_CHECKING: - from azure.core.credentials import TokenCredential, AccessToken + from azure.core.credentials import TokenCredential _LOGGER = logging.getLogger(__name__) +class ServiceBusSASTokenCredential(object): + """The shared access token credential used for authentication. + :param str token: The shared access token string + :param int expiry: The epoch timestamp + """ + def __init__(self, token: str, expiry: int) -> None: + """ + :param str token: The shared access token string + :param int expiry: The epoch timestamp + """ + self.token = token + self.expiry = expiry + self.token_type = b"servicebus.windows.net:sastoken" + + async def get_token(self, *scopes: str, **kwargs: Any) -> AccessToken: # pylint:disable=unused-argument + """ + This method is automatically called when token is about to expire. + """ + return AccessToken(self.token, self.expiry) + + class ServiceBusSharedKeyCredential(object): """The shared access key credential used for authentication. @@ -69,6 +91,15 @@ def __init__( self._auth_uri = None self._properties = create_properties(self._config.user_agent) + def _convert_connection_string_to_kwargs(self, conn_str, **kwargs): + return BaseHandlerSync._convert_connection_string_to_kwargs(self, conn_str, kwargs) + + def _create_credential_from_connection_string_parameters(self, token, token_expiry, policy, key): + if token and token_expiry: + return ServiceBusSASTokenCredential(token, token_expiry) + elif policy and key: + return ServiceBusSharedKeyCredential(policy, key) + async def __aenter__(self): await self._open_with_retry() return self diff --git a/sdk/servicebus/azure-servicebus/azure/servicebus/aio/_servicebus_client_async.py b/sdk/servicebus/azure-servicebus/azure/servicebus/aio/_servicebus_client_async.py index a6827a8ae91a..fe1ec7f0360e 100644 --- a/sdk/servicebus/azure-servicebus/azure/servicebus/aio/_servicebus_client_async.py +++ b/sdk/servicebus/azure-servicebus/azure/servicebus/aio/_servicebus_client_async.py @@ -8,7 +8,7 @@ import uamqp from .._base_handler import _parse_conn_str -from ._base_handler_async import ServiceBusSharedKeyCredential, BaseHandler +from ._base_handler_async import ServiceBusSharedKeyCredential, ServiceBusSASTokenCredential, BaseHandler from ._servicebus_sender_async import ServiceBusSender from ._servicebus_receiver_async import ServiceBusReceiver from ._servicebus_session_receiver_async import ServiceBusSessionReceiver @@ -34,8 +34,8 @@ class ServiceBusClient(object): The namespace format is: `.servicebus.windows.net`. :param ~azure.core.credentials.TokenCredential credential: The credential object used for authentication which implements a particular interface for getting tokens. It accepts - :class:`ServiceBusSharedKeyCredential`, or credential objects - generated by the azure-identity library and objects that implement the `get_token(self, *scopes)` method. + :class: credential objects generated by the azure-identity library and objects that implement the + `get_token(self, *scopes)` method. :keyword str entity_name: Optional entity name, this can be the name of Queue or Topic. It must be specified if the credential is for specific Queue or Topic. :keyword bool logging_enable: Whether to output network trace logs to the logger. Default is `False`. @@ -125,11 +125,15 @@ def from_connection_string( :caption: Create a new instance of the ServiceBusClient from connection string. """ - host, policy, key, entity_in_conn_str = _parse_conn_str(conn_str) + host, policy, key, entity_in_conn_str, token, token_expiry = _parse_conn_str(conn_str) + if token and token_expiry: + credential = ServiceBusSASTokenCredential(token, token_expiry) + elif policy and key: + credential = ServiceBusSharedKeyCredential(policy, key) return cls( fully_qualified_namespace=host, entity_name=entity_in_conn_str or kwargs.pop("entity_name", None), - credential=ServiceBusSharedKeyCredential(policy, key), # type: ignore + credential=credential, # type: ignore **kwargs ) diff --git a/sdk/servicebus/azure-servicebus/azure/servicebus/aio/_servicebus_receiver_async.py b/sdk/servicebus/azure-servicebus/azure/servicebus/aio/_servicebus_receiver_async.py index da6c4b6e72e5..21a11a7ea1b5 100644 --- a/sdk/servicebus/azure-servicebus/azure/servicebus/aio/_servicebus_receiver_async.py +++ b/sdk/servicebus/azure-servicebus/azure/servicebus/aio/_servicebus_receiver_async.py @@ -13,7 +13,6 @@ from ._base_handler_async import BaseHandler, ServiceBusSharedKeyCredential from ._async_message import ReceivedMessage -from .._base_handler import _convert_connection_string_to_kwargs from .._common.receiver_mixins import ReceiverMixin from .._common.constants import ( REQUEST_RESPONSE_UPDATE_DISPOSTION_OPERATION, @@ -54,8 +53,8 @@ class ServiceBusReceiver(collections.abc.AsyncIterator, BaseHandler, ReceiverMix The namespace format is: `.servicebus.windows.net`. :param ~azure.core.credentials.TokenCredential credential: The credential object used for authentication which implements a particular interface for getting tokens. It accepts - :class:`ServiceBusSharedKeyCredential`, or credential objects - generated by the azure-identity library and objects that implement the `get_token(self, *scopes)` method. + :class: credential objects generated by the azure-identity library and objects that implement the + `get_token(self, *scopes)` method. :keyword str queue_name: The path of specific Service Bus Queue the client connects to. :keyword str topic_name: The path of specific Service Bus Topic which contains the Subscription the client connects to. @@ -360,9 +359,8 @@ def from_connection_string( :caption: Create a new instance of the ServiceBusReceiver from connection string. """ - constructor_args = _convert_connection_string_to_kwargs( + constructor_args = self._convert_connection_string_to_kwargs( conn_str, - ServiceBusSharedKeyCredential, **kwargs ) if kwargs.get("queue_name") and kwargs.get("subscription_name"): diff --git a/sdk/servicebus/azure-servicebus/azure/servicebus/aio/_servicebus_sender_async.py b/sdk/servicebus/azure-servicebus/azure/servicebus/aio/_servicebus_sender_async.py index 347ab457e57c..9f7bf6a5d060 100644 --- a/sdk/servicebus/azure-servicebus/azure/servicebus/aio/_servicebus_sender_async.py +++ b/sdk/servicebus/azure-servicebus/azure/servicebus/aio/_servicebus_sender_async.py @@ -10,7 +10,6 @@ from uamqp import SendClientAsync, types from .._common.message import Message, BatchMessage -from .._base_handler import _convert_connection_string_to_kwargs from .._servicebus_sender import SenderMixin from ._base_handler_async import BaseHandler, ServiceBusSharedKeyCredential from .._common.constants import ( @@ -43,8 +42,8 @@ class ServiceBusSender(BaseHandler, SenderMixin): The namespace format is: `.servicebus.windows.net`. :param ~azure.core.credentials.TokenCredential credential: The credential object used for authentication which implements a particular interface for getting tokens. It accepts - :class:`ServiceBusSharedKeyCredential`, or credential objects - generated by the azure-identity library and objects that implement the `get_token(self, *scopes)` method. + :class: credential objects generated by the azure-identity library and objects that implement the + `get_token(self, *scopes)` method. :keyword str queue_name: The path of specific Service Bus Queue the client connects to. Only one of queue_name or topic_name can be provided. :keyword str topic_name: The path of specific Service Bus Topic the client connects to. @@ -236,9 +235,8 @@ def from_connection_string( :caption: Create a new instance of the ServiceBusSender from connection string. """ - constructor_args = _convert_connection_string_to_kwargs( + constructor_args = self._convert_connection_string_to_kwargs( conn_str, - ServiceBusSharedKeyCredential, **kwargs ) return cls(**constructor_args) diff --git a/sdk/servicebus/azure-servicebus/azure/servicebus/aio/_servicebus_session_receiver_async.py b/sdk/servicebus/azure-servicebus/azure/servicebus/aio/_servicebus_session_receiver_async.py index 10abb88b5bf0..7e5964fce494 100644 --- a/sdk/servicebus/azure-servicebus/azure/servicebus/aio/_servicebus_session_receiver_async.py +++ b/sdk/servicebus/azure-servicebus/azure/servicebus/aio/_servicebus_session_receiver_async.py @@ -33,8 +33,8 @@ class ServiceBusSessionReceiver(ServiceBusReceiver, SessionReceiverMixin): The namespace format is: `.servicebus.windows.net`. :param ~azure.core.credentials.TokenCredential credential: The credential object used for authentication which implements a particular interface for getting tokens. It accepts - :class:`ServiceBusSharedKeyCredential`, or credential objects - generated by the azure-identity library and objects that implement the `get_token(self, *scopes)` method. + :class: credential objects generated by the azure-identity library and objects that implement the + `get_token(self, *scopes)` method. :keyword str queue_name: The path of specific Service Bus Queue the client connects to. :keyword str topic_name: The path of specific Service Bus Topic which contains the Subscription the client connects to. diff --git a/sdk/servicebus/azure-servicebus/azure/servicebus/aio/management/_management_client_async.py b/sdk/servicebus/azure-servicebus/azure/servicebus/aio/management/_management_client_async.py index 33aba6288654..843297dc9b89 100644 --- a/sdk/servicebus/azure-servicebus/azure/servicebus/aio/management/_management_client_async.py +++ b/sdk/servicebus/azure-servicebus/azure/servicebus/aio/management/_management_client_async.py @@ -25,7 +25,7 @@ from ..._common.utils import parse_conn_str from ..._common.constants import JWT_TOKEN_SCOPE -from ...aio._base_handler_async import ServiceBusSharedKeyCredential +from ...aio._base_handler_async import ServiceBusSharedKeyCredential, ServiceBusSASTokenCredential from ...management._generated.aio._configuration_async import ServiceBusManagementClientConfiguration from ...management._generated.aio._service_bus_management_client_async import ServiceBusManagementClient \ as ServiceBusManagementClientImpl @@ -49,12 +49,12 @@ class ServiceBusManagementClient: #pylint:disable=too-many-public-methods :param str fully_qualified_namespace: The fully qualified host name for the Service Bus namespace. :param credential: To authenticate to manage the entities of the ServiceBus namespace. - :type credential: Union[AsyncTokenCredential, ~azure.servicebus.aio.ServiceBusSharedKeyCredential] + :type credential: AsyncTokenCredential """ def __init__( self, fully_qualified_namespace: str, - credential: Union["AsyncTokenCredential", ServiceBusSharedKeyCredential], + credential: Union["AsyncTokenCredential"], **kwargs) -> None: self.fully_qualified_namespace = fully_qualified_namespace @@ -136,10 +136,14 @@ def from_connection_string(cls, conn_str: str, **kwargs: Any) -> "ServiceBusMana :param str conn_str: The connection string of the Service Bus Namespace. :rtype: ~azure.servicebus.management.aio.ServiceBusManagementClient """ - endpoint, shared_access_key_name, shared_access_key, _ = parse_conn_str(conn_str) + endpoint, shared_access_key_name, shared_access_key, token, token_expiry, _ = parse_conn_str(conn_str) + if token and token_expiry: + credential = ServiceBusSASTokenCredential(token, token_expiry) + elif shared_access_key_name and shared_access_key: + credential = ServiceBusSharedKeyCredential(shared_access_key_name, shared_access_key) if "//" in endpoint: endpoint = endpoint[endpoint.index("//")+2:] - return cls(endpoint, ServiceBusSharedKeyCredential(shared_access_key_name, shared_access_key), **kwargs) + return cls(endpoint, credential, **kwargs) async def get_queue(self, queue_name: str, **kwargs) -> QueueProperties: """Get the properties of a queue. diff --git a/sdk/servicebus/azure-servicebus/azure/servicebus/management/_management_client.py b/sdk/servicebus/azure-servicebus/azure/servicebus/management/_management_client.py index 9c326af09dd6..efe0c9840bef 100644 --- a/sdk/servicebus/azure-servicebus/azure/servicebus/management/_management_client.py +++ b/sdk/servicebus/azure-servicebus/azure/servicebus/management/_management_client.py @@ -27,7 +27,7 @@ from .._common.constants import JWT_TOKEN_SCOPE from .._common.utils import parse_conn_str -from .._base_handler import ServiceBusSharedKeyCredential +from .._base_handler import ServiceBusSharedKeyCredential, ServiceBusSASTokenCredential from ._shared_key_policy import ServiceBusSharedKeyCredentialPolicy from ._generated._configuration import ServiceBusManagementClientConfiguration from ._generated._service_bus_management_client import ServiceBusManagementClient as ServiceBusManagementClientImpl @@ -46,11 +46,11 @@ class ServiceBusManagementClient: # pylint:disable=too-many-public-methods :param str fully_qualified_namespace: The fully qualified host name for the Service Bus namespace. :param credential: To authenticate to manage the entities of the ServiceBus namespace. - :type credential: Union[TokenCredential, azure.servicebus.ServiceBusSharedKeyCredential] + :type credential: TokenCredential """ def __init__(self, fully_qualified_namespace, credential, **kwargs): - # type: (str, Union[TokenCredential, ServiceBusSharedKeyCredential], Dict[str, Any]) -> None + # type: (str, TokenCredential, Dict[str, Any]) -> None self.fully_qualified_namespace = fully_qualified_namespace self._credential = credential self._endpoint = "https://" + fully_qualified_namespace @@ -130,10 +130,14 @@ def from_connection_string(cls, conn_str, **kwargs): :param str conn_str: The connection string of the Service Bus Namespace. :rtype: ~azure.servicebus.management.ServiceBusManagementClient """ - endpoint, shared_access_key_name, shared_access_key, _ = parse_conn_str(conn_str) + endpoint, shared_access_key_name, shared_access_key, token, token_expiry, _ = parse_conn_str(conn_str) + if token and token_expiry: + credential = ServiceBusSASTokenCredential(token, token_expiry) + elif shared_access_key_name and shared_access_key: + credential = ServiceBusSharedKeyCredential(shared_access_key_name, shared_access_key) if "//" in endpoint: endpoint = endpoint[endpoint.index("//") + 2:] - return cls(endpoint, ServiceBusSharedKeyCredential(shared_access_key_name, shared_access_key), **kwargs) + return cls(endpoint, credential, **kwargs) def get_queue(self, queue_name, **kwargs): # type: (str, Any) -> QueueProperties diff --git a/sdk/servicebus/azure-servicebus/tests/async_tests/mgmt_tests/test_mgmt_queues_async.py b/sdk/servicebus/azure-servicebus/tests/async_tests/mgmt_tests/test_mgmt_queues_async.py index c5cff6fa891d..5aa49954d955 100644 --- a/sdk/servicebus/azure-servicebus/tests/async_tests/mgmt_tests/test_mgmt_queues_async.py +++ b/sdk/servicebus/azure-servicebus/tests/async_tests/mgmt_tests/test_mgmt_queues_async.py @@ -10,7 +10,7 @@ from azure.core.exceptions import HttpResponseError, ResourceNotFoundError, ResourceExistsError from azure.servicebus.aio.management import ServiceBusManagementClient from azure.servicebus.management import QueueProperties -from azure.servicebus.aio import ServiceBusSharedKeyCredential +from azure.servicebus.aio._base_handler_async import ServiceBusSharedKeyCredential from azure.servicebus._common.utils import utc_now from devtools_testutils import AzureMgmtTestCase, CachedResourceGroupPreparer diff --git a/sdk/servicebus/azure-servicebus/tests/async_tests/test_sb_client_async.py b/sdk/servicebus/azure-servicebus/tests/async_tests/test_sb_client_async.py index 351d1fe51a99..5a22118905e7 100644 --- a/sdk/servicebus/azure-servicebus/tests/async_tests/test_sb_client_async.py +++ b/sdk/servicebus/azure-servicebus/tests/async_tests/test_sb_client_async.py @@ -9,6 +9,8 @@ import pytest from azure.servicebus.aio import ServiceBusClient +from azure.servicebus import Message +from azure.servicebus.aio._base_handler_async import ServiceBusSharedKeyCredential from devtools_testutils import AzureMgmtTestCase, CachedResourceGroupPreparer from servicebus_preparer import CachedServiceBusNamespacePreparer, CachedServiceBusQueuePreparer from utilities import get_logger @@ -58,3 +60,31 @@ async def test_async_sb_client_close_spawned_handlers(self, servicebus_namespace assert not sender._handler and not sender._running assert not receiver._handler and not receiver._running assert len(client._handlers) == 0 + + + @pytest.mark.liveTest + @pytest.mark.live_test_only + @CachedResourceGroupPreparer() + @CachedServiceBusNamespacePreparer(name_prefix='servicebustest') + @CachedServiceBusQueuePreparer(name_prefix='servicebustest') + async def test_client_sas_credential_async(self, + servicebus_queue, + servicebus_namespace, + servicebus_namespace_key_name, + servicebus_namespace_primary_key, + servicebus_namespace_connection_string, + **kwargs): + # This should "just work" to validate known-good. + credential = ServiceBusSharedKeyCredential(servicebus_namespace_key_name, servicebus_namespace_primary_key) + hostname = "{}.servicebus.windows.net".format(servicebus_namespace.name) + auth_uri = "sb://{}/{}".format(hostname, servicebus_queue.name) + token = (await credential.get_token(auth_uri)).token + + # Finally let's do it with SAS token + conn str + token_conn_str = "Endpoint=sb://{}/;SharedAccessSignature={};".format(hostname, token.decode()) + + client = ServiceBusClient.from_connection_string(token_conn_str) + async with client: + assert len(client._handlers) == 0 + async with client.get_queue_sender(servicebus_queue.name) as sender: + await sender.send_messages(Message("foo")) \ No newline at end of file diff --git a/sdk/servicebus/azure-servicebus/tests/async_tests/test_subscriptions_async.py b/sdk/servicebus/azure-servicebus/tests/async_tests/test_subscriptions_async.py index 637698ff3046..2e9d3cd260c6 100644 --- a/sdk/servicebus/azure-servicebus/tests/async_tests/test_subscriptions_async.py +++ b/sdk/servicebus/azure-servicebus/tests/async_tests/test_subscriptions_async.py @@ -12,7 +12,8 @@ from datetime import datetime, timedelta from azure.servicebus import Message, PeekMessage, ReceiveSettleMode -from azure.servicebus.aio import ServiceBusClient, ServiceBusSharedKeyCredential +from azure.servicebus.aio import ServiceBusClient +from azure.servicebus.aio._base_handler_async import ServiceBusSharedKeyCredential from azure.servicebus.exceptions import ServiceBusError from devtools_testutils import AzureMgmtTestCase, RandomNameResourceGroupPreparer, CachedResourceGroupPreparer diff --git a/sdk/servicebus/azure-servicebus/tests/async_tests/test_topic_async.py b/sdk/servicebus/azure-servicebus/tests/async_tests/test_topic_async.py index efd4cf48de65..b1b1b2c1a5e9 100644 --- a/sdk/servicebus/azure-servicebus/tests/async_tests/test_topic_async.py +++ b/sdk/servicebus/azure-servicebus/tests/async_tests/test_topic_async.py @@ -14,7 +14,8 @@ from devtools_testutils import AzureMgmtTestCase, RandomNameResourceGroupPreparer, CachedResourceGroupPreparer -from azure.servicebus.aio import ServiceBusClient, ServiceBusSharedKeyCredential +from azure.servicebus.aio import ServiceBusClient +from azure.servicebus.aio._base_handler_async import ServiceBusSharedKeyCredential from azure.servicebus._common.message import Message from servicebus_preparer import ( ServiceBusNamespacePreparer, diff --git a/sdk/servicebus/azure-servicebus/tests/mgmt_tests/test_mgmt_queues.py b/sdk/servicebus/azure-servicebus/tests/mgmt_tests/test_mgmt_queues.py index 7b4bbd133b9e..8d591b11caab 100644 --- a/sdk/servicebus/azure-servicebus/tests/mgmt_tests/test_mgmt_queues.py +++ b/sdk/servicebus/azure-servicebus/tests/mgmt_tests/test_mgmt_queues.py @@ -15,7 +15,7 @@ from azure.servicebus._common.utils import utc_now from utilities import get_logger from azure.core.exceptions import HttpResponseError, ServiceRequestError, ResourceNotFoundError, ResourceExistsError -from azure.servicebus import ServiceBusSharedKeyCredential +from azure.servicebus._base_handler import ServiceBusSharedKeyCredential from devtools_testutils import AzureMgmtTestCase, CachedResourceGroupPreparer from servicebus_preparer import ( diff --git a/sdk/servicebus/azure-servicebus/tests/test_sb_client.py b/sdk/servicebus/azure-servicebus/tests/test_sb_client.py index 703c1b0a397a..8d99a7201dff 100644 --- a/sdk/servicebus/azure-servicebus/tests/test_sb_client.py +++ b/sdk/servicebus/azure-servicebus/tests/test_sb_client.py @@ -13,7 +13,8 @@ from azure.common import AzureHttpError, AzureConflictHttpError from azure.mgmt.servicebus.models import AccessRights -from azure.servicebus import ServiceBusClient, ServiceBusSharedKeyCredential, ServiceBusSender +from azure.servicebus import ServiceBusClient, ServiceBusSender +from azure.servicebus._base_handler import ServiceBusSharedKeyCredential from azure.servicebus._common.message import Message, PeekMessage from azure.servicebus._common.constants import ReceiveSettleMode from azure.servicebus.exceptions import ( @@ -189,3 +190,30 @@ def test_sb_client_close_spawned_handlers(self, servicebus_namespace_connection_ assert not sender._handler and not sender._running assert not receiver._handler and not receiver._running assert len(client._handlers) == 0 + + @pytest.mark.liveTest + @pytest.mark.live_test_only + @CachedResourceGroupPreparer() + @CachedServiceBusNamespacePreparer(name_prefix='servicebustest') + @CachedServiceBusQueuePreparer(name_prefix='servicebustest') + def test_client_sas_credential(self, + servicebus_queue, + servicebus_namespace, + servicebus_namespace_key_name, + servicebus_namespace_primary_key, + servicebus_namespace_connection_string, + **kwargs): + # This should "just work" to validate known-good. + credential = ServiceBusSharedKeyCredential(servicebus_namespace_key_name, servicebus_namespace_primary_key) + hostname = "{}.servicebus.windows.net".format(servicebus_namespace.name) + auth_uri = "sb://{}/{}".format(hostname, servicebus_queue.name) + token = credential.get_token(auth_uri).token + + # Finally let's do it with SAS token + conn str + token_conn_str = "Endpoint=sb://{}/;SharedAccessSignature={};".format(hostname, token.decode()) + + client = ServiceBusClient.from_connection_string(token_conn_str) + with client: + assert len(client._handlers) == 0 + with client.get_queue_sender(servicebus_queue.name) as sender: + sender.send_messages(Message("foo")) \ No newline at end of file diff --git a/sdk/servicebus/azure-servicebus/tests/test_subscriptions.py b/sdk/servicebus/azure-servicebus/tests/test_subscriptions.py index c7bc6d7eac75..0276af86bac3 100644 --- a/sdk/servicebus/azure-servicebus/tests/test_subscriptions.py +++ b/sdk/servicebus/azure-servicebus/tests/test_subscriptions.py @@ -11,7 +11,8 @@ import time from datetime import datetime, timedelta -from azure.servicebus import ServiceBusClient, Message, PeekMessage, ReceiveSettleMode, ServiceBusSharedKeyCredential +from azure.servicebus import ServiceBusClient, Message, PeekMessage, ReceiveSettleMode +from azure.servicebus._base_handler import ServiceBusSharedKeyCredential from azure.servicebus.exceptions import ServiceBusError from devtools_testutils import AzureMgmtTestCase, RandomNameResourceGroupPreparer, CachedResourceGroupPreparer diff --git a/sdk/servicebus/azure-servicebus/tests/test_topic.py b/sdk/servicebus/azure-servicebus/tests/test_topic.py index ecbffa3cba1a..afa1d4b9fe96 100644 --- a/sdk/servicebus/azure-servicebus/tests/test_topic.py +++ b/sdk/servicebus/azure-servicebus/tests/test_topic.py @@ -13,7 +13,8 @@ from devtools_testutils import AzureMgmtTestCase, RandomNameResourceGroupPreparer, CachedResourceGroupPreparer -from azure.servicebus import ServiceBusClient, ServiceBusSharedKeyCredential +from azure.servicebus import ServiceBusClient +from azure.servicebus._base_handler import ServiceBusSharedKeyCredential from azure.servicebus._common.message import Message from servicebus_preparer import ( ServiceBusNamespacePreparer, From 902f92d3b0787e8abff8cb489b47276940639c9d Mon Sep 17 00:00:00 2001 From: Kieran Brantner-Magee Date: Tue, 8 Sep 2020 14:30:58 -0700 Subject: [PATCH 2/8] pylint and mypy fixes --- .../azure/servicebus/_base_handler.py | 31 ++++++++++--------- .../azure/servicebus/_common/utils.py | 15 ++++----- .../azure/servicebus/_servicebus_client.py | 2 +- .../azure/servicebus/_servicebus_receiver.py | 4 +-- .../azure/servicebus/_servicebus_sender.py | 4 +-- .../servicebus/aio/_base_handler_async.py | 18 ++++++----- .../aio/_servicebus_client_async.py | 2 +- .../aio/_servicebus_receiver_async.py | 4 +-- .../aio/_servicebus_sender_async.py | 4 +-- .../management/_management_client_async.py | 6 ++-- .../management/_management_client.py | 4 +-- 11 files changed, 50 insertions(+), 44 deletions(-) diff --git a/sdk/servicebus/azure-servicebus/azure/servicebus/_base_handler.py b/sdk/servicebus/azure-servicebus/azure/servicebus/_base_handler.py index 97d82e0547c6..4a74a5083b2a 100644 --- a/sdk/servicebus/azure-servicebus/azure/servicebus/_base_handler.py +++ b/sdk/servicebus/azure-servicebus/azure/servicebus/_base_handler.py @@ -7,7 +7,7 @@ import uuid import time from datetime import timedelta -from typing import cast, Optional, Tuple, TYPE_CHECKING, Dict, Any, Callable, Type +from typing import cast, Optional, Tuple, TYPE_CHECKING, Dict, Any, Callable try: from urllib import quote_plus # type: ignore @@ -18,6 +18,8 @@ from uamqp import utils from uamqp.message import MessageProperties +from azure.core.credentials import AccessToken + from ._common._configuration import Configuration from .exceptions import ( ServiceBusError, @@ -33,7 +35,6 @@ ASSOCIATEDLINKPROPERTYNAME ) -from azure.core.credentials import AccessToken if TYPE_CHECKING: from azure.core.credentials import TokenCredential @@ -42,7 +43,7 @@ def _parse_conn_str(conn_str): - # type: (str) -> Tuple[str, str, str, str] + # type: (str) -> Tuple[str, Optional[str], Optional[str], str, Optional[str], Optional[int]] endpoint = None shared_access_key_name = None shared_access_key = None @@ -82,10 +83,10 @@ def _parse_conn_str(conn_str): else: host = str(endpoint) - return (host, + return (host, str(shared_access_key_name) if shared_access_key_name else None, str(shared_access_key) if shared_access_key else None, - entity, + entity, str(shared_access_signature) if shared_access_signature else None, shared_access_signature_expiry) @@ -174,40 +175,40 @@ def __init__( self._auth_uri = None self._properties = create_properties(self._config.user_agent) - - def _convert_connection_string_to_kwargs(self, conn_str, **kwargs): - # type: (str, Type, Any) -> Dict[str, Any] + @classmethod + def _convert_connection_string_to_kwargs(cls, conn_str, **kwargs): + # type: (str, Any) -> Dict[str, Any] host, policy, key, entity_in_conn_str, token, token_expiry = _parse_conn_str(conn_str) queue_name = kwargs.get("queue_name") topic_name = kwargs.get("topic_name") if not (queue_name or topic_name or entity_in_conn_str): raise ValueError("Entity name is missing. Please specify `queue_name` or `topic_name`" " or use a connection string including the entity information.") - + if queue_name and topic_name: raise ValueError("`queue_name` and `topic_name` can not be specified simultaneously.") - + entity_in_kwargs = queue_name or topic_name if entity_in_conn_str and entity_in_kwargs and (entity_in_conn_str != entity_in_kwargs): raise ServiceBusAuthenticationError( "Entity names do not match, the entity name in connection string is {};" " the entity name in parameter is {}.".format(entity_in_conn_str, entity_in_kwargs) ) - + kwargs["fully_qualified_namespace"] = host kwargs["entity_name"] = entity_in_conn_str or entity_in_kwargs # This has to be defined seperately to support sync vs async credentials. - kwargs["credential"] = self._create_credential_from_connection_string_parameters(token, + kwargs["credential"] = cls._create_credential_from_connection_string_parameters(token, token_expiry, policy, key) return kwargs - def _create_credential_from_connection_string_parameters(self, token, token_expiry, policy, key): + @classmethod + def _create_credential_from_connection_string_parameters(cls, token, token_expiry, policy, key): if token and token_expiry: return ServiceBusSASTokenCredential(token, token_expiry) - elif policy and key: - return ServiceBusSharedKeyCredential(policy, key) + return ServiceBusSharedKeyCredential(policy, key) def __enter__(self): self._open_with_retry() diff --git a/sdk/servicebus/azure-servicebus/azure/servicebus/_common/utils.py b/sdk/servicebus/azure-servicebus/azure/servicebus/_common/utils.py index 8d8c0f2ea633..f978c2c9d723 100644 --- a/sdk/servicebus/azure-servicebus/azure/servicebus/_common/utils.py +++ b/sdk/servicebus/azure-servicebus/azure/servicebus/_common/utils.py @@ -9,7 +9,8 @@ import logging import functools import platform -from typing import Optional, Dict +import time +from typing import Optional, Dict, Tuple try: from urlparse import urlparse except ImportError: @@ -66,10 +67,10 @@ def utc_now(): # This parse_conn_str is used for mgmt, the other in base_handler for handlers. Should be unified. def parse_conn_str(conn_str): # type: (str) -> Tuple[str, Optional[str], Optional[str], str, Optional[str], Optional[int]] - endpoint = None - shared_access_key_name = None - shared_access_key = None - entity_path = None + endpoint = '' + shared_access_key_name = None # type: Optional[str] + shared_access_key = None # type: Optional[str] + entity_path = '' shared_access_signature = None # type: Optional[str] shared_access_signature_expiry = None # type: Optional[int] for element in conn_str.split(';'): @@ -93,10 +94,10 @@ def parse_conn_str(conn_str): shared_access_signature_expiry = int(time.time() * 2) if not (all((endpoint, shared_access_key_name, shared_access_key)) or all((endpoint, shared_access_signature))): raise ValueError("Invalid connection string") - return (endpoint, + return (endpoint, str(shared_access_key_name) if shared_access_key_name else None, str(shared_access_key) if shared_access_key else None, - entity_path, + entity_path, str(shared_access_signature) if shared_access_signature else None, shared_access_signature_expiry) diff --git a/sdk/servicebus/azure-servicebus/azure/servicebus/_servicebus_client.py b/sdk/servicebus/azure-servicebus/azure/servicebus/_servicebus_client.py index 7ff7e271e8f7..17b66abef18b 100644 --- a/sdk/servicebus/azure-servicebus/azure/servicebus/_servicebus_client.py +++ b/sdk/servicebus/azure-servicebus/azure/servicebus/_servicebus_client.py @@ -153,7 +153,7 @@ def from_connection_string( if token and token_expiry: credential = ServiceBusSASTokenCredential(token, token_expiry) elif policy and key: - credential = ServiceBusSharedKeyCredential(policy, key) + credential = ServiceBusSharedKeyCredential(policy, key) # type: ignore return cls( fully_qualified_namespace=host, entity_name=entity_in_conn_str or kwargs.pop("entity_name", None), diff --git a/sdk/servicebus/azure-servicebus/azure/servicebus/_servicebus_receiver.py b/sdk/servicebus/azure-servicebus/azure/servicebus/_servicebus_receiver.py index 5551c998be73..1247338b12c0 100644 --- a/sdk/servicebus/azure-servicebus/azure/servicebus/_servicebus_receiver.py +++ b/sdk/servicebus/azure-servicebus/azure/servicebus/_servicebus_receiver.py @@ -11,7 +11,7 @@ from uamqp.constants import SenderSettleMode from uamqp.authentication.common import AMQPAuth -from ._base_handler import BaseHandler, ServiceBusSharedKeyCredential +from ._base_handler import BaseHandler from ._common.utils import create_authentication from ._common.message import PeekMessage, ReceivedMessage from ._common.constants import ( @@ -365,7 +365,7 @@ def from_connection_string( :caption: Create a new instance of the ServiceBusReceiver from connection string. """ - constructor_args = self._convert_connection_string_to_kwargs( + constructor_args = cls._convert_connection_string_to_kwargs( conn_str, **kwargs ) diff --git a/sdk/servicebus/azure-servicebus/azure/servicebus/_servicebus_sender.py b/sdk/servicebus/azure-servicebus/azure/servicebus/_servicebus_sender.py index 102edba6ea4f..7803a0a67b00 100644 --- a/sdk/servicebus/azure-servicebus/azure/servicebus/_servicebus_sender.py +++ b/sdk/servicebus/azure-servicebus/azure/servicebus/_servicebus_sender.py @@ -11,7 +11,7 @@ from uamqp import SendClient, types from uamqp.authentication.common import AMQPAuth -from ._base_handler import BaseHandler, ServiceBusSharedKeyCredential +from ._base_handler import BaseHandler from ._common import mgmt_handlers from ._common.message import Message, BatchMessage from .exceptions import ( @@ -297,7 +297,7 @@ def from_connection_string( :caption: Create a new instance of the ServiceBusSender from connection string. """ - constructor_args = self._convert_connection_string_to_kwargs( + constructor_args = cls._convert_connection_string_to_kwargs( conn_str, **kwargs ) diff --git a/sdk/servicebus/azure-servicebus/azure/servicebus/aio/_base_handler_async.py b/sdk/servicebus/azure-servicebus/azure/servicebus/aio/_base_handler_async.py index cab977713df6..d6fb5d3722aa 100644 --- a/sdk/servicebus/azure-servicebus/azure/servicebus/aio/_base_handler_async.py +++ b/sdk/servicebus/azure-servicebus/azure/servicebus/aio/_base_handler_async.py @@ -9,7 +9,10 @@ import uamqp from uamqp.message import MessageProperties -from .._base_handler import _generate_sas_token, _AccessToken + +from azure.core.credentials import AccessToken + +from .._base_handler import _generate_sas_token, _AccessToken, BaseHandler as BaseHandlerSync from .._common._configuration import Configuration from .._common.utils import create_properties from .._common.constants import ( @@ -22,7 +25,6 @@ _create_servicebus_exception ) -from azure.core.credentials import AccessToken if TYPE_CHECKING: from azure.core.credentials import TokenCredential @@ -91,14 +93,16 @@ def __init__( self._auth_uri = None self._properties = create_properties(self._config.user_agent) - def _convert_connection_string_to_kwargs(self, conn_str, **kwargs): - return BaseHandlerSync._convert_connection_string_to_kwargs(self, conn_str, kwargs) + @classmethod + def _convert_connection_string_to_kwargs(cls, conn_str, **kwargs): + # pylint:disable=protected-access + return BaseHandlerSync._convert_connection_string_to_kwargs(conn_str, **kwargs) - def _create_credential_from_connection_string_parameters(self, token, token_expiry, policy, key): + @classmethod + def _create_credential_from_connection_string_parameters(cls, token, token_expiry, policy, key): if token and token_expiry: return ServiceBusSASTokenCredential(token, token_expiry) - elif policy and key: - return ServiceBusSharedKeyCredential(policy, key) + return ServiceBusSharedKeyCredential(policy, key) async def __aenter__(self): await self._open_with_retry() diff --git a/sdk/servicebus/azure-servicebus/azure/servicebus/aio/_servicebus_client_async.py b/sdk/servicebus/azure-servicebus/azure/servicebus/aio/_servicebus_client_async.py index fe1ec7f0360e..d162bec5dc95 100644 --- a/sdk/servicebus/azure-servicebus/azure/servicebus/aio/_servicebus_client_async.py +++ b/sdk/servicebus/azure-servicebus/azure/servicebus/aio/_servicebus_client_async.py @@ -129,7 +129,7 @@ def from_connection_string( if token and token_expiry: credential = ServiceBusSASTokenCredential(token, token_expiry) elif policy and key: - credential = ServiceBusSharedKeyCredential(policy, key) + credential = ServiceBusSharedKeyCredential(policy, key) # type: ignore return cls( fully_qualified_namespace=host, entity_name=entity_in_conn_str or kwargs.pop("entity_name", None), diff --git a/sdk/servicebus/azure-servicebus/azure/servicebus/aio/_servicebus_receiver_async.py b/sdk/servicebus/azure-servicebus/azure/servicebus/aio/_servicebus_receiver_async.py index 21a11a7ea1b5..398bc357e358 100644 --- a/sdk/servicebus/azure-servicebus/azure/servicebus/aio/_servicebus_receiver_async.py +++ b/sdk/servicebus/azure-servicebus/azure/servicebus/aio/_servicebus_receiver_async.py @@ -11,7 +11,7 @@ from uamqp import ReceiveClientAsync, types, Message from uamqp.constants import SenderSettleMode -from ._base_handler_async import BaseHandler, ServiceBusSharedKeyCredential +from ._base_handler_async import BaseHandler from ._async_message import ReceivedMessage from .._common.receiver_mixins import ReceiverMixin from .._common.constants import ( @@ -359,7 +359,7 @@ def from_connection_string( :caption: Create a new instance of the ServiceBusReceiver from connection string. """ - constructor_args = self._convert_connection_string_to_kwargs( + constructor_args = cls._convert_connection_string_to_kwargs( conn_str, **kwargs ) diff --git a/sdk/servicebus/azure-servicebus/azure/servicebus/aio/_servicebus_sender_async.py b/sdk/servicebus/azure-servicebus/azure/servicebus/aio/_servicebus_sender_async.py index 9f7bf6a5d060..267cd703257c 100644 --- a/sdk/servicebus/azure-servicebus/azure/servicebus/aio/_servicebus_sender_async.py +++ b/sdk/servicebus/azure-servicebus/azure/servicebus/aio/_servicebus_sender_async.py @@ -11,7 +11,7 @@ from .._common.message import Message, BatchMessage from .._servicebus_sender import SenderMixin -from ._base_handler_async import BaseHandler, ServiceBusSharedKeyCredential +from ._base_handler_async import BaseHandler from .._common.constants import ( REQUEST_RESPONSE_SCHEDULE_MESSAGE_OPERATION, REQUEST_RESPONSE_CANCEL_SCHEDULED_MESSAGE_OPERATION, @@ -235,7 +235,7 @@ def from_connection_string( :caption: Create a new instance of the ServiceBusSender from connection string. """ - constructor_args = self._convert_connection_string_to_kwargs( + constructor_args = cls._convert_connection_string_to_kwargs( conn_str, **kwargs ) diff --git a/sdk/servicebus/azure-servicebus/azure/servicebus/aio/management/_management_client_async.py b/sdk/servicebus/azure-servicebus/azure/servicebus/aio/management/_management_client_async.py index e2351994ecce..a92b6f63ee01 100644 --- a/sdk/servicebus/azure-servicebus/azure/servicebus/aio/management/_management_client_async.py +++ b/sdk/servicebus/azure-servicebus/azure/servicebus/aio/management/_management_client_async.py @@ -136,14 +136,14 @@ def from_connection_string(cls, conn_str: str, **kwargs: Any) -> "ServiceBusAdmi :param str conn_str: The connection string of the Service Bus Namespace. :rtype: ~azure.servicebus.management.aio.ServiceBusAdministrationClient """ - endpoint, shared_access_key_name, shared_access_key, token, token_expiry, _ = parse_conn_str(conn_str) + endpoint, shared_access_key_name, _, shared_access_key, token, token_expiry = parse_conn_str(conn_str) if token and token_expiry: credential = ServiceBusSASTokenCredential(token, token_expiry) elif shared_access_key_name and shared_access_key: - credential = ServiceBusSharedKeyCredential(shared_access_key_name, shared_access_key) + credential = ServiceBusSharedKeyCredential(shared_access_key_name, shared_access_key) # type: ignore if "//" in endpoint: endpoint = endpoint[endpoint.index("//")+2:] - return cls(endpoint, credential, **kwargs) + return cls(endpoint, credential, **kwargs) # type: ignore async def get_queue(self, queue_name: str, **kwargs) -> QueueProperties: """Get the properties of a queue. diff --git a/sdk/servicebus/azure-servicebus/azure/servicebus/management/_management_client.py b/sdk/servicebus/azure-servicebus/azure/servicebus/management/_management_client.py index ba4ca17c1e83..af922ad52f16 100644 --- a/sdk/servicebus/azure-servicebus/azure/servicebus/management/_management_client.py +++ b/sdk/servicebus/azure-servicebus/azure/servicebus/management/_management_client.py @@ -130,11 +130,11 @@ def from_connection_string(cls, conn_str, **kwargs): :param str conn_str: The connection string of the Service Bus Namespace. :rtype: ~azure.servicebus.management.ServiceBusAdministrationClient """ - endpoint, shared_access_key_name, shared_access_key, token, token_expiry, _ = parse_conn_str(conn_str) + endpoint, shared_access_key_name, shared_access_key, _, token, token_expiry = parse_conn_str(conn_str) if token and token_expiry: credential = ServiceBusSASTokenCredential(token, token_expiry) elif shared_access_key_name and shared_access_key: - credential = ServiceBusSharedKeyCredential(shared_access_key_name, shared_access_key) + credential = ServiceBusSharedKeyCredential(shared_access_key_name, shared_access_key) # type: ignore if "//" in endpoint: endpoint = endpoint[endpoint.index("//") + 2:] return cls(endpoint, credential, **kwargs) From d1062f6eae9a0e1a0c004a9b02e3fec92b05b463 Mon Sep 17 00:00:00 2001 From: Kieran Brantner-Magee Date: Tue, 8 Sep 2020 14:45:18 -0700 Subject: [PATCH 3/8] Add safety net for if signature and key are both provided in connstr (inspired by .nets approach) --- .../azure-servicebus/azure/servicebus/_base_handler.py | 4 +++- .../azure-servicebus/azure/servicebus/_common/utils.py | 9 +++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/sdk/servicebus/azure-servicebus/azure/servicebus/_base_handler.py b/sdk/servicebus/azure-servicebus/azure/servicebus/_base_handler.py index 4a74a5083b2a..545fa074807f 100644 --- a/sdk/servicebus/azure-servicebus/azure/servicebus/_base_handler.py +++ b/sdk/servicebus/azure-servicebus/azure/servicebus/_base_handler.py @@ -71,10 +71,12 @@ def _parse_conn_str(conn_str): except (IndexError, TypeError, ValueError): # Fallback since technically expiry is optional. # An arbitrary, absurdly large number, since you can't renew. shared_access_signature_expiry = int(time.time() * 2) - if not (all((endpoint, shared_access_key_name, shared_access_key)) or all((endpoint, shared_access_signature))): + if not (all((endpoint, shared_access_key_name, shared_access_key)) or all((endpoint, shared_access_signature))) \ + or all((shared_access_key_name, shared_access_signature)): # this latter clause since we don't accept both raise ValueError( "Invalid connection string. Should be in the format: " "Endpoint=sb:///;SharedAccessKeyName=;SharedAccessKey=" + "\nWith alternate option of providing SharedAccessSignature instead of SharedAccessKeyName and Key" ) entity = cast(str, entity_path) left_slash_pos = cast(str, endpoint).find("//") diff --git a/sdk/servicebus/azure-servicebus/azure/servicebus/_common/utils.py b/sdk/servicebus/azure-servicebus/azure/servicebus/_common/utils.py index f978c2c9d723..bc6c84cb1e32 100644 --- a/sdk/servicebus/azure-servicebus/azure/servicebus/_common/utils.py +++ b/sdk/servicebus/azure-servicebus/azure/servicebus/_common/utils.py @@ -92,8 +92,13 @@ def parse_conn_str(conn_str): except (IndexError, TypeError, ValueError): # Fallback since technically expiry is optional. # An arbitrary, absurdly large number, since you can't renew. shared_access_signature_expiry = int(time.time() * 2) - if not (all((endpoint, shared_access_key_name, shared_access_key)) or all((endpoint, shared_access_signature))): - raise ValueError("Invalid connection string") + if not (all((endpoint, shared_access_key_name, shared_access_key)) or all((endpoint, shared_access_signature))) \ + or all((shared_access_key_name, shared_access_signature)): # this latter clause since we don't accept both + raise ValueError( + "Invalid connection string. Should be in the format: " + "Endpoint=sb:///;SharedAccessKeyName=;SharedAccessKey=" + "\nWith alternate option of providing SharedAccessSignature instead of SharedAccessKeyName and Key" + ) return (endpoint, str(shared_access_key_name) if shared_access_key_name else None, str(shared_access_key) if shared_access_key else None, From 892b61279debfd312967500e13529fb14ce75399 Mon Sep 17 00:00:00 2001 From: Kieran Brantner-Magee Date: Tue, 8 Sep 2020 16:03:31 -0700 Subject: [PATCH 4/8] remove spurious changelog line, disable SAS token non-se= SAS test pending UAMQP fix. --- sdk/servicebus/azure-servicebus/CHANGELOG.md | 1 - .../azure-servicebus/tests/test_sb_client.py | 12 +++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/sdk/servicebus/azure-servicebus/CHANGELOG.md b/sdk/servicebus/azure-servicebus/CHANGELOG.md index 10781156dbdd..b43424a116c3 100644 --- a/sdk/servicebus/azure-servicebus/CHANGELOG.md +++ b/sdk/servicebus/azure-servicebus/CHANGELOG.md @@ -4,7 +4,6 @@ **New Features** * Messages can now be sent twice in succession. -* Connection strings now support * Connection strings used with `from_connection_string` methods now support using the `SharedAccessSignature` key in leiu of `sharedaccesskey` and `sharedaccesskeyname`, taking the string of the properly constructed token as value. * Internal AMQP message properties (header, footer, annotations, properties, etc) are now exposed via `Message.amqp_message` diff --git a/sdk/servicebus/azure-servicebus/tests/test_sb_client.py b/sdk/servicebus/azure-servicebus/tests/test_sb_client.py index 8d99a7201dff..70d68126bec5 100644 --- a/sdk/servicebus/azure-servicebus/tests/test_sb_client.py +++ b/sdk/servicebus/azure-servicebus/tests/test_sb_client.py @@ -216,4 +216,14 @@ def test_client_sas_credential(self, with client: assert len(client._handlers) == 0 with client.get_queue_sender(servicebus_queue.name) as sender: - sender.send_messages(Message("foo")) \ No newline at end of file + sender.send_messages(Message("foo")) + + # This is disabled pending UAMQP fix https://github.com/Azure/azure-uamqp-python/issues/170 + # + #token_conn_str_without_se = token_conn_str.split('se=')[0] + token_conn_str.split('se=')[1].split('&')[1] + # + #client = ServiceBusClient.from_connection_string(token_conn_str_without_se) + #with client: + # assert len(client._handlers) == 0 + # with client.get_queue_sender(servicebus_queue.name) as sender: + # sender.send_messages(Message("foo")) \ No newline at end of file From f8d8ad1f244d3efb40b0f0272207f199171b6fac Mon Sep 17 00:00:00 2001 From: Kieran Brantner-Magee Date: Tue, 8 Sep 2020 17:59:24 -0700 Subject: [PATCH 5/8] Fix async mgmt client conn str parsing order --- .../servicebus/aio/management/_management_client_async.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sdk/servicebus/azure-servicebus/azure/servicebus/aio/management/_management_client_async.py b/sdk/servicebus/azure-servicebus/azure/servicebus/aio/management/_management_client_async.py index 916cc2ba201e..8b79c28aa6a0 100644 --- a/sdk/servicebus/azure-servicebus/azure/servicebus/aio/management/_management_client_async.py +++ b/sdk/servicebus/azure-servicebus/azure/servicebus/aio/management/_management_client_async.py @@ -136,7 +136,9 @@ def from_connection_string(cls, conn_str: str, **kwargs: Any) -> "ServiceBusAdmi :param str conn_str: The connection string of the Service Bus Namespace. :rtype: ~azure.servicebus.management.aio.ServiceBusAdministrationClient """ - endpoint, shared_access_key_name, _, shared_access_key, token, token_expiry = parse_conn_str(conn_str) + endpoint, shared_access_key_name, shared_access_key, _, token, token_expiry = parse_conn_str(conn_str) + print(conn_str) + print(shared_access_key_name) if token and token_expiry: credential = ServiceBusSASTokenCredential(token, token_expiry) elif shared_access_key_name and shared_access_key: From f83bf4c571c397888f5e3871e7e5077d74b200a1 Mon Sep 17 00:00:00 2001 From: Kieran Brantner-Magee Date: Tue, 8 Sep 2020 18:11:24 -0700 Subject: [PATCH 6/8] Fix lint error (extra space) --- .../azure/servicebus/aio/management/_management_client_async.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/servicebus/azure-servicebus/azure/servicebus/aio/management/_management_client_async.py b/sdk/servicebus/azure-servicebus/azure/servicebus/aio/management/_management_client_async.py index 8b79c28aa6a0..90c674832868 100644 --- a/sdk/servicebus/azure-servicebus/azure/servicebus/aio/management/_management_client_async.py +++ b/sdk/servicebus/azure-servicebus/azure/servicebus/aio/management/_management_client_async.py @@ -136,7 +136,7 @@ def from_connection_string(cls, conn_str: str, **kwargs: Any) -> "ServiceBusAdmi :param str conn_str: The connection string of the Service Bus Namespace. :rtype: ~azure.servicebus.management.aio.ServiceBusAdministrationClient """ - endpoint, shared_access_key_name, shared_access_key, _, token, token_expiry = parse_conn_str(conn_str) + endpoint, shared_access_key_name, shared_access_key, _, token, token_expiry = parse_conn_str(conn_str) print(conn_str) print(shared_access_key_name) if token and token_expiry: From 7937c8f0c05351127b97925abdae8516e7b5da13 Mon Sep 17 00:00:00 2001 From: KieranBrantnerMagee Date: Wed, 9 Sep 2020 16:00:40 -0700 Subject: [PATCH 7/8] Update sdk/servicebus/azure-servicebus/tests/async_tests/test_sb_client_async.py add trailing newline Co-authored-by: Rakshith Bhyravabhotla --- .../azure-servicebus/tests/async_tests/test_sb_client_async.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/servicebus/azure-servicebus/tests/async_tests/test_sb_client_async.py b/sdk/servicebus/azure-servicebus/tests/async_tests/test_sb_client_async.py index 5a22118905e7..a51571caf082 100644 --- a/sdk/servicebus/azure-servicebus/tests/async_tests/test_sb_client_async.py +++ b/sdk/servicebus/azure-servicebus/tests/async_tests/test_sb_client_async.py @@ -87,4 +87,4 @@ async def test_client_sas_credential_async(self, async with client: assert len(client._handlers) == 0 async with client.get_queue_sender(servicebus_queue.name) as sender: - await sender.send_messages(Message("foo")) \ No newline at end of file + await sender.send_messages(Message("foo")) From 1934db91ac0f55bc7be5b745ce52650c35de5cf3 Mon Sep 17 00:00:00 2001 From: Kieran Brantner-Magee Date: Wed, 9 Sep 2020 16:04:19 -0700 Subject: [PATCH 8/8] remove spurious debugging prints --- .../azure/servicebus/aio/management/_management_client_async.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/sdk/servicebus/azure-servicebus/azure/servicebus/aio/management/_management_client_async.py b/sdk/servicebus/azure-servicebus/azure/servicebus/aio/management/_management_client_async.py index 90c674832868..673b8b8d71cd 100644 --- a/sdk/servicebus/azure-servicebus/azure/servicebus/aio/management/_management_client_async.py +++ b/sdk/servicebus/azure-servicebus/azure/servicebus/aio/management/_management_client_async.py @@ -137,8 +137,6 @@ def from_connection_string(cls, conn_str: str, **kwargs: Any) -> "ServiceBusAdmi :rtype: ~azure.servicebus.management.aio.ServiceBusAdministrationClient """ endpoint, shared_access_key_name, shared_access_key, _, token, token_expiry = parse_conn_str(conn_str) - print(conn_str) - print(shared_access_key_name) if token and token_expiry: credential = ServiceBusSASTokenCredential(token, token_expiry) elif shared_access_key_name and shared_access_key: