]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/commitdiff
msazure_container.py: Implement authentication signature scheme.
authorDaira Hopwood <david-sarah@jacaranda.org>
Mon, 15 Apr 2013 20:03:37 +0000 (21:03 +0100)
committerDaira Hopwood <daira@jacaranda.org>
Wed, 9 Apr 2014 00:33:55 +0000 (01:33 +0100)
Signed-off-by: Daira Hopwood <david-sarah@jacaranda.org>
src/allmydata/storage/backends/cloud/cloud_backend.py
src/allmydata/storage/backends/cloud/msazure/__init__.py [new file with mode: 0644]
src/allmydata/storage/backends/cloud/msazure/msazure_container.py [new file with mode: 0644]
src/allmydata/test/test_storage.py

index 14dee7498b48e07c8dcd5c77da9a7df5a438f435..786c993b6b79f9377b9a140665103d32726bec0a 100644 (file)
@@ -20,7 +20,7 @@ from allmydata.storage.backends.cloud.cloud_common import get_share_key, delete_
 from allmydata.mutable.layout import MUTABLE_MAGIC
 
 
-CLOUD_INTERFACES = ("cloud.s3", "cloud.openstack", "cloud.googlestorage")
+CLOUD_INTERFACES = ("cloud.s3", "cloud.openstack", "cloud.googlestorage", "cloud.msazure")
 
 
 def get_cloud_share(container, storage_index, shnum, total_size):
diff --git a/src/allmydata/storage/backends/cloud/msazure/__init__.py b/src/allmydata/storage/backends/cloud/msazure/__init__.py
new file mode 100644 (file)
index 0000000..36b7c4d
--- /dev/null
@@ -0,0 +1,3 @@
+from allmydata.storage.backends.cloud.msazure.msazure_container import configure_msazure_container
+
+configure_container = configure_msazure_container
diff --git a/src/allmydata/storage/backends/cloud/msazure/msazure_container.py b/src/allmydata/storage/backends/cloud/msazure/msazure_container.py
new file mode 100644 (file)
index 0000000..00561e2
--- /dev/null
@@ -0,0 +1,97 @@
+"""
+Storage backend using Microsoft Azure Blob Storage service.
+
+See http://msdn.microsoft.com/en-us/library/windowsazure/dd179428.aspx for
+details on the authentication scheme.
+"""
+import urlparse
+import base64
+import hmac
+import hashlib
+
+from zope.interface import implements
+
+from allmydata.storage.backends.cloud.cloud_common import IContainer, \
+     ContainerItem, ContainerListing, CommonContainerMixin
+
+def configure_msazure_container(*args):
+    pass
+
+
+class MSAzureStorageContainer(CommonContainerMixin):
+    implements(IContainer)
+
+    USER_AGENT = "Tahoe-LAFS Microsoft Azure client"
+
+    def __init__(self, account_name, account_key, container_name,
+                 override_reactor=None):
+        CommonContainerMixin.__init__(self, container_name, override_reactor)
+        self._account_name = account_name
+        self._account_key = base64.b64decode(account_key)
+
+    def _calculate_presignature(self, method, url, headers):
+        """
+        Calculate the value to be signed for the given request information.
+
+        We only implement a subset of the standard. In particular, we assume
+        x-ms-date header has been provided, so don't include any Date header.
+
+        The HMAC, and formatting into HTTP header, is not done in this layer.
+        """
+        parsed_url = urlparse.urlparse(url)
+        result = method + "\n"
+        # Add standard headers:
+        for header in ['content-encoding', 'content-language',
+                       'content-length', 'content-md5',
+                       'content-type', 'date', 'if-modified-since',
+                        'if-match', 'if-none-match',
+                       'if-unmodified-since', 'range']:
+            value = headers.getRawHeaders(header, [""])[0]
+            if header == "date":
+                value = ""
+            result += value + "\n"
+
+        # Add x-ms headers:
+        x_ms_headers = []
+        x_ms_date = False
+        for name, values in headers.getAllRawHeaders():
+            name = name.lower()
+            if name.startswith("x-ms"):
+                x_ms_headers.append("%s:%s" % (name, values[0]))
+                if name == "x-ms-date":
+                    x_ms_date = True
+        x_ms_headers.sort()
+        if x_ms_headers:
+            result += "\n".join(x_ms_headers) + "\n"
+        if not x_ms_date:
+            raise ValueError("x-ms-date must be included")
+
+        # Add path:
+        result += "/%s%s" % (self._account_name, parsed_url.path)
+
+        # Add query args:
+        query_args = urlparse.parse_qs(parsed_url.query).items()
+        query_args.sort()
+        for name, value in query_args:
+            result += "\n%s:%s" % (name, ",".join(value))
+        return result
+
+    def _calculate_signature(self, method, url, headers):
+        """
+        Calculate the signature for the given request information.
+
+        This includes base64ing and HMACing.
+
+        headers is a twisted.web.http_headers.Headers instance.
+
+        The returned value is suitable for us as an Authorization header.
+        """
+        data = self._calculate_presignature(method, url, headers)
+        signature = hmac.HMAC(self._account_key, data, hashlib.sha256).digest()
+        return "SharedKey %s:%s" % (self._account_name, base64.b64encode(signature))
+
+    def _authorized_http_request(self, what, method, url, request_headers,
+                                 body=None, need_response_body=False):
+        """
+        Do an HTTP request with the addition of a authorization header.
+        """
index 934be4115a6c0c87b12ff95d47c11d212248ceb5..dfbdb3ff2ab2716c3236cc60543697d4df5e96af 100644 (file)
@@ -35,6 +35,7 @@ from allmydata.storage.backends.cloud import mock_cloud, cloud_common
 from allmydata.storage.backends.cloud.mock_cloud import MockContainer
 from allmydata.storage.backends.cloud.openstack import openstack_container
 from allmydata.storage.backends.cloud.googlestorage import googlestorage_container
+from allmydata.storage.backends.cloud.msazure import msazure_container
 from allmydata.storage.bucket import BucketWriter, BucketReader
 from allmydata.storage.common import DataTooLargeError, storage_index_to_dir
 from allmydata.storage.leasedb import SHARETYPE_IMMUTABLE, SHARETYPE_MUTABLE
@@ -1039,6 +1040,160 @@ class GoogleStorageBackend(unittest.TestCase):
     test_head_object.skip = "May not be necessary"
 
 
+
+class MSAzureAuthentication(unittest.TestCase):
+    """
+    Tests for Microsoft Azure Blob Storage authentication.
+    """
+    class FakeRequest:
+        """
+        Emulate request objects used by azure library.
+        """
+        def __init__(self, method, url, headers):
+            from urlparse import urlparse, parse_qs
+            self.headers = [
+                (key.lower(), value[0]) for key, value in headers.getAllRawHeaders()]
+            url = urlparse(url)
+            self.path = url.path
+            self.query = url.query
+            self.method = method
+            self.query = [(k, v[0]) for k, v in parse_qs(url.query, keep_blank_values=True).items()]
+
+    def setUp(self):
+        self.container = msazure_container.MSAzureStorageContainer(
+            "account", "key".encode("base64"), "thebucket")
+
+    def assertSignatureEqual(self, method, url, headers, result, azure_buggy=False):
+        """
+        Assert the given HTTP request parameters produce a value to be signed
+        equal to the given result.
+
+        If possible, assert the signature calculation matches the Microsoft
+        reference implementation.
+        """
+        headers = Headers(headers)
+        self.assertEqual(
+                self.container._calculate_presignature(method, url, headers),
+                result)
+        if azure_buggy:
+            # The reference client is buggy in this case, skip it
+            raise unittest.SkipTest("Azure reference client is buggy in this case")
+
+        # Now, compare our result to that of the Microsoft-provided
+        # implementation, if available:
+        try:
+            from azure.storage import _sign_storage_blob_request
+        except ImportError:
+            raise unittest.SkipTest("No azure installed")
+
+        request = self.FakeRequest(method, url, headers)
+        self.assertEqual(
+            _sign_storage_blob_request(request,
+                                       self.container._account_name,
+                                       self.container._account_key.encode("base64")),
+            self.container._calculate_signature(method, url, headers))
+
+    def test_method(self):
+        """
+        The correct HTTP method is included in the signature.
+        """
+        self.assertSignatureEqual(
+            "HEAD", "http://x/", {"x-ms-date": ["Sun, 11 Oct 2009 21:49:13 GMT"]},
+            "HEAD\n\n\n\n\n\n\n\n\n\n\n\n"
+            "x-ms-date:Sun, 11 Oct 2009 21:49:13 GMT\n"
+            "/account/")
+
+    def test_standard_headers(self):
+        """
+        A specific set of headers are included in the signature, except for
+        Date which is ignored in favor of x-ms-date.
+        """
+        headers = {"Content-Encoding": ["ce"],
+                   "Content-Language": ["cl"],
+                   "Content-Length": ["cl2"],
+                   "Content-MD5": ["cm"],
+                   "Content-Type": ["ct"],
+                   "Date": ["d"],
+                   "If-Modified-Since": ["ims"],
+                   "If-Match": ["im"],
+                   "If-None-Match": ["inm"],
+                   "If-Unmodified-Since": ["ius"],
+                   "Range": ["r"],
+                   "Other": ["o"],
+                   "x-ms-date": ["xmd"]}
+        self.assertSignatureEqual("GET", "http://x/", headers,
+                                  "GET\n"
+                                  "ce\n"
+                                  "cl\n"
+                                  "cl2\n"
+                                  "cm\n"
+                                  "ct\n"
+                                  "\n" # Date value is ignored!
+                                  "ims\n"
+                                  "im\n"
+                                  "inm\n"
+                                  "ius\n"
+                                  "r\n"
+                                  "x-ms-date:xmd\n"
+                                  "/account/", True)
+
+    def test_xms_headers(self):
+        """
+        Headers starting with x-ms are included in the signature.
+        """
+        headers = {"x-ms-foo": ["a"],
+                   "x-ms-z": ["b"],
+                   "x-ms-date": ["c"]}
+        self.assertSignatureEqual("GET", "http://x/", headers,
+                                  "GET\n"
+                                  "\n"
+                                  "\n"
+                                  "\n"
+                                  "\n"
+                                  "\n"
+                                  "\n"
+                                  "\n"
+                                  "\n"
+                                  "\n"
+                                  "\n"
+                                  "\n"
+                                  "x-ms-date:c\n"
+                                  "x-ms-foo:a\n"
+                                  "x-ms-z:b\n"
+                                  "/account/")
+
+    def test_xmsdate_required(self):
+        """
+        The x-ms-date header is mandatory.
+        """
+        self.assertRaises(ValueError,
+                          self.assertSignatureEqual, "GET", "http://x/", {}, "")
+
+    def test_path_and_account(self):
+        """
+        The URL path and account name is included.
+        """
+        self.container._account_name = "theaccount"
+        self.assertSignatureEqual(
+            "HEAD", "http://x/foo/bar", {"x-ms-date": ["d"]},
+            "HEAD\n\n\n\n\n\n\n\n\n\n\n\n"
+            "x-ms-date:d\n"
+            "/theaccount/foo/bar")
+
+    def test_query(self):
+        """
+        The query arguments are included.
+        """
+        value = "hello%20there"
+        self.assertSignatureEqual(
+            "HEAD", "http://x/?z=%s&y=abc" % (value,), {"x-ms-date": ["d"]},
+            "HEAD\n\n\n\n\n\n\n\n\n\n\n\n"
+            "x-ms-date:d\n"
+            "/account/\n"
+            "y:abc\n"
+            "z:hello there")
+
+
 class ServerMixin:
     def allocate(self, account, storage_index, sharenums, size, canary=None):
         # These secrets are not used, but clients still provide them.