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