noosphere_cli/native/commands/sphere/
auth.rs

1use std::{collections::BTreeMap, convert::TryFrom};
2
3use anyhow::{anyhow, Result};
4use cid::Cid;
5use noosphere_core::context::{
6    HasSphereContext, SphereAuthorityRead, SphereAuthorityWrite, SphereWalker,
7};
8use noosphere_core::data::{Did, Jwt, Link};
9use serde_json::{json, Value};
10use ucan::{store::UcanJwtStore, Ucan};
11
12use tokio_stream::StreamExt;
13
14use crate::native::{commands::sphere::save, workspace::Workspace};
15
16/// Add an authorization for another device key (given by its [Did]) to the sphere
17pub async fn auth_add(did: &Did, name: Option<String>, workspace: &Workspace) -> Result<Cid> {
18    workspace.ensure_sphere_initialized()?;
19
20    let mut sphere_context = workspace.sphere_context().await?;
21
22    let current_authorization = sphere_context.get_authorization(did).await?;
23
24    if let Some(authorization) = current_authorization {
25        let cid = Cid::try_from(authorization)?;
26
27        return Err(anyhow!(
28            r#"{} is already authorized to access the sphere
29Here is the identity of the authorization:
30
31  {}
32
33If you want to change it to something new, revoke the current one first:
34
35  orb auth revoke {}
36
37You will be able to add a new one after the old one is revoked"#,
38            did,
39            cid,
40            cid
41        ));
42    }
43
44    let name = match name {
45        Some(name) => name,
46        None => {
47            let random_name = witty_phrase_generator::WPGen::new()
48                .with_words(3)
49                .unwrap_or_else(|| vec!["Unnamed"])
50                .into_iter()
51                .map(String::from)
52                .collect::<Vec<String>>()
53                .join("-");
54            info!(
55                "Note: since no name was specified, the authorization will be saved with the generated name \"{random_name}\""
56            );
57            random_name
58        }
59    };
60
61    let new_authorization = sphere_context.authorize(&name, did).await?;
62    let new_authorization_cid = Cid::try_from(new_authorization)?;
63
64    save(None, workspace).await?;
65
66    info!(
67        r#"Successfully authorized {did} to access your sphere.
68
69IMPORTANT: You MUST sync to enable your gateway to recognize the authorization:
70
71  orb sync
72
73This is the authorization's identity:
74
75  {new_authorization_cid}
76  
77Use this identity when joining the sphere on the other client"#
78    );
79
80    Ok(new_authorization_cid)
81}
82
83fn draw_branch(
84    mut items: Vec<Link<Jwt>>,
85    mut indentation: Vec<bool>,
86    hierarchy: &BTreeMap<Link<Jwt>, Vec<Link<Jwt>>>,
87    meta: &BTreeMap<Link<Jwt>, (String, Did, Jwt)>,
88) {
89    let prefix = indentation
90        .iter()
91        .enumerate()
92        .map(|(index, is_last)| match is_last {
93            false if index == 0 => "│",
94            false => "   │ ",
95            true if index == 0 => " ",
96            true => "    ",
97        })
98        .collect::<String>();
99
100    while let Some(link) = items.pop() {
101        let is_last = items.is_empty();
102
103        if let Some((name, id, _token)) = meta.get(&link) {
104            let (branch_char, trunk_char) = match is_last {
105                true => ('└', ' '),
106                false => ('├', '│'),
107            };
108
109            info!("{prefix}{}── {}", branch_char, name);
110            info!("{prefix}{}   {}", trunk_char, id);
111
112            if let Some(children) = hierarchy.get(&link) {
113                info!("{prefix}{}   │", trunk_char);
114                indentation.push(is_last);
115                draw_branch(children.clone(), indentation.clone(), hierarchy, meta);
116            } else {
117                info!("{prefix}{}", trunk_char);
118            }
119        }
120    }
121}
122
123/// List all authorizations in the sphere, optionally as a tree hierarchy
124/// representing the chain of authority, and optionally in JSON format
125pub async fn auth_list(tree: bool, as_json: bool, workspace: &Workspace) -> Result<()> {
126    let sphere_context = workspace.sphere_context().await?;
127    let sphere_identity = sphere_context.identity().await?;
128    let db = sphere_context.lock().await.db().clone();
129
130    let walker = SphereWalker::from(&sphere_context);
131
132    let authorization_stream = walker.authorization_stream();
133
134    tokio::pin!(authorization_stream);
135
136    let mut authorization_meta: BTreeMap<Link<Jwt>, (String, Did, Jwt)> = BTreeMap::default();
137    let mut max_name_length: usize = 7;
138
139    while let Some((name, identity, link)) = authorization_stream.try_next().await? {
140        let jwt = Jwt(db.require_token(&link).await?);
141        max_name_length = max_name_length.max(name.len());
142        authorization_meta.insert(link, (name, identity, jwt));
143    }
144
145    if tree {
146        let mut authorization_roots = Vec::<Link<Jwt>>::new();
147        let mut authorization_hierarchy: BTreeMap<Link<Jwt>, Vec<Link<Jwt>>> = BTreeMap::default();
148
149        for (link, (_name, _identity, jwt)) in authorization_meta.iter() {
150            let ucan = Ucan::try_from(jwt.as_str())?;
151
152            // TODO(#552): Maybe only consider Noosphere-related proofs here
153            // TODO(#553): Maybe filter on proofs that specifically refer to the current sphere
154            let proofs = ucan
155                .proofs()
156                .clone()
157                .unwrap_or_default()
158                .into_iter()
159                .filter_map(|cid| Cid::try_from(cid.as_str()).ok().map(Link::from))
160                .collect::<Vec<Link<Jwt>>>();
161
162            if *ucan.issuer() == sphere_identity {
163                // TODO(#554): Such an authorization ought not have any topical proofs,
164                // but perhaps we should verify that
165                authorization_roots.push(*link)
166            } else {
167                for proof in proofs {
168                    let items = match authorization_hierarchy.get_mut(&proof) {
169                        Some(items) => items,
170                        None => {
171                            authorization_hierarchy.insert(proof, Vec::new());
172                            authorization_hierarchy.get_mut(&proof).ok_or_else(|| {
173                                anyhow!(
174                                    "Could not access list of child authorizations for {}",
175                                    &proof
176                                )
177                            })?
178                        }
179                    };
180                    items.push(*link);
181                }
182            }
183        }
184
185        if as_json {
186            let mut hierarchy = BTreeMap::new();
187            for (proof, children) in authorization_hierarchy {
188                hierarchy.insert(
189                    proof.to_string(),
190                    children
191                        .iter()
192                        .map(|child| child.to_string())
193                        .collect::<Vec<String>>(),
194                );
195            }
196            let mut meta = BTreeMap::new();
197            for (link, (name, id, token)) in authorization_meta {
198                meta.insert(
199                    link.to_string(),
200                    json!({
201                        "name": name,
202                        "id": id,
203                        "token": token
204                    }),
205                );
206            }
207
208            let roots = authorization_roots
209                .into_iter()
210                .map(|root| root.to_string())
211                .collect::<Vec<String>>();
212
213            info!(
214                "{}",
215                serde_json::to_string_pretty(&json!({
216                    "sphere": sphere_identity,
217                    "roots": roots,
218                    "hierarchy": hierarchy,
219                    "meta": meta
220                }))?
221            );
222        } else {
223            info!(" ⌀ Sphere");
224            info!(" │ {sphere_identity}");
225            info!(" │");
226
227            while let Some(root) = authorization_roots.pop() {
228                let items = vec![root];
229                let indentation = vec![authorization_roots.is_empty()];
230
231                draw_branch(
232                    items,
233                    indentation,
234                    &authorization_hierarchy,
235                    &authorization_meta,
236                );
237            }
238        }
239    } else if as_json {
240        let authorizations: Vec<Value> = authorization_meta
241            .into_iter()
242            .map(|(link, (name, did, token))| {
243                json!({
244                    "name": name,
245                    "id": did,
246                    "link": link.to_string(),
247                    "token": token
248                })
249            })
250            .collect();
251        info!("{}", serde_json::to_string_pretty(&json!(authorizations))?);
252    } else {
253        info!("{:1$}  AUTHORIZED KEY", "NAME", max_name_length);
254        for (_, (name, did, _)) in authorization_meta {
255            info!("{name:max_name_length$}  {did}");
256        }
257    }
258
259    Ok(())
260}
261
262/// Revoke an authorization for another device key by its nickname
263pub async fn auth_revoke(name: &str, workspace: &Workspace) -> Result<()> {
264    workspace.ensure_sphere_initialized()?;
265
266    let mut sphere_context = workspace.sphere_context().await?;
267    let authorizations = sphere_context.get_authorizations_by_name(name).await?;
268
269    if authorizations.is_empty() {
270        return Err(anyhow!("There is no authorization named {:?}", name));
271    }
272
273    for authorization in authorizations.iter() {
274        sphere_context.revoke_authorization(authorization).await?;
275    }
276
277    save(None, workspace).await?;
278
279    Ok(())
280}