//! API-key wrapper and the `tonic` interceptor that attaches it as a Bearer //! token on every outbound gRPC call. The wrapper redacts its inner value in //! `Debug`/`Display` so logs never leak the secret. use std::fmt; use tonic::metadata::MetadataValue; use tonic::service::Interceptor; use tonic::{Request, Status}; /// API key wrapper that avoids exposing raw credentials in formatted output. /// /// Use [`ApiKey::expose_secret`] when the underlying string is genuinely /// needed (for example, building the `authorization` header). #[derive(Clone, Eq, PartialEq)] pub struct ApiKey(String); impl ApiKey { /// Construct an [`ApiKey`] from the raw `mxgw__` string /// returned by the gateway's `apikey` admin command. pub fn new(value: impl Into) -> Self { Self(value.into()) } /// Return the raw key value. Callers must not log or otherwise persist /// the result. pub fn expose_secret(&self) -> &str { &self.0 } } impl fmt::Debug for ApiKey { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { formatter .debug_tuple("ApiKey") .field(&"") .finish() } } impl fmt::Display for ApiKey { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { formatter.write_str("") } } /// `tonic` interceptor that attaches gateway API key metadata. #[derive(Clone, Debug, Default)] pub struct AuthInterceptor { api_key: Option, } impl AuthInterceptor { /// Build an interceptor that injects the supplied API key on every /// request. Pass `None` to disable authentication (useful for local /// development against a gateway with `Authentication:Required = false`). pub fn new(api_key: Option) -> Self { Self { api_key } } } impl Interceptor for AuthInterceptor { fn call(&mut self, mut request: Request<()>) -> Result, Status> { if let Some(api_key) = &self.api_key { let header_value = format!("Bearer {}", api_key.expose_secret()) .parse::>() .map_err(|_| Status::unauthenticated("invalid API key metadata"))?; request.metadata_mut().insert("authorization", header_value); } Ok(request) } } #[cfg(test)] mod tests { use tonic::service::Interceptor; use tonic::Request; use super::{ApiKey, AuthInterceptor}; #[test] fn api_key_debug_is_redacted() { let key = ApiKey::new("mxgw_visible_secret"); assert_eq!(format!("{key:?}"), "ApiKey(\"\")"); assert!(!format!("{key:?}").contains("visible_secret")); assert_eq!(key.to_string(), ""); } #[test] fn interceptor_attaches_bearer_metadata() { let mut interceptor = AuthInterceptor::new(Some(ApiKey::new("mxgw_fixture_secret"))); let request = interceptor.call(Request::new(())).unwrap(); assert_eq!( request.metadata().get("authorization").unwrap(), "Bearer mxgw_fixture_secret" ); } }