Skip to content

Commit

Permalink
feat!: Update capabilites in line with UCAN 0.9/0.10 specs (#105)
Browse files Browse the repository at this point in the history
* 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
jsantell committed Jun 27, 2023
1 parent 40290e7 commit 0bdf98f
Show file tree
Hide file tree
Showing 21 changed files with 803 additions and 287 deletions.
36 changes: 23 additions & 13 deletions ucan/src/builder.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
use std::collections::BTreeMap;

use crate::{
capability::{
proof::ProofDelegationSemantics, Action, Capability, CapabilityIpld, CapabilitySemantics,
Scope,
},
capability::{proof::ProofDelegationSemantics, Capability, CapabilitySemantics},
crypto::KeyMaterial,
serde::Base64Encode,
time::now,
Expand All @@ -29,7 +26,7 @@ where
pub issuer: &'a K,
pub audience: String,

pub capabilities: Vec<CapabilityIpld>,
pub capabilities: Vec<Capability>,

pub expiration: Option<u64>,
pub not_before: Option<u64>,
Expand Down Expand Up @@ -80,7 +77,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,
})
Expand Down Expand Up @@ -116,7 +113,7 @@ where
issuer: Option<&'a K>,
audience: Option<String>,

capabilities: Vec<CapabilityIpld>,
capabilities: Vec<Capability>,

lifetime: Option<u64>,
expiration: Option<u64>,
Expand Down Expand Up @@ -231,12 +228,25 @@ 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<S, A>(mut self, capability: &Capability<S, A>) -> Self
pub fn claiming_capability<C>(mut self, capability: C) -> Self
where
S: Scope,
A: Action,
C: Into<Capability>,
{
self.capabilities.push(CapabilityIpld::from(capability));
self.capabilities.push(capability.into());
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<C>(mut self, capabilities: &[C]) -> Self
where
C: Into<Capability> + Clone,
{
let caps: Vec<Capability> = capabilities
.iter()
.map(|c| <C as Into<Capability>>::into(c.to_owned()))
.collect();
self.capabilities.extend(caps);
self
}

Expand All @@ -251,11 +261,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"),
}
Expand Down
86 changes: 86 additions & 0 deletions ucan/src/capability/caveats.rs
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())
}
}
229 changes: 229 additions & 0 deletions ucan/src/capability/data.rs
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)
}
}
Loading

0 comments on commit 0bdf98f

Please sign in to comment.