noosphere_core/authority/
author.rs

1use crate::{
2    authority::{
3        generate_ed25519_key, Authorization, SphereAbility, SPHERE_SEMANTICS, SUPPORTED_KEYS,
4    },
5    data::Did,
6    view::Sphere,
7};
8use anyhow::{anyhow, Result};
9use noosphere_storage::{SphereDb, Storage};
10use ucan::{
11    chain::ProofChain,
12    crypto::{did::DidParser, KeyMaterial},
13};
14use ucan_key_support::ed25519::Ed25519KeyMaterial;
15
16use super::generate_capability;
17
18/// The level of access that a given user has to a related resource. Broadly,
19/// a user will always have either read/write access (to their own sphere) or
20/// else read-only access (to all other spheres).
21#[derive(PartialEq, Eq, Debug, Clone)]
22pub enum Access {
23    /// Read/write access to a sphere
24    ReadWrite,
25    /// Read-only access to a sphere
26    ReadOnly,
27}
28
29/// An author is a user or program who is reading content from and/or writing
30/// content to a sphere. This construct collects the identity and the
31/// authorization of that entity to make it easier to determine their level of
32/// access to the content of a given sphere.
33#[derive(Clone, Debug)]
34pub struct Author<K>
35where
36    K: KeyMaterial + Clone + 'static,
37{
38    /// [KeyMaterial] that the [Author] has access to
39    pub key: K,
40    /// Optional proof of [Authorization] for the associated key
41    pub authorization: Option<Authorization>,
42}
43
44impl Author<Ed25519KeyMaterial> {
45    /// Produces an "anonymous" author who is guaranteed not to have any
46    /// authorization assigned to it prior to being created
47    pub fn anonymous() -> Self {
48        Author {
49            key: generate_ed25519_key(),
50            authorization: None,
51        }
52    }
53}
54
55impl<K> Author<K>
56where
57    K: KeyMaterial + Clone + 'static,
58{
59    /// Resolve the identity of the author
60    pub async fn identity(&self) -> Result<Did> {
61        Ok(Did(self.key.get_did().await?))
62    }
63
64    /// For cases where some kind of authorization is expected, this accessor
65    /// can be used to automatically produce an error result if the
66    /// authorization is not present
67    pub fn require_authorization(&self) -> Result<&Authorization> {
68        self.authorization
69            .as_ref()
70            .ok_or_else(|| anyhow!("Authorization is required but none is configured"))
71    }
72
73    /// Determine the level of access that the author has to a given sphere
74    pub async fn access_to<S>(&self, sphere_identity: &Did, db: &SphereDb<S>) -> Result<Access>
75    where
76        S: Storage,
77    {
78        let author_did = Did(self.key.get_did().await?);
79
80        // Check if this author _is_ the root sphere authority (e.g., when performing surgery on
81        // the authority section of a sphere)
82        if &author_did == sphere_identity {
83            return Ok(Access::ReadWrite);
84        }
85
86        if let Some(authorization) = &self.authorization {
87            let ucan = authorization.as_ucan(db).await?;
88
89            if ucan.audience() != author_did.as_str() {
90                return Ok(Access::ReadOnly);
91            }
92
93            let sphere = Sphere::at(&db.require_version(sphere_identity).await?.into(), db);
94            match sphere.verify_authorization(authorization).await {
95                Ok(_) => (),
96                Err(error) => {
97                    warn!("Could not verify authorization: {}", error);
98                    return Ok(Access::ReadOnly);
99                }
100            };
101
102            let read_write_capability = generate_capability(sphere_identity, SphereAbility::Push);
103
104            let mut did_parser = DidParser::new(SUPPORTED_KEYS);
105            let proof_chain = match ProofChain::from_ucan(ucan, None, &mut did_parser, db).await {
106                Ok(proof_chain) => proof_chain,
107                Err(error) => {
108                    warn!("Could not construct a verified proof chain: {}", error);
109                    return Ok(Access::ReadOnly);
110                }
111            };
112
113            let capability_infos = proof_chain.reduce_capabilities(&SPHERE_SEMANTICS);
114
115            for info in capability_infos {
116                if info.originators.contains(sphere_identity.as_str())
117                    && info.capability.enables(&read_write_capability)
118                {
119                    return Ok(Access::ReadWrite);
120                }
121            }
122        }
123
124        Ok(Access::ReadOnly)
125    }
126
127    /// Get that DID that corresponds to the underlying credential of this [Author]
128    pub async fn did(&self) -> Result<Did> {
129        Ok(Did(self.key.get_did().await?))
130    }
131
132    /// Returns true if this author is in the delegation chain of authority for
133    /// the given [Authorization], otherwise false.
134    pub async fn is_authorizer_of<S>(
135        &self,
136        authorization: &Authorization,
137        db: &SphereDb<S>,
138    ) -> Result<bool>
139    where
140        S: Storage,
141    {
142        let proof_chain = authorization.as_proof_chain(db).await?;
143        let mut links_to_check = vec![&proof_chain];
144        let author_did = self.did().await?;
145
146        while let Some(link) = links_to_check.pop() {
147            if link.ucan().issuer() == author_did {
148                return Ok(true);
149            }
150
151            links_to_check.extend(link.proofs().iter());
152        }
153
154        Ok(false)
155    }
156}
157
158#[cfg(test)]
159mod tests {
160    use anyhow::Result;
161    use noosphere_storage::{MemoryStorage, SphereDb};
162    use ucan::{builder::UcanBuilder, crypto::KeyMaterial, store::UcanJwtStore};
163
164    use crate::{
165        authority::{generate_capability, generate_ed25519_key, Authorization, SphereAbility},
166        data::Did,
167        view::Sphere,
168    };
169
170    use super::{Access, Author};
171
172    #[cfg(target_arch = "wasm32")]
173    use wasm_bindgen_test::wasm_bindgen_test;
174
175    #[cfg(target_arch = "wasm32")]
176    wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
177
178    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
179    #[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
180    async fn it_gives_read_only_access_when_there_is_no_authorization() -> Result<()> {
181        let author = Author::anonymous();
182        let mut db = SphereDb::new(&MemoryStorage::default()).await?;
183
184        let (sphere, _, _) = Sphere::generate("did:key:foo", &mut db).await?;
185
186        db.set_version(&sphere.get_identity().await?, sphere.cid())
187            .await?;
188
189        let access = author
190            .access_to(&sphere.get_identity().await?, &db)
191            .await
192            .unwrap();
193
194        assert_eq!(access, Access::ReadOnly);
195
196        Ok(())
197    }
198
199    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
200    #[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
201    async fn it_gives_read_write_access_if_the_key_is_authorized() -> Result<()> {
202        let owner_key = generate_ed25519_key();
203        let owner_did = Did(owner_key.get_did().await.unwrap());
204        let mut db = SphereDb::new(&MemoryStorage::default()).await.unwrap();
205
206        let (sphere, authorization, _) = Sphere::generate(&owner_did, &mut db).await.unwrap();
207
208        db.set_version(&sphere.get_identity().await?, sphere.cid())
209            .await?;
210
211        let author = Author {
212            key: owner_key,
213            authorization: Some(authorization),
214        };
215
216        let access = author
217            .access_to(&sphere.get_identity().await.unwrap(), &db)
218            .await
219            .unwrap();
220
221        assert_eq!(access, Access::ReadWrite);
222        Ok(())
223    }
224
225    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
226    #[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
227    async fn it_gives_read_write_access_to_the_root_sphere_credential() -> Result<()> {
228        let owner_key = generate_ed25519_key();
229        let owner_did = Did(owner_key.get_did().await.unwrap());
230        let mut db = SphereDb::new(&MemoryStorage::default()).await.unwrap();
231
232        let (sphere, authorization, mnemonic) =
233            Sphere::generate(&owner_did, &mut db).await.unwrap();
234
235        let root_credential = mnemonic.to_credential()?;
236
237        db.set_version(&sphere.get_identity().await?, sphere.cid())
238            .await?;
239
240        let author = Author {
241            key: root_credential,
242            authorization: Some(authorization),
243        };
244
245        let access = author
246            .access_to(&sphere.get_identity().await.unwrap(), &db)
247            .await
248            .unwrap();
249
250        assert_eq!(access, Access::ReadWrite);
251        Ok(())
252    }
253
254    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
255    #[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
256    async fn it_can_find_itself_in_an_authorization_lineage() -> Result<()> {
257        let owner_key = generate_ed25519_key();
258        let owner_did = Did(owner_key.get_did().await.unwrap());
259        let mut db = SphereDb::new(&MemoryStorage::default()).await.unwrap();
260
261        let (sphere, authorization, _) = Sphere::generate(&owner_did, &mut db).await.unwrap();
262
263        let next_key = generate_ed25519_key();
264        let next_did = Did(next_key.get_did().await.unwrap());
265
266        let final_did = Did("did:key:foo".into());
267
268        let authorization_ucan = authorization.as_ucan(&db).await?;
269
270        let capability =
271            generate_capability(&sphere.get_identity().await?, SphereAbility::Authorize);
272        let next_ucan = UcanBuilder::default()
273            .issued_by(&owner_key)
274            .for_audience(&next_did)
275            .with_lifetime(100)
276            .claiming_capability(&capability)
277            .witnessed_by(&authorization_ucan, None)
278            .build()?
279            .sign()
280            .await?;
281
282        let final_ucan = UcanBuilder::default()
283            .issued_by(&next_key)
284            .for_audience(&final_did)
285            .with_lifetime(100)
286            .claiming_capability(&capability)
287            .witnessed_by(&next_ucan, None)
288            .build()?
289            .sign()
290            .await?;
291
292        db.write_token(&next_ucan.encode()?).await?;
293        db.write_token(&final_ucan.encode()?).await?;
294
295        let author = Author {
296            key: owner_key,
297            authorization: Some(authorization.clone()),
298        };
299
300        let next_authorization = Authorization::Ucan(next_ucan);
301        let final_authorization = Authorization::Ucan(final_ucan);
302
303        assert!(author.is_authorizer_of(&next_authorization, &db).await?);
304        assert!(author.is_authorizer_of(&final_authorization, &db).await?);
305
306        let next_author = Author {
307            key: next_key,
308            authorization: Some(next_authorization),
309        };
310
311        assert!(!next_author.is_authorizer_of(&authorization, &db).await?);
312
313        let unrelated_key = generate_ed25519_key();
314
315        let unrelated_author = Author {
316            key: unrelated_key,
317            authorization: None,
318        };
319
320        assert!(
321            !unrelated_author
322                .is_authorizer_of(&authorization, &db)
323                .await?
324        );
325        assert!(
326            !unrelated_author
327                .is_authorizer_of(&final_authorization, &db)
328                .await?
329        );
330
331        Ok(())
332    }
333}