use anyhow::{ensure, Context as _};
use async_trait::async_trait;
use nkeys::XKey;
use serde::{ser::SerializeStruct, Deserialize, Deserializer, Serialize, Serializer};
use std::collections::HashMap;
use wascap::jwt::{validate_token, CapabilityProvider, Component, Host};
mod errors;
pub use crate::errors::*;
pub const SECRET_API_VERSION: &str = "v1alpha1";
pub const WASMCLOUD_HOST_XKEY: &str = "WasmCloud-Host-Xkey";
pub const RESPONSE_XKEY: &str = "Server-Response-Xkey";
pub const SECRET_TYPE: &str = "secret.wasmcloud.dev/v1alpha1";
pub const SECRET_POLICY_PROPERTIES_TYPE: &str = "properties.secret.wasmcloud.dev/v1alpha1";
pub const SECRET_PREFIX: &str = "SECRET";
#[derive(Serialize, Deserialize, Default)]
pub struct Context {
pub entity_jwt: String,
pub host_jwt: String,
pub application: Application,
}
#[derive(Serialize, Deserialize, Default)]
pub struct Application {
#[serde(default)]
pub name: Option<String>,
#[serde(default)]
pub policy: String,
}
impl Context {
pub fn valid_claims(&self) -> Result<(), ContextValidationError> {
let component_valid = Self::valid_component(&self.entity_jwt);
let provider_valid = Self::valid_provider(&self.entity_jwt);
if provider_valid.is_err() {
if let Err(e) = component_valid {
return Err(ContextValidationError::InvalidComponentJWT(e.to_string()));
}
}
if Self::valid_host(&self.host_jwt).is_err() {
return Err(ContextValidationError::InvalidHostJWT(
Self::valid_host(&self.host_jwt).unwrap_err().to_string(),
));
}
Ok(())
}
fn valid_component(token: &str) -> anyhow::Result<()> {
let v = validate_token::<Component>(token)?;
ensure!(!v.expired, "token expired at `{}`", v.expires_human);
ensure!(
!v.cannot_use_yet,
"token cannot be used before `{}`",
v.not_before_human
);
ensure!(v.signature_valid, "signature is not valid");
Ok(())
}
fn valid_provider(token: &str) -> anyhow::Result<()> {
let v = validate_token::<CapabilityProvider>(token)?;
ensure!(!v.expired, "token expired at `{}`", v.expires_human);
ensure!(
!v.cannot_use_yet,
"token cannot be used before `{}`",
v.not_before_human
);
ensure!(v.signature_valid, "signature is not valid");
Ok(())
}
fn valid_host(token: &str) -> anyhow::Result<()> {
let v = validate_token::<Host>(token)?;
ensure!(!v.expired, "token expired at `{}`", v.expires_human);
ensure!(
!v.cannot_use_yet,
"token cannot be used before `{}`",
v.not_before_human
);
ensure!(v.signature_valid, "signature is not valid");
Ok(())
}
}
#[derive(Serialize, Deserialize)]
pub struct SecretRequest {
pub key: String,
pub field: Option<String>,
pub version: Option<String>,
pub context: Context,
}
#[derive(Serialize, Deserialize, Default)]
pub struct SecretResponse {
#[serde(skip_serializing_if = "Option::is_none")]
pub secret: Option<Secret>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<GetSecretError>,
}
#[derive(Serialize, Deserialize, Default)]
pub struct Secret {
pub version: String,
pub string_secret: Option<String>,
pub binary_secret: Option<Vec<u8>>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SecretConfig {
pub name: String,
pub backend: String,
pub key: String,
pub field: Option<String>,
pub version: Option<String>,
pub policy: Policy,
pub secret_type: String,
}
impl SecretConfig {
pub fn new(
name: String,
backend: String,
key: String,
field: Option<String>,
version: Option<String>,
policy_properties: HashMap<String, serde_json::Value>,
) -> Self {
Self {
name,
backend,
key,
field,
version,
policy: Policy::new(policy_properties),
secret_type: SECRET_TYPE.to_string(),
}
}
pub fn try_into_request(
self,
entity_jwt: &str,
host_jwt: &str,
application_name: Option<&String>,
) -> Result<SecretRequest, anyhow::Error> {
Ok(SecretRequest {
key: self.key,
field: self.field,
version: self.version,
context: Context {
entity_jwt: entity_jwt.to_string(),
host_jwt: host_jwt.to_string(),
application: Application {
name: application_name.cloned(),
policy: serde_json::to_string(&self.policy)
.context("failed to serialize secret policy as string")?,
},
},
})
}
}
impl TryInto<HashMap<String, String>> for SecretConfig {
type Error = anyhow::Error;
fn try_into(self) -> Result<HashMap<String, String>, Self::Error> {
let mut map = HashMap::from([
("name".into(), self.name),
("type".into(), self.secret_type),
("backend".into(), self.backend),
("key".into(), self.key),
]);
if let Some(field) = self.field {
map.insert("field".to_string(), field);
}
if let Some(version) = self.version {
map.insert("version".to_string(), version);
}
map.insert(
"policy".to_string(),
serde_json::to_string(&self.policy).context("failed to serialize policy string")?,
);
Ok(map)
}
}
impl Serialize for SecretConfig {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let field_count = if self.version.is_some() { 6 } else { 5 };
let mut state = serializer.serialize_struct("SecretReference", field_count)?;
state.serialize_field("name", &self.name)?;
state.serialize_field("backend", &self.backend)?;
state.serialize_field("key", &self.key)?;
if let Some(v) = self.version.as_ref() {
state.serialize_field("version", v)?;
}
let policy_json = serde_json::to_string(&self.policy).map_err(serde::ser::Error::custom)?;
state.serialize_field("policy", &policy_json)?;
state.serialize_field("type", &self.secret_type)?;
state.end()
}
}
impl<'de> Deserialize<'de> for SecretConfig {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
struct Helper {
name: String,
backend: String,
key: String,
field: Option<String>,
version: Option<String>,
policy: String,
#[serde(rename = "type")]
ty: String,
}
let helper = Helper::deserialize(deserializer)?;
let policy: Policy =
serde_json::from_str(&helper.policy).map_err(serde::de::Error::custom)?;
Ok(SecretConfig {
name: helper.name,
backend: helper.backend,
key: helper.key,
field: helper.field,
version: helper.version,
policy,
secret_type: helper.ty,
})
}
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
pub struct Policy {
#[serde(rename = "type")]
policy_type: String,
properties: HashMap<String, serde_json::Value>,
}
impl Default for Policy {
fn default() -> Self {
Self {
policy_type: SECRET_POLICY_PROPERTIES_TYPE.to_string(),
properties: Default::default(),
}
}
}
impl Policy {
pub fn new(properties: HashMap<String, serde_json::Value>) -> Self {
Self {
properties,
..Default::default()
}
}
}
#[async_trait]
pub trait SecretsServer {
async fn get(&self, request: SecretRequest) -> Result<SecretResponse, GetSecretError>;
fn server_xkey(&self) -> XKey;
}
#[cfg(test)]
mod test {
use std::collections::HashMap;
#[test]
fn test_secret_config_hashmap_try_into() {
let properties = HashMap::from([(
String::from("key"),
serde_json::Value::String("value".to_string()),
)]);
let secret_config = crate::SecretConfig::new(
"name".to_string(),
"backend".to_string(),
"key".to_string(),
Some("field".to_string()),
Some("version".to_string()),
properties,
);
let map: HashMap<String, String> = secret_config
.clone()
.try_into()
.expect("should be able to convert to hashmap");
assert_eq!(map.get("name"), Some(&secret_config.name));
assert_eq!(map.get("type"), Some(&secret_config.secret_type));
assert_eq!(map.get("backend"), Some(&secret_config.backend));
assert_eq!(map.get("key"), Some(&secret_config.key));
assert_eq!(map.get("field"), secret_config.field.as_ref());
assert_eq!(map.get("version"), secret_config.version.as_ref());
assert_eq!(
map.get("policy"),
Some(
&serde_json::to_string(&secret_config.policy)
.expect("should be able to serialize policy")
)
);
}
}