Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update capabilites in line with Ucan 0.9.0/0.10.0 #105

Merged
merged 1 commit into from
Jun 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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:[email protected]": {
/// "msg/receive": [{}],
/// "msg/send": [{ "draft": true }, { "publish": true, "topic": ["foo"]}]
/// }
/// })).unwrap();
///
/// let resource = capabilities.get("mailto:[email protected]").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:[email protected]": {
/// "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:[email protected]", "msg/receive", &json!({}))),
/// Capability::from(("mailto:[email protected]", "msg/send", &json!({ "draft": true }))),
/// Capability::from(("mailto:[email protected]", "msg/send", &json!({ "publish": true, "topic": ["foo"] }))),
/// ]);
/// ```
pub fn iter(&self) -> CapabilitiesIterator {
jsantell marked this conversation as resolved.
Show resolved Hide resolved
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
Loading