diaspora-event-sdk 0.2.5__tar.gz → 0.2.7__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. {diaspora-event-sdk-0.2.5/diaspora_event_sdk.egg-info → diaspora-event-sdk-0.2.7}/PKG-INFO +2 -2
  2. diaspora-event-sdk-0.2.7/diaspora_event_sdk/sdk/aws_iam_msk.py +119 -0
  3. diaspora-event-sdk-0.2.7/diaspora_event_sdk/sdk/botocore/auth.py +472 -0
  4. diaspora-event-sdk-0.2.7/diaspora_event_sdk/sdk/botocore/awsrequest.py +271 -0
  5. diaspora-event-sdk-0.2.7/diaspora_event_sdk/sdk/botocore/compat.py +352 -0
  6. diaspora-event-sdk-0.2.7/diaspora_event_sdk/sdk/botocore/credentials.py +66 -0
  7. diaspora-event-sdk-0.2.7/diaspora_event_sdk/sdk/botocore/exceptions.py +63 -0
  8. diaspora-event-sdk-0.2.7/diaspora_event_sdk/sdk/botocore/utils.py +174 -0
  9. {diaspora-event-sdk-0.2.5 → diaspora-event-sdk-0.2.7}/diaspora_event_sdk/sdk/kafka_client.py +3 -3
  10. diaspora-event-sdk-0.2.7/diaspora_event_sdk/version.py +1 -0
  11. {diaspora-event-sdk-0.2.5 → diaspora-event-sdk-0.2.7/diaspora_event_sdk.egg-info}/PKG-INFO +2 -2
  12. {diaspora-event-sdk-0.2.5 → diaspora-event-sdk-0.2.7}/diaspora_event_sdk.egg-info/SOURCES.txt +8 -0
  13. {diaspora-event-sdk-0.2.5 → diaspora-event-sdk-0.2.7}/diaspora_event_sdk.egg-info/requires.txt +0 -1
  14. {diaspora-event-sdk-0.2.5 → diaspora-event-sdk-0.2.7}/setup.py +2 -2
  15. diaspora-event-sdk-0.2.7/tests/__init__.py +0 -0
  16. diaspora-event-sdk-0.2.5/diaspora_event_sdk/version.py +0 -1
  17. {diaspora-event-sdk-0.2.5 → diaspora-event-sdk-0.2.7}/LICENSE +0 -0
  18. {diaspora-event-sdk-0.2.5 → diaspora-event-sdk-0.2.7}/MANIFEST.in +0 -0
  19. {diaspora-event-sdk-0.2.5 → diaspora-event-sdk-0.2.7}/README.md +0 -0
  20. {diaspora-event-sdk-0.2.5 → diaspora-event-sdk-0.2.7}/diaspora_event_sdk/__init__.py +0 -0
  21. {diaspora-event-sdk-0.2.5 → diaspora-event-sdk-0.2.7}/diaspora_event_sdk/sdk/__init__.py +0 -0
  22. {diaspora-event-sdk-0.2.5 → diaspora-event-sdk-0.2.7}/diaspora_event_sdk/sdk/_environments.py +0 -0
  23. {diaspora-event-sdk-0.2.5/diaspora_event_sdk/sdk/utils → diaspora-event-sdk-0.2.7/diaspora_event_sdk/sdk/botocore}/__init__.py +0 -0
  24. {diaspora-event-sdk-0.2.5 → diaspora-event-sdk-0.2.7}/diaspora_event_sdk/sdk/client.py +0 -0
  25. {diaspora-event-sdk-0.2.5 → diaspora-event-sdk-0.2.7}/diaspora_event_sdk/sdk/decorators.py +0 -0
  26. {diaspora-event-sdk-0.2.5 → diaspora-event-sdk-0.2.7}/diaspora_event_sdk/sdk/login_manager/__init__.py +0 -0
  27. {diaspora-event-sdk-0.2.5 → diaspora-event-sdk-0.2.7}/diaspora_event_sdk/sdk/login_manager/client_login.py +0 -0
  28. {diaspora-event-sdk-0.2.5 → diaspora-event-sdk-0.2.7}/diaspora_event_sdk/sdk/login_manager/decorators.py +0 -0
  29. {diaspora-event-sdk-0.2.5 → diaspora-event-sdk-0.2.7}/diaspora_event_sdk/sdk/login_manager/globus_auth.py +0 -0
  30. {diaspora-event-sdk-0.2.5 → diaspora-event-sdk-0.2.7}/diaspora_event_sdk/sdk/login_manager/login_flow.py +0 -0
  31. {diaspora-event-sdk-0.2.5 → diaspora-event-sdk-0.2.7}/diaspora_event_sdk/sdk/login_manager/manager.py +0 -0
  32. {diaspora-event-sdk-0.2.5 → diaspora-event-sdk-0.2.7}/diaspora_event_sdk/sdk/login_manager/protocol.py +0 -0
  33. {diaspora-event-sdk-0.2.5 → diaspora-event-sdk-0.2.7}/diaspora_event_sdk/sdk/login_manager/tokenstore.py +0 -0
  34. {diaspora-event-sdk-0.2.5/tests → diaspora-event-sdk-0.2.7/diaspora_event_sdk/sdk/utils}/__init__.py +0 -0
  35. {diaspora-event-sdk-0.2.5 → diaspora-event-sdk-0.2.7}/diaspora_event_sdk/sdk/utils/uuid_like.py +0 -0
  36. {diaspora-event-sdk-0.2.5 → diaspora-event-sdk-0.2.7}/diaspora_event_sdk/sdk/web_client.py +0 -0
  37. {diaspora-event-sdk-0.2.5 → diaspora-event-sdk-0.2.7}/diaspora_event_sdk.egg-info/dependency_links.txt +0 -0
  38. {diaspora-event-sdk-0.2.5 → diaspora-event-sdk-0.2.7}/diaspora_event_sdk.egg-info/top_level.txt +0 -0
  39. {diaspora-event-sdk-0.2.5 → diaspora-event-sdk-0.2.7}/setup.cfg +0 -0
  40. {diaspora-event-sdk-0.2.5 → diaspora-event-sdk-0.2.7}/tests/unit/test_client.py +0 -0
  41. {diaspora-event-sdk-0.2.5 → diaspora-event-sdk-0.2.7}/tox.ini +0 -0
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: diaspora-event-sdk
3
- Version: 0.2.5
3
+ Version: 0.2.7
4
4
  Summary: SDK of Diaspora Event Fabric: Resilience-enabling services for science from HPC to edge
5
5
  Home-page: https://github.com/globus-labs/diaspora-event-sdk
6
- License: LICENSE
6
+ License: Apache 2.0
7
7
  Platform: UNKNOWN
8
8
  Description-Content-Type: text/markdown
9
9
  Provides-Extra: kafka-python
@@ -0,0 +1,119 @@
1
+ # Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ import base64
5
+ # import logging
6
+ from datetime import datetime, timezone
7
+ from urllib.parse import parse_qs, urlparse
8
+
9
+ # import boto3
10
+ # import botocore.session
11
+ # import pkg_resources
12
+ from .botocore.auth import SigV4QueryAuth
13
+ from .botocore.awsrequest import AWSRequest
14
+ # from botocore.config import Config
15
+ from .botocore.credentials import Credentials
16
+
17
+ ENDPOINT_URL_TEMPLATE = "https://kafka.{}.amazonaws.com/"
18
+ DEFAULT_TOKEN_EXPIRY_SECONDS = 900
19
+ DEFAULT_STS_SESSION_NAME = "MSKSASLDefaultSession"
20
+ ACTION_TYPE = "Action"
21
+ ACTION_NAME = "kafka-cluster:Connect"
22
+ SIGNING_NAME = "kafka-cluster"
23
+ USER_AGENT_KEY = "User-Agent"
24
+ LIB_NAME = "aws-msk-iam-sasl-signer-python"
25
+
26
+
27
+ def __get_user_agent__():
28
+ return (f"{LIB_NAME}/1.0.1")
29
+
30
+
31
+ def __get_expiration_time_ms(request):
32
+ """
33
+ Private function that parses the url and gets the expiration time
34
+
35
+ Args: request (AWSRequest): The signed aws request object
36
+ """
37
+ # Parse the signed request
38
+ parsed_url = urlparse(request.url)
39
+ parsed_ul_params = parse_qs(parsed_url.query)
40
+ parsed_signing_time = datetime.strptime(parsed_ul_params['X-Amz-Date'][0],
41
+ "%Y%m%dT%H%M%SZ")
42
+
43
+ # Make the datetime object timezone-aware
44
+ signing_time = parsed_signing_time.replace(tzinfo=timezone.utc)
45
+
46
+ # Convert the Unix timestamp to milliseconds
47
+ expiration_timestamp_seconds = int(
48
+ signing_time.timestamp()) + DEFAULT_TOKEN_EXPIRY_SECONDS
49
+
50
+ # Get lifetime of token
51
+ expiration_timestamp_ms = expiration_timestamp_seconds * 1000
52
+
53
+ return expiration_timestamp_ms
54
+
55
+
56
+ def __construct_auth_token(region, aws_credentials):
57
+ """
58
+ Private function that constructs the authorization token using IAM
59
+ Credentials.
60
+
61
+ Args: region (str): The AWS region where the cluster is located.
62
+ aws_credentials (dict): The credentials to be used to generate signed
63
+ url. Returns: str: A base64-encoded authorization token.
64
+ """
65
+ # Validate credentials are not empty
66
+ if not aws_credentials.access_key or not aws_credentials.secret_key:
67
+ raise ValueError("AWS Credentials can not be empty")
68
+
69
+ # Extract endpoint URL
70
+ endpoint_url = ENDPOINT_URL_TEMPLATE.format(region)
71
+
72
+ # Set up resource path and query parameters
73
+ query_params = {ACTION_TYPE: ACTION_NAME}
74
+
75
+ # Create SigV4 instance
76
+ sig_v4 = SigV4QueryAuth(
77
+ aws_credentials, SIGNING_NAME, region,
78
+ expires=DEFAULT_TOKEN_EXPIRY_SECONDS
79
+ )
80
+
81
+ # Create request with url and parameters
82
+ request = AWSRequest(method="GET", url=endpoint_url, params=query_params)
83
+
84
+ # Add auth to the request and prepare the request
85
+ sig_v4.add_auth(request)
86
+ query_params = {USER_AGENT_KEY: __get_user_agent__()}
87
+ request.params = query_params
88
+ prepped = request.prepare()
89
+
90
+ # Get the signed url
91
+ signed_url = prepped.url
92
+
93
+ # Base 64 encode and remove the padding from the end
94
+ signed_url_bytes = signed_url.encode("utf-8")
95
+ base64_bytes = base64.urlsafe_b64encode(signed_url_bytes)
96
+ base64_encoded_signed_url = base64_bytes.decode("utf-8").rstrip("=")
97
+ return base64_encoded_signed_url, __get_expiration_time_ms(request)
98
+
99
+
100
+ def generate_auth_token(region, aws_debug_creds=False):
101
+ """
102
+ Generates an base64-encoded signed url as auth token to authenticate
103
+ with an Amazon MSK cluster using default IAM credentials.
104
+
105
+ Args:
106
+ region (str): The AWS region where the cluster is located.
107
+ Returns:
108
+ str: A base64-encoded authorization token.
109
+ """
110
+
111
+ # Load credentials
112
+ import os
113
+ assert os.environ["AWS_ACCESS_KEY_ID"]
114
+ assert os.environ["AWS_SECRET_ACCESS_KEY"]
115
+
116
+ aws_credentials = Credentials(os.environ["AWS_ACCESS_KEY_ID"],
117
+ os.environ["AWS_SECRET_ACCESS_KEY"])
118
+
119
+ return __construct_auth_token(region, aws_credentials)
@@ -0,0 +1,472 @@
1
+ # Copyright (c) 2012-2013 Mitch Garnaat http://garnaat.org/
2
+ # Copyright 2012-2014 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License"). You
5
+ # may not use this file except in compliance with the License. A copy of
6
+ # the License is located at
7
+ #
8
+ # http://aws.amazon.com/apache2.0/
9
+ #
10
+ # or in the "license" file accompanying this file. This file is
11
+ # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
12
+ # ANY KIND, either express or implied. See the License for the specific
13
+ # language governing permissions and limitations under the License.
14
+ import calendar
15
+ import datetime
16
+ import functools
17
+ import hmac
18
+ import json
19
+ import logging
20
+ from collections.abc import Mapping
21
+ from email.utils import formatdate
22
+ from hashlib import sha1, sha256
23
+
24
+ from .compat import (
25
+ HTTPHeaders,
26
+ ensure_unicode,
27
+ parse_qs,
28
+ quote,
29
+ urlsplit,
30
+ urlunsplit,
31
+ )
32
+ from .exceptions import NoCredentialsError
33
+ from .utils import (
34
+ is_valid_ipv6_endpoint_url,
35
+ normalize_url_path,
36
+ percent_encode_sequence,
37
+ )
38
+
39
+
40
+ logger = logging.getLogger(__name__)
41
+
42
+
43
+ EMPTY_SHA256_HASH = (
44
+ 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'
45
+ )
46
+ # This is the buffer size used when calculating sha256 checksums.
47
+ # Experimenting with various buffer sizes showed that this value generally
48
+ # gave the best result (in terms of performance).
49
+ PAYLOAD_BUFFER = 1024 * 1024
50
+ ISO8601 = '%Y-%m-%dT%H:%M:%SZ'
51
+ SIGV4_TIMESTAMP = '%Y%m%dT%H%M%SZ'
52
+ SIGNED_HEADERS_BLACKLIST = [
53
+ 'expect',
54
+ 'user-agent',
55
+ 'x-amzn-trace-id',
56
+ ]
57
+ UNSIGNED_PAYLOAD = 'UNSIGNED-PAYLOAD'
58
+ STREAMING_UNSIGNED_PAYLOAD_TRAILER = 'STREAMING-UNSIGNED-PAYLOAD-TRAILER'
59
+
60
+
61
+ def _host_from_url(url):
62
+ # Given URL, derive value for host header. Ensure that value:
63
+ # 1) is lowercase
64
+ # 2) excludes port, if it was the default port
65
+ # 3) excludes userinfo
66
+ url_parts = urlsplit(url)
67
+ host = url_parts.hostname # urlsplit's hostname is always lowercase
68
+ if is_valid_ipv6_endpoint_url(url):
69
+ host = f'[{host}]'
70
+ default_ports = {
71
+ 'http': 80,
72
+ 'https': 443,
73
+ }
74
+ if url_parts.port is not None:
75
+ if url_parts.port != default_ports.get(url_parts.scheme):
76
+ host = '%s:%d' % (host, url_parts.port)
77
+ return host
78
+
79
+
80
+ def _get_body_as_dict(request):
81
+ # For query services, request.data is form-encoded and is already a
82
+ # dict, but for other services such as rest-json it could be a json
83
+ # string or bytes. In those cases we attempt to load the data as a
84
+ # dict.
85
+ data = request.data
86
+ if isinstance(data, bytes):
87
+ data = json.loads(data.decode('utf-8'))
88
+ elif isinstance(data, str):
89
+ data = json.loads(data)
90
+ return data
91
+
92
+
93
+ class BaseSigner:
94
+ REQUIRES_REGION = False
95
+ REQUIRES_TOKEN = False
96
+
97
+ def add_auth(self, request):
98
+ raise NotImplementedError("add_auth")
99
+
100
+
101
+ class SigV4Auth(BaseSigner):
102
+ """
103
+ Sign a request with Signature V4.
104
+ """
105
+
106
+ REQUIRES_REGION = True
107
+
108
+ def __init__(self, credentials, service_name, region_name):
109
+ self.credentials = credentials
110
+ # We initialize these value here so the unit tests can have
111
+ # valid values. But these will get overriden in ``add_auth``
112
+ # later for real requests.
113
+ self._region_name = region_name
114
+ self._service_name = service_name
115
+
116
+ def _sign(self, key, msg, hex=False):
117
+ if hex:
118
+ sig = hmac.new(key, msg.encode('utf-8'), sha256).hexdigest()
119
+ else:
120
+ sig = hmac.new(key, msg.encode('utf-8'), sha256).digest()
121
+ return sig
122
+
123
+ def headers_to_sign(self, request):
124
+ """
125
+ Select the headers from the request that need to be included
126
+ in the StringToSign.
127
+ """
128
+ header_map = HTTPHeaders()
129
+ for name, value in request.headers.items():
130
+ lname = name.lower()
131
+ if lname not in SIGNED_HEADERS_BLACKLIST:
132
+ header_map[lname] = value
133
+ if 'host' not in header_map:
134
+ # TODO: We should set the host ourselves, instead of relying on our
135
+ # HTTP client to set it for us.
136
+ header_map['host'] = _host_from_url(request.url)
137
+ return header_map
138
+
139
+ def canonical_query_string(self, request):
140
+ # The query string can come from two parts. One is the
141
+ # params attribute of the request. The other is from the request
142
+ # url (in which case we have to re-split the url into its components
143
+ # and parse out the query string component).
144
+ if request.params:
145
+ return self._canonical_query_string_params(request.params)
146
+ else:
147
+ return self._canonical_query_string_url(urlsplit(request.url))
148
+
149
+ def _canonical_query_string_params(self, params):
150
+ # [(key, value), (key2, value2)]
151
+ key_val_pairs = []
152
+ if isinstance(params, Mapping):
153
+ params = params.items()
154
+ for key, value in params:
155
+ key_val_pairs.append(
156
+ (quote(key, safe='-_.~'), quote(str(value), safe='-_.~'))
157
+ )
158
+ sorted_key_vals = []
159
+ # Sort by the URI-encoded key names, and in the case of
160
+ # repeated keys, then sort by the value.
161
+ for key, value in sorted(key_val_pairs):
162
+ sorted_key_vals.append(f'{key}={value}')
163
+ canonical_query_string = '&'.join(sorted_key_vals)
164
+ return canonical_query_string
165
+
166
+ def _canonical_query_string_url(self, parts):
167
+ canonical_query_string = ''
168
+ if parts.query:
169
+ # [(key, value), (key2, value2)]
170
+ key_val_pairs = []
171
+ for pair in parts.query.split('&'):
172
+ key, _, value = pair.partition('=')
173
+ key_val_pairs.append((key, value))
174
+ sorted_key_vals = []
175
+ # Sort by the URI-encoded key names, and in the case of
176
+ # repeated keys, then sort by the value.
177
+ for key, value in sorted(key_val_pairs):
178
+ sorted_key_vals.append(f'{key}={value}')
179
+ canonical_query_string = '&'.join(sorted_key_vals)
180
+ return canonical_query_string
181
+
182
+ def canonical_headers(self, headers_to_sign):
183
+ """
184
+ Return the headers that need to be included in the StringToSign
185
+ in their canonical form by converting all header keys to lower
186
+ case, sorting them in alphabetical order and then joining
187
+ them into a string, separated by newlines.
188
+ """
189
+ headers = []
190
+ sorted_header_names = sorted(set(headers_to_sign))
191
+ for key in sorted_header_names:
192
+ value = ','.join(
193
+ self._header_value(v) for v in headers_to_sign.get_all(key)
194
+ )
195
+ headers.append(f'{key}:{ensure_unicode(value)}')
196
+ return '\n'.join(headers)
197
+
198
+ def _header_value(self, value):
199
+ # From the sigv4 docs:
200
+ # Lowercase(HeaderName) + ':' + Trimall(HeaderValue)
201
+ #
202
+ # The Trimall function removes excess white space before and after
203
+ # values, and converts sequential spaces to a single space.
204
+ return ' '.join(value.split())
205
+
206
+ def signed_headers(self, headers_to_sign):
207
+ headers = sorted(n.lower().strip() for n in set(headers_to_sign))
208
+ return ';'.join(headers)
209
+
210
+ def _is_streaming_checksum_payload(self, request):
211
+ checksum_context = request.context.get('checksum', {})
212
+ algorithm = checksum_context.get('request_algorithm')
213
+ return isinstance(algorithm, dict) and algorithm.get('in') == 'trailer'
214
+
215
+ def payload(self, request):
216
+ if self._is_streaming_checksum_payload(request):
217
+ return STREAMING_UNSIGNED_PAYLOAD_TRAILER
218
+ elif not self._should_sha256_sign_payload(request):
219
+ # When payload signing is disabled, we use this static string in
220
+ # place of the payload checksum.
221
+ return UNSIGNED_PAYLOAD
222
+ request_body = request.body
223
+ if request_body and hasattr(request_body, 'seek'):
224
+ position = request_body.tell()
225
+ read_chunksize = functools.partial(
226
+ request_body.read, PAYLOAD_BUFFER
227
+ )
228
+ checksum = sha256()
229
+ for chunk in iter(read_chunksize, b''):
230
+ checksum.update(chunk)
231
+ hex_checksum = checksum.hexdigest()
232
+ request_body.seek(position)
233
+ return hex_checksum
234
+ elif request_body:
235
+ # The request serialization has ensured that
236
+ # request.body is a bytes() type.
237
+ return sha256(request_body).hexdigest()
238
+ else:
239
+ return EMPTY_SHA256_HASH
240
+
241
+ def _should_sha256_sign_payload(self, request):
242
+ # Payloads will always be signed over insecure connections.
243
+ if not request.url.startswith('https'):
244
+ return True
245
+
246
+ # Certain operations may have payload signing disabled by default.
247
+ # Since we don't have access to the operation model, we pass in this
248
+ # bit of metadata through the request context.
249
+ return request.context.get('payload_signing_enabled', True)
250
+
251
+ def canonical_request(self, request):
252
+ cr = [request.method.upper()]
253
+ path = self._normalize_url_path(urlsplit(request.url).path)
254
+ cr.append(path)
255
+ cr.append(self.canonical_query_string(request))
256
+ headers_to_sign = self.headers_to_sign(request)
257
+ cr.append(self.canonical_headers(headers_to_sign) + '\n')
258
+ cr.append(self.signed_headers(headers_to_sign))
259
+ if 'X-Amz-Content-SHA256' in request.headers:
260
+ body_checksum = request.headers['X-Amz-Content-SHA256']
261
+ else:
262
+ body_checksum = self.payload(request)
263
+ cr.append(body_checksum)
264
+ return '\n'.join(cr)
265
+
266
+ def _normalize_url_path(self, path):
267
+ normalized_path = quote(normalize_url_path(path), safe='/~')
268
+ return normalized_path
269
+
270
+ def scope(self, request):
271
+ scope = [self.credentials.access_key]
272
+ scope.append(request.context['timestamp'][0:8])
273
+ scope.append(self._region_name)
274
+ scope.append(self._service_name)
275
+ scope.append('aws4_request')
276
+ return '/'.join(scope)
277
+
278
+ def credential_scope(self, request):
279
+ scope = []
280
+ scope.append(request.context['timestamp'][0:8])
281
+ scope.append(self._region_name)
282
+ scope.append(self._service_name)
283
+ scope.append('aws4_request')
284
+ return '/'.join(scope)
285
+
286
+ def string_to_sign(self, request, canonical_request):
287
+ """
288
+ Return the canonical StringToSign as well as a dict
289
+ containing the original version of all headers that
290
+ were included in the StringToSign.
291
+ """
292
+ sts = ['AWS4-HMAC-SHA256']
293
+ sts.append(request.context['timestamp'])
294
+ sts.append(self.credential_scope(request))
295
+ sts.append(sha256(canonical_request.encode('utf-8')).hexdigest())
296
+ return '\n'.join(sts)
297
+
298
+ def signature(self, string_to_sign, request):
299
+ key = self.credentials.secret_key
300
+ k_date = self._sign(
301
+ (f"AWS4{key}").encode(), request.context["timestamp"][0:8]
302
+ )
303
+ k_region = self._sign(k_date, self._region_name)
304
+ k_service = self._sign(k_region, self._service_name)
305
+ k_signing = self._sign(k_service, 'aws4_request')
306
+ return self._sign(k_signing, string_to_sign, hex=True)
307
+
308
+ def add_auth(self, request):
309
+ if self.credentials is None:
310
+ raise NoCredentialsError()
311
+ datetime_now = datetime.datetime.utcnow()
312
+ request.context['timestamp'] = datetime_now.strftime(SIGV4_TIMESTAMP)
313
+ # This could be a retry. Make sure the previous
314
+ # authorization header is removed first.
315
+ self._modify_request_before_signing(request)
316
+ canonical_request = self.canonical_request(request)
317
+ logger.debug("Calculating signature using v4 auth.")
318
+ logger.debug('CanonicalRequest:\n%s', canonical_request)
319
+ string_to_sign = self.string_to_sign(request, canonical_request)
320
+ logger.debug('StringToSign:\n%s', string_to_sign)
321
+ signature = self.signature(string_to_sign, request)
322
+ logger.debug('Signature:\n%s', signature)
323
+
324
+ self._inject_signature_to_request(request, signature)
325
+
326
+ def _inject_signature_to_request(self, request, signature):
327
+ auth_str = ['AWS4-HMAC-SHA256 Credential=%s' % self.scope(request)]
328
+ headers_to_sign = self.headers_to_sign(request)
329
+ auth_str.append(
330
+ f"SignedHeaders={self.signed_headers(headers_to_sign)}"
331
+ )
332
+ auth_str.append('Signature=%s' % signature)
333
+ request.headers['Authorization'] = ', '.join(auth_str)
334
+ return request
335
+
336
+ def _modify_request_before_signing(self, request):
337
+ if 'Authorization' in request.headers:
338
+ del request.headers['Authorization']
339
+ self._set_necessary_date_headers(request)
340
+ if self.credentials.token:
341
+ if 'X-Amz-Security-Token' in request.headers:
342
+ del request.headers['X-Amz-Security-Token']
343
+ request.headers['X-Amz-Security-Token'] = self.credentials.token
344
+
345
+ if not request.context.get('payload_signing_enabled', True):
346
+ if 'X-Amz-Content-SHA256' in request.headers:
347
+ del request.headers['X-Amz-Content-SHA256']
348
+ request.headers['X-Amz-Content-SHA256'] = UNSIGNED_PAYLOAD
349
+
350
+ def _set_necessary_date_headers(self, request):
351
+ # The spec allows for either the Date _or_ the X-Amz-Date value to be
352
+ # used so we check both. If there's a Date header, we use the date
353
+ # header. Otherwise we use the X-Amz-Date header.
354
+ if 'Date' in request.headers:
355
+ del request.headers['Date']
356
+ datetime_timestamp = datetime.datetime.strptime(
357
+ request.context['timestamp'], SIGV4_TIMESTAMP
358
+ )
359
+ request.headers['Date'] = formatdate(
360
+ int(calendar.timegm(datetime_timestamp.timetuple()))
361
+ )
362
+ if 'X-Amz-Date' in request.headers:
363
+ del request.headers['X-Amz-Date']
364
+ else:
365
+ if 'X-Amz-Date' in request.headers:
366
+ del request.headers['X-Amz-Date']
367
+ request.headers['X-Amz-Date'] = request.context['timestamp']
368
+
369
+
370
+ class SigV4QueryAuth(SigV4Auth):
371
+ DEFAULT_EXPIRES = 3600
372
+
373
+ def __init__(
374
+ self, credentials, service_name, region_name, expires=DEFAULT_EXPIRES
375
+ ):
376
+ super().__init__(credentials, service_name, region_name)
377
+ self._expires = expires
378
+
379
+ def _modify_request_before_signing(self, request):
380
+ # We automatically set this header, so if it's the auto-set value we
381
+ # want to get rid of it since it doesn't make sense for presigned urls.
382
+ content_type = request.headers.get('content-type')
383
+ blacklisted_content_type = (
384
+ 'application/x-www-form-urlencoded; charset=utf-8'
385
+ )
386
+ if content_type == blacklisted_content_type:
387
+ del request.headers['content-type']
388
+
389
+ # Note that we're not including X-Amz-Signature.
390
+ # From the docs: "The Canonical Query String must include all the query
391
+ # parameters from the preceding table except for X-Amz-Signature.
392
+ signed_headers = self.signed_headers(self.headers_to_sign(request))
393
+
394
+ auth_params = {
395
+ 'X-Amz-Algorithm': 'AWS4-HMAC-SHA256',
396
+ 'X-Amz-Credential': self.scope(request),
397
+ 'X-Amz-Date': request.context['timestamp'],
398
+ 'X-Amz-Expires': self._expires,
399
+ 'X-Amz-SignedHeaders': signed_headers,
400
+ }
401
+ if self.credentials.token is not None:
402
+ auth_params['X-Amz-Security-Token'] = self.credentials.token
403
+ # Now parse the original query string to a dict, inject our new query
404
+ # params, and serialize back to a query string.
405
+ url_parts = urlsplit(request.url)
406
+ # parse_qs makes each value a list, but in our case we know we won't
407
+ # have repeated keys so we know we have single element lists which we
408
+ # can convert back to scalar values.
409
+ query_string_parts = parse_qs(url_parts.query, keep_blank_values=True)
410
+ query_dict = {k: v[0] for k, v in query_string_parts.items()}
411
+
412
+ if request.params:
413
+ query_dict.update(request.params)
414
+ request.params = {}
415
+ # The spec is particular about this. It *has* to be:
416
+ # https://<endpoint>?<operation params>&<auth params>
417
+ # You can't mix the two types of params together, i.e just keep doing
418
+ # new_query_params.update(op_params)
419
+ # new_query_params.update(auth_params)
420
+ # percent_encode_sequence(new_query_params)
421
+ operation_params = ''
422
+ if request.data:
423
+ # We also need to move the body params into the query string. To
424
+ # do this, we first have to convert it to a dict.
425
+ query_dict.update(_get_body_as_dict(request))
426
+ request.data = ''
427
+ if query_dict:
428
+ operation_params = percent_encode_sequence(query_dict) + '&'
429
+ new_query_string = (
430
+ f"{operation_params}{percent_encode_sequence(auth_params)}"
431
+ )
432
+ # url_parts is a tuple (and therefore immutable) so we need to create
433
+ # a new url_parts with the new query string.
434
+ # <part> - <index>
435
+ # scheme - 0
436
+ # netloc - 1
437
+ # path - 2
438
+ # query - 3 <-- we're replacing this.
439
+ # fragment - 4
440
+ p = url_parts
441
+ new_url_parts = (p[0], p[1], p[2], new_query_string, p[4])
442
+ request.url = urlunsplit(new_url_parts)
443
+
444
+ def _inject_signature_to_request(self, request, signature):
445
+ # Rather than calculating an "Authorization" header, for the query
446
+ # param quth, we just append an 'X-Amz-Signature' param to the end
447
+ # of the query string.
448
+ request.url += '&X-Amz-Signature=%s' % signature
449
+
450
+
451
+ class S3SigV4QueryAuth(SigV4QueryAuth):
452
+ """S3 SigV4 auth using query parameters.
453
+
454
+ This signer will sign a request using query parameters and signature
455
+ version 4, i.e a "presigned url" signer.
456
+
457
+ Based off of:
458
+
459
+ http://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html
460
+
461
+ """
462
+
463
+ def _normalize_url_path(self, path):
464
+ # For S3, we do not normalize the path.
465
+ return path
466
+
467
+ def payload(self, request):
468
+ # From the doc link above:
469
+ # "You don't include a payload hash in the Canonical Request, because
470
+ # when you create a presigned URL, you don't know anything about the
471
+ # payload. Instead, you use a constant string "UNSIGNED-PAYLOAD".
472
+ return UNSIGNED_PAYLOAD