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

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::new()
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.as_str().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        let backend = Box::new(InMemory::new());
313        let instance = Instance::open(backend)
314            .await
315            .expect("Failed to create test instance");
316
317        // Use User API to create database
318        instance.create_user("test", None).await.unwrap();
319        let mut user = instance.login_user("test", None).await.unwrap();
320        let key_id = user.add_private_key(None).await.unwrap();
321        let database = user.create_database(Doc::new(), &key_id).await.unwrap();
322
323        // Set initial database name using transaction
324        let transaction = database.new_transaction().await.unwrap();
325        let settings_store = SettingsStore::new(&transaction).unwrap();
326        settings_store.set_name("test_db").await.unwrap();
327        transaction.commit().await.unwrap();
328
329        (instance, database)
330    }
331
332    #[tokio::test]
333    async fn test_settings_store_creation() {
334        let (_instance, database) = create_test_database().await;
335        let transaction = database.new_transaction().await.unwrap();
336        let settings_store = SettingsStore::new(&transaction).unwrap();
337
338        // Should be able to create successfully
339        assert!(settings_store.as_doc_store().name() == "_settings");
340    }
341
342    #[tokio::test]
343    async fn test_name_operations() {
344        let (_instance, database) = create_test_database().await;
345        let transaction = database.new_transaction().await.unwrap();
346        let settings_store = SettingsStore::new(&transaction).unwrap();
347
348        // Should be able to get the initial name
349        let name = settings_store.get_name().await.unwrap();
350        assert_eq!(name, "test_db");
351
352        // Should be able to set a new name
353        settings_store.set_name("updated_name").await.unwrap();
354        let updated_name = settings_store.get_name().await.unwrap();
355        assert_eq!(updated_name, "updated_name");
356    }
357
358    #[tokio::test]
359    async fn test_auth_settings_integration() {
360        let (_instance, database) = create_test_database().await;
361        let transaction = database.new_transaction().await.unwrap();
362        let settings_store = SettingsStore::new(&transaction).unwrap();
363
364        // Get the initial auth settings (may contain a default key from database creation)
365        let initial_auth_settings = settings_store.auth_snapshot().await.unwrap();
366        let initial_key_count = initial_auth_settings.get_all_keys().unwrap().len();
367
368        // Should be able to add an auth key (stored by pubkey)
369        let pubkey = PublicKey::random();
370        let auth_key = AuthKey::active(Some("new_test_key"), Permission::Admin(1));
371
372        settings_store
373            .set_auth_key(&pubkey, auth_key.clone())
374            .await
375            .unwrap();
376
377        // Should be able to retrieve the key by pubkey
378        let retrieved_key = settings_store.get_auth_key(&pubkey).await.unwrap();
379        assert_eq!(retrieved_key.name(), auth_key.name());
380        assert_eq!(retrieved_key.permissions(), auth_key.permissions());
381        assert_eq!(retrieved_key.status(), auth_key.status());
382
383        // Should have one more key than initially
384        let final_auth_settings = settings_store.auth_snapshot().await.unwrap();
385        let final_key_count = final_auth_settings.get_all_keys().unwrap().len();
386        assert_eq!(final_key_count, initial_key_count + 1);
387    }
388
389    #[tokio::test]
390    async fn test_auth_key_operations() {
391        let (_instance, database) = create_test_database().await;
392        let transaction = database.new_transaction().await.unwrap();
393        let settings_store = SettingsStore::new(&transaction).unwrap();
394
395        let pubkey = PublicKey::random();
396        let auth_key = AuthKey::active(Some("laptop"), Permission::Write(5));
397
398        // Add key (stored by pubkey)
399        settings_store
400            .set_auth_key(&pubkey, auth_key.clone())
401            .await
402            .unwrap();
403
404        // Verify key exists (lookup by pubkey)
405        let retrieved = settings_store.get_auth_key(&pubkey).await.unwrap();
406        assert_eq!(retrieved.name(), Some("laptop"));
407        assert_eq!(retrieved.status(), &KeyStatus::Active);
408
409        // Revoke key (by pubkey)
410        settings_store.revoke_auth_key(&pubkey).await.unwrap();
411
412        // Verify key is revoked
413        let revoked_key = settings_store.get_auth_key(&pubkey).await.unwrap();
414        assert_eq!(revoked_key.status(), &KeyStatus::Revoked);
415    }
416
417    #[tokio::test]
418    async fn test_rename_auth_key() {
419        let (_instance, database) = create_test_database().await;
420        let transaction = database.new_transaction().await.unwrap();
421        let settings_store = SettingsStore::new(&transaction).unwrap();
422
423        let pubkey = PublicKey::random();
424        let auth_key = AuthKey::active(Some("laptop"), Permission::Write(5));
425
426        settings_store
427            .set_auth_key(&pubkey, auth_key)
428            .await
429            .unwrap();
430
431        // Rename the key
432        settings_store
433            .rename_auth_key(&pubkey, Some("desktop"))
434            .await
435            .unwrap();
436
437        // Verify name changed but permissions preserved
438        let renamed = settings_store.get_auth_key(&pubkey).await.unwrap();
439        assert_eq!(renamed.name(), Some("desktop"));
440        assert_eq!(renamed.permissions(), &Permission::Write(5));
441        assert_eq!(renamed.status(), &KeyStatus::Active);
442
443        // Rename to None (remove name)
444        settings_store.rename_auth_key(&pubkey, None).await.unwrap();
445
446        let unnamed = settings_store.get_auth_key(&pubkey).await.unwrap();
447        assert_eq!(unnamed.name(), None);
448        assert_eq!(unnamed.permissions(), &Permission::Write(5));
449    }
450
451    #[tokio::test]
452    async fn test_multiple_auth_key_writes() {
453        let (_instance, database) = create_test_database().await;
454        let transaction = database.new_transaction().await.unwrap();
455        let settings_store = SettingsStore::new(&transaction).unwrap();
456
457        // Generate pubkeys for the test
458        let pubkey1 = PublicKey::random();
459        let pubkey2 = PublicKey::random();
460
461        // Write keys directly via set_auth_key
462        settings_store
463            .set_auth_key(
464                &pubkey1,
465                AuthKey::active(Some("admin"), Permission::Admin(1)),
466            )
467            .await
468            .unwrap();
469        settings_store
470            .set_auth_key(
471                &pubkey2,
472                AuthKey::active(Some("writer"), Permission::Write(5)),
473            )
474            .await
475            .unwrap();
476
477        // Verify both keys were added (plus any existing keys from database creation)
478        let auth_settings = settings_store.auth_snapshot().await.unwrap();
479        let all_keys = auth_settings.get_all_keys().unwrap();
480        assert!(all_keys.len() >= 2); // At least the two we added
481        assert!(all_keys.contains_key(&pubkey1.to_string()));
482        assert!(all_keys.contains_key(&pubkey2.to_string()));
483    }
484
485    #[tokio::test]
486    async fn test_auth_doc_for_validation() {
487        let (_instance, database) = create_test_database().await;
488        let transaction = database.new_transaction().await.unwrap();
489        let settings_store = SettingsStore::new(&transaction).unwrap();
490
491        // Add a key (stored by pubkey)
492        let pubkey = PublicKey::random();
493        let auth_key = AuthKey::active(Some("validator"), Permission::Read);
494        settings_store
495            .set_auth_key(&pubkey, auth_key)
496            .await
497            .unwrap();
498
499        // Get auth doc for validation
500        let auth_doc = settings_store.get_auth_doc_for_validation().await.unwrap();
501
502        // Should contain the key under keys.{pubkey} as a Doc
503        let auth_settings: AuthSettings = auth_doc.into();
504        let validator_key = auth_settings.get_key_by_pubkey(&pubkey).unwrap();
505        assert_eq!(validator_key.name(), Some("validator"));
506    }
507
508    /// Verifies that SettingsStore writes incremental entries: each entry
509    /// only contains the keys written in that transaction, not the full
510    /// accumulated state. The CRDT merge produces the correct merged view.
511    #[tokio::test]
512    async fn test_auth_settings_entries_are_incremental() {
513        let (_instance, database) = create_test_database().await;
514
515        let pubkey_a = PublicKey::random();
516        let pubkey_b = PublicKey::random();
517
518        // Txn 1: Add key A via SettingsStore
519        let txn1 = database.new_transaction().await.unwrap();
520        let settings1 = SettingsStore::new(&txn1).unwrap();
521        settings1
522            .set_auth_key(
523                &pubkey_a,
524                AuthKey::active(Some("key_a"), Permission::Write(5)),
525            )
526            .await
527            .unwrap();
528        txn1.commit().await.unwrap();
529
530        // Txn 2: Add key B (key A was added in a previous entry)
531        let txn2 = database.new_transaction().await.unwrap();
532        let settings2 = SettingsStore::new(&txn2).unwrap();
533        settings2
534            .set_auth_key(
535                &pubkey_b,
536                AuthKey::active(Some("key_b"), Permission::Admin(1)),
537            )
538            .await
539            .unwrap();
540        let entry_id_2 = txn2.commit().await.unwrap();
541
542        // Inspect raw entry data - entry 2 should NOT contain key A (incremental)
543        let entry2 = database.get_entry(&entry_id_2).await.unwrap();
544        let raw_2: Doc = serde_json::from_str(entry2.data("_settings").unwrap()).unwrap();
545
546        assert!(
547            raw_2.get(format!("auth.keys.{pubkey_a}")).is_none(),
548            "SettingsStore entry should NOT contain key A from prior entry (incremental)"
549        );
550        assert!(
551            raw_2.get(format!("auth.keys.{pubkey_b}")).is_some(),
552            "SettingsStore entry should contain key B"
553        );
554
555        // But reading through SettingsStore merges both entries correctly
556        let txn3 = database.new_transaction().await.unwrap();
557        let settings3 = SettingsStore::new(&txn3).unwrap();
558        let auth = settings3.auth_snapshot().await.unwrap();
559        assert!(
560            auth.get_key_by_pubkey(&pubkey_a).is_ok(),
561            "Merged view should contain key A"
562        );
563        assert!(
564            auth.get_key_by_pubkey(&pubkey_b).is_ok(),
565            "Merged view should contain key B"
566        );
567    }
568
569    #[tokio::test]
570    async fn test_error_handling() {
571        let (_instance, database) = create_test_database().await;
572        let transaction = database.new_transaction().await.unwrap();
573        let settings_store = SettingsStore::new(&transaction).unwrap();
574
575        // Getting non-existent auth key should return KeyNotFound error
576        let nonexistent_pubkey = PublicKey::random();
577        let result = settings_store.get_auth_key(&nonexistent_pubkey).await;
578        assert!(result.is_err());
579        if let Err(Error::Auth(auth_err)) = result {
580            assert!(auth_err.is_not_found());
581        } else {
582            panic!("Expected Auth(KeyNotFound) error");
583        }
584
585        // Revoking non-existent key should fail
586        let revoke_result = settings_store.revoke_auth_key(&nonexistent_pubkey).await;
587        assert!(revoke_result.is_err());
588    }
589}