Skip to main content

cedarling/bootstrap_config/raw_config/
config.rs

1// This software is available under the Apache-2.0 license.
2// See https://www.apache.org/licenses/LICENSE-2.0.txt for full text.
3//
4// Copyright (c) 2024, Gluu, Inc.
5
6#[cfg(not(target_arch = "wasm32"))]
7use super::super::BootstrapConfigLoadingError;
8use super::super::log_config::StdOutMode;
9use super::default_values::{
10    default_enabled_feature_toggle, default_http_client_max_response_size_bytes,
11    default_http_client_max_retries, default_http_client_retry_delay_secs, default_jti,
12    default_jwks_refresh_min_interval, default_log_channel_capacity, default_log_max_retries,
13    default_status_list_refresh_interval_max, default_token_cache_capacity,
14    default_token_cache_max_ttl, default_true,
15};
16#[cfg(not(target_arch = "wasm32"))]
17use super::default_values::{
18    default_http_client_request_timeout, default_stdout_buffer_limit, default_stdout_timeout_millis,
19};
20use super::feature_types::{FeatureToggle, LoggerType};
21use super::json_util::{
22    deserialize_jwks_refresh_interval, deserialize_jwks_refresh_min_interval,
23    deserialize_or_parse_string_as_json, deserialize_status_list_refresh_interval_max,
24    parse_option_string,
25};
26use crate::JwtConfig;
27use crate::LockTransport;
28use crate::jwt_config::{TrustedIssuerLoaderTypeRaw, WorkersCount};
29use crate::log::LogLevel;
30use jsonwebtoken::Algorithm;
31use serde::{Deserialize, Serialize};
32#[cfg(not(target_arch = "wasm32"))]
33use std::collections::HashMap;
34use std::collections::HashSet;
35#[cfg(not(target_arch = "wasm32"))]
36use std::env;
37
38use std::num::NonZeroUsize;
39
40/// Struct that represent mapping mapping `Bootstrap properties` to be JSON and YAML compatible
41/// from [link](https://github.com/JanssenProject/jans/wiki/Cedarling-Nativity-Plan#bootstrap-properties)
42///
43/// This structure is used to deserialize values from ENV VARS so json keys is same as keys in environment variables
44//
45//  All fields should be available to parse from string, because env vars always string.
46#[derive(Deserialize, Serialize, PartialEq, Debug)]
47pub struct BootstrapConfigRaw {
48    ///  Human friendly identifier for the application
49    #[serde(rename = "CEDARLING_APPLICATION_NAME")]
50    pub application_name: String,
51
52    /// Location of policy store JSON, used if policy store is not local, or retreived from Lock Master.
53    #[serde(
54        rename = "CEDARLING_POLICY_STORE_URI",
55        default,
56        deserialize_with = "parse_option_string"
57    )]
58    pub policy_store_uri: Option<String>,
59
60    /// How the Logs will be presented.
61    #[serde(rename = "CEDARLING_LOG_TYPE", default)]
62    pub log_type: LoggerType,
63
64    /// Log level filter for logging. TRACE is lowest. FATAL is highest.
65    #[serde(rename = "CEDARLING_LOG_LEVEL", default)]
66    pub log_level: LogLevel,
67
68    /// If `log_type` is set to [`LogType::Memory`], this is the TTL (time to live) of
69    /// log entities in seconds.
70    #[serde(rename = "CEDARLING_LOG_TTL", default)]
71    #[serde(deserialize_with = "deserialize_or_parse_string_as_json")]
72    pub log_ttl: Option<u64>,
73
74    /// Maximum number of log entities that can be stored using [`LogType::Memory`].
75    /// If value is 0, there is no limit. But if None, default value is applied.
76    #[serde(rename = "CEDARLING_LOG_MAX_ITEMS", default)]
77    #[serde(deserialize_with = "deserialize_or_parse_string_as_json")]
78    pub log_max_items: Option<usize>,
79
80    /// Maximum size of a single log entity in bytes using [`LogType::Memory`].
81    /// If value is 0, there is no limit. But if None, default value is applied.
82    #[serde(rename = "CEDARLING_LOG_MAX_ITEM_SIZE", default)]
83    #[serde(deserialize_with = "deserialize_or_parse_string_as_json")]
84    pub log_max_item_size: Option<usize>,
85
86    /// Logging mode for stdout logger: "async" or "immediate (default)".
87    /// Only applicable for native targets (not WASM).
88    #[serde(rename = "CEDARLING_STDOUT_MODE", default)]
89    #[serde(deserialize_with = "deserialize_or_parse_string_as_json")]
90    pub stdout_mode: StdOutMode,
91
92    /// Flush timeout in milliseconds for async stdout logging.
93    /// Only applicable for native targets (not WASM).
94    #[cfg(not(target_arch = "wasm32"))]
95    #[serde(
96        rename = "CEDARLING_STDOUT_TIMEOUT_MILLIS",
97        default = "default_stdout_timeout_millis"
98    )]
99    #[serde(deserialize_with = "deserialize_or_parse_string_as_json")]
100    pub stdout_timeout_millis: u64,
101
102    /// Buffer size limit in bytes for async stdout logging.
103    /// Only applicable for native targets (not WASM).
104    #[cfg(not(target_arch = "wasm32"))]
105    #[serde(
106        rename = "CEDARLING_STDOUT_BUFFER_LIMIT",
107        default = "default_stdout_buffer_limit"
108    )]
109    #[serde(deserialize_with = "deserialize_or_parse_string_as_json")]
110    pub stdout_buffer_limit: usize,
111
112    /// Token claims that will be used for decision logging.
113    /// Default is jti, but perhaps some other claim is needed.
114    #[serde(
115        rename = "CEDARLING_DECISION_LOG_DEFAULT_JWT_ID",
116        default = "default_jti"
117    )]
118    pub decision_log_default_jwt_id: String,
119
120    /// Path to a local file pointing containing a JWKS.
121    #[serde(
122        rename = "CEDARLING_LOCAL_JWKS",
123        default,
124        deserialize_with = "parse_option_string"
125    )]
126    pub local_jwks: Option<String>,
127
128    /// JSON object with policy store
129    #[serde(rename = "CEDARLING_POLICY_STORE_LOCAL", default)]
130    #[serde(deserialize_with = "deserialize_or_parse_string_as_json")]
131    pub local_policy_store: Option<String>,
132
133    /// Path to a Policy Store JSON file
134    #[serde(
135        rename = "CEDARLING_POLICY_STORE_LOCAL_FN",
136        default,
137        deserialize_with = "parse_option_string"
138    )]
139    pub policy_store_local_fn: Option<String>,
140    /// URL to a Policy Store CJAR file
141    #[serde(
142        rename = "CEDARLING_POLICY_STORE_CJAR_URL",
143        default,
144        deserialize_with = "parse_option_string"
145    )]
146    pub policy_store_cjar_url: Option<String>,
147
148    /// Maximum number of default entities allowed in a policy store.
149    /// This prevents `DoS` attacks by limiting the number of entities that can be loaded.
150    /// If value is 0, there is no limit. But if None, default value is applied.
151    #[serde(rename = "CEDARLING_MAX_DEFAULT_ENTITIES", default)]
152    #[serde(deserialize_with = "deserialize_or_parse_string_as_json")]
153    pub max_default_entities: Option<usize>,
154
155    /// Maximum size of base64-encoded default entity strings in bytes.
156    /// This prevents memory exhaustion attacks from extremely large base64 strings.
157    /// If value is 0, there is no limit. But if None, default value is applied.
158    #[serde(rename = "CEDARLING_MAX_BASE64_SIZE", default)]
159    #[serde(deserialize_with = "deserialize_or_parse_string_as_json")]
160    pub max_base64_size: Option<usize>,
161
162    /// Whether to check the signature of all JWT tokens.
163    ///
164    /// When enabled, this requires the `iss` (Issuer) claim to be present in
165    /// all tokens and the issuer URL must use the `https` scheme.
166    #[serde(
167        rename = "CEDARLING_JWT_SIG_VALIDATION",
168        default = "default_enabled_feature_toggle"
169    )]
170    #[serde(deserialize_with = "deserialize_or_parse_string_as_json")]
171    pub jwt_sig_validation: FeatureToggle,
172
173    /// Whether to check the status of the JWT. On startup.
174    ///
175    /// Cedarling will fetch and retreive the latest Status List JWT from the
176    /// `.well-known/openid-configuration` via the `status_list_endpoint` claim and
177    /// cache it. See the [`IETF Draft`] for more info.
178    ///
179    /// [`IETF Draft`]: https://datatracker.ietf.org/doc/draft-ietf-oauth-status-list/
180    #[serde(
181        rename = "CEDARLING_JWT_STATUS_VALIDATION",
182        default = "default_enabled_feature_toggle"
183    )]
184    #[serde(deserialize_with = "deserialize_or_parse_string_as_json")]
185    pub jwt_status_validation: FeatureToggle,
186
187    /// When enabled, Cedar schema is required and all policies and entities are validated
188    /// against it. When disabled, Cedarling runs without schema-based validation,
189    /// allowing quick-start and prototyping without a schema.
190    #[serde(
191        rename = "CEDARLING_STRICT_SCHEMA_VALIDATION",
192        default = "default_enabled_feature_toggle"
193    )]
194    #[serde(deserialize_with = "deserialize_or_parse_string_as_json")]
195    pub strict_schema_validation: FeatureToggle,
196
197    /// Cedarling will only accept tokens signed with these algorithms.
198    #[serde(
199        rename = "CEDARLING_JWT_SIGNATURE_ALGORITHMS_SUPPORTED",
200        default = "JwtConfig::supported_algorithms"
201    )]
202    #[serde(deserialize_with = "deserialize_or_parse_string_as_json")]
203    pub jwt_signature_algorithms_supported: HashSet<Algorithm>,
204
205    /// If Enabled, the Cedarling will connect to the Lock Master for policies,
206    /// and subscribe for SSE events.
207    #[serde(rename = "CEDARLING_LOCK", default)]
208    pub lock: FeatureToggle,
209
210    /// URI where Cedarling can get JSON file with all required metadata about
211    /// Lock Master, i.e. .well-known/lock-master-configuration.
212    ///
213    /// ***Required*** if `LOCK == enabled`.
214    #[serde(rename = "CEDARLING_LOCK_SERVER_CONFIGURATION_URI", default)]
215    #[serde(deserialize_with = "deserialize_or_parse_string_as_json")]
216    pub lock_server_configuration_uri: Option<String>,
217
218    /// Controls whether Cedarling should listen for SSE config updates.
219    #[serde(rename = "CEDARLING_LOCK_DYNAMIC_CONFIGURATION", default)]
220    pub dynamic_configuration: FeatureToggle,
221
222    /// SSA for DCR in a Lock Master deployment. The Cedarling will validate this
223    /// SSA JWT prior to DCR.
224    #[serde(
225        rename = "CEDARLING_LOCK_SSA_JWT",
226        default,
227        deserialize_with = "parse_option_string"
228    )]
229    pub lock_ssa_jwt: Option<String>,
230
231    /// Pre-issued access token to use for Lock Server authentication, bypassing
232    /// the SSA → DCR → `access_token` flow entirely.
233    ///
234    /// When this property is set, Cedarling will skip Dynamic Client Registration
235    /// and use this access token directly to authenticate with Lock Server endpoints
236    /// (log, health, telemetry). Primarily intended for testing and local development
237    /// to simplify the bootstrap flow; may also be used in environments where the
238    /// DCR flow is not available or access tokens are provisioned externally.
239    ///
240    /// If both `CEDARLING_LOCK_ACCESS_TOKEN_JWT` and `CEDARLING_LOCK_SSA_JWT` are
241    /// set, `CEDARLING_LOCK_ACCESS_TOKEN_JWT` takes precedence and the SSA flow is
242    /// skipped.
243    ///
244    /// Not available on WASM targets.
245    #[cfg(not(target_arch = "wasm32"))]
246    #[serde(
247        rename = "CEDARLING_LOCK_ACCESS_TOKEN_JWT",
248        default,
249        deserialize_with = "parse_option_string"
250    )]
251    pub lock_access_token_jwt: Option<String>,
252
253    /// How often to send log messages to Lock Master (0 to turn off trasmission).
254    #[serde(rename = "CEDARLING_LOCK_LOG_INTERVAL", default)]
255    #[serde(deserialize_with = "deserialize_or_parse_string_as_json")]
256    pub audit_log_interval: u64,
257
258    /// How often to send health messages to Lock Master (0 to turn off transmission).
259    #[serde(rename = "CEDARLING_LOCK_HEALTH_INTERVAL", default)]
260    #[serde(deserialize_with = "deserialize_or_parse_string_as_json")]
261    pub audit_health_interval: u64,
262
263    /// How often to send telemetry messages to Lock Master (0 to turn off transmission).
264    #[serde(rename = "CEDARLING_LOCK_TELEMETRY_INTERVAL", default)]
265    #[serde(deserialize_with = "deserialize_or_parse_string_as_json")]
266    pub audit_telemetry_interval: u64,
267
268    /// Controls whether Cedarling should listen for updates from the Lock Server.
269    #[serde(rename = "CEDARLING_LOCK_LISTEN_SSE", default)]
270    pub listen_sse: FeatureToggle,
271
272    /// Allow interaction with a Lock server with invalid certificates. Used for testing.
273    #[serde(rename = "CEDARLING_LOCK_ACCEPT_INVALID_CERTS", default)]
274    pub accept_invalid_certs: FeatureToggle,
275
276    /// Transport protocol for Lock Server communication ("grpc" or "rest").
277    #[serde(
278        rename = "CEDARLING_LOCK_TRANSPORT",
279        deserialize_with = "deserialize_or_parse_string_as_json",
280        default
281    )]
282    pub lock_transport: LockTransport,
283
284    /// Channel capacity for buffering log entries before they are sent to the lock server.
285    /// Higher values allow more logs to be buffered in memory when the lock server is slow,
286    /// but also increase memory usage.
287    /// Default value is 100.
288    #[serde(
289        rename = "CEDARLING_LOCK_LOG_CHANNEL_CAPACITY",
290        deserialize_with = "deserialize_or_parse_string_as_json",
291        default = "default_log_channel_capacity"
292    )]
293    pub lock_log_channel_capacity: NonZeroUsize,
294
295    /// Maximum number of retry attempts for sending logs to the lock server.
296    /// Uses exponential backoff strategy for retrying.
297    /// Default value is 5.
298    #[serde(
299        rename = "CEDARLING_LOCK_LOG_MAX_RETRIES",
300        deserialize_with = "deserialize_or_parse_string_as_json",
301        default = "default_log_max_retries"
302    )]
303    pub lock_log_max_retries: u32,
304
305    /// Maximum token cache TTL in seconds.
306    ///
307    /// Caps how long a validated token may stay in the cache. The effective
308    /// TTL for an entry is `min(time-until-exp, max_ttl)` when both apply.
309    ///
310    /// - `> 0`: cap each entry's TTL at this value. Also used as the TTL for
311    ///   tokens that do not carry an `exp` claim.
312    /// - `0`: disables the token cache entirely.
313    ///
314    /// Default: `5` seconds — small enough to pick up revocation / status-list
315    /// changes quickly, large enough to amortise repeated requests for the
316    /// same token.
317    #[serde(
318        rename = "CEDARLING_TOKEN_CACHE_MAX_TTL",
319        default = "default_token_cache_max_ttl"
320    )]
321    pub token_cache_max_ttl: usize,
322    /// Maximum number of tokens the cache can store.
323    /// Default value is 100.
324    /// 0 means no limit.
325    #[serde(
326        rename = "CEDARLING_TOKEN_CACHE_CAPACITY",
327        default = "default_token_cache_capacity"
328    )]
329    pub token_cache_capacity: usize,
330    /// Enables eviction policy based on the earliest expiration time.
331    ///
332    /// When the cache reaches its capacity, the entry with the nearest
333    /// expiration timestamp will be removed to make room for a new one.
334    #[serde(
335        rename = "CEDARLING_TOKEN_CACHE_EARLIEST_EXPIRATION_EVICTION",
336        default = "default_true"
337    )]
338    pub token_cache_earliest_expiration_eviction: bool,
339
340    // =========================================================================
341    // Data Store Configuration
342    // =========================================================================
343    /// Maximum number of data entries in the data store (0 = unlimited).
344    /// Default: 10,000
345    #[serde(rename = "CEDARLING_DATA_STORE_MAX_ENTRIES", default)]
346    #[serde(deserialize_with = "deserialize_or_parse_string_as_json")]
347    pub data_store_max_entries: Option<usize>,
348
349    /// Maximum size per data entry in bytes (0 = unlimited).
350    /// Default: 1MB (1,048,576 bytes)
351    #[serde(rename = "CEDARLING_DATA_STORE_MAX_ENTRY_SIZE", default)]
352    #[serde(deserialize_with = "deserialize_or_parse_string_as_json")]
353    pub data_store_max_entry_size: Option<usize>,
354
355    /// Default TTL for data entries in seconds.
356    /// If not set, entries do not expire.
357    #[serde(rename = "CEDARLING_DATA_STORE_DEFAULT_TTL", default)]
358    #[serde(deserialize_with = "deserialize_or_parse_string_as_json")]
359    pub data_store_default_ttl: Option<u64>,
360
361    /// Maximum allowed TTL for data entries in seconds.
362    /// Default: 3600 (1 hour). Entries with TTL exceeding this value will be rejected.
363    /// Note: `0` means a zero-second max TTL (immediate expiry), not unlimited.
364    /// Omitting this property uses the default (1 hour).
365    #[serde(rename = "CEDARLING_DATA_STORE_MAX_TTL", default)]
366    #[serde(deserialize_with = "deserialize_or_parse_string_as_json")]
367    pub data_store_max_ttl: Option<u64>,
368
369    /// Enable metrics tracking (access counts, timestamps) for data entries.
370    /// Default: true
371    #[serde(rename = "CEDARLING_DATA_STORE_ENABLE_METRICS", default)]
372    #[serde(deserialize_with = "deserialize_or_parse_string_as_json")]
373    pub data_store_enable_metrics: Option<bool>,
374
375    /// Memory usage alert threshold as a percentage (0.0-100.0).
376    /// When capacity usage exceeds this threshold, a warning is logged.
377    /// Default: 80.0 (80%)
378    #[serde(rename = "CEDARLING_DATA_STORE_MEMORY_ALERT_THRESHOLD", default)]
379    #[serde(deserialize_with = "deserialize_or_parse_string_as_json")]
380    pub data_store_memory_alert_threshold: Option<f64>,
381    /// Type of trusted issuer loader.
382    /// If not set, synchronous loader is used.
383    /// Can be `SYNC` or `ASYNC`.
384    ///
385    /// Sync loader means that trusted issuers will be loaded on initialization.
386    /// Async loader means that trusted issuers will be loaded in background.
387    #[serde(
388        rename = "CEDARLING_TRUSTED_ISSUER_LOADER_TYPE",
389        default,
390        deserialize_with = "deserialize_or_parse_string_as_json"
391    )]
392    pub trusted_issuer_loader_type: TrustedIssuerLoaderTypeRaw,
393    /// Number of concurrent workers to use when loading trusted issuers.
394    /// Applies to both SYNC (parallel loading during initialization) and ASYNC (parallel background loading) modes.
395    /// Minimum possible value is 1. Zero will be used as 1.
396    ///
397    /// For WASM maximum value is 6, default is 2.
398    /// For native maximum value is 1000, default is 10.
399    #[serde(
400        rename = "CEDARLING_TRUSTED_ISSUER_LOADER_WORKERS",
401        default,
402        deserialize_with = "deserialize_or_parse_string_as_json"
403    )]
404    pub trusted_issuer_loader_workers: WorkersCount,
405    // =========================================================================
406    // HTTP CLIENT CONFIGURATION
407    // =========================================================================
408    /// Per-request timeout in seconds.
409    #[cfg(not(target_arch = "wasm32"))]
410    #[serde(
411        rename = "CEDARLING_HTTP_REQUEST_TIMEOUT",
412        default = "default_http_client_request_timeout",
413        deserialize_with = "deserialize_or_parse_string_as_json"
414    )]
415    pub http_client_request_timeout: u64,
416    /// Maximum number of retry attempts per request.
417    #[serde(
418        rename = "CEDARLING_HTTP_REQUEST_MAX_RETRIES",
419        default = "default_http_client_max_retries",
420        deserialize_with = "deserialize_or_parse_string_as_json"
421    )]
422    pub http_client_request_max_retries: u32,
423
424    /// Base delay between retries in seconds.
425    #[serde(
426        rename = "CEDARLING_HTTP_REQUEST_RETRY_DELAY",
427        default = "default_http_client_retry_delay_secs",
428        deserialize_with = "deserialize_or_parse_string_as_json"
429    )]
430    pub http_client_request_retry_delay: u64,
431
432    /// Maximum HTTP response body size, in bytes. Rejects oversized responses
433    /// (JWKS, OIDC config, status list, policy store, Lock Server endpoints)
434    /// before they're fully buffered into memory. `0` disables the cap.
435    /// Default: 10 MB (`10485760`).
436    #[serde(
437        rename = "CEDARLING_HTTP_MAX_RESPONSE_SIZE_BYTES",
438        default = "default_http_client_max_response_size_bytes",
439        deserialize_with = "deserialize_or_parse_string_as_json"
440    )]
441    pub http_client_max_response_size_bytes: u64,
442
443    /// Optional override for JWKS periodic refresh interval in seconds.
444    /// When set, overrides the `Cache-Control: max-age` from the JWKS endpoint.
445    /// If omitted, the server-driven interval or a 1-hour fallback is used.
446    /// Values below 5 seconds are clamped to 5.
447    #[serde(rename = "CEDARLING_JWKS_REFRESH_INTERVAL", default)]
448    #[serde(deserialize_with = "deserialize_jwks_refresh_interval")]
449    pub jwks_refresh_interval: Option<u64>,
450
451    /// Minimum interval in seconds between on-demand JWKS re-fetches per issuer.
452    /// Prevents abuse from invalid JWT floods triggering excessive re-fetches.
453    /// Default: 30 seconds. Values below 5 seconds are clamped to 5.
454    #[serde(
455        rename = "CEDARLING_JWKS_REFRESH_MIN_INTERVAL",
456        default = "default_jwks_refresh_min_interval"
457    )]
458    #[serde(deserialize_with = "deserialize_jwks_refresh_min_interval")]
459    pub jwks_refresh_min_interval: u64,
460
461    /// Upper bound on the Status List JWT refresh interval, in seconds.
462    ///
463    /// Caps how long Cedarling waits between Status List refreshes. When the Status
464    /// List JWT carries a `ttl` claim, the effective refresh interval is
465    /// `min(jwt_ttl, status_list_refresh_interval_max)`, so the issuer can request a
466    /// *more frequent* refresh but never a less frequent one. When the JWT omits
467    /// `ttl`, this value is used directly. A value of `0` or an unset variable is
468    /// treated as "use the default" (300 seconds) so the status list cannot silently
469    /// go stale forever. Non-zero values below `5` are clamped to `5`.
470    ///
471    /// Fail-closed behavior: if any background refresh fails — fetch error, 5xx
472    /// response, or the status list body is invalid (JWT validation fails,
473    /// deserialization fails, or bit-string parsing fails) — the cached status list
474    /// is dropped and all tokens that reference it are rejected until the next
475    /// successful refresh. This prevents a revoked token from being accepted based on
476    /// stale data at the cost of temporarily denying valid tokens when the issuer's
477    /// status endpoint is unreachable or returns a malformed payload.
478    #[serde(
479        rename = "CEDARLING_JWT_STATUS_LIST_REFRESH_INTERVAL_MAX",
480        default = "default_status_list_refresh_interval_max"
481    )]
482    #[serde(deserialize_with = "deserialize_status_list_refresh_interval_max")]
483    pub status_list_refresh_interval_max: u64,
484
485    /// Base refresh interval, in seconds, for periodic background refresh of
486    /// remote policy stores (`CjarUrl` / `LockServer`). `0` disables refresh and
487    /// preserves the load-once-at-startup behavior. Non-zero values below the
488    /// `MIN_REFRESH_INTERVAL_SECS` floor are clamped at service-init time (with
489    /// a `WARN` log emitted) so the worker never busy-polls the upstream — see
490    /// [`PolicyStoreConfig::effective_refresh_interval`]. A server
491    /// `Cache-Control: max-age` / `Expires` hint can *shorten* the next
492    /// interval but never extends it.
493    #[serde(rename = "CEDARLING_POLICY_STORE_REFRESH_INTERVAL", default)]
494    #[serde(deserialize_with = "deserialize_or_parse_string_as_json")]
495    pub policy_store_refresh_interval_secs: u64,
496}
497
498impl Default for BootstrapConfigRaw {
499    fn default() -> Self {
500        serde_json::from_value(serde_json::json!({"CEDARLING_APPLICATION_NAME":""}))
501            .expect("BootstrapConfigRaw should be deserialized from empty json object")
502    }
503}
504
505impl BootstrapConfigRaw {
506    /// Construct `BootstrapConfig` from environment variables and `BootstrapConfigRaw` config.
507    /// Environment variables have bigger priority.
508    //
509    // Simple implementation that map input structure to JSON map
510    // and map environment variables with prefix `CEDARLING_` to JSON map. And merge it.
511    #[cfg(not(target_arch = "wasm32"))]
512    pub fn from_raw_config_and_env(
513        raw: Option<BootstrapConfigRaw>,
514    ) -> Result<Self, BootstrapConfigLoadingError> {
515        let mut json_config_params = serde_json::json!(raw.unwrap_or_default())
516            .as_object()
517            .map(std::borrow::ToOwned::to_owned)
518            .unwrap_or_default();
519
520        for (k, v) in get_cedarling_env_vars() {
521            // update map with values from env variables
522            json_config_params.insert(k, v);
523        }
524
525        Ok(BootstrapConfigRaw::deserialize(json_config_params)?)
526    }
527}
528
529/// Get environment variables related to `Cedarling`
530#[cfg(not(target_arch = "wasm32"))]
531fn get_cedarling_env_vars() -> HashMap<String, serde_json::Value> {
532    env::vars()
533        .filter_map(|(k, v)| {
534            k.starts_with("CEDARLING_")
535                .then_some((k, serde_json::json!(v)))
536        })
537        .collect()
538}
539
540#[cfg(test)]
541mod tests {
542    use super::*;
543    use crate::jwt_config::{MIN_JWKS_REFRESH_SECS, MIN_STATUS_LIST_REFRESH_SECS};
544    use std::{
545        env,
546        sync::{LazyLock, Mutex},
547    };
548    use test_utils::assert_eq;
549
550    static ENV_MUTEX: LazyLock<Mutex<()>> = LazyLock::new(|| Mutex::new(()));
551
552    fn with_env_vars<F>(vars: &[(&str, &str)], test: F)
553    where
554        F: FnOnce(),
555    {
556        // Ensure only one test modifies env vars at a time
557        let _lock = ENV_MUTEX.lock().unwrap();
558
559        // Create a fresh environment by clearing all CEDARLING_* vars
560        let mut all_vars = Vec::new();
561        for (key, value) in env::vars() {
562            let key_clone = key.clone();
563            all_vars.push((key, value));
564            unsafe {
565                env::remove_var(&key_clone);
566            }
567        }
568
569        // Set new env vars
570        for (key, value) in vars {
571            unsafe {
572                env::set_var(key, value);
573            }
574        }
575
576        test();
577
578        // Clean up
579        for (key, _) in vars {
580            unsafe {
581                env::remove_var(key);
582            }
583        }
584
585        // Restore original environment
586        for (key, value) in all_vars {
587            unsafe {
588                env::set_var(&key, value);
589            }
590        }
591    }
592
593    /// Tests that the default configuration values are correctly set when no environment variables
594    /// or raw config is provided.
595    #[test]
596    fn test_from_raw_config_and_env_defaults() {
597        with_env_vars(&[], || {
598            let config = BootstrapConfigRaw::from_raw_config_and_env(None).unwrap();
599            assert_eq!(
600                config.application_name, "",
601                "Application name should be empty by default"
602            );
603            assert_eq!(
604                config.policy_store_uri, None,
605                "Policy store URI should be None by default"
606            );
607            assert_eq!(
608                config.log_type,
609                LoggerType::Off,
610                "Logger should be off by default"
611            );
612            assert_eq!(
613                config.log_level,
614                LogLevel::WARN,
615                "Default log level should be WARN"
616            );
617            assert_eq!(config.log_ttl, None, "Log TTL should be None by default");
618            assert_eq!(
619                config.decision_log_default_jwt_id, "jti",
620                "Default JWT ID for decision logging should be 'jti'"
621            );
622            assert_eq!(
623                config.lock_transport,
624                LockTransport::Rest,
625                "Default transport should be REST"
626            );
627        });
628    }
629
630    /// Tests that configuration values are correctly set when only a raw config is provided.
631    #[test]
632    fn test_from_raw_config_and_env() {
633        with_env_vars(&[], || {
634            let raw = BootstrapConfigRaw {
635                application_name: "test-app".to_string(),
636                log_type: LoggerType::Memory,
637                log_level: LogLevel::DEBUG,
638                ..Default::default()
639            };
640
641            let config = BootstrapConfigRaw::from_raw_config_and_env(Some(raw)).unwrap();
642
643            assert_eq!(config.application_name, "test-app");
644            assert_eq!(config.log_type, LoggerType::Memory);
645            assert_eq!(config.log_level, LogLevel::DEBUG);
646        });
647    }
648
649    /// Tests that configuration values are correctly set when only environment variables are provided.
650    #[test]
651    fn test_from_raw_config_and_env_with_env_vars() {
652        with_env_vars(
653            &[
654                ("CEDARLING_APPLICATION_NAME", "env-app"),
655                ("CEDARLING_LOG_TYPE", "memory"),
656                ("CEDARLING_LOG_LEVEL", "DEBUG"),
657            ],
658            || {
659                let config = BootstrapConfigRaw::from_raw_config_and_env(None).unwrap();
660
661                assert_eq!(config.application_name, "env-app");
662                assert_eq!(config.log_type, LoggerType::Memory);
663                assert_eq!(config.log_level, LogLevel::DEBUG);
664            },
665        );
666    }
667
668    /// Tests that environment variables override raw config values when both are provided.
669    #[test]
670    fn test_from_raw_config_and_env_vars_override() {
671        with_env_vars(
672            &[
673                ("CEDARLING_APPLICATION_NAME", "env-app"),
674                ("CEDARLING_LOG_TYPE", "memory"),
675            ],
676            || {
677                let raw = BootstrapConfigRaw {
678                    application_name: "test-app".to_string(),
679                    log_type: LoggerType::StdOut,
680                    log_level: LogLevel::INFO,
681                    ..Default::default()
682                };
683
684                let config = BootstrapConfigRaw::from_raw_config_and_env(Some(raw)).unwrap();
685
686                // Env vars should override raw config
687                assert_eq!(config.application_name, "env-app");
688                assert_eq!(config.log_type, LoggerType::Memory);
689
690                // Fields not set in env should use raw config values
691                assert_eq!(config.log_level, LogLevel::INFO);
692            },
693        );
694    }
695
696    /// Tests that an error is returned when an invalid environment variable value is provided.
697    #[test]
698    fn test_from_raw_config_and_env_invalid_env_var() {
699        with_env_vars(&[("CEDARLING_LOG_TYPE", "invalid")], || {
700            let result = BootstrapConfigRaw::from_raw_config_and_env(None);
701            assert!(result.is_err());
702        });
703    }
704
705    /// Tests that empty string values in environment variables are handled correctly.
706    #[test]
707    fn test_from_raw_config_and_env_empty_strings() {
708        with_env_vars(
709            &[
710                ("CEDARLING_APPLICATION_NAME", ""),
711                ("CEDARLING_POLICY_STORE_URI", ""),
712            ],
713            || {
714                let config = BootstrapConfigRaw::from_raw_config_and_env(None).unwrap();
715
716                assert_eq!(config.application_name, "");
717                assert_eq!(config.policy_store_uri, None);
718            },
719        );
720    }
721
722    /// Tests that environment variables for trusted issuer loader are parsed correctly.
723    #[test]
724    fn test_trusted_issuer_loader_env_vars() {
725        with_env_vars(
726            &[
727                ("CEDARLING_TRUSTED_ISSUER_LOADER_TYPE", "ASYNC"),
728                ("CEDARLING_TRUSTED_ISSUER_LOADER_WORKERS", "5"),
729            ],
730            || {
731                let config = BootstrapConfigRaw::from_raw_config_and_env(None).unwrap();
732
733                assert_eq!(
734                    config.trusted_issuer_loader_type,
735                    TrustedIssuerLoaderTypeRaw::Async,
736                    "Loader type should be Async from env var"
737                );
738                assert_eq!(
739                    config.trusted_issuer_loader_workers, 5,
740                    "Worker count should be 5 from env var"
741                );
742            },
743        );
744    }
745
746    /// Tests JSON deserialization for trusted issuer loader fields.
747    #[test]
748    fn test_trusted_issuer_loader_json_deserialization() {
749        // Valid JSON values
750        let valid_cases = vec![
751            (
752                r#"{"CEDARLING_APPLICATION_NAME": "", "CEDARLING_TRUSTED_ISSUER_LOADER_TYPE": "SYNC", "CEDARLING_TRUSTED_ISSUER_LOADER_WORKERS": 3}"#,
753                TrustedIssuerLoaderTypeRaw::Sync,
754                3,
755            ),
756            (
757                r#"{"CEDARLING_APPLICATION_NAME": "", "CEDARLING_TRUSTED_ISSUER_LOADER_TYPE": "ASYNC", "CEDARLING_TRUSTED_ISSUER_LOADER_WORKERS": 1}"#,
758                TrustedIssuerLoaderTypeRaw::Async,
759                1,
760            ),
761        ];
762
763        for (json, expected_type, expected_workers) in valid_cases {
764            let config: BootstrapConfigRaw = serde_json::from_str(json).unwrap();
765            assert_eq!(
766                config.trusted_issuer_loader_type, expected_type,
767                "Loader type mismatch for JSON: {}",
768                json
769            );
770            assert_eq!(
771                config.trusted_issuer_loader_workers, expected_workers,
772                "Worker count mismatch for JSON: {}",
773                json
774            );
775        }
776
777        // Invalid JSON values should produce errors
778        let invalid_cases = vec![
779            r#"{"CEDARLING_APPLICATION_NAME": "", "CEDARLING_TRUSTED_ISSUER_LOADER_TYPE": "INVALID"}"#,
780            r#"{"CEDARLING_APPLICATION_NAME": "", "CEDARLING_TRUSTED_ISSUER_LOADER_WORKERS": -1}"#,
781            r#"{"CEDARLING_APPLICATION_NAME": "", "CEDARLING_TRUSTED_ISSUER_LOADER_WORKERS": "not_a_number"}"#,
782            r#"{"CEDARLING_APPLICATION_NAME": "", "CEDARLING_TRUSTED_ISSUER_LOADER_TYPE": 123}"#,
783        ];
784
785        for json in invalid_cases {
786            let result: Result<BootstrapConfigRaw, _> = serde_json::from_str(json);
787            result.expect_err(&format!("Should fail to parse invalid JSON: {json}"));
788        }
789    }
790
791    /// Tests `CEDARLING_TRUSTED_ISSUER_LOADER_WORKERS` to get clamped minimum value
792    #[test]
793    fn test_trusted_issuer_loader_claps_min() {
794        with_env_vars(&[("CEDARLING_TRUSTED_ISSUER_LOADER_WORKERS", "0")], || {
795            let config = BootstrapConfigRaw::from_raw_config_and_env(None).unwrap();
796
797            assert_eq!(
798                config.trusted_issuer_loader_workers,
799                WorkersCount::MIN,
800                "Default worker count should be {}",
801                WorkersCount::MIN.get()
802            );
803        });
804    }
805
806    /// Tests `CEDARLING_TRUSTED_ISSUER_LOADER_WORKERS` to get clamped max value
807    #[test]
808    fn test_trusted_issuer_loader_claps_max() {
809        let max_val = (WorkersCount::MAX.get() + 100).to_string();
810
811        with_env_vars(
812            &[("CEDARLING_TRUSTED_ISSUER_LOADER_WORKERS", max_val.as_str())],
813            || {
814                let config = BootstrapConfigRaw::from_raw_config_and_env(None).unwrap();
815
816                assert_eq!(
817                    config.trusted_issuer_loader_workers,
818                    WorkersCount::MAX,
819                    "Default worker count should be {}",
820                    WorkersCount::MAX.get()
821                );
822            },
823        );
824    }
825
826    #[test]
827    fn test_jwks_refresh_interval_from_env_var() {
828        with_env_vars(
829            &[
830                ("CEDARLING_JWKS_REFRESH_INTERVAL", "30"),
831                ("CEDARLING_JWKS_REFRESH_MIN_INTERVAL", "60"),
832            ],
833            || {
834                let config = BootstrapConfigRaw::from_raw_config_and_env(None).unwrap();
835
836                assert_eq!(
837                    config.jwks_refresh_interval,
838                    Some(30),
839                    "JWKS refresh interval should match environment value"
840                );
841                assert_eq!(
842                    config.jwks_refresh_min_interval, 60,
843                    "JWKS refresh min interval should match environment value"
844                );
845            },
846        );
847    }
848
849    #[test]
850    fn test_jwks_refresh_interval_clamps_min() {
851        with_env_vars(
852            &[
853                ("CEDARLING_JWKS_REFRESH_INTERVAL", "0"),
854                ("CEDARLING_JWKS_REFRESH_MIN_INTERVAL", "0"),
855            ],
856            || {
857                let config = BootstrapConfigRaw::from_raw_config_and_env(None).unwrap();
858
859                assert_eq!(
860                    config.jwks_refresh_interval,
861                    Some(MIN_JWKS_REFRESH_SECS),
862                    "JWKS refresh interval should be clamped to minimum"
863                );
864                assert_eq!(
865                    config.jwks_refresh_min_interval, MIN_JWKS_REFRESH_SECS,
866                    "JWKS refresh min interval should be clamped to minimum"
867                );
868            },
869        );
870    }
871
872    #[test]
873    fn test_jwks_refresh_interval_clamps_below_min() {
874        with_env_vars(
875            &[
876                ("CEDARLING_JWKS_REFRESH_INTERVAL", "3"),
877                ("CEDARLING_JWKS_REFRESH_MIN_INTERVAL", "3"),
878            ],
879            || {
880                let config = BootstrapConfigRaw::from_raw_config_and_env(None).unwrap();
881
882                assert_eq!(
883                    config.jwks_refresh_interval,
884                    Some(MIN_JWKS_REFRESH_SECS),
885                    "JWKS refresh interval should be clamped to minimum"
886                );
887                assert_eq!(
888                    config.jwks_refresh_min_interval, MIN_JWKS_REFRESH_SECS,
889                    "JWKS refresh min interval should be clamped to minimum"
890                );
891            },
892        );
893    }
894
895    #[test]
896    fn test_status_list_refresh_interval_max_default() {
897        with_env_vars(&[], || {
898            let config = BootstrapConfigRaw::from_raw_config_and_env(None).unwrap();
899            assert_eq!(
900                config.status_list_refresh_interval_max,
901                JwtConfig::DEFAULT_STATUS_LIST_REFRESH_INTERVAL_MAX_SECS,
902                "missing env var should resolve to the JwtConfig default"
903            );
904        });
905    }
906
907    #[test]
908    fn test_status_list_refresh_interval_max_from_env() {
909        with_env_vars(
910            &[("CEDARLING_JWT_STATUS_LIST_REFRESH_INTERVAL_MAX", "120")],
911            || {
912                let config = BootstrapConfigRaw::from_raw_config_and_env(None).unwrap();
913                assert_eq!(
914                    config.status_list_refresh_interval_max, 120,
915                    "status list refresh max should match the value supplied via env var"
916                );
917            },
918        );
919    }
920
921    #[test]
922    fn test_status_list_refresh_interval_max_zero_uses_default() {
923        with_env_vars(
924            &[("CEDARLING_JWT_STATUS_LIST_REFRESH_INTERVAL_MAX", "0")],
925            || {
926                let config = BootstrapConfigRaw::from_raw_config_and_env(None).unwrap();
927                assert_eq!(
928                    config.status_list_refresh_interval_max,
929                    JwtConfig::DEFAULT_STATUS_LIST_REFRESH_INTERVAL_MAX_SECS,
930                    "0 should be treated as 'use the default'"
931                );
932            },
933        );
934    }
935
936    #[test]
937    fn test_status_list_refresh_interval_max_clamps_below_min() {
938        with_env_vars(
939            &[("CEDARLING_JWT_STATUS_LIST_REFRESH_INTERVAL_MAX", "2")],
940            || {
941                let config = BootstrapConfigRaw::from_raw_config_and_env(None).unwrap();
942                assert_eq!(
943                    config.status_list_refresh_interval_max, MIN_STATUS_LIST_REFRESH_SECS,
944                    "non-zero values below the minimum should be clamped"
945                );
946            },
947        );
948    }
949}