]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: RGW proxy: Include `python-requests-aws`
authorPatrick Nawracay <pnawracay@suse.com>
Tue, 20 Mar 2018 08:01:32 +0000 (09:01 +0100)
committerVolker Theile <vtheile@suse.com>
Wed, 11 Apr 2018 10:26:15 +0000 (12:26 +0200)
This commit will include the `python-requests-aws` [1] library.  This library
extends the `requests` library by an authentication mechanism to S3 gateways.

[1] https://github.com/tax/python-requests-aws

Signed-off-by: Patrick Nawracay <pnawracay@suse.com>
src/pybind/mgr/dashboard/awsauth.py [new file with mode: 0644]
src/pybind/mgr/dashboard/tox.ini

diff --git a/src/pybind/mgr/dashboard/awsauth.py b/src/pybind/mgr/dashboard/awsauth.py
new file mode 100644 (file)
index 0000000..47b1e0b
--- /dev/null
@@ -0,0 +1,185 @@
+# -*- coding: utf-8 -*-
+<<<<<<< HEAD
+# pylint: disable-all
+=======
+>>>>>>> 60417c2dcc... dashboard/mgr: RGW proxy: Include `python-requests-aws`
+#
+# Copyright (c) 2012-2013 Paul Tax <paultax@gmail.com> All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#   1. Redistributions of source code must retain the above copyright
+#      notice, this list of conditions and the following disclaimer.
+#
+#   2. Redistributions in binary form must reproduce the above copyright
+#      notice, this list of conditions and the following disclaimer in
+#      the documentation and/or other materials provided with the
+#      distribution.
+#
+#   3. Neither the name of Infrae nor the names of its contributors may
+#      be used to endorse or promote products derived from this software
+#      without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL INFRAE OR
+# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import hmac
+
+from hashlib import sha1 as sha
+
+py3k = False
+try:
+    from urlparse import urlparse, unquote
+    from base64 import encodestring
+except:
+    py3k = True
+    from urllib.parse import urlparse, unquote
+    from base64 import encodebytes as encodestring
+
+from email.utils import formatdate
+
+from requests.auth import AuthBase
+
+
+class S3Auth(AuthBase):
+
+    """Attaches AWS Authentication to the given Request object."""
+
+    service_base_url = 's3.amazonaws.com'
+    # List of Query String Arguments of Interest
+    special_params = [
+        'acl', 'location', 'logging', 'partNumber', 'policy', 'requestPayment',
+        'torrent', 'versioning', 'versionId', 'versions', 'website', 'uploads',
+        'uploadId', 'response-content-type', 'response-content-language',
+        'response-expires', 'response-cache-control', 'delete', 'lifecycle',
+        'response-content-disposition', 'response-content-encoding', 'tagging',
+        'notification', 'cors'
+    ]
+
+    def __init__(self, access_key, secret_key, service_url=None):
+        if service_url:
+            self.service_base_url = service_url
+        self.access_key = str(access_key)
+        self.secret_key = str(secret_key)
+
+    def __call__(self, r):
+        # Create date header if it is not created yet.
+        if 'date' not in r.headers and 'x-amz-date' not in r.headers:
+            r.headers['date'] = formatdate(
+                timeval=None,
+                localtime=False,
+                usegmt=True)
+        signature = self.get_signature(r)
+        if py3k:
+            signature = signature.decode('utf-8')
+        r.headers['Authorization'] = 'AWS %s:%s' % (self.access_key, signature)
+        return r
+
+    def get_signature(self, r):
+        canonical_string = self.get_canonical_string(
+            r.url, r.headers, r.method)
+        if py3k:
+            key = self.secret_key.encode('utf-8')
+            msg = canonical_string.encode('utf-8')
+        else:
+            key = self.secret_key
+            msg = canonical_string
+        h = hmac.new(key, msg, digestmod=sha)
+        return encodestring(h.digest()).strip()
+
+    def get_canonical_string(self, url, headers, method):
+        parsedurl = urlparse(url)
+        objectkey = parsedurl.path[1:]
+        query_args = sorted(parsedurl.query.split('&'))
+
+        bucket = parsedurl.netloc[:-len(self.service_base_url)]
+        if len(bucket) > 1:
+            # remove last dot
+            bucket = bucket[:-1]
+
+        interesting_headers = {
+            'content-md5': '',
+            'content-type': '',
+            'date': ''}
+        for key in headers:
+            lk = key.lower()
+            try:
+                lk = lk.decode('utf-8')
+            except:
+                pass
+            if headers[key] and (lk in interesting_headers.keys()
+                                 or lk.startswith('x-amz-')):
+                interesting_headers[lk] = headers[key].strip()
+
+        # If x-amz-date is used it supersedes the date header.
+        if not py3k:
+            if 'x-amz-date' in interesting_headers:
+                interesting_headers['date'] = ''
+        else:
+            if 'x-amz-date' in interesting_headers:
+                interesting_headers['date'] = ''
+
+        buf = '%s\n' % method
+        for key in sorted(interesting_headers.keys()):
+            val = interesting_headers[key]
+            if key.startswith('x-amz-'):
+                buf += '%s:%s\n' % (key, val)
+            else:
+                buf += '%s\n' % val
+
+        # append the bucket if it exists
+        if bucket != '':
+            buf += '/%s' % bucket
+
+        # add the objectkey. even if it doesn't exist, add the slash
+        buf += '/%s' % objectkey
+
+        params_found = False
+
+        # handle special query string arguments
+        for q in query_args:
+            k = q.split('=')[0]
+            if k in self.special_params:
+                buf += '&' if params_found else '?'
+                params_found = True
+
+                try:
+                    k, v = q.split('=', 1)
+
+                except ValueError:
+                    buf += q
+
+                else:
+                    # Riak CS multipart upload ids look like this, `TFDSheOgTxC2Tsh1qVK73A==`,
+                    # is should be escaped to be included as part of a query string.
+                    #
+                    # A requests mp upload part request may look like
+                    # resp = requests.put(
+                    #     'https://url_here',
+                    #     params={
+                    #         'partNumber': 1,
+                    #         'uploadId': 'TFDSheOgTxC2Tsh1qVK73A=='
+                    #     },
+                    #     data='some data',
+                    #     auth=S3Auth('access_key', 'secret_key')
+                    # )
+                    #
+                    # Requests automatically escapes the values in the `params` dict, so now
+                    # our uploadId is `TFDSheOgTxC2Tsh1qVK73A%3D%3D`,
+                    # if we sign the request with the encoded value the signature will
+                    # not be valid, we'll get 403 Access Denied.
+                    # So we unquote, this is no-op if the value isn't encoded.
+                    buf += '{key}={value}'.format(key=k, value=unquote(v))
+
+        return buf
index b9170256efe3506acc0a96d7f41d5ccac1bbfe7d..08dc57f823e885afd1bf4199f32f5296d6b84410 100644 (file)
@@ -37,4 +37,4 @@ setenv =
 deps=-r{toxinidir}/requirements.txt
 commands=
     pylint --rcfile=.pylintrc --jobs=5 . module.py tools.py controllers tests services
-    pycodestyle --max-line-length=100 --exclude=python2.7,.tox,venv,frontend --ignore=E402,E121,E123,E126,E226,E24,E704,W503 .
+    pycodestyle --max-line-length=100 --exclude=python2.7,.tox,venv,frontend,awsauth.py --ignore=E402,E121,E123,E126,E226,E24,E704,W503 .