diff --git a/ucan/src/builder.rs b/ucan/src/builder.rs index 0a01daa5..653554d6 100644 --- a/ucan/src/builder.rs +++ b/ucan/src/builder.rs @@ -2,7 +2,7 @@ use std::collections::BTreeMap; use crate::{ capability::{ - proof::ProofDelegationSemantics, Action, Capability, CapabilityIpld, CapabilitySemantics, + proof::ProofDelegationSemantics, Ability, Capability, CapabilitySemantics, CapabilityView, Scope, }, crypto::KeyMaterial, @@ -29,7 +29,7 @@ where pub issuer: &'a K, pub audience: String, - pub capabilities: Vec, + pub capabilities: Vec, pub expiration: Option, pub not_before: Option, @@ -80,7 +80,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, }) @@ -116,7 +116,7 @@ where issuer: Option<&'a K>, audience: Option, - capabilities: Vec, + capabilities: Vec, lifetime: Option, expiration: Option, @@ -231,12 +231,26 @@ 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, + A: Ability, { - self.capabilities.push(CapabilityIpld::from(capability)); + self.capabilities.push(Capability::from(capability)); + self + } + + /// 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_from_data(mut self, capability: &Capability) -> Self { + self.capabilities.push(capability.to_owned()); + self + } + + /// Claim capabilities by inheritance (from an authorizing proof) or + /// implicitly by ownership of the resource by this UCAN's issuer + pub fn claiming_capabilities_from_data(mut self, capabilities: &Vec) -> Self { + self.capabilities.extend(capabilities.to_owned()); self } @@ -251,11 +265,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/caveats.rs b/ucan/src/capability/caveats.rs new file mode 100644 index 00000000..e7d508a3 --- /dev/null +++ b/ucan/src/capability/caveats.rs @@ -0,0 +1,86 @@ +use std::ops::Deref; + +use anyhow::{anyhow, Error, Result}; +use serde_json::{Map, Value}; + +#[derive(Clone)] +pub struct Caveat(Map); + +impl Caveat { + /// Determines if this [Caveat] enables/allows the provided caveat. + /// + /// ``` + /// use ucan::capability::{Caveat}; + /// use serde_json::json; + /// + /// let no_caveat = Caveat::try_from(json!({})).unwrap(); + /// let x_caveat = Caveat::try_from(json!({ "x": true })).unwrap(); + /// let x_diff_caveat = Caveat::try_from(json!({ "x": false })).unwrap(); + /// let y_caveat = Caveat::try_from(json!({ "y": true })).unwrap(); + /// let xz_caveat = Caveat::try_from(json!({ "x": true, "z": true })).unwrap(); + /// + /// assert!(no_caveat.enables(&no_caveat)); + /// assert!(x_caveat.enables(&x_caveat)); + /// assert!(no_caveat.enables(&x_caveat)); + /// assert!(x_caveat.enables(&xz_caveat)); + /// + /// assert!(!x_caveat.enables(&x_diff_caveat)); + /// assert!(!x_caveat.enables(&no_caveat)); + /// assert!(!x_caveat.enables(&y_caveat)); + /// ``` + pub fn enables(&self, other: &Caveat) -> bool { + if self.is_empty() { + return true; + } + + if other.is_empty() { + return false; + } + + if self == other { + return true; + } + + for (key, value) in self.iter() { + if let Some(other_value) = other.get(key) { + if value != other_value { + return false; + } + } else { + return false; + } + } + + true + } +} + +impl Deref for Caveat { + type Target = Map; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl PartialEq for Caveat { + fn eq(&self, other: &Caveat) -> bool { + self.0 == other.0 + } +} + +impl TryFrom for Caveat { + type Error = Error; + fn try_from(value: Value) -> Result { + Ok(Caveat(match value { + Value::Object(obj) => obj, + _ => return Err(anyhow!("Caveat must be an object")), + })) + } +} + +impl TryFrom<&Value> for Caveat { + type Error = Error; + fn try_from(value: &Value) -> Result { + Caveat::try_from(value.to_owned()) + } +} diff --git a/ucan/src/capability/data.rs b/ucan/src/capability/data.rs new file mode 100644 index 00000000..09ee4d81 --- /dev/null +++ b/ucan/src/capability/data.rs @@ -0,0 +1,222 @@ +use anyhow::anyhow; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use std::{ + collections::{btree_map::Iter as BTreeMapIter, BTreeMap}, + fmt::Debug, + iter::FlatMap, + ops::Deref, +}; + +#[derive(Debug, Clone, PartialEq, Eq)] +/// Represents a single, flattened capability containing a resource, ability, and caveat. +pub struct Capability { + pub resource: String, + pub ability: String, + pub caveat: Value, +} + +impl Capability { + pub fn new(resource: String, ability: String, caveat: Value) -> Self { + Capability { + resource, + ability, + caveat, + } + } +} + +impl From<(String, String, Value)> for Capability { + fn from(value: (String, String, Value)) -> Self { + Capability::new(value.0, value.1, value.2) + } +} + +impl From<(&str, &str, &Value)> for Capability { + fn from(value: (&str, &str, &Value)) -> Self { + Capability::new(value.0.to_owned(), value.1.to_owned(), value.2.to_owned()) + } +} + +impl From for (String, String, Value) { + fn from(value: Capability) -> Self { + (value.resource, value.ability, value.caveat) + } +} + +type MapImpl = BTreeMap; +type MapIter<'a, K, V> = BTreeMapIter<'a, K, V>; +type AbilitiesImpl = MapImpl>; +type CapabilitiesImpl = MapImpl; +type AbilitiesMapClosure<'a> = Box)) -> Vec + 'a>; +type AbilitiesMap<'a> = + FlatMap>, Vec, 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 as a map-of-maps, adhering to the spec. +/// See `iter()` to deconstruct this map into a sequence of [Capability] datas. +/// +/// ``` +/// use ucan::capability::Capabilities; +/// use serde_json::json; +/// +/// let capabilities = Capabilities::try_from(&json!({ +/// "mailto:username@example.com": { +/// "msg/receive": [{}], +/// "msg/send": [{ "draft": true }, { "publish": true, "topic": ["foo"]}] +/// } +/// })).unwrap(); +/// +/// let resource = capabilities.get("mailto:username@example.com").unwrap(); +/// assert_eq!(resource.get("msg/receive").unwrap(), &vec![json!({})]); +/// assert_eq!(resource.get("msg/send").unwrap(), &vec![json!({ "draft": true }), json!({ "publish": true, "topic": ["foo"] })]) +/// ``` +pub struct Capabilities(CapabilitiesImpl); + +impl Capabilities { + /// Using a [FlatMap] implementation, iterate over a [Capabilities] map-of-map + /// as 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": [{}], + /// "msg/send": [{ "draft": true }, { "publish": true, "topic": ["foo"]}] + /// } + /// })).unwrap(); + /// + /// assert_eq!(capabilities.iter().collect::>(), vec![ + /// Capability::from(("example://example.com/private/84MZ7aqwKn7sNiMGsSbaxsEa6EPnQLoKYbXByxNBrCEr", "wnfs/append", &json!({}))), + /// Capability::from(("mailto:username@example.com", "msg/receive", &json!({}))), + /// Capability::from(("mailto:username@example.com", "msg/send", &json!({ "draft": true }))), + /// Capability::from(("mailto:username@example.com", "msg/send", &json!({ "publish": true, "topic": ["foo"] }))), + /// ]); + /// ``` + pub fn iter(&self) -> CapabilitiesIterator { + self.0 + .iter() + .flat_map(|(resource, abilities): (&String, &AbilitiesImpl)| { + abilities + .iter() + .flat_map(Box::new( + |(ability, caveats): (&String, &Vec)| match caveats.len() { + 0 => vec![], // An empty caveats list is the same as no capability at all + _ => caveats + .iter() + .map(|caveat| { + Capability::from(( + resource.to_owned(), + ability.to_owned(), + caveat.to_owned(), + )) + }) + .collect(), + }, + )) + }) + } +} + +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, caveat) = <(String, String, Value)>::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() + }; + + if !caveat.is_object() { + return Err(anyhow!("Caveat must be an object: {}", caveat)); + } + + if let Some(ability_vec) = resource.get_mut(&ability) { + ability_vec.push(caveat); + } else { + resource.insert(ability, vec![caveat]); + } + } + 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](https://github.com/ucan-wg/spec#3262-abilities): + // 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 + }; + + resources.insert(resource, abilities); + } + + 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..cde4fe33 100644 --- a/ucan/src/capability/mod.rs +++ b/ucan/src/capability/mod.rs @@ -1,7 +1,9 @@ pub mod proof; -mod iterator; +mod caveats; +mod data; mod semantics; -pub use iterator::*; +pub use caveats::*; +pub use data::*; pub use semantics::*; diff --git a/ucan/src/capability/proof.rs b/ucan/src/capability/proof.rs index 798656c4..986bb578 100644 --- a/ucan/src/capability/proof.rs +++ b/ucan/src/capability/proof.rs @@ -1,4 +1,4 @@ -use super::{Action, CapabilitySemantics, Scope}; +use super::{Ability, CapabilitySemantics, Scope}; use anyhow::{anyhow, Result}; use url::Url; @@ -7,7 +7,7 @@ pub enum ProofAction { Delegate, } -impl Action for ProofAction {} +impl Ability for ProofAction {} impl TryFrom for ProofAction { type Error = anyhow::Error; diff --git a/ucan/src/capability/semantics.rs b/ucan/src/capability/semantics.rs index 85365d1a..65966fb4 100644 --- a/ucan/src/capability/semantics.rs +++ b/ucan/src/capability/semantics.rs @@ -1,69 +1,16 @@ -use crate::serde::ser_to_lower_case; -use anyhow::anyhow; -use serde::{Deserialize, Serialize}; -use serde_json::Value; +use super::{Capability, Caveat}; +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; } -pub trait Action: Ord + TryFrom + ToString + Clone {} +pub trait Ability: Ord + TryFrom + ToString + Clone {} #[derive(Clone, Eq, PartialEq)] -pub enum Resource +pub enum ResourceUri where S: Scope, { @@ -71,67 +18,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 +88,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()), } } } @@ -157,13 +104,13 @@ where pub trait CapabilitySemantics where S: Scope, - A: Action, + A: Ability, { 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 +134,168 @@ 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_caveat(&self, caveat: Option<&Value>) -> Value { + if let Some(caveat) = caveat { + caveat.to_owned() + } else { + 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, + caveat: Option<&Value>, + ) -> 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_caveat = self.parse_caveat(caveat); + + Some(CapabilityView::new_with_caveat( + cap_resource, + cap_ability, + cap_caveat, + )) + } + + fn parse_capability(&self, value: &Capability) -> Option> { + self.parse(&value.resource, &value.ability, Some(&value.caveat)) } } #[derive(Clone, Eq, PartialEq)] -pub struct Capability +pub struct CapabilityView where S: Scope, - A: Action, + A: Ability, { - pub with: With, - pub can: A, + pub resource: Resource, + pub ability: A, + pub caveat: Value, } -impl Debug for Capability +impl Debug for CapabilityView where S: Scope, - A: Action, + A: Ability, { 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.caveat)) .finish() } } -impl Capability +impl CapabilityView where S: Scope, - A: Action, + A: Ability, { - 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, + caveat: 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_caveat(resource: Resource, ability: A, caveat: Value) -> Self { + CapabilityView { + resource, + ability, + caveat, + } + } + + pub fn enables(&self, other: &CapabilityView) -> bool { + match ( + Caveat::try_from(self.caveat()), + Caveat::try_from(other.caveat()), + ) { + (Ok(self_caveat), Ok(other_caveat)) => { + self.resource.contains(&other.resource) + && self.ability >= other.ability + && self_caveat.enables(&other_caveat) + } + _ => false, + } + } + + 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 caveat(&self) -> &Value { + &self.caveat } +} - pub fn can(&self) -> &A { - &self.can +impl From<&CapabilityView> for Capability +where + S: Scope, + A: Ability, +{ + fn from(value: &CapabilityView) -> Self { + Capability::new( + value.resource.to_string(), + value.ability.to_string(), + value.caveat.to_owned(), + ) + } +} + +impl From> for Capability +where + S: Scope, + A: Ability, +{ + fn from(value: CapabilityView) -> Self { + Capability::new( + value.resource.to_string(), + value.ability.to_string(), + value.caveat, + ) } } diff --git a/ucan/src/chain.rs b/ucan/src/chain.rs index 8e8ecf73..285efbba 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, + Ability, CapabilitySemantics, CapabilityView, Resource, ResourceUri, Scope, }, crypto::did::DidParser, store::UcanJwtStore, @@ -15,17 +15,17 @@ use std::{collections::BTreeSet, fmt::Debug}; const PROOF_DELEGATION_SEMANTICS: ProofDelegationSemantics = ProofDelegationSemantics {}; #[derive(Eq, PartialEq)] -pub struct CapabilityInfo { +pub struct CapabilityInfo { pub originators: BTreeSet, pub not_before: Option, pub expires_at: Option, - pub capability: Capability, + pub capability: CapabilityView, } impl Debug for CapabilityInfo where S: Scope, - A: Action, + A: Ability, { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("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); @@ -173,7 +177,7 @@ impl ProofChain { where Semantics: CapabilitySemantics, S: Scope, - A: Action, + A: Ability, { // Get the set of inherited attenuations (excluding redelegations) // before further attenuating by own lifetime and capabilities: @@ -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 3c310337..c98970d5 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, @@ -17,7 +17,7 @@ pub struct UcanIpld { pub aud: Principle, pub s: Signature, - pub att: Vec, + pub cap: Capabilities, pub prf: Option>, pub exp: Option, pub fct: Option, @@ -52,7 +52,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(), @@ -80,7 +80,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/lib.rs b/ucan/src/lib.rs index b020bf79..04c5c8db 100644 --- a/ucan/src/lib.rs +++ b/ucan/src/lib.rs @@ -43,7 +43,7 @@ //! ```rust //! use ucan::{ //! chain::{ProofChain, CapabilityInfo}, -//! capability::{CapabilitySemantics, Scope, Action}, +//! capability::{CapabilitySemantics, Scope, Ability}, //! crypto::did::{DidParser, KeyConstructorSlice}, //! store::UcanJwtStore //! }; @@ -56,7 +56,7 @@ //! where //! Semantics: CapabilitySemantics, //! S: Scope, -//! A: Action, +//! A: Ability, //! Store: UcanJwtStore //! { //! let mut did_parser = DidParser::new(SUPPORTED_KEY_TYPES); diff --git a/ucan/src/tests/attenuation.rs b/ucan/src/tests/attenuation.rs index 6b493e9a..54bb0927 100644 --- a/ucan/src/tests/attenuation.rs +++ b/ucan/src/tests/attenuation.rs @@ -1,13 +1,14 @@ use super::fixtures::{EmailSemantics, Identities, SUPPORTED_KEYS}; use crate::{ builder::UcanBuilder, - capability::CapabilitySemantics, + capability::{Capability, CapabilitySemantics}, chain::{CapabilityInfo, ProofChain}, crypto::did::DidParser, store::{MemoryStore, UcanJwtStore}, }; use std::collections::BTreeSet; +use serde_json::json; #[cfg(target_arch = "wasm32")] use wasm_bindgen_test::{wasm_bindgen_test, wasm_bindgen_test_configure}; @@ -22,7 +23,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 +69,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 +83,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 +141,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 +234,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() @@ -302,3 +303,145 @@ pub async fn it_reports_all_chain_options() { } ); } + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), tokio::test)] +pub async fn it_validates_caveats() -> anyhow::Result<()> { + let resource = "mailto:alice@email.com"; + let ability = "email/send"; + + let no_caveat = Capability::from((resource, ability, &json!({}))); + let x_caveat = Capability::from((resource, ability, &json!({ "x": true }))); + let y_caveat = Capability::from((resource, ability, &json!({ "y": true }))); + let z_caveat = Capability::from((resource, ability, &json!({ "z": true }))); + let yz_caveat = Capability::from((resource, ability, &json!({ "y": true, "z": true }))); + + let valid = [ + (vec![&no_caveat], vec![&no_caveat]), + (vec![&x_caveat], vec![&x_caveat]), + (vec![&no_caveat], vec![&x_caveat]), + (vec![&x_caveat, &y_caveat], vec![&x_caveat]), + (vec![&x_caveat, &y_caveat], vec![&x_caveat, &yz_caveat]), + ]; + + let invalid = [ + (vec![&x_caveat], vec![&no_caveat]), + (vec![&x_caveat], vec![&y_caveat]), + ( + vec![&x_caveat, &y_caveat], + vec![&x_caveat, &y_caveat, &z_caveat], + ), + ]; + + for (proof_capabilities, delegated_capabilities) in valid { + let is_successful = + test_capabilities_delegation(&proof_capabilities, &delegated_capabilities).await?; + assert!( + is_successful, + "{} enables {}", + render_caveats(&proof_capabilities), + render_caveats(&delegated_capabilities) + ); + } + + for (proof_capabilities, delegated_capabilities) in invalid { + let is_successful = + test_capabilities_delegation(&proof_capabilities, &delegated_capabilities).await?; + assert!( + !is_successful, + "{} disallows {}", + render_caveats(&proof_capabilities), + render_caveats(&delegated_capabilities) + ); + } + + fn render_caveats(capabilities: &Vec<&Capability>) -> String { + format!( + "{:?}", + capabilities + .iter() + .map(|cap| cap.caveat.to_string()) + .collect::>() + ) + } + + async fn test_capabilities_delegation( + proof_capabilities: &Vec<&Capability>, + delegated_capabilities: &Vec<&Capability>, + ) -> anyhow::Result { + let identities = Identities::new().await; + let mut did_parser = DidParser::new(SUPPORTED_KEYS); + let email_semantics = EmailSemantics {}; + let mut store = MemoryStore::default(); + let proof_capabilities = proof_capabilities + .to_owned() + .into_iter() + .map(|cap| cap.to_owned()) + .collect::>(); + let delegated_capabilities = delegated_capabilities + .to_owned() + .into_iter() + .map(|cap| cap.to_owned()) + .collect::>(); + + let proof_ucan = UcanBuilder::default() + .issued_by(&identities.alice_key) + .for_audience(identities.mallory_did.as_str()) + .with_lifetime(60) + .claiming_capabilities_from_data(&proof_capabilities) + .build()? + .sign() + .await?; + + let ucan = UcanBuilder::default() + .issued_by(&identities.mallory_key) + .for_audience(identities.alice_did.as_str()) + .with_lifetime(50) + .witnessed_by(&proof_ucan, None) + .claiming_capabilities_from_data(&delegated_capabilities) + .build()? + .sign() + .await?; + store.write_token(&proof_ucan.encode().unwrap()).await?; + store.write_token(&ucan.encode().unwrap()).await?; + + let proof_chain = ProofChain::from_ucan(ucan, None, &mut did_parser, &store).await?; + + Ok(enables_capabilities( + &proof_chain, + &email_semantics, + &identities.alice_did, + &delegated_capabilities, + )) + } + + /// Checks proof chain returning true if all desired capabilities are enabled. + fn enables_capabilities( + proof_chain: &ProofChain, + semantics: &EmailSemantics, + originator: &String, + desired_capabilities: &Vec, + ) -> bool { + let capability_infos = proof_chain.reduce_capabilities(semantics); + + for desired_capability in desired_capabilities { + let mut has_capability = false; + for info in &capability_infos { + if info.originators.contains(originator) + && info + .capability + .enables(&semantics.parse_capability(desired_capability).unwrap()) + { + has_capability = true; + break; + } + } + if !has_capability { + return false; + } + } + true + } + + Ok(()) +} diff --git a/ucan/src/tests/builder.rs b/ucan/src/tests/builder.rs index e1b78a9c..6963c546 100644 --- a/ucan/src/tests/builder.rs +++ b/ucan/src/tests/builder.rs @@ -2,7 +2,7 @@ use std::collections::BTreeMap; use crate::{ builder::UcanBuilder, - capability::{CapabilityIpld, CapabilitySemantics}, + capability::{Capabilities, Capability, CapabilitySemantics}, chain::ProofChain, crypto::did::DidParser, store::UcanJwtStore, @@ -39,11 +39,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; @@ -79,9 +79,9 @@ async fn it_builds_with_a_simple_example() { ); 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()); } @@ -109,7 +109,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; @@ -117,7 +117,7 @@ async fn it_prevents_duplicate_proofs() { .issued_by(&identities.alice_key) .for_audience(identities.bob_did.as_str()) .with_lifetime(30) - .claiming_capability(&parent_cap) + .claiming_capability(&parent_cap.into()) .build() .unwrap() .sign() @@ -125,11 +125,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() @@ -137,8 +141,8 @@ async fn it_prevents_duplicate_proofs() { .for_audience(identities.mallory_did.as_str()) .with_lifetime(30) .witnessed_by(&ucan, None) - .claiming_capability(&attenuated_cap_1) - .claiming_capability(&attenuated_cap_2) + .claiming_capability(&attenuated_cap_1.into()) + .claiming_capability(&attenuated_cap_2.into()) .build() .unwrap() .sign() diff --git a/ucan/src/tests/capability.rs b/ucan/src/tests/capability.rs new file mode 100644 index 00000000..11e9bd2d --- /dev/null +++ b/ucan/src/tests/capability.rs @@ -0,0 +1,91 @@ +use crate::capability::{Capabilities, Capability}; +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_can_cast_between_map_and_sequence() { + let cap_foo = Capability::from(("example://foo", "ability/foo", &json!({}))); + let cap_bar_1 = Capability::from(("example://bar", "ability/bar", &json!({ "beep": 1 }))); + let cap_bar_2 = Capability::from(("example://bar", "ability/bar", &json!({ "boop": 1 }))); + + let cap_sequence = vec![cap_bar_1.clone(), cap_bar_2.clone(), cap_foo]; + let cap_map = Capabilities::try_from(&json!({ + "example://bar": { + "ability/bar": [{ "beep": 1 }, { "boop": 1 }] + }, + "example://foo": { "ability/foo": [{}] }, + })) + .unwrap(); + + assert_eq!( + &cap_map.iter().collect::>(), + &cap_sequence, + "Capabilities map to sequence." + ); + assert_eq!( + &Capabilities::try_from(cap_sequence).unwrap(), + &cap_map, + "Capabilities sequence to map." + ); +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), test)] +fn it_rejects_non_compliant_json() { + let failure_cases = [ + (json!([]), "resources must be map"), + ( + json!({ + "resource:foo": [] + }), + "abilities must be map", + ), + ( + json!({"resource:foo": {}}), + "resource must have at least one ability", + ), + ( + json!({"resource:foo": { "ability/read": {} }}), + "caveats must be array", + ), + ( + json!({"resource:foo": { "ability/read": [1] }}), + "caveat must be object", + ), + ]; + + for (json_data, message) in failure_cases { + assert!(Capabilities::try_from(&json_data).is_err(), "{message}"); + } + + assert!(Capabilities::try_from(&json!({ + "resource:foo": { "ability/read": [{}] } + })) + .is_ok()); +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), test)] +fn it_filters_out_empty_caveats_when_iterating() { + let cap_map = Capabilities::try_from(&json!({ + "example://bar": { "ability/bar": [{}] }, + "example://foo": { "ability/foo": [] } + })) + .unwrap(); + + assert_eq!( + cap_map.iter().collect::>(), + vec![Capability::from(( + "example://bar", + "ability/bar", + &json!({}) + ))], + "iter() filters out capabilities with empty caveats" + ); +} diff --git a/ucan/src/tests/fixtures/capabilities/email.rs b/ucan/src/tests/fixtures/capabilities/email.rs index 55f7878c..d8e44524 100644 --- a/ucan/src/tests/fixtures/capabilities/email.rs +++ b/ucan/src/tests/fixtures/capabilities/email.rs @@ -1,4 +1,4 @@ -use crate::capability::{Action, CapabilitySemantics, Scope}; +use crate::capability::{Ability, CapabilitySemantics, Scope}; use anyhow::{anyhow, Result}; use url::Url; @@ -36,7 +36,7 @@ pub enum EmailAction { Send, } -impl Action for EmailAction {} +impl Ability for EmailAction {} impl ToString for EmailAction { fn to_string(&self) -> String { diff --git a/ucan/src/tests/fixtures/capabilities/wnfs.rs b/ucan/src/tests/fixtures/capabilities/wnfs.rs index 23081deb..4b18a3c6 100644 --- a/ucan/src/tests/fixtures/capabilities/wnfs.rs +++ b/ucan/src/tests/fixtures/capabilities/wnfs.rs @@ -1,4 +1,4 @@ -use crate::capability::{Action, CapabilitySemantics, Scope}; +use crate::capability::{Ability, CapabilitySemantics, Scope}; use anyhow::{anyhow, Result}; use url::Url; @@ -11,7 +11,7 @@ pub enum WNFSCapLevel { SuperUser, } -impl Action for WNFSCapLevel {} +impl Ability for WNFSCapLevel {} impl TryFrom for WNFSCapLevel { type Error = anyhow::Error; diff --git a/ucan/src/tests/helpers.rs b/ucan/src/tests/helpers.rs index 55f04ee9..87046508 100644 --- a/ucan/src/tests/helpers.rs +++ b/ucan/src/tests/helpers.rs @@ -15,17 +15,17 @@ 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() .issued_by(&identities.alice_key) .for_audience(identities.mallory_did.as_str()) .with_expiration(1664232146010) - .claiming_capability(&send_email_as_alice) + .claiming_capability(&send_email_as_alice.clone().into()) .build() .unwrap() .sign() @@ -36,7 +36,7 @@ pub async fn scaffold_ucan_builder(identities: &Identities) -> Result Result Result<()> { 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) .with_fact("abc/challenge", json!({ "foo": "bar" })) + .claiming_capability(&send_email_as_alice.into()) .build()? .sign() .await?; @@ -103,7 +111,11 @@ mod validate { "aud": ucan.audience(), "exp": ucan.expires_at(), "nbf": ucan.not_before(), - "att": [], + "cap": { + "mailto:alice@email.com": { + "email/send": [{}] + } + }, "fct": { "abc/challenge": { "foo": "bar" } } @@ -140,7 +152,7 @@ mod validate { "iss": ucan.issuer(), "aud": ucan.audience(), "exp": serde_json::Value::Null, - "att": [] + "cap": {} }, "signed_data": ucan.signed_data(), "signature": ucan.signature() diff --git a/ucan/src/ucan.rs b/ucan/src/ucan.rs index 281cb6df..beaf8d23 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, @@ -35,7 +35,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")] @@ -179,8 +179,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 {