]> git.proxmox.com Git - ceph.git/blob - ceph/src/pybind/mgr/dashboard/awsauth.py
import 15.2.0 Octopus source
[ceph.git] / ceph / src / pybind / mgr / dashboard / awsauth.py
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
34 import hmac
35
36 from hashlib import sha1 as sha
37
38 py3k = False
39 try:
40 from urlparse import urlparse, unquote
41 from base64 import encodestring
42 except ImportError:
43 py3k = True
44 from urllib.parse import urlparse, unquote
45 from base64 import encodebytes as encodestring
46
47 from email.utils import formatdate
48
49 from requests.auth import AuthBase
50
51
52 class S3Auth(AuthBase):
53
54 """Attaches AWS Authentication to the given Request object."""
55
56 service_base_url = 's3.amazonaws.com'
57 # List of Query String Arguments of Interest
58 special_params = [
59 'acl', 'location', 'logging', 'partNumber', 'policy', 'requestPayment',
60 'torrent', 'versioning', 'versionId', 'versions', 'website', 'uploads',
61 'uploadId', 'response-content-type', 'response-content-language',
62 'response-expires', 'response-cache-control', 'delete', 'lifecycle',
63 'response-content-disposition', 'response-content-encoding', 'tagging',
64 'notification', 'cors'
65 ]
66
67 def __init__(self, access_key, secret_key, service_url=None):
68 if service_url:
69 self.service_base_url = service_url
70 self.access_key = str(access_key)
71 self.secret_key = str(secret_key)
72
73 def __call__(self, r):
74 # Create date header if it is not created yet.
75 if 'date' not in r.headers and 'x-amz-date' not in r.headers:
76 r.headers['date'] = formatdate(
77 timeval=None,
78 localtime=False,
79 usegmt=True)
80 signature = self.get_signature(r)
81 if py3k:
82 signature = signature.decode('utf-8')
83 r.headers['Authorization'] = 'AWS %s:%s' % (self.access_key, signature)
84 return r
85
86 def get_signature(self, r):
87 canonical_string = self.get_canonical_string(
88 r.url, r.headers, r.method)
89 if py3k:
90 key = self.secret_key.encode('utf-8')
91 msg = canonical_string.encode('utf-8')
92 else:
93 key = self.secret_key # type: ignore
94 msg = canonical_string
95 h = hmac.new(key, msg, digestmod=sha)
96 return encodestring(h.digest()).strip()
97
98 def get_canonical_string(self, url, headers, method):
99 parsedurl = urlparse(url)
100 objectkey = parsedurl.path[1:]
101 query_args = sorted(parsedurl.query.split('&'))
102
103 bucket = parsedurl.netloc[:-len(self.service_base_url)]
104 if len(bucket) > 1:
105 # remove last dot
106 bucket = bucket[:-1]
107
108 interesting_headers = {
109 'content-md5': '',
110 'content-type': '',
111 'date': ''}
112 for key in headers:
113 lk = key.lower()
114 try:
115 if isinstance(lk, bytes):
116 lk = lk.decode('utf-8')
117 except UnicodeDecodeError:
118 pass
119 if headers[key] and (lk in interesting_headers.keys()
120 or lk.startswith('x-amz-')):
121 interesting_headers[lk] = headers[key].strip()
122
123 # If x-amz-date is used it supersedes the date header.
124 if not py3k:
125 if 'x-amz-date' in interesting_headers:
126 interesting_headers['date'] = ''
127 else:
128 if 'x-amz-date' in interesting_headers:
129 interesting_headers['date'] = ''
130
131 buf = '%s\n' % method
132 for key in sorted(interesting_headers.keys()):
133 val = interesting_headers[key]
134 if key.startswith('x-amz-'):
135 buf += '%s:%s\n' % (key, val)
136 else:
137 buf += '%s\n' % val
138
139 # append the bucket if it exists
140 if bucket != '':
141 buf += '/%s' % bucket
142
143 # add the objectkey. even if it doesn't exist, add the slash
144 buf += '/%s' % objectkey
145
146 params_found = False
147
148 # handle special query string arguments
149 for q in query_args:
150 k = q.split('=')[0]
151 if k in self.special_params:
152 buf += '&' if params_found else '?'
153 params_found = True
154
155 try:
156 k, v = q.split('=', 1)
157
158 except ValueError:
159 buf += q
160
161 else:
162 # Riak CS multipart upload ids look like this, `TFDSheOgTxC2Tsh1qVK73A==`,
163 # is should be escaped to be included as part of a query string.
164 #
165 # A requests mp upload part request may look like
166 # resp = requests.put(
167 # 'https://url_here',
168 # params={
169 # 'partNumber': 1,
170 # 'uploadId': 'TFDSheOgTxC2Tsh1qVK73A=='
171 # },
172 # data='some data',
173 # auth=S3Auth('access_key', 'secret_key')
174 # )
175 #
176 # Requests automatically escapes the values in the `params` dict, so now
177 # our uploadId is `TFDSheOgTxC2Tsh1qVK73A%3D%3D`,
178 # if we sign the request with the encoded value the signature will
179 # not be valid, we'll get 403 Access Denied.
180 # So we unquote, this is no-op if the value isn't encoded.
181 buf += '{key}={value}'.format(key=k, value=unquote(v))
182
183 return buf