mod decode;
pub mod authorization_config;
pub mod jwt_config;
pub mod lock_config;
pub mod log_config;
pub mod policy_store_config;
pub mod raw_config;
#[cfg(not(target_arch = "wasm32"))]
use config::{Config, File};
#[cfg(not(target_arch = "wasm32"))]
use std::{io, path::Path};
use crate::{context_data_api::DataStoreConfig, http::HttpClientConfig};
pub use authorization_config::AuthorizationConfig;
pub use jwt_config::JwtConfig;
pub use lock_config::{LockServiceConfig, LockServiceConfigRaw, LockTransport};
pub use log_config::{LogConfig, LogTypeConfig, MemoryLogConfig};
pub use policy_store_config::{PolicyStoreConfig, PolicyStoreConfigRaw, PolicyStoreSource};
pub use raw_config::{BootstrapConfigRaw, FeatureToggle};
#[derive(Debug, Clone, PartialEq)]
pub struct BootstrapConfig {
pub application_name: String,
pub log_config: LogConfig,
pub policy_store_config: PolicyStoreConfig,
pub jwt_config: JwtConfig,
pub authorization_config: AuthorizationConfig,
pub lock_config: Option<LockServiceConfig>,
pub max_default_entities: Option<usize>,
pub max_base64_size: Option<usize>,
pub data_store_config: DataStoreConfig,
pub http_client_config: HttpClientConfig,
}
impl Default for BootstrapConfig {
fn default() -> Self {
use crate::log::LogLevel;
Self {
application_name: String::new(),
log_config: LogConfig {
log_type: LogTypeConfig::Memory(MemoryLogConfig {
log_ttl: 60,
max_items: None,
max_item_size: None,
}),
log_level: LogLevel::INFO,
},
policy_store_config: PolicyStoreConfig {
source: PolicyStoreSource::Yaml(
"cedar_version: v4.0.0\npolicy_stores: {}\n".to_string(),
),
..Default::default()
},
jwt_config: JwtConfig::new_without_validation(),
authorization_config: AuthorizationConfig::default(),
lock_config: None,
max_default_entities: None,
max_base64_size: None,
data_store_config: DataStoreConfig::default(),
http_client_config: HttpClientConfig::default(),
}
}
}
impl BootstrapConfig {
#[cfg(not(target_arch = "wasm32"))]
pub fn load_from_file<P: AsRef<Path>>(path: P) -> Result<Self, BootstrapConfigLoadingError> {
let config = Config::builder()
.add_source(File::from(path.as_ref()))
.build()
.map_err(|e| BootstrapConfigLoadingError::DecodingJSON(e.to_string()))?;
let raw: BootstrapConfigRaw = config
.try_deserialize()
.map_err(|e| BootstrapConfigLoadingError::DecodingJSON(e.to_string()))?;
raw.try_into()
}
pub fn load_from_json(json: &str) -> Result<Self, BootstrapConfigLoadingError> {
let raw: BootstrapConfigRaw = serde_json::from_str(json)
.map_err(|e| BootstrapConfigLoadingError::DecodingJSON(e.to_string()))?;
raw.try_into()
}
#[cfg(not(target_arch = "wasm32"))]
pub fn from_env() -> Result<Self, BootstrapConfigLoadingError> {
let config = Config::builder()
.add_source(config::Environment::with_prefix("CEDARLING"))
.build()
.map_err(|e| BootstrapConfigLoadingError::DecodingJSON(e.to_string()))?;
let raw: BootstrapConfigRaw = config
.try_deserialize()
.map_err(|e| BootstrapConfigLoadingError::DecodingJSON(e.to_string()))?;
raw.try_into()
}
#[cfg(test)]
pub fn load_default() -> Result<Self, BootstrapConfigLoadingError> {
const DEFAULT_CONFIG: &str = include_str!("../../config/default_config.yaml");
let config = Config::builder()
.add_source(File::from_str(DEFAULT_CONFIG, config::FileFormat::Yaml))
.build()
.map_err(|e| BootstrapConfigLoadingError::DecodingYAML(e.to_string()))?;
let raw: BootstrapConfigRaw = config
.try_deserialize()
.map_err(|e| BootstrapConfigLoadingError::DecodingYAML(e.to_string()))?;
raw.try_into()
}
}
impl TryFrom<BootstrapConfigRaw> for BootstrapConfig {
type Error = BootstrapConfigLoadingError;
fn try_from(raw: BootstrapConfigRaw) -> Result<Self, Self::Error> {
Self::from_raw_config(&raw)
}
}
#[derive(Debug, thiserror::Error)]
pub enum BootstrapConfigLoadingError {
#[cfg(not(target_arch = "wasm32"))]
#[error(
"Unsupported bootstrap config file format for: {0}. Supported formats include: JSON, YAML, TOML"
)]
InvalidFileFormat(String),
#[cfg(not(target_arch = "wasm32"))]
#[error("Failed to read {0}: {1}")]
ReadFile(String, io::Error),
#[error("Failed to decode JSON string into BootstrapConfig: {0}")]
DecodingJSON(String),
#[error("Failed to decode YAML string into BootstrapConfig: {0}")]
DecodingYAML(String),
#[error("Failed to decode TOML string into BootstrapConfig: {0}")]
DecodingTOML(String),
#[error(
"Missing bootstrap property: `CEDARLING_LOG_TTL`. This property is required if \
`CEDARLING_LOG_TYPE` is set to Memory."
)]
MissingLogTTL,
#[error(
"Multiple store options were provided. Make sure you only one of these properties is set: \
`CEDARLING_POLICY_STORE_URI` or `CEDARLING_POLICY_STORE_LOCAL`"
)]
ConflictingPolicyStores,
#[error("No Policy store was provided.")]
MissingPolicyStore,
#[error("Unsupported policy store file format for: {0}. Supported formats include: JSON, YAML")]
UnsupportedPolicyStoreFileFormat(String),
#[error("Failed to load local JWKS from {0}: {1}")]
LoadLocalJwks(String, String),
#[error(
"the `CEDARLING_LOCK` is set to `enabled` but `CEDARLING_LOCK_SERVER_CONFIGURATION_URI` is not set."
)]
MissingLockServerConfigUri,
#[error("Invalid lock server configuration URI: {0}")]
InvalidLockServerConfigUri(url::ParseError),
#[error(
"cjar_url is missing or empty. A valid URL is required for CjarUrl policy store source."
)]
MissingCjarUrl,
#[error(
"`CEDARLING_LOCK_TRANSPORT` is set to `grpc` but `CEDARLING_LOCK_GRPC_ENDPOINT` is not set."
)]
MissingGrpcEndpoint,
}
impl From<url::ParseError> for BootstrapConfigLoadingError {
fn from(err: url::ParseError) -> Self {
BootstrapConfigLoadingError::InvalidLockServerConfigUri(err)
}
}
impl From<serde_json::Error> for BootstrapConfigLoadingError {
fn from(err: serde_json::Error) -> Self {
Self::DecodingJSON(err.to_string())
}
}
#[cfg(test)]
mod tests {
use jsonwebtoken::Algorithm;
use crate::LogLevel;
use super::*;
#[test]
fn test_load_default_config() {
let config = BootstrapConfig::load_default().unwrap();
assert_eq!(config.application_name, "My App");
assert!(matches!(
config.log_config.log_type,
LogTypeConfig::Memory(_)
));
assert_eq!(config.log_config.log_level, LogLevel::DEBUG);
assert!(matches!(
config.policy_store_config.source,
PolicyStoreSource::FileJson(_)
));
assert!(config.jwt_config.jwt_sig_validation);
assert!(config.jwt_config.jwt_status_validation);
assert!(
config
.jwt_config
.signature_algorithms_supported
.contains(&Algorithm::HS256)
);
assert!(
config
.jwt_config
.signature_algorithms_supported
.contains(&Algorithm::RS256)
);
assert_eq!(
config.authorization_config.decision_log_default_jwt_id,
"jti"
);
assert_eq!(config.data_store_config.max_entries, 10000);
assert_eq!(config.data_store_config.default_ttl, None);
assert!(config.data_store_config.enable_metrics);
}
}