diff --git a/jans-cedarling/cedarling/src/data/api.rs b/jans-cedarling/cedarling/src/data/api.rs new file mode 100644 index 00000000000..e4086c67ec8 --- /dev/null +++ b/jans-cedarling/cedarling/src/data/api.rs @@ -0,0 +1,113 @@ +// This software is available under the Apache-2.0 license. +// See https://www.apache.org/licenses/LICENSE-2.0.txt for full text. +// +// Copyright (c) 2024, Gluu, Inc. + +//! Data API trait and supporting types for the DataStore. + +use std::time::Duration; + +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +use super::entry::DataEntry; +use super::error::DataError; + +/// Statistics about the DataStore. +/// +/// Provides insight into the current state and usage of the data store. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct DataStoreStats { + /// Number of entries currently stored + pub entry_count: usize, + /// Maximum number of entries allowed (0 = unlimited) + pub max_entries: usize, + /// Maximum size per entry in bytes (0 = unlimited) + pub max_entry_size: usize, + /// Whether metrics tracking is enabled + pub metrics_enabled: bool, +} + +/// Trait defining the public API for data store operations. +/// +/// This trait provides a consistent interface for pushing, retrieving, +/// and managing data in the store. All operations are thread-safe. +/// +/// The [`Cedarling`](crate::Cedarling) struct implements this trait, providing +/// access to the data store through the main application instance. +/// +/// # Example +/// +/// ```no_run +/// use cedarling::{Cedarling, DataApi, DataError}; +/// use serde_json::json; +/// use std::time::Duration; +/// +/// fn use_data_api(cedarling: &Cedarling) -> Result<(), DataError> { +/// // Push data with a 5-minute TTL +/// cedarling.push_data( +/// "user_roles", +/// json!(["admin", "editor"]), +/// Some(Duration::from_secs(300)), +/// )?; +/// +/// // Retrieve data +/// if let Some(roles) = cedarling.get_data("user_roles")? { +/// println!("User roles: {}", roles); +/// } +/// +/// // List all entries with metadata +/// for entry in cedarling.list_data()? { +/// println!("Key: {}, Type: {:?}", entry.key, entry.data_type); +/// } +/// +/// // Get store statistics +/// let stats = cedarling.get_stats()?; +/// println!("Entries: {}/{}", stats.entry_count, stats.max_entries); +/// +/// // Remove data +/// cedarling.remove_data("user_roles")?; +/// +/// // Clear all data +/// cedarling.clear_data()?; +/// +/// Ok(()) +/// } +/// ``` +pub trait DataApi { + /// Push a value into the store with an optional TTL. + /// + /// If the key already exists, the value will be replaced. + /// If TTL is not provided, the default TTL from configuration is used. + fn push_data(&self, key: &str, value: Value, ttl: Option) -> Result<(), DataError>; + + /// Get a value from the store by key. + /// + /// Returns `Ok(None)` if the key doesn't exist or the entry has expired. + /// If metrics are enabled, increments the access count for the entry. + fn get_data(&self, key: &str) -> Result, DataError>; + + /// Get a data entry with full metadata by key. + /// + /// Returns `Ok(None)` if the key doesn't exist or the entry has expired. + /// Includes metadata like creation time, expiration, access count, and type. + fn get_data_entry(&self, key: &str) -> Result, DataError>; + + /// Remove a value from the store by key. + /// + /// Returns `Ok(true)` if the key existed and was removed, `Ok(false)` otherwise. + fn remove_data(&self, key: &str) -> Result; + + /// Clear all entries from the store. + fn clear_data(&self) -> Result<(), DataError>; + + /// List all entries with their metadata. + /// + /// Returns a vector of `DataEntry` containing key, value, type, and timing metadata. + fn list_data(&self) -> Result, DataError>; + + /// Get statistics about the data store. + /// + /// Returns current entry count, capacity limits, and configuration state. + fn get_stats(&self) -> Result; +} diff --git a/jans-cedarling/cedarling/src/data/config.rs b/jans-cedarling/cedarling/src/data/config.rs index 2765994ff12..4a5d947face 100644 --- a/jans-cedarling/cedarling/src/data/config.rs +++ b/jans-cedarling/cedarling/src/data/config.rs @@ -19,7 +19,7 @@ use std::time::Duration; /// /// ``` /// use std::time::Duration; -/// use cedarling::data::DataStoreConfig; +/// use cedarling::DataStoreConfig; /// /// // No expiration by default, but cap at 1 hour when explicitly set /// let config = DataStoreConfig { diff --git a/jans-cedarling/cedarling/src/data/mod.rs b/jans-cedarling/cedarling/src/data/mod.rs index 20ffcbdcf8e..fe885257881 100644 --- a/jans-cedarling/cedarling/src/data/mod.rs +++ b/jans-cedarling/cedarling/src/data/mod.rs @@ -4,15 +4,18 @@ // Copyright (c) 2024, Gluu, Inc. //! # Data Store Module +//! //! Provides key-value storage for pushed data with TTL support, capacity management, //! and thread-safe concurrent access. +mod api; mod config; mod entry; mod error; mod store; +pub use api::{DataApi, DataStoreStats}; pub use config::{ConfigValidationError, DataStoreConfig}; pub use entry::{CedarType, DataEntry}; pub use error::DataError; -pub use store::DataStore; +pub(crate) use store::DataStore; diff --git a/jans-cedarling/cedarling/src/data/store.rs b/jans-cedarling/cedarling/src/data/store.rs index 5676f1f9c78..aaa82d33d45 100644 --- a/jans-cedarling/cedarling/src/data/store.rs +++ b/jans-cedarling/cedarling/src/data/store.rs @@ -32,7 +32,7 @@ const INFINITE_TTL_SECS: i64 = 315_360_000; // 10 years in seconds /// - `config.default_ttl = None` means entries without explicit TTL will effectively never expire (10 years) /// - `config.max_ttl = None` means no upper limit on TTL values (10 years max) /// - When both `ttl` parameter and `config.default_ttl` are `None`, entries use the infinite TTL -pub struct DataStore { +pub(crate) struct DataStore { storage: RwLock>, config: DataStoreConfig, } @@ -48,7 +48,7 @@ impl DataStore { /// # Errors /// /// Returns `ConfigValidationError` if the configuration is invalid. - pub fn new(config: DataStoreConfig) -> Result { + pub(crate) fn new(config: DataStoreConfig) -> Result { // Validate configuration before creating the store config.validate()?; @@ -94,7 +94,12 @@ impl DataStore { /// - Value size exceeds `max_entry_size` /// - Storage capacity is exceeded /// - TTL exceeds `max_ttl` - pub fn push(&self, key: &str, value: Value, ttl: Option) -> Result<(), DataError> { + pub(crate) fn push( + &self, + key: &str, + value: Value, + ttl: Option, + ) -> Result<(), DataError> { // Validate key if key.is_empty() { return Err(DataError::InvalidKey); @@ -162,7 +167,7 @@ impl DataStore { /// /// Returns `None` if the key doesn't exist or the entry has expired. /// If metrics are enabled, increments the access count for the entry. - pub fn get(&self, key: &str) -> Option { + pub(crate) fn get(&self, key: &str) -> Option { self.get_entry(key).map(|entry| entry.value) } @@ -171,7 +176,7 @@ impl DataStore { /// Returns `None` if the key doesn't exist or the entry has expired. /// If metrics are enabled, increments the access count for the entry. /// Uses read lock initially for better concurrency, upgrading to write lock only when metrics are enabled. - pub fn get_entry(&self, key: &str) -> Option { + pub(crate) fn get_entry(&self, key: &str) -> Option { // First, try with read lock for better concurrency let entry = { let storage = self.storage.read().expect(RWLOCK_EXPECT_MESSAGE); @@ -222,28 +227,28 @@ impl DataStore { /// /// Returns `true` if the key existed and was removed, `false` otherwise. /// Uses write lock for exclusive access. - pub fn remove(&self, key: &str) -> bool { + pub(crate) fn remove(&self, key: &str) -> bool { let mut storage = self.storage.write().expect(RWLOCK_EXPECT_MESSAGE); storage.pop(key).is_some() } /// Clear all entries from the store. /// Uses write lock for exclusive access. - pub fn clear(&self) { + pub(crate) fn clear(&self) { let mut storage = self.storage.write().expect(RWLOCK_EXPECT_MESSAGE); storage.clear(); } /// Get the number of entries currently in the store. /// Uses read lock for concurrent access. - pub fn count(&self) -> usize { + pub(crate) fn count(&self) -> usize { let storage = self.storage.read().expect(RWLOCK_EXPECT_MESSAGE); storage.len() } /// List all keys currently in the store. /// Uses read lock for concurrent access. - pub fn list_keys(&self) -> Vec { + pub(crate) fn list_keys(&self) -> Vec { let storage = self.storage.read().expect(RWLOCK_EXPECT_MESSAGE); storage.get_keys() } @@ -252,13 +257,28 @@ impl DataStore { /// /// This is used for context injection during authorization. /// Returns only the values, not the metadata. - pub fn get_all(&self) -> HashMap { + pub(crate) fn get_all(&self) -> HashMap { let storage = self.storage.read().expect(RWLOCK_EXPECT_MESSAGE); storage .iter() .map(|(k, entry)| (k.clone(), entry.value.clone())) .collect() } + + /// List all entries with their full metadata, excluding expired entries. + pub(crate) fn list_entries(&self) -> Vec { + let storage = self.storage.read().expect(RWLOCK_EXPECT_MESSAGE); + storage + .iter() + .filter(|(_, entry)| !entry.is_expired()) + .map(|(_, entry)| entry.clone()) + .collect() + } + + /// Get the configuration for this store. + pub(crate) fn config(&self) -> &DataStoreConfig { + &self.config + } } /// Convert `std::time::Duration` to `chrono::Duration`. @@ -822,4 +842,45 @@ mod tests { "expected DataStore::new() to return ConfigValidationError when default_ttl exceeds max_ttl" ); } + + // ========================================================================== + // Additional store method tests + // ========================================================================== + + #[test] + fn test_list_entries() { + let store = create_test_store(); + + store + .push("alpha", json!("a"), None) + .expect("push should succeed"); + store + .push("beta", json!("b"), None) + .expect("push should succeed"); + + let entries = store.list_entries(); + + assert_eq!(entries.len(), 2); + + let keys: Vec<&str> = entries.iter().map(|e| e.key.as_str()).collect(); + assert!(keys.contains(&"alpha")); + assert!(keys.contains(&"beta")); + } + + #[test] + fn test_config_accessor() { + let config = DataStoreConfig { + max_entries: 100, + max_entry_size: 512, + enable_metrics: true, + ..Default::default() + }; + let store = DataStore::new(config).expect("should create store"); + + let retrieved_config = store.config(); + + assert_eq!(retrieved_config.max_entries, 100); + assert_eq!(retrieved_config.max_entry_size, 512); + assert!(retrieved_config.enable_metrics); + } } diff --git a/jans-cedarling/cedarling/src/lib.rs b/jans-cedarling/cedarling/src/lib.rs index 63a2113670b..700c21111ea 100644 --- a/jans-cedarling/cedarling/src/lib.rs +++ b/jans-cedarling/cedarling/src/lib.rs @@ -35,7 +35,11 @@ mod tests; use std::sync::Arc; pub use crate::common::json_rules::JsonRule; -pub use crate::data::{CedarType, ConfigValidationError, DataEntry, DataStoreConfig}; +use crate::data::DataStore; +pub use crate::data::{ + CedarType, ConfigValidationError, DataApi, DataEntry, DataError, DataStoreConfig, + DataStoreStats, +}; pub use crate::init::policy_store::{PolicyStoreLoadError, load_policy_store}; use crate::log::BaseLogEntry; #[cfg(test)] @@ -99,6 +103,7 @@ pub enum InitCedarlingError { pub struct Cedarling { log: log::Logger, authz: Arc, + data: Arc, } impl Cedarling { @@ -156,9 +161,17 @@ impl Cedarling { log_policy_store_metadata(&log, metadata); } + // Initialize data store with default configuration + // TODO: Add DataStoreConfig to BootstrapConfig + let data = Arc::new( + DataStore::new(DataStoreConfig::default()) + .expect("default DataStoreConfig should always be valid"), + ); + Ok(Cedarling { log, authz: service_factory.authz_service().await?, + data, }) } @@ -331,3 +344,47 @@ impl LogStorage for Cedarling { self.log.get_logs_by_request_id_and_tag(id, tag) } } + +// implements DataApi for Cedarling +// provides public interface for pushing and retrieving data +impl DataApi for Cedarling { + fn push_data( + &self, + key: &str, + value: serde_json::Value, + ttl: Option, + ) -> Result<(), DataError> { + self.data.push(key, value, ttl) + } + + fn get_data(&self, key: &str) -> Result, DataError> { + Ok(self.data.get(key)) + } + + fn get_data_entry(&self, key: &str) -> Result, DataError> { + Ok(self.data.get_entry(key)) + } + + fn remove_data(&self, key: &str) -> Result { + Ok(self.data.remove(key)) + } + + fn clear_data(&self) -> Result<(), DataError> { + self.data.clear(); + Ok(()) + } + + fn list_data(&self) -> Result, DataError> { + Ok(self.data.list_entries()) + } + + fn get_stats(&self) -> Result { + let config = self.data.config(); + Ok(DataStoreStats { + entry_count: self.data.count(), + max_entries: config.max_entries, + max_entry_size: config.max_entry_size, + metrics_enabled: config.enable_metrics, + }) + } +}