1use cedar_policy::{
2 Authorizer, Entities, Entity, Policy, PolicyId, PolicySet, Request as CedarRequest, Schema,
3};
4use std::collections::{HashMap, HashSet};
5use std::marker::PhantomData;
6use std::sync::{Arc, OnceLock};
7use std::time::{Duration, Instant, SystemTime};
8use std::vec;
9
10use crate::labels::LabelRegistry;
11use crate::policy_match::{
12 action_match_reason, matches_effect, principal_match_reason, resource_match_reason,
13};
14use crate::policy_store::{
15 ExplicitPolicyStore, PolicyStoreId, PolicyStoreLayout, display_policy_id,
16};
17use crate::query::{ActionQuery, PrincipalQuery, ResourceQuery};
18use crate::timers::PhaseTimer;
19use crate::traits::CedarAtom;
20use crate::types::{
21 Decision, DecisionDiagnostics, PermitPolicies, PermitPolicy, PolicyCandidates,
22 PolicyEffectFilter, PolicyMatchReason, PolicyVersion, Request, RequestContext, Resource,
23};
24use crate::{Groups, Principal};
25use crate::{error::PolicyError, loader};
26use arc_swap::ArcSwap;
27
28use sha2::{Digest, Sha256};
29use tracing::debug;
30#[cfg(feature = "observability")]
31use tracing::info_span;
32
33#[cfg(feature = "observability")]
34use crate::metrics::{
35 EvaluationObservation, EvaluationPhases, MatchedPolicySource, get_sink, metrics_enabled,
36 record_evaluation_observation, record_reload,
37};
38
39fn get_authorizer() -> &'static Authorizer {
41 static AUTHORIZER: OnceLock<Authorizer> = OnceLock::new();
42 AUTHORIZER.get_or_init(Authorizer::new)
43}
44
45#[derive(Debug)]
47struct EvalTimers {
48 total_start: Option<Instant>,
50 measure_enabled: bool,
52 debug_enabled: bool,
54 labels: Duration,
56 construct_req: Duration,
58 entities: Duration,
60 groups: Duration,
62 authz: Duration,
64}
65
66impl EvalTimers {
67 fn start(measure_enabled: bool, debug_enabled: bool) -> Self {
68 Self {
69 total_start: measure_enabled.then(Instant::now),
70 measure_enabled,
71 debug_enabled,
72 labels: Duration::ZERO,
73 construct_req: Duration::ZERO,
74 entities: Duration::ZERO,
75 groups: Duration::ZERO,
76 authz: Duration::ZERO,
77 }
78 }
79
80 fn total_elapsed(&self) -> Duration {
81 self.total_start
82 .map_or(Duration::ZERO, |start| start.elapsed())
83 }
84}
85
86struct PreparedRequest {
88 cedar_req: CedarRequest,
89 entities: Entities,
90 timers: EvalTimers,
91 #[cfg(feature = "observability")]
92 sink: crate::metrics::SinkGuard,
93 #[cfg(feature = "observability")]
94 metrics_enabled: bool,
95}
96
97#[derive(Debug)]
99enum PolicySets {
100 Monolithic(Box<PolicySet>),
101 Scoped {
102 layout: PolicyStoreLayout,
103 stores: Vec<PolicySet>,
104 },
105}
106
107enum PolicySetIter<'a> {
108 Monolithic(std::iter::Once<&'a PolicySet>),
109 Scoped(std::slice::Iter<'a, PolicySet>),
110}
111
112impl<'a> Iterator for PolicySetIter<'a> {
113 type Item = &'a PolicySet;
114
115 fn next(&mut self) -> Option<Self::Item> {
116 match self {
117 Self::Monolithic(iter) => iter.next(),
118 Self::Scoped(iter) => iter.next(),
119 }
120 }
121}
122
123impl PolicySets {
124 fn layout(&self) -> Option<&PolicyStoreLayout> {
125 match self {
126 Self::Monolithic(_) => None,
127 Self::Scoped { layout, .. } => Some(layout),
128 }
129 }
130
131 fn resolve(&self, request: &Request) -> Result<&PolicySet, PolicyError> {
132 match self {
133 Self::Monolithic(set) => Ok(set),
134 Self::Scoped { layout, stores } => {
135 let index = layout.resolve_request(&request.action, &request.resource)?;
136 stores.get(index).ok_or_else(|| {
137 PolicyError::PolicyStoreRoutingError(format!(
138 "configured policy-store index {index} is outside layout length {}",
139 layout.stores().len()
140 ))
141 })
142 }
143 }
144 }
145
146 fn iter(&self) -> PolicySetIter<'_> {
147 match self {
148 Self::Monolithic(set) => PolicySetIter::Monolithic(std::iter::once(set)),
149 Self::Scoped { stores, .. } => PolicySetIter::Scoped(stores.iter()),
150 }
151 }
152
153 fn store_ids(&self) -> Option<Vec<PolicyStoreId>> {
154 let Self::Scoped { layout, .. } = self else {
155 return None;
156 };
157 Some(
158 layout
159 .stores()
160 .iter()
161 .map(|store| store.id().clone())
162 .collect(),
163 )
164 }
165}
166
167#[derive(Debug)]
169struct PolicySnapshot {
170 sets: PolicySets,
171 revision: PolicyRevision,
172 permit_policies: HashMap<PolicyId, PermitPolicy>,
173 forbid_policy_ids: HashMap<PolicyId, String>,
174 schema: Option<Arc<Schema>>,
175}
176
177#[derive(Debug)]
179struct PolicyRevision {
180 hash: Arc<str>,
181 loaded_at: Arc<str>,
182}
183
184type Snapshot = Arc<PolicySnapshot>;
186
187struct EngineState {
189 policy: Snapshot,
190 label_registry: Option<LabelRegistry>,
191 generation: u64,
192}
193
194type State = Arc<EngineState>;
195
196impl EngineState {
197 fn version(&self) -> PolicyVersion {
198 PolicyVersion {
199 hash: Arc::clone(&self.policy.revision.hash),
200 loaded_at: Arc::clone(&self.policy.revision.loaded_at),
201 label_set: self
202 .label_registry
203 .as_ref()
204 .and_then(|registry| registry.version().cloned()),
205 generation: self.generation,
206 }
207 }
208}
209
210impl PolicySnapshot {
211 fn from_policy_text(policy_text: &str) -> Result<Self, PolicyError> {
212 Self::from_policy_text_with_schema_and_stores(policy_text, None, None)
213 }
214
215 fn from_policy_text_with_schema(
216 policy_text: &str,
217 schema: Option<Arc<Schema>>,
218 ) -> Result<Self, PolicyError> {
219 Self::from_policy_text_with_schema_and_stores(policy_text, schema, None)
220 }
221
222 fn from_policy_text_with_schema_and_stores(
223 policy_text: &str,
224 schema: Option<Arc<Schema>>,
225 layout: Option<PolicyStoreLayout>,
226 ) -> Result<Self, PolicyError> {
227 let set = match schema.as_deref() {
228 Some(schema) => loader::compile_policy_with_schema(policy_text, schema)?,
229 None => loader::compile_policy(policy_text)?,
230 };
231 let permit_policies = loader::precompute_permit_policies(&set)?;
232 let forbid_policy_ids = loader::precompute_forbid_policy_ids(&set);
233 let sets = match layout {
234 Some(layout) => partition_policy_set(&set, layout)?,
235 None => PolicySets::Monolithic(Box::new(set)),
236 };
237
238 let mut hasher = Sha256::new();
239 hasher.update(policy_text.as_bytes());
240 let digest = hasher.finalize();
241 let mut hash = String::with_capacity(digest.len() * 2);
242 const HEX: &[u8; 16] = b"0123456789abcdef";
243 for byte in digest {
244 hash.push(char::from(HEX[usize::from(byte >> 4)]));
245 hash.push(char::from(HEX[usize::from(byte & 0x0f)]));
246 }
247
248 Ok(PolicySnapshot {
249 sets,
250 revision: PolicyRevision {
251 hash: hash.into(),
252 loaded_at: humantime::format_rfc3339(SystemTime::now())
253 .to_string()
254 .into(),
255 },
256 permit_policies,
257 forbid_policy_ids,
258 schema,
259 })
260 }
261
262 fn schema(&self) -> Option<&Schema> {
263 self.schema.as_deref()
264 }
265}
266
267fn partition_policy_set(
268 source: &PolicySet,
269 layout: PolicyStoreLayout,
270) -> Result<PolicySets, PolicyError> {
271 let mut stores = (0..layout.stores().len())
272 .map(|_| PolicySet::new())
273 .collect::<Vec<_>>();
274 let mut found_global_policy_ids = HashSet::new();
275
276 for policy in source.policies() {
277 let display_id = display_policy_id(policy);
278 let registered_global = policy
279 .annotation("id")
280 .is_some_and(|id| layout.global_policy_ids().contains(id));
281 if registered_global {
282 found_global_policy_ids.insert(display_id.to_string());
283 }
284
285 let explicit = layout.explicit_policy_store(policy)?;
286 if registered_global
287 && let Some(ExplicitPolicyStore::Store(store_index)) = explicit.as_ref()
288 {
289 return Err(PolicyError::PolicyStoreConfigError(format!(
290 "policy '{display_id}' is registered as global but @{POLICY_STORE_ANNOTATION} assigns it to store '{}'",
291 layout.stores()[*store_index].id(),
292 POLICY_STORE_ANNOTATION = crate::POLICY_STORE_ANNOTATION
293 )));
294 }
295 let target_indexes = if registered_global
296 || matches!(explicit, Some(ExplicitPolicyStore::Global))
297 {
298 (0..layout.stores().len()).collect::<Vec<_>>()
299 } else {
300 let candidates = layout.policy_candidates(policy)?;
301 match explicit {
302 Some(ExplicitPolicyStore::Store(store_index)) => {
303 if candidates
304 .first()
305 .is_some_and(|candidate| *candidate != store_index)
306 {
307 return Err(PolicyError::PolicyStoreConfigError(format!(
308 "policy '{display_id}' is assigned to store '{}' but its scope identifies store '{}'",
309 layout.stores()[store_index].id(),
310 layout.stores()[candidates[0]].id()
311 )));
312 }
313 vec![store_index]
314 }
315 None => match candidates.as_slice() {
316 [store_index] => vec![*store_index],
317 [] => {
318 return Err(PolicyError::PolicyStoreConfigError(format!(
319 "policy '{display_id}' cannot be assigned from its configured namespace references; add @{POLICY_STORE_ANNOTATION}(\"store-id\") or mark it global",
320 POLICY_STORE_ANNOTATION = crate::POLICY_STORE_ANNOTATION
321 )));
322 }
323 _ => {
324 return Err(PolicyError::PolicyStoreConfigError(format!(
325 "policy '{display_id}' has an ambiguous policy-store assignment"
326 )));
327 }
328 },
329 Some(ExplicitPolicyStore::Global) => {
330 return Err(PolicyError::PolicyStoreConfigError(format!(
331 "policy '{display_id}' has an inconsistent global assignment"
332 )));
333 }
334 }
335 };
336
337 for target_index in target_indexes {
338 let target_id = layout.stores()[target_index].id();
339 let target = stores.get_mut(target_index).ok_or_else(|| {
340 PolicyError::PolicyStoreConfigError(format!(
341 "policy '{display_id}' resolved to missing store '{target_id}'"
342 ))
343 })?;
344 target.add(policy.clone()).map_err(|error| {
345 PolicyError::PolicyStoreConfigError(format!(
346 "failed to add policy '{display_id}' to store '{target_id}': {error}"
347 ))
348 })?;
349 }
350 }
351
352 let mut missing_global_policy_ids = layout
353 .global_policy_ids()
354 .difference(&found_global_policy_ids)
355 .cloned()
356 .collect::<Vec<_>>();
357 if !missing_global_policy_ids.is_empty() {
358 missing_global_policy_ids.sort();
359 return Err(PolicyError::PolicyStoreConfigError(format!(
360 "global policy IDs were not found in the policy source: {}",
361 missing_global_policy_ids.join(", ")
362 )));
363 }
364
365 Ok(PolicySets::Scoped { layout, stores })
366}
367
368#[inline]
370fn extract_permit_policies(
371 snapshot: &PolicySnapshot,
372 result: &cedar_policy::Response,
373) -> PermitPolicies {
374 if result.decision() != cedar_policy::Decision::Allow {
375 return PermitPolicies::empty();
376 }
377
378 result
379 .diagnostics()
380 .reason()
381 .filter_map(|reason| snapshot.permit_policies.get(reason))
382 .cloned()
383 .collect()
384}
385
386#[inline]
388fn extract_forbid_policy_ids(
389 snapshot: &PolicySnapshot,
390 result: &cedar_policy::Response,
391) -> Vec<String> {
392 if result.decision() != cedar_policy::Decision::Deny {
393 return Vec::new();
394 }
395
396 let mut ids: Vec<String> = result
397 .diagnostics()
398 .reason()
399 .filter_map(|reason| snapshot.forbid_policy_ids.get(reason).cloned())
400 .collect();
401 ids.sort();
402 ids.dedup();
403 ids
404}
405
406#[inline]
408fn request_groups(request: &Request) -> Option<&Groups> {
409 match &request.principal {
410 Principal::User(user) => Some(user.groups()),
411 Principal::Group(_) => None,
412 }
413}
414
415#[inline]
417fn apply_labels(
418 registry: &LabelRegistry,
419 resource: &crate::types::Resource,
420 timers: &mut EvalTimers,
421) -> Option<crate::types::Resource> {
422 let measure_enabled = timers.measure_enabled;
423 let _timer = PhaseTimer::new_if(&mut timers.labels, measure_enabled);
424 #[cfg(feature = "observability")]
425 let _label_span = info_span!("apply_labels").entered();
426 registry.apply_to_clone_if_applicable(resource)
427}
428
429#[inline]
431fn build_effective_context(
432 request_context: Option<&RequestContext>,
433) -> Result<cedar_policy::Context, PolicyError> {
434 match request_context {
435 Some(context) if !context.is_empty() => context.to_cedar_context(),
436 _ => Ok(cedar_policy::Context::empty()),
437 }
438}
439
440#[inline]
443fn build_cedar_req(
444 principal_uid: cedar_policy::EntityUid,
445 action_uid: cedar_policy::EntityUid,
446 resource_uid: cedar_policy::EntityUid,
447 context: cedar_policy::Context,
448 schema: Option<&Schema>,
449 timers: &mut EvalTimers,
450) -> Result<CedarRequest, PolicyError> {
451 let measure_enabled = timers.measure_enabled;
452 let _timer = PhaseTimer::new_if(&mut timers.construct_req, measure_enabled);
453 #[cfg(feature = "observability")]
454 let _req_span = info_span!("construct_cedar_req").entered();
455
456 Ok(CedarRequest::new(
457 principal_uid,
458 action_uid,
459 resource_uid,
460 context,
461 schema,
462 )?)
463}
464
465#[inline]
468fn build_entities(
469 principal_uid: cedar_policy::EntityUid,
470 resource_uid: cedar_policy::EntityUid,
471 resource: &crate::types::Resource,
472 groups: Option<&Groups>,
473 schema: Option<&Schema>,
474 timers: &mut EvalTimers,
475) -> Result<Entities, PolicyError> {
476 let group_uids = {
477 let measure_enabled = timers.measure_enabled;
478 let _timer = PhaseTimer::new_if(&mut timers.groups, measure_enabled);
479 #[cfg(feature = "observability")]
480 let _groups_span = info_span!("resolve_groups").entered();
481
482 let mut group_uids = HashSet::with_capacity(groups.map_or(0, Groups::len));
483 if let Some(groups) = groups {
484 for group in groups {
485 group_uids.insert(group.cedar_entity_uid().clone());
486 }
487 }
488 group_uids
489 };
490
491 let entities = {
494 let measure_enabled = timers.measure_enabled;
495 let _timer = PhaseTimer::new_if(&mut timers.entities, measure_enabled);
496 #[cfg(feature = "observability")]
497 let _entity_span = info_span!("construct_entities").entered();
498
499 let resource_attrs = resource.cedar_attr();
501 let resource_entity =
502 cedar_policy::Entity::new(resource_uid, resource_attrs, Default::default())?;
503
504 let mut all_entities = Vec::with_capacity(group_uids.len() + 2);
507 all_entities.extend(group_uids.iter().cloned().map(Entity::with_uid));
508
509 let principal_entity = Entity::new(principal_uid, HashMap::new(), group_uids)?;
512 all_entities.push(principal_entity);
513 all_entities.push(resource_entity);
514
515 Entities::empty().add_entities(all_entities, schema)?
517 };
518
519 if timers.debug_enabled {
520 debug!(
521 event = "Request",
522 phase = "Entities",
523 time = timers.entities.as_micros(),
524 entity_count = entities.iter().count()
525 );
526 }
527
528 Ok(entities)
529}
530
531mod validation_mode_private {
540 pub trait Sealed {}
541}
542
543pub trait ValidationMode: validation_mode_private::Sealed {}
545
546#[derive(Debug, Clone, Copy)]
558pub struct SchemaFree;
559
560#[derive(Debug, Clone, Copy)]
562pub struct SchemaEnforcing;
563
564impl validation_mode_private::Sealed for SchemaFree {}
565impl validation_mode_private::Sealed for SchemaEnforcing {}
566impl ValidationMode for SchemaFree {}
567impl ValidationMode for SchemaEnforcing {}
568
569#[derive(Clone)]
570pub struct PolicyEngine<M: ValidationMode = SchemaFree> {
571 inner: Arc<ArcSwap<EngineState>>,
573 mode: PhantomData<fn() -> M>,
574}
575
576#[derive(Clone)]
582pub struct EvaluationSession<M: ValidationMode = SchemaFree> {
583 state: State,
584 mode: PhantomData<fn() -> M>,
585}
586
587impl<M: ValidationMode> From<PolicyEngine<M>> for PolicyVersion {
588 fn from(engine: PolicyEngine<M>) -> Self {
589 engine.current_version()
590 }
591}
592
593impl<M: ValidationMode> From<&PolicyEngine<M>> for PolicyVersion {
594 fn from(engine: &PolicyEngine<M>) -> Self {
595 engine.current_version()
596 }
597}
598
599impl<M: ValidationMode> PolicyEngine<M> {
600 fn from_snapshot(snapshot: PolicySnapshot) -> Self {
601 let state = EngineState {
602 policy: Arc::new(snapshot),
603 label_registry: None,
604 generation: 1,
605 };
606 Self {
607 inner: Arc::new(ArcSwap::from(Arc::new(state))),
608 mode: PhantomData,
609 }
610 }
611}
612
613impl PolicyEngine<SchemaFree> {
614 pub fn new_from_str(policy_text: &str) -> Result<Self, PolicyError> {
615 Ok(Self::from_snapshot(PolicySnapshot::from_policy_text(
616 policy_text,
617 )?))
618 }
619
620 pub fn new_from_str_with_policy_stores(
626 policy_text: &str,
627 layout: PolicyStoreLayout,
628 ) -> Result<Self, PolicyError> {
629 Ok(Self::from_snapshot(
630 PolicySnapshot::from_policy_text_with_schema_and_stores(
631 policy_text,
632 None,
633 Some(layout),
634 )?,
635 ))
636 }
637
638 pub fn new_from_str_with_schema(
640 policy_text: &str,
641 schema: Schema,
642 ) -> Result<PolicyEngine<SchemaEnforcing>, PolicyError> {
643 Ok(PolicyEngine::<SchemaEnforcing>::from_snapshot(
644 PolicySnapshot::from_policy_text_with_schema(policy_text, Some(Arc::new(schema)))?,
645 ))
646 }
647
648 pub fn new_from_str_with_schema_and_policy_stores(
650 policy_text: &str,
651 schema: Schema,
652 layout: PolicyStoreLayout,
653 ) -> Result<PolicyEngine<SchemaEnforcing>, PolicyError> {
654 Ok(PolicyEngine::<SchemaEnforcing>::from_snapshot(
655 PolicySnapshot::from_policy_text_with_schema_and_stores(
656 policy_text,
657 Some(Arc::new(schema)),
658 Some(layout),
659 )?,
660 ))
661 }
662
663 pub fn new_from_str_with_cedarschema(
665 policy_text: &str,
666 schema_text: &str,
667 ) -> Result<PolicyEngine<SchemaEnforcing>, PolicyError> {
668 let schema: Schema = schema_text
669 .parse()
670 .map_err(|e| PolicyError::ParseError(format!("failed to parse Cedar schema: {e}")))?;
671 Self::new_from_str_with_schema(policy_text, schema)
672 }
673
674 pub fn new_from_str_with_cedarschema_and_policy_stores(
676 policy_text: &str,
677 schema_text: &str,
678 layout: PolicyStoreLayout,
679 ) -> Result<PolicyEngine<SchemaEnforcing>, PolicyError> {
680 let schema: Schema = schema_text
681 .parse()
682 .map_err(|e| PolicyError::ParseError(format!("failed to parse Cedar schema: {e}")))?;
683 Self::new_from_str_with_schema_and_policy_stores(policy_text, schema, layout)
684 }
685}
686
687impl<M: ValidationMode> PolicyEngine<M> {
688 pub fn with_label_registry(self, registry: LabelRegistry) -> Self {
692 self.set_label_registry(registry);
693 self
694 }
695
696 pub fn set_label_registry(&self, registry: LabelRegistry) {
700 self.inner.rcu(|current| {
701 Arc::new(EngineState {
702 policy: Arc::clone(¤t.policy),
703 label_registry: Some(registry.clone()),
704 generation: current.generation.saturating_add(1),
705 })
706 });
707 }
708
709 pub fn label_registry(&self) -> Option<LabelRegistry> {
711 self.current_state().label_registry.clone()
712 }
713
714 pub fn reload_from_str(&self, policy_text: &str) -> Result<(), PolicyError> {
715 let had_schema = 'compile: loop {
716 let mut expected = self.current_state();
717 let had_schema = expected.policy.schema.is_some();
718 let schema = expected.policy.schema.clone();
719 let layout = expected.policy.sets.layout().cloned();
720 let new_snapshot: Snapshot =
721 Arc::new(PolicySnapshot::from_policy_text_with_schema_and_stores(
722 policy_text,
723 schema,
724 layout,
725 )?);
726
727 loop {
728 match self.install_policy_if_current(&expected, Arc::clone(&new_snapshot)) {
729 Ok(()) => break 'compile had_schema,
730 Err(latest) if Arc::ptr_eq(&expected.policy, &latest.policy) => {
731 expected = latest;
735 }
736 Err(_) => {
737 continue 'compile;
741 }
742 }
743 }
744 };
745 debug!(
746 event = "PolicyReload",
747 schema_enabled = had_schema,
748 schema_reloaded = false
749 );
750 #[cfg(feature = "observability")]
752 record_reload();
753 Ok(())
754 }
755
756 fn current_state(&self) -> State {
758 self.inner.load_full()
759 }
760
761 fn current_snapshot(&self) -> Snapshot {
762 Arc::clone(&self.current_state().policy)
763 }
764
765 fn install_policy(&self, policy: Snapshot) {
766 self.inner.rcu(|current| {
767 Arc::new(EngineState {
768 policy: Arc::clone(&policy),
769 label_registry: current.label_registry.clone(),
770 generation: current.generation.saturating_add(1),
771 })
772 });
773 }
774
775 fn install_policy_if_current(&self, expected: &State, policy: Snapshot) -> Result<(), State> {
777 let replacement = Arc::new(EngineState {
778 policy,
779 label_registry: expected.label_registry.clone(),
780 generation: expected.generation.saturating_add(1),
781 });
782 let previous = self.inner.compare_and_swap(expected, replacement);
783 if Arc::ptr_eq(expected, &previous) {
784 Ok(())
785 } else {
786 Err(Arc::clone(&previous))
787 }
788 }
789
790 pub fn current_version(&self) -> PolicyVersion {
795 self.current_state().version()
796 }
797
798 pub fn session(&self) -> EvaluationSession<M> {
800 EvaluationSession {
801 state: self.current_state(),
802 mode: PhantomData,
803 }
804 }
805
806 pub fn policy_store_ids(&self) -> Option<Vec<PolicyStoreId>> {
808 self.current_state().policy.sets.store_ids()
809 }
810
811 fn prepare(
816 state: &EngineState,
817 request: &Request,
818 request_context: Option<&RequestContext>,
819 ) -> Result<PreparedRequest, PolicyError> {
820 let schema = state.policy.schema();
821 #[cfg(feature = "observability")]
822 let sink = get_sink();
823 #[cfg(feature = "observability")]
824 let metrics_enabled = metrics_enabled(&sink);
825 #[cfg(not(feature = "observability"))]
826 let metrics_enabled = false;
827 let debug_enabled = tracing::enabled!(tracing::Level::DEBUG);
828 let mut timers = EvalTimers::start(debug_enabled || metrics_enabled, debug_enabled);
829
830 let groups = request_groups(request);
831
832 if timers.debug_enabled {
833 debug!(
834 event = "Request",
835 phase = "Evaluation",
836 group_count = groups.map_or(0, Groups::len)
837 );
838 }
839
840 let principal_uid = request.principal.cedar_entity_uid().clone();
842 let action_uid = request.action.cedar_entity_uid().clone();
843
844 let labelled_resource = if let Some(registry) = &state.label_registry {
845 let labelled_resource = apply_labels(registry, &request.resource, &mut timers);
846 if timers.debug_enabled {
847 let resource_for_metrics = labelled_resource.as_ref().unwrap_or(&request.resource);
848 debug!(
849 event = "Request",
850 phase = "LabelsApplied",
851 time = timers.labels.as_micros(),
852 attribute_count = resource_for_metrics.attributes().len()
853 );
854 }
855 labelled_resource
856 } else {
857 if timers.debug_enabled {
858 debug!(
859 event = "Request",
860 phase = "LabelsApplied",
861 time = timers.labels.as_micros()
862 );
863 }
864 None
865 };
866 let resource_for_entities = labelled_resource.as_ref().unwrap_or(&request.resource);
867 let resource_uid = resource_for_entities.cedar_entity_uid().clone();
868 let context = build_effective_context(request_context)?;
869
870 if timers.debug_enabled {
871 debug!(
872 event = "Request",
873 phase = "Parsed",
874 group_count = groups.map_or(0, Groups::len),
875 attribute_count = resource_for_entities.attributes().len(),
876 request_context_attribute_count = request_context.map_or(0, RequestContext::len)
877 );
878 }
879
880 let principal_uid_for_entities = principal_uid.clone();
884 let resource_uid_for_entities = resource_uid.clone();
885
886 let cedar_req = build_cedar_req(
888 principal_uid,
889 action_uid,
890 resource_uid,
891 context,
892 schema,
893 &mut timers,
894 )?;
895
896 let entities = build_entities(
898 principal_uid_for_entities,
899 resource_uid_for_entities,
900 resource_for_entities,
901 groups,
902 schema,
903 &mut timers,
904 )?;
905
906 if timers.debug_enabled {
907 debug!(
908 event = "Request",
909 phase = "GroupsResolved",
910 time = timers.groups.as_micros(),
911 );
912 }
913
914 Ok(PreparedRequest {
915 cedar_req,
916 entities,
917 timers,
918 #[cfg(feature = "observability")]
919 sink,
920 #[cfg(feature = "observability")]
921 metrics_enabled,
922 })
923 }
924
925 #[cfg_attr(
976 feature = "observability",
977 tracing::instrument(name = "policy_evaluation", skip_all)
978 )]
979 pub fn evaluate(&self, request: &Request) -> Result<Decision, PolicyError> {
980 Ok(self
981 .evaluate_internal(request, None, false)?
982 .into_decision())
983 }
984
985 pub fn evaluate_with_context(
987 &self,
988 request: &Request,
989 request_context: &RequestContext,
990 ) -> Result<Decision, PolicyError> {
991 Ok(self
992 .evaluate_internal(request, Some(request_context), false)?
993 .into_decision())
994 }
995
996 pub fn evaluate_with_diagnostics(
998 &self,
999 request: &Request,
1000 ) -> Result<DecisionDiagnostics, PolicyError> {
1001 self.evaluate_internal(request, None, true)
1002 }
1003
1004 pub fn evaluate_with_context_and_diagnostics(
1006 &self,
1007 request: &Request,
1008 request_context: &RequestContext,
1009 ) -> Result<DecisionDiagnostics, PolicyError> {
1010 self.evaluate_internal(request, Some(request_context), true)
1011 }
1012
1013 fn evaluate_internal(
1014 &self,
1015 request: &Request,
1016 request_context: Option<&RequestContext>,
1017 include_forbid_diagnostics: bool,
1018 ) -> Result<DecisionDiagnostics, PolicyError> {
1019 let state = self.current_state();
1020 Self::evaluate_state(&state, request, request_context, include_forbid_diagnostics)
1021 }
1022
1023 fn evaluate_state(
1024 state: &EngineState,
1025 request: &Request,
1026 request_context: Option<&RequestContext>,
1027 include_forbid_diagnostics: bool,
1028 ) -> Result<DecisionDiagnostics, PolicyError> {
1029 let policy_set = state.policy.sets.resolve(request)?;
1030 let mut prepared = Self::prepare(state, request, request_context)?;
1032
1033 let result = {
1035 let measure_enabled = prepared.timers.measure_enabled;
1036 let _timer = PhaseTimer::new_if(&mut prepared.timers.authz, measure_enabled);
1037 #[cfg(feature = "observability")]
1038 let _authz_span = info_span!("authorize").entered();
1039 get_authorizer().is_authorized(&prepared.cedar_req, policy_set, &prepared.entities)
1040 };
1041
1042 if prepared.timers.debug_enabled {
1043 debug!(
1044 event = "Request",
1045 phase = "Authorized",
1046 time = prepared.timers.authz.as_micros(),
1047 decision = ?result.decision(),
1048 );
1049 }
1050
1051 let version = state.version();
1052 if prepared.timers.debug_enabled {
1053 debug!(
1054 event = "Request",
1055 phase = "Result",
1056 time = prepared.timers.total_elapsed().as_micros(),
1057 result = ?result.decision(),
1058 policy_hash = %version.hash,
1059 policy_loaded_at = %version.loaded_at,
1060 );
1061 }
1062
1063 let permit_policies = extract_permit_policies(&state.policy, &result);
1067 let collect_forbid_ids = include_forbid_diagnostics;
1068 let forbid_policy_ids = if collect_forbid_ids {
1069 extract_forbid_policy_ids(&state.policy, &result)
1070 } else {
1071 Vec::new()
1072 };
1073 let decision = Decision::from_cedar(result.decision(), permit_policies, version)?;
1074
1075 #[cfg(feature = "observability")]
1077 {
1078 if prepared.metrics_enabled {
1079 let dur = prepared.timers.total_elapsed();
1080 let allowed = result.decision() == cedar_policy::Decision::Allow;
1081 let phases = EvaluationPhases {
1082 apply_labels_ms: prepared.timers.labels.as_secs_f64() * 1000.0,
1083 construct_entities_ms: prepared.timers.entities.as_secs_f64() * 1000.0,
1084 resolve_groups_ms: prepared.timers.groups.as_secs_f64() * 1000.0,
1085 authorize_ms: prepared.timers.authz.as_secs_f64() * 1000.0,
1086 total_ms: dur.as_secs_f64() * 1000.0,
1087 };
1088 let matched_policies = match decision.permit_policies() {
1089 Some(policies) => MatchedPolicySource::Allow(policies),
1090 None => MatchedPolicySource::Deny {
1091 diagnostics: result.diagnostics(),
1092 policy_ids: &state.policy.forbid_policy_ids,
1093 },
1094 };
1095 let observation = EvaluationObservation::new(
1096 dur,
1097 allowed,
1098 &request.action,
1099 phases,
1100 matched_policies,
1101 );
1102
1103 record_evaluation_observation(&prepared.sink, &observation);
1104 }
1105 }
1106
1107 Ok(DecisionDiagnostics::new(decision, forbid_policy_ids))
1108 }
1109
1110 pub fn list_policies_for_user(
1159 &self,
1160 user: &str,
1161 groups: &[&str],
1162 namespace: &[&str],
1163 ) -> Result<PolicyCandidates, PolicyError> {
1164 self.list_policies_for_user_with_resource_and_effect(
1165 user,
1166 groups,
1167 namespace,
1168 None,
1169 PolicyEffectFilter::Permit,
1170 )
1171 }
1172
1173 pub fn list_policies(&self, request: &Request) -> Result<PolicyCandidates, PolicyError> {
1184 self.list_policies_with_effect(request, PolicyEffectFilter::Permit)
1185 }
1186
1187 pub fn list_policies_with_effect(
1191 &self,
1192 request: &Request,
1193 effect_filter: PolicyEffectFilter,
1194 ) -> Result<PolicyCandidates, PolicyError> {
1195 let principal = PrincipalQuery::from_principal(&request.principal);
1196 let action = ActionQuery::from_action(&request.action);
1197 self.list_policies_dispatch(
1198 &request.principal.to_string(),
1199 &principal,
1200 Some(&action),
1201 Some(&request.resource),
1202 effect_filter,
1203 )
1204 }
1205
1206 pub fn list_policies_for_user_with_resource(
1218 &self,
1219 user: &str,
1220 groups: &[&str],
1221 namespace: &[&str],
1222 resource: Option<&Resource>,
1223 ) -> Result<PolicyCandidates, PolicyError> {
1224 self.list_policies_for_user_with_resource_and_effect(
1225 user,
1226 groups,
1227 namespace,
1228 resource,
1229 PolicyEffectFilter::Permit,
1230 )
1231 }
1232
1233 pub fn list_policies_for_user_with_resource_and_effect(
1235 &self,
1236 user: &str,
1237 groups: &[&str],
1238 namespace: &[&str],
1239 resource: Option<&Resource>,
1240 effect_filter: PolicyEffectFilter,
1241 ) -> Result<PolicyCandidates, PolicyError> {
1242 let principal = PrincipalQuery::for_user(user, groups, namespace)?;
1243 self.list_policies_dispatch(user, &principal, None, resource, effect_filter)
1244 }
1245
1246 pub fn list_policies_for_group(
1255 &self,
1256 group: &str,
1257 namespace: &[&str],
1258 ) -> Result<PolicyCandidates, PolicyError> {
1259 self.list_policies_for_group_with_resource_and_effect(
1260 group,
1261 namespace,
1262 None,
1263 PolicyEffectFilter::Permit,
1264 )
1265 }
1266
1267 pub fn list_policies_for_group_with_resource(
1272 &self,
1273 group: &str,
1274 namespace: &[&str],
1275 resource: Option<&Resource>,
1276 ) -> Result<PolicyCandidates, PolicyError> {
1277 self.list_policies_for_group_with_resource_and_effect(
1278 group,
1279 namespace,
1280 resource,
1281 PolicyEffectFilter::Permit,
1282 )
1283 }
1284
1285 pub fn list_policies_for_group_with_resource_and_effect(
1287 &self,
1288 group: &str,
1289 namespace: &[&str],
1290 resource: Option<&Resource>,
1291 effect_filter: PolicyEffectFilter,
1292 ) -> Result<PolicyCandidates, PolicyError> {
1293 let principal = PrincipalQuery::for_group(group, namespace)?;
1294 self.list_policies_dispatch(group, &principal, None, resource, effect_filter)
1295 }
1296
1297 fn list_policies_dispatch(
1298 &self,
1299 principal_id: &str,
1300 principal: &PrincipalQuery,
1301 action: Option<&ActionQuery>,
1302 resource: Option<&Resource>,
1303 effect_filter: PolicyEffectFilter,
1304 ) -> Result<PolicyCandidates, PolicyError> {
1305 let snapshot = self.current_snapshot();
1306 let resource_query = resource.map(ResourceQuery::from_resource);
1307 let mut matching_policies: Vec<(Policy, Vec<PolicyMatchReason>)> = Vec::new();
1308 let mut seen_policy_ids = HashSet::new();
1309
1310 for set in snapshot.sets.iter() {
1311 for policy in set.policies() {
1312 if !seen_policy_ids.insert(policy.id().clone()) {
1313 continue;
1314 }
1315 if !matches_effect(policy.effect(), effect_filter) {
1316 continue;
1317 }
1318
1319 let Some(principal_reason) =
1320 principal_match_reason(policy.principal_constraint(), principal)
1321 else {
1322 continue;
1323 };
1324
1325 let Some(action_reason) = action_match_reason(policy.action_constraint(), action)
1326 else {
1327 continue;
1328 };
1329
1330 let Some(resource_reason) =
1331 resource_match_reason(policy.resource_constraint(), resource_query.as_ref())
1332 else {
1333 continue;
1334 };
1335
1336 let mut reasons = vec![principal_reason];
1337 if let Some(action_reason) = action_reason {
1338 reasons.push(action_reason);
1339 }
1340 if let Some(resource_reason) = resource_reason {
1341 reasons.push(resource_reason);
1342 }
1343
1344 matching_policies.push((policy.clone(), reasons));
1345 }
1346 }
1347
1348 Ok(PolicyCandidates::new_with_matches(
1349 principal_id,
1350 matching_policies,
1351 ))
1352 }
1353
1354 pub fn policies(&self) -> Vec<Policy> {
1356 let snapshot = self.current_snapshot();
1357 match &snapshot.sets {
1358 PolicySets::Monolithic(set) => set.policies().cloned().collect(),
1359 PolicySets::Scoped { .. } => {
1360 let mut seen_policy_ids = HashSet::new();
1361 let mut policies = snapshot
1362 .sets
1363 .iter()
1364 .flat_map(PolicySet::policies)
1365 .filter(|policy| seen_policy_ids.insert(policy.id().clone()))
1366 .cloned()
1367 .collect::<Vec<_>>();
1368 policies.sort_by(|left, right| left.id().cmp(right.id()));
1369 policies
1370 }
1371 }
1372 }
1373}
1374
1375impl PolicyEngine<SchemaEnforcing> {
1376 pub fn reload_from_str_with_schema(
1378 &self,
1379 policy_text: &str,
1380 schema: Schema,
1381 ) -> Result<(), PolicyError> {
1382 let current_state = self.current_state();
1383 let current_snapshot = ¤t_state.policy;
1384 let layout = current_snapshot.sets.layout().cloned();
1385 let new_snapshot: Snapshot =
1386 Arc::new(PolicySnapshot::from_policy_text_with_schema_and_stores(
1387 policy_text,
1388 Some(Arc::new(schema)),
1389 layout,
1390 )?);
1391 self.install_policy(new_snapshot);
1392 debug!(
1393 event = "PolicyReload",
1394 schema_enabled = true,
1395 schema_reloaded = true,
1396 schema_previously_enabled = true
1397 );
1398 #[cfg(feature = "observability")]
1399 record_reload();
1400 Ok(())
1401 }
1402
1403 pub fn reload_from_str_with_cedarschema(
1405 &self,
1406 policy_text: &str,
1407 schema_text: &str,
1408 ) -> Result<(), PolicyError> {
1409 let schema: Schema = schema_text
1410 .parse()
1411 .map_err(|e| PolicyError::ParseError(format!("failed to parse Cedar schema: {e}")))?;
1412 self.reload_from_str_with_schema(policy_text, schema)
1413 }
1414}
1415
1416impl<M: ValidationMode> EvaluationSession<M> {
1417 pub fn version(&self) -> PolicyVersion {
1419 self.state.version()
1420 }
1421
1422 pub fn evaluate(&self, request: &Request) -> Result<Decision, PolicyError> {
1424 Ok(PolicyEngine::<M>::evaluate_state(&self.state, request, None, false)?.into_decision())
1425 }
1426
1427 pub fn evaluate_with_context(
1429 &self,
1430 request: &Request,
1431 request_context: &RequestContext,
1432 ) -> Result<Decision, PolicyError> {
1433 Ok(
1434 PolicyEngine::<M>::evaluate_state(&self.state, request, Some(request_context), false)?
1435 .into_decision(),
1436 )
1437 }
1438
1439 pub fn evaluate_with_diagnostics(
1441 &self,
1442 request: &Request,
1443 ) -> Result<DecisionDiagnostics, PolicyError> {
1444 PolicyEngine::<M>::evaluate_state(&self.state, request, None, true)
1445 }
1446
1447 pub fn evaluate_with_context_and_diagnostics(
1449 &self,
1450 request: &Request,
1451 request_context: &RequestContext,
1452 ) -> Result<DecisionDiagnostics, PolicyError> {
1453 PolicyEngine::<M>::evaluate_state(&self.state, request, Some(request_context), true)
1454 }
1455}
1456
1457#[cfg(test)]
1458mod tests;