Skip to content

Commit ac979ac

Browse files
authored
[v2] Add support for configured manual auth scheme and bearer token auth (#9620)
1 parent bf26fb5 commit ac979ac

File tree

20 files changed

+919
-19
lines changed

20 files changed

+919
-19
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"type": "feature",
3+
"category": "``bedrock``",
4+
"description": "Add support for retrieving a Bearer token from environment variables to enable bearer authentication with Bedrock services."
5+
}

awscli/botocore/args.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,7 @@ def compute_client_args(
278278
client_config.response_checksum_validation
279279
),
280280
account_id_endpoint_mode=client_config.account_id_endpoint_mode,
281+
auth_scheme_preference=client_config.auth_scheme_preference,
281282
)
282283
self._compute_retry_config(config_kwargs)
283284
self._compute_request_compression_config(config_kwargs)
@@ -286,6 +287,10 @@ def compute_client_args(
286287
self._compute_checksum_config(config_kwargs)
287288
self._compute_inject_host_prefix(client_config, config_kwargs)
288289
self._compute_account_id_endpoint_mode_config(config_kwargs)
290+
self._compute_auth_scheme_preference_config(
291+
client_config, config_kwargs
292+
)
293+
self._compute_signature_version_config(client_config, config_kwargs)
289294
s3_config = self.compute_s3_config(client_config)
290295

291296
is_s3_service = self._is_s3_service(service_name)
@@ -782,3 +787,55 @@ def _compute_account_id_endpoint_mode_config(self, config_kwargs):
782787
)
783788

784789
config_kwargs[config_key] = account_id_endpoint_mode
790+
791+
def _compute_auth_scheme_preference_config(
792+
self, client_config, config_kwargs
793+
):
794+
config_key = 'auth_scheme_preference'
795+
set_in_config_object = False
796+
797+
if client_config and client_config.auth_scheme_preference:
798+
value = client_config.auth_scheme_preference
799+
set_in_config_object = True
800+
else:
801+
value = self._config_store.get_config_variable(config_key)
802+
803+
if value is None:
804+
config_kwargs[config_key] = None
805+
return
806+
807+
if not isinstance(value, str):
808+
raise botocore.exceptions.InvalidConfigError(
809+
error_msg=(
810+
f"{config_key} must be a comma-delimited string. "
811+
f"Received {type(value)} instead: {value}."
812+
)
813+
)
814+
815+
value = ','.join(
816+
item.replace(' ', '').replace('\t', '')
817+
for item in value.split(',')
818+
if item.strip()
819+
)
820+
821+
if set_in_config_object:
822+
value = ClientConfigString(value)
823+
824+
config_kwargs[config_key] = value
825+
826+
def _compute_signature_version_config(self, client_config, config_kwargs):
827+
if client_config and client_config.signature_version:
828+
value = client_config.signature_version
829+
if isinstance(value, str):
830+
config_kwargs['signature_version'] = ClientConfigString(value)
831+
832+
833+
class ConfigObjectWrapper:
834+
"""Base class to mark values set via in-code Config object."""
835+
836+
pass
837+
838+
839+
class ClientConfigString(str, ConfigObjectWrapper):
840+
def __new__(cls, value=None):
841+
return super().__new__(cls, value)

awscli/botocore/auth.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -895,6 +895,38 @@ def resolve_auth_type(auth_trait):
895895
raise UnsupportedSignatureVersionError(signature_version=auth_trait)
896896

897897

898+
def resolve_auth_scheme_preference(preference_list, auth_options):
899+
service_supported = [scheme.split('#')[-1] for scheme in auth_options]
900+
901+
unsupported = [
902+
scheme
903+
for scheme in preference_list
904+
if scheme not in AUTH_PREF_TO_SIGNATURE_VERSION
905+
]
906+
if unsupported:
907+
logger.debug(
908+
f"Unsupported auth schemes in preference list: {', '.join(unsupported)}"
909+
)
910+
911+
combined = preference_list + service_supported
912+
prioritized_schemes = [
913+
scheme
914+
for scheme in dict.fromkeys(combined)
915+
if scheme in service_supported
916+
]
917+
918+
for scheme in prioritized_schemes:
919+
if scheme == 'noAuth':
920+
return AUTH_PREF_TO_SIGNATURE_VERSION[scheme]
921+
sig_version = AUTH_PREF_TO_SIGNATURE_VERSION.get(scheme)
922+
if sig_version in AUTH_TYPE_MAPS:
923+
return sig_version
924+
925+
raise UnsupportedSignatureVersionError(
926+
signature_version=', '.join(sorted(service_supported))
927+
)
928+
929+
898930
# Defined at the bottom instead of the top of the module because the Auth
899931
# classes weren't defined yet.
900932
AUTH_TYPE_MAPS = {
@@ -921,3 +953,13 @@ def resolve_auth_type(auth_trait):
921953
'smithy.api#httpBearerAuth': 'bearer',
922954
'smithy.api#noAuth': 'none',
923955
}
956+
957+
958+
# Mapping used specifically for resolving user-configured auth scheme preferences.
959+
# This is similar to AUTH_TYPE_TO_SIGNATURE_VERSION, but uses simplified keys by
960+
# stripping the auth trait prefixes ('smithy.api#httpBearerAuth' → 'httpBearerAuth').
961+
# These simplified keys match what customers are expected to provide in configuration.
962+
AUTH_PREF_TO_SIGNATURE_VERSION = {
963+
auth_scheme.split('#')[-1]: sig_version
964+
for auth_scheme, sig_version in AUTH_TYPE_TO_SIGNATURE_VERSION.items()
965+
}

awscli/botocore/client.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ def __init__(
7979
exceptions_factory=None,
8080
config_store=None,
8181
user_agent_creator=None,
82+
auth_token_resolver=None,
8283
):
8384
self._loader = loader
8485
self._endpoint_resolver = endpoint_resolver
@@ -92,6 +93,7 @@ def __init__(
9293
# future).
9394
self._config_store = config_store
9495
self._user_agent_creator = user_agent_creator
96+
self._auth_token_resolver = auth_token_resolver
9597

9698
def create_client(
9799
self,
@@ -143,6 +145,10 @@ def create_client(
143145
config_store=self._config_store,
144146
service_signature_version=service_signature_version,
145147
)
148+
if token := self._evaluate_client_specific_token(
149+
service_model.signing_name
150+
):
151+
auth_token = token
146152
client_args = self._get_client_args(
147153
service_model,
148154
region_name,
@@ -446,6 +452,15 @@ def _api_call(self, *args, **kwargs):
446452
_api_call.__doc__ = docstring
447453
return _api_call
448454

455+
def _evaluate_client_specific_token(self, signing_name):
456+
# Resolves an auth_token for the given signing_name.
457+
# Returns None if no resolver is set or if resolution fails.
458+
resolver = self._auth_token_resolver
459+
if not resolver or not signing_name:
460+
return None
461+
462+
return resolver(signing_name=signing_name)
463+
449464

450465
class ClientEndpointBridge:
451466
"""Bridges endpoint data and client creation
@@ -829,6 +844,7 @@ def _make_api_call(self, operation_name, api_params):
829844
'has_streaming_input': operation_model.has_streaming_input,
830845
'auth_type': operation_model.resolved_auth_type,
831846
'unsigned_payload': operation_model.unsigned_payload,
847+
'auth_options': self._service_model.metadata.get('auth'),
832848
}
833849

834850
api_params = self._emit_api_params(

awscli/botocore/config.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,12 @@ class Config:
240240
241241
If a value is not provided, the client will default to ``preferred``.
242242
243+
Defaults to None.
244+
245+
:type auth_scheme_preference: str
246+
:param auth_scheme_preference: A comma-delimited string of case-sensitive
247+
auth scheme names used to determine the client's auth scheme preference.
248+
243249
Defaults to None.
244250
"""
245251

@@ -270,6 +276,7 @@ class Config:
270276
('request_checksum_calculation', None),
271277
('response_checksum_validation', None),
272278
('account_id_endpoint_mode', None),
279+
('auth_scheme_preference', None),
273280
]
274281
)
275282

awscli/botocore/configprovider.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,12 @@
186186
None,
187187
utils.ensure_boolean,
188188
),
189+
'auth_scheme_preference': (
190+
'auth_scheme_preference',
191+
'AWS_AUTH_SCHEME_PREFERENCE',
192+
None,
193+
None,
194+
),
189195
}
190196
# A mapping for the s3 specific configuration vars. These are the configuration
191197
# vars that typically go in the s3 section of the config file. This mapping

awscli/botocore/handlers.py

Lines changed: 107 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import botocore
2828
import botocore.auth
2929
from botocore import UNSIGNED, utils
30+
from botocore.args import ClientConfigString
3031
from botocore.compat import (
3132
MD5_AVAILABLE, # noqa
3233
ETree,
@@ -58,10 +59,12 @@
5859
add_generate_presigned_post,
5960
add_generate_presigned_url,
6061
)
62+
from botocore.useragent import register_feature_id
6163
from botocore.utils import (
6264
SAFE_CHARS,
6365
SERVICE_NAME_ALIASES,
6466
ArnParser,
67+
get_token_from_environment,
6568
hyphenize_service_id, # noqa
6669
is_global_accesspoint, # noqa
6770
percent_encode,
@@ -985,12 +988,14 @@ def remove_qbusiness_chat(class_attributes, **kwargs):
985988
del class_attributes['chat']
986989

987990

988-
def remove_bedrock_runtime_invoke_model_with_bidirectional_stream(class_attributes, **kwargs):
991+
def remove_bedrock_runtime_invoke_model_with_bidirectional_stream(
992+
class_attributes, **kwargs
993+
):
989994
"""Operation requires h2 which is currently unsupported in Python"""
990995
if 'invoke_model_with_bidirectional_stream' in class_attributes:
991996
del class_attributes['invoke_model_with_bidirectional_stream']
992-
993-
997+
998+
994999
def remove_bucket_from_url_paths_from_model(params, model, context, **kwargs):
9951000
"""Strips leading `{Bucket}/` from any operations that have it.
9961001
@@ -1201,6 +1206,100 @@ def _handle_request_validation_mode_member(params, model, **kwargs):
12011206
params.setdefault(mode_member, "ENABLED")
12021207

12031208

1209+
def _set_auth_scheme_preference_signer(context, signing_name, **kwargs):
1210+
"""
1211+
Determines the appropriate signer to use based on the client configuration,
1212+
authentication scheme preferences, and the availability of a bearer token.
1213+
"""
1214+
client_config = context.get('client_config')
1215+
if client_config is None:
1216+
return
1217+
1218+
signature_version = client_config.signature_version
1219+
auth_scheme_preference = client_config.auth_scheme_preference
1220+
auth_options = context.get('auth_options')
1221+
1222+
signature_version_set_in_code = (
1223+
isinstance(signature_version, ClientConfigString)
1224+
or signature_version is botocore.UNSIGNED
1225+
)
1226+
auth_preference_set_in_code = isinstance(
1227+
auth_scheme_preference, ClientConfigString
1228+
)
1229+
has_in_code_configuration = (
1230+
signature_version_set_in_code or auth_preference_set_in_code
1231+
)
1232+
1233+
resolved_signature_version = signature_version
1234+
1235+
# If signature version was not set in code, but an auth scheme preference
1236+
# is available, resolve it based on the preferred schemes and supported auth
1237+
# options for this service.
1238+
if (
1239+
not signature_version_set_in_code
1240+
and auth_scheme_preference
1241+
and auth_options
1242+
):
1243+
preferred_schemes = auth_scheme_preference.split(',')
1244+
resolved = botocore.auth.resolve_auth_scheme_preference(
1245+
preferred_schemes, auth_options
1246+
)
1247+
resolved_signature_version = (
1248+
botocore.UNSIGNED if resolved == 'none' else resolved
1249+
)
1250+
1251+
# Prefer 'bearer' signature version if a bearer token is available, and it
1252+
# is allowed for this service. This can override earlier resolution if the
1253+
# config object didn't explicitly set a signature version.
1254+
if _should_prefer_bearer_auth(
1255+
has_in_code_configuration,
1256+
signing_name,
1257+
resolved_signature_version,
1258+
auth_options,
1259+
):
1260+
register_feature_id('BEARER_SERVICE_ENV_VARS')
1261+
resolved_signature_version = 'bearer'
1262+
1263+
if resolved_signature_version == signature_version:
1264+
return None
1265+
return resolved_signature_version
1266+
1267+
1268+
def _should_prefer_bearer_auth(
1269+
has_in_code_configuration,
1270+
signing_name,
1271+
resolved_signature_version,
1272+
auth_options,
1273+
):
1274+
if signing_name not in get_bearer_auth_supported_services():
1275+
return False
1276+
1277+
if not auth_options or 'smithy.api#httpBearerAuth' not in auth_options:
1278+
return False
1279+
1280+
has_token = get_token_from_environment(signing_name) is not None
1281+
1282+
# Prefer 'bearer' if a bearer token is available, and either:
1283+
# Bearer was already resolved, or
1284+
# No auth-related values were explicitly set in code
1285+
return has_token and (
1286+
resolved_signature_version == 'bearer' or not has_in_code_configuration
1287+
)
1288+
1289+
1290+
def get_bearer_auth_supported_services():
1291+
"""
1292+
Returns a set of services that support bearer token authentication.
1293+
These values correspond to the service's `signingName` property as defined
1294+
in model.py, falling back to `endpointPrefix` if `signingName` is not set.
1295+
1296+
Warning: This is a private interface and is subject to abrupt breaking changes,
1297+
including removal, in any botocore release. It is not intended for external use,
1298+
and its usage outside of botocore is not advised or supported.
1299+
"""
1300+
return {'bedrock'}
1301+
1302+
12041303
def _set_extra_headers_for_unsigned_request(
12051304
request, signature_version, **kwargs
12061305
):
@@ -1242,7 +1341,10 @@ def _set_extra_headers_for_unsigned_request(
12421341
),
12431342
('creating-client-class.lex-runtime-v2', remove_lex_v2_start_conversation),
12441343
('creating-client-class.qbusiness', remove_qbusiness_chat),
1245-
('creating-client-class.bedrock-runtime', remove_bedrock_runtime_invoke_model_with_bidirectional_stream),
1344+
(
1345+
'creating-client-class.bedrock-runtime',
1346+
remove_bedrock_runtime_invoke_model_with_bidirectional_stream,
1347+
),
12461348
('after-call.iam', json_decode_policies),
12471349
('after-call.ec2.GetConsoleOutput', decode_console_output),
12481350
('after-call.cloudformation.GetTemplate', json_decode_template_body),
@@ -1299,6 +1401,7 @@ def _set_extra_headers_for_unsigned_request(
12991401
('choose-signer.sts.AssumeRoleWithSAML', disable_signing),
13001402
('choose-signer.sts.AssumeRoleWithWebIdentity', disable_signing),
13011403
('choose-signer', set_operation_specific_signer),
1404+
('choose-signer', _set_auth_scheme_preference_signer),
13021405
('before-parameter-build.s3.HeadObject', sse_md5),
13031406
('before-parameter-build.s3.GetObject', sse_md5),
13041407
('before-parameter-build.s3.PutObject', sse_md5),

0 commit comments

Comments
 (0)