102 lines
3.1 KiB
Rust
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"
|
|
);
|
|
}
|
|
}
|