-
Notifications
You must be signed in to change notification settings - Fork 15
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat!: Update capabilites in line with UCAN 0.9/0.10 specs (#105)
* Represents capabilities as map-of-maps rather than array of tuples. * Validates caveats in proof chain * Renames 'att' to 'cap' (Ucan spec 0.10.0). * Renames various capability semantics structs with spec names (With=>Resource, Can/Action=>Ability, Resource=>ResourceURI) * Renames 'Capability' to 'CapabilityView'.
- Loading branch information
Showing
21 changed files
with
803 additions
and
287 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
use std::ops::Deref; | ||
|
||
use anyhow::{anyhow, Error, Result}; | ||
use serde_json::{Map, Value}; | ||
|
||
#[derive(Clone)] | ||
pub struct Caveat(Map<String, Value>); | ||
|
||
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<String, Value>; | ||
fn deref(&self) -> &Self::Target { | ||
&self.0 | ||
} | ||
} | ||
|
||
impl PartialEq for Caveat { | ||
fn eq(&self, other: &Caveat) -> bool { | ||
self.0 == other.0 | ||
} | ||
} | ||
|
||
impl TryFrom<Value> for Caveat { | ||
type Error = Error; | ||
fn try_from(value: Value) -> Result<Caveat> { | ||
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> { | ||
Caveat::try_from(value.to_owned()) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,229 @@ | ||
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<&Capability> for Capability { | ||
fn from(value: &Capability) -> Self { | ||
value.to_owned() | ||
} | ||
} | ||
|
||
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<Capability> for (String, String, Value) { | ||
fn from(value: Capability) -> Self { | ||
(value.resource, value.ability, value.caveat) | ||
} | ||
} | ||
|
||
type MapImpl<K, V> = BTreeMap<K, V>; | ||
type MapIter<'a, K, V> = BTreeMapIter<'a, K, V>; | ||
type AbilitiesImpl = MapImpl<String, Vec<Value>>; | ||
type CapabilitiesImpl = MapImpl<String, AbilitiesImpl>; | ||
type AbilitiesMapClosure<'a> = Box<dyn Fn((&'a String, &'a Vec<Value>)) -> Vec<Capability> + 'a>; | ||
type AbilitiesMap<'a> = | ||
FlatMap<MapIter<'a, String, Vec<Value>>, Vec<Capability>, 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, matching the | ||
/// [spec](https://github.com/ucan-wg/spec#326-capabilities--attenuation). | ||
/// 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>>(), 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<Value>)| 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<Vec<Capability>> for Capabilities { | ||
type Error = anyhow::Error; | ||
fn try_from(value: Vec<Capability>) -> Result<Self, Self::Error> { | ||
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<CapabilitiesImpl> for Capabilities { | ||
type Error = anyhow::Error; | ||
|
||
fn try_from(value: CapabilitiesImpl) -> Result<Self, Self::Error> { | ||
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<Self, Self::Error> { | ||
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<Value> = 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) | ||
} | ||
} |
Oops, something went wrong.