summaryrefslogtreecommitdiffstats
path: root/src/pybind/mgr/dashboard/awsauth.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/pybind/mgr/dashboard/awsauth.py')
-rw-r--r--src/pybind/mgr/dashboard/awsauth.py169
1 files changed, 169 insertions, 0 deletions
diff --git a/src/pybind/mgr/dashboard/awsauth.py b/src/pybind/mgr/dashboard/awsauth.py
new file mode 100644
index 000000000..285a2c088
--- /dev/null
+++ b/src/pybind/mgr/dashboard/awsauth.py
@@ -0,0 +1,169 @@
+# -*- coding: utf-8 -*-
+# pylint: disable-all
+#
+# 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 base64 import encodebytes as encodestring
+from email.utils import formatdate
+from hashlib import sha1 as sha
+from urllib.parse import unquote, urlparse
+
+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)
+ 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)
+ key = self.secret_key.encode('utf-8')
+ msg = canonical_string.encode('utf-8')
+ h = hmac.new(key, msg, digestmod=sha)
+ return encodestring(h.digest()).strip()
+
+ def get_interesting_headers(self, headers):
+ interesting_headers = {
+ 'content-md5': '',
+ 'content-type': '',
+ 'date': ''}
+ for key in headers:
+ lk = key.lower()
+ try:
+ if isinstance(lk, bytes):
+ lk = lk.decode('utf-8')
+ except UnicodeDecodeError:
+ 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 'x-amz-date' in interesting_headers:
+ interesting_headers['date'] = ''
+ return interesting_headers
+
+ 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 = self.get_interesting_headers(headers)
+
+ 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