Skip to main content

galata_vault/owner/
mod.rs

1//! The owner API: every operation `gv` performs as a project's owner, as
2//! data in and data out.
3//!
4//! An [`Owner`] is built from a [`KeyStore`] (where held node keys live), a
5//! [`StateStore`] (the known tree and bookkeeping), an optional
6//! [`crate::Events`] observer, and a connector that reaches each project's
7//! pinned server (by default [`crate::client::ClientBuilder`] over HTTP).
8//!
9//! The library asks nothing and prints nothing:
10//! - every confirmation `gv` asks for is a typestate or an argument: a new
11//!   project's key is stored only after [`PendingInit::confirm_kit_stored`];
12//!   exporting a delegation kit takes [`GrantsSubtreeOwnership`]; a rekey
13//!   takes [`RetiresAllTokens`];
14//! - every warning goes to the observer or into an outcome
15//!   ([`Removal`], [`Repair`], [`RekeyOutcome`]);
16//! - no key file is written: the caller stores the kits it is handed.
17//!
18//! ```no_run
19//! use std::sync::Arc;
20//! use galata_vault::owner::{Owner, RetiresAllTokens};
21//! use galata_vault::{FileStateStore, KeyStore, Scope};
22//! # fn keys() -> Arc<dyn KeyStore> { unimplemented!() }
23//!
24//! let mut owner = Owner::open(keys(), Arc::new(FileStateStore::new("/var/lib/acme")))?;
25//! let init = owner.begin_init("acme", "https://vault.example")?;
26//! let kit = init.recovery_kit().render()?; // store it somewhere safe first
27//! # drop(kit);
28//! init.confirm_kit_stored()?;
29//! owner.env_add(&"acme/prod".parse()?)?;
30//! let env = owner.environment(&"acme/prod".parse()?)?;
31//! let token = env.mint(Scope::Read, 0, &[])?;
32//! # let _ = (token, RetiresAllTokens);
33//! # Ok::<(), Box<dyn std::error::Error>>(())
34//! ```
35
36mod env;
37pub(crate) mod kit;
38mod rekey;
39
40pub use env::{Environment, MintedToken, Revocation, TokenInfo};
41pub use kit::{KIT_VERSION, Kit, KitKind, default_file_name};
42pub use rekey::{PendingRekey, RekeyAborted, RekeyOutcome};
43
44use std::collections::{BTreeMap, HashMap};
45use std::sync::Arc;
46
47use crate::client::{Api, Events, NoEvents, Progress, Warning, now};
48use crate::keys::{NodeKey, seal_child_key};
49use crate::proto::children::{ChildEntry, ChildMode};
50use crate::proto::codec::FormatError;
51use crate::proto::ids::B64;
52use crate::proto::path::{EnvPath, Segment};
53
54use crate::audit::{self, AuditReport};
55use crate::error::{Error, code};
56use crate::state::{LocalState, ProjectState, StateStore, TreeNode};
57use crate::store::{KeyName, KeyStore};
58use crate::vault::{DAY, Handle};
59
60/// How an owner reaches a server: its URL in, a typed API out.
61pub type Connector = Arc<dyn Fn(&str) -> Result<Api, Error> + Send + Sync>;
62
63/// Acknowledges that a delegation kit makes its holder the owner of the
64/// node and everything beneath it: they can read, change and delete every
65/// secret there, mint tokens, and rekey it away from you.
66#[derive(Debug, Clone, Copy)]
67pub struct GrantsSubtreeOwnership;
68
69/// Acknowledges that a rekey retires every token in the subtree: they die
70/// with the old vaults and must be minted again.
71#[derive(Debug, Clone, Copy)]
72pub struct RetiresAllTokens;
73
74/// A project, as initialised.
75#[derive(Debug, Clone, PartialEq, Eq)]
76#[non_exhaustive]
77pub struct ProjectInfo {
78    /// The project's path.
79    pub path: EnvPath,
80    /// Its pinned server.
81    pub server: String,
82    /// Its root vault's id (hex).
83    pub vault_id: String,
84}
85
86/// What rediscovery found below a node.
87#[derive(Debug, Clone, PartialEq, Eq)]
88#[non_exhaustive]
89pub struct Discovery {
90    /// Where it started.
91    pub root: EnvPath,
92    /// Every known path at or below the root, sorted.
93    pub found: Vec<EnvPath>,
94    /// Nodes whose vault could not be opened; kept in the tree so they can
95    /// be repaired.
96    pub unreachable: Vec<EnvPath>,
97}
98
99/// What removing an environment did.
100#[derive(Debug, Clone, PartialEq, Eq)]
101#[non_exhaustive]
102pub struct Removal {
103    /// Every environment deleted, deepest first.
104    pub deleted: Vec<EnvPath>,
105    /// The parent whose key is not held here, so its children record still
106    /// lists the removed environment.
107    pub parent_not_updated: Option<EnvPath>,
108}
109
110/// What repairing an environment did.
111#[derive(Debug, Clone, PartialEq, Eq)]
112#[non_exhaustive]
113pub struct Repair {
114    /// Whether its vault was re-created at the same vault id (it had
115    /// expired); its records did not survive.
116    pub created: bool,
117    /// How many known child environments its children record lists again.
118    pub children: usize,
119    /// Rekeyed children whose sealed key was only in the lost record: import
120    /// their kits to reach them again.
121    pub lost: Vec<EnvPath>,
122}
123
124/// A project owner's view: the key tree, and every operation on it.
125pub struct Owner {
126    keys: Arc<dyn KeyStore>,
127    store: Arc<dyn StateStore>,
128    events: Arc<dyn Events>,
129    connect: Connector,
130    apis: HashMap<String, Api>,
131    state: LocalState,
132}
133
134impl std::fmt::Debug for Owner {
135    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
136        f.debug_struct("Owner")
137            .field("projects", &self.state.projects.keys().collect::<Vec<_>>())
138            .finish_non_exhaustive()
139    }
140}
141
142fn default_connector() -> Connector {
143    #[cfg(feature = "http")]
144    {
145        Arc::new(|server: &str| {
146            crate::client::ClientBuilder::new(server)
147                .build()
148                .map_err(Error::build)
149        })
150    }
151    #[cfg(not(feature = "http"))]
152    {
153        Arc::new(|server: &str| {
154            Err(Error::other(format!(
155                "no transport to reach {server}: build galata-vault with the `http` feature, or give the owner a connector"
156            )))
157        })
158    }
159}
160
161fn chain_end(chain: Vec<(EnvPath, NodeKey)>) -> Result<NodeKey, Error> {
162    chain
163        .into_iter()
164        .last()
165        .map(|(_, k)| k)
166        .ok_or_else(|| Error::other("a key chain includes its target"))
167}
168
169fn children_error(e: impl std::fmt::Display) -> Error {
170    Error::other(e.to_string())
171}
172
173/// Whether `vault`'s server announced an expiry at its last status.
174fn announces_expiry(vault: &Handle) -> bool {
175    vault.status().is_some_and(|s| s.expires_at.is_some())
176}
177
178/// Whether `vault`'s server expires vaults for inactivity: its capabilities
179/// give an idle window, or its latest status for the vault named an expiry
180/// time (all a server older than the capabilities document can say).
181fn server_expires(api: &Api, vault: &Handle) -> bool {
182    let announced = matches!(
183        api.capabilities(),
184        Ok(Some(caps)) if caps.idle_expiry_days.is_some()
185    );
186    announced || announces_expiry(vault)
187}
188
189fn levenshtein(a: &str, b: &str) -> usize {
190    let b: Vec<char> = b.chars().collect();
191    let mut row: Vec<usize> = (0..=b.len()).collect();
192    for (i, ca) in a.chars().enumerate() {
193        let mut prev = row[0];
194        row[0] = i + 1;
195        for (j, cb) in b.iter().enumerate() {
196            let cur = row[j + 1];
197            row[j + 1] = (prev + usize::from(ca != *cb)).min(row[j] + 1).min(cur + 1);
198            prev = cur;
199        }
200    }
201    row[b.len()]
202}
203
204/// The closest candidate, if any is plausibly a typo of `target`.
205fn closest<'a>(target: &str, candidates: impl IntoIterator<Item = &'a str>) -> Option<&'a str> {
206    let limit = (target.len() / 3).max(2);
207    candidates
208        .into_iter()
209        .map(|c| (levenshtein(target, c), c))
210        .filter(|(d, _)| *d <= limit)
211        .min()
212        .map(|(_, c)| c)
213}
214
215impl Owner {
216    /// An owner over `keys` and `store`, reaching servers over HTTP (feature
217    /// `http`) and dropping notices. The state is loaded now.
218    pub fn open(keys: Arc<dyn KeyStore>, store: Arc<dyn StateStore>) -> Result<Owner, Error> {
219        let state = store.load().map_err(Error::store)?;
220        Ok(Owner {
221            keys,
222            store,
223            events: Arc::new(NoEvents),
224            connect: default_connector(),
225            apis: HashMap::new(),
226            state,
227        })
228    }
229
230    /// Send expiry, progress and warnings to `events`.
231    pub fn with_events(mut self, events: Arc<dyn Events>) -> Owner {
232        self.events = events;
233        self.apis.clear();
234        self
235    }
236
237    /// Reach each server through `connect` (a proxy, a private CA, a test
238    /// transport, …). The owner's observer is attached to what it returns.
239    pub fn with_connector(
240        mut self,
241        connect: impl Fn(&str) -> Result<Api, Error> + Send + Sync + 'static,
242    ) -> Owner {
243        self.connect = Arc::new(connect);
244        self.apis.clear();
245        self
246    }
247
248    /// The observer notices go to.
249    pub fn events(&self) -> &Arc<dyn Events> {
250        &self.events
251    }
252
253    /// The local state as this owner holds it now.
254    pub fn state(&self) -> &LocalState {
255        &self.state
256    }
257
258    /// Save the local state to the store.
259    pub fn save(&self) -> Result<(), Error> {
260        self.store.save(&self.state).map_err(Error::store)
261    }
262
263    /// Load the local state from the store again, dropping unsaved changes.
264    pub fn reload(&mut self) -> Result<(), Error> {
265        self.state = self.store.load().map_err(Error::store)?;
266        Ok(())
267    }
268
269    /// The API for `server`, built once per owner.
270    pub fn api(&mut self, server: &str) -> Result<Api, Error> {
271        if let Some(api) = self.apis.get(server) {
272            return Ok(api.clone());
273        }
274        let api = (self.connect)(server)?.with_events(self.events.clone());
275        self.apis.insert(server.to_owned(), api.clone());
276        Ok(api)
277    }
278
279    fn api_for(&mut self, path: &EnvPath) -> Result<Api, Error> {
280        let server = self.state.project(path)?.server.clone();
281        self.api(&server)
282    }
283
284    fn held_key(&self, path: &EnvPath) -> Result<NodeKey, Error> {
285        let stored = self
286            .keys
287            .get(&KeyName::Node(path.clone()))
288            .map_err(Error::store)?
289            .ok_or_else(|| {
290                Error::local(
291                    code::NO_KEY,
292                    format!("the key for {path} is missing from the credential store"),
293                )
294            })?;
295        NodeKey::parse(&stored).map_err(|e| match e {
296            FormatError::UnknownVersion { found } => Error::other(format!(
297                "the stored key for {path} carries version {found}, which this build does not know"
298            )),
299            _ => Error::other(format!("the stored key for {path} is damaged")),
300        })
301    }
302
303    fn store_key(&self, path: &EnvPath, key: &NodeKey) -> Result<(), Error> {
304        self.keys
305            .set(&KeyName::Node(path.clone()), &key.encode())
306            .map_err(Error::store)
307    }
308
309    // ------------------------------------------------------------ the tree
310
311    /// The closest known path to `path`, if one is plausibly a typo of it:
312    /// data for a "did you mean" hint.
313    pub fn closest_known(&self, path: &EnvPath) -> Option<EnvPath> {
314        let known = self.state.known_paths();
315        closest(&path.to_string(), known.iter().map(String::as_str)).and_then(|s| s.parse().ok())
316    }
317
318    /// Refuse a path absent from the known tree, before any request, with
319    /// the closest known path as the suggestion ([`Error::UnknownPath`]).
320    pub fn require_known(&self, path: &EnvPath) -> Result<(), Error> {
321        let project_known = self
322            .state
323            .projects
324            .contains_key(path.project_name().as_str());
325        if project_known && self.state.project(path)?.knows(path) {
326            return Ok(());
327        }
328        let message = if project_known {
329            format!("unknown environment {path}")
330        } else {
331            format!("unknown project {}", path.project_name())
332        };
333        Err(Error::UnknownPath {
334            path: path.to_string(),
335            suggestion: self.closest_known(path).map(|p| p.to_string()),
336            project_known,
337            message,
338        })
339    }
340
341    /// The known tree of `project`: path → vault.
342    pub fn tree(&self, project: &EnvPath) -> Result<&BTreeMap<String, TreeNode>, Error> {
343        Ok(&self.state.project(project)?.tree)
344    }
345
346    /// The keys from the deepest held ancestor down to `path`, inclusive.
347    /// Derived keys are recomputed; a sealed one is opened from its parent's
348    /// owner-only children record.
349    fn chain(&self, api: &Api, path: &EnvPath) -> Result<Vec<(EnvPath, NodeKey)>, Error> {
350        let project = self.state.project(path)?.clone();
351        let root = project.held_root_for(path).ok_or_else(|| {
352            Error::local(
353                code::NO_KEY,
354                format!("no key is held for {path} or any node above it"),
355            )
356        })?;
357        let mut key = self.held_key(&root)?;
358        let mut here = root.clone();
359        let mut out = vec![(here.clone(), key.clone())];
360        for seg in &path.segments()[root.depth()..] {
361            let child = here
362                .child(seg.clone())
363                .map_err(|e| Error::local(code::INVALID_PATH, format!("{here}/{seg}: {e}")))?;
364            let sealed = project
365                .tree
366                .get(&child.to_string())
367                .is_some_and(|n| n.sealed);
368            key = if sealed {
369                let parent = Handle::open_owner(api, &key, &here.to_string())?;
370                let (record, _) = parent.children()?;
371                let entry = record.get(seg).ok_or_else(|| {
372                    Error::other(format!("{here}'s children record no longer lists {seg}"))
373                })?;
374                match &entry.mode {
375                    ChildMode::Derived => key.child(seg),
376                    ChildMode::Sealed { key_ct } => {
377                        parent.owner()?.open_child_key(&key_ct.0).map_err(|e| {
378                            Error::key(e, format!("{child}'s sealed key does not open"))
379                        })?
380                    }
381                }
382            } else {
383                key.child(seg)
384            };
385            here = child;
386            out.push((here.clone(), key.clone()));
387        }
388        Ok(out)
389    }
390
391    /// Open `path`'s vault as its owner, keeping its ancestors alive when its
392    /// server expires vaults (its capabilities say so, or its responses
393    /// announce an expiry). What this owner saw there before (its
394    /// pins) is handed to the vault: an older descriptor, or an older
395    /// version of a record, is refused. Call [`Owner::close`] when done, so
396    /// what it saw is remembered.
397    pub fn environment(&mut self, path: &EnvPath) -> Result<Environment, Error> {
398        self.require_known(path)?;
399        let api = self.api_for(path)?;
400        let mut chain = self.chain(&api, path)?;
401        let key = chain
402            .pop()
403            .map(|(_, k)| k)
404            .ok_or_else(|| Error::other("a key chain includes its target"))?;
405        let vault = Handle::open_owner(&api, &key, &path.to_string())?;
406        let id = vault.vault_id.to_hex();
407        if let Some(pins) = self.state.pins.get(&id) {
408            vault.set_pins(pins.clone())?;
409        }
410        self.state.touched.insert(id.clone(), now());
411        let ancestors: Vec<String> = chain
412            .iter()
413            .map(|(_, k)| k.owner().vault_id().to_hex())
414            .collect();
415        if server_expires(&api, &vault) {
416            // Whatever the server said before, it expires vaults now: every
417            // ancestor is due its keep-alive again.
418            self.state.no_expiry.remove(&id);
419            for a in &ancestors {
420                self.state.no_expiry.remove(a);
421            }
422            self.keep_alive(&api, &chain);
423        } else {
424            // A project's vaults share its pinned server, so none of them can
425            // expire either. Keeping them alive would only spend requests and
426            // link the tree by timing.
427            self.state.no_expiry.insert(id);
428            self.state.no_expiry.extend(ancestors);
429        }
430        Environment::new(vault, path.clone())
431    }
432
433    /// Remember what `env` showed (its pins), to catch a server that later
434    /// shows less. Saved with the next [`Owner::save`].
435    pub fn remember(&mut self, env: &Environment) {
436        self.state.pins.insert(env.vault_id(), env.pins());
437    }
438
439    /// Done with `env`: remember what it showed, and save the local state.
440    pub fn close(&mut self, env: Environment) -> Result<(), Error> {
441        self.remember(&env);
442        drop(env);
443        self.save()
444    }
445
446    /// Verify `env`'s audit chain from the head recorded for it, and record
447    /// the new head only on success. With `earlier > 0`, up to that many
448    /// rows at or below the recorded head are included, for display. Saved
449    /// with the next [`Owner::save`].
450    pub fn audit(&mut self, env: &Environment, earlier: usize) -> Result<AuditReport, Error> {
451        audit::verify_recorded(env.handle(), &mut self.state, earlier)
452    }
453
454    /// A signed status request to each ancestor not touched in 24 hours, so
455    /// a project root outlives environments that are used every day. An
456    /// ancestor whose server expires nothing (no idle window in its
457    /// capabilities, and no expiry at its last status) is skipped.
458    fn keep_alive(&mut self, api: &Api, ancestors: &[(EnvPath, NodeKey)]) {
459        let t = now();
460        for (path, key) in ancestors {
461            let id = key.owner().vault_id().to_hex();
462            if self.state.no_expiry.contains(&id)
463                || self
464                    .state
465                    .touched
466                    .get(&id)
467                    .is_some_and(|last| t - last < DAY)
468            {
469                continue;
470            }
471            match Handle::open_owner(api, key, &path.to_string()) {
472                Ok(vault) => {
473                    if !server_expires(api, &vault) {
474                        self.state.no_expiry.insert(id.clone());
475                    }
476                    self.state.touched.insert(id, t);
477                }
478                Err(e) => self.events.warning(&Warning::KeepAliveFailed {
479                    path: path.to_string(),
480                    message: e.to_string(),
481                }),
482            }
483        }
484    }
485
486    /// Rediscover the subtree under `root` from its key alone, replacing the
487    /// cached subtree. A child whose vault cannot be opened is kept, with
488    /// its cached descendants, so it can be repaired. Returns those.
489    fn discover_from(
490        &mut self,
491        api: &Api,
492        root: &EnvPath,
493        key: NodeKey,
494    ) -> Result<Vec<EnvPath>, Error> {
495        let root_sealed = self
496            .state
497            .project(root)?
498            .tree
499            .get(&root.to_string())
500            .is_some_and(|n| n.sealed);
501        let mut found: BTreeMap<String, TreeNode> = BTreeMap::new();
502        let mut unreachable: Vec<EnvPath> = Vec::new();
503        let mut queue: Vec<(EnvPath, NodeKey, bool, Option<NodeKey>)> =
504            vec![(root.clone(), key, root_sealed, None)];
505        while let Some((path, key, sealed, parent_key)) = queue.pop() {
506            let opened = match Handle::open_owner(api, &key, &path.to_string()) {
507                Ok(v) => Ok((v, key.clone(), sealed)),
508                Err(e) => match self.relink(api, &path, &key, parent_key.as_ref()) {
509                    Some((v, held)) => Ok((v, held, true)),
510                    None => Err(e),
511                },
512            };
513            let (vault, key, sealed) = match opened {
514                Ok(found) => found,
515                Err(e) if &path == root => return Err(e),
516                Err(e) => {
517                    self.events.warning(&Warning::Unreachable {
518                        path: path.to_string(),
519                        message: e.to_string(),
520                        vault_missing: matches!(e, Error::VaultNotFound { .. }),
521                    });
522                    found.insert(
523                        path.to_string(),
524                        TreeNode::new(key.owner().vault_id(), sealed),
525                    );
526                    unreachable.push(path);
527                    continue;
528                }
529            };
530            self.state.touched.insert(vault.vault_id.to_hex(), now());
531            found.insert(path.to_string(), TreeNode::new(vault.vault_id, sealed));
532            let (record, _) = vault.children()?;
533            for entry in record.children() {
534                let child = match path.child(entry.seg.clone()) {
535                    Ok(c) => c,
536                    Err(e) => {
537                        self.events.warning(&Warning::ChildTooDeep {
538                            parent: path.to_string(),
539                            message: e.to_string(),
540                        });
541                        continue;
542                    }
543                };
544                let next = match &entry.mode {
545                    ChildMode::Derived => (child, key.child(&entry.seg), false, Some(key.clone())),
546                    ChildMode::Sealed { key_ct } => {
547                        match vault.owner()?.open_child_key(&key_ct.0) {
548                            Ok(k) => (child, k, true, Some(key.clone())),
549                            Err(e) => {
550                                self.events.warning(&Warning::SealedKeyUnopenable {
551                                    path: child.to_string(),
552                                    message: e.to_string(),
553                                });
554                                continue;
555                            }
556                        }
557                    }
558                };
559                queue.push(next);
560            }
561        }
562        let project = self.state.project_mut(root)?;
563        project.tree.retain(|k, _| {
564            let Ok(p) = k.parse::<EnvPath>() else {
565                return false;
566            };
567            !p.starts_with(root) || unreachable.iter().any(|u| &p != u && p.starts_with(u))
568        });
569        project.tree.extend(found);
570        Ok(unreachable)
571    }
572
573    /// A child whose entry predates a rekey (a restore rolled the parent's
574    /// record back): if a key is held for the node, open the vault with it
575    /// and point the parent's entry at it again.
576    fn relink(
577        &self,
578        api: &Api,
579        path: &EnvPath,
580        stale: &NodeKey,
581        parent_key: Option<&NodeKey>,
582    ) -> Option<(Handle, NodeKey)> {
583        let parent_key = parent_key?;
584        let parent = path.parent()?;
585        let stored = self.keys.get(&KeyName::Node(path.clone())).ok()??;
586        let held = NodeKey::parse(&stored).ok()?;
587        if held.owner().vault_id() == stale.owner().vault_id() {
588            return None;
589        }
590        let vault = Handle::open_owner(api, &held, &path.to_string()).ok()?;
591        let relinked = Handle::open_owner(api, parent_key, &parent.to_string())
592            .and_then(|pv| pv.seal_child(path.last(), &held));
593        match relinked {
594            Ok(()) => self.events.progress(&Progress::Relinked {
595                path: path.to_string(),
596                parent: parent.to_string(),
597            }),
598            Err(e) => self.events.warning(&Warning::RelinkFailed {
599                path: path.to_string(),
600                parent: parent.to_string(),
601                message: e.to_string(),
602            }),
603        }
604        Some((vault, held))
605    }
606
607    /// Rediscover everything reachable from the keys held for `project`,
608    /// replacing its known tree. Saved with the next [`Owner::save`].
609    pub fn refresh(&mut self, project: &EnvPath) -> Result<(), Error> {
610        let held: Vec<EnvPath> = self
611            .state
612            .project(project)?
613            .held
614            .iter()
615            .filter_map(|h| h.parse().ok())
616            .collect();
617        if held.is_empty() {
618            return Err(Error::local(
619                code::NO_KEY,
620                format!("no key is held for any node of {project}"),
621            ));
622        }
623        let api = self.api_for(project)?;
624        for root in held {
625            let key = self.held_key(&root)?;
626            self.discover_from(&api, &root, key)?;
627        }
628        Ok(())
629    }
630
631    /// Rediscover the subtree under `root` from its key alone: a report of
632    /// what was found, including nodes that could not be opened.
633    pub fn discover(&mut self, root: &EnvPath) -> Result<Discovery, Error> {
634        self.require_known(root)?;
635        let api = self.api_for(root)?;
636        let key = chain_end(self.chain(&api, root)?)?;
637        let unreachable = self.discover_from(&api, root, key)?;
638        let found = self.found_below(root)?;
639        self.save()?;
640        Ok(Discovery {
641            root: root.clone(),
642            found,
643            unreachable,
644        })
645    }
646
647    fn found_below(&self, root: &EnvPath) -> Result<Vec<EnvPath>, Error> {
648        Ok(self
649            .state
650            .project(root)?
651            .tree
652            .keys()
653            .filter_map(|k| k.parse::<EnvPath>().ok())
654            .filter(|p| p.starts_with(root))
655            .collect())
656    }
657
658    // ------------------------------------------------------------ projects
659
660    /// Start a project: a fresh key and its recovery kit. Nothing reaches the
661    /// server and nothing is stored until [`PendingInit::confirm_kit_stored`],
662    /// so a dropped `PendingInit` leaves no trace.
663    pub fn begin_init(&mut self, project: &str, server: &str) -> Result<PendingInit<'_>, Error> {
664        let seg = Segment::new(project).map_err(|e| {
665            Error::local(
666                code::INVALID_PATH,
667                format!("{project:?} is not a project name: {e}"),
668            )
669        })?;
670        let path = EnvPath::project(seg);
671        let server = crate::proto::url::validate_server_url(server)
672            .map_err(|e| Error::local(code::INVALID_SERVER, e))?;
673        if self.state.projects.contains_key(project) {
674            return Err(Error::local(
675                code::PROJECT_EXISTS,
676                format!("a project named {project} is already configured here"),
677            ));
678        }
679        let key = NodeKey::generate();
680        let kit = Kit::new(KitKind::Recovery, path.clone(), &server, &key);
681        Ok(PendingInit {
682            owner: self,
683            path,
684            server,
685            key,
686            kit,
687        })
688    }
689
690    /// Create the environment `path` below a known parent whose key is
691    /// reachable. Idempotent on the server: a vault left by an interrupted
692    /// add is registered. Saves the local state.
693    pub fn env_add(&mut self, path: &EnvPath) -> Result<(), Error> {
694        let Some(parent) = path.parent() else {
695            return Err(Error::local(
696                code::IS_PROJECT,
697                format!("{path} is a project"),
698            ));
699        };
700        self.require_known(&parent)?;
701        if self.state.project(path)?.knows(path) {
702            return Err(Error::local(
703                code::ENV_EXISTS,
704                format!("{path} already exists"),
705            ));
706        }
707        let api = self.api_for(path)?;
708        let chain = self.chain(&api, &parent)?;
709        let Some((_, parent_key)) = chain.last().cloned() else {
710            return Err(Error::other("a key chain includes its target"));
711        };
712        self.keep_alive(&api, &chain[..chain.len() - 1]);
713        let parent_vault = Handle::open_owner(&api, &parent_key, &parent.to_string())?;
714        let seg = path.last().clone();
715        if parent_vault.children()?.0.get(&seg).is_some() {
716            return Err(Error::local(
717                code::ENV_EXISTS,
718                format!(
719                    "{path} already exists (the known tree of {} is out of date)",
720                    path.project_name()
721                ),
722            ));
723        }
724
725        let key = parent_key.child(&seg);
726        Handle::create(&api, &key, &path.to_string())?;
727        parent_vault.update_children(|r| {
728            if r.get(&seg).is_none() {
729                r.insert(ChildEntry::new(seg.clone(), now(), ChildMode::Derived))
730                    .map_err(children_error)?;
731            }
732            Ok(())
733        })?;
734        let vault_id = key.owner().vault_id();
735        self.state
736            .project_mut(path)?
737            .tree
738            .insert(path.to_string(), TreeNode::new(vault_id, false));
739        self.state.touched.insert(vault_id.to_hex(), now());
740        self.save()
741    }
742
743    /// Delete `path`'s vault and, with `recursive`, every environment below
744    /// it, deepest first. Each deletion is reported as
745    /// [`Progress::Deleted`] as it happens. Saves the local state.
746    pub fn env_remove(&mut self, path: &EnvPath, recursive: bool) -> Result<Removal, Error> {
747        self.require_known(path)?;
748        let api = self.api_for(path)?;
749        let chain = self.chain(&api, path)?;
750        let Some((_, key)) = chain.last().cloned() else {
751            return Err(Error::other("a key chain includes its target"));
752        };
753        // See children other clients may have added.
754        self.discover_from(&api, path, key)?;
755        let below = self.state.project(path)?.descendants(path);
756        if !below.is_empty() && !recursive {
757            let names: Vec<String> = below.iter().map(ToString::to_string).collect();
758            return Err(Error::local(
759                code::HAS_CHILDREN,
760                format!("{path} has environments below it ({})", names.join(", ")),
761            ));
762        }
763
764        let mut deleted = Vec::new();
765        // Deepest first, so every parent still exists while its children go.
766        for node in below.iter().chain(std::iter::once(path)) {
767            let node_key = chain_end(self.chain(&api, node)?)?;
768            match Handle::open_owner(&api, &node_key, &node.to_string()) {
769                Ok(v) => {
770                    v.delete_vault()?;
771                    let id = v.vault_id.to_hex();
772                    self.state.pins.remove(&id);
773                    self.state.audit.remove(&id);
774                }
775                Err(e) => self.events.warning(&Warning::Unreachable {
776                    path: node.to_string(),
777                    message: e.to_string(),
778                    vault_missing: matches!(e, Error::VaultNotFound { .. }),
779                }),
780            }
781            let p = self.state.project_mut(node)?;
782            p.tree.remove(&node.to_string());
783            if let Some(i) = p.held.iter().position(|h| h == &node.to_string()) {
784                p.held.remove(i);
785                self.keys
786                    .delete(&KeyName::Node(node.clone()))
787                    .map_err(Error::store)?;
788            }
789            self.events.progress(&Progress::Deleted {
790                path: node.to_string(),
791            });
792            deleted.push(node.clone());
793        }
794
795        let mut parent_not_updated = None;
796        if let Some(parent) = path.parent() {
797            if chain.len() >= 2 {
798                let (_, parent_key) = &chain[chain.len() - 2];
799                let seg = path.last().clone();
800                Handle::open_owner(&api, parent_key, &parent.to_string())?.update_children(
801                    |r| {
802                        let _ = r.remove(&seg);
803                        Ok(())
804                    },
805                )?;
806            } else {
807                parent_not_updated = Some(parent);
808            }
809        } else {
810            self.state.projects.remove(path.project_name().as_str());
811        }
812        self.save()?;
813        Ok(Removal {
814            deleted,
815            parent_not_updated,
816        })
817    }
818
819    /// Re-create an expired vault at its vault id (its records did not
820    /// survive expiry) and list its known children in its children record
821    /// again. On a live vault only the children record is completed. Saves
822    /// the local state.
823    pub fn env_repair(&mut self, path: &EnvPath) -> Result<Repair, Error> {
824        self.require_known(path)?;
825        let api = self.api_for(path)?;
826        let key = chain_end(self.chain(&api, path)?)?;
827        let created = Handle::create(&api, &key, &path.to_string())?;
828        let vault = Handle::open_owner(&api, &key, &path.to_string())?;
829        if created {
830            // A new vault at the old id: its history restarts, so what this
831            // owner saw of the expired one no longer applies.
832            self.state.pins.remove(&vault.vault_id.to_hex());
833            self.state.audit.remove(&vault.vault_id.to_hex());
834        }
835        let owner_box = vault.owner()?.box_pub();
836
837        let project = self.state.project(path)?.clone();
838        let mut entries = Vec::new();
839        let mut lost = Vec::new();
840        for child in project.children_of(path) {
841            let Some(node) = project.tree.get(&child.to_string()) else {
842                continue;
843            };
844            let mode = if node.sealed {
845                match self
846                    .keys
847                    .get(&KeyName::Node(child.clone()))
848                    .map_err(Error::store)?
849                {
850                    Some(k) => {
851                        let k = NodeKey::parse(&k).map_err(|_| {
852                            Error::other(format!("the stored key for {child} is damaged"))
853                        })?;
854                        let key_ct = seal_child_key(&owner_box, &vault.vault_id, &k)
855                            .map_err(|e| Error::key(e, format!("the sealed key for {child}")))?;
856                        ChildMode::Sealed {
857                            key_ct: B64(key_ct),
858                        }
859                    }
860                    None => {
861                        lost.push(child);
862                        continue;
863                    }
864                }
865            } else {
866                ChildMode::Derived
867            };
868            entries.push(ChildEntry::new(child.last().clone(), now(), mode));
869        }
870        vault.update_children(|r| {
871            for e in &entries {
872                if r.get(&e.seg).is_none() {
873                    r.insert(e.clone()).map_err(children_error)?;
874                }
875            }
876            Ok(())
877        })?;
878        self.state.touched.insert(vault.vault_id.to_hex(), now());
879        drop(vault);
880        self.save()?;
881        Ok(Repair {
882            created,
883            children: entries.len(),
884            lost,
885        })
886    }
887
888    /// A delegation kit for `path`: its holder owns the node and everything
889    /// beneath it, and nothing above. The caller stores it; the library
890    /// writes no file.
891    pub fn export_kit(&mut self, path: &EnvPath, _: GrantsSubtreeOwnership) -> Result<Kit, Error> {
892        self.require_known(path)?;
893        let api = self.api_for(path)?;
894        let key = chain_end(self.chain(&api, path)?)?;
895        let server = self.state.project(path)?.server.clone();
896        let kit = Kit::new(KitKind::Delegation, path.clone(), &server, &key);
897        self.save()?;
898        Ok(kit)
899    }
900
901    /// Register a kit's node (recovery or delegation) and rediscover its
902    /// subtree from its key alone. Saves the local state.
903    pub fn import(&mut self, kit: &Kit) -> Result<Discovery, Error> {
904        let path = kit.path().clone();
905        let key = kit.key()?;
906        let name = path.project_name().to_string();
907        let vault_id = key.owner().vault_id();
908        let api = self.api(kit.server())?;
909        let fresh = match self.state.projects.get(&name) {
910            Some(p) if p.server != kit.server() => {
911                return Err(Error::local(
912                    code::SERVER_MISMATCH,
913                    format!(
914                        "the project {name} here is pinned to {}, but the kit is for {}",
915                        p.server,
916                        kit.server()
917                    ),
918                ));
919            }
920            Some(p) => {
921                if p.tree
922                    .get(&path.to_string())
923                    .is_some_and(|n| n.vault_id != vault_id)
924                {
925                    return Err(Error::local(
926                        code::SERVER_MISMATCH,
927                        format!(
928                            "{path} here is a different vault from the kit's (another project with the same name?)"
929                        ),
930                    ));
931                }
932                false
933            }
934            None => {
935                self.state
936                    .projects
937                    .insert(name.clone(), ProjectState::new(kit.server()));
938                true
939            }
940        };
941        let unreachable = match self.discover_from(&api, &path, key.clone()) {
942            Ok(u) => u,
943            Err(e) => {
944                if fresh {
945                    self.state.projects.remove(&name);
946                }
947                return Err(e);
948            }
949        };
950        self.store_key(&path, &key)?;
951        let project = self.state.project_mut(&path)?;
952        if !project.held.contains(&path.to_string()) {
953            project.held.push(path.to_string());
954        }
955        let found = self.found_below(&path)?;
956        self.save()?;
957        Ok(Discovery {
958            root: path,
959            found,
960            unreachable,
961        })
962    }
963
964    /// Restore a project from its recovery kit (a delegation kit is
965    /// refused: import it with [`Owner::import`]).
966    pub fn recover(&mut self, kit: &Kit) -> Result<Discovery, Error> {
967        if kit.kind() != KitKind::Recovery {
968            return Err(Error::local(
969                code::INVALID_KIT,
970                format!("this is a delegation kit for {}", kit.path()),
971            ));
972        }
973        self.import(kit)
974    }
975}
976
977/// A project being initialised: its key exists and its recovery kit can be
978/// rendered, but nothing is on the server or in the key store yet.
979pub struct PendingInit<'a> {
980    owner: &'a mut Owner,
981    path: EnvPath,
982    server: String,
983    key: NodeKey,
984    kit: Kit,
985}
986
987impl std::fmt::Debug for PendingInit<'_> {
988    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
989        f.debug_struct("PendingInit")
990            .field("path", &self.path)
991            .field("server", &self.server)
992            .finish_non_exhaustive()
993    }
994}
995
996/// Initialisation failed. Whether the project's vault already exists on the
997/// server matters: if it does, the recovery kit is the only copy of its key.
998#[derive(Debug, Clone)]
999pub struct InitError {
1000    error: Error,
1001    vault_created: bool,
1002}
1003
1004impl InitError {
1005    /// What failed.
1006    pub fn error(&self) -> &Error {
1007        &self.error
1008    }
1009
1010    /// Whether the vault was created before the failure (so the kit must be
1011    /// kept).
1012    pub fn vault_created(&self) -> bool {
1013        self.vault_created
1014    }
1015
1016    /// The error alone.
1017    pub fn into_error(self) -> Error {
1018        self.error
1019    }
1020}
1021
1022impl std::fmt::Display for InitError {
1023    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1024        self.error.fmt(f)
1025    }
1026}
1027
1028impl std::error::Error for InitError {
1029    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
1030        Some(&self.error)
1031    }
1032}
1033
1034impl From<InitError> for Error {
1035    fn from(e: InitError) -> Error {
1036        e.error
1037    }
1038}
1039
1040impl PendingInit<'_> {
1041    /// The project's path.
1042    pub fn path(&self) -> &EnvPath {
1043        &self.path
1044    }
1045
1046    /// The pinned server.
1047    pub fn server(&self) -> &str {
1048        &self.server
1049    }
1050
1051    /// The recovery kit: the only way to recover the project. Store it
1052    /// before confirming.
1053    pub fn recovery_kit(&self) -> &Kit {
1054        &self.kit
1055    }
1056
1057    /// The kit is stored safely: create the project's vault (proof of work,
1058    /// signed creation, an empty children record), store its key, and record
1059    /// the project. Saves the local state.
1060    pub fn confirm_kit_stored(self) -> Result<ProjectInfo, InitError> {
1061        let PendingInit {
1062            owner,
1063            path,
1064            server,
1065            key,
1066            kit: _,
1067        } = self;
1068        let before = |error| InitError {
1069            error,
1070            vault_created: false,
1071        };
1072        let after = |error| InitError {
1073            error,
1074            vault_created: true,
1075        };
1076        let api = owner.api(&server).map_err(before)?;
1077        let created = Handle::create(&api, &key, &path.to_string()).map_err(before)?;
1078        if !created {
1079            return Err(before(Error::local(
1080                code::VAULT_EXISTS,
1081                "the server already has a vault for this key; nothing was changed",
1082            )));
1083        }
1084        owner.store_key(&path, &key).map_err(after)?;
1085        let vault_id = key.owner().vault_id();
1086        let mut project = ProjectState::new(server.clone());
1087        project.held.push(path.to_string());
1088        project
1089            .tree
1090            .insert(path.to_string(), TreeNode::new(vault_id, false));
1091        owner.state.projects.insert(path.to_string(), project);
1092        owner.state.touched.insert(vault_id.to_hex(), now());
1093        owner.save().map_err(after)?;
1094        Ok(ProjectInfo {
1095            path,
1096            server,
1097            vault_id: vault_id.to_hex(),
1098        })
1099    }
1100}
1101
1102#[cfg(test)]
1103mod tests {
1104    use super::*;
1105
1106    #[test]
1107    fn suggestions() {
1108        let known = ["acme", "acme/prod", "acme/dev", "acme/staging"];
1109        assert_eq!(closest("acme/prdo", known), Some("acme/prod"));
1110        assert_eq!(closest("acme/stagin", known), Some("acme/staging"));
1111        assert_eq!(closest("zzzz/qqqq", known), None);
1112    }
1113}