diff --git a/sdk/storage/azure-storage-blob/azure/storage/blob/_shared/models.py b/sdk/storage/azure-storage-blob/azure/storage/blob/_shared/models.py index 7185141649f9..50f891de3012 100644 --- a/sdk/storage/azure-storage-blob/azure/storage/blob/_shared/models.py +++ b/sdk/storage/azure-storage-blob/azure/storage/blob/_shared/models.py @@ -420,3 +420,36 @@ def __str__(self): Services.BLOB = Services(blob=True) Services.QUEUE = Services(queue=True) Services.FILE = Services(file=True) + + +class UserDelegationKey(object): + """ + Represents a user delegation key, provided to the user by Azure Storage + based on their Azure Active Directory access token. + + The fields are saved as simple strings since the user does not have to interact with this object; + to generate an identify SAS, the user can simply pass it to the right API. + + :ivar str signed_oid: + Object ID of this token. + :ivar str signed_tid: + Tenant ID of the tenant that issued this token. + :ivar str signed_start: + The datetime this token becomes valid. + :ivar str signed_expiry: + The datetime this token expires. + :ivar str signed_service: + What service this key is valid for. + :ivar str signed_version: + The version identifier of the REST service that created this token. + :ivar str value: + The user delegation key. + """ + def __init__(self): + self.signed_oid = None + self.signed_tid = None + self.signed_start = None + self.signed_expiry = None + self.signed_service = None + self.signed_version = None + self.value = None diff --git a/sdk/storage/azure-storage-blob/azure/storage/blob/_shared/response_handlers.py b/sdk/storage/azure-storage-blob/azure/storage/blob/_shared/response_handlers.py index bf92763aa509..fbf9889d762c 100644 --- a/sdk/storage/azure-storage-blob/azure/storage/blob/_shared/response_handlers.py +++ b/sdk/storage/azure-storage-blob/azure/storage/blob/_shared/response_handlers.py @@ -19,7 +19,8 @@ ClientAuthenticationError, DecodeError) -from .models import StorageErrorCode +from .parser import _to_utc_datetime +from .models import StorageErrorCode, UserDelegationKey if TYPE_CHECKING: @@ -131,3 +132,15 @@ def process_storage_error(storage_error): error.error_code = error_code error.additional_info = additional_data raise error + + +def parse_to_internal_user_delegation_key(service_user_delegation_key): + internal_user_delegation_key = UserDelegationKey() + internal_user_delegation_key.signed_oid = service_user_delegation_key.signed_oid + internal_user_delegation_key.signed_tid = service_user_delegation_key.signed_tid + internal_user_delegation_key.signed_start = _to_utc_datetime(service_user_delegation_key.signed_start) + internal_user_delegation_key.signed_expiry = _to_utc_datetime(service_user_delegation_key.signed_expiry) + internal_user_delegation_key.signed_service = service_user_delegation_key.signed_service + internal_user_delegation_key.signed_version = service_user_delegation_key.signed_version + internal_user_delegation_key.value = service_user_delegation_key.value + return internal_user_delegation_key diff --git a/sdk/storage/azure-storage-blob/azure/storage/blob/_shared_access_signature.py b/sdk/storage/azure-storage-blob/azure/storage/blob/_shared_access_signature.py index 81c7b6d09f7c..b3adf5b2ac1a 100644 --- a/sdk/storage/azure-storage-blob/azure/storage/blob/_shared_access_signature.py +++ b/sdk/storage/azure-storage-blob/azure/storage/blob/_shared_access_signature.py @@ -188,7 +188,7 @@ def generate_container(self, container_name, permission=None, expiry=None, content_encoding, content_language, content_type) sas.add_resource_signature(self.account_name, self.account_key, container_name, - user_delegation_key=None) + user_delegation_key=self.user_delegation_key) return sas.get_token() 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 bd3b2e5cb292..653edab10995 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 @@ -18,8 +18,10 @@ from .._shared.policies_async import ExponentialRetry from .._shared.base_client_async import AsyncStorageAccountHostsMixin from .._shared.response_handlers import return_response_headers, process_storage_error +from .._shared.parser import _to_utc_datetime +from .._shared.response_handlers import parse_to_internal_user_delegation_key from .._generated.aio import AzureBlobStorage -from .._generated.models import StorageErrorException, StorageServiceProperties +from .._generated.models import StorageErrorException, StorageServiceProperties, KeyInfo from ..blob_service_client import BlobServiceClient as BlobServiceClientBase from .container_client_async import ContainerClient from .blob_client_async import BlobClient @@ -110,6 +112,36 @@ def __init__( self._client = AzureBlobStorage(url=self.url, pipeline=self._pipeline, loop=loop) self._loop = loop + @distributed_trace_async + async def get_user_delegation_key(self, key_start_time, # type: datetime + key_expiry_time, # type: datetime + timeout=None, # type: Optional[int] + **kwargs # type: Any + ): + # type: (datetime, datetime, Optional[int]) -> UserDelegationKey + """ + Obtain a user delegation key for the purpose of signing SAS tokens. + A token credential must be present on the service object for this request to succeed. + + :param datetime key_start_time: + A DateTime value. Indicates when the key becomes valid. + :param datetime key_expiry_time: + A DateTime value. Indicates when the key stops being valid. + :param int timeout: + The timeout parameter is expressed in seconds. + :return: The user delegation key. + :rtype: ~azure.storage.blob._shared.models.UserDelegationKey + """ + key_info = KeyInfo(start=_to_utc_datetime(key_start_time), expiry=_to_utc_datetime(key_expiry_time)) + try: + user_delegation_key = await self._client.service.get_user_delegation_key(key_info=key_info, + timeout=timeout, + **kwargs) # type: ignore + except StorageErrorException as error: + process_storage_error(error) + + return parse_to_internal_user_delegation_key(user_delegation_key) # type: ignore + @distributed_trace_async async def get_account_information(self, **kwargs): # type: ignore # type: (Optional[int]) -> Dict[str, str] 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 93b9d6d535b4..e882ed80f29e 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 @@ -29,7 +29,7 @@ validate_and_format_range_headers) from ._shared.response_handlers import return_response_headers, process_storage_error from ._generated import AzureBlobStorage -from ._generated.models import ( +from ._generated.models import ( # pylint: disable=unused-import DeleteSnapshotsOptionType, BlobHTTPHeaders, BlockLookupList, @@ -38,7 +38,9 @@ ModifiedAccessConditions, SequenceNumberAccessConditions, StorageErrorException, + UserDelegationKey, CpkInfo) + from ._deserialize import deserialize_blob_properties, deserialize_blob_stream from ._upload_helpers import ( upload_block_blob, @@ -134,6 +136,7 @@ def __init__( except AttributeError: raise ValueError("Blob URL must be a string.") parsed_url = urlparse(blob_url.rstrip('/')) + if not parsed_url.path and not (container and blob): raise ValueError("Please specify a container and blob name.") if not parsed_url.netloc: @@ -228,12 +231,14 @@ def generate_shared_access_signature( policy_id=None, # type: Optional[str] ip=None, # type: Optional[str] protocol=None, # type: Optional[str] + account_name=None, # type: Optional[str] cache_control=None, # type: Optional[str] content_disposition=None, # type: Optional[str] content_encoding=None, # type: Optional[str] content_language=None, # type: Optional[str] - content_type=None # type: Optional[str] - ): + content_type=None, # type: Optional[str] + user_delegation_key=None # type: Optional[UserDelegationKey] + ): # type: (...) -> Any """ Generates a shared access signature for the blob. @@ -275,6 +280,8 @@ def generate_shared_access_signature( restricts the request to those IP addresses. :param str protocol: Specifies the protocol permitted for a request made. The default value is https. + :param str account_name: + Specifies the account_name when using oauth token as credential. If you use oauth token as credential. :param str cache_control: Response header value for Cache-Control when resource is accessed using this shared access signature. @@ -290,12 +297,24 @@ def generate_shared_access_signature( :param str content_type: Response header value for Content-Type when resource is accessed using this shared access signature. + :param ~azure.storage.blob._shared.models.UserDelegationKey user_delegation_key: + Instead of an account key, the user could pass in a user delegation key. + A user delegation key can be obtained from the service by authenticating with an AAD identity; + this can be accomplished by calling get_user_delegation_key. + When present, the SAS is signed with the user delegation key instead. :return: A Shared Access Signature (sas) token. :rtype: str """ - if not hasattr(self.credential, 'account_key') or not self.credential.account_key: - raise ValueError("No account SAS key available.") - sas = BlobSharedAccessSignature(self.credential.account_name, self.credential.account_key) + if user_delegation_key is not None: + if not hasattr(self.credential, 'account_name') and not account_name: + raise ValueError("No account_name available. Please provide account_name parameter.") + + account_name = self.credential.account_name if hasattr(self.credential, 'account_name') else account_name + sas = BlobSharedAccessSignature(account_name, user_delegation_key=user_delegation_key) + else: + if not hasattr(self.credential, 'account_key') or not self.credential.account_key: + raise ValueError("No account SAS key available.") + sas = BlobSharedAccessSignature(self.credential.account_name, self.credential.account_key) return sas.generate_blob( self.container_name, self.blob_name, 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 23c7937bb108..33af6d0001c2 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 @@ -9,6 +9,7 @@ Union, Optional, Any, Iterable, Dict, List, TYPE_CHECKING ) + try: from urllib.parse import urlparse except ImportError: @@ -18,11 +19,13 @@ from azure.core.tracing.decorator import distributed_trace from ._shared.shared_access_signature import SharedAccessSignature -from ._shared.models import LocationMode, Services +from ._shared.models import LocationMode, Services, UserDelegationKey from ._shared.base_client import StorageAccountHostsMixin, parse_connection_str, parse_query -from ._shared.response_handlers import return_response_headers, process_storage_error +from ._shared.parser import _to_utc_datetime +from ._shared.response_handlers import return_response_headers, process_storage_error, \ + parse_to_internal_user_delegation_key from ._generated import AzureBlobStorage -from ._generated.models import StorageErrorException, StorageServiceProperties +from ._generated.models import StorageErrorException, StorageServiceProperties, KeyInfo from .container_client import ContainerClient from .blob_client import BlobClient from .models import ContainerProperties, ContainerPropertiesPaged @@ -223,6 +226,36 @@ def generate_shared_access_signature( protocol=protocol ) # type: ignore + @distributed_trace + def get_user_delegation_key(self, key_start_time, # type: datetime + key_expiry_time, # type: datetime + timeout=None, # type: Optional[int] + **kwargs # type: Any + ): + # type: (datetime, datetime, Optional[int]) -> UserDelegationKey + """ + Obtain a user delegation key for the purpose of signing SAS tokens. + A token credential must be present on the service object for this request to succeed. + + :param datetime key_start_time: + A DateTime value. Indicates when the key becomes valid. + :param datetime key_expiry_time: + A DateTime value. Indicates when the key stops being valid. + :param int timeout: + The timeout parameter is expressed in seconds. + :return: The user delegation key. + :rtype: ~azure.storage.blob._shared.models.UserDelegationKey + """ + key_info = KeyInfo(start=_to_utc_datetime(key_start_time), expiry=_to_utc_datetime(key_expiry_time)) + try: + user_delegation_key = self._client.service.get_user_delegation_key(key_info=key_info, + timeout=timeout, + **kwargs) # type: ignore + except StorageErrorException as error: + process_storage_error(error) + + return parse_to_internal_user_delegation_key(user_delegation_key) # type: ignore + @distributed_trace def get_account_information(self, **kwargs): # type: ignore # type: (Optional[int]) -> Dict[str, str] 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 a25da2c35e50..bab89ba6802e 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 @@ -119,6 +119,7 @@ def __init__( except AttributeError: raise ValueError("Container URL must be a string.") parsed_url = urlparse(container_url.rstrip('/')) + if not parsed_url.path and not container: raise ValueError("Please specify a container name.") if not parsed_url.netloc: @@ -188,11 +189,13 @@ def generate_shared_access_signature( policy_id=None, # type: Optional[str] ip=None, # type: Optional[str] protocol=None, # type: Optional[str] + account_name=None, # type: Optional[str] cache_control=None, # type: Optional[str] content_disposition=None, # type: Optional[str] content_encoding=None, # type: Optional[str] content_language=None, # type: Optional[str] - content_type=None # type: Optional[str] + content_type=None, # type: Optional[str] + user_delegation_key=None # type Optional[] ): # type: (...) -> Any """Generates a shared access signature for the container. @@ -233,6 +236,8 @@ def generate_shared_access_signature( restricts the request to those IP addresses. :param str protocol: Specifies the protocol permitted for a request made. The default value is https. + :param str account_name: + Specifies the account_name when using oauth token as credential. If you use oauth token as credential. :param str cache_control: Response header value for Cache-Control when resource is accessed using this shared access signature. @@ -248,6 +253,11 @@ def generate_shared_access_signature( :param str content_type: Response header value for Content-Type when resource is accessed using this shared access signature. + :param ~azure.storage.blob._shared.models.UserDelegationKey user_delegation_key: + Instead of an account key, the user could pass in a user delegation key. + A user delegation key can be obtained from the service by authenticating with an AAD identity; + this can be accomplished by calling get_user_delegation_key. + When present, the SAS is signed with the user delegation key instead. :return: A Shared Access Signature (sas) token. :rtype: str @@ -259,9 +269,16 @@ def generate_shared_access_signature( :dedent: 12 :caption: Generating a sas token. """ - if not hasattr(self.credential, 'account_key') and not self.credential.account_key: - raise ValueError("No account SAS key available.") - sas = BlobSharedAccessSignature(self.credential.account_name, self.credential.account_key) + if user_delegation_key is not None: + if not hasattr(self.credential, 'account_name') and not account_name: + raise ValueError("No account_name available. Please provide account_name parameter.") + + account_name = self.credential.account_name if hasattr(self.credential, 'account_name') else account_name + sas = BlobSharedAccessSignature(account_name, user_delegation_key=user_delegation_key) + else: + if not hasattr(self.credential, 'account_key') and not self.credential.account_key: + raise ValueError("No account SAS key available.") + sas = BlobSharedAccessSignature(self.credential.account_name, self.credential.account_key) return sas.generate_container( self.container_name, permission=permission, diff --git a/sdk/storage/azure-storage-blob/tests/recordings/test_common_blob.test_get_user_delegation_key.yaml b/sdk/storage/azure-storage-blob/tests/recordings/test_common_blob.test_get_user_delegation_key.yaml new file mode 100644 index 000000000000..74da6748ca62 --- /dev/null +++ b/sdk/storage/azure-storage-blob/tests/recordings/test_common_blob.test_get_user_delegation_key.yaml @@ -0,0 +1,145 @@ +interactions: +- request: + body: client_id=68390a19-a897-236b-b453-488abf67b4fc&client_secret=3Usxz7pzkOeE7flc6Z187ubs5%2FcJnszGPjAiXmcwhaY%3D&grant_type=client_credentials&scope=https%3A%2F%2Fstorage.azure.com%2F.default + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '188' + Content-Type: + - application/x-www-form-urlencoded + User-Agent: + - python-requests/2.22.0 + method: POST + uri: https://login.microsoftonline.com/32f988bf-54f1-15af-36ab-2d7cd364db47/oauth2/v2.0/token + response: + body: + string: '{"token_type":"Bearer","expires_in":3600,"ext_expires_in":3600,"access_token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6ImllX3FXQ1hoWHh0MXpJRXN1NGM3YWNRVkduNCIsImtpZCI6ImllX3FXQ1hoWHh0MXpJRXN1NGM3YWNRVkduNCJ9.eyJhdWQiOiJodHRwczovL3N0b3JhZ2UuYXp1cmUuY29tIiwiaXNzIjoiaHR0cHM6Ly9zdHMud2luZG93cy5uZXQvNzJmOTg4YmYtODZmMS00MWFmLTkxYWItMmQ3Y2QwMTFkYjQ3LyIsImlhdCI6MTU2ODAxMzI3MSwibmJmIjoxNTY4MDEzMjcxLCJleHAiOjE1NjgwMTcxNzEsImFpbyI6IjQyRmdZRWllNE9INFQ0YkZrR3RoaExHT2hma1pBQT09IiwiYXBwaWQiOiI2ODM5MGExOS1hNjQzLTQ1OGItYjcyNi00MDhhYmY2N2I0ZmMiLCJhcHBpZGFjciI6IjEiLCJpZHAiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC83MmY5ODhiZi04NmYxLTQxYWYtOTFhYi0yZDdjZDAxMWRiNDcvIiwib2lkIjoiYzRmNDgyODktYmI4NC00MDg2LWIyNTAtNmY5NGE4ZjY0Y2VlIiwic3ViIjoiYzRmNDgyODktYmI4NC00MDg2LWIyNTAtNmY5NGE4ZjY0Y2VlIiwidGlkIjoiNzJmOTg4YmYtODZmMS00MWFmLTkxYWItMmQ3Y2QwMTFkYjQ3IiwidXRpIjoiRkpWQVQxM2NYVXFLeFhlc0tUMHNBQSIsInZlciI6IjEuMCJ9.tk74OnnuvrPtFFWZe_v1H6GpGK5hU4n76EoIybK2O_Za8ddlQbBtgeMqNzifX6qlh8bce4RYOSggFNapknf01h9Z53UkdJXusB8IcIoPXv9t3NH-zkrSabU0VpWLh24cas9u5HT-re0YZy4b2T9czKCheU2ltyU3y2VwsFh1OAmaUUJaDUw0OSdQGRcOu2rS5LQAgAQu0UcQZG2bT_IoAeIGCnIIyOG25gaO7mt4_oHqU61b6q9AH72i7shblF1JBQp6eMmTkZe_Y1GnScOACUTh3R82nSQ-AKC2JrXXGCELl9L_bUmbxOVBwJ9F-La4d26axn6NcmFnMzaiGO1tyw"}' + headers: + Cache-Control: + - no-cache, no-store + Content-Length: + - '1233' + Content-Type: + - application/json; charset=utf-8 + Date: + - Mon, 09 Sep 2019 07:19:30 GMT + Expires: + - '-1' + P3P: + - CP="DSP CUR OTPi IND OTRi ONL FIN" + Pragma: + - no-cache + Set-Cookie: + - fpc=Arv4rF3oXGJOuCYLHWg4UbzeSEc1AQAAAAL0B9UOAAAA; expires=Wed, 09-Oct-2019 + 07:19:31 GMT; path=/; secure; HttpOnly + - x-ms-gateway-slice=estsfd; path=/; secure; HttpOnly + - stsservicecookie=estsfd; path=/; secure; HttpOnly + Strict-Transport-Security: + - max-age=31536000; includeSubDomains + X-Content-Type-Options: + - nosniff + x-ms-ests-server: + - 2.1.9368.5 - WUS ProdSlices + x-ms-request-id: + - 4f409514-dc5d-4a5d-8ac5-77ac293d2c00 + status: + code: 200 + message: OK +- request: + body: ' + + 2019-09-09T07:19:30Z2019-09-09T08:19:30Z' + headers: + Accept: + - application/xml + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '130' + Content-Type: + - application/xml; charset=utf-8 + User-Agent: + - azsdk-python-storage-blob/12.0.0b2 Python/3.7.3 (Windows-10-10.0.18362-SP0) + x-ms-client-request-id: + - 2a824312-d2d2-11e9-8739-001a7dda7113 + x-ms-date: + - Mon, 09 Sep 2019 07:19:30 GMT + x-ms-version: + - '2019-02-02' + method: POST + uri: https://oauthstoragename.blob.core.windows.net/?restype=service&comp=userdelegationkey + response: + body: + string: "\uFEFFc4f48289-bb84-4086-b250-6f94a8f64cee32f988bf-54f1-15af-36ab-2d7cd364db472019-09-09T07:19:30Z2019-09-09T08:19:30Zb2019-02-021CVF1CqD5X/XQy7xviJADxqwoA9X1Sh/pQWYA6//6Pc=" + headers: + Content-Type: + - application/xml + Date: + - Mon, 09 Sep 2019 07:19:30 GMT + Server: + - Windows-Azure-Blob/1.0 Microsoft-HTTPAPI/2.0 + Transfer-Encoding: + - chunked + x-ms-client-request-id: + - 2a824312-d2d2-11e9-8739-001a7dda7113 + x-ms-request-id: + - e2241bda-c01e-0028-5ade-66325c000000 + x-ms-version: + - '2019-02-02' + status: + code: 200 + message: OK +- request: + body: ' + + 2019-09-09T07:19:30Z2019-09-09T08:19:30Z' + headers: + Accept: + - application/xml + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '130' + Content-Type: + - application/xml; charset=utf-8 + User-Agent: + - azsdk-python-storage-blob/12.0.0b2 Python/3.7.3 (Windows-10-10.0.18362-SP0) + x-ms-client-request-id: + - 2af3d72e-d2d2-11e9-8331-001a7dda7113 + x-ms-date: + - Mon, 09 Sep 2019 07:19:31 GMT + x-ms-version: + - '2019-02-02' + method: POST + uri: https://oauthstoragename.blob.core.windows.net/?restype=service&comp=userdelegationkey + response: + body: + string: "\uFEFFc4f48289-bb84-4086-b250-6f94a8f64cee32f988bf-54f1-15af-36ab-2d7cd364db472019-09-09T07:19:30Z2019-09-09T08:19:30Zb2019-02-021CVF1CqD5X/XQy7xviJADxqwoA9X1Sh/pQWYA6//6Pc=" + headers: + Content-Type: + - application/xml + Date: + - Mon, 09 Sep 2019 07:19:30 GMT + Server: + - Windows-Azure-Blob/1.0 Microsoft-HTTPAPI/2.0 + Transfer-Encoding: + - chunked + x-ms-client-request-id: + - 2af3d72e-d2d2-11e9-8331-001a7dda7113 + x-ms-request-id: + - e2241c96-c01e-0028-01de-66325c000000 + x-ms-version: + - '2019-02-02' + status: + code: 200 + message: OK +version: 1 diff --git a/sdk/storage/azure-storage-blob/tests/recordings/test_common_blob_async.test_get_user_delegation_key_async.yaml b/sdk/storage/azure-storage-blob/tests/recordings/test_common_blob_async.test_get_user_delegation_key_async.yaml new file mode 100644 index 000000000000..a9e13a93866c --- /dev/null +++ b/sdk/storage/azure-storage-blob/tests/recordings/test_common_blob_async.test_get_user_delegation_key_async.yaml @@ -0,0 +1,155 @@ +interactions: +- request: + body: client_id=68390a19-a897-236b-b453-488abf67b4fc&client_secret=3Usxz7pzkOeE7flc6Z187ubs5%2FcJnszGPjAiXmcwhaY%3D&grant_type=client_credentials&scope=https%3A%2F%2Fstorage.azure.com%2F.default + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '188' + Content-Type: + - application/x-www-form-urlencoded + User-Agent: + - python-requests/2.22.0 + method: POST + uri: https://login.microsoftonline.com/32f988bf-54f1-15af-36ab-2d7cd364db47/oauth2/v2.0/token + response: + body: + string: '{"token_type":"Bearer","expires_in":3599,"ext_expires_in":3599,"access_token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6ImllX3FXQ1hoWHh0MXpJRXN1NGM3YWNRVkduNCIsImtpZCI6ImllX3FXQ1hoWHh0MXpJRXN1NGM3YWNRVkduNCJ9.eyJhdWQiOiJodHRwczovL3N0b3JhZ2UuYXp1cmUuY29tIiwiaXNzIjoiaHR0cHM6Ly9zdHMud2luZG93cy5uZXQvNzJmOTg4YmYtODZmMS00MWFmLTkxYWItMmQ3Y2QwMTFkYjQ3LyIsImlhdCI6MTU2ODAxNDM5OSwibmJmIjoxNTY4MDE0Mzk5LCJleHAiOjE1NjgwMTgyOTksImFpbyI6IjQyRmdZQkE2SWkzVnYrL3dyUzMrMHcrMEZDMmVDQUE9IiwiYXBwaWQiOiI2ODM5MGExOS1hNjQzLTQ1OGItYjcyNi00MDhhYmY2N2I0ZmMiLCJhcHBpZGFjciI6IjEiLCJpZHAiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC83MmY5ODhiZi04NmYxLTQxYWYtOTFhYi0yZDdjZDAxMWRiNDcvIiwib2lkIjoiYzRmNDgyODktYmI4NC00MDg2LWIyNTAtNmY5NGE4ZjY0Y2VlIiwic3ViIjoiYzRmNDgyODktYmI4NC00MDg2LWIyNTAtNmY5NGE4ZjY0Y2VlIiwidGlkIjoiNzJmOTg4YmYtODZmMS00MWFmLTkxYWItMmQ3Y2QwMTFkYjQ3IiwidXRpIjoiUHMyLS1ZQjQ2MHluYUJ5S1ZDTXRBQSIsInZlciI6IjEuMCJ9.d3JgzJ664_Xmbbe7aWRDfUuw4g2W7MtKbCcOFAMko-gN5qXcmSNyAiOGHSUrvuORtEPOLye3CZuBmYw6hczqhl-9dF760mRxLyZjOjqE78YTJkudwU5iV69nr9VIAIMOk5Lh7N3sgUdWrSsz-ZUT8665da1pN3bpsl8zEHf9ZBM1qFia4J9OLtGQRb_Th2jJpm_LbMKdnMJ45cgoYxZeKcofo43we-BHSU5T-Jt4O8i6nalMSelp5ZuLI7KL308sm81iUyrrK3PCGGhGVdNIg5B4cOdMmq3B10m-Kk2gZfoPj2cJrsUkhY005mimBh7aa05B1BIwooRzxCE7t0ne_Q"}' + headers: + Cache-Control: + - no-cache, no-store + Content-Length: + - '1233' + Content-Type: + - application/json; charset=utf-8 + Date: + - Mon, 09 Sep 2019 07:38:19 GMT + Expires: + - '-1' + P3P: + - CP="DSP CUR OTPi IND OTRi ONL FIN" + Pragma: + - no-cache + Set-Cookie: + - fpc=AiNGuczA96ZBt-KrKg4lYNneSEc1AQAAAGv4B9UOAAAA; expires=Wed, 09-Oct-2019 + 07:38:19 GMT; path=/; secure; HttpOnly + - x-ms-gateway-slice=estsfd; path=/; secure; HttpOnly + - stsservicecookie=estsfd; path=/; secure; HttpOnly + Strict-Transport-Security: + - max-age=31536000; includeSubDomains + X-Content-Type-Options: + - nosniff + x-ms-ests-server: + - 2.1.9368.5 - WUS ProdSlices + x-ms-request-id: + - f9becd3e-7880-4ceb-a768-1c8a54232d00 + status: + code: 200 + message: OK +- request: + body: ' + + 2019-09-09T07:38:19Z2019-09-09T08:38:19Z' + headers: + Accept: + - application/xml + Content-Length: + - '130' + Content-Type: + - application/xml; charset=utf-8 + User-Agent: + - azsdk-python-storage-blob/12.0.0b2 Python/3.7.3 (Windows-10-10.0.18362-SP0) + x-ms-client-request-id: + - cb271d62-d2d4-11e9-bda0-001a7dda7113 + x-ms-date: + - Mon, 09 Sep 2019 07:38:19 GMT + x-ms-version: + - '2019-02-02' + method: POST + uri: https://oauthstoragename.blob.core.windows.net/?restype=service&comp=userdelegationkey + response: + body: + string: "\uFEFFc4f48289-bb84-4086-b250-6f94a8f64cee32f988bf-54f1-15af-36ab-2d7cd364db472019-09-09T07:38:19Z2019-09-09T08:38:19Zb2019-02-02kFFrkuiikpAqQywiYShReWmx/tyTvEh8yl8fhjZoH7Q=" + headers: + ? !!python/object/new:multidict._istr.istr + - Content-Type + : application/xml + ? !!python/object/new:multidict._istr.istr + - Date + : Mon, 09 Sep 2019 07:38:19 GMT + ? !!python/object/new:multidict._istr.istr + - Server + : Windows-Azure-Blob/1.0 Microsoft-HTTPAPI/2.0 + ? !!python/object/new:multidict._istr.istr + - Transfer-Encoding + : chunked + x-ms-client-request-id: cb271d62-d2d4-11e9-bda0-001a7dda7113 + x-ms-request-id: f99f7a82-d01e-00b2-4be1-66ac85000000 + x-ms-version: '2019-02-02' + status: + code: 200 + message: OK + url: !!python/object/new:yarl.URL + state: !!python/tuple + - !!python/object/new:urllib.parse.SplitResult + - https + - emilydevtest.blob.core.windows.net + - / + - restype=service&comp=userdelegationkey + - '' +- request: + body: ' + + 2019-09-09T07:38:19Z2019-09-09T08:38:19Z' + headers: + Accept: + - application/xml + Content-Length: + - '130' + Content-Type: + - application/xml; charset=utf-8 + User-Agent: + - azsdk-python-storage-blob/12.0.0b2 Python/3.7.3 (Windows-10-10.0.18362-SP0) + x-ms-client-request-id: + - cb88c8d2-d2d4-11e9-b99d-001a7dda7113 + x-ms-date: + - Mon, 09 Sep 2019 07:38:20 GMT + x-ms-version: + - '2019-02-02' + method: POST + uri: https://oauthstoragename.blob.core.windows.net/?restype=service&comp=userdelegationkey + response: + body: + string: "\uFEFFc4f48289-bb84-4086-b250-6f94a8f64cee32f988bf-54f1-15af-36ab-2d7cd364db472019-09-09T07:38:19Z2019-09-09T08:38:19Zb2019-02-02kFFrkuiikpAqQywiYShReWmx/tyTvEh8yl8fhjZoH7Q=" + headers: + ? !!python/object/new:multidict._istr.istr + - Content-Type + : application/xml + ? !!python/object/new:multidict._istr.istr + - Date + : Mon, 09 Sep 2019 07:38:19 GMT + ? !!python/object/new:multidict._istr.istr + - Server + : Windows-Azure-Blob/1.0 Microsoft-HTTPAPI/2.0 + ? !!python/object/new:multidict._istr.istr + - Transfer-Encoding + : chunked + x-ms-client-request-id: cb88c8d2-d2d4-11e9-b99d-001a7dda7113 + x-ms-request-id: f99f7af3-d01e-00b2-36e1-66ac85000000 + x-ms-version: '2019-02-02' + status: + code: 200 + message: OK + url: !!python/object/new:yarl.URL + state: !!python/tuple + - !!python/object/new:urllib.parse.SplitResult + - https + - emilydevtest.blob.core.windows.net + - / + - restype=service&comp=userdelegationkey + - '' +version: 1 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 b0ad55c71d5f..5cd2a7f5be2f 100644 --- a/sdk/storage/azure-storage-blob/tests/test_common_blob.py +++ b/sdk/storage/azure-storage-blob/tests/test_common_blob.py @@ -1264,6 +1264,67 @@ def test_account_sas(self): self.assertEqual(self.byte_data, blob_response.content) self.assertTrue(container_response.ok) + @record + def test_get_user_delegation_key(self): + # Act + token_credential = self.generate_oauth_token() + + # Action 1: make sure token works + service = BlobServiceClient(self._get_oauth_account_url(), credential=token_credential) + + start = datetime.utcnow() + expiry = datetime.utcnow() + timedelta(hours=1) + user_delegation_key_1 = service.get_user_delegation_key(key_start_time=start, key_expiry_time=expiry) + user_delegation_key_2 = service.get_user_delegation_key(key_start_time=start, key_expiry_time=expiry) + + # Assert key1 is valid + self.assertIsNotNone(user_delegation_key_1.signed_oid) + self.assertIsNotNone(user_delegation_key_1.signed_tid) + self.assertIsNotNone(user_delegation_key_1.signed_start) + self.assertIsNotNone(user_delegation_key_1.signed_expiry) + self.assertIsNotNone(user_delegation_key_1.signed_version) + self.assertIsNotNone(user_delegation_key_1.signed_service) + self.assertIsNotNone(user_delegation_key_1.value) + + # Assert key1 and key2 are equal, since they have the exact same start and end times + self.assertEqual(user_delegation_key_1.signed_oid, user_delegation_key_2.signed_oid) + self.assertEqual(user_delegation_key_1.signed_tid, user_delegation_key_2.signed_tid) + self.assertEqual(user_delegation_key_1.signed_start, user_delegation_key_2.signed_start) + self.assertEqual(user_delegation_key_1.signed_expiry, user_delegation_key_2.signed_expiry) + self.assertEqual(user_delegation_key_1.signed_version, user_delegation_key_2.signed_version) + self.assertEqual(user_delegation_key_1.signed_service, user_delegation_key_2.signed_service) + self.assertEqual(user_delegation_key_1.value, user_delegation_key_2.value) + + def test_user_delegation_sas_for_blob(self): + # SAS URL is calculated from storage key, so this test runs live only + if TestMode.need_recording_file(self.test_mode): + return + + # Arrange + token_credential = self.generate_oauth_token() + service_client = BlobServiceClient(self._get_oauth_account_url(), credential=token_credential) + user_delegation_key = service_client.get_user_delegation_key(datetime.utcnow(), + datetime.utcnow() + timedelta(hours=1)) + + container_client = service_client.create_container(self.get_resource_name('oauthcontainer')) + blob_client = container_client.get_blob_client(self.get_resource_name('oauthblob')) + blob_client.upload_blob(self.byte_data, length=len(self.byte_data)) + + token = blob_client.generate_shared_access_signature( + permission=BlobPermissions.READ, + expiry=datetime.utcnow() + timedelta(hours=1), + user_delegation_key=user_delegation_key, + account_name='emilydevtest', + ) + + # Act + # Use the generated identity sas + new_blob_client = BlobClient(blob_client.url, credential=token) + content = new_blob_client.download_blob() + + # Assert + self.assertEqual(self.byte_data, b"".join(list(content))) + @record def test_token_credential(self): token_credential = self.generate_oauth_token() 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 c77c59749a69..ba8fa6cc8ac6 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 @@ -192,6 +192,15 @@ async def _test_blob_exists(self): # Assert self.assertTrue(exists) + def _generate_oauth_token(self): + from azure.identity.aio import ClientSecretCredential + + return ClientSecretCredential( + self.settings.ACTIVE_DIRECTORY_APPLICATION_ID, + self.settings.ACTIVE_DIRECTORY_APPLICATION_SECRET, + self.settings.ACTIVE_DIRECTORY_TENANT_ID + ) + @record def test_blob_exists(self): loop = asyncio.get_event_loop() @@ -1620,14 +1629,49 @@ def test_account_sas(self): loop = asyncio.get_event_loop() loop.run_until_complete(self._test_account_sas()) + async def _test_get_user_delegation_key(self): + # TODO: figure out why recording does not work + if TestMode.need_recording_file(self.test_mode): + return + # Act + token_credential = self._generate_oauth_token() + + # Action 1: make sure token works + service = BlobServiceClient(self._get_oauth_account_url(), credential=token_credential) + + start = datetime.utcnow() + expiry = datetime.utcnow() + timedelta(hours=1) + user_delegation_key_1 = await service.get_user_delegation_key(key_start_time=start, key_expiry_time=expiry) + user_delegation_key_2 = await service.get_user_delegation_key(key_start_time=start, key_expiry_time=expiry) + + # Assert key1 is valid + self.assertIsNotNone(user_delegation_key_1.signed_oid) + self.assertIsNotNone(user_delegation_key_1.signed_tid) + self.assertIsNotNone(user_delegation_key_1.signed_start) + self.assertIsNotNone(user_delegation_key_1.signed_expiry) + self.assertIsNotNone(user_delegation_key_1.signed_version) + self.assertIsNotNone(user_delegation_key_1.signed_service) + self.assertIsNotNone(user_delegation_key_1.value) + + # Assert key1 and key2 are equal, since they have the exact same start and end times + self.assertEqual(user_delegation_key_1.signed_oid, user_delegation_key_2.signed_oid) + self.assertEqual(user_delegation_key_1.signed_tid, user_delegation_key_2.signed_tid) + self.assertEqual(user_delegation_key_1.signed_start, user_delegation_key_2.signed_start) + self.assertEqual(user_delegation_key_1.signed_expiry, user_delegation_key_2.signed_expiry) + self.assertEqual(user_delegation_key_1.signed_version, user_delegation_key_2.signed_version) + self.assertEqual(user_delegation_key_1.signed_service, user_delegation_key_2.signed_service) + self.assertEqual(user_delegation_key_1.value, user_delegation_key_2.value) + + def test_get_user_delegation_key_async(self): + loop = asyncio.get_event_loop() + loop.run_until_complete(self._test_get_user_delegation_key()) + async def _test_token_credential(self): - pytest.skip("") if TestMode.need_recording_file(self.test_mode): return await self._setup() - token_credential = self.generate_oauth_token() - get_token = token_credential.get_token + token_credential = self._generate_oauth_token() # Action 1: make sure token works service = BlobServiceClient(self._get_oauth_account_url(), credential=token_credential, transport=AiohttpTestTransport()) diff --git a/sdk/storage/azure-storage-blob/tests/test_container.py b/sdk/storage/azure-storage-blob/tests/test_container.py index 4ef49436f78d..18420af2b91c 100644 --- a/sdk/storage/azure-storage-blob/tests/test_container.py +++ b/sdk/storage/azure-storage-blob/tests/test_container.py @@ -24,6 +24,7 @@ AccessPolicy ) +from azure.identity import ClientSecretCredential from testcase import StorageTestCase, TestMode, record, LogCaptured #------------------------------------------------------------------------------ @@ -71,6 +72,14 @@ def _create_container(self, prefix=TEST_CONTAINER_PREFIX): pass return container + def _generate_oauth_token(self): + + return ClientSecretCredential( + self.settings.ACTIVE_DIRECTORY_APPLICATION_ID, + self.settings.ACTIVE_DIRECTORY_APPLICATION_SECRET, + self.settings.ACTIVE_DIRECTORY_TENANT_ID + ) + #--Test cases for containers ----------------------------------------- @record def test_create_container(self): @@ -1068,6 +1077,35 @@ def test_web_container_normal_operations_working(self): # delete container container.delete_container() + def test_user_delegation_sas_for_container(self): + # SAS URL is calculated from storage key, so this test runs live only + if TestMode.need_recording_file(self.test_mode): + return + + # Arrange + token_credential = self.generate_oauth_token() + service_client = BlobServiceClient(self._get_oauth_account_url(), credential=token_credential) + user_delegation_key = service_client.get_user_delegation_key(datetime.utcnow(), + datetime.utcnow() + timedelta(hours=1)) + + container_client = service_client.create_container(self.get_resource_name('oauthcontainer')) + token = container_client.generate_shared_access_signature( + expiry=datetime.utcnow() + timedelta(hours=1), + permission=ContainerPermissions.READ, + user_delegation_key=user_delegation_key, + account_name='emilydevtest' + ) + + blob_client = container_client.get_blob_client(self.get_resource_name('oauthblob')) + blob_content = self.get_random_text_data(1024) + blob_client.upload_blob(blob_content, length=len(blob_content)) + + # Act + new_blob_client = BlobClient(blob_client.url, credential=token) + content = new_blob_client.download_blob() + + # Assert + self.assertEqual(blob_content, b"".join(list(content)).decode('utf-8')) #------------------------------------------------------------------------------ if __name__ == '__main__': diff --git a/sdk/storage/azure-storage-file/azure/storage/file/_shared/models.py b/sdk/storage/azure-storage-file/azure/storage/file/_shared/models.py index 7185141649f9..50f891de3012 100644 --- a/sdk/storage/azure-storage-file/azure/storage/file/_shared/models.py +++ b/sdk/storage/azure-storage-file/azure/storage/file/_shared/models.py @@ -420,3 +420,36 @@ def __str__(self): Services.BLOB = Services(blob=True) Services.QUEUE = Services(queue=True) Services.FILE = Services(file=True) + + +class UserDelegationKey(object): + """ + Represents a user delegation key, provided to the user by Azure Storage + based on their Azure Active Directory access token. + + The fields are saved as simple strings since the user does not have to interact with this object; + to generate an identify SAS, the user can simply pass it to the right API. + + :ivar str signed_oid: + Object ID of this token. + :ivar str signed_tid: + Tenant ID of the tenant that issued this token. + :ivar str signed_start: + The datetime this token becomes valid. + :ivar str signed_expiry: + The datetime this token expires. + :ivar str signed_service: + What service this key is valid for. + :ivar str signed_version: + The version identifier of the REST service that created this token. + :ivar str value: + The user delegation key. + """ + def __init__(self): + self.signed_oid = None + self.signed_tid = None + self.signed_start = None + self.signed_expiry = None + self.signed_service = None + self.signed_version = None + self.value = None diff --git a/sdk/storage/azure-storage-file/azure/storage/file/_shared/response_handlers.py b/sdk/storage/azure-storage-file/azure/storage/file/_shared/response_handlers.py index bf92763aa509..fbf9889d762c 100644 --- a/sdk/storage/azure-storage-file/azure/storage/file/_shared/response_handlers.py +++ b/sdk/storage/azure-storage-file/azure/storage/file/_shared/response_handlers.py @@ -19,7 +19,8 @@ ClientAuthenticationError, DecodeError) -from .models import StorageErrorCode +from .parser import _to_utc_datetime +from .models import StorageErrorCode, UserDelegationKey if TYPE_CHECKING: @@ -131,3 +132,15 @@ def process_storage_error(storage_error): error.error_code = error_code error.additional_info = additional_data raise error + + +def parse_to_internal_user_delegation_key(service_user_delegation_key): + internal_user_delegation_key = UserDelegationKey() + internal_user_delegation_key.signed_oid = service_user_delegation_key.signed_oid + internal_user_delegation_key.signed_tid = service_user_delegation_key.signed_tid + internal_user_delegation_key.signed_start = _to_utc_datetime(service_user_delegation_key.signed_start) + internal_user_delegation_key.signed_expiry = _to_utc_datetime(service_user_delegation_key.signed_expiry) + internal_user_delegation_key.signed_service = service_user_delegation_key.signed_service + internal_user_delegation_key.signed_version = service_user_delegation_key.signed_version + internal_user_delegation_key.value = service_user_delegation_key.value + return internal_user_delegation_key diff --git a/sdk/storage/azure-storage-file/tests/recordings/test_file_async.test_set_file_properties_async.yaml b/sdk/storage/azure-storage-file/tests/recordings/test_file_async.test_set_file_properties_async.yaml index 8552d42a39ee..676c3732a9bb 100644 --- a/sdk/storage/azure-storage-file/tests/recordings/test_file_async.test_set_file_properties_async.yaml +++ b/sdk/storage/azure-storage-file/tests/recordings/test_file_async.test_set_file_properties_async.yaml @@ -7,9 +7,9 @@ interactions: content-type: - application/xml; charset=utf-8 x-ms-client-request-id: - - 8df4997a-d064-11e9-842c-001a7dda7113 + - 3a658f40-d357-11e9-a521-001a7dda7113 x-ms-date: - - Fri, 06 Sep 2019 05:09:50 GMT + - Mon, 09 Sep 2019 23:12:00 GMT x-ms-version: - '2019-02-02' method: PUT @@ -23,18 +23,18 @@ interactions: : '0' ? !!python/object/new:multidict._istr.istr - Date - : Fri, 06 Sep 2019 05:09:50 GMT + : Mon, 09 Sep 2019 23:12:00 GMT ? !!python/object/new:multidict._istr.istr - Etag - : '"0x8D732887226203F"' + : '"0x8D7357B1EAA605B"' ? !!python/object/new:multidict._istr.istr - Last-Modified - : Fri, 06 Sep 2019 05:09:50 GMT + : Mon, 09 Sep 2019 23:12:00 GMT ? !!python/object/new:multidict._istr.istr - Server : Windows-Azure-File/1.0 Microsoft-HTTPAPI/2.0 - x-ms-client-request-id: 8df4997a-d064-11e9-842c-001a7dda7113 - x-ms-request-id: 1cfdd9e5-301a-00aa-1071-6473e2000000 + x-ms-client-request-id: 3a658f40-d357-11e9-a521-001a7dda7113 + x-ms-request-id: 684e1630-f01a-006e-2963-6706db000000 x-ms-version: '2019-02-02' status: code: 201 @@ -55,11 +55,11 @@ interactions: content-type: - application/xml; charset=utf-8 x-ms-client-request-id: - - 8dffc79a-d064-11e9-82dc-001a7dda7113 + - 3a8c7bf8-d357-11e9-a069-001a7dda7113 x-ms-content-length: - '1024' x-ms-date: - - Fri, 06 Sep 2019 05:09:50 GMT + - Mon, 09 Sep 2019 23:12:00 GMT x-ms-file-attributes: - none x-ms-file-creation-time: @@ -83,25 +83,25 @@ interactions: : '0' ? !!python/object/new:multidict._istr.istr - Date - : Fri, 06 Sep 2019 05:09:50 GMT + : Mon, 09 Sep 2019 23:12:01 GMT ? !!python/object/new:multidict._istr.istr - Etag - : '"0x8D73288723342F3"' + : '"0x8D7357B1ED1C9E2"' ? !!python/object/new:multidict._istr.istr - Last-Modified - : Fri, 06 Sep 2019 05:09:50 GMT + : Mon, 09 Sep 2019 23:12:01 GMT ? !!python/object/new:multidict._istr.istr - Server : Windows-Azure-File/1.0 Microsoft-HTTPAPI/2.0 - x-ms-client-request-id: 8dffc79a-d064-11e9-82dc-001a7dda7113 + x-ms-client-request-id: 3a8c7bf8-d357-11e9-a069-001a7dda7113 x-ms-file-attributes: Archive - x-ms-file-change-time: '2019-09-06T05:09:50.9117683Z' - x-ms-file-creation-time: '2019-09-06T05:09:50.9117683Z' + x-ms-file-change-time: '2019-09-09T23:12:01.0529250Z' + x-ms-file-creation-time: '2019-09-09T23:12:01.0529250Z' x-ms-file-id: '13835128424026341376' - x-ms-file-last-write-time: '2019-09-06T05:09:50.9117683Z' + x-ms-file-last-write-time: '2019-09-09T23:12:01.0529250Z' x-ms-file-parent-id: '0' x-ms-file-permission-key: 4099112195243312672*10394889115079208622 - x-ms-request-id: d77571d0-701a-002d-1971-64e087000000 + x-ms-request-id: 64fb3cd0-501a-0093-7e63-6788fe000000 x-ms-request-server-encrypted: 'true' x-ms-version: '2019-02-02' status: @@ -143,9 +143,9 @@ interactions: User-Agent: - azsdk-python-storage-file/12.0.0b2 Python/3.7.3 (Windows-10-10.0.18362-SP0) x-ms-client-request-id: - - 8e0b0ed8-d064-11e9-9d11-001a7dda7113 + - 3ab04b4c-d357-11e9-b80e-001a7dda7113 x-ms-date: - - Fri, 06 Sep 2019 05:09:50 GMT + - Mon, 09 Sep 2019 23:12:01 GMT x-ms-range: - bytes=0-1023 x-ms-version: @@ -166,18 +166,18 @@ interactions: : aHnHh6kpL9BLaCLM3XK+Zg== ? !!python/object/new:multidict._istr.istr - Date - : Fri, 06 Sep 2019 05:09:50 GMT + : Mon, 09 Sep 2019 23:12:01 GMT ? !!python/object/new:multidict._istr.istr - Etag - : '"0x8D73288723762A8"' + : '"0x8D7357B1EDF3AA9"' ? !!python/object/new:multidict._istr.istr - Last-Modified - : Fri, 06 Sep 2019 05:09:50 GMT + : Mon, 09 Sep 2019 23:12:01 GMT ? !!python/object/new:multidict._istr.istr - Server : Windows-Azure-File/1.0 Microsoft-HTTPAPI/2.0 - x-ms-client-request-id: 8e0b0ed8-d064-11e9-9d11-001a7dda7113 - x-ms-request-id: d77571d2-701a-002d-1a71-64e087000000 + x-ms-client-request-id: 3ab04b4c-d357-11e9-b80e-001a7dda7113 + x-ms-request-id: 64fb3cd2-501a-0093-7f63-6788fe000000 x-ms-request-server-encrypted: 'true' x-ms-version: '2019-02-02' status: @@ -199,13 +199,13 @@ interactions: content-type: - application/xml; charset=utf-8 x-ms-client-request-id: - - 8e0e19c2-d064-11e9-a9e2-001a7dda7113 + - 3ab3d6de-d357-11e9-834d-001a7dda7113 x-ms-content-disposition: - inline x-ms-content-language: - spanish x-ms-date: - - Fri, 06 Sep 2019 05:09:51 GMT + - Mon, 09 Sep 2019 23:12:01 GMT x-ms-file-attributes: - preserve x-ms-file-creation-time: @@ -227,25 +227,25 @@ interactions: : '0' ? !!python/object/new:multidict._istr.istr - Date - : Fri, 06 Sep 2019 05:09:50 GMT + : Mon, 09 Sep 2019 23:12:01 GMT ? !!python/object/new:multidict._istr.istr - Etag - : '"0x8D73288723A70A2"' + : '"0x8D7357B1EE1FA79"' ? !!python/object/new:multidict._istr.istr - Last-Modified - : Fri, 06 Sep 2019 05:09:50 GMT + : Mon, 09 Sep 2019 23:12:01 GMT ? !!python/object/new:multidict._istr.istr - Server : Windows-Azure-File/1.0 Microsoft-HTTPAPI/2.0 - x-ms-client-request-id: 8e0e19c2-d064-11e9-a9e2-001a7dda7113 + x-ms-client-request-id: 3ab3d6de-d357-11e9-834d-001a7dda7113 x-ms-file-attributes: Archive - x-ms-file-change-time: '2019-09-06T05:09:50.9588130Z' - x-ms-file-creation-time: '2019-09-06T05:09:50.9117683Z' + x-ms-file-change-time: '2019-09-09T23:12:01.1590265Z' + x-ms-file-creation-time: '2019-09-09T23:12:01.0529250Z' x-ms-file-id: '13835128424026341376' - x-ms-file-last-write-time: '2019-09-06T05:09:50.9117683Z' + x-ms-file-last-write-time: '2019-09-09T23:12:01.0529250Z' x-ms-file-parent-id: '0' x-ms-file-permission-key: 4099112195243312672*10394889115079208622 - x-ms-request-id: d77571d3-701a-002d-1b71-64e087000000 + x-ms-request-id: 64fb3cd3-501a-0093-8063-6788fe000000 x-ms-request-server-encrypted: 'true' x-ms-version: '2019-02-02' status: @@ -267,9 +267,9 @@ interactions: content-type: - application/xml; charset=utf-8 x-ms-client-request-id: - - 8e1198c0-d064-11e9-8d7f-001a7dda7113 + - 3ab6551c-d357-11e9-b471-001a7dda7113 x-ms-date: - - Fri, 06 Sep 2019 05:09:51 GMT + - Mon, 09 Sep 2019 23:12:01 GMT x-ms-version: - '2019-02-02' method: HEAD @@ -292,25 +292,25 @@ interactions: : application/xml; charset=utf-8 ? !!python/object/new:multidict._istr.istr - Date - : Fri, 06 Sep 2019 05:09:50 GMT + : Mon, 09 Sep 2019 23:12:01 GMT ? !!python/object/new:multidict._istr.istr - Etag - : '"0x8D73288723A70A2"' + : '"0x8D7357B1EE1FA79"' ? !!python/object/new:multidict._istr.istr - Last-Modified - : Fri, 06 Sep 2019 05:09:50 GMT + : Mon, 09 Sep 2019 23:12:01 GMT ? !!python/object/new:multidict._istr.istr - Server : Windows-Azure-File/1.0 Microsoft-HTTPAPI/2.0 - x-ms-client-request-id: 8e1198c0-d064-11e9-8d7f-001a7dda7113 + x-ms-client-request-id: 3ab6551c-d357-11e9-b471-001a7dda7113 x-ms-file-attributes: Archive - x-ms-file-change-time: '2019-09-06T05:09:50.9588130Z' - x-ms-file-creation-time: '2019-09-06T05:09:50.9117683Z' + x-ms-file-change-time: '2019-09-09T23:12:01.1590265Z' + x-ms-file-creation-time: '2019-09-09T23:12:01.0529250Z' x-ms-file-id: '13835128424026341376' - x-ms-file-last-write-time: '2019-09-06T05:09:50.9117683Z' + x-ms-file-last-write-time: '2019-09-09T23:12:01.0529250Z' x-ms-file-parent-id: '0' x-ms-file-permission-key: 4099112195243312672*10394889115079208622 - x-ms-request-id: d77571d4-701a-002d-1c71-64e087000000 + x-ms-request-id: 64fb3cd4-501a-0093-0163-6788fe000000 x-ms-server-encrypted: 'true' x-ms-type: File x-ms-version: '2019-02-02' diff --git a/sdk/storage/azure-storage-queue/azure/storage/queue/_shared/models.py b/sdk/storage/azure-storage-queue/azure/storage/queue/_shared/models.py index 7185141649f9..50f891de3012 100644 --- a/sdk/storage/azure-storage-queue/azure/storage/queue/_shared/models.py +++ b/sdk/storage/azure-storage-queue/azure/storage/queue/_shared/models.py @@ -420,3 +420,36 @@ def __str__(self): Services.BLOB = Services(blob=True) Services.QUEUE = Services(queue=True) Services.FILE = Services(file=True) + + +class UserDelegationKey(object): + """ + Represents a user delegation key, provided to the user by Azure Storage + based on their Azure Active Directory access token. + + The fields are saved as simple strings since the user does not have to interact with this object; + to generate an identify SAS, the user can simply pass it to the right API. + + :ivar str signed_oid: + Object ID of this token. + :ivar str signed_tid: + Tenant ID of the tenant that issued this token. + :ivar str signed_start: + The datetime this token becomes valid. + :ivar str signed_expiry: + The datetime this token expires. + :ivar str signed_service: + What service this key is valid for. + :ivar str signed_version: + The version identifier of the REST service that created this token. + :ivar str value: + The user delegation key. + """ + def __init__(self): + self.signed_oid = None + self.signed_tid = None + self.signed_start = None + self.signed_expiry = None + self.signed_service = None + self.signed_version = None + self.value = None diff --git a/sdk/storage/azure-storage-queue/azure/storage/queue/_shared/response_handlers.py b/sdk/storage/azure-storage-queue/azure/storage/queue/_shared/response_handlers.py index bf92763aa509..fbf9889d762c 100644 --- a/sdk/storage/azure-storage-queue/azure/storage/queue/_shared/response_handlers.py +++ b/sdk/storage/azure-storage-queue/azure/storage/queue/_shared/response_handlers.py @@ -19,7 +19,8 @@ ClientAuthenticationError, DecodeError) -from .models import StorageErrorCode +from .parser import _to_utc_datetime +from .models import StorageErrorCode, UserDelegationKey if TYPE_CHECKING: @@ -131,3 +132,15 @@ def process_storage_error(storage_error): error.error_code = error_code error.additional_info = additional_data raise error + + +def parse_to_internal_user_delegation_key(service_user_delegation_key): + internal_user_delegation_key = UserDelegationKey() + internal_user_delegation_key.signed_oid = service_user_delegation_key.signed_oid + internal_user_delegation_key.signed_tid = service_user_delegation_key.signed_tid + internal_user_delegation_key.signed_start = _to_utc_datetime(service_user_delegation_key.signed_start) + internal_user_delegation_key.signed_expiry = _to_utc_datetime(service_user_delegation_key.signed_expiry) + internal_user_delegation_key.signed_service = service_user_delegation_key.signed_service + internal_user_delegation_key.signed_version = service_user_delegation_key.signed_version + internal_user_delegation_key.value = service_user_delegation_key.value + return internal_user_delegation_key