From b4a9ebc10f2c76f19ae8ba47c869dc5d7a4bcc7a Mon Sep 17 00:00:00 2001 From: Jordan Santell Date: Thu, 25 May 2023 12:38:46 -0700 Subject: [PATCH] feat!: Update capabilites in line with Ucan 0.9.0/0.10.0. Fixes #22. * Represents capabilities as map-of-maps rather than array of tuples. * Renames 'att' to 'cap' (Ucan spec 0.10.0). * Renames 'Capability' to 'CapabilityView'. * Leaves caveat parsing up to consumer. --- ucan/src/builder.rs | 16 +- ucan/src/capability/data.rs | 190 ++++++++++++++++++++++++ ucan/src/capability/iterator.rs | 54 ------- ucan/src/capability/mod.rs | 4 +- ucan/src/capability/semantics.rs | 243 +++++++++++++++++-------------- ucan/src/chain.rs | 26 ++-- ucan/src/ipld/capability.rs | 50 ------- ucan/src/ipld/mod.rs | 2 - ucan/src/ipld/ucan.rs | 8 +- ucan/src/tests/attenuation.rs | 14 +- ucan/src/tests/builder.rs | 20 ++- ucan/src/tests/helpers.rs | 4 +- ucan/src/tests/ucan.rs | 16 +- ucan/src/ucan.rs | 13 +- 14 files changed, 396 insertions(+), 264 deletions(-) create mode 100644 ucan/src/capability/data.rs delete mode 100644 ucan/src/capability/iterator.rs delete mode 100644 ucan/src/ipld/capability.rs diff --git a/ucan/src/builder.rs b/ucan/src/builder.rs index a06424e9..dc063464 100644 --- a/ucan/src/builder.rs +++ b/ucan/src/builder.rs @@ -1,6 +1,6 @@ use crate::{ capability::{ - proof::ProofDelegationSemantics, Action, Capability, CapabilityIpld, CapabilitySemantics, + proof::ProofDelegationSemantics, Action, Capability, CapabilitySemantics, CapabilityView, Scope, }, crypto::KeyMaterial, @@ -28,7 +28,7 @@ where pub issuer: &'a K, pub audience: String, - pub capabilities: Vec, + pub capabilities: Vec, pub expiration: u64, pub not_before: Option, @@ -79,7 +79,7 @@ where exp: self.expiration, nbf: self.not_before, nnc: nonce, - att: self.capabilities.clone(), + cap: self.capabilities.clone().try_into()?, fct: facts, prf: proofs, }) @@ -115,7 +115,7 @@ where issuer: Option<&'a K>, audience: Option, - capabilities: Vec, + capabilities: Vec, lifetime: Option, expiration: Option, @@ -228,12 +228,12 @@ where /// Claim a capability by inheritance (from an authorizing proof) or /// implicitly by ownership of the resource by this UCAN's issuer - pub fn claiming_capability(mut self, capability: &Capability) -> Self + pub fn claiming_capability(mut self, capability: &CapabilityView) -> Self where S: Scope, A: Action, { - self.capabilities.push(CapabilityIpld::from(capability)); + self.capabilities.push(Capability::from(capability)); self } @@ -248,11 +248,11 @@ where let proof_index = self.proofs.len() - 1; let proof_delegation = ProofDelegationSemantics {}; let capability = - proof_delegation.parse(&format!("prf:{proof_index}"), "ucan/DELEGATE"); + proof_delegation.parse(&format!("prf:{proof_index}"), "ucan/DELEGATE", None); match capability { Some(capability) => { - self.capabilities.push(CapabilityIpld::from(&capability)); + self.capabilities.push(Capability::from(&capability)); } None => warn!("Could not produce delegation capability"), } diff --git a/ucan/src/capability/data.rs b/ucan/src/capability/data.rs new file mode 100644 index 00000000..afeb5e8f --- /dev/null +++ b/ucan/src/capability/data.rs @@ -0,0 +1,190 @@ +use anyhow::anyhow; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use std::{ + collections::{btree_map::Iter as BTreeMapIter, BTreeMap}, + fmt::Debug, + iter::{FlatMap, Map}, + ops::Deref, +}; + +#[derive(Debug, Clone, PartialEq, Eq)] +/// Represents a single, flattened capability containing a resource, ability, and caveats. +pub struct Capability { + pub resource: String, + pub ability: String, + pub caveats: Vec, +} + +impl Capability { + pub fn new(resource: String, ability: String, caveats: Vec) -> Self { + Capability { + resource, + ability, + caveats, + } + } +} + +impl From<(String, String, Vec)> for Capability { + fn from(value: (String, String, Vec)) -> Self { + Capability::new(value.0, value.1, value.2) + } +} + +impl From<(&str, &str, &Vec)> for Capability { + fn from(value: (&str, &str, &Vec)) -> Self { + Capability::new(value.0.to_owned(), value.1.to_owned(), value.2.to_owned()) + } +} + +impl From for (String, String, Vec) { + fn from(value: Capability) -> Self { + (value.resource, value.ability, value.caveats) + } +} + +type MapImpl = BTreeMap; +type MapIter<'a, K, V> = BTreeMapIter<'a, K, V>; +type AbilitiesImpl = MapImpl>; +type CapabilitiesImpl = MapImpl; +type AbilitiesMapClosure<'a> = Box)) -> Capability + 'a>; +type AbilitiesMap<'a> = Map>, AbilitiesMapClosure<'a>>; +type CapabilitiesIterator<'a> = FlatMap< + MapIter<'a, String, AbilitiesImpl>, + AbilitiesMap<'a>, + fn((&'a String, &'a AbilitiesImpl)) -> AbilitiesMap<'a>, +>; + +#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] +/// The [Capabilities] struct contains capability data in resource-map to ability-map +/// form. Use `iter()` to deconstruct this map into a sequence of [Capability] datas. +/// +/// ``` +/// use ucan::capability::{Capabilities, Capability}; +/// use serde_json::json; +/// +/// let capabilities = Capabilities::try_from(&json!({ +/// "example://example.com/private/84MZ7aqwKn7sNiMGsSbaxsEa6EPnQLoKYbXByxNBrCEr": { +/// "wnfs/append": [{}] +/// }, +/// "mailto:username@example.com": { +/// "msg/receive": [{ "max_count": 5 }], +/// "msg/send": [{}] +/// } +/// })).unwrap(); +/// +/// assert_eq!(capabilities.iter().collect::>(), vec![ +/// Capability::from(("example://example.com/private/84MZ7aqwKn7sNiMGsSbaxsEa6EPnQLoKYbXByxNBrCEr", "wnfs/append", &vec![json!({})])), +/// Capability::from(("mailto:username@example.com", "msg/receive", &vec![json!({ "max_count": 5 })])), +/// Capability::from(("mailto:username@example.com", "msg/send", &vec![json!({})])), +/// ]); +/// ``` +pub struct Capabilities(CapabilitiesImpl); + +impl Capabilities { + pub fn iter(&self) -> CapabilitiesIterator { + self.0 + .iter() + .flat_map(|(resource, abilities): (&String, &AbilitiesImpl)| { + abilities + .iter() + .map(Box::new(|(ability, caveats): (&String, &Vec)| { + Capability::new(resource.to_owned(), ability.to_owned(), caveats.to_owned()) + })) + }) + } +} + +impl Deref for Capabilities { + type Target = CapabilitiesImpl; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl TryFrom> for Capabilities { + type Error = anyhow::Error; + fn try_from(value: Vec) -> Result { + let mut resources: CapabilitiesImpl = BTreeMap::new(); + for capability in value.into_iter() { + let (resource_name, ability, caveats) = + <(String, String, Vec)>::from(capability); + + let resource = if let Some(resource) = resources.get_mut(&resource_name) { + resource + } else { + let resource: AbilitiesImpl = BTreeMap::new(); + resources.insert(resource_name.clone(), resource); + resources.get_mut(&resource_name).unwrap() + }; + + for value in caveats.iter() { + if !value.is_object() { + return Err(anyhow!("Caveat must be an object: {}", value)); + } + } + resource.insert(ability, caveats); + } + Capabilities::try_from(resources) + } +} + +impl TryFrom for Capabilities { + type Error = anyhow::Error; + + fn try_from(value: CapabilitiesImpl) -> Result { + for (resource, abilities) in value.iter() { + if abilities.is_empty() { + // 0.10.0: 3.2.6.2 One or more abilities MUST be given for each resource. + return Err(anyhow!("No abilities given for resource: {}", resource)); + } + } + Ok(Capabilities(value)) + } +} + +impl TryFrom<&Value> for Capabilities { + type Error = anyhow::Error; + + fn try_from(value: &Value) -> Result { + let map = value + .as_object() + .ok_or_else(|| anyhow!("Capabilities must be an object."))?; + let mut resources: CapabilitiesImpl = BTreeMap::new(); + + for (key, value) in map.iter() { + let resource = key.to_owned(); + let abilities_object = value + .as_object() + .ok_or_else(|| anyhow!("Abilities must be an object."))?; + + let abilities = { + let mut abilities: AbilitiesImpl = BTreeMap::new(); + for (key, value) in abilities_object.iter() { + let ability = key.to_owned(); + let mut caveats: Vec = vec![]; + + let array = value + .as_array() + .ok_or_else(|| anyhow!("Caveats must be defined as an array."))?; + for value in array.iter() { + if !value.is_object() { + return Err(anyhow!("Caveat must be an object: {}", value)); + } + caveats.push(value.to_owned()); + } + abilities.insert(ability, caveats); + } + abilities + }; + + if resources.insert(resource, abilities).is_some() { + // 0.10.0: 3.2.6.1 Resources MUST be unique and given as URIs. + return Err(anyhow!("Capability resource is not unique: {}", key)); + } + } + + Capabilities::try_from(resources) + } +} diff --git a/ucan/src/capability/iterator.rs b/ucan/src/capability/iterator.rs deleted file mode 100644 index 39951126..00000000 --- a/ucan/src/capability/iterator.rs +++ /dev/null @@ -1,54 +0,0 @@ -use super::{Action, Capability, CapabilityIpld, CapabilitySemantics, Scope}; -use crate::ucan::Ucan; -use std::marker::PhantomData; - -pub struct CapabilityIterator<'a, Semantics, S, A> -where - Semantics: CapabilitySemantics, - S: Scope, - A: Action, -{ - index: usize, - ucan: &'a Ucan, - semantics: &'a Semantics, - capability_type: PhantomData>, -} - -impl<'a, Semantics, S, A> CapabilityIterator<'a, Semantics, S, A> -where - Semantics: CapabilitySemantics, - S: Scope, - A: Action, -{ - pub fn new(ucan: &'a Ucan, semantics: &'a Semantics) -> Self { - CapabilityIterator { - index: 0, - ucan, - semantics, - capability_type: PhantomData::>, - } - } -} - -impl<'a, Semantics, S, A> Iterator for CapabilityIterator<'a, Semantics, S, A> -where - Semantics: CapabilitySemantics, - S: Scope, - A: Action, -{ - type Item = Capability; - - fn next(&mut self) -> Option { - // TODO(#22): Full support for 0.9 and the nb field - while let Some(CapabilityIpld { with, can, .. }) = self.ucan.attenuation().get(self.index) { - self.index += 1; - - match self.semantics.parse(with.as_str(), can.as_str()) { - Some(capability) => return Some(capability), - None => continue, - }; - } - - None - } -} diff --git a/ucan/src/capability/mod.rs b/ucan/src/capability/mod.rs index c34bb6a2..572d1410 100644 --- a/ucan/src/capability/mod.rs +++ b/ucan/src/capability/mod.rs @@ -1,7 +1,7 @@ pub mod proof; -mod iterator; +mod data; mod semantics; -pub use iterator::*; +pub use data::*; pub use semantics::*; diff --git a/ucan/src/capability/semantics.rs b/ucan/src/capability/semantics.rs index 85365d1a..e130e64e 100644 --- a/ucan/src/capability/semantics.rs +++ b/ucan/src/capability/semantics.rs @@ -1,61 +1,9 @@ -use crate::serde::ser_to_lower_case; -use anyhow::anyhow; -use serde::{Deserialize, Serialize}; -use serde_json::Value; +use super::Capability; +use log::warn; +use serde_json::{json, Value}; use std::fmt::Debug; use url::Url; -#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)] -pub struct CapabilityIpld { - pub with: String, - #[serde(serialize_with = "ser_to_lower_case")] - pub can: String, - pub nb: Option, -} - -impl From<&Capability> for CapabilityIpld { - fn from(capability: &Capability) -> Self { - CapabilityIpld { - with: capability.with().to_string(), - can: capability.can().to_string(), - - // TODO(#22): Full support for 0.9 and the nb field - nb: None, - } - } -} - -impl TryFrom<&Value> for CapabilityIpld { - type Error = anyhow::Error; - - fn try_from(value: &Value) -> Result { - match value { - Value::Object(map) => { - let with = map - .get("with") - .ok_or_else(|| anyhow!("Missing 'with' field"))?; - let can = map - .get("can") - .ok_or_else(|| anyhow!("Missing 'can' field"))?; - let nb = map.get("nb").cloned(); - - let with = match with { - Value::String(with) => with.clone(), - _ => return Err(anyhow!("The 'with' field must be a string")), - }; - - let can = match can { - Value::String(can) => can.to_lowercase(), - _ => return Err(anyhow!("The 'can' field must be a string")), - }; - - Ok(CapabilityIpld { with, can, nb }) - } - _ => Err(anyhow!("Not a valid capability: {}", value)), - } - } -} - pub trait Scope: ToString + TryFrom + PartialEq + Clone { fn contains(&self, other: &Self) -> bool; } @@ -63,7 +11,7 @@ pub trait Scope: ToString + TryFrom + PartialEq + Clone { pub trait Action: Ord + TryFrom + ToString + Clone {} #[derive(Clone, Eq, PartialEq)] -pub enum Resource +pub enum ResourceUri where S: Scope, { @@ -71,67 +19,67 @@ where Unscoped, } -impl Resource +impl ResourceUri where S: Scope, { pub fn contains(&self, other: &Self) -> bool { match self { - Resource::Unscoped => true, - Resource::Scoped(scope) => match other { - Resource::Scoped(other_scope) => scope.contains(other_scope), + ResourceUri::Unscoped => true, + ResourceUri::Scoped(scope) => match other { + ResourceUri::Scoped(other_scope) => scope.contains(other_scope), _ => false, }, } } } -impl ToString for Resource +impl ToString for ResourceUri where S: Scope, { fn to_string(&self) -> String { match self { - Resource::Unscoped => "*".into(), - Resource::Scoped(value) => value.to_string(), + ResourceUri::Unscoped => "*".into(), + ResourceUri::Scoped(value) => value.to_string(), } } } #[derive(Clone, Eq, PartialEq)] -pub enum With +pub enum Resource where S: Scope, { - Resource { kind: Resource }, - My { kind: Resource }, - As { did: String, kind: Resource }, + Resource { kind: ResourceUri }, + My { kind: ResourceUri }, + As { did: String, kind: ResourceUri }, } -impl With +impl Resource where S: Scope, { pub fn contains(&self, other: &Self) -> bool { match (self, other) { ( - With::Resource { kind: resource }, - With::Resource { + Resource::Resource { kind: resource }, + Resource::Resource { kind: other_resource, }, ) => resource.contains(other_resource), ( - With::My { kind: resource }, - With::My { + Resource::My { kind: resource }, + Resource::My { kind: other_resource, }, ) => resource.contains(other_resource), ( - With::As { + Resource::As { did, kind: resource, }, - With::As { + Resource::As { did: other_did, kind: other_resource, }, @@ -141,15 +89,15 @@ where } } -impl ToString for With +impl ToString for Resource where S: Scope, { fn to_string(&self) -> String { match self { - With::Resource { kind } => kind.to_string(), - With::My { kind } => format!("my:{}", kind.to_string()), - With::As { did, kind } => format!("as:{did}:{}", kind.to_string()), + Resource::Resource { kind } => kind.to_string(), + Resource::My { kind } => format!("my:{}", kind.to_string()), + Resource::As { did, kind } => format!("as:{did}:{}", kind.to_string()), } } } @@ -162,8 +110,8 @@ where fn parse_scope(&self, scope: &Url) -> Option { S::try_from(scope.clone()).ok() } - fn parse_action(&self, can: &str) -> Option { - A::try_from(String::from(can)).ok() + fn parse_action(&self, ability: &str) -> Option { + A::try_from(String::from(ability)).ok() } fn extract_did(&self, path: &str) -> Option<(String, String)> { @@ -187,84 +135,155 @@ where Some((format!("did:key:{value}"), path_parts.collect())) } - fn parse_resource(&self, with: &Url) -> Option> { - Some(match with.path() { - "*" => Resource::Unscoped, - _ => Resource::Scoped(self.parse_scope(with)?), + fn parse_resource(&self, resource: &Url) -> Option> { + Some(match resource.path() { + "*" => ResourceUri::Unscoped, + _ => ResourceUri::Scoped(self.parse_scope(resource)?), }) } - fn parse(&self, with: &str, can: &str) -> Option> { - let uri = Url::parse(with).ok()?; + fn parse_caveats(&self, caveats: Option<&Vec>) -> Vec { + if let Some(caveats) = caveats { + caveats.to_owned() + } else { + vec![json!({})] + } + } - let resource = match uri.scheme() { - "my" => With::My { + /// Parse a resource and abilities string and a caveats object. + /// The default "no caveats" (`[{}]`) is implied if `None` caveats given. + fn parse( + &self, + resource: &str, + ability: &str, + caveats: Option<&Vec>, + ) -> Option> { + let uri = Url::parse(resource).ok()?; + + let cap_resource = match uri.scheme() { + "my" => Resource::My { kind: self.parse_resource(&uri)?, }, "as" => { - let (did, with) = self.extract_did(uri.path())?; - let with = Url::parse(with.as_str()).ok()?; - - With::As { + let (did, resource) = self.extract_did(uri.path())?; + Resource::As { did, - kind: self.parse_resource(&with)?, + kind: self.parse_resource(&Url::parse(resource.as_str()).ok()?)?, } } - _ => With::Resource { + _ => Resource::Resource { kind: self.parse_resource(&uri)?, }, }; - let action = match self.parse_action(can) { - Some(action) => action, + let cap_ability = match self.parse_action(ability) { + Some(ability) => ability, None => return None, }; - Some(Capability::new(resource, action)) + let cap_caveats = self.parse_caveats(caveats); + + Some(CapabilityView::new_with_caveats( + cap_resource, + cap_ability, + cap_caveats, + )) + } + + fn parse_capability(&self, value: &Capability) -> Option> { + self.parse(&value.resource, &value.ability, Some(&value.caveats)) } } #[derive(Clone, Eq, PartialEq)] -pub struct Capability +pub struct CapabilityView where S: Scope, A: Action, { - pub with: With, - pub can: A, + pub resource: Resource, + pub ability: A, + pub caveats: Vec, } -impl Debug for Capability +impl Debug for CapabilityView where S: Scope, A: Action, { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Capability") - .field("with", &self.with.to_string()) - .field("can", &self.can.to_string()) + .field("resource", &self.resource.to_string()) + .field("ability", &self.ability.to_string()) + .field("caveats", &serde_json::to_string(&self.caveats)) .finish() } } -impl Capability +impl CapabilityView where S: Scope, A: Action, { - pub fn new(with: With, can: A) -> Self { - Capability { with, can } + /// Creates a new [CapabilityView] semantics view over a capability + /// without caveats. + pub fn new(resource: Resource, ability: A) -> Self { + CapabilityView { + resource, + ability, + caveats: vec![json!({})], + } + } + + /// Creates a new [CapabilityView] semantics view over a capability + /// with caveats. Note that an empty caveats array will imply NO + /// capabilities, rendering this capability meaningless. + pub fn new_with_caveats(resource: Resource, ability: A, caveats: Vec) -> Self { + let capability = CapabilityView { + resource, + ability, + caveats, + }; + + // 0.10.0: The caveat array SHOULD NOT be empty, as an empty array means + // "in no case" (which is equivalent to not listing the ability). + if capability.caveats.is_empty() { + warn!( + "Capability's caveats are empty, rendering this capability meaningless: {:#?}", + capability + ); + } + + capability + } + + pub fn enables(&self, other: &CapabilityView) -> bool { + self.resource.contains(&other.resource) && self.ability >= other.ability + } + + pub fn resource(&self) -> &Resource { + &self.resource } - pub fn enables(&self, other: &Capability) -> bool { - self.with.contains(&other.with) && self.can >= other.can + pub fn ability(&self) -> &A { + &self.ability } - pub fn with(&self) -> &With { - &self.with + pub fn caveats(&self) -> &Vec { + &self.caveats } +} - pub fn can(&self) -> &A { - &self.can +impl From<&CapabilityView> for Capability +where + S: Scope, + A: Action, +{ + fn from(value: &CapabilityView) -> Self { + Capability::new( + value.resource.to_string(), + value.ability.to_string(), + value.caveats.to_owned(), + ) } } diff --git a/ucan/src/chain.rs b/ucan/src/chain.rs index dab6c458..f714cb9b 100644 --- a/ucan/src/chain.rs +++ b/ucan/src/chain.rs @@ -1,7 +1,7 @@ use crate::{ capability::{ proof::{ProofDelegationSemantics, ProofSelection}, - Action, Capability, CapabilityIterator, CapabilitySemantics, Resource, Scope, With, + Action, CapabilitySemantics, CapabilityView, Resource, ResourceUri, Scope, }, crypto::did::DidParser, store::UcanJwtStore, @@ -19,7 +19,7 @@ pub struct CapabilityInfo { pub originators: BTreeSet, pub not_before: Option, pub expires_at: u64, - pub capability: Capability, + pub capability: CapabilityView, } impl Debug for CapabilityInfo @@ -75,17 +75,21 @@ impl ProofChain { let mut redelegations = BTreeSet::::new(); - for capability in CapabilityIterator::new(&ucan, &PROOF_DELEGATION_SEMANTICS) { - match capability.with() { - With::Resource { - kind: Resource::Scoped(ProofSelection::All), + for capability in ucan + .capabilities() + .iter() + .filter_map(|cap| PROOF_DELEGATION_SEMANTICS.parse_capability(&cap)) + { + match capability.resource() { + Resource::Resource { + kind: ResourceUri::Scoped(ProofSelection::All), } => { for index in 0..proofs.len() { redelegations.insert(index); } } - With::Resource { - kind: Resource::Scoped(ProofSelection::Index(index)), + Resource::Resource { + kind: ResourceUri::Scoped(ProofSelection::Index(index)), } => { if *index < proofs.len() { redelegations.insert(*index); @@ -211,7 +215,11 @@ impl ProofChain { }) .collect(); - let self_capabilities_iter = CapabilityIterator::new(&self.ucan, semantics); + let self_capabilities_iter = self + .ucan + .capabilities() + .iter() + .map_while(|data| semantics.parse_capability(&data)); // Get the claimed attenuations of this ucan, cross-checking ancestral // attenuations to discover the originating authority diff --git a/ucan/src/ipld/capability.rs b/ucan/src/ipld/capability.rs deleted file mode 100644 index 0e917777..00000000 --- a/ucan/src/ipld/capability.rs +++ /dev/null @@ -1,50 +0,0 @@ -use crate::serde::ser_to_lower_case; -use serde::{Deserialize, Serialize}; -use serde_json::Value; - -#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)] -pub struct CapabilityIpld { - pub with: String, - #[serde(serialize_with = "ser_to_lower_case")] - pub can: String, - pub nb: Option, -} - -#[cfg(test)] -mod tests { - use super::CapabilityIpld; - use crate::tests::helpers::dag_cbor_roundtrip; - use serde_json::json; - - #[cfg(target_arch = "wasm32")] - use wasm_bindgen_test::{wasm_bindgen_test, wasm_bindgen_test_configure}; - - #[cfg(target_arch = "wasm32")] - wasm_bindgen_test_configure!(run_in_browser); - - #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] - #[cfg_attr(not(target_arch = "wasm32"), test)] - fn it_lower_cases_capability_can_field() { - let capability = dag_cbor_roundtrip(&CapabilityIpld { - with: "foo:bar".into(), - can: "Baz".into(), - nb: None, - }) - .unwrap(); - - assert_eq!(capability.can, "baz"); - } - - #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] - #[cfg_attr(not(target_arch = "wasm32"), test)] - fn it_round_trips_a_capability_with_nb() { - let capability = dag_cbor_roundtrip(&CapabilityIpld { - with: "foo:bar".into(), - can: "Baz".into(), - nb: Some(json!({ "foo": "bar" })), - }) - .unwrap(); - - assert_eq!(capability.nb, Some(json!({ "foo": "bar" }))); - } -} diff --git a/ucan/src/ipld/mod.rs b/ucan/src/ipld/mod.rs index f65403ee..d055d81f 100644 --- a/ucan/src/ipld/mod.rs +++ b/ucan/src/ipld/mod.rs @@ -1,9 +1,7 @@ -mod capability; mod principle; mod signature; mod ucan; pub use self::ucan::*; -pub use capability::*; pub use principle::*; pub use signature::*; diff --git a/ucan/src/ipld/ucan.rs b/ucan/src/ipld/ucan.rs index 2750284e..7d376ce7 100644 --- a/ucan/src/ipld/ucan.rs +++ b/ucan/src/ipld/ucan.rs @@ -1,5 +1,5 @@ use crate::{ - capability::CapabilityIpld, + capability::Capabilities, crypto::JwtSignatureAlgorithm, ipld::{Principle, Signature}, serde::Base64Encode, @@ -18,7 +18,7 @@ pub struct UcanIpld { pub aud: Principle, pub s: Signature, - pub att: Vec, + pub cap: Capabilities, pub prf: Option>, pub exp: u64, pub fct: Option>, @@ -53,7 +53,7 @@ impl TryFrom<&Ucan> for UcanIpld { JwtSignatureAlgorithm::from_str(ucan.algorithm())?, ucan.signature(), ))?, - att: ucan.attenuation().clone(), + cap: ucan.capabilities().clone(), prf, exp: *ucan.expires_at(), fct: ucan.facts().clone(), @@ -81,7 +81,7 @@ impl TryFrom<&UcanIpld> for Ucan { exp: value.exp, nbf: value.nbf, nnc: value.nnc.clone(), - att: value.att.clone(), + cap: value.cap.clone(), fct: value.fct.clone(), prf: value .prf diff --git a/ucan/src/tests/attenuation.rs b/ucan/src/tests/attenuation.rs index 6b493e9a..980d1838 100644 --- a/ucan/src/tests/attenuation.rs +++ b/ucan/src/tests/attenuation.rs @@ -22,7 +22,7 @@ pub async fn it_works_with_a_simple_example() { let email_semantics = EmailSemantics {}; let send_email_as_alice = email_semantics - .parse("mailto:alice@email.com", "email/send") + .parse("mailto:alice@email.com", "email/send", None) .unwrap(); let leaf_ucan = UcanBuilder::default() @@ -68,10 +68,10 @@ pub async fn it_works_with_a_simple_example() { let info = capability_infos.get(0).unwrap(); assert_eq!( - info.capability.with().to_string().as_str(), + info.capability.resource().to_string().as_str(), "mailto:alice@email.com", ); - assert_eq!(info.capability.can().to_string().as_str(), "email/send"); + assert_eq!(info.capability.ability().to_string().as_str(), "email/send"); } #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] @@ -82,7 +82,7 @@ pub async fn it_reports_the_first_issuer_in_the_chain_as_originator() { let email_semantics = EmailSemantics {}; let send_email_as_bob = email_semantics - .parse("mailto:bob@email.com".into(), "email/send".into()) + .parse("mailto:bob@email.com".into(), "email/send".into(), None) .unwrap(); let leaf_ucan = UcanBuilder::default() @@ -140,10 +140,10 @@ pub async fn it_finds_the_right_proof_chain_for_the_originator() { let email_semantics = EmailSemantics {}; let send_email_as_bob = email_semantics - .parse("mailto:bob@email.com".into(), "email/send".into()) + .parse("mailto:bob@email.com".into(), "email/send".into(), None) .unwrap(); let send_email_as_alice = email_semantics - .parse("mailto:alice@email.com".into(), "email/send".into()) + .parse("mailto:alice@email.com".into(), "email/send".into(), None) .unwrap(); let leaf_ucan_alice = UcanBuilder::default() @@ -233,7 +233,7 @@ pub async fn it_reports_all_chain_options() { let email_semantics = EmailSemantics {}; let send_email_as_alice = email_semantics - .parse("mailto:alice@email.com".into(), "email/send".into()) + .parse("mailto:alice@email.com".into(), "email/send".into(), None) .unwrap(); let leaf_ucan_alice = UcanBuilder::default() diff --git a/ucan/src/tests/builder.rs b/ucan/src/tests/builder.rs index 9512ac5b..7e145426 100644 --- a/ucan/src/tests/builder.rs +++ b/ucan/src/tests/builder.rs @@ -1,6 +1,6 @@ use crate::{ builder::UcanBuilder, - capability::{CapabilityIpld, CapabilitySemantics}, + capability::{Capabilities, Capability, CapabilitySemantics}, chain::ProofChain, crypto::did::DidParser, store::UcanJwtStore, @@ -37,11 +37,11 @@ async fn it_builds_with_a_simple_example() { let wnfs_semantics = WNFSSemantics {}; let cap_1 = email_semantics - .parse("mailto:alice@gmail.com", "email/send") + .parse("mailto:alice@gmail.com", "email/send", None) .unwrap(); let cap_2 = wnfs_semantics - .parse("wnfs://alice.fission.name/public", "wnfs/super_user") + .parse("wnfs://alice.fission.name/public", "wnfs/super_user", None) .unwrap(); let expiration = now() + 30; @@ -70,9 +70,9 @@ async fn it_builds_with_a_simple_example() { assert_eq!(ucan.facts(), &Some(vec![fact_1, fact_2])); let expected_attenuations = - Vec::from([CapabilityIpld::from(&cap_1), CapabilityIpld::from(&cap_2)]); + Capabilities::try_from(vec![Capability::from(&cap_1), Capability::from(&cap_2)]).unwrap(); - assert_eq!(ucan.attenuation(), &expected_attenuations); + assert_eq!(ucan.capabilities(), &expected_attenuations); assert!(ucan.nonce().is_some()); } @@ -100,7 +100,7 @@ async fn it_prevents_duplicate_proofs() { let wnfs_semantics = WNFSSemantics {}; let parent_cap = wnfs_semantics - .parse("wnfs://alice.fission.name/public", "wnfs/super_user") + .parse("wnfs://alice.fission.name/public", "wnfs/super_user", None) .unwrap(); let identities = Identities::new().await; @@ -116,11 +116,15 @@ async fn it_prevents_duplicate_proofs() { .unwrap(); let attenuated_cap_1 = wnfs_semantics - .parse("wnfs://alice.fission.name/public/Apps", "wnfs/create") + .parse("wnfs://alice.fission.name/public/Apps", "wnfs/create", None) .unwrap(); let attenuated_cap_2 = wnfs_semantics - .parse("wnfs://alice.fission.name/public/Domains", "wnfs/create") + .parse( + "wnfs://alice.fission.name/public/Domains", + "wnfs/create", + None, + ) .unwrap(); let next_ucan = UcanBuilder::default() diff --git a/ucan/src/tests/helpers.rs b/ucan/src/tests/helpers.rs index 55f04ee9..c178a4c0 100644 --- a/ucan/src/tests/helpers.rs +++ b/ucan/src/tests/helpers.rs @@ -15,10 +15,10 @@ where pub async fn scaffold_ucan_builder(identities: &Identities) -> Result> { let email_semantics = EmailSemantics {}; let send_email_as_bob = email_semantics - .parse("mailto:bob@email.com".into(), "email/send".into()) + .parse("mailto:bob@email.com".into(), "email/send".into(), None) .unwrap(); let send_email_as_alice = email_semantics - .parse("mailto:alice@email.com".into(), "email/send".into()) + .parse("mailto:alice@email.com".into(), "email/send".into(), None) .unwrap(); let leaf_ucan_alice = UcanBuilder::default() diff --git a/ucan/src/tests/ucan.rs b/ucan/src/tests/ucan.rs index f41f0def..dd0d6fda 100644 --- a/ucan/src/tests/ucan.rs +++ b/ucan/src/tests/ucan.rs @@ -1,8 +1,9 @@ mod validate { use crate::{ builder::UcanBuilder, + capability::CapabilitySemantics, crypto::did::DidParser, - tests::fixtures::{Identities, SUPPORTED_KEYS}, + tests::fixtures::{EmailSemantics, Identities, SUPPORTED_KEYS}, time::now, ucan::Ucan, }; @@ -76,11 +77,18 @@ mod validate { #[cfg_attr(not(target_arch = "wasm32"), tokio::test)] async fn it_can_be_serialized_as_json() { let identities = Identities::new().await; + + let email_semantics = EmailSemantics {}; + let send_email_as_alice = email_semantics + .parse("mailto:alice@email.com".into(), "email/send".into(), None) + .unwrap(); + let ucan = UcanBuilder::default() .issued_by(&identities.alice_key) .for_audience(identities.bob_did.as_str()) .not_before(now() / 1000) .with_lifetime(30) + .claiming_capability(&send_email_as_alice) .build() .unwrap() .sign() @@ -102,7 +110,11 @@ mod validate { "aud": ucan.audience(), "exp": ucan.expires_at(), "nbf": ucan.not_before(), - "att": [], + "cap": { + "mailto:alice@email.com": { + "email/send": [{}] + } + } }, "signed_data": ucan.signed_data(), "signature": ucan.signature() diff --git a/ucan/src/ucan.rs b/ucan/src/ucan.rs index c31899bc..61a8bd14 100644 --- a/ucan/src/ucan.rs +++ b/ucan/src/ucan.rs @@ -1,5 +1,5 @@ use crate::{ - capability::CapabilityIpld, + capability::Capabilities, crypto::did::DidParser, serde::{Base64Encode, DagJson}, time::now, @@ -33,7 +33,7 @@ pub struct UcanPayload { pub nbf: Option, #[serde(skip_serializing_if = "Option::is_none")] pub nnc: Option, - pub att: Vec, + pub cap: Capabilities, #[serde(skip_serializing_if = "Option::is_none")] pub fct: Option>, #[serde(skip_serializing_if = "Option::is_none")] @@ -171,8 +171,13 @@ impl Ucan { &self.payload.nnc } - pub fn attenuation(&self) -> &Vec { - &self.payload.att + #[deprecated(since = "0.4.0", note = "use `capabilities()`")] + pub fn attenuation(&self) -> &Capabilities { + self.capabilities() + } + + pub fn capabilities(&self) -> &Capabilities { + &self.payload.cap } pub fn facts(&self) -> &Option> {