You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@libcloud.apache.org by cl...@apache.org on 2020/01/14 13:56:28 UTC

[libcloud] branch trunk updated: Implement SAS URL generation for Azure Storage (#1408)

This is an automated email from the ASF dual-hosted git repository.

clewolff pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/libcloud.git


The following commit(s) were added to refs/heads/trunk by this push:
     new bb865b1  Implement SAS URL generation for Azure Storage (#1408)
bb865b1 is described below

commit bb865b1ee029b9bceaa83917c9b9f7b16033547e
Author: Clemens Wolff <cl...@apache.org>
AuthorDate: Tue Jan 14 08:56:18 2020 -0500

    Implement SAS URL generation for Azure Storage (#1408)
---
 CHANGES.rst                               | 13 +++++
 libcloud/storage/drivers/azure_blobs.py   | 80 ++++++++++++++++++++++++++++++-
 libcloud/test/storage/test_azure_blobs.py | 10 ++++
 3 files changed, 102 insertions(+), 1 deletion(-)

diff --git a/CHANGES.rst b/CHANGES.rst
index 4c003f0..c558f77 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -113,6 +113,19 @@ Storage
   (GITHUB-1397)
   [Clemens Wolff - @c-w]
 
+- [Azure Blobs] Implement ``get_object_cdn_url`` for the Azure Storage driver.
+
+  Leveraging Azure storage service shared access signatures, the Azure Storage
+  driver can now be used to generate temporary URLs that grant clients read
+  access to objects. The URLs expire after a certain period of time, either
+  configured via the ``ex_expiry`` argument or the
+  ``LIBCLOUD_AZURE_STORAGE_CDN_URL_EXPIRY_HOURS`` environment variable
+  (default: 24 hours).
+
+  Reported by @rvolykh.
+  (GITHUB-1403, GITHUB-1408)
+  [Clemens Wolff - @c-w]
+
 Changes in Apache Libcloud v2.8.0
 ---------------------------------
 
diff --git a/libcloud/storage/drivers/azure_blobs.py b/libcloud/storage/drivers/azure_blobs.py
index 3a2e2a3..a7cc601 100644
--- a/libcloud/storage/drivers/azure_blobs.py
+++ b/libcloud/storage/drivers/azure_blobs.py
@@ -16,11 +16,15 @@
 from __future__ import with_statement
 
 import base64
+import hashlib
+import hmac
 import os
 import binascii
+from datetime import datetime, timedelta
 
 from libcloud.utils.py3 import ET
 from libcloud.utils.py3 import httplib
+from libcloud.utils.py3 import urlencode
 from libcloud.utils.py3 import urlquote
 from libcloud.utils.py3 import tostring
 from libcloud.utils.py3 import b
@@ -65,6 +69,16 @@ AZURE_LEASE_PERIOD = int(
 
 AZURE_STORAGE_HOST_SUFFIX = 'blob.core.windows.net'
 
+AZURE_STORAGE_CDN_URL_DATE_FORMAT = '%Y-%m-%dT%H:%M:%SZ'
+
+AZURE_STORAGE_CDN_URL_START_MINUTES = float(
+    os.getenv('LIBCLOUD_AZURE_STORAGE_CDN_URL_START_MINUTES', '5')
+)
+
+AZURE_STORAGE_CDN_URL_EXPIRY_HOURS = float(
+    os.getenv('LIBCLOUD_AZURE_STORAGE_CDN_URL_EXPIRY_HOURS', '24')
+)
+
 
 class AzureBlobLease(object):
     """
@@ -180,7 +194,7 @@ class AzureBlobsConnection(AzureConnection):
 
         return action
 
-    API_VERSION = '2016-05-31'
+    API_VERSION = '2018-11-09'
 
 
 class AzureBlobsStorageDriver(StorageDriver):
@@ -489,6 +503,70 @@ class AzureBlobsStorageDriver(StorageDriver):
         raise ObjectDoesNotExistError(value=None, driver=self,
                                       object_name=object_name)
 
+    def get_object_cdn_url(self, obj,
+                           ex_expiry=AZURE_STORAGE_CDN_URL_EXPIRY_HOURS):
+        """
+        Return a SAS URL that enables reading the given object.
+
+        :param obj: Object instance.
+        :type  obj: :class:`Object`
+
+        :param ex_expiry: The number of hours after which the URL expires.
+                          Defaults to 24 hours.
+        :type  ex_expiry: ``float``
+
+        :return: A SAS URL for the object.
+        :rtype: ``str``
+        """
+        object_path = self._get_object_path(obj.container, obj.name)
+
+        now = datetime.utcnow()
+        start = now - timedelta(minutes=AZURE_STORAGE_CDN_URL_START_MINUTES)
+        expiry = now + timedelta(hours=ex_expiry)
+
+        params = {
+            'st': start.strftime(AZURE_STORAGE_CDN_URL_DATE_FORMAT),
+            'se': expiry.strftime(AZURE_STORAGE_CDN_URL_DATE_FORMAT),
+            'sp': 'r',
+            'spr': 'https' if self.secure else 'http,https',
+            'sv': self.connectionCls.API_VERSION,
+            'sr': 'b',
+        }
+
+        string_to_sign = '\n'.join((
+            params['sp'],
+            params['st'],
+            params['se'],
+            '/blob/{}{}'.format(self.key, object_path),
+            '',  # signedIdentifier
+            '',  # signedIP
+            params['spr'],
+            params['sv'],
+            params['sr'],
+            '',  # snapshot
+            '',  # rscc
+            '',  # rscd
+            '',  # rsce
+            '',  # rscl
+            '',  # rsct
+        ))
+
+        params['sig'] = base64.b64encode(
+            hmac.new(
+                self.secret,
+                string_to_sign.encode('utf-8'),
+                hashlib.sha256
+            ).digest()
+        ).decode('utf-8')
+
+        return '{scheme}://{host}:{port}{action}?{sas_token}'.format(
+            scheme='https' if self.secure else 'http',
+            host=self.connection.host,
+            port=self.connection.port,
+            action=self.connection.morph_action_hook(object_path),
+            sas_token=urlencode(params),
+        )
+
     def _get_container_path(self, container):
         """
         Return a container path
diff --git a/libcloud/test/storage/test_azure_blobs.py b/libcloud/test/storage/test_azure_blobs.py
index 389d581..acd10d1 100644
--- a/libcloud/test/storage/test_azure_blobs.py
+++ b/libcloud/test/storage/test_azure_blobs.py
@@ -502,6 +502,16 @@ class AzureBlobsTests(unittest.TestCase):
         self.assertTrue(container.extra['lease']['state'], 'available')
         self.assertTrue(container.extra['meta_data']['meta1'], 'value1')
 
+    def test_get_object_cdn_url(self):
+        obj = self.driver.get_object(container_name='test_container200',
+                                     object_name='test')
+
+        url = urlparse.urlparse(self.driver.get_object_cdn_url(obj))
+        query = urlparse.parse_qs(url.query)
+
+        self.assertEqual(len(query['sig']), 1)
+        self.assertGreater(len(query['sig'][0]), 0)
+
     def test_get_object_container_doesnt_exist(self):
         # This method makes two requests which makes mocking the response a bit
         # trickier