Sfoglia il codice sorgente

Merge pull request #294 from FabioRosado/fr/signed-url

Allow users to create signed urls with write permissions
Nuwan Goonasekera 4 anni fa
parent
commit
3edd2c58cb

+ 5 - 1
cloudbridge/interfaces/resources.py

@@ -2204,7 +2204,7 @@ class BucketObject(CloudResource):
         pass
 
     @abstractmethod
-    def generate_url(self, expires_in):
+    def generate_url(self, expires_in, writable=False):
         """
         Generate a signed URL to this object.
 
@@ -2214,6 +2214,10 @@ class BucketObject(CloudResource):
 
         :type expires_in: ``int``
         :param expires_in: Time to live of the generated URL in seconds.
+        :type writable: ``bool``
+        :param writable: Write permission for this signed URL. Users with the URL
+            will be able to upload to this object, but they will NOT be able to
+            read from it.
 
         :rtype: ``str``
         :return: A URL to access the object.

+ 5 - 1
cloudbridge/providers/aws/resources.py

@@ -871,7 +871,11 @@ class AWSBucketObject(BaseBucketObject):
     def delete(self):
         self._obj.delete()
 
-    def generate_url(self, expires_in):
+    def generate_url(self, expires_in, writable=False):
+        if writable:
+            return self._provider.s3_conn.meta.client.generate_presigned_post(
+                self._obj.bucket_name, self.id, ExpiresIn=expires_in
+            )
         return self._provider.s3_conn.meta.client.generate_presigned_url(
             'get_object',
             Params={'Bucket': self._obj.bucket_name, 'Key': self.id},

+ 2 - 2
cloudbridge/providers/azure/azure_client.py

@@ -451,7 +451,7 @@ class AzureClient(object):
         blob_client = self.blob_client(container_name, blob_name)
         blob_client.delete_blob(delete_snapshots)
 
-    def get_blob_url(self, container_name, blob_name, expiry_time):
+    def get_blob_url(self, container_name, blob_name, expiry_time, writable):
         now = datetime.datetime.utcnow()
         expiry = now + datetime.timedelta(
             seconds=expiry_time)
@@ -462,7 +462,7 @@ class AzureClient(object):
         )
         sas = generate_blob_sas(
             self.storage_account, container_name, blob_name,
-            permission=BlobSasPermissions(read=True), expiry=expiry,
+            permission=BlobSasPermissions(read=True, write=writable), expiry=expiry,
             user_delegation_key=delegation_key
         )
         url = (

+ 2 - 2
cloudbridge/providers/azure/resources.py

@@ -258,12 +258,12 @@ class AzureBucketObject(BaseBucketObject):
         """
         self._blob_client.delete_blob()
 
-    def generate_url(self, expires_in):
+    def generate_url(self, expires_in, writable=False):
         """
         Generate a URL to this object.
         """
         return self._provider.azure_client.get_blob_url(
-            self._container, self.name, expires_in)
+            self._container, self.name, expires_in, writable)
 
     def refresh(self):
         pass

+ 9 - 2
cloudbridge/providers/gcp/resources.py

@@ -1975,14 +1975,21 @@ class GCPBucketObject(BaseBucketObject):
              .delete(bucket=self._obj['bucket'], object=self.name)
              .execute())
 
-    def generate_url(self, expires_in):
+    def generate_url(self, expires_in, writable=False):
         """
         Generates a signed URL accessible to everyone.
+
+        Note that if the user asks for write permissions, we need
+        to set the `http_method` as PUT so the user can keep updating
+        the files with the same URL.
+
         """
+        http_method = "PUT" if writable else "GET"
+
         # pylint:disable=protected-access
         return helpers.generate_signed_url(
             self._provider._credentials, self._obj['bucket'], self.name,
-            expiration=expires_in)
+            expiration=expires_in, http_method=http_method)
 
     def refresh(self):
         # pylint:disable=protected-access

+ 3 - 2
cloudbridge/providers/openstack/resources.py

@@ -1316,7 +1316,8 @@ class OpenStackBucketObject(BaseBucketObject):
                 result = result and del_res['success']
         return result
 
-    def generate_url(self, expires_in):
+    def generate_url(self, expires_in, writable=False):
+        http_method = "POST" if writable else "GET"
         # Set a temp url key on the object (http://bit.ly/2NBiXGD)
         temp_url_key = "cloudbridge-tmp-url-key"
         self._provider.swift.post_account(
@@ -1325,7 +1326,7 @@ class OpenStackBucketObject(BaseBucketObject):
         access_point = "{0}://{1}".format(base_url.scheme, base_url.netloc)
         url_path = "/".join([base_url.path, self.cbcontainer.name, self.name])
         return urljoin(access_point, generate_temp_url(url_path, expires_in,
-                                                       temp_url_key, 'GET'))
+                                                       temp_url_key, http_method))
 
     def refresh(self):
         self._obj = self.cbcontainer.objects.get(self.id)._obj

+ 53 - 0
docs/topics/object_storage.rst

@@ -68,3 +68,56 @@ Once a provider is obtained, you can access the container as usual:
     bucket = provider.storage.buckets.get(container)
     obj = bucket.objects.create('my_object.txt')
     obj.upload_from_file(source)
+
+
+Generating signed URLs
+----------------------
+
+Signed URLs are a great way to allow users who do not have credentials for
+the cloud provider of your choice, to interact with an object within a
+storage bucket.
+
+You can generate signed URLs with ``GET`` permissions to allow a user to
+get an object. 
+
+.. code-block:: python
+ 
+    provider = CloudProviderFactory().create_provider(
+        ProviderList.AWS,
+        {'aws_access_key': 'ACCESS_KEY',
+         'aws_secret_key': 'SECRET_KEY',
+         'aws_session_token': 'MY_SESSION_TOKEN'}) 
+
+    bucket = provider.storage.buckets.get("my-bucket")
+    obj = bucket.objects.get("my-file.txt")
+
+    url = obj.generate_url(expires_in=7200)
+
+You can also generate a signed URL with `PUT``permissions to allow users 
+to upload files to your storage bucket.
+
+.. code-block:: python
+ 
+    provider = CloudProviderFactory().create_provider(
+        ProviderList.AWS,
+        {'aws_access_key': 'ACCESS_KEY',
+         'aws_secret_key': 'SECRET_KEY',
+         'aws_session_token': 'MY_SESSION_TOKEN'}) 
+
+    bucket = provider.storage.buckets.get("my-bucket")
+    obj = bucket.objects.create("my-file.txt")
+    url = obj.generate_url(expires_in=7200, writable=True)
+
+
+With your signed URL, you or someone on your team can upload a file like this
+
+.. code-block:: python
+
+    import requests
+
+    content = b"Hello world!"
+    # Example for AWS
+    requests.put(url["url"], data=url["fields"], files={"file": ("my-file.txt", content)})
+
+    # Example for GCP/Azure
+    requests.put(url, data=content)

+ 24 - 0
tests/test_object_store_service.py

@@ -193,6 +193,30 @@ class CloudObjectStoreServiceTestCase(ProviderTestBase):
                         " access generated url")
                 self.assertEqual(requests.get(url).content, content)
 
+    @helpers.skipIfNoService(['storage.buckets'])
+    def test_generate_url_write_permissions(self):
+        name = "cbtestbucketobjs-{0}".format(helpers.get_uuid())
+        test_bucket = self.provider.storage.buckets.create(name)
+
+        with cb_helpers.cleanup_action(lambda: test_bucket.delete()):
+            obj_name = "hello_upload_download.txt"
+            obj = test_bucket.objects.create(obj_name)
+
+            with cb_helpers.cleanup_action(lambda: obj.delete()):
+                content = b"Hello World. Generate a url."
+
+                url = obj.generate_url(100, writable=True)
+                if isinstance(self.provider, TestMockHelperMixin):
+                    raise self.skipTest(
+                        "Skipping rest of test - mock providers can't"
+                        " access generated url")
+                else:
+                    requests.put(url, data=content)
+                
+                obj = test_bucket.objects.get(obj_name)
+                obj_content = [content for content in obj.iter_content()]
+                self.assertEqual(obj_content[0], content)
+
     @helpers.skipIfNoService(['storage.buckets'])
     def test_upload_download_bucket_content_from_file(self):
         name = "cbtestbucketobjs-{0}".format(helpers.get_uuid())