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

eidetica/store/
settings_store.rs

1//! Settings store for managing database settings and authentication configuration
2//!
3//! This module provides a high-level interface for managing the `_settings` subtree,
4//! including specialized methods for authentication configuration. It wraps DocStore
5//! to provide settings-specific functionality while maintaining proper CRDT semantics.
6
7use crate::{
8    Result, Transaction,
9    auth::{
10        crypto::PublicKey,
11        settings::AuthSettings,
12        types::{AuthKey, DelegatedTreeRef, KeyStatus},
13    },
14    crdt::{CRDTError, Doc, doc},
15    height::HeightStrategy,
16    store::DocStore,
17};
18
19/// A settings-specific Store that wraps DocStore and provides specialized methods
20/// for managing database settings and authentication configuration.
21///
22/// SettingsStore provides a clean abstraction over the `_settings` subtree, offering
23/// type-safe methods for common settings operations while delegating to AuthSettings
24/// for authentication-specific functionality.
25pub struct SettingsStore {
26    /// The underlying DocStore for the _settings subtree
27    inner: DocStore,
28}
29
30impl SettingsStore {
31    /// Create a new SettingsStore from a Transaction
32    ///
33    /// This creates a SettingsStore that operates on the `_settings` subtree
34    /// within the given transaction context.
35    ///
36    /// This is crate-private - users should use `Transaction::get_settings()` instead.
37    ///
38    /// # Arguments
39    /// * `transaction` - The transaction to operate within
40    ///
41    /// # Returns
42    /// A Result containing the SettingsStore or an error if creation fails
43    pub(crate) fn new(transaction: &Transaction) -> Result<Self> {
44        // Note: We create DocStore directly here instead of using Store::load()
45        // because SettingsStore is a wrapper that doesn't implement the Store trait itself.
46        // This avoids the async requirement for this simple internal construction.
47        let inner = DocStore {
48            name: "_settings".to_string(),
49            txn: transaction.clone(),
50        };
51        Ok(Self { inner })
52    }
53
54    /// Get the database name from settings
55    ///
56    /// # Returns
57    /// The database name as a string, or an error if not found or invalid
58    pub async fn get_name(&self) -> Result<String> {
59        self.inner.get_string("name").await
60    }
61
62    /// Set the database name in settings
63    ///
64    /// # Arguments
65    /// * `name` - The name to set for the database
66    ///
67    /// # Returns
68    /// Result indicating success or failure
69    pub async fn set_name(&self, name: &str) -> Result<()> {
70        self.inner.set_result("name", name).await
71    }
72
73    /// Get a value from settings by key
74    ///
75    /// # Arguments
76    /// * `key` - The key to retrieve
77    ///
78    /// # Returns
79    /// The value associated with the key, or an error if not found
80    pub async fn get(&self, key: impl AsRef<str>) -> Result<doc::Value> {
81        self.inner.get(key).await
82    }
83
84    /// Get a string value from settings by key
85    ///
86    /// # Arguments
87    /// * `key` - The key to retrieve
88    ///
89    /// # Returns
90    /// The string value associated with the key, or an error if not found or wrong type
91    pub async fn get_string(&self, key: impl AsRef<str>) -> Result<String> {
92        self.inner.get_string(key).await
93    }
94
95    /// Get all settings as a Doc
96    ///
97    /// Returns a complete snapshot of all settings in the _settings subtree.
98    ///
99    /// # Returns
100    /// A Doc containing all current settings
101    pub async fn get_all(&self) -> Result<Doc> {
102        self.inner.get_all().await
103    }
104
105    /// Get the height strategy for this database.
106    ///
107    /// Returns [`HeightStrategy::Incremental`] if no strategy is configured,
108    /// ensuring backwards compatibility with existing databases.
109    ///
110    /// # Returns
111    /// The configured height strategy, or the default (Incremental)
112    pub async fn get_height_strategy(&self) -> Result<HeightStrategy> {
113        match self.inner.get("height_strategy").await {
114            Ok(value) => {
115                // HeightStrategy is stored as JSON in a Text value
116                let json = match value {
117                    doc::Value::Text(s) => s,
118                    _ => return Ok(HeightStrategy::default()),
119                };
120                serde_json::from_str(&json).map_err(|e| {
121                    CRDTError::DeserializationFailed {
122                        reason: e.to_string(),
123                    }
124                    .into()
125                })
126            }
127            Err(e) if e.is_not_found() => Ok(HeightStrategy::default()),
128            Err(e) => Err(e),
129        }
130    }
131
132    /// Set the height strategy for this database.
133    ///
134    /// # Arguments
135    /// * `strategy` - The height strategy to use
136    pub async fn set_height_strategy(&self, strategy: HeightStrategy) -> Result<()> {
137        let json =
138            serde_json::to_string(&strategy).map_err(|e| CRDTError::SerializationFailed {
139                reason: e.to_string(),
140            })?;
141        self.inner
142            .set("height_strategy", doc::Value::Text(json))
143            .await
144    }
145
146    /// Get a snapshot of the current authentication settings
147    ///
148    /// Returns a **cloned** `AuthSettings` built from the merged CRDT state.
149    /// Mutations to the returned value do not propagate back to the store;
150    /// use the dedicated methods (e.g. `set_auth_key`, `rename_auth_key`,
151    /// `revoke_auth_key`) to persist changes.
152    ///
153    /// # Returns
154    /// An AuthSettings snapshot of the current auth configuration
155    pub async fn auth_snapshot(&self) -> Result<AuthSettings> {
156        let all = self.inner.get_all().await?;
157        match all.get("auth") {
158            Some(doc::Value::Doc(auth_doc)) => Ok(auth_doc.clone().into()),
159            _ => Ok(AuthSettings::new()),
160        }
161    }
162
163    /// Set an authentication key in the settings
164    ///
165    /// This method provides upsert behavior for authentication keys:
166    /// - If the pubkey doesn't exist: creates the key entry
167    /// - If the pubkey exists: updates the key with new permissions/status/name
168    ///
169    /// Keys are stored by pubkey (the cryptographic public key string).
170    /// The AuthKey contains optional name metadata and permission information.
171    ///
172    /// Writes are incremental: only the specific key path is written to the
173    /// entry, not the full auth state. The CRDT merge produces the correct
174    /// merged view when reading.
175    ///
176    /// # Arguments
177    /// * `pubkey` - The public key
178    /// * `key` - The AuthKey containing name, permissions, and status
179    ///
180    /// # Returns
181    /// Result indicating success or failure
182    pub async fn set_auth_key(&self, pubkey: &PublicKey, key: AuthKey) -> Result<()> {
183        let pubkey_str = pubkey.to_string();
184        self.inner.set(format!("auth.keys.{pubkey_str}"), key).await
185    }
186
187    /// Set the global authentication permission
188    ///
189    /// Stores the global permission at the `auth.global` path, separate from
190    /// individual key entries in the `auth.keys` namespace.
191    ///
192    /// # Arguments
193    /// * `key` - The AuthKey containing the global permission level and status
194    ///
195    /// # Returns
196    /// Result indicating success or failure
197    pub async fn set_global_auth_key(&self, key: AuthKey) -> Result<()> {
198        self.inner.set("auth.global", key).await
199    }
200
201    /// Get the global authentication permission
202    ///
203    /// Reads from the `auth.global` path via an auth snapshot.
204    ///
205    /// # Returns
206    /// AuthKey if the global permission is configured, or error if not present
207    pub async fn get_global_auth_key(&self) -> Result<AuthKey> {
208        let auth_settings = self.auth_snapshot().await?;
209        auth_settings.get_global_key()
210    }
211
212    /// Get an authentication key from the settings by public key
213    ///
214    /// # Arguments
215    /// * `pubkey` - The public key to retrieve
216    ///
217    /// # Returns
218    /// AuthKey if found, or error if not present or operation fails
219    pub async fn get_auth_key(&self, pubkey: &PublicKey) -> Result<AuthKey> {
220        let auth_settings = self.auth_snapshot().await?;
221        auth_settings.get_key_by_pubkey(pubkey)
222    }
223
224    /// Rename an authentication key in the settings
225    ///
226    /// Updates only the display name of an existing key, preserving its
227    /// permissions and status.
228    ///
229    /// # Arguments
230    /// * `pubkey` - The public key of the key to rename
231    /// * `name` - The new display name, or None to remove the name
232    ///
233    /// # Returns
234    /// Result indicating success or failure
235    pub async fn rename_auth_key(&self, pubkey: &PublicKey, name: Option<&str>) -> Result<()> {
236        let auth = self.auth_snapshot().await?;
237        let mut key = auth.get_key_by_pubkey(pubkey)?;
238        key.set_name(name);
239        self.set_auth_key(pubkey, key).await
240    }
241
242    /// Revoke an authentication key in the settings
243    ///
244    /// # Arguments
245    /// * `pubkey` - The public key of the key to revoke
246    ///
247    /// # Returns
248    /// Result indicating success or failure
249    pub async fn revoke_auth_key(&self, pubkey: &PublicKey) -> Result<()> {
250        let auth = self.auth_snapshot().await?;
251        let mut key = auth.get_key_by_pubkey(pubkey)?;
252        key.set_status(KeyStatus::Revoked);
253        self.set_auth_key(pubkey, key).await
254    }
255
256    /// Add a delegated tree reference to the settings
257    ///
258    /// The delegation is stored by root tree ID, extracted from `tree_ref.tree.root`.
259    ///
260    /// # Arguments
261    /// * `tree_ref` - The delegated tree reference to add
262    ///
263    /// # Returns
264    /// Result indicating success or failure
265    pub async fn add_delegated_tree(&self, tree_ref: DelegatedTreeRef) -> Result<()> {
266        let root_id = tree_ref.tree.root.to_string();
267        self.inner
268            .set(format!("auth.delegations.{root_id}"), tree_ref)
269            .await
270    }
271
272    /// Get the auth document for validation purposes
273    ///
274    /// This returns the raw Doc containing auth configuration, suitable for
275    /// use with AuthValidator and other validation components that expect
276    /// the raw CRDT state.
277    ///
278    /// # Returns
279    /// A Doc containing the auth configuration
280    pub async fn get_auth_doc_for_validation(&self) -> Result<Doc> {
281        let auth_settings = self.auth_snapshot().await?;
282        Ok(auth_settings.as_doc().clone())
283    }
284
285    /// Get access to the underlying DocStore for advanced operations
286    ///
287    /// This provides direct access to the DocStore for cases where the
288    /// SettingsStore abstraction is insufficient.
289    ///
290    /// # Returns
291    /// A reference to the underlying DocStore
292    pub fn as_doc_store(&self) -> &DocStore {
293        &self.inner
294    }
295}
296
297#[cfg(test)]
298mod tests {
299    use super::*;
300    use crate::{
301        Database, Error, Instance,
302        auth::{
303            crypto::PublicKey,
304            types::{KeyStatus, Permission},
305        },
306        backend::database::InMemory,
307        crdt::Doc,
308        store::Store,
309    };
310
311    async fn create_test_database() -> (Instance, Database) {
312        // Bootstrap "test" as the initial user, who is automatically Admin.
313        let (instance, mut user) = Instance::create_backend(
314            Box::new(InMemory::new()),
315            crate::NewUser::passwordless("test"),
316        )
317        .await
318        .expect("Failed to create test instance");
319
320        let (database, _) = user.new_database().build().await.unwrap();
321
322        // Set initial database name using transaction
323        let transaction = database.new_transaction().await.unwrap();
324        let settings_store = SettingsStore::new(&transaction).unwrap();
325        settings_store.set_name("test_db").await.unwrap();
326        transaction.commit().await.unwrap();
327
328        (instance, database)
329    }
330
331    #[tokio::test]
332    async fn test_settings_store_creation() {
333        let (_instance, database) = create_test_database().await;
334        let transaction = database.new_transaction().await.unwrap();
335        let settings_store = SettingsStore::new(&transaction).unwrap();
336
337        // Should be able to create successfully
338        assert!(settings_store.as_doc_store().name() == "_settings");
339    }
340
341    #[tokio::test]
342    async fn test_name_operations() {
343        let (_instance, database) = create_test_database().await;
344        let transaction = database.new_transaction().await.unwrap();
345        let settings_store = SettingsStore::new(&transaction).unwrap();
346
347        // Should be able to get the initial name
348        let name = settings_store.get_name().await.unwrap();
349        assert_eq!(name, "test_db");
350
351        // Should be able to set a new name
352        settings_store.set_name("updated_name").await.unwrap();
353        let updated_name = settings_store.get_name().await.unwrap();
354        assert_eq!(updated_name, "updated_name");
355    }
356
357    #[tokio::test]
358    async fn test_auth_settings_integration() {
359        let (_instance, database) = create_test_database().await;
360        let transaction = database.new_transaction().await.unwrap();
361        let settings_store = SettingsStore::new(&transaction).unwrap();
362
363        // Get the initial auth settings (may contain a default key from database creation)
364        let initial_auth_settings = settings_store.auth_snapshot().await.unwrap();
365        let initial_key_count = initial_auth_settings.get_all_keys().unwrap().len();
366
367        // Should be able to add an auth key (stored by pubkey)
368        let pubkey = PublicKey::random();
369        let auth_key = AuthKey::active(Some("new_test_key"), Permission::Admin(1));
370
371        settings_store
372            .set_auth_key(&pubkey, auth_key.clone())
373            .await
374            .unwrap();
375
376        // Should be able to retrieve the key by pubkey
377        let retrieved_key = settings_store.get_auth_key(&pubkey).await.unwrap();
378        assert_eq!(retrieved_key.name(), auth_key.name());
379        assert_eq!(retrieved_key.permissions(), auth_key.permissions());
380        assert_eq!(retrieved_key.status(), auth_key.status());
381
382        // Should have one more key than initially
383        let final_auth_settings = settings_store.auth_snapshot().await.unwrap();
384        let final_key_count = final_auth_settings.get_all_keys().unwrap().len();
385        assert_eq!(final_key_count, initial_key_count + 1);
386    }
387
388    #[tokio::test]
389    async fn test_auth_key_operations() {
390        let (_instance, database) = create_test_database().await;
391        let transaction = database.new_transaction().await.unwrap();
392        let settings_store = SettingsStore::new(&transaction).unwrap();
393
394        let pubkey = PublicKey::random();
395        let auth_key = AuthKey::active(Some("laptop"), Permission::Write(5));
396
397        // Add key (stored by pubkey)
398        settings_store
399            .set_auth_key(&pubkey, auth_key.clone())
400            .await
401            .unwrap();
402
403        // Verify key exists (lookup by pubkey)
404        let retrieved = settings_store.get_auth_key(&pubkey).await.unwrap();
405        assert_eq!(retrieved.name(), Some("laptop"));
406        assert_eq!(retrieved.status(), &KeyStatus::Active);
407
408        // Revoke key (by pubkey)
409        settings_store.revoke_auth_key(&pubkey).await.unwrap();
410
411        // Verify key is revoked
412        let revoked_key = settings_store.get_auth_key(&pubkey).await.unwrap();
413        assert_eq!(revoked_key.status(), &KeyStatus::Revoked);
414    }
415
416    #[tokio::test]
417    async fn test_rename_auth_key() {
418        let (_instance, database) = create_test_database().await;
419        let transaction = database.new_transaction().await.unwrap();
420        let settings_store = SettingsStore::new(&transaction).unwrap();
421
422        let pubkey = PublicKey::random();
423        let auth_key = AuthKey::active(Some("laptop"), Permission::Write(5));
424
425        settings_store
426            .set_auth_key(&pubkey, auth_key)
427            .await
428            .unwrap();
429
430        // Rename the key
431        settings_store
432            .rename_auth_key(&pubkey, Some("desktop"))
433            .await
434            .unwrap();
435
436        // Verify name changed but permissions preserved
437        let renamed = settings_store.get_auth_key(&pubkey).await.unwrap();
438        assert_eq!(renamed.name(), Some("desktop"));
439        assert_eq!(renamed.permissions(), &Permission::Write(5));
440        assert_eq!(renamed.status(), &KeyStatus::Active);
441
442        // Rename to None (remove name)
443        settings_store.rename_auth_key(&pubkey, None).await.unwrap();
444
445        let unnamed = settings_store.get_auth_key(&pubkey).await.unwrap();
446        assert_eq!(unnamed.name(), None);
447        assert_eq!(unnamed.permissions(), &Permission::Write(5));
448    }
449
450    #[tokio::test]
451    async fn test_multiple_auth_key_writes() {
452        let (_instance, database) = create_test_database().await;
453        let transaction = database.new_transaction().await.unwrap();
454        let settings_store = SettingsStore::new(&transaction).unwrap();
455
456        // Generate pubkeys for the test
457        let pubkey1 = PublicKey::random();
458        let pubkey2 = PublicKey::random();
459
460        // Write keys directly via set_auth_key
461        settings_store
462            .set_auth_key(
463                &pubkey1,
464                AuthKey::active(Some("admin"), Permission::Admin(1)),
465            )
466            .await
467            .unwrap();
468        settings_store
469            .set_auth_key(
470                &pubkey2,
471                AuthKey::active(Some("writer"), Permission::Write(5)),
472            )
473            .await
474            .unwrap();
475
476        // Verify both keys were added (plus any existing keys from database creation)
477        let auth_settings = settings_store.auth_snapshot().await.unwrap();
478        let all_keys = auth_settings.get_all_keys().unwrap();
479        assert!(all_keys.len() >= 2); // At least the two we added
480        assert!(all_keys.contains_key(&pubkey1.to_string()));
481        assert!(all_keys.contains_key(&pubkey2.to_string()));
482    }
483
484    #[tokio::test]
485    async fn test_auth_doc_for_validation() {
486        let (_instance, database) = create_test_database().await;
487        let transaction = database.new_transaction().await.unwrap();
488        let settings_store = SettingsStore::new(&transaction).unwrap();
489
490        // Add a key (stored by pubkey)
491        let pubkey = PublicKey::random();
492        let auth_key = AuthKey::active(Some("validator"), Permission::Read);
493        settings_store
494            .set_auth_key(&pubkey, auth_key)
495            .await
496            .unwrap();
497
498        // Get auth doc for validation
499        let auth_doc = settings_store.get_auth_doc_for_validation().await.unwrap();
500
501        // Should contain the key under keys.{pubkey} as a Doc
502        let auth_settings: AuthSettings = auth_doc.into();
503        let validator_key = auth_settings.get_key_by_pubkey(&pubkey).unwrap();
504        assert_eq!(validator_key.name(), Some("validator"));
505    }
506
507    /// Verifies that SettingsStore writes incremental entries: each entry
508    /// only contains the keys written in that transaction, not the full
509    /// accumulated state. The CRDT merge produces the correct merged view.
510    #[tokio::test]
511    async fn test_auth_settings_entries_are_incremental() {
512        let (_instance, database) = create_test_database().await;
513
514        let pubkey_a = PublicKey::random();
515        let pubkey_b = PublicKey::random();
516
517        // Txn 1: Add key A via SettingsStore
518        let txn1 = database.new_transaction().await.unwrap();
519        let settings1 = SettingsStore::new(&txn1).unwrap();
520        settings1
521            .set_auth_key(
522                &pubkey_a,
523                AuthKey::active(Some("key_a"), Permission::Write(5)),
524            )
525            .await
526            .unwrap();
527        txn1.commit().await.unwrap();
528
529        // Txn 2: Add key B (key A was added in a previous entry)
530        let txn2 = database.new_transaction().await.unwrap();
531        let settings2 = SettingsStore::new(&txn2).unwrap();
532        settings2
533            .set_auth_key(
534                &pubkey_b,
535                AuthKey::active(Some("key_b"), Permission::Admin(1)),
536            )
537            .await
538            .unwrap();
539        let entry_id_2 = txn2.commit().await.unwrap();
540
541        // Inspect raw entry data - entry 2 should NOT contain key A (incremental)
542        let entry2 = database.get_entry(&entry_id_2).await.unwrap();
543        let raw_2: Doc = serde_json::from_slice(entry2.data("_settings").unwrap()).unwrap();
544
545        assert!(
546            raw_2.get(format!("auth.keys.{pubkey_a}")).is_none(),
547            "SettingsStore entry should NOT contain key A from prior entry (incremental)"
548        );
549        assert!(
550            raw_2.get(format!("auth.keys.{pubkey_b}")).is_some(),
551            "SettingsStore entry should contain key B"
552        );
553
554        // But reading through SettingsStore merges both entries correctly
555        let txn3 = database.new_transaction().await.unwrap();
556        let settings3 = SettingsStore::new(&txn3).unwrap();
557        let auth = settings3.auth_snapshot().await.unwrap();
558        assert!(
559            auth.get_key_by_pubkey(&pubkey_a).is_ok(),
560            "Merged view should contain key A"
561        );
562        assert!(
563            auth.get_key_by_pubkey(&pubkey_b).is_ok(),
564            "Merged view should contain key B"
565        );
566    }
567
568    #[tokio::test]
569    async fn test_error_handling() {
570        let (_instance, database) = create_test_database().await;
571        let transaction = database.new_transaction().await.unwrap();
572        let settings_store = SettingsStore::new(&transaction).unwrap();
573
574        // Getting non-existent auth key should return KeyNotFound error
575        let nonexistent_pubkey = PublicKey::random();
576        let result = settings_store.get_auth_key(&nonexistent_pubkey).await;
577        assert!(result.is_err());
578        if let Err(Error::Auth(ref auth_err)) = result {
579            assert!(auth_err.is_not_found());
580        } else {
581            panic!("Expected Auth(KeyNotFound) error");
582        }
583
584        // Revoking non-existent key should fail
585        let revoke_result = settings_store.revoke_auth_key(&nonexistent_pubkey).await;
586        assert!(revoke_result.is_err());
587    }
588}