Skip to content

Commit e4f4e92

Browse files
authored
Add support for account-based endpoints (#4097)
## Motivation and Context #3776 ## Description This PR adds support for [account-based endpoints](https://docs.aws.amazon.com/sdkref/latest/guide/feature-account-endpoints.html) in AWS SDKs. The code changes in the PR consolidate the previous sub-PRs that have been reviewed already: - #4047 - #4057 - #4087 - #4091 - #4094 ## Testing - All tests added in the sub-PRs above - Added account ID-related integration tests for DynamoDB ([commit](60edd12)) ## Checklist - [x] For changes to the smithy-rs codegen or runtime crates, I have created a changelog entry Markdown file in the `.changelog` directory, specifying "client," "server," or both in the `applies_to` key. - [x] For changes to the AWS SDK, generated SDK code, or SDK runtime crates, I have created a changelog entry Markdown file in the `.changelog` directory, specifying "aws-sdk-rust" in the `applies_to` key. ---- _By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice._
1 parent aa834af commit e4f4e92

File tree

111 files changed

+4771
-808
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

111 files changed

+4771
-808
lines changed

.changelog/1745264370.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
applies_to:
3+
- aws-sdk-rust
4+
authors:
5+
- ysaito1001
6+
references:
7+
- smithy-rs#3776
8+
breaking: false
9+
new_feature: true
10+
bug_fix: false
11+
---
12+
Add support for the account-based endpoints in AWS SDKs. For more details, please refer to the [AWS SDKs and Tools Reference Guide on Account-Based Endpoints](https://docs.aws.amazon.com/sdkref/latest/guide/feature-account-endpoints.html).

.changelog/1745265476.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
applies_to:
3+
- client
4+
authors:
5+
- ysaito1001
6+
references:
7+
- smithy-rs#3776
8+
breaking: true
9+
new_feature: false
10+
bug_fix: false
11+
---
12+
[AuthSchemeId](https://docs.rs/aws-smithy-runtime-api/1.7.4/aws_smithy_runtime_api/client/auth/struct.AuthSchemeId.html) no longer implements the `Copy` trait. This type has primarily been used by the Smithy code generator, so this change is not expected to affect users of SDKs.

aws/rust-runtime/Cargo.lock

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

aws/rust-runtime/aws-config/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "aws-config"
3-
version = "1.6.1"
3+
version = "1.6.2"
44
authors = [
55
"AWS Rust SDK Team <[email protected]>",
66
"Russell Cohen <[email protected]>",

aws/rust-runtime/aws-config/src/credential_process.rs

Lines changed: 87 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@
99
1010
use crate::json_credentials::{json_parse_loop, InvalidJsonCredentials};
1111
use crate::sensitive_command::CommandWithSensitiveArgs;
12+
use aws_credential_types::attributes::AccountId;
1213
use aws_credential_types::provider::{self, error::CredentialsError, future, ProvideCredentials};
1314
use aws_credential_types::Credentials;
1415
use aws_smithy_json::deserialize::Token;
16+
use std::borrow::Cow;
1517
use std::process::Command;
1618
use std::time::SystemTime;
1719
use time::format_description::well_known::Rfc3339;
@@ -53,6 +55,7 @@ use time::OffsetDateTime;
5355
#[derive(Debug)]
5456
pub struct CredentialProcessProvider {
5557
command: CommandWithSensitiveArgs<String>,
58+
profile_account_id: Option<AccountId>,
5659
}
5760

5861
impl ProvideCredentials for CredentialProcessProvider {
@@ -69,13 +72,12 @@ impl CredentialProcessProvider {
6972
pub fn new(command: String) -> Self {
7073
Self {
7174
command: CommandWithSensitiveArgs::new(command),
75+
profile_account_id: None,
7276
}
7377
}
7478

75-
pub(crate) fn from_command(command: &CommandWithSensitiveArgs<&str>) -> Self {
76-
Self {
77-
command: command.to_owned_string(),
78-
}
79+
pub(crate) fn builder() -> Builder {
80+
Builder::default()
7981
}
8082

8183
async fn credentials(&self) -> provider::Result {
@@ -120,12 +122,44 @@ impl CredentialProcessProvider {
120122
))
121123
})?;
122124

123-
parse_credential_process_json_credentials(output).map_err(|invalid| {
124-
CredentialsError::provider_error(format!(
125+
parse_credential_process_json_credentials(output, self.profile_account_id.as_ref()).map_err(
126+
|invalid| {
127+
CredentialsError::provider_error(format!(
125128
"Error retrieving credentials from external process, could not parse response: {}",
126129
invalid
127130
))
128-
})
131+
},
132+
)
133+
}
134+
}
135+
136+
#[derive(Debug, Default)]
137+
pub(crate) struct Builder {
138+
command: Option<CommandWithSensitiveArgs<String>>,
139+
profile_account_id: Option<AccountId>,
140+
}
141+
142+
impl Builder {
143+
pub(crate) fn command(mut self, command: CommandWithSensitiveArgs<String>) -> Self {
144+
self.command = Some(command);
145+
self
146+
}
147+
148+
#[allow(dead_code)] // only used in unit tests
149+
pub(crate) fn account_id(mut self, account_id: impl Into<AccountId>) -> Self {
150+
self.set_account_id(Some(account_id.into()));
151+
self
152+
}
153+
154+
pub(crate) fn set_account_id(&mut self, account_id: Option<AccountId>) {
155+
self.profile_account_id = account_id;
156+
}
157+
158+
pub(crate) fn build(self) -> CredentialProcessProvider {
159+
CredentialProcessProvider {
160+
command: self.command.expect("should be set"),
161+
profile_account_id: self.profile_account_id,
162+
}
129163
}
130164
}
131165

@@ -134,22 +168,29 @@ impl CredentialProcessProvider {
134168
/// Returns an error if the response cannot be successfully parsed or is missing keys.
135169
///
136170
/// Keys are case insensitive.
171+
/// The function optionally takes `profile_account_id` that originates from the profile section.
172+
/// If process execution result does not contain an account ID, the function uses it as a fallback.
137173
pub(crate) fn parse_credential_process_json_credentials(
138174
credentials_response: &str,
175+
profile_account_id: Option<&AccountId>,
139176
) -> Result<Credentials, InvalidJsonCredentials> {
140177
let mut version = None;
141178
let mut access_key_id = None;
142179
let mut secret_access_key = None;
143180
let mut session_token = None;
144181
let mut expiration = None;
182+
let mut account_id = profile_account_id
183+
.as_ref()
184+
.map(|id| Cow::Borrowed(id.as_str()));
145185
json_parse_loop(credentials_response.as_bytes(), |key, value| {
146186
match (key, value) {
147187
/*
148188
"Version": 1,
149189
"AccessKeyId": "ASIARTESTID",
150190
"SecretAccessKey": "TESTSECRETKEY",
151191
"SessionToken": "TESTSESSIONTOKEN",
152-
"Expiration": "2022-05-02T18:36:00+00:00"
192+
"Expiration": "2022-05-02T18:36:00+00:00",
193+
"AccountId": "111122223333"
153194
*/
154195
(key, Token::ValueNumber { value, .. }) if key.eq_ignore_ascii_case("Version") => {
155196
version = Some(i32::try_from(*value).map_err(|err| {
@@ -173,6 +214,9 @@ pub(crate) fn parse_credential_process_json_credentials(
173214
(key, Token::ValueString { value, .. }) if key.eq_ignore_ascii_case("Expiration") => {
174215
expiration = Some(value.to_unescaped()?)
175216
}
217+
(key, Token::ValueString { value, .. }) if key.eq_ignore_ascii_case("AccountId") => {
218+
account_id = Some(value.to_unescaped()?)
219+
}
176220

177221
_ => {}
178222
};
@@ -197,13 +241,14 @@ pub(crate) fn parse_credential_process_json_credentials(
197241
if expiration.is_none() {
198242
tracing::debug!("no expiration provided for credentials provider credentials. these credentials will never be refreshed.")
199243
}
200-
Ok(Credentials::new(
201-
access_key_id,
202-
secret_access_key,
203-
session_token.map(|tok| tok.to_string()),
204-
expiration,
205-
"CredentialProcess",
206-
))
244+
let mut builder = Credentials::builder()
245+
.access_key_id(access_key_id)
246+
.secret_access_key(secret_access_key)
247+
.provider_name("CredentialProcess");
248+
builder.set_session_token(session_token.map(String::from));
249+
builder.set_expiry(expiration);
250+
builder.set_account_id(account_id.map(AccountId::from));
251+
Ok(builder.build())
207252
}
208253

209254
fn parse_expiration(expiration: impl AsRef<str>) -> Result<SystemTime, InvalidJsonCredentials> {
@@ -218,6 +263,7 @@ fn parse_expiration(expiration: impl AsRef<str>) -> Result<SystemTime, InvalidJs
218263
#[cfg(test)]
219264
mod test {
220265
use crate::credential_process::CredentialProcessProvider;
266+
use crate::sensitive_command::CommandWithSensitiveArgs;
221267
use aws_credential_types::provider::ProvideCredentials;
222268
use std::time::{Duration, SystemTime};
223269
use time::format_description::well_known::Rfc3339;
@@ -229,12 +275,13 @@ mod test {
229275
#[cfg_attr(windows, ignore)]
230276
async fn test_credential_process() {
231277
let provider = CredentialProcessProvider::new(String::from(
232-
r#"echo '{ "Version": 1, "AccessKeyId": "ASIARTESTID", "SecretAccessKey": "TESTSECRETKEY", "SessionToken": "TESTSESSIONTOKEN", "Expiration": "2022-05-02T18:36:00+00:00" }'"#,
278+
r#"echo '{ "Version": 1, "AccessKeyId": "ASIARTESTID", "SecretAccessKey": "TESTSECRETKEY", "SessionToken": "TESTSESSIONTOKEN", "AccountId": "123456789001", "Expiration": "2022-05-02T18:36:00+00:00" }'"#,
233279
));
234280
let creds = provider.provide_credentials().await.expect("valid creds");
235281
assert_eq!(creds.access_key_id(), "ASIARTESTID");
236282
assert_eq!(creds.secret_access_key(), "TESTSECRETKEY");
237283
assert_eq!(creds.session_token(), Some("TESTSESSIONTOKEN"));
284+
assert_eq!(creds.account_id().unwrap().as_str(), "123456789001");
238285
assert_eq!(
239286
creds.expiry(),
240287
Some(
@@ -268,4 +315,28 @@ mod test {
268315
.await
269316
.expect_err("timeout forced");
270317
}
318+
319+
#[tokio::test]
320+
async fn credentials_with_fallback_account_id() {
321+
let provider = CredentialProcessProvider::builder()
322+
.command(CommandWithSensitiveArgs::new(String::from(
323+
r#"echo '{ "Version": 1, "AccessKeyId": "ASIARTESTID", "SecretAccessKey": "TESTSECRETKEY" }'"#,
324+
)))
325+
.account_id("012345678901")
326+
.build();
327+
let creds = provider.provide_credentials().await.unwrap();
328+
assert_eq!("012345678901", creds.account_id().unwrap().as_str());
329+
}
330+
331+
#[tokio::test]
332+
async fn fallback_account_id_shadowed_by_account_id_in_process_output() {
333+
let provider = CredentialProcessProvider::builder()
334+
.command(CommandWithSensitiveArgs::new(String::from(
335+
r#"echo '{ "Version": 1, "AccessKeyId": "ASIARTESTID", "SecretAccessKey": "TESTSECRETKEY", "AccountId": "111122223333" }'"#,
336+
)))
337+
.account_id("012345678901")
338+
.build();
339+
let creds = provider.provide_credentials().await.unwrap();
340+
assert_eq!("111122223333", creds.account_id().unwrap().as_str());
341+
}
271342
}

aws/rust-runtime/aws-config/src/default_provider.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,3 +66,6 @@ pub mod request_min_compression_size_bytes;
6666

6767
/// Default provider chains for request/response checksum configuration
6868
pub mod checksums;
69+
70+
/// Default provider chain for account-based endpoint mode
71+
pub mod account_id_endpoint_mode;
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
use crate::provider_config::ProviderConfig;
7+
use aws_runtime::env_config::EnvConfigValue;
8+
use aws_smithy_types::error::display::DisplayErrorContext;
9+
use aws_types::endpoint_config::AccountIdEndpointMode;
10+
use std::str::FromStr;
11+
12+
mod env {
13+
pub(super) const ACCOUNT_ID_ENDPOINT_MODE: &str = "AWS_ACCOUNT_ID_ENDPOINT_MODE";
14+
}
15+
16+
mod profile_key {
17+
pub(super) const ACCOUNT_ID_ENDPOINT_MODE: &str = "account_id_endpoint_mode";
18+
}
19+
20+
/// Load the value for the Account-based endpoint mode
21+
///
22+
/// This checks the following sources:
23+
/// 1. The environment variable `AWS_ACCOUNT_ID_ENDPOINT_MODE=preferred/disabled/required`
24+
/// 2. The profile key `account_id_endpoint_mode=preferred/disabled/required`
25+
///
26+
/// If invalid values are found, the provider will return `None` and an error will be logged.
27+
pub(crate) async fn account_id_endpoint_mode_provider(
28+
provider_config: &ProviderConfig,
29+
) -> Option<AccountIdEndpointMode> {
30+
let env = provider_config.env();
31+
let profiles = provider_config.profile().await;
32+
33+
EnvConfigValue::new()
34+
.env(env::ACCOUNT_ID_ENDPOINT_MODE)
35+
.profile(profile_key::ACCOUNT_ID_ENDPOINT_MODE)
36+
.validate(&env, profiles, AccountIdEndpointMode::from_str)
37+
.map_err(|err| tracing::warn!(err = %DisplayErrorContext(&err), "invalid value for `AccountIdEndpointMode`"))
38+
.unwrap_or(None)
39+
}
40+
41+
#[cfg(test)]
42+
mod test {
43+
use super::account_id_endpoint_mode_provider;
44+
use super::env;
45+
#[allow(deprecated)]
46+
use crate::profile::profile_file::{ProfileFileKind, ProfileFiles};
47+
use crate::provider_config::ProviderConfig;
48+
use aws_types::os_shim_internal::{Env, Fs};
49+
use tracing_test::traced_test;
50+
51+
#[tokio::test]
52+
#[traced_test]
53+
async fn log_error_on_invalid_value() {
54+
let conf = ProviderConfig::empty().with_env(Env::from_slice(&[(
55+
env::ACCOUNT_ID_ENDPOINT_MODE,
56+
"invalid",
57+
)]));
58+
assert_eq!(None, account_id_endpoint_mode_provider(&conf).await);
59+
assert!(logs_contain("invalid value for `AccountIdEndpointMode`"));
60+
}
61+
62+
#[tokio::test]
63+
#[traced_test]
64+
async fn environment_priority() {
65+
let conf = ProviderConfig::empty()
66+
.with_env(Env::from_slice(&[(
67+
env::ACCOUNT_ID_ENDPOINT_MODE,
68+
"disabled",
69+
)]))
70+
.with_profile_config(
71+
Some(
72+
#[allow(deprecated)]
73+
ProfileFiles::builder()
74+
.with_file(
75+
#[allow(deprecated)]
76+
ProfileFileKind::Config,
77+
"conf",
78+
)
79+
.build(),
80+
),
81+
None,
82+
)
83+
.with_fs(Fs::from_slice(&[(
84+
"conf",
85+
"[default]\naccount_id_endpoint_mode = required",
86+
)]));
87+
assert_eq!(
88+
"disabled".to_owned(),
89+
account_id_endpoint_mode_provider(&conf)
90+
.await
91+
.unwrap()
92+
.to_string(),
93+
);
94+
}
95+
}

0 commit comments

Comments
 (0)