diff --git a/crates/api_clients/artcraft/artcraft_api_keys/src/lib.rs b/crates/api_clients/artcraft/artcraft_api_keys/src/lib.rs index 7217e37284..05d290fa25 100644 --- a/crates/api_clients/artcraft/artcraft_api_keys/src/lib.rs +++ b/crates/api_clients/artcraft/artcraft_api_keys/src/lib.rs @@ -28,16 +28,18 @@ impl ArtcraftApiKey { parse_from_header_value(header_value) } - pub fn as_str(&self) -> &str { + /// The full, unredacted key value as a borrowed `&str`. + /// + /// NB: unlike `Display`/`Debug` (which redact), this exposes the complete secret — the + /// `_be_careful` suffix is a reminder to use it only where the real value is genuinely needed + /// (storage, the create-key response, tests), never in logs or error messages. + pub fn as_str_be_careful(&self) -> &str { &self.0 } - /// The full, unredacted key value as an owned `String`. - /// - /// NB: unlike `Display`/`Debug` (which redact), this returns the complete secret — use it only - /// where the real value is needed (storage, the create-key response, tests). - #[allow(clippy::inherent_to_string_shadow_display)] - pub fn to_string(&self) -> String { + /// The full, unredacted key value as an owned `String`. See [`Self::as_str_be_careful`] for the + /// caveat behind the `_be_careful` suffix. + pub fn to_string_be_careful(&self) -> String { self.0.clone() } @@ -70,7 +72,7 @@ mod tests { #[test] fn new_from_str_and_as_str_round_trip() { let key = ArtcraftApiKey::new_from_str(SAMPLE_KEY); - assert_eq!(key.as_str(), SAMPLE_KEY); + assert_eq!(key.as_str_be_careful(), SAMPLE_KEY); assert_eq!(key.0, SAMPLE_KEY); } diff --git a/crates/api_clients/artcraft/artcraft_api_keys/src/parse_from_header_value.rs b/crates/api_clients/artcraft/artcraft_api_keys/src/parse_from_header_value.rs index 1acca19c5e..61bdfce32c 100644 --- a/crates/api_clients/artcraft/artcraft_api_keys/src/parse_from_header_value.rs +++ b/crates/api_clients/artcraft/artcraft_api_keys/src/parse_from_header_value.rs @@ -64,7 +64,7 @@ mod tests { const SAMPLE_KEY: &str = "artcraft_api_55ax0zhd580m598r6n4n7szdwjb2b28sypapvawh"; fn parsed(header_value: &str) -> Option { - parse_from_header_value(header_value).map(|key| key.to_string()) + parse_from_header_value(header_value).map(|key| key.to_string_be_careful()) } #[test] @@ -120,6 +120,6 @@ mod tests { #[test] fn returns_an_artcraft_api_key() { let key = parse_from_header_value(&format!("Bearer {SAMPLE_KEY}")).expect("should parse"); - assert_eq!(key.as_str(), SAMPLE_KEY); + assert_eq!(key.as_str_be_careful(), SAMPLE_KEY); } } diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/api_keys/create_api_key_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/api_keys/create_api_key_handler.rs index 383443d370..a40d9a9e92 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/api_keys/create_api_key_handler.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/api_keys/create_api_key_handler.rs @@ -75,7 +75,7 @@ pub async fn create_api_key_handler( ip_address: &ip_address, name: &name, maybe_description: maybe_description.as_deref(), - api_key: api_key.as_str(), + api_key: api_key.as_str_be_careful(), mysql_executor: &mut *conn, phantom: PhantomData, }).await.map_err(|err| { @@ -88,6 +88,6 @@ pub async fn create_api_key_handler( api_key_token: token, // Return the full secret value (never the redacted Debug/Display form). This is the only // time it is ever exposed. - api_key: api_key.to_string(), + api_key: api_key.to_string_be_careful(), })) } diff --git a/crates/service/web/storyteller_web/src/http_server/web_utils/get_authorization_header_api_key.rs b/crates/service/web/storyteller_web/src/http_server/web_utils/get_authorization_header_api_key.rs index b32489d69f..fad5f03ba2 100644 --- a/crates/service/web/storyteller_web/src/http_server/web_utils/get_authorization_header_api_key.rs +++ b/crates/service/web/storyteller_web/src/http_server/web_utils/get_authorization_header_api_key.rs @@ -60,7 +60,7 @@ mod tests { .insert_header((header_name, value.as_str())) .to_http_request(); assert_eq!( - get_authorization_header_api_key(&http_request).map(|key| key.to_string()), + get_authorization_header_api_key(&http_request).map(|key| key.to_string_be_careful()), Some(SAMPLE_KEY.to_string()), "failed for header name {header_name:?}"); } @@ -132,13 +132,13 @@ mod tests { fn parse_authorization_header_api_key(header_value: &str) -> Option { ArtcraftApiKey::parse_from_authorization_header_value(header_value) - .map(|key| key.to_string()) + .map(|key| key.to_string_be_careful()) } fn api_key_for_authorization_header(value: &str) -> Option { let http_request = TestRequest::default() .insert_header(("Authorization", value)) .to_http_request(); - get_authorization_header_api_key(&http_request).map(|key| key.to_string()) + get_authorization_header_api_key(&http_request).map(|key| key.to_string_be_careful()) } } diff --git a/crates/service/web/storyteller_web/src/util/generate_api_key.rs b/crates/service/web/storyteller_web/src/util/generate_api_key.rs index 10f7c3e770..9ea2c8e10e 100644 --- a/crates/service/web/storyteller_web/src/util/generate_api_key.rs +++ b/crates/service/web/storyteller_web/src/util/generate_api_key.rs @@ -54,19 +54,19 @@ mod tests { #[test] fn starts_with_prefix() { - assert!(generate_api_key().as_str().starts_with("artcraft_api_")); - assert!(generate_api_key().as_str().starts_with(API_KEY_PREFIX)); + assert!(generate_api_key().as_str_be_careful().starts_with("artcraft_api_")); + assert!(generate_api_key().as_str_be_careful().starts_with(API_KEY_PREFIX)); } #[test] fn has_expected_total_length() { - assert_eq!(generate_api_key().as_str().len(), API_KEY_PREFIX.len() + ENTROPY_CHAR_COUNT); + assert_eq!(generate_api_key().as_str_be_careful().len(), API_KEY_PREFIX.len() + ENTROPY_CHAR_COUNT); } #[test] fn entropy_suffix_is_crockford_lowercase() { let key = generate_api_key(); - let suffix = key.as_str().strip_prefix(API_KEY_PREFIX).expect("key must start with the prefix"); + let suffix = key.as_str_be_careful().strip_prefix(API_KEY_PREFIX).expect("key must start with the prefix"); assert_eq!(suffix.len(), ENTROPY_CHAR_COUNT); for byte in suffix.bytes() { @@ -84,21 +84,21 @@ mod tests { fn forbidden_characters_are_absent() { let key = generate_api_key(); for forbidden in ['.', '+', '/'] { - assert!(!key.as_str().contains(forbidden), "key must not contain {:?}", forbidden); + assert!(!key.as_str_be_careful().contains(forbidden), "key must not contain {:?}", forbidden); } } #[test] fn underscores_only_appear_in_the_prefix() { let key = generate_api_key(); - let suffix = key.as_str().strip_prefix(API_KEY_PREFIX).expect("key must start with the prefix"); + let suffix = key.as_str_be_careful().strip_prefix(API_KEY_PREFIX).expect("key must start with the prefix"); assert!(!suffix.contains('_'), "the random body must not contain underscores"); } #[test] fn entropy_suffix_is_alphanumeric() { let key = generate_api_key(); - let suffix = key.as_str().strip_prefix(API_KEY_PREFIX).expect("key must start with the prefix"); + let suffix = key.as_str_be_careful().strip_prefix(API_KEY_PREFIX).expect("key must start with the prefix"); assert!(suffix.chars().all(|c| c.is_ascii_alphanumeric())); }