Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 113 additions & 0 deletions jans-cedarling/cedarling/src/data/api.rs
Original file line number Diff line number Diff line change
@@ -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<Duration>) -> 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<Option<Value>, 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<Option<DataEntry>, 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<bool, DataError>;

/// 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<Vec<DataEntry>, DataError>;

/// Get statistics about the data store.
///
/// Returns current entry count, capacity limits, and configuration state.
fn get_stats(&self) -> Result<DataStoreStats, DataError>;
}
2 changes: 1 addition & 1 deletion jans-cedarling/cedarling/src/data/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
5 changes: 4 additions & 1 deletion jans-cedarling/cedarling/src/data/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
81 changes: 71 additions & 10 deletions jans-cedarling/cedarling/src/data/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<SparKV<DataEntry>>,
config: DataStoreConfig,
}
Expand All @@ -48,7 +48,7 @@ impl DataStore {
/// # Errors
///
/// Returns `ConfigValidationError` if the configuration is invalid.
pub fn new(config: DataStoreConfig) -> Result<Self, ConfigValidationError> {
pub(crate) fn new(config: DataStoreConfig) -> Result<Self, ConfigValidationError> {
// Validate configuration before creating the store
config.validate()?;

Expand Down Expand Up @@ -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<StdDuration>) -> Result<(), DataError> {
pub(crate) fn push(
&self,
key: &str,
value: Value,
ttl: Option<StdDuration>,
) -> Result<(), DataError> {
// Validate key
if key.is_empty() {
return Err(DataError::InvalidKey);
Expand Down Expand Up @@ -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<Value> {
pub(crate) fn get(&self, key: &str) -> Option<Value> {
self.get_entry(key).map(|entry| entry.value)
}

Expand All @@ -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<DataEntry> {
pub(crate) fn get_entry(&self, key: &str) -> Option<DataEntry> {
// First, try with read lock for better concurrency
let entry = {
let storage = self.storage.read().expect(RWLOCK_EXPECT_MESSAGE);
Expand Down Expand Up @@ -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<String> {
pub(crate) fn list_keys(&self) -> Vec<String> {
let storage = self.storage.read().expect(RWLOCK_EXPECT_MESSAGE);
storage.get_keys()
}
Expand All @@ -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<String, Value> {
pub(crate) fn get_all(&self) -> HashMap<String, Value> {
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<DataEntry> {
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`.
Expand Down Expand Up @@ -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);
}
}
59 changes: 58 additions & 1 deletion jans-cedarling/cedarling/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -99,6 +103,7 @@ pub enum InitCedarlingError {
pub struct Cedarling {
log: log::Logger,
authz: Arc<Authz>,
data: Arc<DataStore>,
}

impl Cedarling {
Expand Down Expand Up @@ -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(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think eventually we will move builing DataStore to the service factory

DataStore::new(DataStoreConfig::default())
.expect("default DataStoreConfig should always be valid"),
);

Ok(Cedarling {
log,
authz: service_factory.authz_service().await?,
data,
})
}

Expand Down Expand Up @@ -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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does the DataStore implement DataApi when Cedarling is already implementing it why not call the methods in DataStore directly here instead? Something like self.data.push(key, value, ttl) I may have missed something though

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no u didn't miss anything ur right, am currently refactoring it because i also have to update the tests thats why i will push the fix in a moment but thanks for pointing it out

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolved in 9634d72

fn push_data(
&self,
key: &str,
value: serde_json::Value,
ttl: Option<std::time::Duration>,
) -> Result<(), DataError> {
self.data.push(key, value, ttl)
}

fn get_data(&self, key: &str) -> Result<Option<serde_json::Value>, DataError> {
Ok(self.data.get(key))
}

fn get_data_entry(&self, key: &str) -> Result<Option<DataEntry>, DataError> {
Ok(self.data.get_entry(key))
}

fn remove_data(&self, key: &str) -> Result<bool, DataError> {
Ok(self.data.remove(key))
}

fn clear_data(&self) -> Result<(), DataError> {
self.data.clear();
Ok(())
}

fn list_data(&self) -> Result<Vec<DataEntry>, DataError> {
Ok(self.data.list_entries())
}

fn get_stats(&self) -> Result<DataStoreStats, DataError> {
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,
})
}
}