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::{HashMap, 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, BatchAuthorizeMultiIssuerRequest, BatchAuthorizeResponse,
55 BatchAuthorizeUnsignedRequest, BatchItem, CedarEntityMapping, EntityData, RequestUnsigned,
56 TokenInput,
57};
58pub use authz::{
59 AuthorizeError, AuthorizeResult, BatchItemError, MultiIssuerAuthorizeResult,
60};
61pub use bootstrap_config::*;
62pub use cedar_policy::PolicyId;
66use common::app_types::{self, ApplicationName};
67pub use common::policy_store::{PolicyEffect, PolicyMetadata};
68pub use http::HttpClientConfig;
69use init::ServiceFactory;
70use init::policy_store::{LoadedPolicyStore, load_policy_store};
71use init::policy_store_refresh::{
72 AuthzRebuilder, PolicyStoreRefreshHandle, RefreshSource, RefreshWorkerSeed, WorkerContext,
73 spawn_refresh_worker,
74};
75use init::service_config::{ServiceConfig, ServiceConfigError};
76use init::service_factory::ServiceInitError;
77use lock::InitLockServiceError;
78use lock::health_registry::HealthStatus;
79use log::interface::LogWriter;
80use log::{BaseLogEntry, LogEntry};
81pub use log::{LogLevel, LogStorage};
82
83use semver::Version;
84
85const BUILD_COMMIT: Option<&str> = option_env!("CEDARLING_BUILD_COMMIT");
88const BUILD_TIMESTAMP: Option<&str> = option_env!("CEDARLING_BUILD_TIMESTAMP");
91
92#[doc(hidden)]
93pub mod bindings {
94 pub use cedar_policy;
95
96 pub use super::log::{
97 AuthorizationLogInfo, Decision, Diagnostics, LogEntry, PolicyEvaluationError,
98 };
99 pub use crate::http::spawn_task;
100 pub use crate::sparkv;
101 pub use serde_json;
102 pub use serde_yaml_ng;
103}
104
105#[derive(Debug, thiserror::Error)]
107pub enum InitCedarlingError {
108 #[error(transparent)]
110 ServiceConfig(#[from] ServiceConfigError),
111 #[error(transparent)]
113 ServiceInit(#[from] ServiceInitError),
114 #[error(transparent)]
116 BootstrapConfigLoading(#[from] BootstrapConfigLoadingError),
117 #[error(transparent)]
119 DataStoreInit(#[from] ConfigValidationError),
120 #[cfg(feature = "blocking")]
121 #[error(transparent)]
123 RuntimeInit(std::io::Error),
124 #[error("failed to initialize the Lock Service: {0}")]
127 InitLockService(#[from] InitLockServiceError),
128}
129
130#[derive(Clone)]
133pub struct Cedarling {
134 log: log::Logger,
135 authz: Arc<arc_swap::ArcSwap<Authz>>,
141 data: Arc<DataStore>,
142 _refresh_handle: Option<Arc<PolicyStoreRefreshHandle>>,
147}
148
149impl Cedarling {
150 #[cfg(not(target_arch = "wasm32"))]
154 pub async fn new_with_env(
155 raw_config: Option<BootstrapConfigRaw>,
156 ) -> Result<Cedarling, InitCedarlingError> {
157 let config = BootstrapConfig::from_raw_config_and_env(raw_config)?;
158 Self::new(&config).await
159 }
160
161 pub async fn new(config: &BootstrapConfig) -> Result<Cedarling, InitCedarlingError> {
163 let pdp_id = app_types::PdpID::new();
164 let app_name = (!config.application_name.is_empty())
165 .then(|| ApplicationName::from(config.application_name.clone()));
166
167 let metrics = Arc::new(
168 if config
169 .lock_config
170 .as_ref()
171 .is_some_and(|c| c.telemetry_interval.is_some())
172 {
173 MetricsCollector::new(0)
174 } else {
175 MetricsCollector::disabled()
176 },
177 );
178
179 let log = crate::log::init_logger(
180 &config.log_config,
181 pdp_id,
182 app_name,
183 config.lock_config.as_ref(),
184 metrics.clone(),
185 config.http_client_config,
186 )
187 .await?;
188
189 log.log_any(
190 LogEntry::new(BaseLogEntry::new_system_opt_request_id(
191 LogLevel::INFO,
192 None,
193 ))
194 .set_message("Cedarling initialization started".to_string())
195 .set_build_info(BUILD_COMMIT, BUILD_TIMESTAMP),
196 );
197
198 let (service_config, refresh_seed) = perform_bootstrap_load(config, &log).await?;
203
204 let policy_count = service_config
205 .policy_store
206 .policies
207 .get_set()
208 .num_of_policies();
209 metrics.set_policy_count(policy_count);
210
211 let data = Arc::new(DataStore::new(
213 config.data_store_config.clone(),
214 metrics.clone(),
215 )?);
216
217 let mut service_factory = ServiceFactory::new(
218 config,
219 service_config,
220 log.clone(),
221 data.clone(),
222 metrics.clone(),
223 );
224
225 if let Some(metadata) = service_factory.policy_store_metadata() {
227 log_policy_store_metadata(&log, metadata);
228 }
229
230 if let Some(registry) = log.health_registry() {
231 registry.register("core", || HealthStatus::Success);
232 registry.register("policy_load", move || {
233 if policy_count > 0 {
234 HealthStatus::Success
235 } else {
236 HealthStatus::Failure
237 }
238 });
239 }
240
241 let authz = service_factory.authz_service().await?;
242 let authz_swap = Arc::new(arc_swap::ArcSwap::from(authz));
243
244 let refresh_handle = maybe_spawn_refresh_worker(
245 config,
246 &service_factory,
247 authz_swap.clone(),
248 log.clone(),
249 data.clone(),
250 metrics.clone(),
251 refresh_seed,
252 );
253
254 Ok(Cedarling {
255 log,
256 authz: authz_swap,
257 data,
258 _refresh_handle: refresh_handle,
259 })
260 }
261
262 #[allow(clippy::unused_async)]
270 pub async fn authorize_unsigned(
271 &self,
272 request: RequestUnsigned,
273 ) -> Result<AuthorizeResult, AuthorizeError> {
274 self.authz.load().authorize_unsigned(&request)
275 }
276
277 #[allow(clippy::unused_async)]
288 pub async fn authorize_unsigned_batch(
289 &self,
290 request: BatchAuthorizeUnsignedRequest,
291 ) -> Result<BatchAuthorizeResponse<Result<AuthorizeResult, BatchItemError>>, AuthorizeError>
292 {
293 self.authz.load().authorize_unsigned_batch(&request)
294 }
295
296 #[allow(clippy::unused_async)]
299 pub async fn authorize_multi_issuer(
300 &self,
301 request: AuthorizeMultiIssuerRequest,
302 ) -> Result<MultiIssuerAuthorizeResult, AuthorizeError> {
303 self.authz.load().authorize_multi_issuer(&request)
304 }
305
306 #[allow(clippy::unused_async)]
316 pub async fn authorize_multi_issuer_batch(
317 &self,
318 request: BatchAuthorizeMultiIssuerRequest,
319 ) -> Result<
320 BatchAuthorizeResponse<Result<MultiIssuerAuthorizeResult, BatchItemError>>,
321 AuthorizeError,
322 > {
323 self.authz.load().authorize_multi_issuer_batch(&request)
324 }
325
326 pub fn get_matching_policies_unsigned(
332 &self,
333 principal: Option<&EntityData>,
334 actions: &[String],
335 resources: &[EntityData],
336 ) -> Result<Vec<PolicyMetadata>, AuthorizeError> {
337 self.authz
338 .load()
339 .get_matching_policies_unsigned(principal, actions, resources)
340 }
341
342 pub fn get_matching_policies_multi_issuer(
347 &self,
348 tokens: &[TokenInput],
349 actions: &[String],
350 resources: &[EntityData],
351 ) -> Result<Vec<PolicyMetadata>, AuthorizeError> {
352 self.authz
353 .load()
354 .get_matching_policies_multi_issuer(tokens, actions, resources)
355 }
356
357 pub fn annotations_map<'a>(
370 &self,
371 ids: impl IntoIterator<Item = &'a PolicyId>,
372 ) -> HashMap<String, String> {
373 self.authz.load().annotations_map(ids)
374 }
375
376 pub fn annotation_values<'a>(
386 &self,
387 ids: impl IntoIterator<Item = &'a PolicyId>,
388 key: &str,
389 ) -> Vec<String> {
390 self.authz.load().annotation_values(ids, key)
391 }
392
393 pub fn annotations_by_policy<'a>(
403 &self,
404 ids: impl IntoIterator<Item = &'a PolicyId>,
405 ) -> HashMap<String, HashMap<String, String>> {
406 self.authz.load().annotations_by_policy(ids)
407 }
408
409 pub async fn shut_down(&self) {
411 self.log.shut_down().await;
412 }
413}
414
415impl TrustedIssuerLoadingInfo for Cedarling {
416 fn is_trusted_issuer_loaded_by_name(&self, issuer_id: &str) -> bool {
417 self.authz
418 .load()
419 .is_trusted_issuer_loaded_by_name(issuer_id)
420 }
421
422 fn is_trusted_issuer_loaded_by_iss(&self, iss_claim: &str) -> bool {
423 self.authz.load().is_trusted_issuer_loaded_by_iss(iss_claim)
424 }
425
426 fn total_issuers(&self) -> usize {
427 self.authz.load().total_issuers()
428 }
429
430 fn loaded_trusted_issuers_count(&self) -> usize {
431 self.authz.load().loaded_trusted_issuers_count()
432 }
433
434 fn loaded_trusted_issuer_ids(&self) -> HashSet<String> {
435 self.authz.load().loaded_trusted_issuer_ids()
436 }
437
438 fn failed_trusted_issuer_ids(&self) -> HashSet<String> {
439 self.authz.load().failed_trusted_issuer_ids()
440 }
441}
442
443async fn perform_bootstrap_load(
450 config: &BootstrapConfig,
451 log: &log::Logger,
452) -> Result<(ServiceConfig, RefreshWorkerSeed), ServiceConfigError> {
453 let raw_load: Result<(http::HttpClient, LoadedPolicyStore), ServiceConfigError> = async {
454 let http_client = http::HttpClient::new(config.http_client_config)?;
455 let loaded = load_policy_store(
456 &config.policy_store_config,
457 &http_client,
458 config.authorization_config.strict_schema_validation,
459 )
460 .await?;
461 Ok((http_client, loaded))
462 }
463 .await;
464
465 let (http_client, loaded) = raw_load
466 .inspect(|_| {
467 log.log_any(
468 LogEntry::new(BaseLogEntry::new_system_opt_request_id(
469 LogLevel::DEBUG,
470 None,
471 ))
472 .set_message("configuration parsed successfully".to_string()),
473 );
474 })
475 .inspect_err(|err| {
476 log.log_any(
477 LogEntry::new(BaseLogEntry::new_system_opt_request_id(
478 LogLevel::ERROR,
479 None,
480 ))
481 .set_error(err.to_string())
482 .set_message("configuration parsed with error".to_string()),
483 );
484 })?;
485
486 let LoadedPolicyStore {
487 store: policy_store,
488 body_hash,
489 validators,
490 } = loaded;
491 Ok((
492 ServiceConfig {
493 policy_store,
494 http_client,
495 },
496 RefreshWorkerSeed {
497 initial_body_hash: body_hash,
498 initial_validators: validators,
499 },
500 ))
501}
502
503fn maybe_spawn_refresh_worker(
510 config: &BootstrapConfig,
511 service_factory: &ServiceFactory<'_>,
512 authz_swap: Arc<arc_swap::ArcSwap<authz::Authz>>,
513 log: log::Logger,
514 data: Arc<context_data_api::DataStore>,
515 metrics: Arc<authz::metrics::MetricsCollector>,
516 seed: RefreshWorkerSeed,
517) -> Option<Arc<PolicyStoreRefreshHandle>> {
518 if !config.policy_store_config.refresh_enabled() {
519 return None;
520 }
521 let source = RefreshSource::from_policy_store_source(&config.policy_store_config.source)?;
522 let (interval_secs, clamped) = config.policy_store_config.effective_refresh_interval();
523 if clamped {
524 log.log_any(
525 LogEntry::new(BaseLogEntry::new_system_opt_request_id(LogLevel::WARN, None))
526 .set_message(format!(
527 "CEDARLING_POLICY_STORE_REFRESH_INTERVAL={} is below the minimum; clamped to {} seconds",
528 config.policy_store_config.refresh_interval_secs,
529 interval_secs,
530 )),
531 );
532 }
533 let rebuilder = AuthzRebuilder {
534 jwt_config: config.jwt_config.clone(),
535 authorization_config: config.authorization_config.clone(),
536 http_client: service_factory.http_client_for_refresh(),
537 log: log.clone(),
538 data_store: data,
539 metrics: metrics.clone(),
540 };
541 let ctx = WorkerContext {
542 source,
543 interval_secs,
544 http_client: service_factory.http_client_for_refresh(),
545 rebuilder,
546 authz_swap,
547 metrics,
548 log,
549 initial_body_hash: seed.initial_body_hash,
550 initial_validators: seed.initial_validators,
551 strict_schema_validation: config.authorization_config.strict_schema_validation,
552 };
553 Some(Arc::new(spawn_refresh_worker(ctx)))
554}
555
556fn log_policy_store_metadata(
560 log: &log::Logger,
561 metadata: &crate::common::policy_store::PolicyStoreMetadata,
562) {
563 let mut details = format!(
565 "Policy store '{}' (ID: {}) v{} loaded",
566 metadata.name(),
567 if metadata.id().is_empty() {
568 "<auto>"
569 } else {
570 metadata.id()
571 },
572 metadata.version()
573 );
574
575 if let Some(desc) = metadata.description() {
577 let _ = write!(details, " - {desc}");
578 }
579
580 let _ = write!(details, " [Cedar {}]", metadata.cedar_version());
582
583 if let Some(created) = metadata.created_date() {
585 let _ = write!(details, " (created: {})", created.format("%Y-%m-%d"));
586 }
587 if let Some(updated) = metadata.updated_date() {
588 let _ = write!(details, " (updated: {})", updated.format("%Y-%m-%d"));
589 }
590
591 log.log_any(
592 LogEntry::new(BaseLogEntry::new_system_opt_request_id(
593 LogLevel::DEBUG,
594 None,
595 ))
596 .set_message(details),
597 );
598
599 let current_cedar_version: Version = cedar_policy::get_lang_version();
601 match metadata.is_compatible_with_cedar(¤t_cedar_version) {
602 Ok(true) => {
603 log.log_any(
604 LogEntry::new(BaseLogEntry::new_system_opt_request_id(
605 LogLevel::DEBUG,
606 None,
607 ))
608 .set_message(format!(
609 "Policy store Cedar version {} is compatible with runtime version {}",
610 metadata.cedar_version(),
611 current_cedar_version
612 )),
613 );
614 },
615 Ok(false) => {
616 log.log_any(
617 LogEntry::new(BaseLogEntry::new_system_opt_request_id(
618 LogLevel::WARN,
619 None,
620 ))
621 .set_message(format!(
622 "Policy store Cedar version {} may not be compatible with runtime version {}",
623 metadata.cedar_version(),
624 current_cedar_version
625 )),
626 );
627 },
628 Err(e) => {
629 log.log_any(
630 LogEntry::new(BaseLogEntry::new_system_opt_request_id(
631 LogLevel::WARN,
632 None,
633 ))
634 .set_message(format!("Could not check Cedar version compatibility: {e}")),
635 );
636 },
637 }
638
639 if let Some(parsed_version) = metadata.version_parsed() {
641 log.log_any(
642 LogEntry::new(BaseLogEntry::new_system_opt_request_id(
643 LogLevel::TRACE,
644 None,
645 ))
646 .set_message(format!(
647 "Policy store semantic version: {}.{}.{}",
648 parsed_version.major, parsed_version.minor, parsed_version.patch
649 )),
650 );
651 }
652}
653
654impl LogStorage for Cedarling {
657 fn pop_logs(&self) -> Vec<serde_json::Value> {
658 self.log.pop_logs()
659 }
660
661 fn get_log_by_id(&self, id: &str) -> Option<serde_json::Value> {
662 self.log.get_log_by_id(id)
663 }
664
665 fn get_log_ids(&self) -> Vec<String> {
666 self.log.get_log_ids()
667 }
668
669 fn get_logs_by_tag(&self, tag: &str) -> Vec<serde_json::Value> {
670 self.log.get_logs_by_tag(tag)
671 }
672
673 fn get_logs_by_request_id(&self, request_id: &str) -> Vec<serde_json::Value> {
674 self.log.get_logs_by_request_id(request_id)
675 }
676
677 fn get_logs_by_request_id_and_tag(&self, id: &str, tag: &str) -> Vec<serde_json::Value> {
678 self.log.get_logs_by_request_id_and_tag(id, tag)
679 }
680}
681
682fn calculate_capacity_usage(
685 entry_count: usize,
686 max_entries: usize,
687 memory_alert_threshold: f64,
688) -> (f64, bool) {
689 #[allow(clippy::cast_precision_loss)]
691 let capacity_usage_percent = if max_entries > 0 {
692 (entry_count as f64 / max_entries as f64) * 100.0
693 } else {
694 0.0 };
696 let memory_alert_triggered = capacity_usage_percent >= memory_alert_threshold;
697 (capacity_usage_percent, memory_alert_triggered)
698}
699
700impl DataApi for Cedarling {
702 fn push_data_ctx(
703 &self,
704 key: &str,
705 value: serde_json::Value,
706 ttl: Option<std::time::Duration>,
707 ) -> Result<(), DataError> {
708 self.data.push(key, value, ttl)?;
709
710 let config = self.data.config();
712 if config.max_entries > 0 {
713 let entry_count = self.data.count();
714 let (capacity_usage_percent, memory_alert_triggered) = calculate_capacity_usage(
715 entry_count,
716 config.max_entries,
717 config.memory_alert_threshold,
718 );
719 if memory_alert_triggered {
720 let log_entry = LogEntry::new(BaseLogEntry::new_system_opt_request_id(
721 LogLevel::WARN,
722 None,
723 ))
724 .set_message(format!(
725 "DataStore memory usage alert: {:.1}% capacity used ({}/{} entries), threshold: {:.1}%",
726 capacity_usage_percent,
727 entry_count,
728 config.max_entries,
729 config.memory_alert_threshold
730 ));
731 self.log.log_any(log_entry);
732 }
733 }
734
735 Ok(())
736 }
737
738 fn get_data_ctx(&self, key: &str) -> Result<Option<serde_json::Value>, DataError> {
739 Ok(self.data.get(key))
740 }
741
742 fn get_data_entry_ctx(&self, key: &str) -> Result<Option<DataEntry>, DataError> {
743 Ok(self.data.get_entry(key))
744 }
745
746 fn remove_data_ctx(&self, key: &str) -> Result<bool, DataError> {
747 Ok(self.data.remove(key))
748 }
749
750 fn clear_data_ctx(&self) -> Result<(), DataError> {
751 self.data.clear();
752 Ok(())
753 }
754
755 fn list_data_ctx(&self) -> Result<Vec<DataEntry>, DataError> {
756 Ok(self.data.list_entries())
757 }
758
759 fn get_stats_ctx(&self) -> Result<DataStoreStats, DataError> {
760 let config = self.data.config();
761 let entry_count = self.data.count();
762 let total_size_bytes = self.data.total_size();
763 let avg_entry_size_bytes = total_size_bytes.checked_div(entry_count).unwrap_or(0);
764
765 let (capacity_usage_percent, memory_alert_triggered) = calculate_capacity_usage(
767 entry_count,
768 config.max_entries,
769 config.memory_alert_threshold,
770 );
771
772 Ok(DataStoreStats {
773 entry_count,
774 max_entries: config.max_entries,
775 max_entry_size: config.max_entry_size,
776 metrics_enabled: config.enable_metrics,
777 total_size_bytes,
778 avg_entry_size_bytes,
779 capacity_usage_percent,
780 memory_alert_threshold: config.memory_alert_threshold,
781 memory_alert_triggered,
782 })
783 }
784}