use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
#[derive(Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ExternalSecretPayload {
pub context: String,
pub account: String,
pub secret: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub access_key_id: Option<String>,
}
impl Drop for ExternalSecretPayload {
fn drop(&mut self) {
zeroize::Zeroize::zeroize(&mut self.secret);
}
}
impl std::fmt::Debug for ExternalSecretPayload {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ExternalSecretPayload")
.field("context", &self.context)
.field("account", &self.account)
.field("secret", &"<redacted>")
.field("access_key_id", &self.access_key_id)
.finish()
}
}
#[derive(Clone, Serialize, Deserialize)]
#[serde(
tag = "kind",
rename_all = "camelCase",
rename_all_fields = "camelCase",
deny_unknown_fields
)]
#[non_exhaustive]
pub enum ExternalCredentialPayload {
AwsSession {
access_key_id: String,
secret_access_key: String,
session_token: String,
expiration: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
region: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
provider_request_id: Option<String>,
},
BearerToken {
token: String,
token_type: String,
expires_at: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
scope: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
provider_request_id: Option<String>,
},
PresignedRequest {
method: String,
url: String,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
headers: BTreeMap<String, String>,
expires_at: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
provider_request_id: Option<String>,
},
}
impl ExternalCredentialPayload {
pub fn expires_at(&self) -> &str {
match self {
Self::AwsSession { expiration, .. } => expiration,
Self::BearerToken { expires_at, .. } | Self::PresignedRequest { expires_at, .. } => {
expires_at
}
}
}
}
impl Drop for ExternalCredentialPayload {
fn drop(&mut self) {
use zeroize::Zeroize;
match self {
Self::AwsSession {
secret_access_key,
session_token,
..
} => {
secret_access_key.zeroize();
session_token.zeroize();
}
Self::BearerToken { token, .. } => token.zeroize(),
Self::PresignedRequest { url, .. } => url.zeroize(),
}
}
}
impl std::fmt::Debug for ExternalCredentialPayload {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let kind = match self {
Self::AwsSession { .. } => "awsSession",
Self::BearerToken { .. } => "bearerToken",
Self::PresignedRequest { .. } => "presignedRequest",
};
f.debug_struct("ExternalCredentialPayload")
.field("kind", &kind)
.field("expires_at", &self.expires_at())
.field("credential", &"<redacted>")
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn the_shapes_are_the_specifications() {
let secret = ExternalSecretPayload {
context: "community".into(),
account: "r2".into(),
secret: "s".into(),
access_key_id: Some("AKID".into()),
};
assert_eq!(
serde_json::to_value(&secret).unwrap(),
json!({ "context": "community", "account": "r2", "secret": "s", "accessKeyId": "AKID" })
);
let cred = ExternalCredentialPayload::PresignedRequest {
method: "GET".into(),
url: "https://b.example/k?X-Amz-Signature=x".into(),
headers: BTreeMap::new(),
expires_at: "2026-10-10T09:00:00Z".into(),
provider_request_id: None,
};
assert_eq!(
serde_json::to_value(&cred).unwrap(),
json!({ "kind": "presignedRequest", "method": "GET",
"url": "https://b.example/k?X-Amz-Signature=x", "expiresAt": "2026-10-10T09:00:00Z" })
);
let aws: ExternalCredentialPayload = serde_json::from_value(json!({
"kind": "awsSession", "accessKeyId": "ASIA", "secretAccessKey": "k",
"sessionToken": "t", "expiration": "2026-10-10T09:00:00Z", "region": "eu-west-1",
"providerRequestId": "req-1",
}))
.unwrap();
assert_eq!(aws.expires_at(), "2026-10-10T09:00:00Z");
assert!(format!("{aws:?}").contains("<redacted>"));
assert!(
serde_json::from_value::<ExternalCredentialPayload>(json!({
"kind": "bearerToken", "token": "t", "tokenType": "Bearer",
"expiresAt": "2026-10-10T09:00:00Z", "unexpected": 1,
}))
.is_err(),
"unknown members are refused"
);
}
}