Skip to main content

treetop_core/
engine.rs

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
39/// Static cached Authorizer instance (stateless, reusable across evaluations).
40fn get_authorizer() -> &'static Authorizer {
41    static AUTHORIZER: OnceLock<Authorizer> = OnceLock::new();
42    AUTHORIZER.get_or_init(Authorizer::new)
43}
44
45/// Aggregates timing information for all evaluation phases.
46#[derive(Debug)]
47struct EvalTimers {
48    /// Total elapsed time from start of evaluation
49    total_start: Option<Instant>,
50    /// Whether individual phase timers should sample the clock.
51    measure_enabled: bool,
52    /// Whether debug tracing was enabled when evaluation started.
53    debug_enabled: bool,
54    /// Time spent applying labels
55    labels: Duration,
56    /// Time spent constructing Cedar request
57    construct_req: Duration,
58    /// Time spent building Cedar entities
59    entities: Duration,
60    /// Time spent resolving groups
61    groups: Duration,
62    /// Time spent performing authorization
63    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
86/// Result of preparing a request for authorization: Cedar request, entities, snapshot, and phase timings.
87struct 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/// Compiled authorization policy sets for a monolithic or partitioned engine.
98#[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/// Immutable snapshot of compiled policy sets, along with metadata.
168#[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/// Policy-only metadata that cannot be mistaken for a published engine version.
178#[derive(Debug)]
179struct PolicyRevision {
180    hash: Arc<str>,
181    loaded_at: Arc<str>,
182}
183
184/// Convenience alias for a shared policy snapshot.
185type Snapshot = Arc<PolicySnapshot>;
186
187/// One coherent, immutable generation of all authorization behavior.
188struct 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/// Extract all permit policies from the Cedar authorization result.
369#[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/// Extract matched forbid policy IDs from a Cedar authorization result.
387#[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/// Iterate over groups from a request principal.
407#[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/// Apply label augmentations to a resource.
416#[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/// Build the Cedar request context from the optional typed context.
430#[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/// Build a Cedar request from the authorization request and resource.
441/// UIDs should be pre-converted to avoid redundant conversions.
442#[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/// Build Cedar entities for the principal, resource, and groups.
466/// UIDs should be pre-converted to avoid redundant conversions.
467#[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    // Group resolution is deliberately measured outside this phase so phase
492    // totals are non-overlapping.
493    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        // Construct resource entity
500        let resource_attrs = resource.cedar_attr();
501        let resource_entity =
502            cedar_policy::Entity::new(resource_uid, resource_attrs, Default::default())?;
503
504        // Construct group entities before moving the parent set into the
505        // principal. This avoids cloning the complete HashSet allocation.
506        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        // Construct principal entity with groups as parents, then batch all
510        // entities into a single call. Entity order has no Cedar semantics.
511        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        // Combine all entities in a single call to reduce overhead
516        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
531/// The main engine handle. Thread-safe and cheaply cloneable.
532///
533/// Cloning is cheap (just increments Arc refcounts), but for multithreaded
534/// applications, wrapping in `Arc<PolicyEngine>` and using `Arc::clone()`
535/// is more idiomatic and makes ownership clearer.
536///
537/// For single-threaded use or when passing the engine to a single thread,
538/// you can simply clone it directly.
539mod validation_mode_private {
540    pub trait Sealed {}
541}
542
543/// Type-level policy for Cedar schema enforcement.
544pub trait ValidationMode: validation_mode_private::Sealed {}
545
546/// Marker for an engine that does not use a Cedar schema.
547///
548/// Schema-replacing reloads are intentionally unavailable in this mode:
549///
550/// ```compile_fail
551/// use treetop_core::{PolicyEngine, Schema};
552///
553/// let engine = PolicyEngine::new_from_str("permit(principal, action, resource);").unwrap();
554/// let schema: Schema = "entity User;".parse().unwrap();
555/// engine.reload_from_str_with_schema("permit(principal, action, resource);", schema);
556/// ```
557#[derive(Debug, Clone, Copy)]
558pub struct SchemaFree;
559
560/// Marker for an engine that always validates with a Cedar schema.
561#[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    /// Shared pointer to the atomically replaceable authorization state.
572    inner: Arc<ArcSwap<EngineState>>,
573    mode: PhantomData<fn() -> M>,
574}
575
576/// A frozen, cheaply cloneable authorization-state generation.
577///
578/// Every evaluation through a session uses the same policies, schema, policy
579/// stores, labelers, and version. Create a new session to observe a successful
580/// reload.
581#[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    /// Create an engine that partitions policies into namespace-owned stores.
621    ///
622    /// Existing monolithic constructors remain unchanged. Store assignment is
623    /// validated before the engine is returned, and each request must resolve
624    /// to exactly one declared store or evaluation fails closed.
625    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    /// Create a new policy engine with schema-based policy and request validation.
639    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    /// Create a namespace-partitioned engine with schema validation.
649    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    /// Create a new policy engine from policy text and Cedar schema text.
664    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    /// Create a namespace-partitioned engine from policy and Cedar schema text.
675    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    /// Create a new policy engine with a label registry.
689    ///
690    /// This is a convenience method that combines `new_from_str` and `with_label_registry`.
691    pub fn with_label_registry(self, registry: LabelRegistry) -> Self {
692        self.set_label_registry(registry);
693        self
694    }
695
696    /// Set or replace the label registry for this engine.
697    ///
698    /// This allows updating the labelers after the engine has been created.
699    pub fn set_label_registry(&self, registry: LabelRegistry) {
700        self.inner.rcu(|current| {
701            Arc::new(EngineState {
702                policy: Arc::clone(&current.policy),
703                label_registry: Some(registry.clone()),
704                generation: current.generation.saturating_add(1),
705            })
706        });
707    }
708
709    /// Clone the current immutable label registry, if one is configured.
710    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                        // A label-only update won the race. The compiled policy
732                        // still uses the current schema and store layout, so
733                        // retry publication while preserving the newer labels.
734                        expected = latest;
735                    }
736                    Err(_) => {
737                        // Another policy/schema generation won the race. Compile
738                        // again against that generation so a normal reload can
739                        // never restore a superseded schema.
740                        continue 'compile;
741                    }
742                }
743            }
744        };
745        debug!(
746            event = "PolicyReload",
747            schema_enabled = had_schema,
748            schema_reloaded = false
749        );
750        // Track reloads for metrics (no-op if feature disabled or no sink configured)
751        #[cfg(feature = "observability")]
752        record_reload();
753        Ok(())
754    }
755
756    /// Get the current immutable snapshot.
757    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    /// Publish a compiled policy only if its source state is still current.
776    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    /// Get the complete current authorization-state version.
791    ///
792    /// The policy hash, policy load time, label-set version, and engine
793    /// generation all come from the same atomic state load.
794    pub fn current_version(&self) -> PolicyVersion {
795        self.current_state().version()
796    }
797
798    /// Capture one coherent authorization-state generation for batch work.
799    pub fn session(&self) -> EvaluationSession<M> {
800        EvaluationSession {
801            state: self.current_state(),
802            mode: PhantomData,
803        }
804    }
805
806    /// Return configured policy-store IDs, or `None` for a monolithic engine.
807    pub fn policy_store_ids(&self) -> Option<Vec<PolicyStoreId>> {
808        self.current_state().policy.sets.store_ids()
809    }
810
811    /// Prepare a request for authorization: accumulate labels, build Cedar entities, resolve groups.
812    ///
813    /// This separates request preparation from the authorization decision, making both
814    /// more testable and the main hot path more readable.
815    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        // Convert each UID once after trusted label derivation is complete.
841        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        // The request and entity graph both own the same principal and resource
881        // IDs. Clone each UID once, then move both copies into Cedar instead of
882        // cloning again inside both builders.
883        let principal_uid_for_entities = principal_uid.clone();
884        let resource_uid_for_entities = resource_uid.clone();
885
886        // Build Cedar request with pre-converted UIDs
887        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        // Build entities with pre-converted UIDs and potentially-modified resource
897        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    /// Evaluate a policy request against the currently loaded policy set.
926    ///
927    /// This method performs a complete Cedar policy evaluation:
928    /// 1. Applies any registered labelers to augment resource attributes
929    /// 2. Constructs Cedar entities for the principal (including groups), action, and resource
930    /// 3. Executes the Cedar authorization decision
931    /// 4. Returns either `Allow` (with the matching policy) or `Deny`, both including version metadata
932    ///
933    /// # Arguments
934    ///
935    /// * `request` - The authorization request containing the principal, action, and resource
936    ///
937    /// # Returns
938    ///
939    /// * `Ok(decision)` with [`Decision::is_allowed`] returning `true` if at least one permit policy
940    ///   matches and no forbid policies match, or `false` otherwise
941    /// * `Err(PolicyError)` - If there's an error constructing entities, parsing the request, or during evaluation
942    ///
943    /// # Examples
944    ///
945    /// ```rust
946    /// use treetop_core::{PolicyEngine, Request, Principal, User, Action, Resource};
947    ///
948    /// let policies = r#"
949    ///     permit (
950    ///         principal == User::"alice",
951    ///         action == Action::"read",
952    ///         resource == Document::"doc1"
953    ///     );
954    /// "#;
955    ///
956    /// let engine = PolicyEngine::new_from_str(policies).unwrap();
957    ///
958    /// let request = Request {
959    ///     principal: Principal::User(User::new("alice", None, None).unwrap()),
960    ///     action: Action::new("read", None).unwrap(),
961    ///     resource: Resource::new("Document", "doc1").unwrap(),
962    /// };
963    ///
964    /// let decision = engine.evaluate(&request).unwrap();
965    /// assert!(decision.is_allowed());
966    ///
967    /// // Access version information
968    /// println!("Allowed by policy version: {}", decision.version().hash);
969    /// ```
970    ///
971    /// # Thread Safety
972    ///
973    /// This method is thread-safe and lock-free. Multiple threads can evaluate requests
974    /// concurrently without blocking each other.
975    #[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    /// Evaluate a request with explicit Cedar request context.
986    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    /// Evaluate a request and include deny-side forbid diagnostics.
997    pub fn evaluate_with_diagnostics(
998        &self,
999        request: &Request,
1000    ) -> Result<DecisionDiagnostics, PolicyError> {
1001        self.evaluate_internal(request, None, true)
1002    }
1003
1004    /// Evaluate a request with explicit context and include deny diagnostics.
1005    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        // Prepare the request: apply labels, build entities, resolve groups
1031        let mut prepared = Self::prepare(state, request, request_context)?;
1032
1033        // Perform authorization with RAII timing (using cached Authorizer)
1034        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        // Permit metadata is part of Allow decisions. Forbid IDs are only
1064        // materialized when the caller explicitly requests diagnostics. Metrics
1065        // borrow matching IDs from the Cedar response and compiled snapshot.
1066        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        // Record metrics (no-op when no sink is configured or feature disabled)
1076        #[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    /// List permit-policy candidates whose scope matches a user.
1111    ///
1112    /// This mirrors [`PolicyEngine::evaluate`] input shape for principal identity:
1113    /// user id + groups + shared namespace.
1114    ///
1115    /// Matching includes all Cedar principal-constraint forms:
1116    /// - `principal == User::"..."`
1117    /// - `principal in Group::"..."`
1118    /// - `principal`
1119    /// - `principal is User`
1120    /// - `principal is User in Group::"..."`
1121    ///
1122    /// Cedar `when` and `unless` clauses are not evaluated. The result is not
1123    /// an authorization decision; use [`PolicyEngine::evaluate`] to authorize.
1124    /// Resource constraints are not applied in this method. To additionally
1125    /// filter by policy resource constraints, use
1126    /// [`PolicyEngine::list_policies_for_user_with_resource`].
1127    ///
1128    /// Output is deterministic: policies are sorted by Cedar policy ID.
1129    /// Each returned policy includes match reasons via `PolicyCandidates::matches()`.
1130    ///
1131    /// # Arguments
1132    ///
1133    /// * `user` - User ID
1134    /// * `groups` - Group IDs the user belongs to
1135    /// * `namespace` - Optional shared namespace path for both user and groups
1136    ///
1137    /// # Returns
1138    ///
1139    /// * `Ok(PolicyCandidates)` - Matching policies and match metadata
1140    /// * `Err(PolicyError)` - If entity UID construction fails
1141    ///
1142    /// # Examples
1143    ///
1144    /// ```rust
1145    /// use treetop_core::PolicyEngine;
1146    ///
1147    /// let policies = r#"
1148    ///     permit (principal == User::"alice", action, resource);
1149    ///     permit (principal in Group::"admins", action, resource);
1150    /// "#;
1151    ///
1152    /// let engine = PolicyEngine::new_from_str(policies).unwrap();
1153    /// let candidates = engine.list_policies_for_user("alice", &["admins"], &[]).unwrap();
1154    ///
1155    /// assert_eq!(candidates.policies().len(), 2);
1156    /// assert!(!candidates.matches().is_empty());
1157    /// ```
1158    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    /// List permit-policy candidates whose scope matches a concrete request.
1174    ///
1175    /// This mirrors [`PolicyEngine::evaluate`] by accepting `&Request` and uses:
1176    /// - the request principal (including user group membership, if any)
1177    /// - the request action
1178    /// - the request resource
1179    ///
1180    /// Cedar `when` and `unless` clauses are not evaluated. The result is not
1181    /// an authorization decision. This method defaults to permit policies; use
1182    /// [`PolicyEngine::list_policies_with_effect`] for an explicit effect.
1183    pub fn list_policies(&self, request: &Request) -> Result<PolicyCandidates, PolicyError> {
1184        self.list_policies_with_effect(request, PolicyEffectFilter::Permit)
1185    }
1186
1187    /// List policy candidates for a request, with explicit effect filtering.
1188    ///
1189    /// Cedar `when` and `unless` clauses are not evaluated.
1190    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    /// List all policies applicable to a user, optionally filtering by resource constraints.
1207    ///
1208    /// This variant applies both principal and resource constraints:
1209    /// - principal constraints as described in [`PolicyEngine::list_policies_for_user`]
1210    /// - resource constraints (`==`, `in`, `is`, `is in`, `any`) when `resource` is provided
1211    ///
1212    /// When `resource` is `None`, behavior is equivalent to
1213    /// [`PolicyEngine::list_policies_for_user`].
1214    ///
1215    /// Returned `PolicyCandidates` includes match reasons for principal and, when
1216    /// applicable, resource matches.
1217    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    /// List all policies applicable to a user with optional resource and effect filtering.
1234    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    /// List all policies applicable to a group principal.
1247    ///
1248    /// Useful when callers model group identities directly as principals
1249    /// (mirroring `Principal::Group` in [`PolicyEngine::evaluate`]).
1250    ///
1251    /// Resource constraints are not applied in this method. To also filter by
1252    /// resource constraints, use
1253    /// [`PolicyEngine::list_policies_for_group_with_resource`].
1254    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    /// List all policies applicable to a group principal, optionally filtering by resource constraints.
1268    ///
1269    /// This applies principal constraints for a group principal and, when
1270    /// `resource` is provided, resource constraints as well.
1271    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    /// List all policies applicable to a group principal with optional resource and effect filtering.
1286    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    /// Return all policies in the current coherent engine state.
1355    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    /// Reload policies and replace the enforced schema in one atomic update.
1377    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 = &current_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    /// Reload policies and replace the enforced schema from Cedar schema text.
1404    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    /// Return the complete version used by every evaluation in this session.
1418    pub fn version(&self) -> PolicyVersion {
1419        self.state.version()
1420    }
1421
1422    /// Evaluate a request against this session's frozen state.
1423    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    /// Evaluate a request with explicit Cedar request context.
1428    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    /// Evaluate a request and include deny-side forbid diagnostics.
1440    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    /// Evaluate a contextual request and include deny-side forbid diagnostics.
1448    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;