use std::future::Future;
use std::path::PathBuf;
use secrecy::SecretString;
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum InternalCredential {
None,
BootstrapToken(SecretString),
KubeServiceAccountToken {
token_path: PathBuf,
audience: String,
},
MtlsIdentity {
cert: PathBuf,
key: PathBuf,
ca: PathBuf,
},
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[non_exhaustive]
#[serde(tag = "type")]
pub enum PlatformIdentity {
KubernetesServiceAccount {
namespace: String,
service_account: String,
pod: Option<String>,
},
Spiffe {
trust_domain: String,
name: String,
version: String,
},
Shared {
name: String,
},
OutboundMarker,
#[serde(other)]
Unknown,
}
impl PlatformIdentity {
#[must_use]
pub fn peer_name(&self) -> &str {
match self {
Self::KubernetesServiceAccount {
service_account, ..
} => service_account,
Self::Spiffe { name, .. } | Self::Shared { name } => name,
Self::Unknown => "<unknown>",
Self::OutboundMarker => "<outbound-marker>",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct PlatformSecurityContext {
identity: PlatformIdentity,
}
impl PlatformSecurityContext {
#[must_use]
pub fn new(identity: PlatformIdentity) -> Self {
Self { identity }
}
#[must_use]
pub fn outbound_marker() -> Self {
Self {
identity: PlatformIdentity::OutboundMarker,
}
}
#[must_use]
pub fn is_outbound_marker(&self) -> bool {
matches!(self.identity, PlatformIdentity::OutboundMarker)
}
#[must_use]
pub fn identity(&self) -> &PlatformIdentity {
&self.identity
}
#[must_use]
pub fn into_identity(self) -> PlatformIdentity {
self.identity
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PeerAuthenticated {
pub name: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PlatformAuthEnforced;
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum InternalAuthNError {
#[error("invalid internal credential")]
InvalidToken,
#[error("internal-auth backend unavailable")]
Unavailable,
#[error("internal authentication failed: {0}")]
Other(String),
}
pub trait InternalAuthenticator: Send + Sync {
fn authenticate(
&self,
token: &str,
) -> impl Future<Output = Result<PlatformIdentity, InternalAuthNError>> + Send;
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use super::*;
#[test]
fn platform_identity_peer_name() {
let sa = PlatformIdentity::KubernetesServiceAccount {
namespace: "toolkit".to_owned(),
service_account: "flight-control".to_owned(),
pod: Some("flight-control-0".to_owned()),
};
assert_eq!(sa.peer_name(), "flight-control");
let spiffe = PlatformIdentity::Spiffe {
trust_domain: "example.org".to_owned(),
name: "mini-chat".to_owned(),
version: "1.0.0".to_owned(),
};
assert_eq!(spiffe.peer_name(), "mini-chat");
}
#[test]
fn an_unrecognised_identity_tag_decodes_to_unknown() {
let identity: PlatformIdentity =
serde_json::from_str(r#"{"type":"future_method"}"#).unwrap();
assert_eq!(identity, PlatformIdentity::Unknown);
assert_eq!(
identity.peer_name(),
"<unknown>",
"an unrecognised identity must not resolve to a usable caller name"
);
}
#[test]
fn outbound_marker_carries_no_identity() {
let marker = PlatformSecurityContext::outbound_marker();
assert_eq!(marker.identity(), &PlatformIdentity::OutboundMarker);
assert_eq!(marker.identity().peer_name(), "<outbound-marker>");
assert!(marker.is_outbound_marker());
}
#[test]
fn the_outbound_marker_is_distinguishable_from_an_unrecognised_peer() {
let marker = PlatformSecurityContext::outbound_marker();
let unrecognised = PlatformSecurityContext::new(PlatformIdentity::Unknown);
assert_ne!(marker.identity(), unrecognised.identity());
assert!(marker.is_outbound_marker());
assert!(
!unrecognised.is_outbound_marker(),
"a peer this build does not recognise is still a peer, not our own marker"
);
}
#[test]
fn platform_security_context_roundtrips_serde() {
let ctx = PlatformSecurityContext::new(PlatformIdentity::KubernetesServiceAccount {
namespace: "toolkit".to_owned(),
service_account: "directory-service".to_owned(),
pod: None,
});
let json = serde_json::to_string(&ctx).unwrap();
let back: PlatformSecurityContext = serde_json::from_str(&json).unwrap();
assert_eq!(back, ctx);
}
}