-
Notifications
You must be signed in to change notification settings - Fork 3.3k
[ServiceBus] Support SAS token-via-connection-string auth, and remove ServiceBusSharedKeyCredential export #13627
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
Changes from 2 commits
e666f57
ac20634
902f92d
d1062f6
892b612
e05c349
f3b51fb
f8d8ad1
f83bf4c
df456b6
7937c8f
1934db9
c7ee6f2
78486e7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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=<timestamp>" 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) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not super confident on Have we tested this case -- no expiry provided in signature and we default to a very large number? The concern I have here is that as you know our uamqp is built upon C, whether this would lead to integer overflow on certain platforms, making expiry being some negative value.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wisdom beyond your years. My inclination would be to use some vestige of INT_MAX. I'll poke around, but shout if you know off the cuff the incantation that I want to properly align with whatever is being done in the AMQP level. (Worded more clearly; will I be shooting myself in the foot if I use sys.maxint as far as you know of uamqp?)
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Per our OOB discussion: Writing this test ended up exposing the following uamqp bug Added issue for future investigation, noting this in the test stub that we can reenable once it's fixed. |
||
| 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://<FQDN>/;SharedAccessKeyName=<KeyName>;SharedAccessKey=<KeyValue>" | ||
|
|
@@ -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) | ||
|
KieranBrantnerMagee marked this conversation as resolved.
|
||
|
|
||
|
|
||
| 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) | ||
|
KieranBrantnerMagee marked this conversation as resolved.
Outdated
|
||
|
|
||
| def __enter__(self): | ||
| self._open_with_retry() | ||
| return self | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.