use crate::RESOURCE_PREFIX;
use cid::Cid;
use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use serde_with::{serde_as, DeserializeAs, SerializeAs};
use iri_string::types::UriString;
use siws::message::SiwsMessage;
use ucan_capabilities_object::{
Ability, AbilityNameRef, AbilityNamespaceRef, Capabilities, CapsInner, ConvertError,
ConvertResult, NotaBeneCollection,
};
#[serde_as]
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Capability<NB> {
#[serde(rename = "att")]
attenuations: Capabilities<NB>,
#[serde(rename = "prf")]
#[serde_as(as = "Vec<B58Cid>")]
proof: Vec<Cid>,
}
impl<NB> Capability<NB> {
pub fn new() -> Self {
Self {
attenuations: Capabilities::new(),
proof: Default::default(),
}
}
pub fn can<T, A>(
&self,
target: T,
action: A,
) -> ConvertResult<Option<&NotaBeneCollection<NB>>, UriString, Ability, T, A>
where
T: TryInto<UriString>,
A: TryInto<Ability>,
{
self.attenuations.can(target, action)
}
pub fn can_do(&self, target: &UriString, action: &Ability) -> Option<&NotaBeneCollection<NB>> {
self.attenuations.can_do(target, action)
}
pub fn merge<NB1, NB2>(self, other: Capability<NB1>) -> Capability<NB2>
where
NB2: From<NB> + From<NB1>,
{
let (caps, mut proofs) = self.into_inner();
for proof in &other.proof {
if proofs.contains(proof) {
continue;
}
proofs.push(*proof);
}
Capability {
attenuations: caps.merge(other.attenuations),
proof: proofs,
}
}
pub fn with_action(
&mut self,
target: UriString,
action: Ability,
nb: impl IntoIterator<Item = BTreeMap<String, NB>>,
) -> &mut Self {
self.attenuations.with_action(target, action, nb);
self
}
pub fn with_action_convert<T, A>(
&mut self,
target: T,
action: A,
nb: impl IntoIterator<Item = BTreeMap<String, NB>>,
) -> Result<&mut Self, ConvertError<T::Error, A::Error>>
where
T: TryInto<UriString>,
A: TryInto<Ability>,
{
self.attenuations.with_action_convert(target, action, nb)?;
Ok(self)
}
pub fn with_actions(
&mut self,
target: UriString,
abilities: impl IntoIterator<Item = (Ability, impl IntoIterator<Item = BTreeMap<String, NB>>)>,
) -> &mut Self {
self.attenuations.with_actions(target, abilities);
self
}
pub fn with_actions_convert<T, A, N>(
&mut self,
target: T,
abilities: impl IntoIterator<Item = (A, N)>,
) -> Result<&mut Self, ConvertError<T::Error, A::Error>>
where
T: TryInto<UriString>,
A: TryInto<Ability>,
N: IntoIterator<Item = BTreeMap<String, NB>>,
{
self.attenuations.with_actions_convert(target, abilities)?;
Ok(self)
}
pub fn abilities(&self) -> &CapsInner<NB> {
self.attenuations.abilities()
}
pub fn abilities_for<T>(
&self,
target: T,
) -> Result<Option<&BTreeMap<Ability, NotaBeneCollection<NB>>>, T::Error>
where
T: TryInto<UriString>,
{
self.attenuations.abilities_for(target)
}
pub fn proof(&self) -> &[Cid] {
&self.proof
}
pub fn with_proof(mut self, proof: &Cid) -> Self {
if self.proof.contains(proof) {
return self;
}
self.proof.push(*proof);
self
}
pub fn with_proofs<'l>(mut self, proofs: impl IntoIterator<Item = &'l Cid>) -> Self {
for proof in proofs {
if self.proof.contains(proof) {
continue;
}
self.proof.push(*proof);
}
self
}
fn to_line_groups(
&self,
) -> impl Iterator<Item = (&UriString, AbilityNamespaceRef, Vec<AbilityNameRef>)> {
self.attenuations
.abilities()
.iter()
.flat_map(|(resource, abilities)| {
abilities
.iter()
.fold(
BTreeMap::<AbilityNamespaceRef, Vec<AbilityNameRef>>::new(),
|mut map, (ability, _)| {
map.entry(ability.namespace())
.or_default()
.push(ability.name());
map
},
)
.into_iter()
.map(move |(namespace, names)| (resource, namespace, names))
})
}
fn to_statement_lines(&self) -> impl Iterator<Item = String> + '_ {
self.to_line_groups().map(|(resource, namespace, names)| {
format!(
"'{}': {} for '{}'.",
namespace,
names
.iter()
.map(|an| format!("'{an}'"))
.collect::<Vec<String>>()
.join(", "),
resource
)
})
}
pub fn into_inner(self) -> (Capabilities<NB>, Vec<Cid>) {
(self.attenuations, self.proof)
}
pub fn to_statement(&self) -> String {
[
"I further authorize the stated URI to perform the following actions on my behalf:"
.to_string(),
self.to_statement_lines()
.enumerate()
.map(|(n, line)| format!(" ({}) {line}", n + 1))
.collect(),
]
.concat()
}
}
impl<NB> Capability<NB>
where
NB: Serialize,
{
fn encode(&self) -> Result<String, EncodingError> {
serde_jcs::to_vec(self)
.map_err(EncodingError::Ser)
.map(|bytes| base64::encode_config(bytes, base64::URL_SAFE_NO_PAD))
}
pub fn build_message(&self, mut message: SiwsMessage) -> Result<SiwsMessage, EncodingError> {
if self.attenuations.abilities().is_empty() {
return Ok(message);
}
let statement = self.to_statement();
let encoded: UriString = self.try_into()?;
message.resources.push(encoded);
let m = message.statement.unwrap_or_default();
message.statement = Some(if m.is_empty() {
statement
} else {
format!("{m} {statement}")
});
Ok(message)
}
}
impl<NB> Capability<NB>
where
NB: for<'a> Deserialize<'a>,
{
pub fn extract_and_verify(message: &SiwsMessage) -> Result<Option<Self>, VerificationError> {
if let Some(c) = Self::extract(message)? {
let expected = c.to_statement();
match &message.statement {
Some(s) if s.ends_with(&expected) => Ok(Some(c)),
_ => Err(VerificationError::IncorrectStatement(expected)),
}
} else {
Ok(None)
}
}
fn extract(message: &SiwsMessage) -> Result<Option<Self>, DecodingError> {
message
.resources
.iter()
.last()
.filter(|u| u.as_str().starts_with(RESOURCE_PREFIX))
.map(Self::try_from)
.transpose()
}
fn decode(encoded: &str) -> Result<Self, DecodingError> {
base64::decode_config(encoded, base64::URL_SAFE_NO_PAD)
.map_err(DecodingError::Base64Decode)
.and_then(|bytes| serde_json::from_slice(&bytes).map_err(DecodingError::De))
}
}
impl<NB> Default for Capability<NB> {
fn default() -> Self {
Self::new()
}
}
impl<NB> TryFrom<&UriString> for Capability<NB>
where
NB: for<'a> Deserialize<'a>,
{
type Error = DecodingError;
fn try_from(uri: &UriString) -> Result<Self, Self::Error> {
uri.as_str()
.strip_prefix(RESOURCE_PREFIX)
.ok_or_else(|| DecodingError::InvalidResourcePrefix(uri.to_string()))
.and_then(Capability::decode)
}
}
impl<NB> TryFrom<&Capability<NB>> for UriString
where
NB: Serialize,
{
type Error = EncodingError;
fn try_from(cap: &Capability<NB>) -> Result<Self, Self::Error> {
cap.encode()
.map(|encoded| format!("{RESOURCE_PREFIX}{encoded}"))
.and_then(|s| s.parse().map_err(EncodingError::UriParse))
}
}
#[derive(thiserror::Error, Debug)]
pub enum DecodingError {
#[error(
"invalid resource prefix (expected prefix: {}, found: {0})",
RESOURCE_PREFIX
)]
InvalidResourcePrefix(String),
#[error("failed to decode base64 capability resource: {0}")]
Base64Decode(#[from] base64::DecodeError),
#[error("failed to deserialize capability from json: {0}")]
De(#[from] serde_json::Error),
}
#[derive(thiserror::Error, Debug)]
pub enum EncodingError {
#[error("unable to parse capability as a URI: {0}")]
UriParse(#[from] iri_string::validate::Error),
#[error("failed to serialize capability to json: {0}")]
Ser(#[from] serde_json::Error),
}
#[derive(thiserror::Error, Debug)]
pub enum VerificationError {
#[error("error decoding capabilities: {0}")]
Decoding(#[from] DecodingError),
#[error("incorrect statement in siwe message, expected to end with: {0}")]
IncorrectStatement(String),
}
struct B58Cid;
impl SerializeAs<Cid> for B58Cid {
fn serialize_as<S>(source: &Cid, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(
&source
.to_string_of_base(cid::multibase::Base::Base58Btc)
.map_err(serde::ser::Error::custom)?,
)
}
}
impl<'de> DeserializeAs<'de, Cid> for B58Cid {
fn deserialize_as<D>(deserializer: D) -> Result<Cid, D::Error>
where
D: serde::Deserializer<'de>,
{
use std::str::FromStr;
let s = String::deserialize(deserializer)?;
if !s.starts_with('z') {
return Err(serde::de::Error::custom("non-base58btc encoded Cid"));
};
Cid::from_str(&s).map_err(serde::de::Error::custom)
}
}
#[cfg(test)]
mod test {
use super::*;
const JSON_CAP: &str = include_str!("../tests/serialized_cap.json");
#[test]
fn deser() {
let cap: Capability<serde_json::Value> = serde_json::from_str(JSON_CAP).unwrap();
let reser = serde_jcs::to_string(&cap).unwrap();
assert_eq!(JSON_CAP.trim(), reser);
}
}