Skip to content

Commit 4254203

Browse files
Rakshith BhyravabhotlaKieranBrantnerMageeyunhaoling
authored
Service Bus Connection String Parser (#14772)
* initial * sol * changes * lint * dict mixin * comments * Update sdk/servicebus/azure-servicebus/CHANGELOG.md Co-authored-by: KieranBrantnerMagee <[email protected]> * lint * change to method * hide shared access key * fix * Apply suggestions from code review Co-authored-by: Adam Ling (MSFT) <[email protected]> * coments * few more changes * tests * add back sky * test * changelog * Update sdk/servicebus/azure-servicebus/CHANGELOG.md Co-authored-by: KieranBrantnerMagee <[email protected]> Co-authored-by: Adam Ling (MSFT) <[email protected]>
1 parent 6f17604 commit 4254203

File tree

4 files changed

+171
-1
lines changed

4 files changed

+171
-1
lines changed

sdk/servicebus/azure-servicebus/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
- `ServiceBusReceiver`: `receive_deferred_messages`, `peek_messages` and `renew_message_lock`
1010
- `ServiceBusSession`: `get_state`, `set_state` and `renew_lock`
1111
* `azure.servicebus.exceptions.ServiceBusError` now inherits from `azure.core.exceptions.AzureError`.
12+
* Added a `parse_connection_string` method which parses a connection string into a properties bag containing its component parts
1213

1314
**Breaking Changes**
1415

sdk/servicebus/azure-servicebus/azure/servicebus/__init__.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@
1515
from ._common.message import ServiceBusMessage, ServiceBusMessageBatch, ServiceBusReceivedMessage
1616
from ._common.constants import ReceiveMode, SubQueue, NEXT_AVAILABLE_SESSION
1717
from ._common.auto_lock_renewer import AutoLockRenewer
18+
from ._common._connection_string_parser import (
19+
parse_connection_string,
20+
ServiceBusConnectionStringProperties
21+
)
1822

1923
TransportType = constants.TransportType
2024

@@ -30,5 +34,7 @@
3034
'ServiceBusSession',
3135
'ServiceBusSender',
3236
'TransportType',
33-
'AutoLockRenewer'
37+
'AutoLockRenewer',
38+
'parse_connection_string',
39+
'ServiceBusConnectionStringProperties'
3440
]
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
# --------------------------------------------------------------------------------------------
2+
# Copyright (c) Microsoft Corporation. All rights reserved.
3+
# Licensed under the MIT License. See License.txt in the project root for license information.
4+
# --------------------------------------------------------------------------------------------
5+
try:
6+
from urllib.parse import urlparse
7+
except ImportError:
8+
from urlparse import urlparse # type: ignore
9+
10+
from azure.servicebus.management._models import DictMixin
11+
12+
class ServiceBusConnectionStringProperties(DictMixin):
13+
"""
14+
Properties of a connection string.
15+
"""
16+
def __init__(self, **kwargs):
17+
self._fully_qualified_namespace = kwargs.pop('fully_qualified_namespace', None)
18+
self._endpoint = kwargs.pop('endpoint', None)
19+
self._entity_path = kwargs.pop('entity_path', None)
20+
self._shared_access_signature = kwargs.pop('shared_access_signature', None)
21+
self._shared_access_key_name = kwargs.pop('shared_access_key_name', None)
22+
self._shared_access_key = kwargs.pop('shared_access_key', None)
23+
24+
@property
25+
def fully_qualified_namespace(self):
26+
"""The fully qualified host name for the Service Bus namespace.
27+
The namespace format is: `<yournamespace>.servicebus.windows.net`.
28+
"""
29+
return self._fully_qualified_namespace
30+
31+
@property
32+
def endpoint(self):
33+
"""The endpoint for the Service Bus resource. In the format sb://<FQDN>/
34+
"""
35+
return self._endpoint
36+
37+
@property
38+
def entity_path(self):
39+
"""Optional. Represents the name of the queue/topic.
40+
"""
41+
return self._entity_path
42+
43+
@property
44+
def shared_access_signature(self):
45+
"""
46+
This can be provided instead of the shared_access_key_name and the shared_access_key.
47+
"""
48+
return self._shared_access_signature
49+
50+
@property
51+
def shared_access_key_name(self):
52+
"""
53+
The name of the shared_access_key. This must be used along with the shared_access_key.
54+
"""
55+
return self._shared_access_key_name
56+
57+
@property
58+
def shared_access_key(self):
59+
"""
60+
The shared_access_key can be used along with the shared_access_key_name as a credential.
61+
"""
62+
return self._shared_access_key
63+
64+
65+
def parse_connection_string(conn_str):
66+
# type(str) -> ServiceBusConnectionStringProperties
67+
"""Parse the connection string into a properties bag containing its component parts.
68+
69+
:param conn_str: The connection string that has to be parsed.
70+
:type conn_str: str
71+
:rtype: ~azure.servicebus.ServiceBusConnectionStringProperties
72+
"""
73+
conn_settings = [s.split("=", 1) for s in conn_str.split(";")]
74+
if any(len(tup) != 2 for tup in conn_settings):
75+
raise ValueError("Connection string is either blank or malformed.")
76+
conn_settings = dict(conn_settings)
77+
shared_access_signature = None
78+
for key, value in conn_settings.items():
79+
if key.lower() == 'sharedaccesssignature':
80+
shared_access_signature = value
81+
shared_access_key = conn_settings.get('SharedAccessKey')
82+
shared_access_key_name = conn_settings.get('SharedAccessKeyName')
83+
if any([shared_access_key, shared_access_key_name]) and not all([shared_access_key, shared_access_key_name]):
84+
raise ValueError("Connection string must have both SharedAccessKeyName and SharedAccessKey.")
85+
if shared_access_signature is not None and shared_access_key is not None:
86+
raise ValueError("Only one of the SharedAccessKey or SharedAccessSignature must be present.")
87+
endpoint = conn_settings.get('Endpoint')
88+
if not endpoint:
89+
raise ValueError("Connection string is either blank or malformed.")
90+
parsed = urlparse(endpoint.rstrip('/'))
91+
if not parsed.netloc:
92+
raise ValueError("Invalid Endpoint on the Connection String.")
93+
namespace = parsed.netloc.strip()
94+
props = {
95+
'fully_qualified_namespace': namespace,
96+
'endpoint': endpoint,
97+
'entity_path': conn_settings.get('EntityPath'),
98+
'shared_access_signature': shared_access_signature,
99+
'shared_access_key_name': shared_access_key_name,
100+
'shared_access_key': shared_access_key
101+
}
102+
return ServiceBusConnectionStringProperties(**props)
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
#-------------------------------------------------------------------------
2+
# Copyright (c) Microsoft Corporation. All rights reserved.
3+
# Licensed under the MIT License. See License.txt in the project root for
4+
# license information.
5+
#--------------------------------------------------------------------------
6+
7+
import os
8+
import pytest
9+
from azure.servicebus import (
10+
ServiceBusConnectionStringProperties,
11+
parse_connection_string,
12+
)
13+
14+
from devtools_testutils import AzureMgmtTestCase
15+
16+
class ServiceBusConnectionStringParserTests(AzureMgmtTestCase):
17+
def test_sb_conn_str_parse_cs(self, **kwargs):
18+
conn_str = 'Endpoint=sb://resourcename.servicebus.windows.net/;SharedAccessKeyName=test;SharedAccessKey=THISISATESTKEYXXXXXXXXXXXXXXXXXXXXXXXXXXXX='
19+
parse_result = parse_connection_string(conn_str)
20+
assert parse_result.endpoint == 'sb://resourcename.servicebus.windows.net/'
21+
assert parse_result.fully_qualified_namespace == 'resourcename.servicebus.windows.net'
22+
assert parse_result.shared_access_key_name == 'test'
23+
assert parse_result.shared_access_key == 'THISISATESTKEYXXXXXXXXXXXXXXXXXXXXXXXXXXXX='
24+
25+
def test_sb_conn_str_parse_sas_and_shared_key(self, **kwargs):
26+
conn_str = 'Endpoint=sb://resourcename.servicebus.windows.net/;SharedAccessKeyName=test;SharedAccessKey=THISISATESTKEYXXXXXXXXXXXXXXXXXXXXXXXXXXXX=;SharedAccessSignature=THISISASASXXXXXXX='
27+
with pytest.raises(ValueError) as e:
28+
parse_result = parse_connection_string(conn_str)
29+
assert str(e.value) == 'Only one of the SharedAccessKey or SharedAccessSignature must be present.'
30+
31+
def test_sb_parse_malformed_conn_str_no_endpoint(self, **kwargs):
32+
conn_str = 'SharedAccessKeyName=test;SharedAccessKey=THISISATESTKEYXXXXXXXXXXXXXXXXXXXXXXXXXXXX='
33+
with pytest.raises(ValueError) as e:
34+
parse_result = parse_connection_string(conn_str)
35+
assert str(e.value) == 'Connection string is either blank or malformed.'
36+
37+
def test_sb_parse_malformed_conn_str_no_netloc(self, **kwargs):
38+
conn_str = 'Endpoint=MALFORMED;SharedAccessKeyName=test;SharedAccessKey=THISISATESTKEYXXXXXXXXXXXXXXXXXXXXXXXXXXXX='
39+
with pytest.raises(ValueError) as e:
40+
parse_result = parse_connection_string(conn_str)
41+
assert str(e.value) == 'Invalid Endpoint on the Connection String.'
42+
43+
def test_sb_parse_conn_str_sas(self, **kwargs):
44+
conn_str = 'Endpoint=sb://resourcename.servicebus.windows.net/;SharedAccessSignature=THISISATESTKEYXXXXXXXXXXXXXXXXXXXXXXXXXXXX='
45+
parse_result = parse_connection_string(conn_str)
46+
assert parse_result.endpoint == 'sb://resourcename.servicebus.windows.net/'
47+
assert parse_result.fully_qualified_namespace == 'resourcename.servicebus.windows.net'
48+
assert parse_result.shared_access_signature == 'THISISATESTKEYXXXXXXXXXXXXXXXXXXXXXXXXXXXX='
49+
assert parse_result.shared_access_key_name == None
50+
51+
def test_sb_parse_conn_str_no_keyname(self, **kwargs):
52+
conn_str = 'Endpoint=sb://resourcename.servicebus.windows.net/;SharedAccessKey=THISISATESTKEYXXXXXXXXXXXXXXXXXXXXXXXXXXXX='
53+
with pytest.raises(ValueError) as e:
54+
parse_result = parse_connection_string(conn_str)
55+
assert str(e.value) == 'Connection string must have both SharedAccessKeyName and SharedAccessKey.'
56+
57+
def test_sb_parse_conn_str_no_key(self, **kwargs):
58+
conn_str = 'Endpoint=sb://resourcename.servicebus.windows.net/;SharedAccessKeyName=Test'
59+
with pytest.raises(ValueError) as e:
60+
parse_result = parse_connection_string(conn_str)
61+
assert str(e.value) == 'Connection string must have both SharedAccessKeyName and SharedAccessKey.'

0 commit comments

Comments
 (0)