1use crate::RESOURCE_PREFIX;
2use cid::Cid;
3use std::collections::BTreeMap;
4
5use serde::{Deserialize, Serialize};
6use serde_with::{serde_as, DeserializeAs, SerializeAs};
7
8use iri_string::types::UriString;
9use siwe::Message;
10
11use ucan_capabilities_object::{
12 Ability, AbilityNameRef, AbilityNamespaceRef, Capabilities, CapsInner, ConvertError,
13 ConvertResult, NotaBeneCollection,
14};
15
16#[serde_as]
18#[derive(Clone, Debug, Serialize, Deserialize)]
19pub struct Capability<NB> {
20 #[serde(rename = "att")]
22 attenuations: Capabilities<NB>,
23
24 #[serde(rename = "prf")]
26 #[serde_as(as = "Vec<B58Cid>")]
27 proof: Vec<Cid>,
28}
29
30impl<NB> Capability<NB> {
31 pub fn new() -> Self {
33 Self {
34 attenuations: Capabilities::new(),
35 proof: Default::default(),
36 }
37 }
38
39 pub fn can<T, A>(
41 &self,
42 target: T,
43 action: A,
44 ) -> ConvertResult<Option<&NotaBeneCollection<NB>>, UriString, Ability, T, A>
45 where
46 T: TryInto<UriString>,
47 A: TryInto<Ability>,
48 {
49 self.attenuations.can(target, action)
50 }
51
52 pub fn can_do(&self, target: &UriString, action: &Ability) -> Option<&NotaBeneCollection<NB>> {
54 self.attenuations.can_do(target, action)
55 }
56
57 pub fn merge<NB1, NB2>(self, other: Capability<NB1>) -> Capability<NB2>
59 where
60 NB2: From<NB> + From<NB1>,
61 {
62 let (caps, mut proofs) = self.into_inner();
63 for proof in &other.proof {
64 if proofs.contains(proof) {
65 continue;
66 }
67 proofs.push(*proof);
68 }
69
70 Capability {
71 attenuations: caps.merge(other.attenuations),
72 proof: proofs,
73 }
74 }
75
76 pub fn with_action(
78 &mut self,
79 target: UriString,
80 action: Ability,
81 nb: impl IntoIterator<Item = BTreeMap<String, NB>>,
82 ) -> &mut Self {
83 self.attenuations.with_action(target, action, nb);
84 self
85 }
86
87 pub fn with_action_convert<T, A>(
91 &mut self,
92 target: T,
93 action: A,
94 nb: impl IntoIterator<Item = BTreeMap<String, NB>>,
95 ) -> Result<&mut Self, ConvertError<T::Error, A::Error>>
96 where
97 T: TryInto<UriString>,
98 A: TryInto<Ability>,
99 {
100 self.attenuations.with_action_convert(target, action, nb)?;
101 Ok(self)
102 }
103
104 pub fn with_actions(
106 &mut self,
107 target: UriString,
108 abilities: impl IntoIterator<Item = (Ability, impl IntoIterator<Item = BTreeMap<String, NB>>)>,
109 ) -> &mut Self {
110 self.attenuations.with_actions(target, abilities);
111 self
112 }
113
114 pub fn with_actions_convert<T, A, N>(
118 &mut self,
119 target: T,
120 abilities: impl IntoIterator<Item = (A, N)>,
121 ) -> Result<&mut Self, ConvertError<T::Error, A::Error>>
122 where
123 T: TryInto<UriString>,
124 A: TryInto<Ability>,
125 N: IntoIterator<Item = BTreeMap<String, NB>>,
126 {
127 self.attenuations.with_actions_convert(target, abilities)?;
128 Ok(self)
129 }
130
131 pub fn abilities(&self) -> &CapsInner<NB> {
133 self.attenuations.abilities()
134 }
135
136 pub fn abilities_for<T>(
138 &self,
139 target: T,
140 ) -> Result<Option<&BTreeMap<Ability, NotaBeneCollection<NB>>>, T::Error>
141 where
142 T: TryInto<UriString>,
143 {
144 self.attenuations.abilities_for(target)
145 }
146
147 pub fn proof(&self) -> &[Cid] {
149 &self.proof
150 }
151
152 pub fn with_proof(mut self, proof: &Cid) -> Self {
154 if self.proof.contains(proof) {
155 return self;
156 }
157 self.proof.push(*proof);
158 self
159 }
160
161 pub fn with_proofs<'l>(mut self, proofs: impl IntoIterator<Item = &'l Cid>) -> Self {
163 for proof in proofs {
164 if self.proof.contains(proof) {
165 continue;
166 }
167 self.proof.push(*proof);
168 }
169 self
170 }
171
172 fn to_line_groups(
173 &self,
174 ) -> impl Iterator<Item = (&UriString, AbilityNamespaceRef, Vec<AbilityNameRef>)> {
175 self.attenuations
176 .abilities()
177 .iter()
178 .flat_map(|(resource, abilities)| {
179 abilities
181 .iter()
182 .fold(
183 BTreeMap::<AbilityNamespaceRef, Vec<AbilityNameRef>>::new(),
184 |mut map, (ability, _)| {
185 map.entry(ability.namespace())
186 .or_default()
187 .push(ability.name());
188 map
189 },
190 )
191 .into_iter()
192 .map(move |(namespace, names)| (resource, namespace, names))
193 })
194 }
195
196 fn to_statement_lines(&self) -> impl Iterator<Item = String> + '_ {
197 self.to_line_groups().map(|(resource, namespace, names)| {
198 format!(
199 "'{}': {} for '{}'.",
200 namespace,
201 names
202 .iter()
203 .map(|an| format!("'{an}'"))
204 .collect::<Vec<String>>()
205 .join(", "),
206 resource
207 )
208 })
209 }
210
211 pub fn into_inner(self) -> (Capabilities<NB>, Vec<Cid>) {
212 (self.attenuations, self.proof)
213 }
214 pub fn to_statement(&self) -> String {
216 [
217 "I further authorize the stated URI to perform the following actions on my behalf:"
218 .to_string(),
219 self.to_statement_lines()
220 .enumerate()
221 .map(|(n, line)| format!(" ({}) {line}", n + 1))
222 .collect(),
223 ]
224 .concat()
225 }
226}
227
228impl<NB> Capability<NB>
229where
230 NB: Serialize,
231{
232 fn encode(&self) -> Result<String, EncodingError> {
233 serde_jcs::to_vec(self)
234 .map_err(EncodingError::Ser)
235 .map(|bytes| base64::encode_config(bytes, base64::URL_SAFE_NO_PAD))
236 }
237
238 pub fn build_message(&self, mut message: Message) -> Result<Message, EncodingError> {
240 if self.attenuations.abilities().is_empty() {
241 return Ok(message);
242 }
243 let statement = self.to_statement();
244 let encoded: UriString = self.try_into()?;
245 message.resources.push(encoded);
246 let m = message.statement.unwrap_or_default();
247 message.statement = Some(if m.is_empty() {
248 statement
249 } else {
250 format!("{m} {statement}")
251 });
252 Ok(message)
253 }
254}
255
256impl<NB> Capability<NB>
257where
258 NB: for<'a> Deserialize<'a>,
259{
260 pub fn extract_and_verify(message: &Message) -> Result<Option<Self>, VerificationError> {
262 if let Some(c) = Self::extract(message)? {
263 let expected = c.to_statement();
264 match &message.statement {
265 Some(s) if s.ends_with(&expected) => Ok(Some(c)),
266 _ => Err(VerificationError::IncorrectStatement(expected)),
267 }
268 } else {
269 Ok(None)
271 }
272 }
273
274 fn extract(message: &Message) -> Result<Option<Self>, DecodingError> {
275 message
276 .resources
277 .iter()
278 .last()
279 .filter(|u| u.as_str().starts_with(RESOURCE_PREFIX))
280 .map(Self::try_from)
281 .transpose()
282 }
283
284 fn decode(encoded: &str) -> Result<Self, DecodingError> {
285 base64::decode_config(encoded, base64::URL_SAFE_NO_PAD)
286 .map_err(DecodingError::Base64Decode)
287 .and_then(|bytes| serde_json::from_slice(&bytes).map_err(DecodingError::De))
288 }
289}
290
291impl<NB> Default for Capability<NB> {
292 fn default() -> Self {
293 Self::new()
294 }
295}
296
297impl<NB> TryFrom<&UriString> for Capability<NB>
298where
299 NB: for<'a> Deserialize<'a>,
300{
301 type Error = DecodingError;
302 fn try_from(uri: &UriString) -> Result<Self, Self::Error> {
303 uri.as_str()
304 .strip_prefix(RESOURCE_PREFIX)
305 .ok_or_else(|| DecodingError::InvalidResourcePrefix(uri.to_string()))
306 .and_then(Capability::decode)
307 }
308}
309
310impl<NB> TryFrom<&Capability<NB>> for UriString
311where
312 NB: Serialize,
313{
314 type Error = EncodingError;
315 fn try_from(cap: &Capability<NB>) -> Result<Self, Self::Error> {
316 cap.encode()
317 .map(|encoded| format!("{RESOURCE_PREFIX}{encoded}"))
318 .and_then(|s| s.parse().map_err(EncodingError::UriParse))
319 }
320}
321
322#[derive(thiserror::Error, Debug)]
323pub enum DecodingError {
324 #[error(
325 "invalid resource prefix (expected prefix: {}, found: {0})",
326 RESOURCE_PREFIX
327 )]
328 InvalidResourcePrefix(String),
329 #[error("failed to decode base64 capability resource: {0}")]
330 Base64Decode(#[from] base64::DecodeError),
331 #[error("failed to deserialize capability from json: {0}")]
332 De(#[from] serde_json::Error),
333}
334
335#[derive(thiserror::Error, Debug)]
336pub enum EncodingError {
337 #[error("unable to parse capability as a URI: {0}")]
338 UriParse(#[from] iri_string::validate::Error),
339 #[error("failed to serialize capability to json: {0}")]
340 Ser(#[from] serde_json::Error),
341}
342
343#[derive(thiserror::Error, Debug)]
344pub enum VerificationError {
345 #[error("error decoding capabilities: {0}")]
346 Decoding(#[from] DecodingError),
347 #[error("incorrect statement in siwe message, expected to end with: {0}")]
348 IncorrectStatement(String),
349}
350
351struct B58Cid;
352
353impl SerializeAs<Cid> for B58Cid {
354 fn serialize_as<S>(source: &Cid, serializer: S) -> Result<S::Ok, S::Error>
355 where
356 S: serde::Serializer,
357 {
358 serializer.serialize_str(
359 &source
360 .to_string_of_base(cid::multibase::Base::Base58Btc)
361 .map_err(serde::ser::Error::custom)?,
362 )
363 }
364}
365
366impl<'de> DeserializeAs<'de, Cid> for B58Cid {
367 fn deserialize_as<D>(deserializer: D) -> Result<Cid, D::Error>
368 where
369 D: serde::Deserializer<'de>,
370 {
371 use std::str::FromStr;
372 let s = String::deserialize(deserializer)?;
373 if !s.starts_with('z') {
374 return Err(serde::de::Error::custom("non-base58btc encoded Cid"));
375 };
376 Cid::from_str(&s).map_err(serde::de::Error::custom)
377 }
378}
379
380#[cfg(test)]
381mod test {
382 use super::*;
383
384 const JSON_CAP: &str = include_str!("../tests/serialized_cap.json");
385
386 #[test]
387 fn deser() {
388 let cap: Capability<serde_json::Value> = serde_json::from_str(JSON_CAP).unwrap();
389 let reser = serde_jcs::to_string(&cap).unwrap();
390 assert_eq!(JSON_CAP.trim(), reser);
391 }
392}