1#![deny(missing_docs)]
7#![warn(unreachable_pub)]
8#![allow(clippy::missing_errors_doc)]
9mod async_sleep;
18mod authz;
19mod bootstrap_config;
20mod common;
21mod context_data_api;
22mod entity_builder;
23mod http;
24mod http_utils;
25mod init;
26mod jwt;
27mod lock;
28mod log;
29#[doc(hidden)]
31pub mod sparkv;
32
33#[cfg(not(target_arch = "wasm32"))]
34#[cfg(feature = "blocking")]
35pub mod blocking;
36
37#[doc(hidden)]
38#[cfg(test)]
39mod tests;
40
41use std::collections::HashSet;
42use std::{fmt::Write, sync::Arc};
43
44use crate::authz::metrics::MetricsCollector;
45use crate::context_data_api::DataStore;
46pub use crate::context_data_api::{
47 CedarType, CedarValueMapper, ConfigValidationError, DataApi, DataEntry, DataError,
48 DataStoreConfig, DataStoreStats, DataValidator, ExtensionValue, ValidationConfig,
49 ValidationError, ValidationResult, ValueMappingError,
50};
51pub use crate::jwt::TrustedIssuerLoadingInfo;
52use authz::Authz;
53pub use authz::request::{
54 AuthorizeMultiIssuerRequest, CedarEntityMapping, EntityData, RequestUnsigned, TokenInput,
55};
56pub use authz::{AuthorizeError, AuthorizeResult, MultiIssuerAuthorizeResult};
57pub use bootstrap_config::*;
58use common::app_types::{self, ApplicationName};
59pub use common::policy_store::{PolicyEffect, PolicyMetadata};
60pub use http::HttpClientConfig;
61use init::ServiceFactory;
62use init::policy_store::{LoadedPolicyStore, load_policy_store};
63use init::policy_store_refresh::{
64 AuthzRebuilder, PolicyStoreRefreshHandle, RefreshSource, RefreshWorkerSeed, WorkerContext,
65 spawn_refresh_worker,
66};
67use init::service_config::{ServiceConfig, ServiceConfigError};
68use init::service_factory::ServiceInitError;
69use lock::InitLockServiceError;
70use lock::health_registry::HealthStatus;
71use log::interface::LogWriter;
72use log::{BaseLogEntry, LogEntry};
73pub use log::{LogLevel, LogStorage};
74
75use semver::Version;
76
77const BUILD_COMMIT: Option<&str> = option_env!("CEDARLING_BUILD_COMMIT");
80const BUILD_TIMESTAMP: Option<&str> = option_env!("CEDARLING_BUILD_TIMESTAMP");
83
84#[doc(hidden)]
85pub mod bindings {
86 pub use cedar_policy;
87
88 pub use super::log::{
89 AuthorizationLogInfo, Decision, Diagnostics, LogEntry, PolicyEvaluationError,
90 };
91 pub use crate::common::policy_store::PolicyStore;
92 pub use crate::http::spawn_task;
93 pub use crate::sparkv;
94 pub use serde_json;
95 pub use serde_yaml_ng;
96}
97
98#[derive(Debug, thiserror::Error)]
100pub enum InitCedarlingError {
101 #[error(transparent)]
103 ServiceConfig(#[from] ServiceConfigError),
104 #[error(transparent)]
106 ServiceInit(#[from] ServiceInitError),
107 #[error(transparent)]
109 BootstrapConfigLoading(#[from] BootstrapConfigLoadingError),
110 #[error(transparent)]
112 DataStoreInit(#[from] ConfigValidationError),
113 #[cfg(feature = "blocking")]
114 #[error(transparent)]
116 RuntimeInit(std::io::Error),
117 #[error("failed to initialize the Lock Service: {0}")]
120 InitLockService(#[from] InitLockServiceError),
121}
122
123#[derive(Clone)]
126pub struct Cedarling {
127 log: log::Logger,
128 authz: Arc<arc_swap::ArcSwap<Authz>>,
134 data: Arc<DataStore>,
135 _refresh_handle: Option<Arc<PolicyStoreRefreshHandle>>,
140}
141
142impl Cedarling {
143 #[cfg(not(target_arch = "wasm32"))]
147 pub async fn new_with_env(
148 raw_config: Option<BootstrapConfigRaw>,
149 ) -> Result<Cedarling, InitCedarlingError> {
150 let config = BootstrapConfig::from_raw_config_and_env(raw_config)?;
151 Self::new(&config).await
152 }
153
154 pub async fn new(config: &BootstrapConfig) -> Result<Cedarling, InitCedarlingError> {
156 let pdp_id = app_types::PdpID::new();
157 let app_name = (!config.application_name.is_empty())
158 .then(|| ApplicationName::from(config.application_name.clone()));
159
160 let metrics = Arc::new(
161 if config
162 .lock_config
163 .as_ref()
164 .is_some_and(|c| c.telemetry_interval.is_some())
165 {
166 MetricsCollector::new(0)
167 } else {
168 MetricsCollector::disabled()
169 },
170 );
171
172 let log = crate::log::init_logger(
173 &config.log_config,
174 pdp_id,
175 app_name,
176 config.lock_config.as_ref(),
177 metrics.clone(),
178 config.http_client_config,
179 )
180 .await?;
181
182 log.log_any(
183 LogEntry::new(BaseLogEntry::new_system_opt_request_id(
184 LogLevel::INFO,
185 None,
186 ))
187 .set_message("Cedarling initialization started".to_string())
188 .set_build_info(BUILD_COMMIT, BUILD_TIMESTAMP),
189 );
190
191 let (service_config, refresh_seed) = perform_bootstrap_load(config, &log).await?;
196
197 let policy_count = service_config
198 .policy_store
199 .policies
200 .get_set()
201 .num_of_policies();
202 metrics.set_policy_count(policy_count);
203
204 let data = Arc::new(DataStore::new(
206 config.data_store_config.clone(),
207 metrics.clone(),
208 )?);
209
210 let mut service_factory = ServiceFactory::new(
211 config,
212 service_config,
213 log.clone(),
214 data.clone(),
215 metrics.clone(),
216 );
217
218 if let Some(metadata) = service_factory.policy_store_metadata() {
220 log_policy_store_metadata(&log, metadata);
221 }
222
223 if let Some(registry) = log.health_registry() {
224 registry.register("core", || HealthStatus::Success);
225 registry.register("policy_load", move || {
226 if policy_count > 0 {
227 HealthStatus::Success
228 } else {
229 HealthStatus::Failure
230 }
231 });
232 }
233
234 let authz = service_factory.authz_service().await?;
235 let authz_swap = Arc::new(arc_swap::ArcSwap::from(authz));
236
237 let refresh_handle = maybe_spawn_refresh_worker(
238 config,
239 &service_factory,
240 authz_swap.clone(),
241 log.clone(),
242 data.clone(),
243 metrics.clone(),
244 refresh_seed,
245 );
246
247 Ok(Cedarling {
248 log,
249 authz: authz_swap,
250 data,
251 _refresh_handle: refresh_handle,
252 })
253 }
254
255 #[allow(clippy::unused_async)]
263 pub async fn authorize_unsigned(
264 &self,
265 request: RequestUnsigned,
266 ) -> Result<AuthorizeResult, AuthorizeError> {
267 self.authz.load().authorize_unsigned(&request)
268 }
269
270 #[allow(clippy::unused_async)]
273 pub async fn authorize_multi_issuer(
274 &self,
275 request: AuthorizeMultiIssuerRequest,
276 ) -> Result<MultiIssuerAuthorizeResult, AuthorizeError> {
277 self.authz.load().authorize_multi_issuer(&request)
278 }
279
280 pub fn get_matching_policies_unsigned(
286 &self,
287 principal: Option<&EntityData>,
288 actions: &[String],
289 resources: &[EntityData],
290 ) -> Result<Vec<PolicyMetadata>, AuthorizeError> {
291 self.authz
292 .load()
293 .get_matching_policies_unsigned(principal, actions, resources)
294 }
295
296 pub fn get_matching_policies_multi_issuer(
301 &self,
302 tokens: &[TokenInput],
303 actions: &[String],
304 resources: &[EntityData],
305 ) -> Result<Vec<PolicyMetadata>, AuthorizeError> {
306 self.authz
307 .load()
308 .get_matching_policies_multi_issuer(tokens, actions, resources)
309 }
310
311 pub async fn shut_down(&self) {
313 self.log.shut_down().await;
314 }
315}
316
317impl TrustedIssuerLoadingInfo for Cedarling {
318 fn is_trusted_issuer_loaded_by_name(&self, issuer_id: &str) -> bool {
319 self.authz
320 .load()
321 .is_trusted_issuer_loaded_by_name(issuer_id)
322 }
323
324 fn is_trusted_issuer_loaded_by_iss(&self, iss_claim: &str) -> bool {
325 self.authz.load().is_trusted_issuer_loaded_by_iss(iss_claim)
326 }
327
328 fn total_issuers(&self) -> usize {
329 self.authz.load().total_issuers()
330 }
331
332 fn loaded_trusted_issuers_count(&self) -> usize {
333 self.authz.load().loaded_trusted_issuers_count()
334 }
335
336 fn loaded_trusted_issuer_ids(&self) -> HashSet<String> {
337 self.authz.load().loaded_trusted_issuer_ids()
338 }
339
340 fn failed_trusted_issuer_ids(&self) -> HashSet<String> {
341 self.authz.load().failed_trusted_issuer_ids()
342 }
343}
344
345async fn perform_bootstrap_load(
352 config: &BootstrapConfig,
353 log: &log::Logger,
354) -> Result<(ServiceConfig, RefreshWorkerSeed), ServiceConfigError> {
355 let raw_load: Result<(http::HttpClient, LoadedPolicyStore), ServiceConfigError> = async {
356 let http_client = http::HttpClient::new(config.http_client_config)?;
357 let loaded = load_policy_store(
358 &config.policy_store_config,
359 &http_client,
360 config.authorization_config.strict_schema_validation,
361 )
362 .await?;
363 Ok((http_client, loaded))
364 }
365 .await;
366
367 let (http_client, loaded) = raw_load
368 .inspect(|_| {
369 log.log_any(
370 LogEntry::new(BaseLogEntry::new_system_opt_request_id(
371 LogLevel::DEBUG,
372 None,
373 ))
374 .set_message("configuration parsed successfully".to_string()),
375 );
376 })
377 .inspect_err(|err| {
378 log.log_any(
379 LogEntry::new(BaseLogEntry::new_system_opt_request_id(
380 LogLevel::ERROR,
381 None,
382 ))
383 .set_error(err.to_string())
384 .set_message("configuration parsed with error".to_string()),
385 );
386 })?;
387
388 let LoadedPolicyStore {
389 store: policy_store,
390 body_hash,
391 validators,
392 } = loaded;
393 Ok((
394 ServiceConfig {
395 policy_store,
396 http_client,
397 },
398 RefreshWorkerSeed {
399 initial_body_hash: body_hash,
400 initial_validators: validators,
401 },
402 ))
403}
404
405fn maybe_spawn_refresh_worker(
412 config: &BootstrapConfig,
413 service_factory: &ServiceFactory<'_>,
414 authz_swap: Arc<arc_swap::ArcSwap<authz::Authz>>,
415 log: log::Logger,
416 data: Arc<context_data_api::DataStore>,
417 metrics: Arc<authz::metrics::MetricsCollector>,
418 seed: RefreshWorkerSeed,
419) -> Option<Arc<PolicyStoreRefreshHandle>> {
420 if !config.policy_store_config.refresh_enabled() {
421 return None;
422 }
423 let source = RefreshSource::from_policy_store_source(&config.policy_store_config.source)?;
424 let (interval_secs, clamped) = config.policy_store_config.effective_refresh_interval();
425 if clamped {
426 log.log_any(
427 LogEntry::new(BaseLogEntry::new_system_opt_request_id(LogLevel::WARN, None))
428 .set_message(format!(
429 "CEDARLING_POLICY_STORE_REFRESH_INTERVAL={} is below the minimum; clamped to {} seconds",
430 config.policy_store_config.refresh_interval_secs,
431 interval_secs,
432 )),
433 );
434 }
435 let rebuilder = AuthzRebuilder {
436 jwt_config: config.jwt_config.clone(),
437 authorization_config: config.authorization_config.clone(),
438 http_client: service_factory.http_client_for_refresh(),
439 log: log.clone(),
440 data_store: data,
441 metrics: metrics.clone(),
442 };
443 let ctx = WorkerContext {
444 source,
445 interval_secs,
446 http_client: service_factory.http_client_for_refresh(),
447 rebuilder,
448 authz_swap,
449 metrics,
450 log,
451 initial_body_hash: seed.initial_body_hash,
452 initial_validators: seed.initial_validators,
453 strict_schema_validation: config.authorization_config.strict_schema_validation,
454 };
455 Some(Arc::new(spawn_refresh_worker(ctx)))
456}
457
458fn log_policy_store_metadata(
462 log: &log::Logger,
463 metadata: &crate::common::policy_store::PolicyStoreMetadata,
464) {
465 let mut details = format!(
467 "Policy store '{}' (ID: {}) v{} loaded",
468 metadata.name(),
469 if metadata.id().is_empty() {
470 "<auto>"
471 } else {
472 metadata.id()
473 },
474 metadata.version()
475 );
476
477 if let Some(desc) = metadata.description() {
479 let _ = write!(details, " - {desc}");
480 }
481
482 let _ = write!(details, " [Cedar {}]", metadata.cedar_version());
484
485 if let Some(created) = metadata.created_date() {
487 let _ = write!(details, " (created: {})", created.format("%Y-%m-%d"));
488 }
489 if let Some(updated) = metadata.updated_date() {
490 let _ = write!(details, " (updated: {})", updated.format("%Y-%m-%d"));
491 }
492
493 log.log_any(
494 LogEntry::new(BaseLogEntry::new_system_opt_request_id(
495 LogLevel::DEBUG,
496 None,
497 ))
498 .set_message(details),
499 );
500
501 let current_cedar_version: Version = cedar_policy::get_lang_version();
503 match metadata.is_compatible_with_cedar(¤t_cedar_version) {
504 Ok(true) => {
505 log.log_any(
506 LogEntry::new(BaseLogEntry::new_system_opt_request_id(
507 LogLevel::DEBUG,
508 None,
509 ))
510 .set_message(format!(
511 "Policy store Cedar version {} is compatible with runtime version {}",
512 metadata.cedar_version(),
513 current_cedar_version
514 )),
515 );
516 },
517 Ok(false) => {
518 log.log_any(
519 LogEntry::new(BaseLogEntry::new_system_opt_request_id(
520 LogLevel::WARN,
521 None,
522 ))
523 .set_message(format!(
524 "Policy store Cedar version {} may not be compatible with runtime version {}",
525 metadata.cedar_version(),
526 current_cedar_version
527 )),
528 );
529 },
530 Err(e) => {
531 log.log_any(
532 LogEntry::new(BaseLogEntry::new_system_opt_request_id(
533 LogLevel::WARN,
534 None,
535 ))
536 .set_message(format!("Could not check Cedar version compatibility: {e}")),
537 );
538 },
539 }
540
541 if let Some(parsed_version) = metadata.version_parsed() {
543 log.log_any(
544 LogEntry::new(BaseLogEntry::new_system_opt_request_id(
545 LogLevel::TRACE,
546 None,
547 ))
548 .set_message(format!(
549 "Policy store semantic version: {}.{}.{}",
550 parsed_version.major, parsed_version.minor, parsed_version.patch
551 )),
552 );
553 }
554}
555
556impl LogStorage for Cedarling {
559 fn pop_logs(&self) -> Vec<serde_json::Value> {
560 self.log.pop_logs()
561 }
562
563 fn get_log_by_id(&self, id: &str) -> Option<serde_json::Value> {
564 self.log.get_log_by_id(id)
565 }
566
567 fn get_log_ids(&self) -> Vec<String> {
568 self.log.get_log_ids()
569 }
570
571 fn get_logs_by_tag(&self, tag: &str) -> Vec<serde_json::Value> {
572 self.log.get_logs_by_tag(tag)
573 }
574
575 fn get_logs_by_request_id(&self, request_id: &str) -> Vec<serde_json::Value> {
576 self.log.get_logs_by_request_id(request_id)
577 }
578
579 fn get_logs_by_request_id_and_tag(&self, id: &str, tag: &str) -> Vec<serde_json::Value> {
580 self.log.get_logs_by_request_id_and_tag(id, tag)
581 }
582}
583
584fn calculate_capacity_usage(
587 entry_count: usize,
588 max_entries: usize,
589 memory_alert_threshold: f64,
590) -> (f64, bool) {
591 #[allow(clippy::cast_precision_loss)]
593 let capacity_usage_percent = if max_entries > 0 {
594 (entry_count as f64 / max_entries as f64) * 100.0
595 } else {
596 0.0 };
598 let memory_alert_triggered = capacity_usage_percent >= memory_alert_threshold;
599 (capacity_usage_percent, memory_alert_triggered)
600}
601
602impl DataApi for Cedarling {
604 fn push_data_ctx(
605 &self,
606 key: &str,
607 value: serde_json::Value,
608 ttl: Option<std::time::Duration>,
609 ) -> Result<(), DataError> {
610 self.data.push(key, value, ttl)?;
611
612 let config = self.data.config();
614 if config.max_entries > 0 {
615 let entry_count = self.data.count();
616 let (capacity_usage_percent, memory_alert_triggered) = calculate_capacity_usage(
617 entry_count,
618 config.max_entries,
619 config.memory_alert_threshold,
620 );
621 if memory_alert_triggered {
622 let log_entry = LogEntry::new(BaseLogEntry::new_system_opt_request_id(
623 LogLevel::WARN,
624 None,
625 ))
626 .set_message(format!(
627 "DataStore memory usage alert: {:.1}% capacity used ({}/{} entries), threshold: {:.1}%",
628 capacity_usage_percent,
629 entry_count,
630 config.max_entries,
631 config.memory_alert_threshold
632 ));
633 self.log.log_any(log_entry);
634 }
635 }
636
637 Ok(())
638 }
639
640 fn get_data_ctx(&self, key: &str) -> Result<Option<serde_json::Value>, DataError> {
641 Ok(self.data.get(key))
642 }
643
644 fn get_data_entry_ctx(&self, key: &str) -> Result<Option<DataEntry>, DataError> {
645 Ok(self.data.get_entry(key))
646 }
647
648 fn remove_data_ctx(&self, key: &str) -> Result<bool, DataError> {
649 Ok(self.data.remove(key))
650 }
651
652 fn clear_data_ctx(&self) -> Result<(), DataError> {
653 self.data.clear();
654 Ok(())
655 }
656
657 fn list_data_ctx(&self) -> Result<Vec<DataEntry>, DataError> {
658 Ok(self.data.list_entries())
659 }
660
661 fn get_stats_ctx(&self) -> Result<DataStoreStats, DataError> {
662 let config = self.data.config();
663 let entry_count = self.data.count();
664 let total_size_bytes = self.data.total_size();
665 let avg_entry_size_bytes = total_size_bytes.checked_div(entry_count).unwrap_or(0);
666
667 let (capacity_usage_percent, memory_alert_triggered) = calculate_capacity_usage(
669 entry_count,
670 config.max_entries,
671 config.memory_alert_threshold,
672 );
673
674 Ok(DataStoreStats {
675 entry_count,
676 max_entries: config.max_entries,
677 max_entry_size: config.max_entry_size,
678 metrics_enabled: config.enable_metrics,
679 total_size_bytes,
680 avg_entry_size_bytes,
681 capacity_usage_percent,
682 memory_alert_threshold: config.memory_alert_threshold,
683 memory_alert_triggered,
684 })
685 }
686}