1# Copyright 2014 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2#
3# Licensed under the Apache License, Version 2.0 (the "License"). You
4# may not use this file except in compliance with the License. A copy of
5# the License is located at
6#
7# http://aws.amazon.com/apache2.0/
8#
9# or in the "license" file accompanying this file. This file is
10# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
11# ANY KIND, either express or implied. See the License for the specific
12# language governing permissions and limitations under the License.
13import base64
14import datetime
15import json
16import weakref
17
18import botocore
19import botocore.auth
20from botocore.awsrequest import create_request_object, prepare_request_dict
21from botocore.compat import OrderedDict
22from botocore.exceptions import (
23 ParamValidationError,
24 UnknownClientMethodError,
25 UnknownSignatureVersionError,
26 UnsupportedSignatureVersionError,
27)
28from botocore.tokens import FrozenAuthToken
29from botocore.utils import (
30 ArnParser,
31 datetime2timestamp,
32 fix_s3_host, # noqa: F401
33)
34
35
36class RequestSigner:
37 """
38 An object to sign requests before they go out over the wire using
39 one of the authentication mechanisms defined in ``auth.py``. This
40 class fires two events scoped to a service and operation name:
41
42 * choose-signer: Allows overriding the auth signer name.
43 * before-sign: Allows mutating the request before signing.
44
45 Together these events allow for customization of the request
46 signing pipeline, including overrides, request path manipulation,
47 and disabling signing per operation.
48
49
50 :type service_id: botocore.model.ServiceId
51 :param service_id: The service id for the service, e.g. ``S3``
52
53 :type region_name: string
54 :param region_name: Name of the service region, e.g. ``us-east-1``
55
56 :type signing_name: string
57 :param signing_name: Service signing name. This is usually the
58 same as the service name, but can differ. E.g.
59 ``emr`` vs. ``elasticmapreduce``.
60
61 :type signature_version: string
62 :param signature_version: Signature name like ``v4``.
63
64 :type credentials: :py:class:`~botocore.credentials.Credentials`
65 :param credentials: User credentials with which to sign requests.
66
67 :type event_emitter: :py:class:`~botocore.hooks.BaseEventHooks`
68 :param event_emitter: Extension mechanism to fire events.
69 """
70
71 def __init__(
72 self,
73 service_id,
74 region_name,
75 signing_name,
76 signature_version,
77 credentials,
78 event_emitter,
79 auth_token=None,
80 ):
81 self._region_name = region_name
82 self._signing_name = signing_name
83 self._signature_version = signature_version
84 self._credentials = credentials
85 self._auth_token = auth_token
86 self._service_id = service_id
87
88 # We need weakref to prevent leaking memory in Python 2.6 on Linux 2.6
89 self._event_emitter = weakref.proxy(event_emitter)
90
91 @property
92 def region_name(self):
93 return self._region_name
94
95 @property
96 def signature_version(self):
97 return self._signature_version
98
99 @property
100 def signing_name(self):
101 return self._signing_name
102
103 def handler(self, operation_name=None, request=None, **kwargs):
104 # This is typically hooked up to the "request-created" event
105 # from a client's event emitter. When a new request is created
106 # this method is invoked to sign the request.
107 # Don't call this method directly.
108 return self.sign(operation_name, request)
109
110 def sign(
111 self,
112 operation_name,
113 request,
114 region_name=None,
115 signing_type='standard',
116 expires_in=None,
117 signing_name=None,
118 ):
119 """Sign a request before it goes out over the wire.
120
121 :type operation_name: string
122 :param operation_name: The name of the current operation, e.g.
123 ``ListBuckets``.
124 :type request: AWSRequest
125 :param request: The request object to be sent over the wire.
126
127 :type region_name: str
128 :param region_name: The region to sign the request for.
129
130 :type signing_type: str
131 :param signing_type: The type of signing to perform. This can be one of
132 three possible values:
133
134 * 'standard' - This should be used for most requests.
135 * 'presign-url' - This should be used when pre-signing a request.
136 * 'presign-post' - This should be used when pre-signing an S3 post.
137
138 :type expires_in: int
139 :param expires_in: The number of seconds the presigned url is valid
140 for. This parameter is only valid for signing type 'presign-url'.
141
142 :type signing_name: str
143 :param signing_name: The name to use for the service when signing.
144 """
145 explicit_region_name = region_name
146 if region_name is None:
147 region_name = self._region_name
148
149 if signing_name is None:
150 signing_name = self._signing_name
151
152 signature_version = self._choose_signer(
153 operation_name, signing_type, request.context
154 )
155
156 # Allow mutating request before signing
157 self._event_emitter.emit(
158 f'before-sign.{self._service_id.hyphenize()}.{operation_name}',
159 request=request,
160 signing_name=signing_name,
161 region_name=self._region_name,
162 signature_version=signature_version,
163 request_signer=self,
164 operation_name=operation_name,
165 )
166
167 if signature_version != botocore.UNSIGNED:
168 kwargs = {
169 'signing_name': signing_name,
170 'region_name': region_name,
171 'signature_version': signature_version,
172 }
173 if expires_in is not None:
174 kwargs['expires'] = expires_in
175 signing_context = request.context.get('signing', {})
176 if not explicit_region_name and signing_context.get('region'):
177 kwargs['region_name'] = signing_context['region']
178 if signing_context.get('signing_name'):
179 kwargs['signing_name'] = signing_context['signing_name']
180 if signing_context.get('request_credentials'):
181 kwargs['request_credentials'] = signing_context[
182 'request_credentials'
183 ]
184 if signing_context.get('identity_cache') is not None:
185 self._resolve_identity_cache(
186 kwargs,
187 signing_context['identity_cache'],
188 signing_context['cache_key'],
189 )
190 try:
191 auth = self.get_auth_instance(**kwargs)
192 except UnknownSignatureVersionError as e:
193 if signing_type != 'standard':
194 raise UnsupportedSignatureVersionError(
195 signature_version=signature_version
196 )
197 else:
198 raise e
199
200 auth.add_auth(request)
201
202 def _resolve_identity_cache(self, kwargs, cache, cache_key):
203 kwargs['identity_cache'] = cache
204 kwargs['cache_key'] = cache_key
205
206 def _choose_signer(self, operation_name, signing_type, context):
207 """
208 Allow setting the signature version via the choose-signer event.
209 A value of `botocore.UNSIGNED` means no signing will be performed.
210
211 :param operation_name: The operation to sign.
212 :param signing_type: The type of signing that the signer is to be used
213 for.
214 :return: The signature version to sign with.
215 """
216 signing_type_suffix_map = {
217 'presign-post': '-presign-post',
218 'presign-url': '-query',
219 }
220 suffix = signing_type_suffix_map.get(signing_type, '')
221
222 # operation specific signing context takes precedent over client-level
223 # defaults
224 signature_version = context.get('auth_type') or self._signature_version
225 signing = context.get('signing', {})
226 signing_name = signing.get('signing_name', self._signing_name)
227 region_name = signing.get('region', self._region_name)
228 if (
229 signature_version is not botocore.UNSIGNED
230 and not signature_version.endswith(suffix)
231 ):
232 signature_version += suffix
233
234 handler, response = self._event_emitter.emit_until_response(
235 f'choose-signer.{self._service_id.hyphenize()}.{operation_name}',
236 signing_name=signing_name,
237 region_name=region_name,
238 signature_version=signature_version,
239 context=context,
240 )
241
242 if response is not None:
243 signature_version = response
244 # The suffix needs to be checked again in case we get an improper
245 # signature version from choose-signer.
246 if (
247 signature_version is not botocore.UNSIGNED
248 and not signature_version.endswith(suffix)
249 ):
250 signature_version += suffix
251
252 return signature_version
253
254 def get_auth_instance(
255 self,
256 signing_name,
257 region_name,
258 signature_version=None,
259 request_credentials=None,
260 **kwargs,
261 ):
262 """
263 Get an auth instance which can be used to sign a request
264 using the given signature version.
265
266 :type signing_name: string
267 :param signing_name: Service signing name. This is usually the
268 same as the service name, but can differ. E.g.
269 ``emr`` vs. ``elasticmapreduce``.
270
271 :type region_name: string
272 :param region_name: Name of the service region, e.g. ``us-east-1``
273
274 :type signature_version: string
275 :param signature_version: Signature name like ``v4``.
276
277 :rtype: :py:class:`~botocore.auth.BaseSigner`
278 :return: Auth instance to sign a request.
279 """
280 if signature_version is None:
281 signature_version = self._signature_version
282
283 cls = botocore.auth.AUTH_TYPE_MAPS.get(signature_version)
284 if cls is None:
285 raise UnknownSignatureVersionError(
286 signature_version=signature_version
287 )
288
289 if cls.REQUIRES_TOKEN is True:
290 if self._auth_token and not isinstance(
291 self._auth_token, FrozenAuthToken
292 ):
293 frozen_token = self._auth_token.get_frozen_token()
294 else:
295 frozen_token = self._auth_token
296 auth = cls(frozen_token)
297 return auth
298
299 credentials = request_credentials or self._credentials
300 if getattr(cls, "REQUIRES_IDENTITY_CACHE", None) is True:
301 cache = kwargs["identity_cache"]
302 key = kwargs["cache_key"]
303 credentials = cache.get_credentials(key)
304 del kwargs["cache_key"]
305
306 # If there's no credentials provided (i.e credentials is None),
307 # then we'll pass a value of "None" over to the auth classes,
308 # which already handle the cases where no credentials have
309 # been provided.
310 frozen_credentials = None
311 if credentials is not None:
312 frozen_credentials = credentials.get_frozen_credentials()
313 kwargs['credentials'] = frozen_credentials
314 if cls.REQUIRES_REGION:
315 if self._region_name is None:
316 raise botocore.exceptions.NoRegionError()
317 kwargs['region_name'] = region_name
318 kwargs['service_name'] = signing_name
319 auth = cls(**kwargs)
320 return auth
321
322 # Alias get_auth for backwards compatibility.
323 get_auth = get_auth_instance
324
325 def generate_presigned_url(
326 self,
327 request_dict,
328 operation_name,
329 expires_in=3600,
330 region_name=None,
331 signing_name=None,
332 ):
333 """Generates a presigned url
334
335 :type request_dict: dict
336 :param request_dict: The prepared request dictionary returned by
337 ``botocore.awsrequest.prepare_request_dict()``
338
339 :type operation_name: str
340 :param operation_name: The operation being signed.
341
342 :type expires_in: int
343 :param expires_in: The number of seconds the presigned url is valid
344 for. By default it expires in an hour (3600 seconds)
345
346 :type region_name: string
347 :param region_name: The region name to sign the presigned url.
348
349 :type signing_name: str
350 :param signing_name: The name to use for the service when signing.
351
352 :returns: The presigned url
353 """
354 request = create_request_object(request_dict)
355 self.sign(
356 operation_name,
357 request,
358 region_name,
359 'presign-url',
360 expires_in,
361 signing_name,
362 )
363
364 request.prepare()
365 return request.url
366
367
368class CloudFrontSigner:
369 '''A signer to create a signed CloudFront URL.
370
371 First you create a cloudfront signer based on a normalized RSA signer::
372
373 import rsa
374 def rsa_signer(message):
375 private_key = open('private_key.pem', 'r').read()
376 return rsa.sign(
377 message,
378 rsa.PrivateKey.load_pkcs1(private_key.encode('utf8')),
379 'SHA-1') # CloudFront requires SHA-1 hash
380 cf_signer = CloudFrontSigner(key_id, rsa_signer)
381
382 To sign with a canned policy::
383
384 signed_url = cf_signer.generate_signed_url(
385 url, date_less_than=datetime(2015, 12, 1))
386
387 To sign with a custom policy::
388
389 signed_url = cf_signer.generate_signed_url(url, policy=my_policy)
390 '''
391
392 def __init__(self, key_id, rsa_signer):
393 """Create a CloudFrontSigner.
394
395 :type key_id: str
396 :param key_id: The CloudFront Key Pair ID
397
398 :type rsa_signer: callable
399 :param rsa_signer: An RSA signer.
400 Its only input parameter will be the message to be signed,
401 and its output will be the signed content as a binary string.
402 The hash algorithm needed by CloudFront is SHA-1.
403 """
404 self.key_id = key_id
405 self.rsa_signer = rsa_signer
406
407 def generate_presigned_url(self, url, date_less_than=None, policy=None):
408 """Creates a signed CloudFront URL based on given parameters.
409
410 :type url: str
411 :param url: The URL of the protected object
412
413 :type date_less_than: datetime
414 :param date_less_than: The URL will expire after that date and time
415
416 :type policy: str
417 :param policy: The custom policy, possibly built by self.build_policy()
418
419 :rtype: str
420 :return: The signed URL.
421 """
422 both_args_supplied = date_less_than is not None and policy is not None
423 neither_arg_supplied = date_less_than is None and policy is None
424 if both_args_supplied or neither_arg_supplied:
425 e = 'Need to provide either date_less_than or policy, but not both'
426 raise ValueError(e)
427 if date_less_than is not None:
428 # We still need to build a canned policy for signing purpose
429 policy = self.build_policy(url, date_less_than)
430 if isinstance(policy, str):
431 policy = policy.encode('utf8')
432 if date_less_than is not None:
433 params = [f'Expires={int(datetime2timestamp(date_less_than))}']
434 else:
435 params = [f"Policy={self._url_b64encode(policy).decode('utf8')}"]
436 signature = self.rsa_signer(policy)
437 params.extend(
438 [
439 f"Signature={self._url_b64encode(signature).decode('utf8')}",
440 f"Key-Pair-Id={self.key_id}",
441 ]
442 )
443 return self._build_url(url, params)
444
445 def _build_url(self, base_url, extra_params):
446 separator = '&' if '?' in base_url else '?'
447 return base_url + separator + '&'.join(extra_params)
448
449 def build_policy(
450 self, resource, date_less_than, date_greater_than=None, ip_address=None
451 ):
452 """A helper to build policy.
453
454 :type resource: str
455 :param resource: The URL or the stream filename of the protected object
456
457 :type date_less_than: datetime
458 :param date_less_than: The URL will expire after the time has passed
459
460 :type date_greater_than: datetime
461 :param date_greater_than: The URL will not be valid until this time
462
463 :type ip_address: str
464 :param ip_address: Use 'x.x.x.x' for an IP, or 'x.x.x.x/x' for a subnet
465
466 :rtype: str
467 :return: The policy in a compact string.
468 """
469 # Note:
470 # 1. Order in canned policy is significant. Special care has been taken
471 # to ensure the output will match the order defined by the document.
472 # There is also a test case to ensure that order.
473 # SEE: http://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-creating-signed-url-canned-policy.html#private-content-canned-policy-creating-policy-statement
474 # 2. Albeit the order in custom policy is not required by CloudFront,
475 # we still use OrderedDict internally to ensure the result is stable
476 # and also matches canned policy requirement.
477 # SEE: http://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-creating-signed-url-custom-policy.html
478 moment = int(datetime2timestamp(date_less_than))
479 condition = OrderedDict({"DateLessThan": {"AWS:EpochTime": moment}})
480 if ip_address:
481 if '/' not in ip_address:
482 ip_address += '/32'
483 condition["IpAddress"] = {"AWS:SourceIp": ip_address}
484 if date_greater_than:
485 moment = int(datetime2timestamp(date_greater_than))
486 condition["DateGreaterThan"] = {"AWS:EpochTime": moment}
487 ordered_payload = [('Resource', resource), ('Condition', condition)]
488 custom_policy = {"Statement": [OrderedDict(ordered_payload)]}
489 return json.dumps(custom_policy, separators=(',', ':'))
490
491 def _url_b64encode(self, data):
492 # Required by CloudFront. See also:
493 # http://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-linux-openssl.html
494 return (
495 base64.b64encode(data)
496 .replace(b'+', b'-')
497 .replace(b'=', b'_')
498 .replace(b'/', b'~')
499 )
500
501
502def add_generate_db_auth_token(class_attributes, **kwargs):
503 class_attributes['generate_db_auth_token'] = generate_db_auth_token
504
505
506def add_dsql_generate_db_auth_token_methods(class_attributes, **kwargs):
507 class_attributes['generate_db_connect_auth_token'] = (
508 dsql_generate_db_connect_auth_token
509 )
510 class_attributes['generate_db_connect_admin_auth_token'] = (
511 dsql_generate_db_connect_admin_auth_token
512 )
513
514
515def generate_db_auth_token(self, DBHostname, Port, DBUsername, Region=None):
516 """Generates an auth token used to connect to a db with IAM credentials.
517
518 :type DBHostname: str
519 :param DBHostname: The hostname of the database to connect to.
520
521 :type Port: int
522 :param Port: The port number the database is listening on.
523
524 :type DBUsername: str
525 :param DBUsername: The username to log in as.
526
527 :type Region: str
528 :param Region: The region the database is in. If None, the client
529 region will be used.
530
531 :return: A presigned url which can be used as an auth token.
532 """
533 region = Region
534 if region is None:
535 region = self.meta.region_name
536
537 params = {
538 'Action': 'connect',
539 'DBUser': DBUsername,
540 }
541
542 request_dict = {
543 'url_path': '/',
544 'query_string': '',
545 'headers': {},
546 'body': params,
547 'method': 'GET',
548 }
549
550 # RDS requires that the scheme not be set when sent over. This can cause
551 # issues when signing because the Python url parsing libraries follow
552 # RFC 1808 closely, which states that a netloc must be introduced by `//`.
553 # Otherwise the url is presumed to be relative, and thus the whole
554 # netloc would be treated as a path component. To work around this we
555 # introduce https here and remove it once we're done processing it.
556 scheme = 'https://'
557 endpoint_url = f'{scheme}{DBHostname}:{Port}'
558 prepare_request_dict(request_dict, endpoint_url)
559 presigned_url = self._request_signer.generate_presigned_url(
560 operation_name='connect',
561 request_dict=request_dict,
562 region_name=region,
563 expires_in=900,
564 signing_name='rds-db',
565 )
566 return presigned_url[len(scheme) :]
567
568
569def _dsql_generate_db_auth_token(
570 self, Hostname, Action, Region=None, ExpiresIn=900
571):
572 """Generate a DSQL database token for an arbitrary action.
573
574 :type Hostname: str
575 :param Hostname: The DSQL endpoint host name.
576
577 :type Action: str
578 :param Action: Action to perform on the cluster (DbConnectAdmin or DbConnect).
579
580 :type Region: str
581 :param Region: The AWS region where the DSQL Cluster is hosted. If None, the client region will be used.
582
583 :type ExpiresIn: int
584 :param ExpiresIn: The token expiry duration in seconds (default is 900 seconds).
585
586 :return: A presigned url which can be used as an auth token.
587 """
588 possible_actions = ("DbConnect", "DbConnectAdmin")
589
590 if Action not in possible_actions:
591 raise ParamValidationError(
592 report=f"Received {Action} for action but expected one of: {', '.join(possible_actions)}"
593 )
594
595 if Region is None:
596 Region = self.meta.region_name
597
598 request_dict = {
599 'url_path': '/',
600 'query_string': '',
601 'headers': {},
602 'body': {
603 'Action': Action,
604 },
605 'method': 'GET',
606 }
607 scheme = 'https://'
608 endpoint_url = f'{scheme}{Hostname}'
609 prepare_request_dict(request_dict, endpoint_url)
610 presigned_url = self._request_signer.generate_presigned_url(
611 operation_name=Action,
612 request_dict=request_dict,
613 region_name=Region,
614 expires_in=ExpiresIn,
615 signing_name='dsql',
616 )
617 return presigned_url[len(scheme) :]
618
619
620def dsql_generate_db_connect_auth_token(
621 self, Hostname, Region=None, ExpiresIn=900
622):
623 """Generate a DSQL database token for the "DbConnect" action.
624
625 :type Hostname: str
626 :param Hostname: The DSQL endpoint host name.
627
628 :type Region: str
629 :param Region: The AWS region where the DSQL Cluster is hosted. If None, the client region will be used.
630
631 :type ExpiresIn: int
632 :param ExpiresIn: The token expiry duration in seconds (default is 900 seconds).
633
634 :return: A presigned url which can be used as an auth token.
635 """
636 return _dsql_generate_db_auth_token(
637 self, Hostname, "DbConnect", Region, ExpiresIn
638 )
639
640
641def dsql_generate_db_connect_admin_auth_token(
642 self, Hostname, Region=None, ExpiresIn=900
643):
644 """Generate a DSQL database token for the "DbConnectAdmin" action.
645
646 :type Hostname: str
647 :param Hostname: The DSQL endpoint host name.
648
649 :type Region: str
650 :param Region: The AWS region where the DSQL Cluster is hosted. If None, the client region will be used.
651
652 :type ExpiresIn: int
653 :param ExpiresIn: The token expiry duration in seconds (default is 900 seconds).
654
655 :return: A presigned url which can be used as an auth token.
656 """
657 return _dsql_generate_db_auth_token(
658 self, Hostname, "DbConnectAdmin", Region, ExpiresIn
659 )
660
661
662class S3PostPresigner:
663 def __init__(self, request_signer):
664 self._request_signer = request_signer
665
666 def generate_presigned_post(
667 self,
668 request_dict,
669 fields=None,
670 conditions=None,
671 expires_in=3600,
672 region_name=None,
673 ):
674 """Generates the url and the form fields used for a presigned s3 post
675
676 :type request_dict: dict
677 :param request_dict: The prepared request dictionary returned by
678 ``botocore.awsrequest.prepare_request_dict()``
679
680 :type fields: dict
681 :param fields: A dictionary of prefilled form fields to build on top
682 of.
683
684 :type conditions: list
685 :param conditions: A list of conditions to include in the policy. Each
686 element can be either a list or a structure. For example:
687
688 .. code:: python
689
690 [
691 {"acl": "public-read"},
692 {"bucket": "amzn-s3-demo-bucket"},
693 ["starts-with", "$key", "mykey"]
694 ]
695
696 :type expires_in: int
697 :param expires_in: The number of seconds the presigned post is valid
698 for.
699
700 :type region_name: string
701 :param region_name: The region name to sign the presigned post to.
702
703 :rtype: dict
704 :returns: A dictionary with two elements: ``url`` and ``fields``.
705 Url is the url to post to. Fields is a dictionary filled with
706 the form fields and respective values to use when submitting the
707 post. For example:
708
709 .. code:: python
710
711 {
712 'url': 'https://amzn-s3-demo-bucket.s3.amazonaws.com',
713 'fields': {
714 'acl': 'public-read',
715 'key': 'mykey',
716 'signature': 'mysignature',
717 'policy': 'mybase64 encoded policy'
718 }
719 }
720 """
721 if fields is None:
722 fields = {}
723
724 if conditions is None:
725 conditions = []
726
727 # Create the policy for the post.
728 policy = {}
729
730 # Create an expiration date for the policy
731 datetime_now = datetime.datetime.utcnow()
732 expire_date = datetime_now + datetime.timedelta(seconds=expires_in)
733 policy['expiration'] = expire_date.strftime(botocore.auth.ISO8601)
734
735 # Append all of the conditions that the user supplied.
736 policy['conditions'] = []
737 for condition in conditions:
738 policy['conditions'].append(condition)
739
740 # Store the policy and the fields in the request for signing
741 request = create_request_object(request_dict)
742 request.context['s3-presign-post-fields'] = fields
743 request.context['s3-presign-post-policy'] = policy
744
745 self._request_signer.sign(
746 'PutObject', request, region_name, 'presign-post'
747 )
748 # Return the url and the fields for th form to post.
749 return {'url': request.url, 'fields': fields}
750
751
752def add_generate_presigned_url(class_attributes, **kwargs):
753 class_attributes['generate_presigned_url'] = generate_presigned_url
754
755
756def generate_presigned_url(
757 self, ClientMethod, Params=None, ExpiresIn=3600, HttpMethod=None
758):
759 """Generate a presigned url given a client, its method, and arguments
760
761 :type ClientMethod: string
762 :param ClientMethod: The client method to presign for
763
764 :type Params: dict
765 :param Params: The parameters normally passed to
766 ``ClientMethod``.
767
768 :type ExpiresIn: int
769 :param ExpiresIn: The number of seconds the presigned url is valid
770 for. By default it expires in an hour (3600 seconds)
771
772 :type HttpMethod: string
773 :param HttpMethod: The http method to use on the generated url. By
774 default, the http method is whatever is used in the method's model.
775
776 :returns: The presigned url
777 """
778 client_method = ClientMethod
779 params = Params
780 if params is None:
781 params = {}
782 expires_in = ExpiresIn
783 http_method = HttpMethod
784 context = {
785 'is_presign_request': True,
786 'use_global_endpoint': _should_use_global_endpoint(self),
787 }
788
789 request_signer = self._request_signer
790
791 try:
792 operation_name = self._PY_TO_OP_NAME[client_method]
793 except KeyError:
794 raise UnknownClientMethodError(method_name=client_method)
795
796 operation_model = self.meta.service_model.operation_model(operation_name)
797 params = self._emit_api_params(
798 api_params=params,
799 operation_model=operation_model,
800 context=context,
801 )
802 bucket_is_arn = ArnParser.is_arn(params.get('Bucket', ''))
803 (
804 endpoint_url,
805 additional_headers,
806 properties,
807 ) = self._resolve_endpoint_ruleset(
808 operation_model,
809 params,
810 context,
811 ignore_signing_region=(not bucket_is_arn),
812 )
813
814 request_dict = self._convert_to_request_dict(
815 api_params=params,
816 operation_model=operation_model,
817 endpoint_url=endpoint_url,
818 context=context,
819 headers=additional_headers,
820 set_user_agent_header=False,
821 )
822
823 # Switch out the http method if user specified it.
824 if http_method is not None:
825 request_dict['method'] = http_method
826
827 # Generate the presigned url.
828 return request_signer.generate_presigned_url(
829 request_dict=request_dict,
830 expires_in=expires_in,
831 operation_name=operation_name,
832 )
833
834
835def add_generate_presigned_post(class_attributes, **kwargs):
836 class_attributes['generate_presigned_post'] = generate_presigned_post
837
838
839def generate_presigned_post(
840 self, Bucket, Key, Fields=None, Conditions=None, ExpiresIn=3600
841):
842 """Builds the url and the form fields used for a presigned s3 post
843
844 :type Bucket: string
845 :param Bucket: The name of the bucket to presign the post to. Note that
846 bucket related conditions should not be included in the
847 ``conditions`` parameter.
848
849 :type Key: string
850 :param Key: Key name, optionally add ${filename} to the end to
851 attach the submitted filename. Note that key related conditions and
852 fields are filled out for you and should not be included in the
853 ``Fields`` or ``Conditions`` parameter.
854
855 :type Fields: dict
856 :param Fields: A dictionary of prefilled form fields to build on top
857 of. Elements that may be included are acl, Cache-Control,
858 Content-Type, Content-Disposition, Content-Encoding, Expires,
859 success_action_redirect, redirect, success_action_status,
860 and x-amz-meta-.
861
862 Note that if a particular element is included in the fields
863 dictionary it will not be automatically added to the conditions
864 list. You must specify a condition for the element as well.
865
866 :type Conditions: list
867 :param Conditions: A list of conditions to include in the policy. Each
868 element can be either a list or a structure. For example:
869
870 .. code:: python
871
872 [
873 {"acl": "public-read"},
874 ["content-length-range", 2, 5],
875 ["starts-with", "$success_action_redirect", ""]
876 ]
877
878 Conditions that are included may pertain to acl,
879 content-length-range, Cache-Control, Content-Type,
880 Content-Disposition, Content-Encoding, Expires,
881 success_action_redirect, redirect, success_action_status,
882 and/or x-amz-meta-.
883
884 Note that if you include a condition, you must specify
885 a valid value in the fields dictionary as well. A value will
886 not be added automatically to the fields dictionary based on the
887 conditions.
888
889 :type ExpiresIn: int
890 :param ExpiresIn: The number of seconds the presigned post
891 is valid for.
892
893 :rtype: dict
894 :returns: A dictionary with two elements: ``url`` and ``fields``.
895 Url is the url to post to. Fields is a dictionary filled with
896 the form fields and respective values to use when submitting the
897 post. For example:
898
899 .. code:: python
900
901 {
902 'url': 'https://amzn-s3-demo-bucket.s3.amazonaws.com',
903 'fields': {
904 'acl': 'public-read',
905 'key': 'mykey',
906 'signature': 'mysignature',
907 'policy': 'mybase64 encoded policy'
908 }
909 }
910 """
911 bucket = Bucket
912 key = Key
913 fields = Fields
914 conditions = Conditions
915 expires_in = ExpiresIn
916
917 if fields is None:
918 fields = {}
919 else:
920 fields = fields.copy()
921
922 if conditions is None:
923 conditions = []
924
925 context = {
926 'is_presign_request': True,
927 'use_global_endpoint': _should_use_global_endpoint(self),
928 }
929
930 post_presigner = S3PostPresigner(self._request_signer)
931
932 # We choose the CreateBucket operation model because its url gets
933 # serialized to what a presign post requires.
934 operation_model = self.meta.service_model.operation_model('CreateBucket')
935 params = self._emit_api_params(
936 api_params={'Bucket': bucket},
937 operation_model=operation_model,
938 context=context,
939 )
940 bucket_is_arn = ArnParser.is_arn(params.get('Bucket', ''))
941 (
942 endpoint_url,
943 additional_headers,
944 properties,
945 ) = self._resolve_endpoint_ruleset(
946 operation_model,
947 params,
948 context,
949 ignore_signing_region=(not bucket_is_arn),
950 )
951
952 request_dict = self._convert_to_request_dict(
953 api_params=params,
954 operation_model=operation_model,
955 endpoint_url=endpoint_url,
956 context=context,
957 headers=additional_headers,
958 set_user_agent_header=False,
959 )
960
961 # Append that the bucket name to the list of conditions.
962 conditions.append({'bucket': bucket})
963
964 # If the key ends with filename, the only constraint that can be
965 # imposed is if it starts with the specified prefix.
966 if key.endswith('${filename}'):
967 conditions.append(["starts-with", '$key', key[: -len('${filename}')]])
968 else:
969 conditions.append({'key': key})
970
971 # Add the key to the fields.
972 fields['key'] = key
973
974 return post_presigner.generate_presigned_post(
975 request_dict=request_dict,
976 fields=fields,
977 conditions=conditions,
978 expires_in=expires_in,
979 )
980
981
982def _should_use_global_endpoint(client):
983 if client.meta.partition != 'aws':
984 return False
985 s3_config = client.meta.config.s3
986 if s3_config:
987 if s3_config.get('use_dualstack_endpoint', False):
988 return False
989 if (
990 s3_config.get('us_east_1_regional_endpoint') == 'regional'
991 and client.meta.config.region_name == 'us-east-1'
992 ):
993 return False
994 if s3_config.get('addressing_style') == 'virtual':
995 return False
996 return True