Skip to content

Commit

Permalink
fix(host)!: update format for serialized claims
Browse files Browse the repository at this point in the history
Signed-off-by: Connor Smith <[email protected]>
  • Loading branch information
connorsmith256 committed Nov 25, 2023
1 parent 28c5f14 commit afcc501
Show file tree
Hide file tree
Showing 2 changed files with 192 additions and 88 deletions.
264 changes: 180 additions & 84 deletions crates/host/src/wasmbus/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,11 @@ use ulid::Ulid;
use uuid::Uuid;
use wascap::{jwt, prelude::ClaimsBuilder};
use wasmcloud_control_interface::{
ActorAuctionAck, ActorAuctionRequest, ActorDescription, HostInventory, LinkDefinition,
LinkDefinitionList, ProviderAuctionAck, ProviderAuctionRequest, ProviderDescription,
RegistryCredential, RegistryCredentialMap, RemoveLinkDefinitionRequest, ScaleActorCommand,
StartProviderCommand, StopActorCommand, StopHostCommand, StopProviderCommand,
UpdateActorCommand,
ActorAuctionAck, ActorAuctionRequest, ActorDescription, GetClaimsResponse, HostInventory,
LinkDefinition, LinkDefinitionList, ProviderAuctionAck, ProviderAuctionRequest,
ProviderDescription, RegistryCredential, RegistryCredentialMap, RemoveLinkDefinitionRequest,
ScaleActorCommand, StartProviderCommand, StopActorCommand, StopHostCommand,
StopProviderCommand, UpdateActorCommand,
};
use wasmcloud_core::chunking::{ChunkEndpoint, CHUNK_RPC_EXTRA_TIME, CHUNK_THRESHOLD_BYTES};
use wasmcloud_core::{
Expand Down Expand Up @@ -1599,50 +1599,52 @@ impl Claims {

impl From<StoredClaims> for Claims {
fn from(claims: StoredClaims) -> Self {
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);

// rely on the fact that serialized actor claims don't include a contract_id
if claims.contract_id.is_empty() {
let tags =
(!claims.tags.is_empty()).then(|| claims.tags.split(',').map(Into::into).collect());
let caps = (!claims.capabilities.is_empty())
.then(|| claims.capabilities.split(',').map(Into::into).collect());
let call_alias = (!claims.call_alias.is_empty()).then_some(claims.call_alias);
let metadata = jwt::Actor {
name,
tags,
caps,
rev,
ver,
call_alias,
..Default::default()
};
let claims = ClaimsBuilder::new()
.subject(&claims.subject)
.issuer(&claims.issuer)
.with_metadata(metadata)
.build();
Claims::Actor(claims)
} else {
let config_schema: Option<serde_json::Value> = claims
.config_schema
.and_then(|schema| serde_json::from_str(&schema).ok());
let metadata = jwt::CapabilityProvider {
name,
capid: claims.contract_id,
rev,
ver,
config_schema,
..Default::default()
};
let claims = ClaimsBuilder::new()
.subject(&claims.subject)
.issuer(&claims.issuer)
.with_metadata(metadata)
.build();
Claims::Provider(claims)
match claims {
StoredClaims::Actor(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(|| claims.tags);
let caps = (!claims.capabilities.is_empty()).then(|| claims.capabilities);
let call_alias = (!claims.call_alias.is_empty()).then_some(claims.call_alias);
let metadata = jwt::Actor {
name,
tags,
caps,
rev,
ver,
call_alias,
..Default::default()
};
let claims = ClaimsBuilder::new()
.subject(&claims.subject)
.issuer(&claims.issuer)
.with_metadata(metadata)
.build();
Claims::Actor(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,
capid: claims.contract_id,
rev,
ver,
config_schema,
..Default::default()
};
let claims = ClaimsBuilder::new()
.subject(&claims.subject)
.issuer(&claims.issuer)
.with_metadata(metadata)
.build();
Claims::Provider(claims)
}
}
}
}
Expand Down Expand Up @@ -3640,25 +3642,21 @@ impl Host {

#[instrument(level = "debug", skip_all)]
async fn handle_claims(&self) -> anyhow::Result<Bytes> {
// TODO: update control interface client to have a more specific type definition for
// GetClaimsResponse, so we can re-use it here. Currently it's Vec<HashMap<String, String>>
#[derive(Serialize)]
struct ClaimsResponse {
claims: Vec<StoredClaims>,
}

trace!("handling claims");

let (actor_claims, provider_claims) =
join!(self.actor_claims.read(), self.provider_claims.read());
let actor_claims = actor_claims.values().cloned().map(Claims::Actor);
let provider_claims = provider_claims.values().cloned().map(Claims::Provider);
let claims = actor_claims
let claims: Vec<StoredClaims> = actor_claims
.chain(provider_claims)
.flat_map(TryFrom::try_from)
.collect();
let res = serde_json::to_vec(&ClaimsResponse { claims })
.context("failed to serialize response")?;

let res = serde_json::to_vec(&GetClaimsResponse {
claims: claims.into_iter().map(|c| c.into()).collect(),
})
.context("failed to serialize response")?;
Ok(res.into())
}

Expand Down Expand Up @@ -4014,7 +4012,11 @@ impl Host {
}
};
let claims: StoredClaims = claims.try_into()?;
let key = format!("CLAIMS_{}", claims.subject);
let subject = match &claims {
StoredClaims::Actor(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)
Expand Down Expand Up @@ -4351,21 +4353,41 @@ impl Host {
}
}

// TODO: use a better format https://github.com/wasmCloud/wasmCloud/issues/508
// TODO: remove StoredClaims in #1093
#[derive(Debug, Serialize, Deserialize)]
#[serde(untagged)]
enum StoredClaims {
Actor(StoredActorClaims),
Provider(StoredProviderClaims),
}

#[derive(Debug, Default, Serialize, Deserialize)]
struct StoredClaims {
struct StoredActorClaims {
call_alias: String,
#[serde(rename = "caps")]
capabilities: String,
#[serde(alias = "caps", deserialize_with = "deserialize_messy_vec")]
capabilities: Vec<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)]
struct StoredProviderClaims {
contract_id: String,
#[serde(rename = "iss")]
#[serde(alias = "iss")]
issuer: String,
name: String,
#[serde(rename = "rev")]
#[serde(alias = "rev")]
revision: String,
#[serde(rename = "sub")]
#[serde(alias = "sub")]
subject: String,
tags: String,
version: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
config_schema: Option<String>,
Expand All @@ -4391,17 +4413,16 @@ impl TryFrom<Claims> for StoredClaims {
call_alias,
..
} = metadata.context("no metadata found on actor claims")?;
Ok(StoredClaims {
Ok(StoredClaims::Actor(StoredActorClaims {
call_alias: call_alias.unwrap_or_default(),
capabilities: caps.unwrap_or_default().join(","),
capabilities: caps.unwrap_or_default(),
issuer,
name: name.unwrap_or_default(),
revision: rev.unwrap_or_default().to_string(),
subject,
tags: tags.unwrap_or_default().join(","),
tags: tags.unwrap_or_default(),
version: ver.unwrap_or_default(),
..Default::default()
})
}))
}
Claims::Provider(jwt::Claims {
issuer,
Expand All @@ -4417,16 +4438,15 @@ impl TryFrom<Claims> for StoredClaims {
config_schema,
..
} = metadata.context("no metadata found on provider claims")?;
Ok(StoredClaims {
Ok(StoredClaims::Provider(StoredProviderClaims {
contract_id,
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()),
..Default::default()
})
}))
}
}
}
Expand Down Expand Up @@ -4454,17 +4474,16 @@ impl TryFrom<&Claims> for StoredClaims {
} = metadata
.as_ref()
.context("no metadata found on actor claims")?;
Ok(StoredClaims {
Ok(StoredClaims::Actor(StoredActorClaims {
call_alias: call_alias.clone().unwrap_or_default(),
capabilities: caps.clone().unwrap_or_default().join(","),
capabilities: caps.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().join(","),
tags: tags.clone().unwrap_or_default(),
version: ver.clone().unwrap_or_default(),
..Default::default()
})
}))
}
Claims::Provider(jwt::Claims {
issuer,
Expand All @@ -4482,21 +4501,98 @@ impl TryFrom<&Claims> for StoredClaims {
} = metadata
.as_ref()
.context("no metadata found on provider claims")?;
Ok(StoredClaims {
Ok(StoredClaims::Provider(StoredProviderClaims {
contract_id: contract_id.clone(),
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),
..Default::default()
})
}))
}
}
}
}

impl From<StoredClaims> for HashMap<String, String> {
fn from(claims: StoredClaims) -> Self {
match claims {
StoredClaims::Actor(claims) => HashMap::from([
("call_alias".to_string(), claims.call_alias),
("capabilities".to_string(), claims.capabilities.join(",")),
("issuer".to_string(), claims.issuer),
("name".to_string(), claims.name),
("revision".to_string(), claims.revision),
("subject".to_string(), claims.subject),
("tags".to_string(), claims.tags.join(",")),
("version".to_string(), claims.version),
]),
StoredClaims::Provider(claims) => HashMap::from([
("contract_id".to_string(), claims.contract_id),
("issuer".to_string(), claims.issuer),
("name".to_string(), claims.name),
("revision".to_string(), claims.revision),
("subject".to_string(), claims.subject),
("version".to_string(), claims.version),
(
"config_schema".to_string(),
claims.config_schema.unwrap_or_default(),
),
]),
}
}
}

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)
}
// Helper struct to deserialize either a comma-delimited string or an actual array of strings
struct MessyVec(pub Vec<String>);

struct MessyVecVisitor;

// Since this is "temporary" code to preserve backwards compatibility with already-serialized claims,
// we use fully-qualified names instead of importing
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)
}
}

fn human_friendly_uptime(uptime: Duration) -> String {
// strip sub-seconds, then convert to human-friendly format
humantime::format_duration(
Expand Down
Loading

0 comments on commit afcc501

Please sign in to comment.