Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
512d5a4
[File][RestParity]Rest Parity Sync
xiafu-msft Aug 29, 2019
8bed721
[File][RestParity]Rest Parity Async
xiafu-msft Aug 29, 2019
22b801a
[File][RestParity]Add Rest Parity Async Recording Files
xiafu-msft Aug 29, 2019
bfd3fbd
[File][RestParity]Fix CI
xiafu-msft Sep 6, 2019
7707acf
[File][RestParity]Recording again to fix CI
xiafu-msft Sep 6, 2019
e945a70
Add Generated Code
xiafu-msft Sep 6, 2019
8c821a6
Stylistic Things and Record
xiafu-msft Sep 6, 2019
8c5c22c
[Swagger][BugFix]workaround to fix swagger generated code
xiafu-msft Sep 9, 2019
d46e3dc
[File][RestParity]Add Create_Permission API and Test
xiafu-msft Sep 9, 2019
fcf0ec6
Fix Test
xiafu-msft Sep 9, 2019
3c6b3e0
Fix Pylint
xiafu-msft Sep 9, 2019
62dc377
Revert the workaround
xiafu-msft Sep 9, 2019
3d00c52
[File][RestParity]Tweak Documentation and Tests
xiafu-msft Sep 9, 2019
9afed1d
delete .dat file
xiafu-msft Sep 9, 2019
bd9f32e
[File][RestParity]Rest Parity Async
xiafu-msft Aug 29, 2019
99a07ad
[Blob][IdentitySAS]Add Identity SAS
xiafu-msft Aug 30, 2019
e7811c1
[Blob][IdentitySAS]Stylistic Things
xiafu-msft Sep 9, 2019
a565b90
[Blob][IdentitySAS]Stylistic Things
xiafu-msft Sep 9, 2019
aed02d4
[Blob][IdentitySAS]Add account_name parameter
xiafu-msft Sep 9, 2019
17540b4
Fix Pylint
xiafu-msft Sep 9, 2019
9c327cc
Merge branch 'feature/storage-preview3' into identity-sas
xiafu-msft Sep 9, 2019
450a66e
Merge branch 'feature/storage-preview3' into identity-sas
xiafu-msft Sep 9, 2019
969816b
Fix Pylint
xiafu-msft Sep 10, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
[Blob][IdentitySAS]Add Identity SAS
  • Loading branch information
xiafu-msft committed Sep 9, 2019
commit 99a07adfa3a43cad8d30ca72dff4f77ad342bebc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

import sys

from .models import UserDelegationKey

if sys.version_info < (3,):
def _str(value):
if isinstance(value, unicode): # pylint: disable=undefined-variable
Expand All @@ -18,3 +20,15 @@ def _str(value):

def _to_utc_datetime(value):
return value.strftime('%Y-%m-%dT%H:%M:%SZ')


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
Original file line number Diff line number Diff line change
Expand Up @@ -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()


Expand Down
18 changes: 14 additions & 4 deletions sdk/storage/azure-storage-blob/azure/storage/blob/blob_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ def __init__(
except AttributeError:
raise ValueError("Blob URL must be a string.")
parsed_url = urlparse(blob_url.rstrip('/'))
self.account_name = parsed_url.hostname.split('.')[0]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will not always work - a customer could be using a custom URL, it wont always be the storage account name

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See how this is handled in generate_shared_access_signature for reference

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parsed_url.hostname.split('.')[0] [](start = 28, length = 33)

Not going to work with custom domains

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there any pattern we can follow for customized domain?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so - a customized domain could be supplied with no key from which to generate a sas. An error is already raised in get_blob_shared_access_signature in this scenario, so you shouldn't need to do additional work for this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

while I think account_name is required to generate sas token, if users are using oauth token as credential, then account_name cannot be parsed out right?

if not parsed_url.path and not (container and blob):
raise ValueError("Please specify a container and blob name.")
if not parsed_url.netloc:
Expand Down Expand Up @@ -231,7 +232,8 @@ def generate_shared_access_signature(
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: (...) -> Any
"""
Expand Down Expand Up @@ -289,12 +291,20 @@ 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:
sas = BlobSharedAccessSignature(self.account_name, user_delegation_key=user_delegation_key)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

self.account_name [](start = 44, length = 17)

use self.credential.account_name instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

credential could be different type, I think this is only working for SharedKeyCredential

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct - we can only guarantee that we have the account_name if the user is authenticating with SharedKeyCredentials

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

based on our discussion offline, we want to add a parameter account_name to generate_shared_access_signature method, so when they are using oauth token as credential, they have to provide account_name to generate sas token.

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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@
Union, Optional, Any, Iterable, Dict, List,
TYPE_CHECKING
)

from ._generated.models import KeyInfo
from ._shared.parser import _to_utc_datetime, _parse_to_internal_user_delegation_key

try:
from urllib.parse import urlparse
except ImportError:
Expand All @@ -18,7 +22,7 @@
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 ._generated import AzureBlobStorage
Expand Down Expand Up @@ -223,6 +227,33 @@ 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]
):
# 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)
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]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ def __init__(
except AttributeError:
raise ValueError("Container URL must be a string.")
parsed_url = urlparse(container_url.rstrip('/'))
self.account_name = parsed_url.hostname.split('.')[0]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment as above re account name

if not parsed_url.path and not container:
raise ValueError("Please specify a container name.")
if not parsed_url.netloc:
Expand Down Expand Up @@ -192,7 +193,8 @@ def generate_shared_access_signature(
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.
Expand Down Expand Up @@ -248,6 +250,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

Expand All @@ -259,9 +266,12 @@ 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:
sas = BlobSharedAccessSignature(self.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,
Expand Down
61 changes: 61 additions & 0 deletions sdk/storage/azure-storage-blob/tests/test_common_blob.py
Original file line number Diff line number Diff line change
Expand Up @@ -1264,6 +1264,67 @@ def test_account_sas(self):
self.assertEqual(self.byte_data, blob_response.content)
self.assertTrue(container_response.ok)

def test_get_user_delegation_key(self):
if TestMode.need_recording_file(self.test_mode):
return
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add comment to clarify why the test cannot run in playback mode

# 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,
)

# 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()
Expand Down
37 changes: 37 additions & 0 deletions sdk/storage/azure-storage-blob/tests/test_container.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
AccessPolicy
)

from azure.identity import ClientSecretCredential
from testcase import StorageTestCase, TestMode, record, LogCaptured

#------------------------------------------------------------------------------
Expand Down Expand Up @@ -71,6 +72,14 @@ def _create_container(self, prefix=TEST_CONTAINER_PREFIX):
pass
return container

def _generate_oauth_token(self):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this being used? Also the testcase.py should have the same method AFAIK.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is for async version. the one in testcase is for sync


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):
Expand Down Expand Up @@ -1068,6 +1077,34 @@ 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,
)

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__':
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading