use std::path::{Path, PathBuf};
use secrecy::SecretString;
use serde::{Deserialize, Serialize};
use crate::authenticator::DynInternalAuthenticator;
use crate::shared_secret::{InvalidSharedSecret, SharedSecretInternalAuthenticator};
pub const DEFAULT_INTERNAL_PEER_NAME: &str = "toolkit-internal";
#[derive(Clone, Deserialize)]
#[serde(tag = "provider", rename_all = "snake_case")]
pub enum InternalAuthConfig {
SharedSecret {
secret: SecretString,
#[serde(default = "default_peer_name")]
peer_name: String,
},
Kube {
#[serde(deserialize_with = "non_empty_audiences")]
audiences: Vec<String>,
#[serde(default)]
token_path: Option<PathBuf>,
},
}
fn default_peer_name() -> String {
DEFAULT_INTERNAL_PEER_NAME.to_owned()
}
fn non_empty_audiences<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::de::Error as _;
let audiences = Vec::<String>::deserialize(deserializer)?;
if audiences.is_empty() {
return Err(D::Error::custom(
"internal_auth provider=kube requires a non-empty `audiences` list; an empty one \
disables audience verification and accepts any ServiceAccount token in the cluster",
));
}
Ok(audiences)
}
impl std::fmt::Debug for InternalAuthConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::SharedSecret { peer_name, .. } => f
.debug_struct("SharedSecret")
.field("secret", &"<redacted>")
.field("peer_name", peer_name)
.finish(),
Self::Kube {
audiences,
token_path,
} => f
.debug_struct("Kube")
.field("audiences", audiences)
.field("token_path", token_path)
.finish(),
}
}
}
impl Serialize for InternalAuthConfig {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
use serde::ser::SerializeMap;
match self {
Self::SharedSecret { peer_name, .. } => {
let mut map = serializer.serialize_map(Some(3))?;
map.serialize_entry("provider", "shared_secret")?;
map.serialize_entry("secret", "<redacted>")?;
map.serialize_entry("peer_name", peer_name)?;
map.end()
}
Self::Kube {
audiences,
token_path,
} => {
let mut map = serializer.serialize_map(Some(3))?;
map.serialize_entry("provider", "kube")?;
map.serialize_entry("audiences", audiences)?;
map.serialize_entry("token_path", token_path)?;
map.end()
}
}
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum InvalidInternalAuth {
#[error(transparent)]
SharedSecret(#[from] InvalidSharedSecret),
#[error(
"internal_auth provider=kube requires a non-empty `audiences` list; an empty one \
disables audience verification and accepts any ServiceAccount token in the cluster"
)]
EmptyKubeAudiences,
}
#[derive(Debug)]
pub enum BuiltAuthenticator {
Built(DynInternalAuthenticator),
RequiresExternalBackend,
}
impl InternalAuthConfig {
pub fn build_authenticator(&self) -> Result<BuiltAuthenticator, InvalidInternalAuth> {
match self {
Self::SharedSecret { secret, peer_name } => {
let auth =
SharedSecretInternalAuthenticator::try_new(secret.clone(), peer_name.clone())?;
Ok(BuiltAuthenticator::Built(DynInternalAuthenticator::new(
auth,
)))
}
Self::Kube { audiences, .. } if audiences.is_empty() => {
Err(InvalidInternalAuth::EmptyKubeAudiences)
}
Self::Kube { .. } => Ok(BuiltAuthenticator::RequiresExternalBackend),
}
}
#[must_use]
pub fn shared_secret(&self) -> Option<SecretString> {
match self {
Self::SharedSecret { secret, .. } => Some(secret.clone()),
Self::Kube { .. } => None,
}
}
#[must_use]
pub fn is_kube(&self) -> bool {
matches!(self, Self::Kube { .. })
}
#[must_use]
pub fn kube_audiences(&self) -> Option<&[String]> {
match self {
Self::Kube { audiences, .. } => Some(audiences),
Self::SharedSecret { .. } => None,
}
}
#[must_use]
pub fn kube_token_path(&self) -> Option<&Path> {
match self {
Self::Kube { token_path, .. } => token_path.as_deref(),
Self::SharedSecret { .. } => None,
}
}
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use super::*;
use crate::internal_auth::{InternalAuthNError, InternalAuthenticator, PlatformIdentity};
use secrecy::ExposeSecret;
#[test]
fn debug_redacts_shared_secret_but_keeps_other_fields() {
let cfg = InternalAuthConfig::SharedSecret {
secret: SecretString::from("super-secret-token"),
peer_name: "hello".to_owned(),
};
let rendered = format!("{cfg:?}");
assert!(
!rendered.contains("super-secret-token"),
"secret must never appear in Debug output: {rendered}"
);
assert!(rendered.contains("<redacted>"), "expected redaction marker");
assert!(rendered.contains("hello"), "peer_name must be preserved");
let kube = InternalAuthConfig::Kube {
audiences: vec!["toolkit-internal".to_owned()],
token_path: Some(PathBuf::from("/var/run/secrets/tokens/t")),
};
let rendered = format!("{kube:?}");
assert!(rendered.contains("toolkit-internal"));
assert!(rendered.contains("/var/run/secrets/tokens/t"));
}
#[test]
fn serialize_redacts_shared_secret_but_keeps_other_fields() {
let cfg = InternalAuthConfig::SharedSecret {
secret: SecretString::from("super-secret-token"),
peer_name: "hello".to_owned(),
};
let json = serde_json::to_string(&cfg).expect("serialize");
assert!(
!json.contains("super-secret-token"),
"secret must never be serialized in plaintext: {json}"
);
assert!(json.contains("<redacted>"), "expected redaction marker");
assert!(json.contains("hello"), "peer_name must be preserved");
assert!(json.contains("shared_secret"), "provider tag preserved");
let round: InternalAuthConfig = serde_json::from_str(&json).expect("round-trip");
assert!(matches!(round, InternalAuthConfig::SharedSecret { .. }));
let kube = InternalAuthConfig::Kube {
audiences: vec!["toolkit-internal".to_owned()],
token_path: Some(PathBuf::from("/var/run/secrets/tokens/t")),
};
let json = serde_json::to_string(&kube).expect("serialize");
assert!(json.contains("kube"));
assert!(json.contains("toolkit-internal"));
assert!(json.contains("/var/run/secrets/tokens/t"));
}
#[tokio::test]
async fn deserializes_shared_secret_with_default_peer_name() {
let cfg: InternalAuthConfig = serde_json::from_value(serde_json::json!({
"provider": "shared_secret",
"secret": "s"
}))
.unwrap();
match &cfg {
InternalAuthConfig::SharedSecret { secret, peer_name } => {
assert_eq!(secret.expose_secret(), "s");
assert_eq!(peer_name, DEFAULT_INTERNAL_PEER_NAME);
}
InternalAuthConfig::Kube { .. } => panic!("expected shared_secret"),
}
assert!(!cfg.is_kube());
let BuiltAuthenticator::Built(authenticator) =
cfg.build_authenticator().expect("a valid secret")
else {
panic!("shared_secret builds its validator here");
};
let identity = authenticator
.authenticate("s")
.await
.expect("the configured secret must authenticate");
assert_eq!(
identity,
PlatformIdentity::Shared {
name: DEFAULT_INTERNAL_PEER_NAME.to_owned()
},
"the caller must be labelled with the configured peer name"
);
let rejected = authenticator.authenticate("not-the-secret").await;
assert!(
matches!(rejected, Err(InternalAuthNError::InvalidToken)),
"a wrong secret must be rejected, got {rejected:?}"
);
}
#[test]
fn deserializes_kube_with_token_path() {
let cfg: InternalAuthConfig = serde_json::from_value(serde_json::json!({
"provider": "kube",
"audiences": ["toolkit-internal"],
"token_path": "/var/run/secrets/tokens/toolkit-internal"
}))
.unwrap();
assert!(cfg.is_kube());
assert_eq!(
cfg.kube_audiences(),
Some(&["toolkit-internal".to_owned()][..])
);
assert_eq!(
cfg.kube_token_path().map(Path::to_owned),
Some(PathBuf::from("/var/run/secrets/tokens/toolkit-internal"))
);
assert!(matches!(
cfg.build_authenticator(),
Ok(BuiltAuthenticator::RequiresExternalBackend)
));
assert!(cfg.shared_secret().is_none());
}
#[test]
fn kube_built_in_code_with_no_audiences_is_refused() {
let unbound = InternalAuthConfig::Kube {
audiences: Vec::new(),
token_path: None,
};
assert!(matches!(
unbound.build_authenticator(),
Err(InvalidInternalAuth::EmptyKubeAudiences)
));
let bound = InternalAuthConfig::Kube {
audiences: vec!["toolkit-internal".to_owned()],
token_path: None,
};
assert!(matches!(
bound.build_authenticator(),
Ok(BuiltAuthenticator::RequiresExternalBackend)
));
}
#[test]
fn kube_without_audiences_is_refused() {
let err =
serde_json::from_value::<InternalAuthConfig>(serde_json::json!({ "provider": "kube" }))
.expect_err("kube without audiences must not deserialize");
assert!(
err.to_string().contains("audiences"),
"the error must name the field: {err}"
);
let err = serde_json::from_value::<InternalAuthConfig>(
serde_json::json!({ "provider": "kube", "audiences": [] }),
)
.expect_err("an explicitly empty list is the same weakness");
assert!(err.to_string().contains("audiences"), "got: {err}");
}
#[test]
fn kube_with_audiences_and_no_token_path_is_inbound_only() {
let cfg: InternalAuthConfig = serde_json::from_value(
serde_json::json!({ "provider": "kube", "audiences": ["toolkit-internal"] }),
)
.unwrap();
assert!(cfg.is_kube());
assert_eq!(
cfg.kube_token_path(),
None,
"no token path means inbound-only: nothing is minted outbound"
);
}
#[test]
fn rejects_an_unknown_provider() {
let result: Result<InternalAuthConfig, _> =
serde_json::from_value(serde_json::json!({ "provider": "kubernetes" }));
assert!(
result.is_err(),
"an unrecognised provider must not deserialize"
);
}
#[test]
fn shared_secret_accessors_return_none_for_kube_fields() {
let cfg = InternalAuthConfig::SharedSecret {
secret: SecretString::from("s"),
peer_name: "hello".to_owned(),
};
assert_eq!(
cfg.shared_secret().map(|s| s.expose_secret().to_owned()),
Some("s".to_owned())
);
assert!(cfg.kube_audiences().is_none());
assert!(cfg.kube_token_path().is_none());
}
}