Skip to content

Commit

Permalink
pass account_id_endpoint_mode as an input param to resolver and sim…
Browse files Browse the repository at this point in the history
…plify resolution branching logic
  • Loading branch information
davidlm committed Oct 18, 2023
1 parent 75f46cf commit c3a4168
Show file tree
Hide file tree
Showing 3 changed files with 64 additions and 69 deletions.
3 changes: 3 additions & 0 deletions botocore/args.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ def get_client_args(
endpoint_bridge,
event_emitter,
credentials,
new_config.account_id_endpoint_mode,
)

# Copy the session's user agent factory and adds client configuration.
Expand Down Expand Up @@ -630,6 +631,7 @@ def _build_endpoint_resolver(
endpoint_bridge,
event_emitter,
credentials,
account_id_endpoint_mode,
):
if endpoints_ruleset_data is None:
return None
Expand Down Expand Up @@ -678,6 +680,7 @@ def _build_endpoint_resolver(
use_ssl=is_secure,
requested_auth_scheme=sig_version,
credentials=credentials,
account_id_endpoint_mode=account_id_endpoint_mode,
)

def compute_endpoint_resolver_builtin_defaults(
Expand Down
60 changes: 27 additions & 33 deletions botocore/regions.py
Original file line number Diff line number Diff line change
Expand Up @@ -459,9 +459,10 @@ class EndpointResolverBuiltins(str, Enum):
class EndpointRulesetResolver:
"""Resolves endpoints using a service's endpoint ruleset"""

DEFAULT_ACCOUNT_ID_ENDPOINT_MODE = 'preferred'
# Allowed values for the ``account_id_endpoint_mode`` config field.
VALID_ACCOUNT_ID_ENDPOINT_MODES = (
'preferred',
DEFAULT_ACCOUNT_ID_ENDPOINT_MODE,
'disabled',
'required',
)
Expand All @@ -477,6 +478,7 @@ def __init__(
use_ssl=True,
requested_auth_scheme=None,
credentials=None,
account_id_endpoint_mode=None,
):
self._provider = EndpointProvider(
ruleset_data=endpoint_ruleset_data,
Expand All @@ -491,6 +493,9 @@ def __init__(
self._requested_auth_scheme = requested_auth_scheme
self._instance_cache = {}
self._credentials = credentials
if account_id_endpoint_mode is None:
account_id_endpoint_mode = self.DEFAULT_ACCOUNT_ID_ENDPOINT_MODE
self._account_id_endpoint_mode = account_id_endpoint_mode

def construct_endpoint(
self,
Expand Down Expand Up @@ -559,7 +564,7 @@ def _get_provider_params(
customized_builtins = self._get_customized_builtins(
operation_model, call_args, request_context
)
self._resolve_account_id_builtin(request_context, customized_builtins)
self._resolve_account_id_builtin(customized_builtins)
for param_name, param_def in self._param_definitions.items():
param_val = self._resolve_param_from_context(
param_name=param_name,
Expand All @@ -576,54 +581,43 @@ def _get_provider_params(

return provider_params

def _resolve_account_id_builtin(self, request_context, builtins):
"""Resolve the ``AWS::Auth::AccountId`` builtin if configured in the
ruleset, account ID based routing is enabled and it has not already
been resolved in a custom handler.
"""
def _resolve_account_id_builtin(self, builtins):
"""Resolve the ``AWS::Auth::AccountId`` builtin."""
# do this check separately so mode is only validated if it's being used
if 'AccountId' not in self._param_definitions:
return

acct_id_ep_mode = self._resolve_account_id_endpoint_mode(
request_context
)
self._validate_account_id_endpoint_mode()
acct_id_builtin_key = EndpointResolverBuiltins.AWS_ACCOUNT_ID
if acct_id_ep_mode == 'disabled':
if not self._should_resolve_account_id_builtin():
# Unset the account ID if endpoint mode is disabled.
builtins[acct_id_builtin_key] = None
return

elif builtins.get(acct_id_builtin_key) is None:
self._do_resolve_account_id_builtin(acct_id_ep_mode, builtins)

def _resolve_account_id_endpoint_mode(self, request_context):
"""Resolve the account ID endpoint mode for the request."""
not_presign = not request_context.get('is_presign_request', False)
should_sign = self._requested_auth_scheme != UNSIGNED
creds_available = self._credentials is not None

if not_presign and should_sign and creds_available:
ep_mode = request_context['client_config'].account_id_endpoint_mode
return self._validate_account_id_endpoint_mode(ep_mode)
return 'disabled'
if builtins.get(acct_id_builtin_key) is None:
self._do_resolve_account_id_builtin(builtins)

def _validate_account_id_endpoint_mode(self, account_id_endpoint_mode):
def _validate_account_id_endpoint_mode(self):
valid_modes = self.VALID_ACCOUNT_ID_ENDPOINT_MODES
if account_id_endpoint_mode not in valid_modes:
if self._account_id_endpoint_mode not in valid_modes:
error_msg = (
f'Invalid value "{account_id_endpoint_mode}" for '
'account_id_endpoint_mode. Valid values are: '
f'Invalid value "{self._account_id_endpoint_mode}" '
'for account_id_endpoint_mode. Valid values are: '
f'{", ".join(valid_modes)}.'
)
raise InvalidConfigError(error_msg=error_msg)
return account_id_endpoint_mode

def _do_resolve_account_id_builtin(
self, account_id_endpoint_mode, builtins
):
def _should_resolve_account_id_builtin(self):
signing_enabled = self._requested_auth_scheme != UNSIGNED
creds_available = self._credentials is not None
mode_enabled = self._account_id_endpoint_mode != 'disabled'
return signing_enabled and creds_available and mode_enabled

def _do_resolve_account_id_builtin(self, builtins):
frozen_creds = self._credentials.get_frozen_credentials()
account_id = frozen_creds.account_id
if account_id is None:
acct_id_ep_mode = account_id_endpoint_mode
acct_id_ep_mode = self._account_id_endpoint_mode
msg = f'"account_id_endpoint_mode" is set to {acct_id_ep_mode}'
if acct_id_ep_mode == 'preferred':
LOG.debug('%s, but account ID was not found.', msg)
Expand Down
70 changes: 34 additions & 36 deletions tests/unit/test_endpoint_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
import pytest

from botocore import UNSIGNED
from botocore.config import Config
from botocore.credentials import Credentials
from botocore.endpoint_provider import (
EndpointProvider,
Expand Down Expand Up @@ -553,9 +552,16 @@ def operation_model_empty_context_params():
token="token",
account_id="1234567890",
)
REQUIRED = 'required'
PREFERRED = 'preferred'
DISABLED = 'disabled'
URL_NO_ACCOUNT_ID = "https://amazonaws.com"
URL_WITH_ACCOUNT_ID = "https://1234567890.amazonaws.com"


def create_ruleset_resolver(ruleset, bulitins, credentials, auth_scheme):
def create_ruleset_resolver(
ruleset, bulitins, credentials, auth_scheme, account_id_endpoint_mode
):
service_model = Mock()
service_model.client_context_parameters = []
return EndpointRulesetResolver(
Expand All @@ -567,84 +573,71 @@ def create_ruleset_resolver(ruleset, bulitins, credentials, auth_scheme):
event_emitter=Mock(),
credentials=credentials,
requested_auth_scheme=auth_scheme,
account_id_endpoint_mode=account_id_endpoint_mode,
)


ACCT_ID_REQUIRED_CONTEXT = {
"client_config": Config(account_id_endpoint_mode="required")
}
ACCT_ID_PREFERRED_CONTEXT = {
"client_config": Config(account_id_endpoint_mode="preferred")
}
ACCT_ID_DISABLED_CONTEXT = {
"client_config": Config(account_id_endpoint_mode="disabled")
}

URL_NO_ACCOUNT_ID = "https://amazonaws.com"
URL_WITH_ACCOUNT_ID = "https://1234567890.amazonaws.com"


@pytest.mark.parametrize(
"builtins, credentials, auth_scheme, request_context, expected_url",
"builtins, credentials, auth_scheme, account_id_endpoint_mode, expected_url",
[
(
BUILTINS_WITH_UNRESOLVED_ACCOUNT_ID,
CREDENTIALS,
None,
ACCT_ID_REQUIRED_CONTEXT,
REQUIRED,
URL_WITH_ACCOUNT_ID,
),
# custom account ID takes precedence over credentials
(
BUILTINS_WITH_RESOLVED_ACCOUNT_ID,
CREDENTIALS,
None,
ACCT_ID_REQUIRED_CONTEXT,
REQUIRED,
"https://0987654321.amazonaws.com",
),
(
BUILTINS_WITH_UNRESOLVED_ACCOUNT_ID,
CREDENTIALS,
None,
ACCT_ID_DISABLED_CONTEXT,
DISABLED,
URL_NO_ACCOUNT_ID,
),
# custom account ID removed if account ID mode is disabled
(
BUILTINS_WITH_RESOLVED_ACCOUNT_ID,
CREDENTIALS,
None,
ACCT_ID_DISABLED_CONTEXT,
DISABLED,
URL_NO_ACCOUNT_ID,
),
(
BUILTINS_WITH_UNRESOLVED_ACCOUNT_ID,
BUILTINS_WITH_RESOLVED_ACCOUNT_ID,
CREDENTIALS,
UNSIGNED,
ACCT_ID_REQUIRED_CONTEXT,
REQUIRED,
URL_NO_ACCOUNT_ID,
),
(
BUILTINS_WITH_UNRESOLVED_ACCOUNT_ID,
CREDENTIALS,
None,
{**ACCT_ID_REQUIRED_CONTEXT, "is_presign_request": True},
UNSIGNED,
REQUIRED,
URL_NO_ACCOUNT_ID,
),
# no credentials
(
BUILTINS_WITH_UNRESOLVED_ACCOUNT_ID,
None,
None,
ACCT_ID_PREFERRED_CONTEXT,
PREFERRED,
URL_NO_ACCOUNT_ID,
),
# no account ID in credentials
(
BUILTINS_WITH_UNRESOLVED_ACCOUNT_ID,
Credentials(access_key="foo", secret_key="bar", token="baz"),
None,
ACCT_ID_PREFERRED_CONTEXT,
PREFERRED,
URL_NO_ACCOUNT_ID,
),
],
Expand All @@ -655,39 +648,43 @@ def test_account_id_builtin(
builtins,
credentials,
auth_scheme,
request_context,
account_id_endpoint_mode,
expected_url,
):
resolver = create_ruleset_resolver(
account_id_ruleset, builtins, credentials, auth_scheme
account_id_ruleset,
builtins,
credentials,
auth_scheme,
account_id_endpoint_mode,
)
endpoint = resolver.construct_endpoint(
operation_model=operation_model_empty_context_params,
request_context=request_context,
request_context={},
call_args={},
)
assert endpoint.url == expected_url


@pytest.mark.parametrize(
"credentials, request_context, expected_error",
"credentials, account_id_endpoint_mode, expected_error",
[
# invalid value for mode
(
CREDENTIALS,
{"client_config": Config(account_id_endpoint_mode="foo")},
"foo",
InvalidConfigError,
),
# mode is case sensitive
(
CREDENTIALS,
{"client_config": Config(account_id_endpoint_mode="PREFERRED")},
"PREFERRED",
InvalidConfigError,
),
# no account ID found but required
(
Credentials(access_key="foo", secret_key="bar", token="baz"),
ACCT_ID_REQUIRED_CONTEXT,
REQUIRED,
AccountIdNotFound,
),
],
Expand All @@ -696,18 +693,19 @@ def test_account_id_error_cases(
operation_model_empty_context_params,
account_id_ruleset,
credentials,
request_context,
account_id_endpoint_mode,
expected_error,
):
resolver = create_ruleset_resolver(
account_id_ruleset,
BUILTINS_WITH_UNRESOLVED_ACCOUNT_ID,
credentials,
None,
account_id_endpoint_mode,
)
with pytest.raises(expected_error):
resolver.construct_endpoint(
operation_model=operation_model_empty_context_params,
request_context=request_context,
request_context={},
call_args={},
)

0 comments on commit c3a4168

Please sign in to comment.