use std::collections::HashMap;
use anyhow::Context as _;
use serde::{Deserialize, Serialize};
use tracing::{instrument, trace};
use wascap::{jwt, prelude::ClaimsBuilder};
#[derive(Debug, Serialize, Deserialize)]
#[serde(untagged)]
pub(crate) enum StoredClaims {
Component(StoredComponentClaims),
Provider(StoredProviderClaims),
}
#[derive(Debug, Default, Serialize, Deserialize)]
pub(crate) struct StoredComponentClaims {
call_alias: String,
#[serde(alias = "iss")]
issuer: String,
name: String,
#[serde(alias = "rev")]
revision: String,
#[serde(alias = "sub")]
subject: String,
#[serde(deserialize_with = "deserialize_messy_vec")]
tags: Vec<String>,
version: String,
}
#[derive(Debug, Default, Serialize, Deserialize)]
pub(crate) struct StoredProviderClaims {
#[serde(alias = "iss")]
issuer: String,
name: String,
#[serde(alias = "rev")]
revision: String,
#[serde(alias = "sub")]
subject: String,
version: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
config_schema: Option<String>,
}
impl TryFrom<Claims> for StoredClaims {
type Error = anyhow::Error;
fn try_from(claims: Claims) -> Result<Self, Self::Error> {
match claims {
Claims::Component(jwt::Claims {
issuer,
subject,
metadata,
..
}) => {
let jwt::Component {
name,
tags,
rev,
ver,
call_alias,
..
} = metadata.context("no metadata found on component claims")?;
Ok(StoredClaims::Component(StoredComponentClaims {
call_alias: call_alias.unwrap_or_default(),
issuer,
name: name.unwrap_or_default(),
revision: rev.unwrap_or_default().to_string(),
subject,
tags: tags.unwrap_or_default(),
version: ver.unwrap_or_default(),
}))
}
Claims::Provider(jwt::Claims {
issuer,
subject,
metadata,
..
}) => {
let jwt::CapabilityProvider {
name,
rev,
ver,
config_schema,
..
} = metadata.context("no metadata found on provider claims")?;
Ok(StoredClaims::Provider(StoredProviderClaims {
issuer,
name: name.unwrap_or_default(),
revision: rev.unwrap_or_default().to_string(),
subject,
version: ver.unwrap_or_default(),
config_schema: config_schema.map(|schema| schema.to_string()),
}))
}
}
}
}
impl TryFrom<&Claims> for StoredClaims {
type Error = anyhow::Error;
fn try_from(claims: &Claims) -> Result<Self, Self::Error> {
match claims {
Claims::Component(jwt::Claims {
issuer,
subject,
metadata,
..
}) => {
let jwt::Component {
name,
tags,
rev,
ver,
call_alias,
..
} = metadata
.as_ref()
.context("no metadata found on component claims")?;
Ok(StoredClaims::Component(StoredComponentClaims {
call_alias: call_alias.clone().unwrap_or_default(),
issuer: issuer.clone(),
name: name.clone().unwrap_or_default(),
revision: rev.unwrap_or_default().to_string(),
subject: subject.clone(),
tags: tags.clone().unwrap_or_default(),
version: ver.clone().unwrap_or_default(),
}))
}
Claims::Provider(jwt::Claims {
issuer,
subject,
metadata,
..
}) => {
let jwt::CapabilityProvider {
name,
rev,
ver,
config_schema,
..
} = metadata
.as_ref()
.context("no metadata found on provider claims")?;
Ok(StoredClaims::Provider(StoredProviderClaims {
issuer: issuer.clone(),
name: name.clone().unwrap_or_default(),
revision: rev.unwrap_or_default().to_string(),
subject: subject.clone(),
version: ver.clone().unwrap_or_default(),
config_schema: config_schema.as_ref().map(ToString::to_string),
}))
}
}
}
}
#[allow(clippy::implicit_hasher)]
impl From<StoredClaims> for HashMap<String, String> {
fn from(claims: StoredClaims) -> Self {
match claims {
StoredClaims::Component(claims) => HashMap::from([
("call_alias".to_string(), claims.call_alias),
("iss".to_string(), claims.issuer.clone()), ("issuer".to_string(), claims.issuer),
("name".to_string(), claims.name),
("rev".to_string(), claims.revision.clone()), ("revision".to_string(), claims.revision),
("sub".to_string(), claims.subject.clone()), ("subject".to_string(), claims.subject),
("tags".to_string(), claims.tags.join(",")),
("version".to_string(), claims.version),
]),
StoredClaims::Provider(claims) => HashMap::from([
("iss".to_string(), claims.issuer.clone()), ("issuer".to_string(), claims.issuer),
("name".to_string(), claims.name),
("rev".to_string(), claims.revision.clone()), ("revision".to_string(), claims.revision),
("sub".to_string(), claims.subject.clone()), ("subject".to_string(), claims.subject),
("version".to_string(), claims.version),
(
"config_schema".to_string(),
claims.config_schema.unwrap_or_default(),
),
]),
}
}
}
#[allow(clippy::large_enum_variant)] pub(crate) enum Claims {
Component(jwt::Claims<jwt::Component>),
Provider(jwt::Claims<jwt::CapabilityProvider>),
}
impl Claims {
pub(crate) fn subject(&self) -> &str {
match self {
Claims::Component(claims) => &claims.subject,
Claims::Provider(claims) => &claims.subject,
}
}
}
impl From<StoredClaims> for Claims {
fn from(claims: StoredClaims) -> Self {
match claims {
StoredClaims::Component(claims) => {
let name = (!claims.name.is_empty()).then_some(claims.name);
let rev = claims.revision.parse().ok();
let ver = (!claims.version.is_empty()).then_some(claims.version);
let tags = (!claims.tags.is_empty()).then_some(claims.tags);
let call_alias = (!claims.call_alias.is_empty()).then_some(claims.call_alias);
let metadata = jwt::Component {
name,
tags,
rev,
ver,
call_alias,
..Default::default()
};
let claims = ClaimsBuilder::new()
.subject(&claims.subject)
.issuer(&claims.issuer)
.with_metadata(metadata)
.build();
Claims::Component(claims)
}
StoredClaims::Provider(claims) => {
let name = (!claims.name.is_empty()).then_some(claims.name);
let rev = claims.revision.parse().ok();
let ver = (!claims.version.is_empty()).then_some(claims.version);
let config_schema: Option<serde_json::Value> = claims
.config_schema
.and_then(|schema| serde_json::from_str(&schema).ok());
let metadata = jwt::CapabilityProvider {
name,
rev,
ver,
config_schema,
..Default::default()
};
let claims = ClaimsBuilder::new()
.subject(&claims.subject)
.issuer(&claims.issuer)
.with_metadata(metadata)
.build();
Claims::Provider(claims)
}
}
}
}
impl super::Host {
#[instrument(level = "debug", skip_all)]
pub(crate) async fn store_claims(&self, claims: Claims) -> anyhow::Result<()> {
match &claims {
Claims::Component(claims) => {
self.store_component_claims(claims.clone()).await?;
}
Claims::Provider(claims) => {
let mut provider_claims = self.provider_claims.write().await;
provider_claims.insert(claims.subject.clone(), claims.clone());
}
};
let claims: StoredClaims = claims.try_into()?;
let subject = match &claims {
StoredClaims::Component(claims) => &claims.subject,
StoredClaims::Provider(claims) => &claims.subject,
};
let key = format!("CLAIMS_{subject}");
trace!(?claims, ?key, "storing claims");
let bytes = serde_json::to_vec(&claims)
.context("failed to serialize claims")?
.into();
self.data
.put(key, bytes)
.await
.context("failed to put claims")?;
Ok(())
}
}
fn deserialize_messy_vec<'de, D: serde::Deserializer<'de>>(
deserializer: D,
) -> Result<Vec<String>, D::Error> {
MessyVec::deserialize(deserializer).map(|messy_vec| messy_vec.0)
}
struct MessyVec(pub Vec<String>);
struct MessyVecVisitor;
impl<'de> serde::de::Visitor<'de> for MessyVecVisitor {
type Value = MessyVec;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("string or array of strings")
}
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
where
A: serde::de::SeqAccess<'de>,
{
let mut values = Vec::new();
while let Some(value) = seq.next_element()? {
values.push(value);
}
Ok(MessyVec(values))
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(MessyVec(value.split(',').map(String::from).collect()))
}
}
impl<'de> Deserialize<'de> for MessyVec {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::de::Deserializer<'de>,
{
deserializer.deserialize_any(MessyVecVisitor)
}
}