Skip to main content

treetop_bundle/
engine.rs

1//! Explicit schema modes for engines prepared from optional-schema bundles.
2
3use treetop_core::{
4    Decision, EvaluationSession, LabelRegistry, PolicyCandidates, PolicyEngine, PolicyError,
5    PolicyStoreId, PolicyVersion, Request, RequestContext, SchemaEnforcing, SchemaFree,
6};
7
8/// A prepared engine retaining the bundle's schema validation capability.
9///
10/// Match the variant to access mode-specific Core operations. A schema-free
11/// engine cannot be represented as schema-enforcing:
12///
13/// ```compile_fail
14/// use treetop_bundle::PreparedEngine;
15/// use treetop_core::PolicyEngine;
16/// let free = PolicyEngine::new_from_str("").unwrap();
17/// let enforcing = PreparedEngine::SchemaEnforcing(free);
18/// ```
19#[derive(Clone)]
20pub enum PreparedEngine {
21    /// A bundle without an enforcing Cedar schema.
22    SchemaFree(PolicyEngine<SchemaFree>),
23    /// A bundle with a fully validated, enforcing Cedar schema.
24    SchemaEnforcing(PolicyEngine<SchemaEnforcing>),
25}
26
27/// One frozen authorization generation, retaining its schema mode.
28#[derive(Clone)]
29pub enum PreparedEvaluationSession {
30    /// A frozen generation without schema enforcement.
31    SchemaFree(EvaluationSession<SchemaFree>),
32    /// A frozen generation with schema enforcement.
33    SchemaEnforcing(EvaluationSession<SchemaEnforcing>),
34}
35
36impl From<PolicyEngine<SchemaFree>> for PreparedEngine {
37    fn from(engine: PolicyEngine<SchemaFree>) -> Self {
38        Self::SchemaFree(engine)
39    }
40}
41
42impl From<PolicyEngine<SchemaEnforcing>> for PreparedEngine {
43    fn from(engine: PolicyEngine<SchemaEnforcing>) -> Self {
44        Self::SchemaEnforcing(engine)
45    }
46}
47
48impl PreparedEngine {
49    /// Install a validated registry while retaining the engine's schema mode.
50    pub fn with_label_registry(self, registry: LabelRegistry) -> Self {
51        match self {
52            Self::SchemaFree(engine) => Self::SchemaFree(engine.with_label_registry(registry)),
53            Self::SchemaEnforcing(engine) => {
54                Self::SchemaEnforcing(engine.with_label_registry(registry))
55            }
56        }
57    }
58
59    /// Capture one complete generation for all evaluations and version reporting.
60    pub fn session(&self) -> PreparedEvaluationSession {
61        match self {
62            Self::SchemaFree(engine) => PreparedEvaluationSession::SchemaFree(engine.session()),
63            Self::SchemaEnforcing(engine) => {
64                PreparedEvaluationSession::SchemaEnforcing(engine.session())
65            }
66        }
67    }
68    /// Return the current complete authorization-state version.
69    pub fn current_version(&self) -> PolicyVersion {
70        match self {
71            Self::SchemaFree(engine) => engine.current_version(),
72            Self::SchemaEnforcing(engine) => engine.current_version(),
73        }
74    }
75
76    /// Return configured store IDs, or None for a monolithic engine.
77    pub fn policy_store_ids(&self) -> Option<Vec<PolicyStoreId>> {
78        match self {
79            Self::SchemaFree(engine) => engine.policy_store_ids(),
80            Self::SchemaEnforcing(engine) => engine.policy_store_ids(),
81        }
82    }
83
84    /// List structural permit-policy candidates; these do not authorize operations.
85    ///
86    /// Returns Core validation errors for malformed principal inputs.
87    pub fn list_policies_for_user(
88        &self,
89        user: &str,
90        groups: &[&str],
91        namespace: &[&str],
92    ) -> Result<PolicyCandidates, PolicyError> {
93        match self {
94            Self::SchemaFree(engine) => engine.list_policies_for_user(user, groups, namespace),
95            Self::SchemaEnforcing(engine) => engine.list_policies_for_user(user, groups, namespace),
96        }
97    }
98
99    /// Evaluate a request, returning Core errors for invalid request data.
100    pub fn evaluate(&self, request: &Request) -> Result<Decision, PolicyError> {
101        match self {
102            Self::SchemaFree(engine) => engine.evaluate(request),
103            Self::SchemaEnforcing(engine) => engine.evaluate(request),
104        }
105    }
106
107    /// Evaluate with explicit context, returning Core validation/evaluation errors.
108    pub fn evaluate_with_context(
109        &self,
110        request: &Request,
111        context: &RequestContext,
112    ) -> Result<Decision, PolicyError> {
113        match self {
114            Self::SchemaFree(engine) => engine.evaluate_with_context(request, context),
115            Self::SchemaEnforcing(engine) => engine.evaluate_with_context(request, context),
116        }
117    }
118}
119
120impl PreparedEvaluationSession {
121    /// Return the exact version used by every evaluation in this session.
122    pub fn version(&self) -> PolicyVersion {
123        match self {
124            Self::SchemaFree(engine) => engine.version(),
125            Self::SchemaEnforcing(engine) => engine.version(),
126        }
127    }
128
129    /// Evaluate a request, returning Core errors for invalid request data.
130    pub fn evaluate(&self, request: &Request) -> Result<Decision, PolicyError> {
131        match self {
132            Self::SchemaFree(engine) => engine.evaluate(request),
133            Self::SchemaEnforcing(engine) => engine.evaluate(request),
134        }
135    }
136
137    /// Evaluate with explicit context, returning Core validation/evaluation errors.
138    pub fn evaluate_with_context(
139        &self,
140        request: &Request,
141        context: &RequestContext,
142    ) -> Result<Decision, PolicyError> {
143        match self {
144            Self::SchemaFree(engine) => engine.evaluate_with_context(request, context),
145            Self::SchemaEnforcing(engine) => engine.evaluate_with_context(request, context),
146        }
147    }
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153    use treetop_core::{Action, Principal, Resource, User};
154
155    #[test]
156    fn session_retains_policy_generation_after_reload() {
157        let core = PolicyEngine::new_from_str("permit(principal, action, resource);").unwrap();
158        let prepared = PreparedEngine::from(core.clone());
159        let session = prepared.session();
160        let request = Request {
161            principal: Principal::User(User::new("alice", None, None).unwrap()),
162            action: Action::new("read", None).unwrap(),
163            resource: Resource::new("Document", "one").unwrap(),
164        };
165        core.reload_from_str("forbid(principal, action, resource);")
166            .unwrap();
167        let old = session.evaluate(&request).unwrap();
168        assert!(old.is_allowed());
169        assert_eq!(old.version(), &session.version());
170        assert!(!prepared.evaluate(&request).unwrap().is_allowed());
171        assert!(prepared.current_version().generation > session.version().generation);
172    }
173
174    #[test]
175    fn schema_enforcing_variant_retains_validation() {
176        let schema = r#"entity User; entity Document; action "read" appliesTo {
177            principal: [User], resource: [Document], context: {}
178        };"#;
179        let core = PolicyEngine::new_from_str_with_cedarschema(
180            "permit(principal, action, resource);",
181            schema,
182        )
183        .unwrap();
184        let prepared = PreparedEngine::from(core);
185        assert!(matches!(prepared, PreparedEngine::SchemaEnforcing(_)));
186        let request = Request {
187            principal: Principal::User(User::new("alice", None, None).unwrap()),
188            action: Action::new("unknown", None).unwrap(),
189            resource: Resource::new("Document", "one").unwrap(),
190        };
191        assert!(prepared.session().evaluate(&request).is_err());
192    }
193}