From a6bbb104b911c08f6f5f181e7557991cc9985bab Mon Sep 17 00:00:00 2001 From: haileyesus2433 Date: Thu, 22 Jan 2026 08:39:57 -0500 Subject: [PATCH 1/4] feat(data): introduce DataApi trait and enhance DataStore functionality - Added `DataApi` trait to define a consistent interface for data store operations, including methods for pushing, retrieving, and managing data. - Implemented `DataStoreStats` struct to provide insights into the data store's state and usage. - Updated `DataStore` to implement the `DataApi` trait, enhancing its functionality with new methods for data management. - Enhanced documentation for the data module, including examples and descriptions of new components. Signed-off-by: haileyesus2433 --- jans-cedarling/cedarling/src/data/api.rs | 98 +++++++++ jans-cedarling/cedarling/src/data/mod.rs | 43 ++++ jans-cedarling/cedarling/src/data/store.rs | 227 +++++++++++++++++++++ jans-cedarling/cedarling/src/lib.rs | 5 +- 4 files changed, 372 insertions(+), 1 deletion(-) create mode 100644 jans-cedarling/cedarling/src/data/api.rs diff --git a/jans-cedarling/cedarling/src/data/api.rs b/jans-cedarling/cedarling/src/data/api.rs new file mode 100644 index 00000000000..8aeb8f65742 --- /dev/null +++ b/jans-cedarling/cedarling/src/data/api.rs @@ -0,0 +1,98 @@ +// 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. +/// +/// # Examples +/// +/// ```ignore +/// use cedarling::data::{DataApi, DataStore, DataStoreConfig}; +/// use serde_json::json; +/// use std::time::Duration; +/// +/// let store = DataStore::new(DataStoreConfig::default())?; +/// +/// // Push data with TTL +/// store.push_data("user_roles", json!(["admin", "editor"]), Some(Duration::from_secs(300)))?; +/// +/// // Retrieve data +/// if let Some(roles) = store.get_data("user_roles")? { +/// println!("User roles: {}", roles); +/// } +/// +/// // List all entries with metadata +/// for entry in store.list_data()? { +/// println!("Key: {}, Type: {:?}", entry.key, entry.data_type); +/// } +/// +/// // Get store statistics +/// let stats = store.get_stats()?; +/// println!("Entries: {}/{}", stats.entry_count, stats.max_entries); +/// ``` +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/mod.rs b/jans-cedarling/cedarling/src/data/mod.rs index 20ffcbdcf8e..79ba2ca4179 100644 --- a/jans-cedarling/cedarling/src/data/mod.rs +++ b/jans-cedarling/cedarling/src/data/mod.rs @@ -4,14 +4,57 @@ // 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. +//! +//! # Overview +//! +//! The data module enables applications to push external data into Cedarling's +//! evaluation context. This data becomes available in Cedar policies through +//! the `context.data` namespace. +//! +//! # Components +//! +//! - [`DataStore`] - Thread-safe key-value store with TTL support +//! - [`DataApi`] - Trait defining the public data access interface +//! - [`DataEntry`] - Entry structure with value and metadata +//! - [`DataStoreStats`] - Statistics about store state +//! - [`DataStoreConfig`] - Configuration for storage limits and TTL +//! +//! # Example +//! +//! ```ignore +//! use cedarling::data::{DataApi, DataStore, DataStoreConfig}; +//! use serde_json::json; +//! use std::time::Duration; +//! +//! // Create store with default configuration +//! let store = DataStore::new(DataStoreConfig::default())?; +//! +//! // Push data with 5-minute TTL +//! store.push_data("user_context", json!({ +//! "department": "engineering", +//! "clearance_level": 3 +//! }), Some(Duration::from_secs(300)))?; +//! +//! // Retrieve and use data +//! if let Some(ctx) = store.get_data("user_context")? { +//! println!("User context: {}", ctx); +//! } +//! +//! // Check store statistics +//! let stats = store.get_stats()?; +//! println!("Entries: {}", stats.entry_count); +//! ``` +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; diff --git a/jans-cedarling/cedarling/src/data/store.rs b/jans-cedarling/cedarling/src/data/store.rs index 5676f1f9c78..24decf56759 100644 --- a/jans-cedarling/cedarling/src/data/store.rs +++ b/jans-cedarling/cedarling/src/data/store.rs @@ -11,6 +11,7 @@ use chrono::Duration as ChronoDuration; use serde_json::Value; use sparkv::{Config as SparKVConfig, Error as SparKVError, SparKV}; +use super::api::{DataApi, DataStoreStats}; use super::config::{ConfigValidationError, DataStoreConfig}; use super::entry::DataEntry; use super::error::DataError; @@ -259,6 +260,53 @@ impl DataStore { .map(|(k, entry)| (k.clone(), entry.value.clone())) .collect() } + + /// List all entries with their full metadata. + fn list_entries(&self) -> Vec { + let storage = self.storage.read().expect(RWLOCK_EXPECT_MESSAGE); + storage.iter().map(|(_, entry)| entry.clone()).collect() + } +} + +impl DataApi for DataStore { + fn push_data( + &self, + key: &str, + value: Value, + ttl: Option, + ) -> Result<(), DataError> { + self.push(key, value, ttl) + } + + fn get_data(&self, key: &str) -> Result, DataError> { + Ok(self.get(key)) + } + + fn get_data_entry(&self, key: &str) -> Result, DataError> { + Ok(self.get_entry(key)) + } + + fn remove_data(&self, key: &str) -> Result { + Ok(self.remove(key)) + } + + fn clear_data(&self) -> Result<(), DataError> { + self.clear(); + Ok(()) + } + + fn list_data(&self) -> Result, DataError> { + Ok(self.list_entries()) + } + + fn get_stats(&self) -> Result { + Ok(DataStoreStats { + entry_count: self.count(), + max_entries: self.config.max_entries, + max_entry_size: self.config.max_entry_size, + metrics_enabled: self.config.enable_metrics, + }) + } } /// Convert `std::time::Duration` to `chrono::Duration`. @@ -822,4 +870,183 @@ mod tests { "expected DataStore::new() to return ConfigValidationError when default_ttl exceeds max_ttl" ); } + + // ========================================================================== + // DataApi trait tests + // ========================================================================== + + #[test] + fn test_data_api_push_and_get() { + let store = create_test_store(); + + // Use trait methods + store + .push_data("api_key", json!({"role": "admin"}), None) + .expect("push_data should succeed"); + + let result = store.get_data("api_key").expect("get_data should succeed"); + assert_eq!(result, Some(json!({"role": "admin"}))); + + // Non-existent key + let missing = store + .get_data("nonexistent") + .expect("get_data should succeed for missing key"); + assert_eq!(missing, None); + } + + #[test] + fn test_data_api_get_entry() { + let store = create_test_store(); + + store + .push_data( + "entry_key", + json!("test_value"), + Some(StdDuration::from_secs(600)), + ) + .expect("push_data should succeed"); + + let entry = store + .get_data_entry("entry_key") + .expect("get_data_entry should succeed") + .expect("entry should exist"); + + assert_eq!(entry.key, "entry_key"); + assert_eq!(entry.value, json!("test_value")); + assert!(entry.expires_at.is_some()); + } + + #[test] + fn test_data_api_remove() { + let store = create_test_store(); + + store + .push_data("to_remove", json!(123), None) + .expect("push_data should succeed"); + + let removed = store + .remove_data("to_remove") + .expect("remove_data should succeed"); + assert!(removed, "should return true when key existed"); + + let removed_again = store + .remove_data("to_remove") + .expect("remove_data should succeed"); + assert!(!removed_again, "should return false when key doesn't exist"); + + assert_eq!( + store + .get_data("to_remove") + .expect("get_data should succeed"), + None + ); + } + + #[test] + fn test_data_api_clear() { + let store = create_test_store(); + + store + .push_data("key1", json!(1), None) + .expect("push_data should succeed"); + store + .push_data("key2", json!(2), None) + .expect("push_data should succeed"); + store + .push_data("key3", json!(3), None) + .expect("push_data should succeed"); + + assert_eq!(store.count(), 3); + + store.clear_data().expect("clear_data should succeed"); + + assert_eq!(store.count(), 0); + } + + #[test] + fn test_data_api_list() { + let store = create_test_store(); + + store + .push_data("alpha", json!("a"), None) + .expect("push_data should succeed"); + store + .push_data("beta", json!("b"), None) + .expect("push_data should succeed"); + + let entries = store.list_data().expect("list_data should succeed"); + + 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_data_api_stats() { + let config = DataStoreConfig { + max_entries: 100, + max_entry_size: 512, + enable_metrics: true, + ..Default::default() + }; + let store = DataStore::new(config).expect("should create store"); + + store + .push_data("stat_key", json!("value"), None) + .expect("push_data should succeed"); + + let stats = store.get_stats().expect("get_stats should succeed"); + + assert_eq!(stats.entry_count, 1); + assert_eq!(stats.max_entries, 100); + assert_eq!(stats.max_entry_size, 512); + assert!(stats.metrics_enabled); + } + + #[test] + fn test_data_api_ttl_handling() { + let store = create_test_store(); + + // Push with explicit TTL + store + .push_data( + "ttl_key", + json!("expires_soon"), + Some(StdDuration::from_secs(60)), + ) + .expect("push_data with TTL should succeed"); + + let entry = store + .get_data_entry("ttl_key") + .expect("get_data_entry should succeed") + .expect("entry should exist"); + + assert!( + entry.expires_at.is_some(), + "entry should have expiration time" + ); + } + + #[test] + fn test_data_api_ttl_exceeded_error() { + let config = DataStoreConfig { + max_ttl: Some(StdDuration::from_secs(60)), // 1 minute max + ..Default::default() + }; + let store = DataStore::new(config).expect("should create store"); + + // Try to push with TTL exceeding max + let result = store.push_data( + "long_ttl", + json!("value"), + Some(StdDuration::from_secs(120)), // 2 minutes + ); + + assert!( + matches!(result, Err(DataError::TTLExceeded { .. })), + "expected TTLExceeded error when TTL exceeds max_ttl" + ); + } } diff --git a/jans-cedarling/cedarling/src/lib.rs b/jans-cedarling/cedarling/src/lib.rs index 63a2113670b..7982bc45fd7 100644 --- a/jans-cedarling/cedarling/src/lib.rs +++ b/jans-cedarling/cedarling/src/lib.rs @@ -35,7 +35,10 @@ mod tests; use std::sync::Arc; pub use crate::common::json_rules::JsonRule; -pub use crate::data::{CedarType, ConfigValidationError, DataEntry, DataStoreConfig}; +pub use crate::data::{ + CedarType, ConfigValidationError, DataApi, DataEntry, DataError, DataStore, DataStoreConfig, + DataStoreStats, +}; pub use crate::init::policy_store::{PolicyStoreLoadError, load_policy_store}; use crate::log::BaseLogEntry; #[cfg(test)] From 47315f10aa5cd9aac686beae5dbe7ae4cd14aed9 Mon Sep 17 00:00:00 2001 From: haileyesus2433 Date: Thu, 22 Jan 2026 09:15:12 -0500 Subject: [PATCH 2/4] feat(data): update list_entries to exclude expired entries Signed-off-by: haileyesus2433 --- jans-cedarling/cedarling/src/data/store.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/jans-cedarling/cedarling/src/data/store.rs b/jans-cedarling/cedarling/src/data/store.rs index 24decf56759..99eacf7fe9f 100644 --- a/jans-cedarling/cedarling/src/data/store.rs +++ b/jans-cedarling/cedarling/src/data/store.rs @@ -261,10 +261,14 @@ impl DataStore { .collect() } - /// List all entries with their full metadata. + /// List all entries with their full metadata, excluding expired entries. fn list_entries(&self) -> Vec { let storage = self.storage.read().expect(RWLOCK_EXPECT_MESSAGE); - storage.iter().map(|(_, entry)| entry.clone()).collect() + storage + .iter() + .filter(|(_, entry)| !entry.is_expired()) + .map(|(_, entry)| entry.clone()) + .collect() } } From 653c1b768127638fd211bfc9928dca4fa65e3156 Mon Sep 17 00:00:00 2001 From: haileyesus2433 Date: Fri, 23 Jan 2026 01:54:46 -0500 Subject: [PATCH 3/4] feat(data): integrate DataStore into Cedarling and implement DataApi methods - Added `DataStore` as a member of the `Cedarling` struct to manage data storage. - Implemented the `DataApi` trait for `Cedarling`, providing methods for data manipulation including push, get, remove, and list operations. - Enhanced initialization of `Cedarling` to include a default `DataStoreConfig`. Signed-off-by: haileyesus2433 --- jans-cedarling/cedarling/src/data/api.rs | 49 ++++++++++++++------- jans-cedarling/cedarling/src/data/config.rs | 2 +- jans-cedarling/cedarling/src/data/mod.rs | 42 +----------------- jans-cedarling/cedarling/src/data/store.rs | 25 ++++++----- jans-cedarling/cedarling/src/lib.rs | 49 ++++++++++++++++++++- 5 files changed, 97 insertions(+), 70 deletions(-) diff --git a/jans-cedarling/cedarling/src/data/api.rs b/jans-cedarling/cedarling/src/data/api.rs index 8aeb8f65742..e4086c67ec8 100644 --- a/jans-cedarling/cedarling/src/data/api.rs +++ b/jans-cedarling/cedarling/src/data/api.rs @@ -33,31 +33,46 @@ pub struct DataStoreStats { /// This trait provides a consistent interface for pushing, retrieving, /// and managing data in the store. All operations are thread-safe. /// -/// # Examples +/// The [`Cedarling`](crate::Cedarling) struct implements this trait, providing +/// access to the data store through the main application instance. /// -/// ```ignore -/// use cedarling::data::{DataApi, DataStore, DataStoreConfig}; +/// # Example +/// +/// ```no_run +/// use cedarling::{Cedarling, DataApi, DataError}; /// use serde_json::json; /// use std::time::Duration; /// -/// let store = DataStore::new(DataStoreConfig::default())?; +/// 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)), +/// )?; /// -/// // Push data with TTL -/// store.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); +/// } /// -/// // Retrieve data -/// if let Some(roles) = store.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); +/// } /// -/// // List all entries with metadata -/// for entry in store.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); /// -/// // Get store statistics -/// let stats = store.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. 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 79ba2ca4179..fe885257881 100644 --- a/jans-cedarling/cedarling/src/data/mod.rs +++ b/jans-cedarling/cedarling/src/data/mod.rs @@ -7,46 +7,6 @@ //! //! Provides key-value storage for pushed data with TTL support, capacity management, //! and thread-safe concurrent access. -//! -//! # Overview -//! -//! The data module enables applications to push external data into Cedarling's -//! evaluation context. This data becomes available in Cedar policies through -//! the `context.data` namespace. -//! -//! # Components -//! -//! - [`DataStore`] - Thread-safe key-value store with TTL support -//! - [`DataApi`] - Trait defining the public data access interface -//! - [`DataEntry`] - Entry structure with value and metadata -//! - [`DataStoreStats`] - Statistics about store state -//! - [`DataStoreConfig`] - Configuration for storage limits and TTL -//! -//! # Example -//! -//! ```ignore -//! use cedarling::data::{DataApi, DataStore, DataStoreConfig}; -//! use serde_json::json; -//! use std::time::Duration; -//! -//! // Create store with default configuration -//! let store = DataStore::new(DataStoreConfig::default())?; -//! -//! // Push data with 5-minute TTL -//! store.push_data("user_context", json!({ -//! "department": "engineering", -//! "clearance_level": 3 -//! }), Some(Duration::from_secs(300)))?; -//! -//! // Retrieve and use data -//! if let Some(ctx) = store.get_data("user_context")? { -//! println!("User context: {}", ctx); -//! } -//! -//! // Check store statistics -//! let stats = store.get_stats()?; -//! println!("Entries: {}", stats.entry_count); -//! ``` mod api; mod config; @@ -58,4 +18,4 @@ 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 99eacf7fe9f..21eb9c5a11a 100644 --- a/jans-cedarling/cedarling/src/data/store.rs +++ b/jans-cedarling/cedarling/src/data/store.rs @@ -33,7 +33,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, } @@ -49,7 +49,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()?; @@ -95,7 +95,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); @@ -163,7 +168,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) } @@ -172,7 +177,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); @@ -223,28 +228,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() } @@ -253,7 +258,7 @@ 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() diff --git a/jans-cedarling/cedarling/src/lib.rs b/jans-cedarling/cedarling/src/lib.rs index 7982bc45fd7..897de147f5b 100644 --- a/jans-cedarling/cedarling/src/lib.rs +++ b/jans-cedarling/cedarling/src/lib.rs @@ -35,8 +35,9 @@ mod tests; use std::sync::Arc; pub use crate::common::json_rules::JsonRule; +use crate::data::DataStore; pub use crate::data::{ - CedarType, ConfigValidationError, DataApi, DataEntry, DataError, DataStore, DataStoreConfig, + CedarType, ConfigValidationError, DataApi, DataEntry, DataError, DataStoreConfig, DataStoreStats, }; pub use crate::init::policy_store::{PolicyStoreLoadError, load_policy_store}; @@ -102,6 +103,7 @@ pub enum InitCedarlingError { pub struct Cedarling { log: log::Logger, authz: Arc, + data: Arc, } impl Cedarling { @@ -159,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, }) } @@ -334,3 +344,40 @@ 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_data(key, value, ttl) + } + + fn get_data(&self, key: &str) -> Result, DataError> { + self.data.get_data(key) + } + + fn get_data_entry(&self, key: &str) -> Result, DataError> { + self.data.get_data_entry(key) + } + + fn remove_data(&self, key: &str) -> Result { + self.data.remove_data(key) + } + + fn clear_data(&self) -> Result<(), DataError> { + self.data.clear_data() + } + + fn list_data(&self) -> Result, DataError> { + self.data.list_data() + } + + fn get_stats(&self) -> Result { + self.data.get_stats() + } +} From 9634d7286d5e5e481fec37966f4c584ec79aeb70 Mon Sep 17 00:00:00 2001 From: haileyesus2433 Date: Fri, 23 Jan 2026 03:25:17 -0500 Subject: [PATCH 4/4] refactor(data): streamline DataApi methods in Cedarling and DataStore - Updated `Cedarling` to use direct method calls from `DataStore`, simplifying the implementation of the `DataApi` trait. - Refactored `DataStore` methods to improve clarity and maintainability, including making `list_entries` public. - Enhanced `get_stats` method to directly return statistics from the `DataStore` configuration. Signed-off-by: haileyesus2433 --- jans-cedarling/cedarling/src/data/store.rs | 207 ++------------------- jans-cedarling/cedarling/src/lib.rs | 21 ++- 2 files changed, 30 insertions(+), 198 deletions(-) diff --git a/jans-cedarling/cedarling/src/data/store.rs b/jans-cedarling/cedarling/src/data/store.rs index 21eb9c5a11a..aaa82d33d45 100644 --- a/jans-cedarling/cedarling/src/data/store.rs +++ b/jans-cedarling/cedarling/src/data/store.rs @@ -11,7 +11,6 @@ use chrono::Duration as ChronoDuration; use serde_json::Value; use sparkv::{Config as SparKVConfig, Error as SparKVError, SparKV}; -use super::api::{DataApi, DataStoreStats}; use super::config::{ConfigValidationError, DataStoreConfig}; use super::entry::DataEntry; use super::error::DataError; @@ -267,7 +266,7 @@ impl DataStore { } /// List all entries with their full metadata, excluding expired entries. - fn list_entries(&self) -> Vec { + pub(crate) fn list_entries(&self) -> Vec { let storage = self.storage.read().expect(RWLOCK_EXPECT_MESSAGE); storage .iter() @@ -275,46 +274,10 @@ impl DataStore { .map(|(_, entry)| entry.clone()) .collect() } -} -impl DataApi for DataStore { - fn push_data( - &self, - key: &str, - value: Value, - ttl: Option, - ) -> Result<(), DataError> { - self.push(key, value, ttl) - } - - fn get_data(&self, key: &str) -> Result, DataError> { - Ok(self.get(key)) - } - - fn get_data_entry(&self, key: &str) -> Result, DataError> { - Ok(self.get_entry(key)) - } - - fn remove_data(&self, key: &str) -> Result { - Ok(self.remove(key)) - } - - fn clear_data(&self) -> Result<(), DataError> { - self.clear(); - Ok(()) - } - - fn list_data(&self) -> Result, DataError> { - Ok(self.list_entries()) - } - - fn get_stats(&self) -> Result { - Ok(DataStoreStats { - entry_count: self.count(), - max_entries: self.config.max_entries, - max_entry_size: self.config.max_entry_size, - metrics_enabled: self.config.enable_metrics, - }) + /// Get the configuration for this store. + pub(crate) fn config(&self) -> &DataStoreConfig { + &self.config } } @@ -881,109 +844,21 @@ mod tests { } // ========================================================================== - // DataApi trait tests + // Additional store method tests // ========================================================================== #[test] - fn test_data_api_push_and_get() { - let store = create_test_store(); - - // Use trait methods - store - .push_data("api_key", json!({"role": "admin"}), None) - .expect("push_data should succeed"); - - let result = store.get_data("api_key").expect("get_data should succeed"); - assert_eq!(result, Some(json!({"role": "admin"}))); - - // Non-existent key - let missing = store - .get_data("nonexistent") - .expect("get_data should succeed for missing key"); - assert_eq!(missing, None); - } - - #[test] - fn test_data_api_get_entry() { - let store = create_test_store(); - - store - .push_data( - "entry_key", - json!("test_value"), - Some(StdDuration::from_secs(600)), - ) - .expect("push_data should succeed"); - - let entry = store - .get_data_entry("entry_key") - .expect("get_data_entry should succeed") - .expect("entry should exist"); - - assert_eq!(entry.key, "entry_key"); - assert_eq!(entry.value, json!("test_value")); - assert!(entry.expires_at.is_some()); - } - - #[test] - fn test_data_api_remove() { - let store = create_test_store(); - - store - .push_data("to_remove", json!(123), None) - .expect("push_data should succeed"); - - let removed = store - .remove_data("to_remove") - .expect("remove_data should succeed"); - assert!(removed, "should return true when key existed"); - - let removed_again = store - .remove_data("to_remove") - .expect("remove_data should succeed"); - assert!(!removed_again, "should return false when key doesn't exist"); - - assert_eq!( - store - .get_data("to_remove") - .expect("get_data should succeed"), - None - ); - } - - #[test] - fn test_data_api_clear() { + fn test_list_entries() { let store = create_test_store(); store - .push_data("key1", json!(1), None) - .expect("push_data should succeed"); + .push("alpha", json!("a"), None) + .expect("push should succeed"); store - .push_data("key2", json!(2), None) - .expect("push_data should succeed"); - store - .push_data("key3", json!(3), None) - .expect("push_data should succeed"); - - assert_eq!(store.count(), 3); - - store.clear_data().expect("clear_data should succeed"); + .push("beta", json!("b"), None) + .expect("push should succeed"); - assert_eq!(store.count(), 0); - } - - #[test] - fn test_data_api_list() { - let store = create_test_store(); - - store - .push_data("alpha", json!("a"), None) - .expect("push_data should succeed"); - store - .push_data("beta", json!("b"), None) - .expect("push_data should succeed"); - - let entries = store.list_data().expect("list_data should succeed"); + let entries = store.list_entries(); assert_eq!(entries.len(), 2); @@ -993,7 +868,7 @@ mod tests { } #[test] - fn test_data_api_stats() { + fn test_config_accessor() { let config = DataStoreConfig { max_entries: 100, max_entry_size: 512, @@ -1002,60 +877,10 @@ mod tests { }; let store = DataStore::new(config).expect("should create store"); - store - .push_data("stat_key", json!("value"), None) - .expect("push_data should succeed"); + let retrieved_config = store.config(); - let stats = store.get_stats().expect("get_stats should succeed"); - - assert_eq!(stats.entry_count, 1); - assert_eq!(stats.max_entries, 100); - assert_eq!(stats.max_entry_size, 512); - assert!(stats.metrics_enabled); - } - - #[test] - fn test_data_api_ttl_handling() { - let store = create_test_store(); - - // Push with explicit TTL - store - .push_data( - "ttl_key", - json!("expires_soon"), - Some(StdDuration::from_secs(60)), - ) - .expect("push_data with TTL should succeed"); - - let entry = store - .get_data_entry("ttl_key") - .expect("get_data_entry should succeed") - .expect("entry should exist"); - - assert!( - entry.expires_at.is_some(), - "entry should have expiration time" - ); - } - - #[test] - fn test_data_api_ttl_exceeded_error() { - let config = DataStoreConfig { - max_ttl: Some(StdDuration::from_secs(60)), // 1 minute max - ..Default::default() - }; - let store = DataStore::new(config).expect("should create store"); - - // Try to push with TTL exceeding max - let result = store.push_data( - "long_ttl", - json!("value"), - Some(StdDuration::from_secs(120)), // 2 minutes - ); - - assert!( - matches!(result, Err(DataError::TTLExceeded { .. })), - "expected TTLExceeded error when TTL exceeds max_ttl" - ); + 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 897de147f5b..700c21111ea 100644 --- a/jans-cedarling/cedarling/src/lib.rs +++ b/jans-cedarling/cedarling/src/lib.rs @@ -354,30 +354,37 @@ impl DataApi for Cedarling { value: serde_json::Value, ttl: Option, ) -> Result<(), DataError> { - self.data.push_data(key, value, ttl) + self.data.push(key, value, ttl) } fn get_data(&self, key: &str) -> Result, DataError> { - self.data.get_data(key) + Ok(self.data.get(key)) } fn get_data_entry(&self, key: &str) -> Result, DataError> { - self.data.get_data_entry(key) + Ok(self.data.get_entry(key)) } fn remove_data(&self, key: &str) -> Result { - self.data.remove_data(key) + Ok(self.data.remove(key)) } fn clear_data(&self) -> Result<(), DataError> { - self.data.clear_data() + self.data.clear(); + Ok(()) } fn list_data(&self) -> Result, DataError> { - self.data.list_data() + Ok(self.data.list_entries()) } fn get_stats(&self) -> Result { - self.data.get_stats() + 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, + }) } }