use std::collections::HashMap;
use std::sync::Arc;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use zeroize::Zeroizing;
use crate::executor::PipelineResult;
use crate::extensions::{
ClientExtension, DelegationExtension, Extensions, RawCredentialsExtension, SecurityExtension,
SubjectExtension, WorkloadIdentity,
};
use crate::impl_plugin_payload;
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TokenSource {
Bearer,
UserToken,
Mtls,
SpiffeJwtSvid,
ApiKey,
#[serde(untagged)]
Custom(String),
}
impl Default for TokenSource {
fn default() -> Self {
TokenSource::Bearer
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IdentityPayload {
#[serde(skip)]
raw_token: Zeroizing<String>,
source: TokenSource,
#[serde(default, skip_serializing_if = "Option::is_none")]
source_header: Option<String>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
headers: HashMap<String, String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
client_host: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
client_port: Option<u16>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub subject: Option<SubjectExtension>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub client: Option<ClientExtension>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub caller_workload: Option<WorkloadIdentity>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub delegation: Option<DelegationExtension>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub raw_credentials: Option<RawCredentialsExtension>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub resolved_at: Option<DateTime<Utc>>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub raw_claims: HashMap<String, serde_json::Value>,
}
impl IdentityPayload {
pub fn new(raw_token: impl Into<String>, source: TokenSource) -> Self {
Self {
raw_token: Zeroizing::new(raw_token.into()),
source,
source_header: None,
headers: HashMap::new(),
client_host: None,
client_port: None,
subject: None,
client: None,
caller_workload: None,
delegation: None,
raw_credentials: None,
resolved_at: None,
raw_claims: HashMap::new(),
}
}
pub fn with_source_header(mut self, h: impl Into<String>) -> Self {
self.source_header = Some(h.into());
self
}
pub fn with_headers(mut self, h: HashMap<String, String>) -> Self {
self.headers = h;
self
}
pub fn with_client_host(mut self, h: impl Into<String>) -> Self {
self.client_host = Some(h.into());
self
}
pub fn with_client_port(mut self, port: u16) -> Self {
self.client_port = Some(port);
self
}
pub fn raw_token(&self) -> &str {
&self.raw_token
}
pub fn source(&self) -> &TokenSource {
&self.source
}
pub fn source_header(&self) -> Option<&str> {
self.source_header.as_deref()
}
pub fn headers(&self) -> &HashMap<String, String> {
&self.headers
}
pub fn client_host(&self) -> Option<&str> {
self.client_host.as_deref()
}
pub fn client_port(&self) -> Option<u16> {
self.client_port
}
pub fn merge(&mut self, other: IdentityPayload) {
if other.subject.is_some() {
self.subject = other.subject;
}
if other.client.is_some() {
self.client = other.client;
}
if other.caller_workload.is_some() {
self.caller_workload = other.caller_workload;
}
if other.delegation.is_some() {
self.delegation = other.delegation;
}
if other.raw_credentials.is_some() {
self.raw_credentials = other.raw_credentials;
}
if other.resolved_at.is_some() {
self.resolved_at = other.resolved_at;
}
for (k, v) in other.raw_claims {
self.raw_claims.insert(k, v);
}
}
pub fn from_pipeline_result(result: &PipelineResult) -> Option<Self> {
result
.modified_payload
.as_ref()
.and_then(|p| p.as_any().downcast_ref::<IdentityPayload>())
.cloned()
}
pub fn apply_to_extensions(&self, mut ext: Extensions) -> Extensions {
let needs_security_update =
self.subject.is_some() || self.client.is_some() || self.caller_workload.is_some();
if needs_security_update {
let mut sec: SecurityExtension = ext
.security
.as_ref()
.map(|arc| (**arc).clone())
.unwrap_or_default();
if let Some(s) = &self.subject {
sec.subject = Some(s.clone());
}
if let Some(c) = &self.client {
sec.client = Some(c.clone());
}
if let Some(w) = &self.caller_workload {
sec.caller_workload = Some(w.clone());
}
ext.security = Some(Arc::new(sec));
}
if let Some(rc) = &self.raw_credentials {
ext.raw_credentials = Some(Arc::new(rc.clone()));
}
if let Some(d) = &self.delegation {
ext.delegation = Some(Arc::new(d.clone()));
}
ext
}
}
impl_plugin_payload!(IdentityPayload);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn raw_token_serializes_without_secret() {
let p = IdentityPayload::new("eyJhbGciOiJSUzI1NiJ9.payload.sig", TokenSource::Bearer);
let json = serde_json::to_string(&p).unwrap();
assert!(
!json.contains("eyJhbGciOiJSUzI1NiJ9"),
"raw_token leaked into serialized form: {}",
json,
);
assert!(json.contains("bearer"));
}
#[test]
fn deserialize_yields_empty_raw_token() {
let json = r#"{"source":"bearer"}"#;
let p: IdentityPayload = serde_json::from_str(json).unwrap();
assert_eq!(p.raw_token(), "");
assert_eq!(p.source(), &TokenSource::Bearer);
}
#[test]
fn token_source_custom_round_trips() {
let s = TokenSource::Custom("magic-link".into());
let json = serde_json::to_string(&s).unwrap();
let back: TokenSource = serde_json::from_str(&json).unwrap();
assert_eq!(s, back);
}
#[test]
fn input_builders_chain() {
let mut h = HashMap::new();
h.insert("user-agent".to_string(), "curl/8.0".to_string());
let p = IdentityPayload::new("tok", TokenSource::Bearer)
.with_source_header("Authorization")
.with_headers(h)
.with_client_host("10.0.0.1")
.with_client_port(443);
assert_eq!(p.raw_token(), "tok");
assert_eq!(p.source_header(), Some("Authorization"));
assert_eq!(p.client_host(), Some("10.0.0.1"));
assert_eq!(p.client_port(), Some(443));
assert_eq!(
p.headers().get("user-agent").map(String::as_str),
Some("curl/8.0")
);
}
#[test]
fn handler_can_populate_output_on_clone() {
let original = IdentityPayload::new("eyJ.tok", TokenSource::Bearer);
let mut updated = original.clone();
updated.subject = Some(SubjectExtension {
id: Some("alice".into()),
..Default::default()
});
assert_eq!(updated.raw_token(), "eyJ.tok"); assert_eq!(
updated.subject.as_ref().unwrap().id.as_deref(),
Some("alice")
);
assert!(original.subject.is_none());
}
#[test]
fn merge_overlays_some_onto_none() {
let mut base = IdentityPayload::new("tok", TokenSource::Bearer);
base.subject = Some(SubjectExtension {
id: Some("alice".into()),
..Default::default()
});
let mut overlay = IdentityPayload::new("tok", TokenSource::Bearer);
overlay.caller_workload = Some(WorkloadIdentity {
spiffe_id: Some("spiffe://corp.com/inbound".into()),
..Default::default()
});
base.merge(overlay);
assert_eq!(base.subject.as_ref().unwrap().id.as_deref(), Some("alice"));
assert!(base.caller_workload.is_some());
}
}