Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/botocore/signers.py: 17%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

264 statements  

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