Skip to content
Merged
1 change: 1 addition & 0 deletions sdk/servicebus/azure-servicebus/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
- `ServiceBusReceiver`: `receive_deferred_messages`, `peek_messages` and `renew_message_lock`
- `ServiceBusSession`: `get_state`, `set_state` and `renew_lock`
* `azure.servicebus.exceptions.ServiceBusError` now inherits from `azure.core.exceptions.AzureError`.
* Added a `parse_connection_string` method which parses a connection string into a properties bag containing its component parts

**Breaking Changes**

Expand Down
8 changes: 7 additions & 1 deletion sdk/servicebus/azure-servicebus/azure/servicebus/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@
from ._common.message import ServiceBusMessage, ServiceBusMessageBatch, ServiceBusReceivedMessage
from ._common.constants import ReceiveMode, SubQueue, NEXT_AVAILABLE_SESSION
from ._common.auto_lock_renewer import AutoLockRenewer
from ._common._connection_string_parser import (
parse_connection_string,
ServiceBusConnectionStringProperties
)

TransportType = constants.TransportType

Expand All @@ -30,5 +34,7 @@
'ServiceBusSession',
'ServiceBusSender',
'TransportType',
'AutoLockRenewer'
'AutoLockRenewer',
'parse_connection_string',
'ServiceBusConnectionStringProperties'
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------
try:
from urllib.parse import urlparse
except ImportError:
from urlparse import urlparse # type: ignore

from azure.servicebus.management._models import DictMixin

class ServiceBusConnectionStringProperties(DictMixin):
"""
Properties of a connection string.
"""
def __init__(self, **kwargs):
self._fully_qualified_namespace = kwargs.pop('fully_qualified_namespace', None)
self._endpoint = kwargs.pop('endpoint', None)
self._entity_path = kwargs.pop('entity_path', None)
self._shared_access_signature = kwargs.pop('shared_access_signature', None)
self._shared_access_key_name = kwargs.pop('shared_access_key_name', None)
self._shared_access_key = kwargs.pop('shared_access_key', None)

@property
def fully_qualified_namespace(self):
"""The fully qualified host name for the Service Bus namespace.
The namespace format is: `<yournamespace>.servicebus.windows.net`.
"""
return self._fully_qualified_namespace

@property
def endpoint(self):
"""The endpoint for the Service Bus resource. In the format sb://<FQDN>/
"""
return self._endpoint

@property
def entity_path(self):
"""Optional. Represents the name of the queue/topic.
"""
return self._entity_path

@property
def shared_access_signature(self):
"""
This can be provided instead of the shared_access_key_name and the shared_access_key.
"""
return self._shared_access_signature

@property
def shared_access_key_name(self):
"""
The name of the shared_access_key. This must be used along with the shared_access_key.
"""
return self._shared_access_key_name

@property
def shared_access_key(self):
"""
The shared_access_key can be used along with the shared_access_key_name as a credential.
"""
return self._shared_access_key


def parse_connection_string(conn_str):
# type(str) -> ServiceBusConnectionStringProperties
"""Parse the connection string into a properties bag containing its component parts.

:param conn_str: The connection string that has to be parsed.
:type conn_str: str
:rtype: ~azure.servicebus.ServiceBusConnectionStringProperties
"""
conn_settings = [s.split("=", 1) for s in conn_str.split(";")]
if any(len(tup) != 2 for tup in conn_settings):
raise ValueError("Connection string is either blank or malformed.")
conn_settings = dict(conn_settings)
shared_access_signature = None
for key, value in conn_settings.items():
if key.lower() == 'sharedaccesssignature':
shared_access_signature = value
shared_access_key = conn_settings.get('SharedAccessKey')
shared_access_key_name = conn_settings.get('SharedAccessKeyName')
if any([shared_access_key, shared_access_key_name]) and not all([shared_access_key, shared_access_key_name]):
raise ValueError("Connection string must have both SharedAccessKeyName and SharedAccessKey.")
if shared_access_signature is not None and shared_access_key is not None:
raise ValueError("Only one of the SharedAccessKey or SharedAccessSignature must be present.")
endpoint = conn_settings.get('Endpoint')
if not endpoint:
raise ValueError("Connection string is either blank or malformed.")
parsed = urlparse(endpoint.rstrip('/'))
if not parsed.netloc:
raise ValueError("Invalid Endpoint on the Connection String.")
namespace = parsed.netloc.strip()
props = {
'fully_qualified_namespace': namespace,
'endpoint': endpoint,
'entity_path': conn_settings.get('EntityPath'),
'shared_access_signature': shared_access_signature,
'shared_access_key_name': shared_access_key_name,
'shared_access_key': shared_access_key
}
return ServiceBusConnectionStringProperties(**props)
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
#-------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for
# license information.
#--------------------------------------------------------------------------

import os
import pytest
from azure.servicebus import (
ServiceBusConnectionStringProperties,
parse_connection_string,
)

from devtools_testutils import AzureMgmtTestCase

class ServiceBusConnectionStringParserTests(AzureMgmtTestCase):
def test_sb_conn_str_parse_cs(self, **kwargs):
conn_str = 'Endpoint=sb://resourcename.servicebus.windows.net/;SharedAccessKeyName=test;SharedAccessKey=THISISATESTKEYXXXXXXXXXXXXXXXXXXXXXXXXXXXX='
parse_result = parse_connection_string(conn_str)
assert parse_result.endpoint == 'sb://resourcename.servicebus.windows.net/'
assert parse_result.fully_qualified_namespace == 'resourcename.servicebus.windows.net'
assert parse_result.shared_access_key_name == 'test'
assert parse_result.shared_access_key == 'THISISATESTKEYXXXXXXXXXXXXXXXXXXXXXXXXXXXX='

def test_sb_conn_str_parse_sas_and_shared_key(self, **kwargs):
conn_str = 'Endpoint=sb://resourcename.servicebus.windows.net/;SharedAccessKeyName=test;SharedAccessKey=THISISATESTKEYXXXXXXXXXXXXXXXXXXXXXXXXXXXX=;SharedAccessSignature=THISISASASXXXXXXX='
with pytest.raises(ValueError) as e:
parse_result = parse_connection_string(conn_str)
assert str(e.value) == 'Only one of the SharedAccessKey or SharedAccessSignature must be present.'

def test_sb_parse_malformed_conn_str_no_endpoint(self, **kwargs):
conn_str = 'SharedAccessKeyName=test;SharedAccessKey=THISISATESTKEYXXXXXXXXXXXXXXXXXXXXXXXXXXXX='
with pytest.raises(ValueError) as e:
parse_result = parse_connection_string(conn_str)
assert str(e.value) == 'Connection string is either blank or malformed.'

def test_sb_parse_malformed_conn_str_no_netloc(self, **kwargs):
conn_str = 'Endpoint=MALFORMED;SharedAccessKeyName=test;SharedAccessKey=THISISATESTKEYXXXXXXXXXXXXXXXXXXXXXXXXXXXX='
with pytest.raises(ValueError) as e:
parse_result = parse_connection_string(conn_str)
assert str(e.value) == 'Invalid Endpoint on the Connection String.'

def test_sb_parse_conn_str_sas(self, **kwargs):
conn_str = 'Endpoint=sb://resourcename.servicebus.windows.net/;SharedAccessSignature=THISISATESTKEYXXXXXXXXXXXXXXXXXXXXXXXXXXXX='
parse_result = parse_connection_string(conn_str)
assert parse_result.endpoint == 'sb://resourcename.servicebus.windows.net/'
assert parse_result.fully_qualified_namespace == 'resourcename.servicebus.windows.net'
assert parse_result.shared_access_signature == 'THISISATESTKEYXXXXXXXXXXXXXXXXXXXXXXXXXXXX='
assert parse_result.shared_access_key_name == None

def test_sb_parse_conn_str_no_keyname(self, **kwargs):
conn_str = 'Endpoint=sb://resourcename.servicebus.windows.net/;SharedAccessKey=THISISATESTKEYXXXXXXXXXXXXXXXXXXXXXXXXXXXX='
with pytest.raises(ValueError) as e:
parse_result = parse_connection_string(conn_str)
assert str(e.value) == 'Connection string must have both SharedAccessKeyName and SharedAccessKey.'

def test_sb_parse_conn_str_no_key(self, **kwargs):
conn_str = 'Endpoint=sb://resourcename.servicebus.windows.net/;SharedAccessKeyName=Test'
with pytest.raises(ValueError) as e:
parse_result = parse_connection_string(conn_str)
assert str(e.value) == 'Connection string must have both SharedAccessKeyName and SharedAccessKey.'