diff options
Diffstat (limited to 'src/pybind/mgr/dashboard/awsauth.py')
-rw-r--r-- | src/pybind/mgr/dashboard/awsauth.py | 169 |
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 |