Files
mxaccessgw/clients/rust/src/auth.rs
T

102 lines
3.1 KiB
Rust

//! 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_<key-id>_<secret>` string
/// returned by the gateway's `apikey` admin command.
pub fn new(value: impl Into<String>) -> 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(&"<redacted>")
.finish()
}
}
impl fmt::Display for ApiKey {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("<redacted>")
}
}
/// `tonic` interceptor that attaches gateway API key metadata.
#[derive(Clone, Debug, Default)]
pub struct AuthInterceptor {
api_key: Option<ApiKey>,
}
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<ApiKey>) -> Self {
Self { api_key }
}
}
impl Interceptor for AuthInterceptor {
fn call(&mut self, mut request: Request<()>) -> Result<Request<()>, Status> {
if let Some(api_key) = &self.api_key {
let header_value = format!("Bearer {}", api_key.expose_secret())
.parse::<MetadataValue<_>>()
.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(\"<redacted>\")");
assert!(!format!("{key:?}").contains("visible_secret"));
assert_eq!(key.to_string(), "<redacted>");
}
#[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"
);
}
}