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.
13"""Resolves regions and endpoints.
14
15This module implements endpoint resolution, including resolving endpoints for a
16given service and region and resolving the available endpoints for a service
17in a specific AWS partition.
18"""
19
20import copy
21import logging
22import re
23from enum import Enum
24
25import jmespath
26
27from botocore import UNSIGNED, xform_name
28from botocore.auth import AUTH_TYPE_MAPS, HAS_CRT
29from botocore.crt import CRT_SUPPORTED_AUTH_TYPES
30from botocore.endpoint_provider import EndpointProvider
31from botocore.exceptions import (
32 EndpointProviderError,
33 EndpointVariantError,
34 InvalidEndpointConfigurationError,
35 InvalidHostLabelError,
36 MissingDependencyException,
37 NoRegionError,
38 ParamValidationError,
39 UnknownEndpointResolutionBuiltInName,
40 UnknownRegionError,
41 UnknownSignatureVersionError,
42 UnsupportedS3AccesspointConfigurationError,
43 UnsupportedS3ConfigurationError,
44 UnsupportedS3ControlArnError,
45 UnsupportedS3ControlConfigurationError,
46)
47from botocore.useragent import register_feature_id
48from botocore.utils import ensure_boolean, instance_cache
49
50LOG = logging.getLogger(__name__)
51DEFAULT_URI_TEMPLATE = '{service}.{region}.{dnsSuffix}' # noqa
52DEFAULT_SERVICE_DATA = {'endpoints': {}}
53
54
55class BaseEndpointResolver:
56 """Resolves regions and endpoints. Must be subclassed."""
57
58 def construct_endpoint(self, service_name, region_name=None):
59 """Resolves an endpoint for a service and region combination.
60
61 :type service_name: string
62 :param service_name: Name of the service to resolve an endpoint for
63 (e.g., s3)
64
65 :type region_name: string
66 :param region_name: Region/endpoint name to resolve (e.g., us-east-1)
67 if no region is provided, the first found partition-wide endpoint
68 will be used if available.
69
70 :rtype: dict
71 :return: Returns a dict containing the following keys:
72 - partition: (string, required) Resolved partition name
73 - endpointName: (string, required) Resolved endpoint name
74 - hostname: (string, required) Hostname to use for this endpoint
75 - sslCommonName: (string) sslCommonName to use for this endpoint.
76 - credentialScope: (dict) Signature version 4 credential scope
77 - region: (string) region name override when signing.
78 - service: (string) service name override when signing.
79 - signatureVersions: (list<string>) A list of possible signature
80 versions, including s3, v4, v2, and s3v4
81 - protocols: (list<string>) A list of supported protocols
82 (e.g., http, https)
83 - ...: Other keys may be included as well based on the metadata
84 """
85 raise NotImplementedError
86
87 def get_available_partitions(self):
88 """Lists the partitions available to the endpoint resolver.
89
90 :return: Returns a list of partition names (e.g., ["aws", "aws-cn"]).
91 """
92 raise NotImplementedError
93
94 def get_available_endpoints(
95 self, service_name, partition_name='aws', allow_non_regional=False
96 ):
97 """Lists the endpoint names of a particular partition.
98
99 :type service_name: string
100 :param service_name: Name of a service to list endpoint for (e.g., s3)
101
102 :type partition_name: string
103 :param partition_name: Name of the partition to limit endpoints to.
104 (e.g., aws for the public AWS endpoints, aws-cn for AWS China
105 endpoints, aws-us-gov for AWS GovCloud (US) Endpoints, etc.
106
107 :type allow_non_regional: bool
108 :param allow_non_regional: Set to True to include endpoints that are
109 not regional endpoints (e.g., s3-external-1,
110 fips-us-gov-west-1, etc).
111 :return: Returns a list of endpoint names (e.g., ["us-east-1"]).
112 """
113 raise NotImplementedError
114
115
116class EndpointResolver(BaseEndpointResolver):
117 """Resolves endpoints based on partition endpoint metadata"""
118
119 _UNSUPPORTED_DUALSTACK_PARTITIONS = ['aws-iso', 'aws-iso-b']
120
121 def __init__(self, endpoint_data, uses_builtin_data=False):
122 """
123 :type endpoint_data: dict
124 :param endpoint_data: A dict of partition data.
125
126 :type uses_builtin_data: boolean
127 :param uses_builtin_data: Whether the endpoint data originates in the
128 package's data directory.
129 """
130 if 'partitions' not in endpoint_data:
131 raise ValueError('Missing "partitions" in endpoint data')
132 self._endpoint_data = endpoint_data
133 self.uses_builtin_data = uses_builtin_data
134
135 def get_service_endpoints_data(self, service_name, partition_name='aws'):
136 for partition in self._endpoint_data['partitions']:
137 if partition['partition'] != partition_name:
138 continue
139 services = partition['services']
140 if service_name not in services:
141 continue
142 return services[service_name]['endpoints']
143
144 def get_available_partitions(self):
145 result = []
146 for partition in self._endpoint_data['partitions']:
147 result.append(partition['partition'])
148 return result
149
150 def get_available_endpoints(
151 self,
152 service_name,
153 partition_name='aws',
154 allow_non_regional=False,
155 endpoint_variant_tags=None,
156 ):
157 result = []
158 for partition in self._endpoint_data['partitions']:
159 if partition['partition'] != partition_name:
160 continue
161 services = partition['services']
162 if service_name not in services:
163 continue
164 service_endpoints = services[service_name]['endpoints']
165 for endpoint_name in service_endpoints:
166 is_regional_endpoint = endpoint_name in partition['regions']
167 # Only regional endpoints can be modeled with variants
168 if endpoint_variant_tags and is_regional_endpoint:
169 variant_data = self._retrieve_variant_data(
170 service_endpoints[endpoint_name], endpoint_variant_tags
171 )
172 if variant_data:
173 result.append(endpoint_name)
174 elif allow_non_regional or is_regional_endpoint:
175 result.append(endpoint_name)
176 return result
177
178 def get_partition_dns_suffix(
179 self, partition_name, endpoint_variant_tags=None
180 ):
181 for partition in self._endpoint_data['partitions']:
182 if partition['partition'] == partition_name:
183 if endpoint_variant_tags:
184 variant = self._retrieve_variant_data(
185 partition.get('defaults'), endpoint_variant_tags
186 )
187 if variant and 'dnsSuffix' in variant:
188 return variant['dnsSuffix']
189 else:
190 return partition['dnsSuffix']
191 return None
192
193 def construct_endpoint(
194 self,
195 service_name,
196 region_name=None,
197 partition_name=None,
198 use_dualstack_endpoint=False,
199 use_fips_endpoint=False,
200 ):
201 if (
202 service_name == 's3'
203 and use_dualstack_endpoint
204 and region_name is None
205 ):
206 region_name = 'us-east-1'
207
208 if partition_name is not None:
209 valid_partition = None
210 for partition in self._endpoint_data['partitions']:
211 if partition['partition'] == partition_name:
212 valid_partition = partition
213
214 if valid_partition is not None:
215 result = self._endpoint_for_partition(
216 valid_partition,
217 service_name,
218 region_name,
219 use_dualstack_endpoint,
220 use_fips_endpoint,
221 True,
222 )
223 return result
224 return None
225
226 # Iterate over each partition until a match is found.
227 for partition in self._endpoint_data['partitions']:
228 if use_dualstack_endpoint and (
229 partition['partition']
230 in self._UNSUPPORTED_DUALSTACK_PARTITIONS
231 ):
232 continue
233 result = self._endpoint_for_partition(
234 partition,
235 service_name,
236 region_name,
237 use_dualstack_endpoint,
238 use_fips_endpoint,
239 )
240 if result:
241 return result
242
243 def get_partition_for_region(self, region_name):
244 for partition in self._endpoint_data['partitions']:
245 if self._region_match(partition, region_name):
246 return partition['partition']
247 raise UnknownRegionError(
248 region_name=region_name,
249 error_msg='No partition found for provided region_name.',
250 )
251
252 def _endpoint_for_partition(
253 self,
254 partition,
255 service_name,
256 region_name,
257 use_dualstack_endpoint,
258 use_fips_endpoint,
259 force_partition=False,
260 ):
261 partition_name = partition["partition"]
262 if (
263 use_dualstack_endpoint
264 and partition_name in self._UNSUPPORTED_DUALSTACK_PARTITIONS
265 ):
266 error_msg = (
267 "Dualstack endpoints are currently not supported"
268 f" for {partition_name} partition"
269 )
270 raise EndpointVariantError(tags=['dualstack'], error_msg=error_msg)
271
272 # Get the service from the partition, or an empty template.
273 service_data = partition['services'].get(
274 service_name, DEFAULT_SERVICE_DATA
275 )
276 # Use the partition endpoint if no region is supplied.
277 if region_name is None:
278 if 'partitionEndpoint' in service_data:
279 region_name = service_data['partitionEndpoint']
280 else:
281 raise NoRegionError()
282
283 resolve_kwargs = {
284 'partition': partition,
285 'service_name': service_name,
286 'service_data': service_data,
287 'endpoint_name': region_name,
288 'use_dualstack_endpoint': use_dualstack_endpoint,
289 'use_fips_endpoint': use_fips_endpoint,
290 }
291
292 # Attempt to resolve the exact region for this partition.
293 if region_name in service_data['endpoints']:
294 return self._resolve(**resolve_kwargs)
295
296 # Check to see if the endpoint provided is valid for the partition.
297 if self._region_match(partition, region_name) or force_partition:
298 # Use the partition endpoint if set and not regionalized.
299 partition_endpoint = service_data.get('partitionEndpoint')
300 is_regionalized = service_data.get('isRegionalized', True)
301 if partition_endpoint and not is_regionalized:
302 LOG.debug(
303 'Using partition endpoint for %s, %s: %s',
304 service_name,
305 region_name,
306 partition_endpoint,
307 )
308 resolve_kwargs['endpoint_name'] = partition_endpoint
309 return self._resolve(**resolve_kwargs)
310 LOG.debug(
311 'Creating a regex based endpoint for %s, %s',
312 service_name,
313 region_name,
314 )
315 return self._resolve(**resolve_kwargs)
316
317 def _region_match(self, partition, region_name):
318 if region_name in partition['regions']:
319 return True
320 if 'regionRegex' in partition:
321 return re.compile(partition['regionRegex']).match(region_name)
322 return False
323
324 def _retrieve_variant_data(self, endpoint_data, tags):
325 variants = endpoint_data.get('variants', [])
326 for variant in variants:
327 if set(variant['tags']) == set(tags):
328 result = variant.copy()
329 return result
330
331 def _create_tag_list(self, use_dualstack_endpoint, use_fips_endpoint):
332 tags = []
333 if use_dualstack_endpoint:
334 tags.append('dualstack')
335 if use_fips_endpoint:
336 tags.append('fips')
337 return tags
338
339 def _resolve_variant(
340 self, tags, endpoint_data, service_defaults, partition_defaults
341 ):
342 result = {}
343 for variants in [endpoint_data, service_defaults, partition_defaults]:
344 variant = self._retrieve_variant_data(variants, tags)
345 if variant:
346 self._merge_keys(variant, result)
347 return result
348
349 def _resolve(
350 self,
351 partition,
352 service_name,
353 service_data,
354 endpoint_name,
355 use_dualstack_endpoint,
356 use_fips_endpoint,
357 ):
358 endpoint_data = service_data.get('endpoints', {}).get(
359 endpoint_name, {}
360 )
361
362 if endpoint_data.get('deprecated'):
363 LOG.warning(
364 f'Client is configured with the deprecated endpoint: {endpoint_name}'
365 )
366
367 service_defaults = service_data.get('defaults', {})
368 partition_defaults = partition.get('defaults', {})
369 tags = self._create_tag_list(use_dualstack_endpoint, use_fips_endpoint)
370
371 if tags:
372 result = self._resolve_variant(
373 tags, endpoint_data, service_defaults, partition_defaults
374 )
375 if result == {}:
376 error_msg = (
377 f"Endpoint does not exist for {service_name} "
378 f"in region {endpoint_name}"
379 )
380 raise EndpointVariantError(tags=tags, error_msg=error_msg)
381 self._merge_keys(endpoint_data, result)
382 else:
383 result = endpoint_data
384
385 # If dnsSuffix has not already been consumed from a variant definition
386 if 'dnsSuffix' not in result:
387 result['dnsSuffix'] = partition['dnsSuffix']
388
389 result['partition'] = partition['partition']
390 result['endpointName'] = endpoint_name
391
392 # Merge in the service defaults then the partition defaults.
393 self._merge_keys(service_defaults, result)
394 self._merge_keys(partition_defaults, result)
395
396 result['hostname'] = self._expand_template(
397 partition,
398 result['hostname'],
399 service_name,
400 endpoint_name,
401 result['dnsSuffix'],
402 )
403 if 'sslCommonName' in result:
404 result['sslCommonName'] = self._expand_template(
405 partition,
406 result['sslCommonName'],
407 service_name,
408 endpoint_name,
409 result['dnsSuffix'],
410 )
411
412 return result
413
414 def _merge_keys(self, from_data, result):
415 for key in from_data:
416 if key not in result:
417 result[key] = from_data[key]
418
419 def _expand_template(
420 self, partition, template, service_name, endpoint_name, dnsSuffix
421 ):
422 return template.format(
423 service=service_name, region=endpoint_name, dnsSuffix=dnsSuffix
424 )
425
426
427class EndpointResolverBuiltins(str, Enum):
428 # The AWS Region configured for the SDK client (str)
429 AWS_REGION = "AWS::Region"
430 # Whether the UseFIPSEndpoint configuration option has been enabled for
431 # the SDK client (bool)
432 AWS_USE_FIPS = "AWS::UseFIPS"
433 # Whether the UseDualStackEndpoint configuration option has been enabled
434 # for the SDK client (bool)
435 AWS_USE_DUALSTACK = "AWS::UseDualStack"
436 # Whether the global endpoint should be used with STS, rather than the
437 # regional endpoint for us-east-1 (bool)
438 AWS_STS_USE_GLOBAL_ENDPOINT = "AWS::STS::UseGlobalEndpoint"
439 # Whether the global endpoint should be used with S3, rather than the
440 # regional endpoint for us-east-1 (bool)
441 AWS_S3_USE_GLOBAL_ENDPOINT = "AWS::S3::UseGlobalEndpoint"
442 # Whether S3 Transfer Acceleration has been requested (bool)
443 AWS_S3_ACCELERATE = "AWS::S3::Accelerate"
444 # Whether S3 Force Path Style has been enabled (bool)
445 AWS_S3_FORCE_PATH_STYLE = "AWS::S3::ForcePathStyle"
446 # Whether to use the ARN region or raise an error when ARN and client
447 # region differ (for s3 service only, bool)
448 AWS_S3_USE_ARN_REGION = "AWS::S3::UseArnRegion"
449 # Whether to use the ARN region or raise an error when ARN and client
450 # region differ (for s3-control service only, bool)
451 AWS_S3CONTROL_USE_ARN_REGION = 'AWS::S3Control::UseArnRegion'
452 # Whether multi-region access points (MRAP) should be disabled (bool)
453 AWS_S3_DISABLE_MRAP = "AWS::S3::DisableMultiRegionAccessPoints"
454 # Whether a custom endpoint has been configured (str)
455 SDK_ENDPOINT = "SDK::Endpoint"
456 # An AWS account ID that can be optionally configured for the SDK client (str)
457 ACCOUNT_ID = "AWS::Auth::AccountId"
458 # Whether an endpoint should include an account ID (str)
459 ACCOUNT_ID_ENDPOINT_MODE = "AWS::Auth::AccountIdEndpointMode"
460
461
462class EndpointRulesetResolver:
463 """Resolves endpoints using a service's endpoint ruleset"""
464
465 def __init__(
466 self,
467 endpoint_ruleset_data,
468 partition_data,
469 service_model,
470 builtins,
471 client_context,
472 event_emitter,
473 use_ssl=True,
474 requested_auth_scheme=None,
475 ):
476 self._provider = EndpointProvider(
477 ruleset_data=endpoint_ruleset_data,
478 partition_data=partition_data,
479 )
480 self._param_definitions = self._provider.ruleset.parameters
481 self._service_model = service_model
482 self._builtins = builtins
483 self._client_context = client_context
484 self._event_emitter = event_emitter
485 self._use_ssl = use_ssl
486 self._requested_auth_scheme = requested_auth_scheme
487 self._instance_cache = {}
488
489 def construct_endpoint(
490 self,
491 operation_model,
492 call_args,
493 request_context,
494 ):
495 """Invokes the provider with params defined in the service's ruleset"""
496 if call_args is None:
497 call_args = {}
498
499 if request_context is None:
500 request_context = {}
501
502 provider_params = self._get_provider_params(
503 operation_model, call_args, request_context
504 )
505 LOG.debug(
506 f'Calling endpoint provider with parameters: {provider_params}'
507 )
508 try:
509 provider_result = self._provider.resolve_endpoint(
510 **provider_params
511 )
512 except EndpointProviderError as ex:
513 botocore_exception = self.ruleset_error_to_botocore_exception(
514 ex, provider_params
515 )
516 if botocore_exception is None:
517 raise
518 else:
519 raise botocore_exception from ex
520 LOG.debug(f'Endpoint provider result: {provider_result.url}')
521
522 # The endpoint provider does not support non-secure transport.
523 if not self._use_ssl and provider_result.url.startswith('https://'):
524 provider_result = provider_result._replace(
525 url=f'http://{provider_result.url[8:]}'
526 )
527
528 # Multi-valued headers are not supported in botocore. Replace the list
529 # of values returned for each header with just its first entry,
530 # dropping any additionally entries.
531 provider_result = provider_result._replace(
532 headers={
533 key: val[0] for key, val in provider_result.headers.items()
534 }
535 )
536
537 return provider_result
538
539 def _get_provider_params(
540 self, operation_model, call_args, request_context
541 ):
542 """Resolve a value for each parameter defined in the service's ruleset
543
544 The resolution order for parameter values is:
545 1. Operation-specific static context values from the service definition
546 2. Operation-specific dynamic context values from API parameters
547 3. Client-specific context parameters
548 4. Built-in values such as region, FIPS usage, ...
549 """
550 provider_params = {}
551 # Builtin values can be customized for each operation by hooks
552 # subscribing to the ``before-endpoint-resolution.*`` event.
553 customized_builtins = self._get_customized_builtins(
554 operation_model, call_args, request_context
555 )
556 for param_name, param_def in self._param_definitions.items():
557 param_val = self._resolve_param_from_context(
558 param_name=param_name,
559 operation_model=operation_model,
560 call_args=call_args,
561 )
562 if param_val is None and param_def.builtin is not None:
563 param_val = self._resolve_param_as_builtin(
564 builtin_name=param_def.builtin,
565 builtins=customized_builtins,
566 )
567 if param_val is not None:
568 provider_params[param_name] = param_val
569 self._register_endpoint_feature_ids(param_name, param_val)
570
571 return provider_params
572
573 def _resolve_param_from_context(
574 self, param_name, operation_model, call_args
575 ):
576 static = self._resolve_param_as_static_context_param(
577 param_name, operation_model
578 )
579 if static is not None:
580 return static
581 dynamic = self._resolve_param_as_dynamic_context_param(
582 param_name, operation_model, call_args
583 )
584 if dynamic is not None:
585 return dynamic
586 operation_context_params = (
587 self._resolve_param_as_operation_context_param(
588 param_name, operation_model, call_args
589 )
590 )
591 if operation_context_params is not None:
592 return operation_context_params
593 return self._resolve_param_as_client_context_param(param_name)
594
595 def _resolve_param_as_static_context_param(
596 self, param_name, operation_model
597 ):
598 static_ctx_params = self._get_static_context_params(operation_model)
599 return static_ctx_params.get(param_name)
600
601 def _resolve_param_as_dynamic_context_param(
602 self, param_name, operation_model, call_args
603 ):
604 dynamic_ctx_params = self._get_dynamic_context_params(operation_model)
605 if param_name in dynamic_ctx_params:
606 member_name = dynamic_ctx_params[param_name]
607 return call_args.get(member_name)
608
609 def _resolve_param_as_client_context_param(self, param_name):
610 client_ctx_params = self._get_client_context_params()
611 if param_name in client_ctx_params:
612 client_ctx_varname = client_ctx_params[param_name]
613 return self._client_context.get(client_ctx_varname)
614
615 def _resolve_param_as_operation_context_param(
616 self, param_name, operation_model, call_args
617 ):
618 operation_ctx_params = operation_model.operation_context_parameters
619 if param_name in operation_ctx_params:
620 path = operation_ctx_params[param_name]['path']
621 return jmespath.search(path, call_args)
622
623 def _resolve_param_as_builtin(self, builtin_name, builtins):
624 if builtin_name not in EndpointResolverBuiltins.__members__.values():
625 raise UnknownEndpointResolutionBuiltInName(name=builtin_name)
626 builtin = builtins.get(builtin_name)
627 if callable(builtin):
628 return builtin()
629 return builtin
630
631 @instance_cache
632 def _get_static_context_params(self, operation_model):
633 """Mapping of param names to static param value for an operation"""
634 return {
635 param.name: param.value
636 for param in operation_model.static_context_parameters
637 }
638
639 @instance_cache
640 def _get_dynamic_context_params(self, operation_model):
641 """Mapping of param names to member names for an operation"""
642 return {
643 param.name: param.member_name
644 for param in operation_model.context_parameters
645 }
646
647 @instance_cache
648 def _get_client_context_params(self):
649 """Mapping of param names to client configuration variable"""
650 return {
651 param.name: xform_name(param.name)
652 for param in self._service_model.client_context_parameters
653 }
654
655 def _get_customized_builtins(
656 self, operation_model, call_args, request_context
657 ):
658 service_id = self._service_model.service_id.hyphenize()
659 customized_builtins = copy.copy(self._builtins)
660 # Handlers are expected to modify the builtins dict in place.
661 self._event_emitter.emit(
662 f'before-endpoint-resolution.{service_id}',
663 builtins=customized_builtins,
664 model=operation_model,
665 params=call_args,
666 context=request_context,
667 )
668 return customized_builtins
669
670 def auth_schemes_to_signing_ctx(self, auth_schemes):
671 """Convert an Endpoint's authSchemes property to a signing_context dict
672
673 :type auth_schemes: list
674 :param auth_schemes: A list of dictionaries taken from the
675 ``authSchemes`` property of an Endpoint object returned by
676 ``EndpointProvider``.
677
678 :rtype: str, dict
679 :return: Tuple of auth type string (to be used in
680 ``request_context['auth_type']``) and signing context dict (for use
681 in ``request_context['signing']``).
682 """
683 if not isinstance(auth_schemes, list) or len(auth_schemes) == 0:
684 raise TypeError("auth_schemes must be a non-empty list.")
685
686 LOG.debug(
687 'Selecting from endpoint provider\'s list of auth schemes: %s. '
688 'User selected auth scheme is: "%s"',
689 ', '.join([f'"{s.get("name")}"' for s in auth_schemes]),
690 self._requested_auth_scheme,
691 )
692
693 if self._requested_auth_scheme == UNSIGNED:
694 return 'none', {}
695
696 auth_schemes = [
697 {**scheme, 'name': self._strip_sig_prefix(scheme['name'])}
698 for scheme in auth_schemes
699 ]
700 if self._requested_auth_scheme is not None:
701 try:
702 # Use the first scheme that matches the requested scheme,
703 # after accounting for naming differences between botocore and
704 # endpoint rulesets. Keep the requested name.
705 name, scheme = next(
706 (self._requested_auth_scheme, s)
707 for s in auth_schemes
708 if self._does_botocore_authname_match_ruleset_authname(
709 self._requested_auth_scheme, s['name']
710 )
711 )
712 except StopIteration:
713 # For legacy signers, no match will be found. Do not raise an
714 # exception, instead default to the logic in botocore
715 # customizations.
716 return None, {}
717 else:
718 try:
719 name, scheme = next(
720 (s['name'], s)
721 for s in auth_schemes
722 if s['name'] in AUTH_TYPE_MAPS
723 )
724 except StopIteration:
725 # If no auth scheme was specifically requested and an
726 # authSchemes list is present in the Endpoint object but none
727 # of the entries are supported, raise an exception.
728 fixable_with_crt = False
729 auth_type_options = [s['name'] for s in auth_schemes]
730 if not HAS_CRT:
731 fixable_with_crt = any(
732 scheme in CRT_SUPPORTED_AUTH_TYPES
733 for scheme in auth_type_options
734 )
735
736 if fixable_with_crt:
737 raise MissingDependencyException(
738 msg='This operation requires an additional dependency.'
739 ' Use pip install botocore[crt] before proceeding.'
740 )
741 else:
742 raise UnknownSignatureVersionError(
743 signature_version=', '.join(auth_type_options)
744 )
745
746 signing_context = {}
747 if 'signingRegion' in scheme:
748 signing_context['region'] = scheme['signingRegion']
749 elif 'signingRegionSet' in scheme:
750 if len(scheme['signingRegionSet']) > 0:
751 signing_context['region'] = ','.join(
752 scheme['signingRegionSet']
753 )
754 if 'signingName' in scheme:
755 signing_context.update(signing_name=scheme['signingName'])
756 if 'disableDoubleEncoding' in scheme:
757 signing_context['disableDoubleEncoding'] = ensure_boolean(
758 scheme['disableDoubleEncoding']
759 )
760
761 LOG.debug(
762 'Selected auth type "%s" as "%s" with signing context params: %s',
763 scheme['name'], # original name without "sig"
764 name, # chosen name can differ when `signature_version` is set
765 signing_context,
766 )
767 return name, signing_context
768
769 def _strip_sig_prefix(self, auth_name):
770 """Normalize auth type names by removing any "sig" prefix"""
771 return auth_name[3:] if auth_name.startswith('sig') else auth_name
772
773 def _does_botocore_authname_match_ruleset_authname(self, botoname, rsname):
774 """
775 Whether a valid string provided as signature_version parameter for
776 client construction refers to the same auth methods as a string
777 returned by the endpoint ruleset provider. This accounts for:
778
779 * The ruleset prefixes auth names with "sig"
780 * The s3 and s3control rulesets don't distinguish between v4[a] and
781 s3v4[a] signers
782 * The v2, v3, and HMAC v1 based signers (s3, s3-*) are botocore legacy
783 features and do not exist in the rulesets
784 * Only characters up to the first dash are considered
785
786 Example matches:
787 * v4, sigv4
788 * v4, v4
789 * s3v4, sigv4
790 * s3v7, sigv7 (hypothetical example)
791 * s3v4a, sigv4a
792 * s3v4-query, sigv4
793
794 Example mismatches:
795 * v4a, sigv4
796 * s3, sigv4
797 * s3-presign-post, sigv4
798 """
799 rsname = self._strip_sig_prefix(rsname)
800 botoname = botoname.split('-')[0]
801 if botoname != 's3' and botoname.startswith('s3'):
802 botoname = botoname[2:]
803 return rsname == botoname
804
805 def ruleset_error_to_botocore_exception(self, ruleset_exception, params):
806 """Attempts to translate ruleset errors to pre-existing botocore
807 exception types by string matching exception strings.
808 """
809 msg = ruleset_exception.kwargs.get('msg')
810 if msg is None:
811 return
812
813 if msg.startswith('Invalid region in ARN: '):
814 # Example message:
815 # "Invalid region in ARN: `us-we$t-2` (invalid DNS name)"
816 try:
817 label = msg.split('`')[1]
818 except IndexError:
819 label = msg
820 return InvalidHostLabelError(label=label)
821
822 service_name = self._service_model.service_name
823 if service_name == 's3':
824 if (
825 msg == 'S3 Object Lambda does not support S3 Accelerate'
826 or msg == 'Accelerate cannot be used with FIPS'
827 ):
828 return UnsupportedS3ConfigurationError(msg=msg)
829 if (
830 msg.startswith('S3 Outposts does not support')
831 or msg.startswith('S3 MRAP does not support')
832 or msg.startswith('S3 Object Lambda does not support')
833 or msg.startswith('Access Points do not support')
834 or msg.startswith('Invalid configuration:')
835 or msg.startswith('Client was configured for partition')
836 ):
837 return UnsupportedS3AccesspointConfigurationError(msg=msg)
838 if msg.lower().startswith('invalid arn:'):
839 return ParamValidationError(report=msg)
840 if service_name == 's3control':
841 if msg.startswith('Invalid ARN:'):
842 arn = params.get('Bucket')
843 return UnsupportedS3ControlArnError(arn=arn, msg=msg)
844 if msg.startswith('Invalid configuration:') or msg.startswith(
845 'Client was configured for partition'
846 ):
847 return UnsupportedS3ControlConfigurationError(msg=msg)
848 if msg == "AccountId is required but not set":
849 return ParamValidationError(report=msg)
850 if service_name == 'events':
851 if msg.startswith(
852 'Invalid Configuration: FIPS is not supported with '
853 'EventBridge multi-region endpoints.'
854 ):
855 return InvalidEndpointConfigurationError(msg=msg)
856 if msg == 'EndpointId must be a valid host label.':
857 return InvalidEndpointConfigurationError(msg=msg)
858 return None
859
860 def _register_endpoint_feature_ids(self, param_name, param_val):
861 if param_name == 'AccountIdEndpointMode':
862 register_feature_id(f'ACCOUNT_ID_MODE_{param_val.upper()}')
863 elif param_name == 'AccountId':
864 register_feature_id('RESOLVED_ACCOUNT_ID')