Skip to content

Commit 85d2621

Browse files
authored
Improve error messaging for auth scheme selection (#3277)
This patch adds a stack-allocated list of explored auth schemes and the reason each didn't work to the auth orchestrator so that its error message is much easier to decipher. ---- _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 75b4c35 commit 85d2621

File tree

5 files changed

+323
-7
lines changed

5 files changed

+323
-7
lines changed

CHANGELOG.next.toml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,15 @@ This is designed for unit tests and using local mocks like DynamoDB Local and Lo
4949
meta = { "breaking" = false, "tada" = true, "bug" = false }
5050
author = "rcoh"
5151
references = ["smithy-rs#3279", "aws-sdk-rust#971"]
52+
53+
[[aws-sdk-rust]]
54+
message = "Improve the error messages for when auth fails to select an auth scheme for a request."
55+
references = ["aws-sdk-rust#979", "smithy-rs#3277"]
56+
meta = { "breaking" = false, "tada" = false, "bug" = false }
57+
author = "jdisanti"
58+
59+
[[smithy-rs]]
60+
message = "Improve the error messages for when auth fails to select an auth scheme for a request."
61+
references = ["smithy-rs#3277"]
62+
meta = { "breaking" = false, "tada" = false, "bug" = false, "target" = "client" }
63+
author = "jdisanti"
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
use aws_sdk_dynamodb::config::Region;
7+
use aws_sdk_dynamodb::error::DisplayErrorContext;
8+
use aws_sdk_dynamodb::{Client, Config};
9+
use aws_smithy_runtime::assert_str_contains;
10+
use aws_smithy_runtime::client::http::test_util::capture_request;
11+
12+
#[tokio::test]
13+
async fn auth_scheme_error() {
14+
let (http_client, _) = capture_request(None);
15+
let config = Config::builder()
16+
.behavior_version_latest()
17+
.http_client(http_client)
18+
.region(Region::new("us-west-2"))
19+
// intentionally omitting credentials_provider
20+
.build();
21+
let client = Client::from_conf(config);
22+
23+
let err = client
24+
.list_tables()
25+
.send()
26+
.await
27+
.expect_err("there is no credential provider, so this must fail");
28+
assert_str_contains!(
29+
DisplayErrorContext(&err).to_string(),
30+
"\"sigv4\" wasn't a valid option because there was no identity resolver for it. Be sure to set an identity"
31+
);
32+
}

rust-runtime/aws-smithy-runtime/src/client/orchestrator/auth.rs

Lines changed: 220 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,18 +20,86 @@ use std::error::Error as StdError;
2020
use std::fmt;
2121
use tracing::trace;
2222

23+
#[derive(Debug)]
24+
struct NoMatchingAuthSchemeError(ExploredList);
25+
26+
impl fmt::Display for NoMatchingAuthSchemeError {
27+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
28+
let explored = &self.0;
29+
30+
// Use the information we have about the auth options that were explored to construct
31+
// as helpful of an error message as possible.
32+
if explored.items().count() == 0 {
33+
return f.write_str(
34+
"no auth options are available. This can happen if there's \
35+
a problem with the service model, or if there is a codegen bug.",
36+
);
37+
}
38+
if explored
39+
.items()
40+
.all(|explored| matches!(explored.result, ExploreResult::NoAuthScheme))
41+
{
42+
return f.write_str(
43+
"no auth schemes are registered. This can happen if there's \
44+
a problem with the service model, or if there is a codegen bug.",
45+
);
46+
}
47+
48+
let mut try_add_identity = false;
49+
let mut likely_bug = false;
50+
f.write_str("failed to select an auth scheme to sign the request with.")?;
51+
for item in explored.items() {
52+
write!(
53+
f,
54+
" \"{}\" wasn't a valid option because ",
55+
item.scheme_id.as_str()
56+
)?;
57+
f.write_str(match item.result {
58+
ExploreResult::NoAuthScheme => {
59+
likely_bug = true;
60+
"no auth scheme was registered for it."
61+
}
62+
ExploreResult::NoIdentityResolver => {
63+
try_add_identity = true;
64+
"there was no identity resolver for it."
65+
}
66+
ExploreResult::MissingEndpointConfig => {
67+
likely_bug = true;
68+
"there is auth config in the endpoint config, but this scheme wasn't listed in it \
69+
(see https://github.com/smithy-lang/smithy-rs/discussions/3281 for more details)."
70+
}
71+
ExploreResult::NotExplored => {
72+
debug_assert!(false, "this should be unreachable");
73+
"<unknown>"
74+
}
75+
})?;
76+
}
77+
if try_add_identity {
78+
f.write_str(" Be sure to set an identity, such as credentials, auth token, or other identity type that is required for this service.")?;
79+
} else if likely_bug {
80+
f.write_str(" This is likely a bug.")?;
81+
}
82+
if explored.truncated {
83+
f.write_str(" Note: there were other auth schemes that were evaluated that weren't listed here.")?;
84+
}
85+
86+
Ok(())
87+
}
88+
}
89+
90+
impl StdError for NoMatchingAuthSchemeError {}
91+
2392
#[derive(Debug)]
2493
enum AuthOrchestrationError {
25-
NoMatchingAuthScheme,
94+
MissingEndpointConfig,
2695
BadAuthSchemeEndpointConfig(Cow<'static, str>),
2796
}
2897

2998
impl fmt::Display for AuthOrchestrationError {
3099
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
31100
match self {
32-
Self::NoMatchingAuthScheme => f.write_str(
33-
"no auth scheme matched auth scheme options. This is a bug. Please file an issue.",
34-
),
101+
// This error is never bubbled up
102+
Self::MissingEndpointConfig => f.write_str("missing endpoint config"),
35103
Self::BadAuthSchemeEndpointConfig(message) => f.write_str(message),
36104
}
37105
}
@@ -59,6 +127,8 @@ pub(super) async fn orchestrate_auth(
59127
"orchestrating auth",
60128
);
61129

130+
let mut explored = ExploredList::default();
131+
62132
// Iterate over IDs of possibly-supported auth schemes
63133
for &scheme_id in options.as_ref() {
64134
// For each ID, try to resolve the corresponding auth scheme.
@@ -95,16 +165,21 @@ pub(super) async fn orchestrate_auth(
95165
)?;
96166
return Ok(());
97167
}
98-
Err(AuthOrchestrationError::NoMatchingAuthScheme) => {
168+
Err(AuthOrchestrationError::MissingEndpointConfig) => {
169+
explored.push(scheme_id, ExploreResult::MissingEndpointConfig);
99170
continue;
100171
}
101172
Err(other_err) => return Err(other_err.into()),
102173
}
174+
} else {
175+
explored.push(scheme_id, ExploreResult::NoIdentityResolver);
103176
}
177+
} else {
178+
explored.push(scheme_id, ExploreResult::NoAuthScheme);
104179
}
105180
}
106181

107-
Err(AuthOrchestrationError::NoMatchingAuthScheme.into())
182+
Err(NoMatchingAuthSchemeError(explored).into())
108183
}
109184

110185
fn extract_endpoint_auth_scheme_config(
@@ -135,10 +210,66 @@ fn extract_endpoint_auth_scheme_config(
135210
.and_then(Document::as_string);
136211
config_scheme_id == Some(scheme_id.as_str())
137212
})
138-
.ok_or(AuthOrchestrationError::NoMatchingAuthScheme)?;
213+
.ok_or(AuthOrchestrationError::MissingEndpointConfig)?;
139214
Ok(AuthSchemeEndpointConfig::from(Some(auth_scheme_config)))
140215
}
141216

217+
#[derive(Debug)]
218+
enum ExploreResult {
219+
NotExplored,
220+
NoAuthScheme,
221+
NoIdentityResolver,
222+
MissingEndpointConfig,
223+
}
224+
225+
/// Information about an evaluated auth option.
226+
/// This should be kept small so it can fit in an array on the stack.
227+
#[derive(Debug)]
228+
struct ExploredAuthOption {
229+
scheme_id: AuthSchemeId,
230+
result: ExploreResult,
231+
}
232+
impl Default for ExploredAuthOption {
233+
fn default() -> Self {
234+
Self {
235+
scheme_id: AuthSchemeId::new(""),
236+
result: ExploreResult::NotExplored,
237+
}
238+
}
239+
}
240+
241+
const MAX_EXPLORED_LIST_LEN: usize = 8;
242+
243+
/// Stack allocated list of explored auth options for error messaging
244+
#[derive(Default)]
245+
struct ExploredList {
246+
items: [ExploredAuthOption; MAX_EXPLORED_LIST_LEN],
247+
len: usize,
248+
truncated: bool,
249+
}
250+
impl ExploredList {
251+
fn items(&self) -> impl Iterator<Item = &ExploredAuthOption> {
252+
self.items.iter().take(self.len)
253+
}
254+
255+
fn push(&mut self, scheme_id: AuthSchemeId, result: ExploreResult) {
256+
if self.len + 1 >= self.items.len() {
257+
self.truncated = true;
258+
} else {
259+
self.items[self.len] = ExploredAuthOption { scheme_id, result };
260+
self.len += 1;
261+
}
262+
}
263+
}
264+
impl fmt::Debug for ExploredList {
265+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
266+
f.debug_struct("ExploredList")
267+
.field("items", &&self.items[0..self.len])
268+
.field("truncated", &self.truncated)
269+
.finish()
270+
}
271+
}
272+
142273
#[cfg(all(test, feature = "test-util"))]
143274
mod tests {
144275
use super::*;
@@ -481,4 +612,86 @@ mod tests {
481612
.unwrap()
482613
);
483614
}
615+
616+
#[test]
617+
fn friendly_error_messages() {
618+
let err = NoMatchingAuthSchemeError(ExploredList::default());
619+
assert_eq!(
620+
"no auth options are available. This can happen if there's a problem with \
621+
the service model, or if there is a codegen bug.",
622+
err.to_string()
623+
);
624+
625+
let mut list = ExploredList::default();
626+
list.push(
627+
AuthSchemeId::new("SigV4"),
628+
ExploreResult::NoIdentityResolver,
629+
);
630+
list.push(
631+
AuthSchemeId::new("SigV4a"),
632+
ExploreResult::NoIdentityResolver,
633+
);
634+
let err = NoMatchingAuthSchemeError(list);
635+
assert_eq!(
636+
"failed to select an auth scheme to sign the request with. \
637+
\"SigV4\" wasn't a valid option because there was no identity resolver for it. \
638+
\"SigV4a\" wasn't a valid option because there was no identity resolver for it. \
639+
Be sure to set an identity, such as credentials, auth token, or other identity \
640+
type that is required for this service.",
641+
err.to_string()
642+
);
643+
644+
// It should prioritize the suggestion to try an identity before saying it's a bug
645+
let mut list = ExploredList::default();
646+
list.push(
647+
AuthSchemeId::new("SigV4"),
648+
ExploreResult::NoIdentityResolver,
649+
);
650+
list.push(
651+
AuthSchemeId::new("SigV4a"),
652+
ExploreResult::MissingEndpointConfig,
653+
);
654+
let err = NoMatchingAuthSchemeError(list);
655+
assert_eq!(
656+
"failed to select an auth scheme to sign the request with. \
657+
\"SigV4\" wasn't a valid option because there was no identity resolver for it. \
658+
\"SigV4a\" wasn't a valid option because there is auth config in the endpoint \
659+
config, but this scheme wasn't listed in it (see \
660+
https://github.com/smithy-lang/smithy-rs/discussions/3281 for more details). \
661+
Be sure to set an identity, such as credentials, auth token, or other identity \
662+
type that is required for this service.",
663+
err.to_string()
664+
);
665+
666+
// Otherwise, it should suggest it's a bug
667+
let mut list = ExploredList::default();
668+
list.push(
669+
AuthSchemeId::new("SigV4a"),
670+
ExploreResult::MissingEndpointConfig,
671+
);
672+
let err = NoMatchingAuthSchemeError(list);
673+
assert_eq!(
674+
"failed to select an auth scheme to sign the request with. \
675+
\"SigV4a\" wasn't a valid option because there is auth config in the endpoint \
676+
config, but this scheme wasn't listed in it (see \
677+
https://github.com/smithy-lang/smithy-rs/discussions/3281 for more details). \
678+
This is likely a bug.",
679+
err.to_string()
680+
);
681+
682+
// Truncation should be indicated
683+
let mut list = ExploredList::default();
684+
for _ in 0..=MAX_EXPLORED_LIST_LEN {
685+
list.push(
686+
AuthSchemeId::new("dontcare"),
687+
ExploreResult::MissingEndpointConfig,
688+
);
689+
}
690+
let err = NoMatchingAuthSchemeError(list).to_string();
691+
if !err.contains(
692+
"Note: there were other auth schemes that were evaluated that weren't listed here",
693+
) {
694+
panic!("The error should indicate that the explored list was truncated.");
695+
}
696+
}
484697
}

rust-runtime/aws-smithy-runtime/src/test_util.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,5 @@
55

66
/// Utility for capturing and displaying logs during a unit test.
77
pub mod capture_test_logs;
8+
9+
mod assertions;
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
/// Asserts that a given string value `$str` contains a substring `$expected`.
7+
///
8+
/// This macro can also take a custom panic message with formatting.
9+
#[macro_export]
10+
macro_rules! assert_str_contains {
11+
($str:expr, $expected:expr) => {
12+
assert_str_contains!($str, $expected, "")
13+
};
14+
($str:expr, $expected:expr, $($fmt_args:tt)+) => {{
15+
let s = $str;
16+
let expected = $expected;
17+
if !s.contains(&expected) {
18+
panic!(
19+
"assertion failed: `str.contains(expected)`\n{:>8}: {expected}\n{:>8}: {s}\n{}",
20+
"expected",
21+
"str",
22+
::std::fmt::format(::std::format_args!($($fmt_args)+)),
23+
);
24+
}
25+
}};
26+
}
27+
28+
#[cfg(test)]
29+
mod tests {
30+
use std::panic::{catch_unwind, UnwindSafe};
31+
32+
fn expect_panic(f: impl FnOnce() -> () + UnwindSafe) -> String {
33+
*catch_unwind(f)
34+
.expect_err("it should fail")
35+
.downcast::<String>()
36+
.expect("it should be a string")
37+
}
38+
39+
#[test]
40+
fn assert_str_contains() {
41+
assert_str_contains!("foobar", "bar");
42+
assert_str_contains!("foobar", "o");
43+
44+
assert_eq!(
45+
"assertion failed: `str.contains(expected)`\nexpected: not-in-it\n str: foobar\n",
46+
expect_panic(|| assert_str_contains!("foobar", "not-in-it"))
47+
);
48+
assert_eq!(
49+
"assertion failed: `str.contains(expected)`\nexpected: not-in-it\n str: foobar\nsome custom message",
50+
expect_panic(|| assert_str_contains!("foobar", "not-in-it", "some custom message"))
51+
);
52+
assert_eq!(
53+
"assertion failed: `str.contains(expected)`\nexpected: not-in-it\n str: foobar\nsome custom message with formatting",
54+
expect_panic(|| assert_str_contains!("foobar", "not-in-it", "some custom message with {}", "formatting"))
55+
);
56+
}
57+
}

0 commit comments

Comments
 (0)