diff --git a/sdk/storage/azure-storage-blob-changefeed/azure/storage/blob/changefeed/_change_feed_client.py b/sdk/storage/azure-storage-blob-changefeed/azure/storage/blob/changefeed/_change_feed_client.py index 429969905d93..8bda21455e88 100644 --- a/sdk/storage/azure-storage-blob-changefeed/azure/storage/blob/changefeed/_change_feed_client.py +++ b/sdk/storage/azure-storage-blob-changefeed/azure/storage/blob/changefeed/_change_feed_client.py @@ -24,9 +24,11 @@ class ChangeFeedClient(object): # pylint: disable=too-many-public-methods The URI to the storage account. :param credential: The credentials with which to authenticate. This is optional if the - account URL already has a SAS token. The value can be a SAS token string, an account + account URL already has a SAS token. The value can be a SAS token string, + an instance of a AzureSasCredential from azure.core.credentials, an account shared access key, or an instance of a TokenCredentials class from azure.identity. - If the URL already has a SAS token, specifying an explicit credential will take priority. + If the resource URI already contains a SAS token, this will be ignored in favor of an explicit credential + - except in the case of AzureSasCredential, where the conflicting SAS tokens will raise a ValueError. :keyword str secondary_hostname: The hostname of the secondary endpoint. @@ -60,7 +62,8 @@ def from_connection_string( :param credential: The credentials with which to authenticate. This is optional if the account URL already has a SAS token, or the connection string already has shared - access key values. The value can be a SAS token string, an account shared access + access key values. The value can be a SAS token string, + an instance of a AzureSasCredential from azure.core.credentials, an account shared access key, or an instance of a TokenCredentials class from azure.identity. Credentials provided here will take precedence over those in the connection string. :returns: A change feed client. diff --git a/sdk/storage/azure-storage-blob/CHANGELOG.md b/sdk/storage/azure-storage-blob/CHANGELOG.md index 51b5ba55f6df..6aa8b1b4fecc 100644 --- a/sdk/storage/azure-storage-blob/CHANGELOG.md +++ b/sdk/storage/azure-storage-blob/CHANGELOG.md @@ -1,7 +1,8 @@ # Release History ## 12.7.0b2 (Unreleased) - +**New features** +- Added support for `AzureSasCredential` to allow SAS rotation in long living clients. ## 12.7.0b1 (2020-12-07) **New features** diff --git a/sdk/storage/azure-storage-blob/azure/storage/blob/__init__.py b/sdk/storage/azure-storage-blob/azure/storage/blob/__init__.py index 937d74b54037..9164961ea10a 100644 --- a/sdk/storage/azure-storage-blob/azure/storage/blob/__init__.py +++ b/sdk/storage/azure-storage-blob/azure/storage/blob/__init__.py @@ -81,9 +81,11 @@ def upload_blob_to_url( :type data: bytes or str or Iterable :param credential: The credentials with which to authenticate. This is optional if the - blob URL already has a SAS token. The value can be a SAS token string, an account + blob URL already has a SAS token. The value can be a SAS token string, + an instance of a AzureSasCredential from azure.core.credentials, an account shared access key, or an instance of a TokenCredentials class from azure.identity. - If the URL already has a SAS token, specifying an explicit credential will take priority. + If the resource URI already contains a SAS token, this will be ignored in favor of an explicit credential + - except in the case of AzureSasCredential, where the conflicting SAS tokens will raise a ValueError. :keyword bool overwrite: Whether the blob to be uploaded should overwrite the current data. If True, upload_blob_to_url will overwrite any existing data. If set to False, the @@ -136,8 +138,10 @@ def download_blob_from_url( :param credential: The credentials with which to authenticate. This is optional if the blob URL already has a SAS token or the blob is public. The value can be a SAS token string, + an instance of a AzureSasCredential from azure.core.credentials, an account shared access key, or an instance of a TokenCredentials class from azure.identity. - If the URL already has a SAS token, specifying an explicit credential will take priority. + If the resource URI already contains a SAS token, this will be ignored in favor of an explicit credential + - except in the case of AzureSasCredential, where the conflicting SAS tokens will raise a ValueError. :keyword bool overwrite: Whether the local file should be overwritten if it already exists. The default value is `False` - in which case a ValueError will be raised if the file already exists. If set to diff --git a/sdk/storage/azure-storage-blob/azure/storage/blob/_blob_client.py b/sdk/storage/azure-storage-blob/azure/storage/blob/_blob_client.py index 4aab92da823d..56ec5f5fa221 100644 --- a/sdk/storage/azure-storage-blob/azure/storage/blob/_blob_client.py +++ b/sdk/storage/azure-storage-blob/azure/storage/blob/_blob_client.py @@ -88,9 +88,11 @@ class BlobClient(StorageAccountHostsMixin): # pylint: disable=too-many-public-m or the response returned from :func:`create_snapshot`. :param credential: The credentials with which to authenticate. This is optional if the - account URL already has a SAS token. The value can be a SAS token string, an account + account URL already has a SAS token. The value can be a SAS token string, + an instance of a AzureSasCredential from azure.core.credentials, an account shared access key, or an instance of a TokenCredentials class from azure.identity. - If the URL already has a SAS token, specifying an explicit credential will take priority. + If the resource URI already contains a SAS token, this will be ignored in favor of an explicit credential + - except in the case of AzureSasCredential, where the conflicting SAS tokens will raise a ValueError. :keyword str api_version: The Storage API version to use for requests. Default value is '2019-07-07'. Setting to an older version may result in reduced feature compatibility. @@ -202,9 +204,11 @@ def from_blob_url(cls, blob_url, credential=None, snapshot=None, **kwargs): :param credential: The credentials with which to authenticate. This is optional if the account URL already has a SAS token, or the connection string already has shared - access key values. The value can be a SAS token string, an account shared access + access key values. The value can be a SAS token string, + an instance of a AzureSasCredential from azure.core.credentials, an account shared access key, or an instance of a TokenCredentials class from azure.identity. - Credentials provided here will take precedence over those in the connection string. + If the resource URI already contains a SAS token, this will be ignored in favor of an explicit credential + - except in the case of AzureSasCredential, where the conflicting SAS tokens will raise a ValueError. :param str snapshot: The optional blob snapshot on which to operate. This can be the snapshot ID string or the response returned from :func:`create_snapshot`. If specified, this will override @@ -281,7 +285,8 @@ def from_connection_string( :param credential: The credentials with which to authenticate. This is optional if the account URL already has a SAS token, or the connection string already has shared - access key values. The value can be a SAS token string, an account shared access + access key values. The value can be a SAS token string, + an instance of a AzureSasCredential from azure.core.credentials, an account shared access key, or an instance of a TokenCredentials class from azure.identity. Credentials provided here will take precedence over those in the connection string. :returns: A Blob client. diff --git a/sdk/storage/azure-storage-blob/azure/storage/blob/_blob_service_client.py b/sdk/storage/azure-storage-blob/azure/storage/blob/_blob_service_client.py index 47308da2c33b..8658363f8248 100644 --- a/sdk/storage/azure-storage-blob/azure/storage/blob/_blob_service_client.py +++ b/sdk/storage/azure-storage-blob/azure/storage/blob/_blob_service_client.py @@ -66,9 +66,11 @@ class BlobServiceClient(StorageAccountHostsMixin): authenticated with a SAS token. :param credential: The credentials with which to authenticate. This is optional if the - account URL already has a SAS token. The value can be a SAS token string, an account + account URL already has a SAS token. The value can be a SAS token string, + an instance of a AzureSasCredential from azure.core.credentials, an account shared access key, or an instance of a TokenCredentials class from azure.identity. - If the URL already has a SAS token, specifying an explicit credential will take priority. + If the resource URI already contains a SAS token, this will be ignored in favor of an explicit credential + - except in the case of AzureSasCredential, where the conflicting SAS tokens will raise a ValueError. :keyword str api_version: The Storage API version to use for requests. Default value is '2019-07-07'. Setting to an older version may result in reduced feature compatibility. @@ -149,7 +151,8 @@ def from_connection_string( :param credential: The credentials with which to authenticate. This is optional if the account URL already has a SAS token, or the connection string already has shared - access key values. The value can be a SAS token string, an account shared access + access key values. The value can be a SAS token string, + an instance of a AzureSasCredential from azure.core.credentials, an account shared access key, or an instance of a TokenCredentials class from azure.identity. Credentials provided here will take precedence over those in the connection string. :returns: A Blob service client. diff --git a/sdk/storage/azure-storage-blob/azure/storage/blob/_container_client.py b/sdk/storage/azure-storage-blob/azure/storage/blob/_container_client.py index 8d18fb77c39f..8788d14b704a 100644 --- a/sdk/storage/azure-storage-blob/azure/storage/blob/_container_client.py +++ b/sdk/storage/azure-storage-blob/azure/storage/blob/_container_client.py @@ -84,9 +84,11 @@ class ContainerClient(StorageAccountHostsMixin): :type container_name: str :param credential: The credentials with which to authenticate. This is optional if the - account URL already has a SAS token. The value can be a SAS token string, an account + account URL already has a SAS token. The value can be a SAS token string, + an instance of a AzureSasCredential from azure.core.credentials, an account shared access key, or an instance of a TokenCredentials class from azure.identity. - If the URL already has a SAS token, specifying an explicit credential will take priority. + If the resource URI already contains a SAS token, this will be ignored in favor of an explicit credential + - except in the case of AzureSasCredential, where the conflicting SAS tokens will raise a ValueError. :keyword str api_version: The Storage API version to use for requests. Default value is '2019-07-07'. Setting to an older version may result in reduced feature compatibility. @@ -173,9 +175,11 @@ def from_container_url(cls, container_url, credential=None, **kwargs): :param credential: The credentials with which to authenticate. This is optional if the account URL already has a SAS token, or the connection string already has shared - access key values. The value can be a SAS token string, an account shared access + access key values. The value can be a SAS token string, + an instance of a AzureSasCredential from azure.core.credentials, an account shared access key, or an instance of a TokenCredentials class from azure.identity. - Credentials provided here will take precedence over those in the connection string. + If the resource URI already contains a SAS token, this will be ignored in favor of an explicit credential + - except in the case of AzureSasCredential, where the conflicting SAS tokens will raise a ValueError. :returns: A container client. :rtype: ~azure.storage.blob.ContainerClient """ @@ -219,7 +223,8 @@ def from_connection_string( :param credential: The credentials with which to authenticate. This is optional if the account URL already has a SAS token, or the connection string already has shared - access key values. The value can be a SAS token string, an account shared access + access key values. The value can be a SAS token string, + an instance of a AzureSasCredential from azure.core.credentials, an account shared access key, or an instance of a TokenCredentials class from azure.identity. Credentials provided here will take precedence over those in the connection string. :returns: A container client. diff --git a/sdk/storage/azure-storage-blob/azure/storage/blob/_shared/base_client.py b/sdk/storage/azure-storage-blob/azure/storage/blob/_shared/base_client.py index 3f2d63c36f35..801023d614e9 100644 --- a/sdk/storage/azure-storage-blob/azure/storage/blob/_shared/base_client.py +++ b/sdk/storage/azure-storage-blob/azure/storage/blob/_shared/base_client.py @@ -26,6 +26,7 @@ import six from azure.core.configuration import Configuration +from azure.core.credentials import AzureSasCredential from azure.core.exceptions import HttpResponseError from azure.core.pipeline import Pipeline from azure.core.pipeline.transport import RequestsTransport, HttpTransport @@ -36,7 +37,8 @@ ProxyPolicy, DistributedTracingPolicy, HttpLoggingPolicy, - UserAgentPolicy + UserAgentPolicy, + AzureSasCredentialPolicy ) from .constants import STORAGE_OAUTH_SCOPE, SERVICE_HOST_BASE, CONNECTION_TIMEOUT, READ_TIMEOUT @@ -208,6 +210,9 @@ def _format_query_string(self, sas_token, credential, snapshot=None, share_snaps query_str += "snapshot={}&".format(self.snapshot) if share_snapshot: query_str += "sharesnapshot={}&".format(self.snapshot) + if sas_token and isinstance(credential, AzureSasCredential): + raise ValueError( + "You cannot use AzureSasCredential when the resource URI also contains a Shared Access Signature.") if sas_token and not credential: query_str += sas_token elif is_credential_sastoken(credential): @@ -222,6 +227,8 @@ def _create_pipeline(self, credential, **kwargs): self._credential_policy = BearerTokenCredentialPolicy(credential, STORAGE_OAUTH_SCOPE) elif isinstance(credential, SharedKeyCredentialPolicy): self._credential_policy = credential + elif isinstance(credential, AzureSasCredential): + self._credential_policy = AzureSasCredentialPolicy(credential) elif credential is not None: raise TypeError("Unsupported credential: {}".format(credential)) diff --git a/sdk/storage/azure-storage-blob/azure/storage/blob/_shared/base_client_async.py b/sdk/storage/azure-storage-blob/azure/storage/blob/_shared/base_client_async.py index a45d635ee2a8..6ce19c753a5a 100644 --- a/sdk/storage/azure-storage-blob/azure/storage/blob/_shared/base_client_async.py +++ b/sdk/storage/azure-storage-blob/azure/storage/blob/_shared/base_client_async.py @@ -9,6 +9,8 @@ TYPE_CHECKING ) import logging + +from azure.core.credentials import AzureSasCredential from azure.core.pipeline import AsyncPipeline from azure.core.async_paging import AsyncList from azure.core.exceptions import HttpResponseError @@ -18,6 +20,7 @@ AsyncRedirectPolicy, DistributedTracingPolicy, HttpLoggingPolicy, + AzureSasCredentialPolicy, ) from azure.core.pipeline.transport import AsyncHttpTransport @@ -70,6 +73,8 @@ def _create_pipeline(self, credential, **kwargs): self._credential_policy = AsyncBearerTokenCredentialPolicy(credential, STORAGE_OAUTH_SCOPE) elif isinstance(credential, SharedKeyCredentialPolicy): self._credential_policy = credential + elif isinstance(credential, AzureSasCredential): + self._credential_policy = AzureSasCredentialPolicy(credential) elif credential is not None: raise TypeError("Unsupported credential: {}".format(credential)) config = kwargs.get('_configuration') or create_configuration(**kwargs) diff --git a/sdk/storage/azure-storage-blob/azure/storage/blob/aio/__init__.py b/sdk/storage/azure-storage-blob/azure/storage/blob/aio/__init__.py index 247f39e1ffde..33c10319aaa0 100644 --- a/sdk/storage/azure-storage-blob/azure/storage/blob/aio/__init__.py +++ b/sdk/storage/azure-storage-blob/azure/storage/blob/aio/__init__.py @@ -32,9 +32,11 @@ async def upload_blob_to_url( :type data: bytes or str or Iterable :param credential: The credentials with which to authenticate. This is optional if the - blob URL already has a SAS token. The value can be a SAS token string, an account + blob URL already has a SAS token. The value can be a SAS token string, + an instance of a AzureSasCredential from azure.core.credentials, an account shared access key, or an instance of a TokenCredentials class from azure.identity. - If the URL already has a SAS token, specifying an explicit credential will take priority. + If the resource URI already contains a SAS token, this will be ignored in favor of an explicit credential + - except in the case of AzureSasCredential, where the conflicting SAS tokens will raise a ValueError. :keyword bool overwrite: Whether the blob to be uploaded should overwrite the current data. If True, upload_blob_to_url will overwrite any existing data. If set to False, the @@ -87,8 +89,10 @@ async def download_blob_from_url( :param credential: The credentials with which to authenticate. This is optional if the blob URL already has a SAS token or the blob is public. The value can be a SAS token string, + an instance of a AzureSasCredential from azure.core.credentials, an account shared access key, or an instance of a TokenCredentials class from azure.identity. - If the URL already has a SAS token, specifying an explicit credential will take priority. + If the resource URI already contains a SAS token, this will be ignored in favor of an explicit credential + - except in the case of AzureSasCredential, where the conflicting SAS tokens will raise a ValueError. :keyword bool overwrite: Whether the local file should be overwritten if it already exists. The default value is `False` - in which case a ValueError will be raised if the file already exists. If set to diff --git a/sdk/storage/azure-storage-blob/azure/storage/blob/aio/_blob_client_async.py b/sdk/storage/azure-storage-blob/azure/storage/blob/aio/_blob_client_async.py index dab877a168a1..1e1ae551339f 100644 --- a/sdk/storage/azure-storage-blob/azure/storage/blob/aio/_blob_client_async.py +++ b/sdk/storage/azure-storage-blob/azure/storage/blob/aio/_blob_client_async.py @@ -57,9 +57,11 @@ class BlobClient(AsyncStorageAccountHostsMixin, BlobClientBase): # pylint: disa or the response returned from :func:`create_snapshot`. :param credential: The credentials with which to authenticate. This is optional if the - account URL already has a SAS token. The value can be a SAS token string, an account + account URL already has a SAS token. The value can be a SAS token string, + an instance of a AzureSasCredential from azure.core.credentials, an account shared access key, or an instance of a TokenCredentials class from azure.identity. - If the URL already has a SAS token, specifying an explicit credential will take priority. + If the resource URI already contains a SAS token, this will be ignored in favor of an explicit credential + - except in the case of AzureSasCredential, where the conflicting SAS tokens will raise a ValueError. :keyword str api_version: The Storage API version to use for requests. Default value is '2019-07-07'. Setting to an older version may result in reduced feature compatibility. diff --git a/sdk/storage/azure-storage-blob/azure/storage/blob/aio/_blob_service_client_async.py b/sdk/storage/azure-storage-blob/azure/storage/blob/aio/_blob_service_client_async.py index 42a1c0c6e963..4e91743c38be 100644 --- a/sdk/storage/azure-storage-blob/azure/storage/blob/aio/_blob_service_client_async.py +++ b/sdk/storage/azure-storage-blob/azure/storage/blob/aio/_blob_service_client_async.py @@ -61,9 +61,11 @@ class BlobServiceClient(AsyncStorageAccountHostsMixin, BlobServiceClientBase): authenticated with a SAS token. :param credential: The credentials with which to authenticate. This is optional if the - account URL already has a SAS token. The value can be a SAS token string, an account + account URL already has a SAS token. The value can be a SAS token string, + an instance of a AzureSasCredential from azure.core.credentials, an account shared access key, or an instance of a TokenCredentials class from azure.identity. - If the URL already has a SAS token, specifying an explicit credential will take priority. + If the resource URI already contains a SAS token, this will be ignored in favor of an explicit credential + - except in the case of AzureSasCredential, where the conflicting SAS tokens will raise a ValueError. :keyword str api_version: The Storage API version to use for requests. Default value is '2019-07-07'. Setting to an older version may result in reduced feature compatibility. diff --git a/sdk/storage/azure-storage-blob/azure/storage/blob/aio/_container_client_async.py b/sdk/storage/azure-storage-blob/azure/storage/blob/aio/_container_client_async.py index 63e1d537a906..e26fe2388539 100644 --- a/sdk/storage/azure-storage-blob/azure/storage/blob/aio/_container_client_async.py +++ b/sdk/storage/azure-storage-blob/azure/storage/blob/aio/_container_client_async.py @@ -60,9 +60,11 @@ class ContainerClient(AsyncStorageAccountHostsMixin, ContainerClientBase): :type container_name: str :param credential: The credentials with which to authenticate. This is optional if the - account URL already has a SAS token. The value can be a SAS token string, an account + account URL already has a SAS token. The value can be a SAS token string, + an instance of a AzureSasCredential from azure.core.credentials, an account shared access key, or an instance of a TokenCredentials class from azure.identity. - If the URL already has a SAS token, specifying an explicit credential will take priority. + If the resource URI already contains a SAS token, this will be ignored in favor of an explicit credential + - except in the case of AzureSasCredential, where the conflicting SAS tokens will raise a ValueError. :keyword str api_version: The Storage API version to use for requests. Default value is '2019-07-07'. Setting to an older version may result in reduced feature compatibility. diff --git a/sdk/storage/azure-storage-blob/setup.py b/sdk/storage/azure-storage-blob/setup.py index 460193ff88e8..4d3edd301d13 100644 --- a/sdk/storage/azure-storage-blob/setup.py +++ b/sdk/storage/azure-storage-blob/setup.py @@ -93,7 +93,7 @@ 'tests.common' ]), install_requires=[ - "azure-core<2.0.0,>=1.9.0", + "azure-core<2.0.0,>=1.10.0", "msrest>=0.6.10", "cryptography>=2.1.4" ], diff --git a/sdk/storage/azure-storage-blob/tests/test_blob_client.py b/sdk/storage/azure-storage-blob/tests/test_blob_client.py index c82ba195c256..90aa16c5b39e 100644 --- a/sdk/storage/azure-storage-blob/tests/test_blob_client.py +++ b/sdk/storage/azure-storage-blob/tests/test_blob_client.py @@ -7,6 +7,7 @@ import pytest import platform +from azure.core.credentials import AzureSasCredential from azure.core.exceptions import AzureError from azure.storage.blob import ( VERSION, @@ -100,6 +101,34 @@ def test_create_service_with_sas(self, resource_group, location, storage_account self.assertTrue(service.url.endswith(self.sas_token)) self.assertIsNone(service.credential) + @GlobalStorageAccountPreparer() + def test_create_service_with_sas_credential(self, resource_group, location, storage_account, storage_account_key): + # Arrange + sas_credential = AzureSasCredential(self.sas_token) + + for service_type in SERVICES: + # Act + service = service_type( + self.account_url(storage_account, "blob"), credential=sas_credential, container_name='foo', blob_name='bar') + + # Assert + self.assertIsNotNone(service) + self.assertEqual(service.account_name, storage_account.name) + self.assertTrue(service.url.startswith('https://' + storage_account.name + '.blob.core.windows.net')) + self.assertFalse(service.url.endswith(self.sas_token)) + self.assertEqual(service.credential, sas_credential) + + @GlobalStorageAccountPreparer() + def test_create_service_with_sas_credential_url_raises_if_sas_is_in_uri(self, resource_group, location, storage_account, storage_account_key): + # Arrange + sas_credential = AzureSasCredential(self.sas_token) + + for service_type in SERVICES: + # Act + with self.assertRaises(ValueError): + service = service_type( + self.account_url(storage_account, "blob") + "?sig=foo", credential=sas_credential, container_name='foo', blob_name='bar') + @GlobalStorageAccountPreparer() def test_create_service_with_token(self, resource_group, location, storage_account, storage_account_key): for service_type in SERVICES: diff --git a/sdk/storage/azure-storage-blob/tests/test_blob_client_async.py b/sdk/storage/azure-storage-blob/tests/test_blob_client_async.py index 0de666766538..9c8f612cde28 100644 --- a/sdk/storage/azure-storage-blob/tests/test_blob_client_async.py +++ b/sdk/storage/azure-storage-blob/tests/test_blob_client_async.py @@ -8,6 +8,7 @@ import platform import asyncio +from azure.core.credentials import AzureSasCredential from azure.storage.blob import VERSION from azure.storage.blob.aio import ( BlobServiceClient, @@ -100,6 +101,34 @@ def test_create_service_with_sas_async(self, resource_group, location, storage_a self.assertTrue(service.url.endswith(self.sas_token)) self.assertIsNone(service.credential) + @GlobalStorageAccountPreparer() + def test_create_service_with_sas_credential_async(self, resource_group, location, storage_account, storage_account_key): + # Arrange + sas_credential = AzureSasCredential(self.sas_token) + + for service_type in SERVICES: + # Act + service = service_type( + self.account_url(storage_account, "blob"), credential=sas_credential, container_name='foo', blob_name='bar') + + # Assert + self.assertIsNotNone(service) + self.assertEqual(service.account_name, storage_account.name) + self.assertTrue(service.url.startswith('https://' + storage_account.name + '.blob.core.windows.net')) + self.assertFalse(service.url.endswith(self.sas_token)) + self.assertEqual(service.credential, sas_credential) + + @GlobalStorageAccountPreparer() + def test_create_service_with_sas_credential_url_raises_if_sas_is_in_uri_async(self, resource_group, location, storage_account, storage_account_key): + # Arrange + sas_credential = AzureSasCredential(self.sas_token) + + for service_type in SERVICES: + # Act + with self.assertRaises(ValueError): + service = service_type( + self.account_url(storage_account, "blob") + "?sig=foo", credential=sas_credential, container_name='foo', blob_name='bar') + @GlobalStorageAccountPreparer() def test_create_service_with_token_async(self, resource_group, location, storage_account, storage_account_key): for service_type in SERVICES: diff --git a/sdk/storage/azure-storage-blob/tests/test_common_blob.py b/sdk/storage/azure-storage-blob/tests/test_common_blob.py index ea08616a3cea..5054cdf59358 100644 --- a/sdk/storage/azure-storage-blob/tests/test_common_blob.py +++ b/sdk/storage/azure-storage-blob/tests/test_common_blob.py @@ -15,6 +15,7 @@ from datetime import datetime, timedelta from azure.core import MatchConditions +from azure.core.credentials import AzureSasCredential from azure.core.exceptions import ( HttpResponseError, ResourceNotFoundError, @@ -1710,6 +1711,34 @@ def test_account_sas(self, resource_group, location, storage_account, storage_ac self.assertEqual(self.byte_data, blob_response.content) self.assertTrue(container_response.ok) + @pytest.mark.live_test_only + @GlobalStorageAccountPreparer() + def test_account_sas_credential(self, resource_group, location, storage_account, storage_account_key): + # SAS URL is calculated from storage key, so this test runs live only + + self._setup(storage_account, storage_account_key) + blob_name = self._create_block_blob() + + token = generate_account_sas( + self.bsc.account_name, + self.bsc.credential.account_key, + ResourceTypes(container=True, object=True), + AccountSasPermissions(read=True), + datetime.utcnow() + timedelta(hours=1), + ) + + # Act + blob = BlobClient( + self.bsc.url, container_name=self.container_name, blob_name=blob_name, credential=AzureSasCredential(token)) + container = ContainerClient( + self.bsc.url, container_name=self.container_name, credential=AzureSasCredential(token)) + blob_properties = blob.get_blob_properties() + container_properties = container.get_container_properties() + + # Assert + self.assertEqual(blob_name, blob_properties.name) + self.assertEqual(self.container_name, container_properties.name) + @GlobalStorageAccountPreparer() def test_get_user_delegation_key(self, resource_group, location, storage_account, storage_account_key): # Act diff --git a/sdk/storage/azure-storage-blob/tests/test_common_blob_async.py b/sdk/storage/azure-storage-blob/tests/test_common_blob_async.py index fcaefbfcfe42..5e1222c55c61 100644 --- a/sdk/storage/azure-storage-blob/tests/test_common_blob_async.py +++ b/sdk/storage/azure-storage-blob/tests/test_common_blob_async.py @@ -17,6 +17,7 @@ from datetime import datetime, timedelta from azure.core import MatchConditions +from azure.core.credentials import AzureSasCredential from azure.core.exceptions import ( HttpResponseError, ResourceNotFoundError, @@ -1868,6 +1869,34 @@ async def test_account_sas(self, resource_group, location, storage_account, stor self.assertEqual(self.byte_data, blob_response.content) self.assertTrue(container_response.ok) + @pytest.mark.live_test_only + @GlobalStorageAccountPreparer() + async def test_account_sas_credential(self, resource_group, location, storage_account, storage_account_key): + # SAS URL is calculated from storage key, so this test runs live only + + await self._setup(storage_account, storage_account_key) + blob_name = await self._create_block_blob() + + token = generate_account_sas( + self.bsc.account_name, + self.bsc.credential.account_key, + ResourceTypes(container=True, object=True), + AccountSasPermissions(read=True), + datetime.utcnow() + timedelta(hours=1), + ) + + # Act + blob = BlobClient( + self.bsc.url, container_name=self.container_name, blob_name=blob_name, credential=AzureSasCredential(token)) + container = ContainerClient( + self.bsc.url, container_name=self.container_name, credential=AzureSasCredential(token)) + blob_properties = await blob.get_blob_properties() + container_properties = await container.get_container_properties() + + # Assert + self.assertEqual(blob_name, blob_properties.name) + self.assertEqual(self.container_name, container_properties.name) + @pytest.mark.live_test_only @GlobalStorageAccountPreparer() @AsyncStorageTestCase.await_prepared_test diff --git a/sdk/storage/azure-storage-file-datalake/CHANGELOG.md b/sdk/storage/azure-storage-file-datalake/CHANGELOG.md index 159e5011efb2..c2e5e3a2b9c1 100644 --- a/sdk/storage/azure-storage-file-datalake/CHANGELOG.md +++ b/sdk/storage/azure-storage-file-datalake/CHANGELOG.md @@ -1,6 +1,7 @@ # Release History ## 12.2.1 (Unreleased) - +**New features** +- Added support for `AzureSasCredential` to allow SAS rotation in long living clients. ## 12.2.0 (2020-11-10) **Stable release of preview features** diff --git a/sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/_data_lake_directory_client.py b/sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/_data_lake_directory_client.py index c9a201c7b588..d23f24552dbe 100644 --- a/sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/_data_lake_directory_client.py +++ b/sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/_data_lake_directory_client.py @@ -37,9 +37,11 @@ class DataLakeDirectoryClient(PathClient): :type directory_name: str :param credential: The credentials with which to authenticate. This is optional if the - account URL already has a SAS token. The value can be a SAS token string, and account + account URL already has a SAS token. The value can be a SAS token string, + an instance of a AzureSasCredential from azure.core.credentials, and account shared access key, or an instance of a TokenCredentials class from azure.identity. - If the URL already has a SAS token, specifying an explicit credential will take priority. + If the resource URI already contains a SAS token, this will be ignored in favor of an explicit credential + - except in the case of AzureSasCredential, where the conflicting SAS tokens will raise a ValueError. .. admonition:: Example: @@ -83,7 +85,8 @@ def from_connection_string( :param credential: The credentials with which to authenticate. This is optional if the account URL already has a SAS token, or the connection string already has shared - access key values. The value can be a SAS token string, and account shared access + access key values. The value can be a SAS token string, + an instance of a AzureSasCredential from azure.core.credentials, and account shared access key, or an instance of a TokenCredentials class from azure.identity. Credentials provided here will take precedence over those in the connection string. :return a DataLakeDirectoryClient diff --git a/sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/_data_lake_file_client.py b/sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/_data_lake_file_client.py index 5b0d757fa40e..d314712109c0 100644 --- a/sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/_data_lake_file_client.py +++ b/sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/_data_lake_file_client.py @@ -47,9 +47,11 @@ class DataLakeFileClient(PathClient): :type file_path: str :param credential: The credentials with which to authenticate. This is optional if the - account URL already has a SAS token. The value can be a SAS token string, and account + account URL already has a SAS token. The value can be a SAS token string, + an instance of a AzureSasCredential from azure.core.credentials, an account shared access key, or an instance of a TokenCredentials class from azure.identity. - If the URL already has a SAS token, specifying an explicit credential will take priority. + If the resource URI already contains a SAS token, this will be ignored in favor of an explicit credential + - except in the case of AzureSasCredential, where the conflicting SAS tokens will raise a ValueError. .. admonition:: Example: @@ -93,7 +95,8 @@ def from_connection_string( :param credential: The credentials with which to authenticate. This is optional if the account URL already has a SAS token, or the connection string already has shared - access key values. The value can be a SAS token string, and account shared access + access key values. The value can be a SAS token string, + an instance of a AzureSasCredential from azure.core.credentials, an account shared access key, or an instance of a TokenCredentials class from azure.identity. Credentials provided here will take precedence over those in the connection string. :return a DataLakeFileClient diff --git a/sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/_data_lake_service_client.py b/sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/_data_lake_service_client.py index bb04c6790b35..c97ae67c8763 100644 --- a/sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/_data_lake_service_client.py +++ b/sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/_data_lake_service_client.py @@ -41,9 +41,11 @@ class DataLakeServiceClient(StorageAccountHostsMixin): authenticated with a SAS token. :param credential: The credentials with which to authenticate. This is optional if the - account URL already has a SAS token. The value can be a SAS token string, and account + account URL already has a SAS token. The value can be a SAS token string, + an instance of a AzureSasCredential from azure.core.credentials, an account shared access key, or an instance of a TokenCredentials class from azure.identity. - If the URL already has a SAS token, specifying an explicit credential will take priority. + If the resource URI already contains a SAS token, this will be ignored in favor of an explicit credential + - except in the case of AzureSasCredential, where the conflicting SAS tokens will raise a ValueError. .. admonition:: Example: @@ -122,7 +124,8 @@ def from_connection_string( :param credential: The credentials with which to authenticate. This is optional if the account URL already has a SAS token, or the connection string already has shared - access key values. The value can be a SAS token string, and account shared access + access key values. The value can be a SAS token string, + an instance of a AzureSasCredential from azure.core.credentials, an account shared access key, or an instance of a TokenCredentials class from azure.identity. Credentials provided here will take precedence over those in the connection string. :return a DataLakeServiceClient diff --git a/sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/_file_system_client.py b/sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/_file_system_client.py index 48acc59984b9..ba01aaf8a16e 100644 --- a/sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/_file_system_client.py +++ b/sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/_file_system_client.py @@ -45,9 +45,11 @@ class FileSystemClient(StorageAccountHostsMixin): :type file_system_name: str :param credential: The credentials with which to authenticate. This is optional if the - account URL already has a SAS token. The value can be a SAS token string, and account + account URL already has a SAS token. The value can be a SAS token string, + an instance of a AzureSasCredential from azure.core.credentials, an account shared access key, or an instance of a TokenCredentials class from azure.identity. - If the URL already has a SAS token, specifying an explicit credential will take priority. + If the resource URI already contains a SAS token, this will be ignored in favor of an explicit credential + - except in the case of AzureSasCredential, where the conflicting SAS tokens will raise a ValueError. .. admonition:: Example: @@ -137,7 +139,8 @@ def from_connection_string( :param credential: The credentials with which to authenticate. This is optional if the account URL already has a SAS token, or the connection string already has shared - access key values. The value can be a SAS token string, and account shared access + access key values. The value can be a SAS token string, + an instance of a AzureSasCredential from azure.core.credentials, an account shared access key, or an instance of a TokenCredentials class from azure.identity. Credentials provided here will take precedence over those in the connection string. :return a FileSystemClient diff --git a/sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/_shared/base_client.py b/sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/_shared/base_client.py index 0483d21c3c33..7b0258f45d0f 100644 --- a/sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/_shared/base_client.py +++ b/sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/_shared/base_client.py @@ -26,6 +26,7 @@ import six from azure.core.configuration import Configuration +from azure.core.credentials import AzureSasCredential from azure.core.exceptions import HttpResponseError from azure.core.pipeline import Pipeline from azure.core.pipeline.transport import RequestsTransport, HttpTransport @@ -36,7 +37,8 @@ ProxyPolicy, DistributedTracingPolicy, HttpLoggingPolicy, - UserAgentPolicy + UserAgentPolicy, + AzureSasCredentialPolicy ) from .constants import STORAGE_OAUTH_SCOPE, SERVICE_HOST_BASE, CONNECTION_TIMEOUT, READ_TIMEOUT @@ -208,6 +210,9 @@ def _format_query_string(self, sas_token, credential, snapshot=None, share_snaps query_str += "snapshot={}&".format(self.snapshot) if share_snapshot: query_str += "sharesnapshot={}&".format(self.snapshot) + if sas_token and isinstance(credential, AzureSasCredential): + raise ValueError( + "You cannot use AzureSasCredential when the resource URI also contains a Shared Access Signature.") if sas_token and not credential: query_str += sas_token elif is_credential_sastoken(credential): @@ -222,6 +227,8 @@ def _create_pipeline(self, credential, **kwargs): self._credential_policy = BearerTokenCredentialPolicy(credential, STORAGE_OAUTH_SCOPE) elif isinstance(credential, SharedKeyCredentialPolicy): self._credential_policy = credential + elif isinstance(credential, AzureSasCredential): + self._credential_policy = AzureSasCredentialPolicy(credential) elif credential is not None: raise TypeError("Unsupported credential: {}".format(credential)) diff --git a/sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/_shared/base_client_async.py b/sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/_shared/base_client_async.py index c1d860a93927..091c350b4896 100644 --- a/sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/_shared/base_client_async.py +++ b/sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/_shared/base_client_async.py @@ -9,6 +9,8 @@ TYPE_CHECKING ) import logging + +from azure.core.credentials import AzureSasCredential from azure.core.pipeline import AsyncPipeline from azure.core.async_paging import AsyncList from azure.core.exceptions import HttpResponseError @@ -18,6 +20,7 @@ AsyncRedirectPolicy, DistributedTracingPolicy, HttpLoggingPolicy, + AzureSasCredentialPolicy, ) from azure.core.pipeline.transport import AsyncHttpTransport @@ -70,6 +73,8 @@ def _create_pipeline(self, credential, **kwargs): self._credential_policy = AsyncBearerTokenCredentialPolicy(credential, STORAGE_OAUTH_SCOPE) elif isinstance(credential, SharedKeyCredentialPolicy): self._credential_policy = credential + elif isinstance(credential, AzureSasCredential): + self._credential_policy = AzureSasCredentialPolicy(credential) elif credential is not None: raise TypeError("Unsupported credential: {}".format(credential)) config = kwargs.get('_configuration') or create_configuration(**kwargs) diff --git a/sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/aio/_data_lake_directory_client_async.py b/sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/aio/_data_lake_directory_client_async.py index 16c396646fb6..3b399d6fd37b 100644 --- a/sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/aio/_data_lake_directory_client_async.py +++ b/sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/aio/_data_lake_directory_client_async.py @@ -39,9 +39,11 @@ class DataLakeDirectoryClient(PathClient, DataLakeDirectoryClientBase): :type directory_name: str :param credential: The credentials with which to authenticate. This is optional if the - account URL already has a SAS token. The value can be a SAS token string, and account + account URL already has a SAS token. The value can be a SAS token string, + an instance of a AzureSasCredential from azure.core.credentials, an account shared access key, or an instance of a TokenCredentials class from azure.identity. - If the URL already has a SAS token, specifying an explicit credential will take priority. + If the resource URI already contains a SAS token, this will be ignored in favor of an explicit credential + - except in the case of AzureSasCredential, where the conflicting SAS tokens will raise a ValueError. .. admonition:: Example: diff --git a/sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/aio/_data_lake_file_client_async.py b/sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/aio/_data_lake_file_client_async.py index 3a4e9c862da8..9bbc7c267149 100644 --- a/sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/aio/_data_lake_file_client_async.py +++ b/sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/aio/_data_lake_file_client_async.py @@ -40,9 +40,11 @@ class DataLakeFileClient(PathClient, DataLakeFileClientBase): :type file_path: str :param credential: The credentials with which to authenticate. This is optional if the - account URL already has a SAS token. The value can be a SAS token string, and account + account URL already has a SAS token. The value can be a SAS token string, + an instance of a AzureSasCredential from azure.core.credentials, an account shared access key, or an instance of a TokenCredentials class from azure.identity. - If the URL already has a SAS token, specifying an explicit credential will take priority. + If the resource URI already contains a SAS token, this will be ignored in favor of an explicit credential + - except in the case of AzureSasCredential, where the conflicting SAS tokens will raise a ValueError. .. admonition:: Example: diff --git a/sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/aio/_data_lake_service_client_async.py b/sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/aio/_data_lake_service_client_async.py index 498a70c4a61a..facaba2f9bc7 100644 --- a/sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/aio/_data_lake_service_client_async.py +++ b/sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/aio/_data_lake_service_client_async.py @@ -40,9 +40,11 @@ class DataLakeServiceClient(AsyncStorageAccountHostsMixin, DataLakeServiceClient authenticated with a SAS token. :param credential: The credentials with which to authenticate. This is optional if the - account URL already has a SAS token. The value can be a SAS token string, and account + account URL already has a SAS token. The value can be a SAS token string, + an instance of a AzureSasCredential from azure.core.credentials, an account shared access key, or an instance of a TokenCredentials class from azure.identity. - If the URL already has a SAS token, specifying an explicit credential will take priority. + If the resource URI already contains a SAS token, this will be ignored in favor of an explicit credential + - except in the case of AzureSasCredential, where the conflicting SAS tokens will raise a ValueError. .. admonition:: Example: diff --git a/sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/aio/_file_system_client_async.py b/sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/aio/_file_system_client_async.py index 774be3d4e874..be90fb2a92f1 100644 --- a/sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/aio/_file_system_client_async.py +++ b/sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/aio/_file_system_client_async.py @@ -55,9 +55,11 @@ class FileSystemClient(AsyncStorageAccountHostsMixin, FileSystemClientBase): :type file_system_name: str :param credential: The credentials with which to authenticate. This is optional if the - account URL already has a SAS token. The value can be a SAS token string, and account + account URL already has a SAS token. The value can be a SAS token string, + an instance of a AzureSasCredential from azure.core.credentials, an account shared access key, or an instance of a TokenCredentials class from azure.identity. - If the URL already has a SAS token, specifying an explicit credential will take priority. + If the resource URI already contains a SAS token, this will be ignored in favor of an explicit credential + - except in the case of AzureSasCredential, where the conflicting SAS tokens will raise a ValueError. .. admonition:: Example: diff --git a/sdk/storage/azure-storage-file-datalake/setup.py b/sdk/storage/azure-storage-file-datalake/setup.py index de3cf363ece8..3af039a51945 100644 --- a/sdk/storage/azure-storage-file-datalake/setup.py +++ b/sdk/storage/azure-storage-file-datalake/setup.py @@ -92,7 +92,7 @@ 'tests', ]), install_requires=[ - "azure-core<2.0.0,>=1.9.0", + "azure-core<2.0.0,>=1.10.0", "msrest>=0.6.10", "azure-storage-blob<13.0.0,>=12.6.0" ], diff --git a/sdk/storage/azure-storage-file-datalake/tests/test_file.py b/sdk/storage/azure-storage-file-datalake/tests/test_file.py index 09ccc98d2a20..616cbe5290fd 100644 --- a/sdk/storage/azure-storage-file-datalake/tests/test_file.py +++ b/sdk/storage/azure-storage-file-datalake/tests/test_file.py @@ -11,6 +11,7 @@ import pytest from azure.core import MatchConditions +from azure.core.credentials import AzureSasCredential from azure.core.exceptions import HttpResponseError, ResourceExistsError, ResourceNotFoundError, \ ClientAuthenticationError, ResourceModifiedError @@ -502,16 +503,21 @@ def test_account_sas(self): datetime.utcnow() + timedelta(hours=1), ) - # read the created file which is under root directory - file_client = DataLakeFileClient(self.dsc.url, self.file_system_name, file_name, credential=token) - properties = file_client.get_file_properties() + for credential in [token, AzureSasCredential(token)]: + # read the created file which is under root directory + file_client = DataLakeFileClient(self.dsc.url, self.file_system_name, file_name, credential=credential) + properties = file_client.get_file_properties() - # make sure we can read the file properties - self.assertIsNotNone(properties) + # make sure we can read the file properties + self.assertIsNotNone(properties) - # try to write to the created file with the token - with self.assertRaises(HttpResponseError): - file_client.append_data(b"abcd", 0, 4) + # try to write to the created file with the token + with self.assertRaises(HttpResponseError): + file_client.append_data(b"abcd", 0, 4) + + def test_account_sas_raises_if_sas_already_in_uri(self): + with self.assertRaises(ValueError): + DataLakeFileClient(self.dsc.url + "?sig=foo", self.file_system_name, "foo", credential=AzureSasCredential("?foo=bar")) @record def test_file_sas_only_applies_to_file_level(self): diff --git a/sdk/storage/azure-storage-file-datalake/tests/test_file_async.py b/sdk/storage/azure-storage-file-datalake/tests/test_file_async.py index 11c507ffe7e9..fe9e14bd5821 100644 --- a/sdk/storage/azure-storage-file-datalake/tests/test_file_async.py +++ b/sdk/storage/azure-storage-file-datalake/tests/test_file_async.py @@ -10,6 +10,7 @@ import asyncio from azure.core import MatchConditions +from azure.core.credentials import AzureSasCredential from azure.core.exceptions import HttpResponseError, ResourceExistsError, ResourceNotFoundError, \ ClientAuthenticationError, ResourceModifiedError @@ -503,22 +504,27 @@ async def _test_account_sas(self): datetime.utcnow() + timedelta(hours=1), ) - # read the created file which is under root directory - file_client = DataLakeFileClient(self.dsc.url, self.file_system_name, file_name, credential=token) - properties = await file_client.get_file_properties() + for credential in [token, AzureSasCredential(token)]: + # read the created file which is under root directory + file_client = DataLakeFileClient(self.dsc.url, self.file_system_name, file_name, credential=credential) + properties = await file_client.get_file_properties() - # make sure we can read the file properties - self.assertIsNotNone(properties) + # make sure we can read the file properties + self.assertIsNotNone(properties) - # try to write to the created file with the token - with self.assertRaises(HttpResponseError): - await file_client.append_data(b"abcd", 0, 4) + # try to write to the created file with the token + with self.assertRaises(HttpResponseError): + await file_client.append_data(b"abcd", 0, 4) @record def test_account_sas_async(self): loop = asyncio.get_event_loop() loop.run_until_complete(self._test_account_sas()) + def test_account_sas_raises_if_sas_already_in_uri(self): + with self.assertRaises(ValueError): + DataLakeFileClient(self.dsc.url + "?sig=foo", self.file_system_name, "foo", credential=AzureSasCredential("?foo=bar")) + async def _test_file_sas_only_applies_to_file_level(self): # SAS URL is calculated from storage key, so this test runs live only if TestMode.need_recording_file(self.test_mode): diff --git a/sdk/storage/azure-storage-file-share/CHANGELOG.md b/sdk/storage/azure-storage-file-share/CHANGELOG.md index ea9281839ef1..40e72050c75c 100644 --- a/sdk/storage/azure-storage-file-share/CHANGELOG.md +++ b/sdk/storage/azure-storage-file-share/CHANGELOG.md @@ -1,7 +1,8 @@ # Release History ## 12.4.0b2 (Unreleased) - +**New features** +- Added support for `AzureSasCredential` to allow SAS rotation in long living clients. ## 12.4.0b1 (2020-12-07) **New features** diff --git a/sdk/storage/azure-storage-file-share/azure/storage/fileshare/_directory_client.py b/sdk/storage/azure-storage-file-share/azure/storage/fileshare/_directory_client.py index f1c7c05554d4..f5b166b8bbaa 100644 --- a/sdk/storage/azure-storage-file-share/azure/storage/fileshare/_directory_client.py +++ b/sdk/storage/azure-storage-file-share/azure/storage/fileshare/_directory_client.py @@ -60,7 +60,8 @@ class ShareDirectoryClient(StorageAccountHostsMixin): or the response returned from :func:`ShareClient.create_snapshot`. :param credential: The credential with which to authenticate. This is optional if the - account URL already has a SAS token. The value can be a SAS token string or an account + account URL already has a SAS token. The value can be a SAS token string, + an instance of a AzureSasCredential from azure.core.credentials or an account shared access key. :keyword str api_version: The Storage API version to use for requests. Default value is '2019-07-07'. @@ -131,7 +132,8 @@ def from_directory_url(cls, directory_url, # type: str or the response returned from :func:`ShareClient.create_snapshot`. :param credential: The credential with which to authenticate. This is optional if the - account URL already has a SAS token. The value can be a SAS token string or an account + account URL already has a SAS token. The value can be a SAS token string, + an instance of a AzureSasCredential from azure.core.credentials or an account shared access key. :returns: A directory client. :rtype: ~azure.storage.fileshare.ShareDirectoryClient @@ -193,7 +195,8 @@ def from_connection_string( The directory path. :param credential: The credential with which to authenticate. This is optional if the - account URL already has a SAS token. The value can be a SAS token string or an account + account URL already has a SAS token. The value can be a SAS token string, + an instance of a AzureSasCredential from azure.core.credentials or an account shared access key. :returns: A directory client. :rtype: ~azure.storage.fileshare.ShareDirectoryClient diff --git a/sdk/storage/azure-storage-file-share/azure/storage/fileshare/_file_client.py b/sdk/storage/azure-storage-file-share/azure/storage/fileshare/_file_client.py index 631adac62418..2ffb3f337ca2 100644 --- a/sdk/storage/azure-storage-file-share/azure/storage/fileshare/_file_client.py +++ b/sdk/storage/azure-storage-file-share/azure/storage/fileshare/_file_client.py @@ -110,7 +110,8 @@ class ShareFileClient(StorageAccountHostsMixin): or the response returned from :func:`ShareClient.create_snapshot`. :param credential: The credential with which to authenticate. This is optional if the - account URL already has a SAS token. The value can be a SAS token string or an account + account URL already has a SAS token. The value can be a SAS token string, + an instance of a AzureSasCredential from azure.core.credentials or an account shared access key. :keyword str api_version: The Storage API version to use for requests. Default value is '2019-07-07'. @@ -184,7 +185,8 @@ def from_file_url( or the response returned from :func:`ShareClient.create_snapshot`. :param credential: The credential with which to authenticate. This is optional if the - account URL already has a SAS token. The value can be a SAS token string or an account + account URL already has a SAS token. The value can be a SAS token string, + an instance of a AzureSasCredential from azure.core.credentials or an account shared access key. :returns: A File client. :rtype: ~azure.storage.fileshare.ShareFileClient @@ -244,7 +246,8 @@ def from_connection_string( or the response returned from :func:`ShareClient.create_snapshot`. :param credential: The credential with which to authenticate. This is optional if the - account URL already has a SAS token. The value can be a SAS token string or an account + account URL already has a SAS token. The value can be a SAS token string, + an instance of a AzureSasCredential from azure.core.credentials or an account shared access key. :returns: A File client. :rtype: ~azure.storage.fileshare.ShareFileClient diff --git a/sdk/storage/azure-storage-file-share/azure/storage/fileshare/_share_client.py b/sdk/storage/azure-storage-file-share/azure/storage/fileshare/_share_client.py index 9b1a47f829d1..d51e1ccc7a38 100644 --- a/sdk/storage/azure-storage-file-share/azure/storage/fileshare/_share_client.py +++ b/sdk/storage/azure-storage-file-share/azure/storage/fileshare/_share_client.py @@ -58,7 +58,8 @@ class ShareClient(StorageAccountHostsMixin): or the response returned from :func:`create_snapshot`. :param credential: The credential with which to authenticate. This is optional if the - account URL already has a SAS token. The value can be a SAS token string or an account + account URL already has a SAS token. The value can be a SAS token string, + an instance of a AzureSasCredential from azure.core.credentials or an account shared access key. :keyword str api_version: The Storage API version to use for requests. Default value is '2019-07-07'. @@ -125,7 +126,8 @@ def from_share_url(cls, share_url, # type: str or the response returned from :func:`create_snapshot`. :param credential: The credential with which to authenticate. This is optional if the - account URL already has a SAS token. The value can be a SAS token string or an account + account URL already has a SAS token. The value can be a SAS token string, + an instance of a AzureSasCredential from azure.core.credentials or an account shared access key. :returns: A share client. :rtype: ~azure.storage.fileshare.ShareClient @@ -197,7 +199,8 @@ def from_connection_string( or the response returned from :func:`create_snapshot`. :param credential: The credential with which to authenticate. This is optional if the - account URL already has a SAS token. The value can be a SAS token string or an account + account URL already has a SAS token. The value can be a SAS token string, + an instance of a AzureSasCredential from azure.core.credentials or an account shared access key. :returns: A share client. :rtype: ~azure.storage.fileshare.ShareClient diff --git a/sdk/storage/azure-storage-file-share/azure/storage/fileshare/_share_service_client.py b/sdk/storage/azure-storage-file-share/azure/storage/fileshare/_share_service_client.py index d6b240e9efa7..b5c73756d79e 100644 --- a/sdk/storage/azure-storage-file-share/azure/storage/fileshare/_share_service_client.py +++ b/sdk/storage/azure-storage-file-share/azure/storage/fileshare/_share_service_client.py @@ -53,7 +53,8 @@ class ShareServiceClient(StorageAccountHostsMixin): authenticated with a SAS token. :param credential: The credential with which to authenticate. This is optional if the - account URL already has a SAS token. The value can be a SAS token string or an account + account URL already has a SAS token. The value can be a SAS token string, + an instance of a AzureSasCredential from azure.core.credentials or an account shared access key. :keyword str api_version: The Storage API version to use for requests. Default value is '2019-07-07'. @@ -118,7 +119,8 @@ def from_connection_string( A connection string to an Azure Storage account. :param credential: The credential with which to authenticate. This is optional if the - account URL already has a SAS token. The value can be a SAS token string or an account + account URL already has a SAS token. The value can be a SAS token string, + an instance of a AzureSasCredential from azure.core.credentials or an account shared access key. :returns: A File Share service client. :rtype: ~azure.storage.fileshare.ShareServiceClient diff --git a/sdk/storage/azure-storage-file-share/azure/storage/fileshare/_shared/base_client.py b/sdk/storage/azure-storage-file-share/azure/storage/fileshare/_shared/base_client.py index 6b647b7afdf6..6117e8bb6706 100644 --- a/sdk/storage/azure-storage-file-share/azure/storage/fileshare/_shared/base_client.py +++ b/sdk/storage/azure-storage-file-share/azure/storage/fileshare/_shared/base_client.py @@ -26,6 +26,7 @@ import six from azure.core.configuration import Configuration +from azure.core.credentials import AzureSasCredential from azure.core.exceptions import HttpResponseError from azure.core.pipeline import Pipeline from azure.core.pipeline.transport import RequestsTransport, HttpTransport @@ -36,7 +37,8 @@ ProxyPolicy, DistributedTracingPolicy, HttpLoggingPolicy, - UserAgentPolicy + UserAgentPolicy, + AzureSasCredentialPolicy ) from .constants import STORAGE_OAUTH_SCOPE, SERVICE_HOST_BASE, CONNECTION_TIMEOUT, READ_TIMEOUT @@ -209,6 +211,9 @@ def _format_query_string(self, sas_token, credential, snapshot=None, share_snaps query_str += "snapshot={}&".format(self.snapshot) if share_snapshot: query_str += "sharesnapshot={}&".format(self.snapshot) + if sas_token and isinstance(credential, AzureSasCredential): + raise ValueError( + "You cannot use AzureSasCredential when the resource URI also contains a Shared Access Signature.") if sas_token and not credential: query_str += sas_token elif is_credential_sastoken(credential): @@ -223,6 +228,8 @@ def _create_pipeline(self, credential, **kwargs): self._credential_policy = BearerTokenCredentialPolicy(credential, STORAGE_OAUTH_SCOPE) elif isinstance(credential, SharedKeyCredentialPolicy): self._credential_policy = credential + elif isinstance(credential, AzureSasCredential): + self._credential_policy = AzureSasCredentialPolicy(credential) elif credential is not None: raise TypeError("Unsupported credential: {}".format(credential)) diff --git a/sdk/storage/azure-storage-file-share/azure/storage/fileshare/_shared/base_client_async.py b/sdk/storage/azure-storage-file-share/azure/storage/fileshare/_shared/base_client_async.py index d252ad063fb6..8bf6126e6d8d 100644 --- a/sdk/storage/azure-storage-file-share/azure/storage/fileshare/_shared/base_client_async.py +++ b/sdk/storage/azure-storage-file-share/azure/storage/fileshare/_shared/base_client_async.py @@ -9,6 +9,8 @@ TYPE_CHECKING ) import logging + +from azure.core.credentials import AzureSasCredential from azure.core.pipeline import AsyncPipeline from azure.core.async_paging import AsyncList from azure.core.exceptions import HttpResponseError @@ -18,6 +20,7 @@ AsyncRedirectPolicy, DistributedTracingPolicy, HttpLoggingPolicy, + AzureSasCredentialPolicy, ) from azure.core.pipeline.transport import AsyncHttpTransport @@ -71,6 +74,8 @@ def _create_pipeline(self, credential, **kwargs): self._credential_policy = AsyncBearerTokenCredentialPolicy(credential, STORAGE_OAUTH_SCOPE) elif isinstance(credential, SharedKeyCredentialPolicy): self._credential_policy = credential + elif isinstance(credential, AzureSasCredential): + self._credential_policy = AzureSasCredentialPolicy(credential) elif credential is not None: raise TypeError("Unsupported credential: {}".format(credential)) config = kwargs.get('_configuration') or create_configuration(**kwargs) diff --git a/sdk/storage/azure-storage-file-share/azure/storage/fileshare/aio/_directory_client_async.py b/sdk/storage/azure-storage-file-share/azure/storage/fileshare/aio/_directory_client_async.py index 29b63969fbfe..848a5e025e72 100644 --- a/sdk/storage/azure-storage-file-share/azure/storage/fileshare/aio/_directory_client_async.py +++ b/sdk/storage/azure-storage-file-share/azure/storage/fileshare/aio/_directory_client_async.py @@ -56,7 +56,8 @@ class ShareDirectoryClient(AsyncStorageAccountHostsMixin, ShareDirectoryClientBa or the response returned from :func:`ShareClient.create_snapshot`. :param credential: The credential with which to authenticate. This is optional if the - account URL already has a SAS token. The value can be a SAS token string or an account + account URL already has a SAS token. The value can be a SAS token string, + an instance of a AzureSasCredential from azure.core.credentials or an account shared access key. :keyword str api_version: The Storage API version to use for requests. Default value is '2019-07-07'. diff --git a/sdk/storage/azure-storage-file-share/azure/storage/fileshare/aio/_file_client_async.py b/sdk/storage/azure-storage-file-share/azure/storage/fileshare/aio/_file_client_async.py index d008e1bc6539..49cc4f0e03b3 100644 --- a/sdk/storage/azure-storage-file-share/azure/storage/fileshare/aio/_file_client_async.py +++ b/sdk/storage/azure-storage-file-share/azure/storage/fileshare/aio/_file_client_async.py @@ -104,7 +104,8 @@ class ShareFileClient(AsyncStorageAccountHostsMixin, ShareFileClientBase): or the response returned from :func:`ShareClient.create_snapshot`. :param credential: The credential with which to authenticate. This is optional if the - account URL already has a SAS token. The value can be a SAS token string or an account + account URL already has a SAS token. The value can be a SAS token string, + an instance of a AzureSasCredential from azure.core.credentials or an account shared access key. :keyword str api_version: The Storage API version to use for requests. Default value is '2019-07-07'. diff --git a/sdk/storage/azure-storage-file-share/azure/storage/fileshare/aio/_share_client_async.py b/sdk/storage/azure-storage-file-share/azure/storage/fileshare/aio/_share_client_async.py index fd9d92a19e6c..eff2e4835cac 100644 --- a/sdk/storage/azure-storage-file-share/azure/storage/fileshare/aio/_share_client_async.py +++ b/sdk/storage/azure-storage-file-share/azure/storage/fileshare/aio/_share_client_async.py @@ -53,7 +53,8 @@ class ShareClient(AsyncStorageAccountHostsMixin, ShareClientBase): or the response returned from :func:`create_snapshot`. :param credential: The credential with which to authenticate. This is optional if the - account URL already has a SAS token. The value can be a SAS token string or an account + account URL already has a SAS token. The value can be a SAS token string, + an instance of a AzureSasCredential from azure.core.credentials or an account shared access key. :keyword str api_version: The Storage API version to use for requests. Default value is '2019-07-07'. diff --git a/sdk/storage/azure-storage-file-share/azure/storage/fileshare/aio/_share_service_client_async.py b/sdk/storage/azure-storage-file-share/azure/storage/fileshare/aio/_share_service_client_async.py index af67dcd83213..0e53101415e4 100644 --- a/sdk/storage/azure-storage-file-share/azure/storage/fileshare/aio/_share_service_client_async.py +++ b/sdk/storage/azure-storage-file-share/azure/storage/fileshare/aio/_share_service_client_async.py @@ -52,7 +52,8 @@ class ShareServiceClient(AsyncStorageAccountHostsMixin, ShareServiceClientBase): authenticated with a SAS token. :param credential: The credential with which to authenticate. This is optional if the - account URL already has a SAS token. The value can be a SAS token string or an account + account URL already has a SAS token. The value can be a SAS token string, + an instance of a AzureSasCredential from azure.core.credentials or an account shared access key. :keyword str api_version: The Storage API version to use for requests. Default value is '2019-07-07'. diff --git a/sdk/storage/azure-storage-file-share/setup.py b/sdk/storage/azure-storage-file-share/setup.py index fb313f0e58fe..c56a5c47ad39 100644 --- a/sdk/storage/azure-storage-file-share/setup.py +++ b/sdk/storage/azure-storage-file-share/setup.py @@ -78,7 +78,7 @@ 'tests', ]), install_requires=[ - "azure-core<2.0.0,>=1.9.0", + "azure-core<2.0.0,>=1.10.0", "msrest>=0.6.10", "cryptography>=2.1.4" ], diff --git a/sdk/storage/azure-storage-file-share/tests/test_file.py b/sdk/storage/azure-storage-file-share/tests/test_file.py index 408c689908d4..e9a8e2a9a690 100644 --- a/sdk/storage/azure-storage-file-share/tests/test_file.py +++ b/sdk/storage/azure-storage-file-share/tests/test_file.py @@ -14,6 +14,7 @@ import pytest import uuid from azure.core import MatchConditions +from azure.core.credentials import AzureSasCredential from azure.core.exceptions import HttpResponseError, ResourceNotFoundError, ResourceExistsError from devtools_testutils import ResourceGroupPreparer, StorageAccountPreparer @@ -1879,6 +1880,43 @@ def test_account_sas(self, resource_group, location, storage_account, storage_ac self.assertTrue(response.ok) self.assertEqual(self.short_byte_data, response.content) + @GlobalStorageAccountPreparer() + def test_account_sas_credential(self, resource_group, location, storage_account, storage_account_key): + # SAS URL is calculated from storage key, so this test runs live only + if not self.is_live: + return + + self._setup(storage_account, storage_account_key) + file_client = self._create_file() + token = generate_account_sas( + self.fsc.account_name, + self.fsc.credential.account_key, + ResourceTypes(object=True), + AccountSasPermissions(read=True), + datetime.utcnow() + timedelta(hours=1), + ) + + # Act + file_client = ShareFileClient( + self.account_url(storage_account, "file"), + share_name=self.share_name, + file_path=file_client.file_name, + credential=AzureSasCredential(token)) + + properties = file_client.get_file_properties() + + # Assert + self.assertIsNotNone(properties) + + @GlobalStorageAccountPreparer() + def test_account_sas_raises_if_sas_already_in_uri(self, resource_group, location, storage_account, storage_account_key): + with self.assertRaises(ValueError): + ShareFileClient( + self.account_url(storage_account, "file") + "?sig=foo", + share_name="foo", + file_path="foo", + credential=AzureSasCredential("?foo=bar")) + @GlobalStorageAccountPreparer() def test_shared_read_access_file(self, resource_group, location, storage_account, storage_account_key): # SAS URL is calculated from storage key, so this test runs live only diff --git a/sdk/storage/azure-storage-file-share/tests/test_file_async.py b/sdk/storage/azure-storage-file-share/tests/test_file_async.py index 46822ff652c1..1f6beb51d679 100644 --- a/sdk/storage/azure-storage-file-share/tests/test_file_async.py +++ b/sdk/storage/azure-storage-file-share/tests/test_file_async.py @@ -10,6 +10,8 @@ import unittest from datetime import datetime, timedelta import asyncio + +from azure.core.credentials import AzureSasCredential from azure.core.pipeline.transport import AioHttpTransport from multidict import CIMultiDict, CIMultiDictProxy import requests @@ -2037,6 +2039,44 @@ async def test_account_sas_async(self, resource_group, location, storage_account self.assertTrue(response.ok) self.assertEqual(self.short_byte_data, response.content) + @GlobalStorageAccountPreparer() + @AsyncStorageTestCase.await_prepared_test + async def test_account_sas_credential_async(self, resource_group, location, storage_account, storage_account_key): + # SAS URL is calculated from storage key, so this test runs live only + if not self.is_live: + return + + self._setup(storage_account, storage_account_key) + file_client = await self._create_file(storage_account, storage_account_key) + token = generate_account_sas( + self.fsc.account_name, + self.fsc.credential.account_key, + ResourceTypes(object=True), + AccountSasPermissions(read=True), + datetime.utcnow() + timedelta(hours=1), + ) + + # Act + file_client = ShareFileClient( + self.account_url(storage_account, "file"), + share_name=self.share_name, + file_path=file_client.file_name, + credential=AzureSasCredential(token)) + + properties = await file_client.get_file_properties() + + # Assert + self.assertIsNotNone(properties) + + @GlobalStorageAccountPreparer() + def test_account_sas_raises_if_sas_already_in_uri(self, resource_group, location, storage_account, storage_account_key): + with self.assertRaises(ValueError): + ShareFileClient( + self.account_url(storage_account, "file") + "?sig=foo", + share_name="foo", + file_path="foo", + credential=AzureSasCredential("?foo=bar")) + @GlobalStorageAccountPreparer() @AsyncStorageTestCase.await_prepared_test async def test_shared_read_access_file_async(self, resource_group, location, storage_account, storage_account_key): diff --git a/sdk/storage/azure-storage-queue/CHANGELOG.md b/sdk/storage/azure-storage-queue/CHANGELOG.md index 5cb051216baf..ea3b826d9d3e 100644 --- a/sdk/storage/azure-storage-queue/CHANGELOG.md +++ b/sdk/storage/azure-storage-queue/CHANGELOG.md @@ -1,7 +1,8 @@ # Release History ## 12.1.5 (Unreleased) - +**New features** +- Added support for `AzureSasCredential` to allow SAS rotation in long living clients. ## 12.1.4 (2020-11-10) **New feature** diff --git a/sdk/storage/azure-storage-queue/azure/storage/queue/_queue_client.py b/sdk/storage/azure-storage-queue/azure/storage/queue/_queue_client.py index 4271bf2a6fc8..7bebbdbed064 100644 --- a/sdk/storage/azure-storage-queue/azure/storage/queue/_queue_client.py +++ b/sdk/storage/azure-storage-queue/azure/storage/queue/_queue_client.py @@ -47,7 +47,8 @@ class QueueClient(StorageAccountHostsMixin): :type queue_name: str :param credential: The credentials with which to authenticate. This is optional if the - account URL already has a SAS token. The value can be a SAS token string, an account + account URL already has a SAS token. The value can be a SAS token string, + an instance of a AzureSasCredential from azure.core.credentials, an account shared access key, or an instance of a TokenCredentials class from azure.identity. :keyword str api_version: The Storage API version to use for requests. Default value is '2019-07-07'. @@ -122,7 +123,8 @@ def from_queue_url(cls, queue_url, credential=None, **kwargs): :param str queue_url: The full URI to the queue, including SAS token if used. :param credential: The credentials with which to authenticate. This is optional if the - account URL already has a SAS token. The value can be a SAS token string, an account + account URL already has a SAS token. The value can be a SAS token string, + an instance of a AzureSasCredential from azure.core.credentials, an account shared access key, or an instance of a TokenCredentials class from azure.identity. :returns: A queue client. :rtype: ~azure.storage.queue.QueueClient @@ -168,7 +170,8 @@ def from_connection_string( :param credential: The credentials with which to authenticate. This is optional if the account URL already has a SAS token, or the connection string already has shared - access key values. The value can be a SAS token string, an account shared access + access key values. The value can be a SAS token string, + an instance of a AzureSasCredential from azure.core.credentials, an account shared access key, or an instance of a TokenCredentials class from azure.identity. :returns: A queue client. :rtype: ~azure.storage.queue.QueueClient diff --git a/sdk/storage/azure-storage-queue/azure/storage/queue/_queue_service_client.py b/sdk/storage/azure-storage-queue/azure/storage/queue/_queue_service_client.py index 44fc4bd9403e..9a67862f144a 100644 --- a/sdk/storage/azure-storage-queue/azure/storage/queue/_queue_service_client.py +++ b/sdk/storage/azure-storage-queue/azure/storage/queue/_queue_service_client.py @@ -56,7 +56,8 @@ class QueueServiceClient(StorageAccountHostsMixin): authenticated with a SAS token. :param credential: The credentials with which to authenticate. This is optional if the - account URL already has a SAS token. The value can be a SAS token string, an account + account URL already has a SAS token. The value can be a SAS token string, + an instance of a AzureSasCredential from azure.core.credentials, an account shared access key, or an instance of a TokenCredentials class from azure.identity. :keyword str api_version: The Storage API version to use for requests. Default value is '2019-07-07'. @@ -123,7 +124,8 @@ def from_connection_string( :param credential: The credentials with which to authenticate. This is optional if the account URL already has a SAS token, or the connection string already has shared - access key values. The value can be a SAS token string, an account shared access + access key values. The value can be a SAS token string, + an instance of a AzureSasCredential from azure.core.credentials, an account shared access key, or an instance of a TokenCredentials class from azure.identity. :returns: A Queue service client. :rtype: ~azure.storage.queue.QueueClient diff --git a/sdk/storage/azure-storage-queue/azure/storage/queue/_shared/base_client.py b/sdk/storage/azure-storage-queue/azure/storage/queue/_shared/base_client.py index b4e1df818888..e8788ec65b40 100644 --- a/sdk/storage/azure-storage-queue/azure/storage/queue/_shared/base_client.py +++ b/sdk/storage/azure-storage-queue/azure/storage/queue/_shared/base_client.py @@ -26,6 +26,7 @@ import six from azure.core.configuration import Configuration +from azure.core.credentials import AzureSasCredential from azure.core.exceptions import HttpResponseError from azure.core.pipeline import Pipeline from azure.core.pipeline.transport import RequestsTransport, HttpTransport @@ -37,6 +38,7 @@ DistributedTracingPolicy, HttpLoggingPolicy, UserAgentPolicy, + AzureSasCredentialPolicy, ) from .constants import STORAGE_OAUTH_SCOPE, SERVICE_HOST_BASE, CONNECTION_TIMEOUT, READ_TIMEOUT @@ -209,6 +211,9 @@ def _format_query_string(self, sas_token, credential, snapshot=None, share_snaps query_str += "snapshot={}&".format(self.snapshot) if share_snapshot: query_str += "sharesnapshot={}&".format(self.snapshot) + if sas_token and isinstance(credential, AzureSasCredential): + raise ValueError( + "You cannot use AzureSasCredential when the resource URI also contains a Shared Access Signature.") if sas_token and not credential: query_str += sas_token elif is_credential_sastoken(credential): @@ -223,6 +228,8 @@ def _create_pipeline(self, credential, **kwargs): self._credential_policy = BearerTokenCredentialPolicy(credential, STORAGE_OAUTH_SCOPE) elif isinstance(credential, SharedKeyCredentialPolicy): self._credential_policy = credential + elif isinstance(credential, AzureSasCredential): + self._credential_policy = AzureSasCredentialPolicy(credential) elif credential is not None: raise TypeError("Unsupported credential: {}".format(credential)) diff --git a/sdk/storage/azure-storage-queue/azure/storage/queue/_shared/base_client_async.py b/sdk/storage/azure-storage-queue/azure/storage/queue/_shared/base_client_async.py index d252ad063fb6..aea9b85064ff 100644 --- a/sdk/storage/azure-storage-queue/azure/storage/queue/_shared/base_client_async.py +++ b/sdk/storage/azure-storage-queue/azure/storage/queue/_shared/base_client_async.py @@ -9,6 +9,8 @@ TYPE_CHECKING ) import logging + +from azure.core.credentials import AzureSasCredential from azure.core.pipeline import AsyncPipeline from azure.core.async_paging import AsyncList from azure.core.exceptions import HttpResponseError @@ -17,7 +19,7 @@ AsyncBearerTokenCredentialPolicy, AsyncRedirectPolicy, DistributedTracingPolicy, - HttpLoggingPolicy, + HttpLoggingPolicy, AzureSasCredentialPolicy, ) from azure.core.pipeline.transport import AsyncHttpTransport @@ -71,6 +73,8 @@ def _create_pipeline(self, credential, **kwargs): self._credential_policy = AsyncBearerTokenCredentialPolicy(credential, STORAGE_OAUTH_SCOPE) elif isinstance(credential, SharedKeyCredentialPolicy): self._credential_policy = credential + elif isinstance(credential, AzureSasCredential): + self._credential_policy = AzureSasCredentialPolicy(credential) elif credential is not None: raise TypeError("Unsupported credential: {}".format(credential)) config = kwargs.get('_configuration') or create_configuration(**kwargs) diff --git a/sdk/storage/azure-storage-queue/azure/storage/queue/aio/_queue_client_async.py b/sdk/storage/azure-storage-queue/azure/storage/queue/aio/_queue_client_async.py index d2d3e49af02c..b9b611ef18d0 100644 --- a/sdk/storage/azure-storage-queue/azure/storage/queue/aio/_queue_client_async.py +++ b/sdk/storage/azure-storage-queue/azure/storage/queue/aio/_queue_client_async.py @@ -65,7 +65,8 @@ class QueueClient(AsyncStorageAccountHostsMixin, QueueClientBase): :type queue_name: str :param credential: The credentials with which to authenticate. This is optional if the - account URL already has a SAS token. The value can be a SAS token string, an account + account URL already has a SAS token. The value can be a SAS token string, + an instance of a AzureSasCredential from azure.core.credentials, an account shared access key, or an instance of a TokenCredentials class from azure.identity. :keyword str api_version: The Storage API version to use for requests. Default value is '2019-07-07'. diff --git a/sdk/storage/azure-storage-queue/azure/storage/queue/aio/_queue_service_client_async.py b/sdk/storage/azure-storage-queue/azure/storage/queue/aio/_queue_service_client_async.py index 42d0d45f4583..63c11ff004f1 100644 --- a/sdk/storage/azure-storage-queue/azure/storage/queue/aio/_queue_service_client_async.py +++ b/sdk/storage/azure-storage-queue/azure/storage/queue/aio/_queue_service_client_async.py @@ -61,7 +61,8 @@ class QueueServiceClient(AsyncStorageAccountHostsMixin, QueueServiceClientBase): authenticated with a SAS token. :param credential: The credentials with which to authenticate. This is optional if the - account URL already has a SAS token. The value can be a SAS token string, an account + account URL already has a SAS token. The value can be a SAS token string, + an instance of a AzureSasCredential from azure.core.credentials, an account shared access key, or an instance of a TokenCredentials class from azure.identity. :keyword str api_version: The Storage API version to use for requests. Default value is '2019-07-07'. diff --git a/sdk/storage/azure-storage-queue/setup.py b/sdk/storage/azure-storage-queue/setup.py index 7410d0043652..60cde0fed4e6 100644 --- a/sdk/storage/azure-storage-queue/setup.py +++ b/sdk/storage/azure-storage-queue/setup.py @@ -81,7 +81,7 @@ 'tests.common' ]), install_requires=[ - "azure-core<2.0.0,>=1.9.0", + "azure-core<2.0.0,>=1.10.0", "msrest>=0.6.10", "cryptography>=2.1.4" ], diff --git a/sdk/storage/azure-storage-queue/tests/test_queue.py b/sdk/storage/azure-storage-queue/tests/test_queue.py index d6f21a3f03f8..58deba7c5552 100644 --- a/sdk/storage/azure-storage-queue/tests/test_queue.py +++ b/sdk/storage/azure-storage-queue/tests/test_queue.py @@ -10,6 +10,8 @@ import unittest import pytest import sys + +from azure.core.credentials import AzureSasCredential from dateutil.tz import tzutc from datetime import ( datetime, @@ -559,20 +561,28 @@ def test_account_sas(self, resource_group, location, storage_account, storage_ac ) # Act - service = QueueServiceClient( - account_url=qsc.url, - credential=token, - ) - new_queue_client = service.get_queue_client(queue_client.queue_name) - result = new_queue_client.peek_messages() + for credential in [token, AzureSasCredential(token)]: + service = QueueServiceClient( + account_url=qsc.url, + credential=credential, + ) + new_queue_client = service.get_queue_client(queue_client.queue_name) + result = new_queue_client.peek_messages() - # Assert - self.assertIsNotNone(result) - self.assertEqual(1, len(result)) - message = result[0] - self.assertIsNotNone(message) - self.assertNotEqual('', message.id) - self.assertEqual(u'message1', message.content) + # Assert + self.assertIsNotNone(result) + self.assertEqual(1, len(result)) + message = result[0] + self.assertIsNotNone(message) + self.assertNotEqual('', message.id) + self.assertEqual(u'message1', message.content) + + @GlobalStorageAccountPreparer() + def test_account_sas_raises_if_sas_already_in_uri(self, resource_group, location, storage_account, storage_account_key): + with self.assertRaises(ValueError): + QueueServiceClient( + self.account_url(storage_account, "queue") + "?sig=foo", + credential=AzureSasCredential("?foo=bar")) @pytest.mark.live_test_only @GlobalStorageAccountPreparer() diff --git a/sdk/storage/azure-storage-queue/tests/test_queue_async.py b/sdk/storage/azure-storage-queue/tests/test_queue_async.py index eea4e4c9f114..49054ec94a70 100644 --- a/sdk/storage/azure-storage-queue/tests/test_queue_async.py +++ b/sdk/storage/azure-storage-queue/tests/test_queue_async.py @@ -8,6 +8,8 @@ import unittest import pytest import asyncio + +from azure.core.credentials import AzureSasCredential from dateutil.tz import tzutc from datetime import ( datetime, @@ -603,20 +605,28 @@ async def test_account_sas(self, resource_group, location, storage_account, stor ) # Act - service = QueueServiceClient( - account_url=qsc.url, - credential=token, - ) - new_queue_client = service.get_queue_client(queue_client.queue_name) - result = await new_queue_client.peek_messages() + for credential in [token, AzureSasCredential(token)]: + service = QueueServiceClient( + account_url=qsc.url, + credential=credential, + ) + new_queue_client = service.get_queue_client(queue_client.queue_name) + result = await new_queue_client.peek_messages() - # Assert - self.assertIsNotNone(result) - self.assertEqual(1, len(result)) - message = result[0] - self.assertIsNotNone(message) - self.assertNotEqual('', message.id) - self.assertEqual(u'message1', message.content) + # Assert + self.assertIsNotNone(result) + self.assertEqual(1, len(result)) + message = result[0] + self.assertIsNotNone(message) + self.assertNotEqual('', message.id) + self.assertEqual(u'message1', message.content) + + @GlobalStorageAccountPreparer() + def test_account_sas_raises_if_sas_already_in_uri(self, resource_group, location, storage_account, storage_account_key): + with self.assertRaises(ValueError): + QueueServiceClient( + self.account_url(storage_account, "queue") + "?sig=foo", + credential=AzureSasCredential("?foo=bar")) @pytest.mark.live_test_only @GlobalStorageAccountPreparer() diff --git a/shared_requirements.txt b/shared_requirements.txt index 3cc0fd93c3a0..3dd62467788c 100644 --- a/shared_requirements.txt +++ b/shared_requirements.txt @@ -139,14 +139,14 @@ avro<2.0.0,>=1.10.0 #override azure-search-documents azure-core<2.0.0,>=1.4.0 #override azure-ai-formrecognizer msrest>=0.6.12 #override azure-ai-formrecognizer azure-core<2.0.0,>=1.8.2 -#override azure-storage-blob azure-core<2.0.0,>=1.9.0 +#override azure-storage-blob azure-core<2.0.0,>=1.10.0 #override azure-storage-blob msrest>=0.6.10 #override azure-storage-blob-changefeed azure-storage-blob>=12.5.0,<13.0.0 -#override azure-storage-queue azure-core<2.0.0,>=1.9.0 +#override azure-storage-queue azure-core<2.0.0,>=1.10.0 #override azure-storage-queue msrest>=0.6.10 #override azure-storage-file-share msrest>=0.6.10 -#override azure-storage-file-share azure-core<2.0.0,>=1.9.0 -#override azure-storage-file-datalake azure-core<2.0.0,>=1.9.0 +#override azure-storage-file-share azure-core<2.0.0,>=1.10.0 +#override azure-storage-file-datalake azure-core<2.0.0,>=1.10.0 #override azure-storage-file-datalake msrest>=0.6.10 #override azure-storage-file-datalake azure-storage-blob<13.0.0,>=12.6.0 #override azure-security-attestation msrest>=0.6.0