]> git.proxmox.com Git - ceph.git/blame - ceph/src/pybind/mgr/dashboard/awsauth.py
buildsys: change download over to reef release
[ceph.git] / ceph / src / pybind / mgr / dashboard / awsauth.py
CommitLineData
11fdf7f2
TL
1# -*- coding: utf-8 -*-
2# pylint: disable-all
3#
4# Copyright (c) 2012-2013 Paul Tax <paultax@gmail.com> All rights reserved.
5#
6# Redistribution and use in source and binary forms, with or without
7# modification, are permitted provided that the following conditions are
8# met:
9#
10# 1. Redistributions of source code must retain the above copyright
11# notice, this list of conditions and the following disclaimer.
12#
13# 2. Redistributions in binary form must reproduce the above copyright
14# notice, this list of conditions and the following disclaimer in
15# the documentation and/or other materials provided with the
16# distribution.
17#
18# 3. Neither the name of Infrae nor the names of its contributors may
19# be used to endorse or promote products derived from this software
20# without specific prior written permission.
21#
22# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
23# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
24# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
25# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL INFRAE OR
26# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
27# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
28# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
29# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
30# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
31# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
32# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
33
34import hmac
f67539c2 35from base64 import encodebytes as encodestring
11fdf7f2 36from email.utils import formatdate
f67539c2
TL
37from hashlib import sha1 as sha
38from urllib.parse import unquote, urlparse
11fdf7f2
TL
39
40from requests.auth import AuthBase
41
42
43class S3Auth(AuthBase):
44
45 """Attaches AWS Authentication to the given Request object."""
46
47 service_base_url = 's3.amazonaws.com'
48 # List of Query String Arguments of Interest
49 special_params = [
50 'acl', 'location', 'logging', 'partNumber', 'policy', 'requestPayment',
51 'torrent', 'versioning', 'versionId', 'versions', 'website', 'uploads',
52 'uploadId', 'response-content-type', 'response-content-language',
53 'response-expires', 'response-cache-control', 'delete', 'lifecycle',
54 'response-content-disposition', 'response-content-encoding', 'tagging',
55 'notification', 'cors'
56 ]
57
58 def __init__(self, access_key, secret_key, service_url=None):
59 if service_url:
60 self.service_base_url = service_url
61 self.access_key = str(access_key)
62 self.secret_key = str(secret_key)
63
64 def __call__(self, r):
65 # Create date header if it is not created yet.
66 if 'date' not in r.headers and 'x-amz-date' not in r.headers:
67 r.headers['date'] = formatdate(
68 timeval=None,
69 localtime=False,
70 usegmt=True)
71 signature = self.get_signature(r)
f67539c2 72 signature = signature.decode('utf-8')
11fdf7f2
TL
73 r.headers['Authorization'] = 'AWS %s:%s' % (self.access_key, signature)
74 return r
75
76 def get_signature(self, r):
77 canonical_string = self.get_canonical_string(
78 r.url, r.headers, r.method)
f67539c2
TL
79 key = self.secret_key.encode('utf-8')
80 msg = canonical_string.encode('utf-8')
11fdf7f2
TL
81 h = hmac.new(key, msg, digestmod=sha)
82 return encodestring(h.digest()).strip()
83
84 def get_canonical_string(self, url, headers, method):
85 parsedurl = urlparse(url)
86 objectkey = parsedurl.path[1:]
87 query_args = sorted(parsedurl.query.split('&'))
88
89 bucket = parsedurl.netloc[:-len(self.service_base_url)]
90 if len(bucket) > 1:
91 # remove last dot
92 bucket = bucket[:-1]
93
94 interesting_headers = {
95 'content-md5': '',
96 'content-type': '',
97 'date': ''}
98 for key in headers:
99 lk = key.lower()
100 try:
101 if isinstance(lk, bytes):
102 lk = lk.decode('utf-8')
103 except UnicodeDecodeError:
104 pass
105 if headers[key] and (lk in interesting_headers.keys()
106 or lk.startswith('x-amz-')):
107 interesting_headers[lk] = headers[key].strip()
108
109 # If x-amz-date is used it supersedes the date header.
f67539c2
TL
110 if 'x-amz-date' in interesting_headers:
111 interesting_headers['date'] = ''
11fdf7f2
TL
112
113 buf = '%s\n' % method
114 for key in sorted(interesting_headers.keys()):
115 val = interesting_headers[key]
116 if key.startswith('x-amz-'):
117 buf += '%s:%s\n' % (key, val)
118 else:
119 buf += '%s\n' % val
120
121 # append the bucket if it exists
122 if bucket != '':
123 buf += '/%s' % bucket
124
125 # add the objectkey. even if it doesn't exist, add the slash
126 buf += '/%s' % objectkey
127
128 params_found = False
129
130 # handle special query string arguments
131 for q in query_args:
132 k = q.split('=')[0]
133 if k in self.special_params:
134 buf += '&' if params_found else '?'
135 params_found = True
136
137 try:
138 k, v = q.split('=', 1)
139
140 except ValueError:
141 buf += q
142
143 else:
144 # Riak CS multipart upload ids look like this, `TFDSheOgTxC2Tsh1qVK73A==`,
145 # is should be escaped to be included as part of a query string.
146 #
147 # A requests mp upload part request may look like
148 # resp = requests.put(
149 # 'https://url_here',
150 # params={
151 # 'partNumber': 1,
152 # 'uploadId': 'TFDSheOgTxC2Tsh1qVK73A=='
153 # },
154 # data='some data',
155 # auth=S3Auth('access_key', 'secret_key')
156 # )
157 #
158 # Requests automatically escapes the values in the `params` dict, so now
159 # our uploadId is `TFDSheOgTxC2Tsh1qVK73A%3D%3D`,
160 # if we sign the request with the encoded value the signature will
161 # not be valid, we'll get 403 Access Denied.
162 # So we unquote, this is no-op if the value isn't encoded.
163 buf += '{key}={value}'.format(key=k, value=unquote(v))
164
165 return buf