Development Documentation (main branch) - For stable release docs, see docs.rs/eidetica
Skip to main content

eidetica/auth/
settings.rs

1//! Authentication settings management for Eidetica
2//!
3//! This module provides a wrapper around Doc for managing authentication
4//! settings. Keys are indexed by pubkey to prevent collision bugs.
5//! Names are optional metadata that can be used as hints in signatures.
6
7use std::collections::HashMap;
8
9use serde::{Deserialize, Serialize};
10
11use super::errors::AuthError;
12use crate::{
13    Result,
14    auth::{
15        crypto::PublicKey,
16        types::{AuthKey, DelegatedTreeRef, KeyHint, KeyStatus, Permission, ResolvedAuth, SigKey},
17    },
18    crdt::{Doc, doc::Value},
19    entry::ID,
20};
21
22/// Authentication settings view/interface over Doc data
23///
24/// Keys are stored by pubkey in the "keys" sub-object.
25/// Delegations are stored by root tree ID in the "delegations" sub-object.
26/// Global permission is stored in the "global" sub-object.
27#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct AuthSettings {
29    /// Doc data from _settings.auth - this is a view, not the authoritative copy
30    inner: Doc,
31}
32
33impl From<Doc> for AuthSettings {
34    fn from(doc: Doc) -> Self {
35        Self { inner: doc }
36    }
37}
38
39impl From<AuthSettings> for Doc {
40    fn from(settings: AuthSettings) -> Doc {
41        settings.inner
42    }
43}
44
45impl AuthSettings {
46    /// Create a new empty auth settings view
47    pub fn new() -> Self {
48        Self { inner: Doc::new() }
49    }
50
51    /// Get the underlying Doc for direct access
52    pub fn as_doc(&self) -> &Doc {
53        &self.inner
54    }
55
56    /// Get mutable access to the underlying Doc
57    pub fn as_doc_mut(&mut self) -> &mut Doc {
58        &mut self.inner
59    }
60
61    // ==================== Key Operations ====================
62
63    /// Add a new authentication key by pubkey (fails if key already exists)
64    ///
65    /// # Arguments
66    /// * `pubkey` - The public key
67    /// * `key` - The AuthKey containing permissions, status, and optional name
68    pub fn add_key(&mut self, pubkey: &PublicKey, key: AuthKey) -> Result<()> {
69        let pubkey_str = pubkey.to_string();
70
71        // Check if key already exists
72        if self.get_key_by_str(&pubkey_str).is_ok() {
73            return Err(AuthError::KeyAlreadyExists {
74                key_name: pubkey_str,
75            }
76            .into());
77        }
78
79        self.inner.set(format!("keys.{pubkey_str}"), key);
80        Ok(())
81    }
82
83    /// Explicitly overwrite an existing authentication key
84    pub fn overwrite_key(&mut self, pubkey: &PublicKey, key: AuthKey) -> Result<()> {
85        let pubkey_str = pubkey.to_string();
86        self.inner.set(format!("keys.{pubkey_str}"), key);
87        Ok(())
88    }
89
90    /// Get a key by its public key
91    pub fn get_key_by_pubkey(&self, pubkey: &PublicKey) -> Result<AuthKey> {
92        self.get_key_by_str(&pubkey.to_string())
93    }
94
95    /// Get a key by its public key string
96    ///
97    /// Internal helper used by `get_key_by_pubkey` and `resolve_hint` (which
98    /// already has strings from Doc storage).
99    fn get_key_by_str(&self, pubkey: &str) -> Result<AuthKey> {
100        match self.inner.get(format!("keys.{pubkey}")) {
101            Some(Value::Doc(doc)) => AuthKey::try_from(doc).map_err(|e| {
102                AuthError::InvalidKeyFormat {
103                    reason: e.to_string(),
104                }
105                .into()
106            }),
107            Some(_) => Err(AuthError::InvalidKeyFormat {
108                reason: format!("key '{pubkey}' is not a Doc"),
109            }
110            .into()),
111            None => Err(AuthError::KeyNotFound {
112                key_name: pubkey.to_string(),
113            }
114            .into()),
115        }
116    }
117
118    /// Find keys by name (may return multiple if names collide)
119    ///
120    /// Returns Vec of (pubkey, AuthKey) tuples sorted by pubkey for deterministic ordering.
121    pub fn find_keys_by_name(&self, name: &str) -> Vec<(String, AuthKey)> {
122        let mut matches = Vec::new();
123
124        // Get all keys and filter by name
125        if let Ok(all_keys) = self.get_all_keys() {
126            for (pubkey, auth_key) in all_keys {
127                if auth_key.name() == Some(name) {
128                    matches.push((pubkey, auth_key));
129                }
130            }
131        }
132
133        // Sort by pubkey for deterministic ordering
134        matches.sort_by(|a, b| a.0.cmp(&b.0));
135        matches
136    }
137
138    /// Get all authentication keys
139    pub fn get_all_keys(&self) -> Result<HashMap<String, AuthKey>> {
140        let mut result: HashMap<String, AuthKey> = HashMap::new();
141
142        // Get the "keys" sub-doc
143        if let Some(Value::Doc(keys_doc)) = self.inner.get("keys") {
144            for (pubkey, value) in keys_doc.iter() {
145                if let Value::Doc(key_doc) = value
146                    && let Ok(auth_key) = AuthKey::try_from(key_doc)
147                {
148                    result.insert(pubkey.clone(), auth_key);
149                }
150            }
151        }
152
153        Ok(result)
154    }
155
156    /// Rename a key by pubkey
157    ///
158    /// Updates only the display name of an existing key, preserving its
159    /// permissions and status.
160    pub fn rename_key(&mut self, pubkey: &PublicKey, name: Option<&str>) -> Result<()> {
161        let pubkey_str = pubkey.to_string();
162        let mut auth_key = self.get_key_by_str(&pubkey_str)?;
163        auth_key.set_name(name);
164        self.inner.set(format!("keys.{pubkey_str}"), auth_key);
165        Ok(())
166    }
167
168    /// Revoke a key by pubkey
169    pub fn revoke_key(&mut self, pubkey: &PublicKey) -> Result<()> {
170        let pubkey_str = pubkey.to_string();
171        let mut auth_key = self.get_key_by_str(&pubkey_str)?;
172        auth_key.set_status(KeyStatus::Revoked);
173        self.inner.set(format!("keys.{pubkey_str}"), auth_key);
174        Ok(())
175    }
176
177    // ==================== Delegation Operations ====================
178
179    /// Add or update a delegated tree reference
180    ///
181    /// The delegation is stored by root tree ID, extracted from `tree_ref.tree.root`.
182    /// This ensures collision-resistant storage similar to key storage by pubkey.
183    pub fn add_delegated_tree(&mut self, tree_ref: DelegatedTreeRef) -> Result<()> {
184        let root_id = tree_ref.tree.root.to_string();
185        self.inner.set(format!("delegations.{root_id}"), tree_ref);
186        Ok(())
187    }
188
189    /// Get a delegated tree reference by root tree ID
190    pub fn get_delegated_tree(&self, root_id: &ID) -> Result<DelegatedTreeRef> {
191        match self.inner.get(format!("delegations.{root_id}")) {
192            Some(Value::Doc(doc)) => DelegatedTreeRef::try_from(doc).map_err(|e| {
193                AuthError::InvalidAuthConfiguration {
194                    reason: format!("Invalid delegated tree format: {e}"),
195                }
196                .into()
197            }),
198            Some(_) => Err(AuthError::InvalidAuthConfiguration {
199                reason: format!("delegation '{root_id}' is not a Doc"),
200            }
201            .into()),
202            None => Err(AuthError::DelegationNotFound {
203                tree_id: root_id.clone(),
204            }
205            .into()),
206        }
207    }
208
209    /// Get all delegated tree references
210    ///
211    /// Returns a map from root tree ID to the delegation reference.
212    pub fn get_all_delegated_trees(&self) -> Result<HashMap<ID, DelegatedTreeRef>> {
213        let mut result: HashMap<ID, DelegatedTreeRef> = HashMap::new();
214
215        // Get the "delegations" sub-doc
216        if let Some(Value::Doc(delegations_doc)) = self.inner.get("delegations") {
217            for (root_id_str, value) in delegations_doc.iter() {
218                if let Value::Doc(doc) = value
219                    && let Ok(tree_ref) = DelegatedTreeRef::try_from(doc)
220                {
221                    let root_id = ID::parse(root_id_str)?;
222                    result.insert(root_id, tree_ref);
223                }
224            }
225        }
226
227        Ok(result)
228    }
229
230    // ==================== Global Permission ====================
231
232    /// Set the global permission
233    ///
234    /// Stores the global permission at the `global` path, separate from
235    /// individual key entries in the `keys` namespace.
236    pub fn set_global_permission(&mut self, key: AuthKey) {
237        self.inner.set("global", key);
238    }
239
240    /// Get the global permission AuthKey
241    ///
242    /// Reads from the `global` path.
243    pub fn get_global_key(&self) -> Result<AuthKey> {
244        match self.inner.get("global") {
245            Some(Value::Doc(doc)) => AuthKey::try_from(doc).map_err(|e| {
246                AuthError::InvalidKeyFormat {
247                    reason: e.to_string(),
248                }
249                .into()
250            }),
251            Some(_) => Err(AuthError::InvalidKeyFormat {
252                reason: "global key is not a Doc".to_string(),
253            }
254            .into()),
255            None => Err(AuthError::KeyNotFound {
256                key_name: "global".to_string(),
257            }
258            .into()),
259        }
260    }
261
262    // ==================== Key Hint Resolution ====================
263
264    /// Resolve a key hint to matching authentication info
265    ///
266    /// Returns Vec of ResolvedAuth. For pubkey hints, returns at most one.
267    /// For name hints, may return multiple if names collide. Caller should try each
268    /// until signature verifies.
269    ///
270    /// # Name Collision Handling
271    ///
272    /// When multiple keys share the same name, all matching keys are returned.
273    /// The caller (typically `validate_entry`) should iterate through the matches
274    /// and attempt signature verification with each until one succeeds.
275    pub fn resolve_hint(&self, hint: &KeyHint) -> Result<Vec<ResolvedAuth>> {
276        // Handle global permission
277        if hint.is_global() {
278            // Global hint - check that global permission exists
279            let global_key =
280                self.get_global_key()
281                    .map_err(|_| AuthError::InvalidAuthConfiguration {
282                        reason: "Global hint used but no global permission configured".to_string(),
283                    })?;
284
285            // The actual pubkey is directly in hint.pubkey
286            let actual_pubkey =
287                hint.pubkey
288                    .as_ref()
289                    .ok_or_else(|| AuthError::InvalidAuthConfiguration {
290                        reason: "Global hint has no pubkey".to_string(),
291                    })?;
292
293            // Return ResolvedAuth with actual pubkey and global permission
294            // There is only 1 global, no need to look for others
295            return Ok(vec![ResolvedAuth {
296                public_key: actual_pubkey.clone(),
297                effective_permission: *global_key.permissions(),
298                key_status: global_key.status().clone(),
299            }]);
300        }
301
302        // Direct pubkey lookup
303        if let Some(pubkey) = &hint.pubkey {
304            return match self.get_key_by_pubkey(pubkey) {
305                Ok(key) => Ok(vec![ResolvedAuth {
306                    public_key: pubkey.clone(),
307                    effective_permission: *key.permissions(),
308                    key_status: key.status().clone(),
309                }]),
310                Err(e) => Err(e),
311            };
312        }
313
314        // Name lookup - may return multiple matches
315        if let Some(name) = &hint.name {
316            let matches = self.find_keys_by_name(name);
317            if matches.is_empty() {
318                return Err(AuthError::KeyNotFound {
319                    key_name: name.clone(),
320                }
321                .into());
322            }
323            // Convert all matches to ResolvedAuth
324            let mut results = Vec::with_capacity(matches.len());
325            for (pubkey, auth_key) in matches {
326                results.push(ResolvedAuth {
327                    public_key: PublicKey::from_prefixed_string(&pubkey)?,
328                    effective_permission: *auth_key.permissions(),
329                    key_status: auth_key.status().clone(),
330                });
331            }
332            return Ok(results);
333        }
334
335        // No hint set - empty/unsigned
336        Ok(vec![])
337    }
338
339    // ==================== Permission Helpers ====================
340
341    /// Check if global permission exists and is active
342    pub fn has_global_permission(&self) -> bool {
343        self.get_global_permission().is_some()
344    }
345
346    /// Get global permission level if it exists and is active
347    pub fn get_global_permission(&self) -> Option<Permission> {
348        if let Ok(key) = self.get_global_key()
349            && *key.status() == KeyStatus::Active
350        {
351            Some(*key.permissions())
352        } else {
353            None
354        }
355    }
356
357    /// Find all SigKeys that a public key can use to access this database
358    ///
359    /// Returns (SigKey, Permission) tuples sorted by permission (highest first)
360    pub fn find_all_sigkeys_for_pubkey(&self, pubkey: &PublicKey) -> Vec<(SigKey, Permission)> {
361        let mut results = Vec::new();
362
363        // Check if this pubkey has an active direct key entry. A revoked or
364        // inactive key is not a usable access method, and callers treat a
365        // returned SigKey as authorization (bootstrap access checks,
366        // `open_database`, operation signing), so discovery must not offer it.
367        if let Ok(auth_key) = self.get_key_by_pubkey(pubkey)
368            && auth_key.is_active()
369        {
370            results.push((SigKey::from_pubkey(pubkey), *auth_key.permissions()));
371        }
372
373        // Check if global permission exists
374        if let Some(global_perm) = self.get_global_permission() {
375            results.push((SigKey::global(pubkey), global_perm));
376        }
377
378        // Note: Delegation path discovery happens at the Database::find_sigkeys() level
379        // because it requires async Instance access to load delegated tree auth settings.
380
381        // Sort by permission, highest first (reverse sort since Permission Ord has higher > lower)
382        results.sort_by_key(|b| std::cmp::Reverse(b.1));
383        results
384    }
385
386    /// Resolve which SigKey should be used for an operation
387    ///
388    /// Returns the SigKey with highest permission for the given pubkey.
389    pub fn resolve_sig_key_for_operation(
390        &self,
391        pubkey: &PublicKey,
392    ) -> Result<(SigKey, Permission)> {
393        let matches = self.find_all_sigkeys_for_pubkey(pubkey);
394
395        matches.into_iter().next().ok_or_else(|| {
396            AuthError::PermissionDenied {
397                reason: format!("No active key found for pubkey: {pubkey}"),
398            }
399            .into()
400        })
401    }
402
403    // ==================== Key Modification Authorization ====================
404
405    /// Check if a signing key can modify an existing target key
406    pub fn can_modify_key(
407        &self,
408        signing_key: &ResolvedAuth,
409        target_pubkey: &PublicKey,
410    ) -> Result<bool> {
411        // Must have admin permissions to modify keys
412        if !signing_key.effective_permission.can_admin() {
413            return Ok(false);
414        }
415
416        // Get target key info
417        let target_key = self.get_key_by_pubkey(target_pubkey)?;
418
419        // Signing key must be >= target key permissions
420        Ok(signing_key.effective_permission >= *target_key.permissions())
421    }
422
423    /// Check if a signing key can create a new key with the specified permissions
424    pub fn can_create_key(
425        &self,
426        signing_key: &ResolvedAuth,
427        new_key_permissions: &Permission,
428    ) -> Result<bool> {
429        // Must have admin permissions to create keys
430        if !signing_key.effective_permission.can_admin() {
431            return Ok(false);
432        }
433
434        // Signing key must be >= new key permissions
435        Ok(signing_key.effective_permission >= *new_key_permissions)
436    }
437}
438
439impl Default for AuthSettings {
440    fn default() -> Self {
441        Self::new()
442    }
443}
444
445#[cfg(test)]
446mod tests {
447    use super::*;
448    use crate::crdt::CRDT;
449
450    #[test]
451    fn test_auth_settings_basic_operations() {
452        let mut settings = AuthSettings::new();
453
454        let pubkey = PublicKey::random();
455        let auth_key = AuthKey::active(Some("laptop"), Permission::Write(10));
456
457        settings.add_key(&pubkey, auth_key.clone()).unwrap();
458
459        // Retrieve the key
460        let retrieved = settings.get_key_by_pubkey(&pubkey).unwrap();
461        assert_eq!(retrieved.name(), Some("laptop"));
462        assert_eq!(retrieved.permissions(), auth_key.permissions());
463        assert_eq!(retrieved.status(), auth_key.status());
464    }
465
466    #[test]
467    fn test_find_keys_by_name() {
468        let mut settings = AuthSettings::new();
469
470        let pubkey1 = PublicKey::random();
471        let pubkey2 = PublicKey::random();
472
473        // Add two keys with same name
474        settings
475            .add_key(
476                &pubkey1,
477                AuthKey::active(Some("device"), Permission::Write(10)),
478            )
479            .unwrap();
480        settings
481            .add_key(
482                &pubkey2,
483                AuthKey::active(Some("device"), Permission::Admin(1)),
484            )
485            .unwrap();
486
487        // Find by name should return both
488        let matches = settings.find_keys_by_name("device");
489        assert_eq!(matches.len(), 2);
490    }
491
492    #[test]
493    fn test_revoke_key() {
494        let mut settings = AuthSettings::new();
495
496        let pubkey = PublicKey::random();
497        let auth_key = AuthKey::active(Some("laptop"), Permission::Admin(5));
498
499        settings.add_key(&pubkey, auth_key).unwrap();
500
501        // Revoke the key
502        settings.revoke_key(&pubkey).unwrap();
503
504        // Check that it's revoked
505        let retrieved = settings.get_key_by_pubkey(&pubkey).unwrap();
506        assert_eq!(retrieved.status(), &KeyStatus::Revoked);
507    }
508
509    #[test]
510    fn test_global_permission() {
511        let mut settings = AuthSettings::new();
512
513        // No global permission initially
514        assert!(!settings.has_global_permission());
515        assert_eq!(settings.get_global_permission(), None);
516
517        // Set global Write(10) permission
518        let global_key = AuthKey::active(None, Permission::Write(10));
519        settings.set_global_permission(global_key);
520
521        // Global permission should now be detected
522        assert!(settings.has_global_permission());
523        assert_eq!(
524            settings.get_global_permission(),
525            Some(Permission::Write(10))
526        );
527    }
528
529    #[test]
530    fn test_resolve_hint_pubkey() {
531        let mut settings = AuthSettings::new();
532
533        let pubkey = PublicKey::random();
534        settings
535            .add_key(
536                &pubkey,
537                AuthKey::active(Some("laptop"), Permission::Write(10)),
538            )
539            .unwrap();
540
541        // Resolve by pubkey hint
542        let hint = KeyHint::from_pubkey(&pubkey);
543        let matches = settings.resolve_hint(&hint).unwrap();
544        assert_eq!(matches.len(), 1);
545        assert_eq!(matches[0].public_key, pubkey);
546        assert_eq!(matches[0].effective_permission, Permission::Write(10));
547    }
548
549    #[test]
550    fn test_resolve_hint_name() {
551        let mut settings = AuthSettings::new();
552
553        let pubkey = PublicKey::random();
554        settings
555            .add_key(
556                &pubkey,
557                AuthKey::active(Some("laptop"), Permission::Write(10)),
558            )
559            .unwrap();
560
561        // Resolve by name hint
562        let hint = KeyHint::from_name("laptop");
563        let matches = settings.resolve_hint(&hint).unwrap();
564        assert_eq!(matches.len(), 1);
565        assert_eq!(matches[0].public_key, pubkey);
566        assert_eq!(matches[0].effective_permission, Permission::Write(10));
567    }
568
569    #[test]
570    fn test_resolve_hint_global() {
571        let mut settings = AuthSettings::new();
572
573        // Set global permission
574        settings.set_global_permission(AuthKey::active(None, Permission::Write(10)));
575
576        let actual_pubkey = PublicKey::random();
577        let hint = KeyHint::global(&actual_pubkey);
578        let matches = settings.resolve_hint(&hint).unwrap();
579
580        assert_eq!(matches.len(), 1);
581        assert_eq!(matches[0].public_key, actual_pubkey);
582        assert_eq!(matches[0].effective_permission, Permission::Write(10));
583    }
584
585    #[test]
586    fn test_find_all_sigkeys_for_pubkey() {
587        let mut settings = AuthSettings::new();
588
589        let pubkey = PublicKey::random();
590
591        // No keys - should return empty vec
592        let results = settings.find_all_sigkeys_for_pubkey(&pubkey);
593        assert_eq!(results.len(), 0);
594
595        // Add direct key
596        settings
597            .add_key(
598                &pubkey,
599                AuthKey::active(Some("device1"), Permission::Write(5)),
600            )
601            .unwrap();
602
603        let results = settings.find_all_sigkeys_for_pubkey(&pubkey);
604        assert_eq!(results.len(), 1);
605
606        // Set global permission
607        settings.set_global_permission(AuthKey::active(None, Permission::Write(10)));
608
609        let results = settings.find_all_sigkeys_for_pubkey(&pubkey);
610        assert_eq!(results.len(), 2);
611
612        // A revoked direct key is not a usable access method and must not be
613        // discovered — otherwise a revoked (delegated) key could bootstrap.
614        // Checked against fresh settings so no global '*' grant masks the result.
615        let mut revoked_settings = AuthSettings::new();
616        let revoked_pubkey = PublicKey::random();
617        revoked_settings
618            .add_key(
619                &revoked_pubkey,
620                AuthKey::new(Some("revoked"), Permission::Admin(0), KeyStatus::Revoked),
621            )
622            .unwrap();
623        assert!(
624            revoked_settings
625                .find_all_sigkeys_for_pubkey(&revoked_pubkey)
626                .is_empty(),
627            "revoked direct key should not be offered as an access method"
628        );
629    }
630
631    #[test]
632    fn test_resolve_sig_key_for_operation() {
633        let mut settings = AuthSettings::new();
634
635        let pubkey = PublicKey::random();
636
637        // No keys configured - should fail
638        let result = settings.resolve_sig_key_for_operation(&pubkey);
639        assert!(result.is_err());
640
641        // Add device key
642        settings
643            .add_key(
644                &pubkey,
645                AuthKey::active(Some("device"), Permission::Write(5)),
646            )
647            .unwrap();
648
649        // Should resolve to direct pubkey
650        let (sig_key, granted_perm) = settings.resolve_sig_key_for_operation(&pubkey).unwrap();
651        assert!(sig_key.has_pubkey_hint(&pubkey));
652        assert_eq!(granted_perm, Permission::Write(5));
653    }
654
655    #[test]
656    fn test_auth_settings_merge() {
657        let mut settings1 = AuthSettings::new();
658        let mut settings2 = AuthSettings::new();
659
660        let pubkey1 = PublicKey::random();
661        let pubkey2 = PublicKey::random();
662
663        settings1
664            .add_key(
665                &pubkey1,
666                AuthKey::active(Some("key1"), Permission::Write(10)),
667            )
668            .unwrap();
669        settings2
670            .add_key(
671                &pubkey2,
672                AuthKey::active(Some("key2"), Permission::Admin(5)),
673            )
674            .unwrap();
675
676        // Merge at Doc level
677        let merged_doc = settings1.as_doc().merge(settings2.as_doc()).unwrap();
678        let merged_settings: AuthSettings = merged_doc.into();
679
680        // Both keys should be present
681        assert!(merged_settings.get_key_by_pubkey(&pubkey1).is_ok());
682        assert!(merged_settings.get_key_by_pubkey(&pubkey2).is_ok());
683    }
684}