]> git.proxmox.com Git - ceph.git/blame - ceph/src/pybind/mgr/dashboard/awsauth.py
update ceph source to reef 18.1.2
[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
1e59de90 84 def get_interesting_headers(self, headers):
11fdf7f2
TL
85 interesting_headers = {
86 'content-md5': '',
87 'content-type': '',
88 'date': ''}
89 for key in headers:
90 lk = key.lower()
91 try:
92 if isinstance(lk, bytes):
93 lk = lk.decode('utf-8')
94 except UnicodeDecodeError:
95 pass
96 if headers[key] and (lk in interesting_headers.keys()
97 or lk.startswith('x-amz-')):
98 interesting_headers[lk] = headers[key].strip()
99
100 # If x-amz-date is used it supersedes the date header.
f67539c2
TL
101 if 'x-amz-date' in interesting_headers:
102 interesting_headers['date'] = ''
1e59de90
TL
103 return interesting_headers
104
105 def get_canonical_string(self, url, headers, method):
106 parsedurl = urlparse(url)
107 objectkey = parsedurl.path[1:]
108 query_args = sorted(parsedurl.query.split('&'))
109
110 bucket = parsedurl.netloc[:-len(self.service_base_url)]
111 if len(bucket) > 1:
112 # remove last dot
113 bucket = bucket[:-1]
114
115 interesting_headers = self.get_interesting_headers(headers)
11fdf7f2
TL
116
117 buf = '%s\n' % method
118 for key in sorted(interesting_headers.keys()):
119 val = interesting_headers[key]
120 if key.startswith('x-amz-'):
121 buf += '%s:%s\n' % (key, val)
122 else:
123 buf += '%s\n' % val
124
125 # append the bucket if it exists
126 if bucket != '':
127 buf += '/%s' % bucket
128
129 # add the objectkey. even if it doesn't exist, add the slash
130 buf += '/%s' % objectkey
131
132 params_found = False
133
134 # handle special query string arguments
135 for q in query_args:
136 k = q.split('=')[0]
137 if k in self.special_params:
138 buf += '&' if params_found else '?'
139 params_found = True
140
141 try:
142 k, v = q.split('=', 1)
143
144 except ValueError:
145 buf += q
146
147 else:
148 # Riak CS multipart upload ids look like this, `TFDSheOgTxC2Tsh1qVK73A==`,
149 # is should be escaped to be included as part of a query string.
150 #
151 # A requests mp upload part request may look like
152 # resp = requests.put(
153 # 'https://url_here',
154 # params={
155 # 'partNumber': 1,
156 # 'uploadId': 'TFDSheOgTxC2Tsh1qVK73A=='
157 # },
158 # data='some data',
159 # auth=S3Auth('access_key', 'secret_key')
160 # )
161 #
162 # Requests automatically escapes the values in the `params` dict, so now
163 # our uploadId is `TFDSheOgTxC2Tsh1qVK73A%3D%3D`,
164 # if we sign the request with the encoded value the signature will
165 # not be valid, we'll get 403 Access Denied.
166 # So we unquote, this is no-op if the value isn't encoded.
167 buf += '{key}={value}'.format(key=k, value=unquote(v))
168
169 return buf