Skip to main content

cedar_policy/api/
tpe.rs

1/*
2 * Copyright Cedar Contributors
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      https://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17use std::collections::{BTreeMap, HashMap, HashSet};
18use std::sync::Arc;
19
20use cedar_policy_core::ast::{self, Value};
21use cedar_policy_core::authorizer::Decision;
22use cedar_policy_core::batched_evaluator::is_authorized_batched;
23use cedar_policy_core::batched_evaluator::{
24    err::BatchedEvalError, EntityLoader as EntityLoaderInternal,
25};
26use cedar_policy_core::evaluator::{EvaluationError, RestrictedEvaluator};
27use cedar_policy_core::extensions::Extensions;
28use cedar_policy_core::tpe;
29use itertools::Itertools;
30use ref_cast::RefCast;
31use smol_str::SmolStr;
32
33use crate::{
34    api, tpe_err, Authorizer, Context, Entities, Entity, EntityId, EntityTypeName, EntityUid,
35    PartialEntityError, PartialRequestCreationError, PermissionQueryError, Policy, PolicyId,
36    PolicySet, Request, RequestValidationError, RestrictedExpression, Schema,
37    TpeReauthorizationError,
38};
39
40/// A partial [`EntityUid`].
41/// That is, its [`EntityId`] could be unknown
42#[doc = include_str!("../../experimental_warning.md")]
43#[repr(transparent)]
44#[derive(Debug, Clone, RefCast)]
45pub struct PartialEntityUid(pub(crate) tpe::request::PartialEntityUID);
46
47#[doc(hidden)]
48impl AsRef<tpe::request::PartialEntityUID> for PartialEntityUid {
49    fn as_ref(&self) -> &tpe::request::PartialEntityUID {
50        &self.0
51    }
52}
53
54impl PartialEntityUid {
55    /// Construct a [`PartialEntityUid`]
56    pub fn new(ty: EntityTypeName, id: Option<EntityId>) -> Self {
57        Self(tpe::request::PartialEntityUID {
58            ty: ty.0,
59            eid: id.map(|id| <EntityId as AsRef<ast::Eid>>::as_ref(&id).clone()),
60        })
61    }
62
63    /// Construct a [`PartialEntityUid`] from a concrete [`EntityUid`].
64    pub fn from_concrete(euid: EntityUid) -> Self {
65        let (ty, eid) = euid.0.components();
66        Self(tpe::request::PartialEntityUID { ty, eid: Some(eid) })
67    }
68}
69
70/// A partial [`Request`]
71/// Its principal/resource types and action must be known and its context
72/// must either be fully known or unknown
73#[doc = include_str!("../../experimental_warning.md")]
74#[repr(transparent)]
75#[derive(Debug, Clone, RefCast)]
76pub struct PartialRequest(pub(crate) tpe::request::PartialRequest);
77
78#[doc(hidden)]
79impl AsRef<tpe::request::PartialRequest> for PartialRequest {
80    fn as_ref(&self) -> &tpe::request::PartialRequest {
81        &self.0
82    }
83}
84
85impl PartialRequest {
86    /// Construct a valid [`PartialRequest`] according to a [`Schema`]
87    pub fn new(
88        principal: PartialEntityUid,
89        action: EntityUid,
90        resource: PartialEntityUid,
91        context: Option<Context>,
92        schema: &Schema,
93    ) -> Result<Self, PartialRequestCreationError> {
94        let context = context
95            .map(|c| match c.0 {
96                ast::Context::RestrictedResidual(_) => {
97                    Err(PartialRequestCreationError::ContextContainsUnknowns)
98                }
99                ast::Context::Value(m) => Ok(m),
100            })
101            .transpose()?;
102        tpe::request::PartialRequest::new(principal.0, action.0, resource.0, context, &schema.0)
103            .map(Self)
104            .map_err(|e| PartialRequestCreationError::Validation(e.into()))
105    }
106}
107
108/// Like [`PartialRequest`] but only `resource` can be unknown
109///
110/// Intended for use with [`PolicySet::query_resource`].
111#[doc = include_str!("../../experimental_warning.md")]
112#[repr(transparent)]
113#[derive(Debug, Clone, RefCast)]
114pub struct ResourceQueryRequest(pub(crate) PartialRequest);
115
116impl ResourceQueryRequest {
117    /// Construct a valid [`ResourceQueryRequest`] according to a [`Schema`]
118    pub fn new(
119        principal: EntityUid,
120        action: EntityUid,
121        resource: EntityTypeName,
122        context: Context,
123        schema: &Schema,
124    ) -> Result<Self, PartialRequestCreationError> {
125        PartialRequest::new(
126            PartialEntityUid(principal.0.into()),
127            action,
128            PartialEntityUid::new(resource, None),
129            Some(context),
130            schema,
131        )
132        .map(Self)
133    }
134
135    fn principal(&self) -> EntityUid {
136        #[expect(
137            clippy::unwrap_used,
138            reason = "constructor requires concrete principal"
139        )]
140        EntityUid(self.0 .0.principal().clone().try_into().unwrap())
141    }
142
143    fn context(&self) -> Context {
144        #[expect(clippy::unwrap_used, reason = "constructor requires concrete context")]
145        let context_attrs = self.0 .0.context_attrs().unwrap();
146        #[expect(
147            clippy::unwrap_used,
148            reason = "building context from BTreeMap iter, so no duplicates are possible"
149        )]
150        Context::from_pairs(
151            context_attrs
152                .iter()
153                .map(|(a, v)| (a.to_string(), RestrictedExpression(v.clone().into()))),
154        )
155        .unwrap()
156    }
157
158    /// Convert this to a [`Request`] by providing the resource [`EntityId`]
159    ///
160    /// Even though the partial request was already validated in [`ResourceQueryRequest::new`],
161    /// to ensure that the concrete request returned here is valid we still need to
162    /// check the resource entity id. If the resource has an enum entity type,
163    /// then its id must be one of the listed instances of that type.
164    pub fn to_request(
165        &self,
166        resource_id: EntityId,
167        schema: Option<&Schema>,
168    ) -> Result<Request, RequestValidationError> {
169        Request::new(
170            self.principal(),
171            EntityUid(self.0 .0.action().clone()),
172            EntityUid::from_type_name_and_id(
173                EntityTypeName(self.0 .0.resource_type().clone()),
174                resource_id,
175            ),
176            self.context(),
177            schema,
178        )
179    }
180}
181
182/// Like [`PartialRequest`] but only `principal` can be unknown
183///
184/// Intended for use with [`PolicySet::query_principal`].
185#[doc = include_str!("../../experimental_warning.md")]
186#[repr(transparent)]
187#[derive(Debug, Clone, RefCast)]
188pub struct PrincipalQueryRequest(pub(crate) PartialRequest);
189
190impl PrincipalQueryRequest {
191    /// Construct a valid [`PrincipalQueryRequest`] according to a [`Schema`]
192    pub fn new(
193        principal: EntityTypeName,
194        action: EntityUid,
195        resource: EntityUid,
196        context: Context,
197        schema: &Schema,
198    ) -> Result<Self, PartialRequestCreationError> {
199        PartialRequest::new(
200            PartialEntityUid::new(principal, None),
201            action,
202            PartialEntityUid(resource.0.into()),
203            Some(context),
204            schema,
205        )
206        .map(Self)
207    }
208
209    fn resource(&self) -> EntityUid {
210        #[expect(clippy::unwrap_used, reason = "constructor requires concrete resource")]
211        EntityUid(self.0 .0.resource().clone().try_into().unwrap())
212    }
213
214    fn context(&self) -> Context {
215        #[expect(clippy::unwrap_used, reason = "constructor requires concrete context")]
216        let context_attrs = self.0 .0.context_attrs().unwrap();
217        #[expect(
218            clippy::unwrap_used,
219            reason = "building context from BTreeMap iter, so no duplicates are possible"
220        )]
221        Context::from_pairs(
222            context_attrs
223                .iter()
224                .map(|(a, v)| (a.to_string(), RestrictedExpression(v.clone().into()))),
225        )
226        .unwrap()
227    }
228
229    /// Convert this to a [`Request`] by providing the principal [`EntityId`]
230    ///
231    /// Even though the partial request was already validated in [`PrincipalQueryRequest::new`],
232    /// to ensure that the concrete request returned here is valid we still need to
233    /// check the principal entity id. If the principal has an enum entity type,
234    /// then its id must be one of the listed instances of that type.
235    pub fn to_request(
236        &self,
237        principal_id: EntityId,
238        schema: Option<&Schema>,
239    ) -> Result<Request, RequestValidationError> {
240        Request::new(
241            EntityUid::from_type_name_and_id(
242                EntityTypeName(self.0 .0.principal_type().clone()),
243                principal_id,
244            ),
245            EntityUid(self.0 .0.action().clone()),
246            self.resource(),
247            self.context(),
248            schema,
249        )
250    }
251}
252
253/// Defines a [`PartialRequest`] which additionally leaves the action
254/// undefined, enabling queries listing what actions might be authorized.
255///
256/// See [`PolicySet::query_action`] for documentation and example usage.
257#[doc = include_str!("../../experimental_warning.md")]
258#[derive(Debug, Clone)]
259pub struct ActionQueryRequest {
260    principal: PartialEntityUid,
261    resource: PartialEntityUid,
262    context: Option<Arc<BTreeMap<SmolStr, Value>>>,
263    schema: Schema,
264}
265
266impl ActionQueryRequest {
267    /// Construct an [`ActionQueryRequest`].
268    ///
269    /// Unlike [`PartialRequest::new`], this constructor cannot validate the
270    /// request because request validation requires knowing the specific action
271    /// being authorized. Further, [`PolicySet::query_action`] cannot report
272    /// request validation errors because it is expected that the
273    /// `principal`, `resource`, and `context` will be invalid for many of the
274    /// actions in the schema.
275    pub fn new(
276        principal: PartialEntityUid,
277        resource: PartialEntityUid,
278        context: Option<Context>,
279        schema: Schema,
280    ) -> Result<Self, PartialRequestCreationError> {
281        let context = context
282            .map(|c| match c.0 {
283                ast::Context::RestrictedResidual(_) => {
284                    Err(PartialRequestCreationError::ContextContainsUnknowns)
285                }
286                ast::Context::Value(m) => Ok(m),
287            })
288            .transpose()?;
289        Ok(Self {
290            principal,
291            resource,
292            context,
293            schema,
294        })
295    }
296
297    fn partial_request(
298        &self,
299        action: EntityUid,
300    ) -> Result<PartialRequest, cedar_policy_core::validator::RequestValidationError> {
301        tpe::request::PartialRequest::new(
302            self.principal.0.clone(),
303            action.0,
304            self.resource.0.clone(),
305            self.context.clone(),
306            &self.schema.0,
307        )
308        .map(PartialRequest)
309    }
310}
311
312/// Partial [`Entity`]
313#[doc = include_str!("../../experimental_warning.md")]
314#[repr(transparent)]
315#[derive(Debug, Clone, RefCast)]
316pub struct PartialEntity(pub(crate) tpe::entities::PartialEntity);
317
318impl PartialEntity {
319    /// Construct a [`PartialEntity`]
320    pub fn new(
321        uid: EntityUid,
322        attrs: Option<BTreeMap<SmolStr, RestrictedExpression>>,
323        ancestors: Option<HashSet<EntityUid>>,
324        tags: Option<BTreeMap<SmolStr, RestrictedExpression>>,
325        schema: &Schema,
326    ) -> Result<Self, PartialEntityError> {
327        Ok(Self(tpe::entities::PartialEntity::new(
328            uid.0,
329            attrs
330                .map(|ps| {
331                    ps.into_iter()
332                        .map(|(k, v)| {
333                            Ok((
334                                k,
335                                RestrictedEvaluator::new(Extensions::all_available())
336                                    .interpret(v.0.as_borrowed())?,
337                            ))
338                        })
339                        .collect::<Result<BTreeMap<_, _>, EvaluationError>>()
340                })
341                .transpose()?,
342            ancestors.map(|s| s.into_iter().map(|e| e.0).collect()),
343            tags.map(|ps| {
344                ps.into_iter()
345                    .map(|(k, v)| {
346                        Ok((
347                            k,
348                            RestrictedEvaluator::new(Extensions::all_available())
349                                .interpret(v.0.as_borrowed())?,
350                        ))
351                    })
352                    .collect::<Result<BTreeMap<_, _>, EvaluationError>>()
353            })
354            .transpose()?,
355            &schema.0,
356        )?))
357    }
358}
359
360/// Partial [`Entities`]
361#[doc = include_str!("../../experimental_warning.md")]
362#[repr(transparent)]
363#[derive(Debug, Clone, RefCast)]
364pub struct PartialEntities(pub(crate) tpe::entities::PartialEntities);
365
366#[doc(hidden)]
367impl AsRef<tpe::entities::PartialEntities> for PartialEntities {
368    fn as_ref(&self) -> &tpe::entities::PartialEntities {
369        &self.0
370    }
371}
372
373impl PartialEntities {
374    /// Construct [`PartialEntities`] from a JSON value
375    /// The `parent`, `attrs`, `tags` field must be either fully known or
376    /// unknown. And parent entities cannot have unknown parents.
377    pub fn from_json_value(
378        value: serde_json::Value,
379        schema: &Schema,
380    ) -> Result<Self, tpe_err::EntitiesError> {
381        tpe::entities::PartialEntities::from_json_value(value, &schema.0).map(Self)
382    }
383
384    /// Construct [`PartialEntities`] given a fully concrete [`Entities`]
385    pub fn from_concrete(
386        entities: Entities,
387        schema: &Schema,
388    ) -> Result<Self, tpe_err::EntitiesError> {
389        tpe::entities::PartialEntities::from_concrete(entities.0, &schema.0).map(Self)
390    }
391
392    /// Create a `PartialEntities` with no entities
393    pub fn empty() -> Self {
394        Self(tpe::entities::PartialEntities::new())
395    }
396
397    /// Construct [`PartialEntities`] from an iterator of [`PartialEntity`]
398    pub fn from_partial_entities(
399        entities: impl IntoIterator<Item = PartialEntity>,
400        schema: &Schema,
401    ) -> Result<Self, tpe_err::EntitiesError> {
402        Ok(Self(tpe::entities::PartialEntities::from_entities(
403            entities.into_iter().map(|entity| entity.0),
404            &schema.0,
405        )?))
406    }
407}
408
409/// A response to a partial authorization request.
410///
411/// Most callers will want to first check if a concrete authorization decision was reached using
412/// [`TpeResponse::decision`] before inspecting the unevaluated policies with
413/// [`TpeResponse::residual_policies`] or resuming evaluation after providing
414/// the missing parts of the request with [`TpeResponse::reauthorize`].
415#[doc = include_str!("../../experimental_warning.md")]
416#[repr(transparent)]
417#[derive(Debug, Clone, RefCast)]
418pub struct TpeResponse<'a>(pub(crate) tpe::response::Response<'a>);
419
420#[doc(hidden)]
421impl<'a> AsRef<tpe::response::Response<'a>> for TpeResponse<'a> {
422    fn as_ref(&self) -> &tpe::response::Response<'a> {
423        &self.0
424    }
425}
426
427impl TpeResponse<'_> {
428    /// Get the authorization decision, if TPE reached a concrete decision.
429    ///
430    /// This function can return three possible values:
431    /// * `Some(Decision::Allow)`, when there was enough information in the
432    ///    partial request to concretely decide that the request should be allowed.
433    /// * `Some(Decision::Deny)`, when there was enough information to decide
434    ///    that the request should be denied.
435    /// * `None`, when the partial request did _not_ provide enough information to reach an
436    ///    authorization decision. In this case you can use [`TpeResponse::reauthorize`] to provide
437    ///    the missing parts of the request and reach a concrete decision.
438    pub fn decision(&self) -> Option<Decision> {
439        self.0.decision()
440    }
441
442    /// Get the determining policies for the partial authorization decision.
443    /// These are a subset of the determining policies in the response returned
444    /// after calling [`TpeResponse::reauthorize`] with a concrete request and entities.
445    ///
446    /// When [`TpeResponse::decision`] returns a concrete allow or deny, the
447    /// determining policies returned by this function are exactly the policies from
448    /// [`TpeResponse::true_permits`] or [`TpeResponse::true_forbids`] respectively.
449    ///
450    /// If partial authorization does not reach a decision, then this function
451    /// returns `None`. It's reasonable to treat this response as "no known
452    /// determining policies", in which case you can call this function as
453    /// `response.reason().into_iter().flatten()`.
454    pub fn reason(&self) -> Option<impl Iterator<Item = &PolicyId>> {
455        Some(self.0.reason()?.map(PolicyId::ref_cast))
456    }
457
458    /// Get the permit policies that did not reach a concrete value or error for the partial request.
459    ///
460    /// This function only returns the `PolicyId`s for residual policies.
461    /// To access the residual policy conditions, use [`TpeResponse::residual_policies`].
462    ///
463    /// These policies could be determining policies _if_ the eventual
464    /// concrete authorization decision is `Allow` _and_ they are satisfied by
465    /// the concrete request. If the [`TpeResponse::decision`] is `Deny`, then
466    /// they cannot be determining.
467    pub fn residual_permits(&self) -> impl Iterator<Item = &PolicyId> {
468        self.0
469            .residual_permits()
470            .map(|rp| PolicyId::ref_cast(rp.get_policy_id()))
471    }
472
473    /// Get the permit policies that are concretely satisfied by the partial request.
474    ///
475    /// To properly interpret the ids returned from this function you need to
476    /// consider them in the context of [`TpeResponse::decision`]:
477    /// * For a concrete `Allow` decision, these are a subset of the concrete
478    ///   determining policies and are exactly the policies returned by
479    ///   [`TpeResponse::reason`].
480    /// * For a concrete `Deny` decision, these are not determining policies. The
481    ///   iterator may be empty if no permits were satisfied, or it may contain
482    ///   satisfied permits which have been overridden by at least one satisfied
483    ///   forbid policy.
484    /// * For an unknown decision, these will be a subset of the determining
485    ///   policies _if_ the eventual concrete authorization decision is `Allow`,
486    ///   but they may still be overridden by any non-trivial residual forbid
487    ///   policy.
488    pub fn true_permits(&self) -> impl Iterator<Item = &PolicyId> {
489        self.0
490            .true_permits()
491            .map(|rp| PolicyId::ref_cast(rp.get_policy_id()))
492    }
493
494    /// Get the permit policies that are concretely not satisfied by the partial request.
495    ///
496    /// These policies evaluate to `false`, so they have no impact on the
497    /// partial authorization decision or on any subsequent concrete decision
498    /// after reauthorization.
499    pub fn false_permits(&self) -> impl Iterator<Item = &PolicyId> {
500        self.0
501            .false_permits()
502            .map(|rp| PolicyId::ref_cast(rp.get_policy_id()))
503    }
504
505    /// Get the permit policies that encountered concrete errors for the partial request.
506    ///
507    /// These policies errored, so they have no impact on the partial
508    /// authorization decision. Erroring policies are not generally expected
509    /// since partial evaluation works only on _validated_ policies, but it is still
510    /// possible to encounter errors, e.g., on integer overflow.
511    pub fn error_permits(&self) -> impl Iterator<Item = &PolicyId> {
512        self.0
513            .error_permits()
514            .map(|rp| PolicyId::ref_cast(rp.get_policy_id()))
515    }
516
517    /// Get the forbid policies that did not reach a concrete value or error for the partial request.
518    ///
519    /// This function only returns the `PolicyId`s for residual policies.
520    /// To access the residual policy conditions, use [`TpeResponse::residual_policies`].
521    ///
522    /// The presence of any residual forbids means that [`TpeResponse::decision`] _cannot_ return
523    /// a concrete `Allow` decision. We do not have enough information to say that these forbid
524    /// policies do not apply, so they might still override any satisfied permit policies.
525    pub fn residual_forbids(&self) -> impl Iterator<Item = &PolicyId> {
526        self.0
527            .residual_forbids()
528            .map(|rp| PolicyId::ref_cast(rp.get_policy_id()))
529    }
530
531    /// Get the forbid policies that are concretely satisfied by the partial request.
532    ///
533    /// Presence of any satisfied forbids guarantees that they are exactly the
534    /// policies returned by [`TpeResponse::reason`] and that [`TpeResponse::decision`]
535    /// must return a concrete `Deny`.
536    pub fn true_forbids(&self) -> impl Iterator<Item = &PolicyId> {
537        self.0
538            .true_forbids()
539            .map(|rp| PolicyId::ref_cast(rp.get_policy_id()))
540    }
541
542    /// Get the forbid policies that are concretely not satisfied by the partial request.
543    ///
544    /// These policies evaluate to `false`, so they have no impact on the
545    /// partial authorization decision or on any subsequent concrete decision
546    /// after reauthorization.
547    pub fn false_forbids(&self) -> impl Iterator<Item = &PolicyId> {
548        self.0
549            .false_forbids()
550            .map(|rp| PolicyId::ref_cast(rp.get_policy_id()))
551    }
552
553    /// Get the forbid policies that encountered concrete errors for the partial request.
554    ///
555    /// These policies errored, so they have no impact on the partial
556    /// authorization decision. Erroring policies are not generally expected
557    /// since partial evaluation works only on _validated_ policies, but it is still
558    /// possible to encounter errors, e.g., on integer overflow.
559    pub fn error_forbids(&self) -> impl Iterator<Item = &PolicyId> {
560        self.0
561            .error_forbids()
562            .map(|rp| PolicyId::ref_cast(rp.get_policy_id()))
563    }
564
565    /// Perform reauthorization, taking the residual policies and further
566    /// evaluating them with a concrete request and entities.
567    ///
568    /// If [`TpeResponse::decision`] returns a decision, then reauthorization
569    /// will always reach the same decision. If it does not, then this function
570    /// allows you to provide any data omitted from the partial request in order
571    /// to reach a concrete decision.
572    pub fn reauthorize(
573        &self,
574        request: &Request,
575        entities: &Entities,
576    ) -> Result<api::Response, TpeReauthorizationError> {
577        self.0
578            .reauthorize(&request.0, &entities.0)
579            .map(Into::into)
580            .map_err(Into::into)
581    }
582
583    /// Returns an iterator of non-trivial (meaning more than just `true`
584    /// or `false`, or an error) residuals as [`Policy`]s.
585    ///
586    /// To find policies that reached a concrete value, use, e.g., [`TpeResponse::true_permits`].
587    ///
588    /// Each returned [`Policy`] inherits its [`PolicyId`] and
589    /// annotations from the corresponding input policy. Its scope is
590    /// unconstrained and its condition is a single `when` clause containing
591    /// the residual expression.
592    ///
593    /// Call [`Policy::to_pst()`] on each result to get a [`pst::Policy`](crate::pst::Policy)
594    /// for structured inspection.
595    ///
596    /// ```no_run
597    /// # use cedar_policy::{PolicySet, PartialRequest, PartialEntities, Schema};
598    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
599    /// # let (policy_set, request, entities, schema) : (&PolicySet, &PartialRequest, &PartialEntities, &Schema) = panic!();
600    /// let response = policy_set.tpe(&request, &entities, &schema)?;
601    /// for policy in response.residual_policies() {
602    ///     let pst_policy = policy.to_pst()?;
603    ///     for clause in pst_policy.body().clauses() {
604    ///         // inspect the residual expression via pst::Clause / pst::Expr
605    ///     }
606    /// }
607    /// # Ok(())
608    /// # }
609    /// ```
610    ///
611    /// When inspecting these policies, be aware that they may contain
612    /// [`pst::Expr::ResidualError`](crate::pst::Expr::ResidualError) nodes
613    /// which do not normally exist in Cedar expressions. These represent
614    /// subexpressions which are statically known to error; however, the whole
615    /// residual policy might or might not error, regardless of whether it
616    /// contains these nodes.
617    pub fn residual_policies(&self) -> impl Iterator<Item = Policy> + '_ {
618        self.0
619            .residual_permits()
620            .chain(self.0.residual_forbids())
621            .map(|p| Policy::from_ast(p.clone().into()))
622    }
623
624    /// Return all residuals as [`Policy`]s, including concretely `true`, `false`, and error residuals.
625    ///
626    /// Each returned [`Policy`] inherits its [`PolicyId`](crate::PolicyId) and
627    /// annotations from the corresponding input policy. Its scope is
628    /// unconstrained and its condition is a single `when` clause containing
629    /// the residual expression.
630    ///
631    /// Use [`TpeResponse::residual_policies`] to skip `true`, `false`, and error residuals.
632    ///
633    /// See [`TpeResponse::residual_policies`] for documentation on how to inspect policies using the PST.
634    pub fn policies(&self) -> impl Iterator<Item = Policy> + '_ {
635        self.0
636            .policies()
637            .map(|p| Policy::from_ast(p.clone().into()))
638    }
639
640    /// Return all residuals as a [`PolicySet`], including concretely `true`, `false`, and error residuals.
641    ///
642    /// This returns exactly the same policies as [`TpeResponse::policies`], but collected into a policy set.
643    pub fn policy_set(&self) -> PolicySet {
644        PolicySet::from_ast(self.0.policy_set())
645    }
646
647    /// Deprecated alias for [`TpeResponse::residual_policies`]
648    #[deprecated(
649        since = "4.12.0",
650        note = "TpeResponse::residual_policies now returns only non-trivial residual policies"
651    )]
652    pub fn nontrivial_residual_policies(&'_ self) -> impl Iterator<Item = Policy> + '_ {
653        self.residual_policies()
654    }
655
656    /// Get the residual policy for a specific [`PolicyId`], if it exists.
657    ///
658    /// See [`TpeResponse::residual_policies`] for documentation on how to inspect policies using the PST.
659    pub fn get_policy(&self, id: &PolicyId) -> Option<Policy> {
660        self.0
661            .get_residual_policy(id.as_ref())
662            .map(|p| Policy::from_ast(p.clone().into()))
663    }
664}
665
666/// Entity loader trait for batched evaluation.
667///
668/// Loads entities on demand, returning `None` for missing entities.
669/// The `load_entities` function must load all requested entities,
670/// and must compute and include all ancestors of the requested entities.
671/// Loading more entities than requested is allowed.
672#[doc = include_str!("../../experimental_warning.md")]
673pub trait EntityLoader {
674    /// Load all entities for the given set of entity UIDs.
675    /// Returns a map from [`EntityUid`] to [`Option<Entity>`], where `None` indicates
676    /// the entity does not exist.
677    fn load_entities(&mut self, uids: &HashSet<EntityUid>) -> HashMap<EntityUid, Option<Entity>>;
678}
679
680/// Wrapper struct used to convert an [`EntityLoader`] to an `EntityLoaderInternal`
681struct EntityLoaderWrapper<'a>(&'a mut dyn EntityLoader);
682
683impl EntityLoaderInternal for EntityLoaderWrapper<'_> {
684    fn load_entities(
685        &mut self,
686        uids: &HashSet<ast::EntityUID>,
687    ) -> HashMap<ast::EntityUID, Option<ast::Entity>> {
688        let ids = uids
689            .iter()
690            .map(|id| EntityUid::ref_cast(id).clone())
691            .collect();
692        self.0
693            .load_entities(&ids)
694            .into_iter()
695            .map(|(uid, entity)| (uid.0, entity.map(|e| e.0)))
696            .collect()
697    }
698}
699
700/// Simple entity loader implementation that loads from a pre-existing Entities store
701#[doc = include_str!("../../experimental_warning.md")]
702#[derive(Debug)]
703
704pub struct TestEntityLoader<'a> {
705    entities: &'a Entities,
706}
707
708impl<'a> TestEntityLoader<'a> {
709    /// Create a new [`TestEntityLoader`] from an existing Entities store
710    pub fn new(entities: &'a Entities) -> Self {
711        Self { entities }
712    }
713}
714
715impl EntityLoader for TestEntityLoader<'_> {
716    fn load_entities(&mut self, uids: &HashSet<EntityUid>) -> HashMap<EntityUid, Option<Entity>> {
717        uids.iter()
718            .map(|uid| {
719                let entity = self.entities.get(uid).cloned();
720                (uid.clone(), entity)
721            })
722            .collect()
723    }
724}
725
726impl PolicySet {
727    /// Perform type-aware partial evaluation on this [`PolicySet`].
728    ///
729    /// If successful, the result is a [`TpeResponse`] containing the authorization decision, if
730    /// one was reached, and residual policies ready for re-authorization. Use [`TpeResponse::decision`]
731    /// to check the decision and [`TpeResponse::residual_policies`] to get the residuals as
732    /// [`Policy`] objects. You can then call [`Policy::to_pst`] to convert them to [`pst::Policy`](crate::pst::Policy)
733    /// for structured inspection of the residual expression tree.
734    #[doc = include_str!("../../experimental_warning.md")]
735    pub fn tpe<'a>(
736        &self,
737        request: &'a PartialRequest,
738        entities: &'a PartialEntities,
739        schema: &'a Schema,
740    ) -> Result<TpeResponse<'a>, tpe_err::TpeError> {
741        use cedar_policy_core::tpe::is_authorized;
742        let ps = &self.ast;
743        let res = is_authorized(ps, &request.0, &entities.0, &schema.0)?;
744        Ok(TpeResponse(res))
745    }
746
747    /// Like [`Authorizer::is_authorized`] but uses an [`EntityLoader`] to load
748    /// entities on demand.
749    ///
750    /// Calls `loader` at most `max_iters` times, returning
751    /// early if an authorization result is reached.
752    /// Otherwise, it iterates `max_iters` times and returns
753    /// a partial result.
754    ///
755    #[doc = include_str!("../../experimental_warning.md")]
756    pub fn is_authorized_batched(
757        &self,
758        query: &Request,
759        schema: &Schema,
760        loader: &mut dyn EntityLoader,
761        max_iters: u32,
762    ) -> Result<Decision, BatchedEvalError> {
763        is_authorized_batched(
764            &query.0,
765            &self.ast,
766            &schema.0,
767            &mut EntityLoaderWrapper(loader),
768            max_iters,
769        )
770    }
771
772    /// Perform a permission query on the resource
773    #[doc = include_str!("../../experimental_warning.md")]
774    pub fn query_resource(
775        &self,
776        request: &ResourceQueryRequest,
777        entities: &Entities,
778        schema: &Schema,
779    ) -> Result<impl Iterator<Item = EntityUid>, PermissionQueryError> {
780        let partial_entities = PartialEntities::from_concrete(entities.clone(), schema)?;
781        let tpe_response = self.tpe(&request.0, &partial_entities, schema)?;
782        let policies = tpe_response.policy_set();
783        match tpe_response.decision() {
784            Some(Decision::Allow) => Ok(entities
785                .iter()
786                .filter(|entity| entity.0.uid().entity_type() == request.0.0.resource_type())
787                .map(Entity::uid)
788                .collect_vec()
789                .into_iter()),
790            Some(Decision::Deny) => Ok(vec![].into_iter()),
791            None => Ok(entities
792                .iter()
793                .filter(|entity| entity.0.uid().entity_type() == request.0.0.resource_type())
794                .filter(|entity| {
795                    #[expect(
796                        clippy::unwrap_used, reason = "`to_request` cannot panic because we do not pass a schema. However, the correctness of the authorization
797                        decision depends on having valid a request and entities, but we do not do any validation here. Entities were already validated by
798                        `PartialEntities::from_concrete`. The request was _mostly_ validated by its constructor, but the concrete request could still be invalid
799                        if the resource entity is an enum entity and the id is not an instance of that enum. This cannot happen here because we draw candidate
800                        resources from the entities, which we know are valid."
801                    )]
802                    let req = request.to_request(entity.uid().id().clone(), None).unwrap();
803                    let authorizer = Authorizer::new();
804                    let auth_response = authorizer
805                        .is_authorized(
806                            &req,
807                            &policies,
808                            entities,
809                        );
810                    auth_response.decision() == Decision::Allow
811                })
812                .map(Entity::uid)
813                .collect_vec()
814                .into_iter()),
815        }
816    }
817
818    /// Perform a permission query on the principal
819    #[doc = include_str!("../../experimental_warning.md")]
820    pub fn query_principal(
821        &self,
822        request: &PrincipalQueryRequest,
823        entities: &Entities,
824        schema: &Schema,
825    ) -> Result<impl Iterator<Item = EntityUid>, PermissionQueryError> {
826        let partial_entities = PartialEntities::from_concrete(entities.clone(), schema)?;
827        let tpe_response = self.tpe(&request.0, &partial_entities, schema)?;
828        let policies = tpe_response.policy_set();
829        match tpe_response.decision() {
830            Some(Decision::Allow) => Ok(entities
831                .iter()
832                .filter(|entity| entity.0.uid().entity_type() == request.0.0.principal_type())
833                .map(Entity::uid)
834                .collect_vec()
835                .into_iter()),
836            Some(Decision::Deny) => Ok(vec![].into_iter()),
837            None => Ok(entities
838                .iter()
839                .filter(|entity| entity.0.uid().entity_type() == request.0.0.principal_type())
840                .filter(|entity| {
841                    #[expect(
842                        clippy::unwrap_used, reason = "`to_request` cannot panic because we do not pass a schema. However, the correctness of the authorization
843                        decision depends on having valid a request and entities, but we do not do any validation here. Entities were already validated by
844                        `PartialEntities::from_concrete`. The request was _mostly_ validated by its constructor, but the concrete request could still be invalid
845                        if the principal entity is an enum entity and the id is not an instance of that enum. This cannot happen here because we draw candidate
846                        principals from the entities, which we know are valid."
847                    )]
848                    let req = request.to_request(entity.uid().id().clone(), None).unwrap();
849                    let authorizer = Authorizer::new();
850                    let auth_response = authorizer
851                        .is_authorized(
852                            &req,
853                            &policies,
854                            entities,
855                        );
856                    auth_response.decision() == Decision::Allow
857                })
858                .map(Entity::uid)
859                .collect_vec()
860                .into_iter()),
861        }
862    }
863
864    /// Given a [`ActionQueryRequest`] (a partial request without a concrete
865    /// action) enumerate actions in the schema which might be authorized
866    /// for that request.
867    ///
868    /// Each action is returned with a partial authorization decision.  If
869    /// the action is definitely authorized, then it is `Some(Decision::Allow)`.
870    /// If we did not reach a concrete authorization decision, then it is
871    /// `None`. Actions which are definitely not authorized (i.e., the
872    /// decision is `Some(Decision::Deny)`) are not returned by this
873    /// function. It is also possible that some actions without a concrete
874    /// authorization decision are never authorized if the residual
875    /// expressions after partial evaluation are not satisfiable.
876    ///
877    /// If the partial request for a particular action is invalid (e.g., the
878    /// action does not apply to the type of principal and resource), then
879    /// that action is not included in the result regardless of whether a
880    /// request with that action would be authorized.
881    ///
882    /// ```
883    /// # use cedar_policy::{PolicySet, Schema, ActionQueryRequest, PartialEntities, PartialEntityUid, Decision, EntityUid, Entities};
884    /// # use std::str::FromStr;
885    /// # let policies = PolicySet::from_str(r#"
886    /// #     permit(principal, action == Action::"edit", resource) when { context.should_allow };
887    /// #     permit(principal, action == Action::"view", resource);
888    /// # "#).unwrap();
889    /// # let schema = Schema::from_str("
890    /// #     entity User, Photo;
891    /// #     action view, edit appliesTo {
892    /// #       principal: User,
893    /// #       resource: Photo,
894    /// #       context: { should_allow: Bool, }
895    /// #     };
896    /// # ").unwrap();
897    /// # let entities = PartialEntities::empty();
898    ///
899    /// // Construct a request for a concrete principal and resource, but leaving the context unknown so
900    /// // that we can see all actions that might be authorized for some context.
901    /// let request = ActionQueryRequest::new(
902    ///     PartialEntityUid::from_concrete(r#"User::"alice""#.parse().unwrap()),
903    ///     PartialEntityUid::from_concrete(r#"Photo::"vacation.jpg""#.parse().unwrap()),
904    ///     None,
905    ///     schema,
906    /// ).unwrap();
907    ///
908    /// // All actions which might be allowed for this principal and resource.
909    /// // The exact authorization result may depend on currently unknown
910    /// // context and entity data.
911    /// let possibly_allowed_actions: Vec<&EntityUid> =
912    ///     policies.query_action(&request, &entities)
913    ///             .unwrap()
914    ///             .map(|(a, _)| a)
915    ///             .collect();
916    /// # let mut possibly_allowed_actions = possibly_allowed_actions;
917    /// # possibly_allowed_actions.sort();
918    /// # assert_eq!(&possibly_allowed_actions, &[&r#"Action::"edit""#.parse().unwrap(), &r#"Action::"view""#.parse().unwrap()]);
919    ///
920    /// // These actions are definitely allowed for this principal and resource.
921    /// // These will be allowed for _any_ context.
922    /// let allowed_actions: Vec<&EntityUid> =
923    ///     policies.query_action(&request, &entities).unwrap()
924    ///             .filter(|(_, resp)| resp == &Some(Decision::Allow))
925    ///             .map(|(a, _)| a)
926    ///             .collect();
927    /// # assert_eq!(&allowed_actions, &[&r#"Action::"view""#.parse().unwrap()]);
928    /// ```
929    #[doc = include_str!("../../experimental_warning.md")]
930    pub fn query_action<'a>(
931        &self,
932        request: &'a ActionQueryRequest,
933        entities: &PartialEntities,
934    ) -> Result<impl Iterator<Item = (&'a EntityUid, Option<Decision>)>, PermissionQueryError> {
935        let mut authorized_actions = Vec::new();
936        // We only consider actions that apply to the type of the requested
937        // principal and resource. Any requests for different actions would
938        // be invalid, so they should never be authorized. Not however that
939        // an authorization request for _could_ return `Allow` if the caller
940        // ignores the request validation error.
941        for action in request
942            .schema
943            .0
944            .actions_for_principal_and_resource(&request.principal.0.ty, &request.resource.0.ty)
945        {
946            // If we fail to construct a partial request, then the partial context is not valid for
947            // the context type declared for this action. This action should never be authorized,
948            // but with the same caveats about invalid requests.
949            if let Ok(partial_request) = request.partial_request(action.clone().into()) {
950                let decision = self
951                    .tpe(&partial_request, entities, &request.schema)?
952                    .decision();
953                if decision != Some(Decision::Deny) {
954                    authorized_actions.push((RefCast::ref_cast(action), decision));
955                }
956            }
957        }
958        Ok(authorized_actions.into_iter())
959    }
960}
961
962#[cfg(test)]
963mod tpe_tests {
964    use std::{
965        collections::{BTreeMap, HashSet},
966        str::FromStr,
967    };
968
969    use cedar_policy_core::tpe::err::EntitiesError;
970    use cool_asserts::assert_matches;
971
972    use crate::{PartialEntity, PartialEntityError, RestrictedExpression, Schema};
973
974    #[test]
975    fn entity_construction() {
976        let schema = Schema::from_str(
977            r"
978            entity A in B tags Long;
979            entity B;
980        ",
981        )
982        .unwrap();
983        PartialEntity::new(
984            r#"A::"foo""#.parse().unwrap(),
985            None,
986            Some(HashSet::from_iter([r#"B::"b""#.parse().unwrap()])),
987            Some(BTreeMap::from_iter([(
988                "".into(),
989                RestrictedExpression::new_long(1),
990            )])),
991            &schema,
992        )
993        .unwrap();
994        assert_matches!(
995            PartialEntity::new(
996                r#"A::"foo""#.parse().unwrap(),
997                None,
998                Some(HashSet::from_iter([r#"C::"c""#.parse().unwrap()])),
999                Some(BTreeMap::from_iter([(
1000                    "".into(),
1001                    RestrictedExpression::new_long(1)
1002                )])),
1003                &schema
1004            ),
1005            Err(PartialEntityError::Entities(EntitiesError::Validation(_)))
1006        );
1007
1008        assert_matches!(
1009            PartialEntity::new(
1010                r#"A::"foo""#.parse().unwrap(),
1011                None,
1012                Some(HashSet::from_iter([r#"B::"b""#.parse().unwrap()])),
1013                Some(BTreeMap::from_iter([(
1014                    "".into(),
1015                    RestrictedExpression::new_bool(true)
1016                )])),
1017                &schema
1018            ),
1019            Err(PartialEntityError::Entities(EntitiesError::Validation(_)))
1020        );
1021    }
1022
1023    mod streaming_service {
1024        use std::{collections::BTreeMap, str::FromStr};
1025
1026        use cedar_policy_core::{authorizer::Decision, tpe::err::EntitiesError};
1027        use cool_asserts::assert_matches;
1028        use itertools::Itertools;
1029        use similar_asserts::assert_eq;
1030
1031        use crate::{
1032            ActionConstraint, ActionQueryRequest, Context, Entities, EntityId, EntityUid,
1033            PartialEntities, PartialEntity, PartialEntityError, PartialEntityUid, PartialRequest,
1034            PolicySet, PrincipalConstraint, PrincipalQueryRequest, Request, ResourceConstraint,
1035            ResourceQueryRequest, RestrictedExpression, Schema,
1036        };
1037
1038        #[test]
1039        fn entities_construction() {
1040            let schema = schema();
1041            PartialEntity::new(
1042                r#"Movie::"foo""#.parse().unwrap(),
1043                None,
1044                None,
1045                None,
1046                &schema,
1047            )
1048            .unwrap();
1049            PartialEntity::new(
1050                r#"Show::"foo""#.parse().unwrap(),
1051                Some(BTreeMap::from_iter([
1052                    ("isFree".into(), RestrictedExpression::new_bool(true)),
1053                    (
1054                        "releaseDate".into(),
1055                        RestrictedExpression::new_datetime("2025-01-01"),
1056                    ),
1057                    (
1058                        "isEarlyAccess".into(),
1059                        RestrictedExpression::new_bool(false),
1060                    ),
1061                ])),
1062                None,
1063                None,
1064                &schema,
1065            )
1066            .unwrap();
1067
1068            assert_matches!(
1069                PartialEntity::new(
1070                    r#"Show::"foo""#.parse().unwrap(),
1071                    Some(BTreeMap::from_iter([
1072                        ("isFree".into(), RestrictedExpression::new_bool(true)),
1073                        (
1074                            "isEarlyAccess".into(),
1075                            RestrictedExpression::new_bool(false)
1076                        ),
1077                    ])),
1078                    None,
1079                    None,
1080                    &schema
1081                ),
1082                Err(PartialEntityError::Entities(EntitiesError::Validation(_)))
1083            );
1084
1085            let e1 = PartialEntity::new(
1086                r#"Show::"foo""#.parse().unwrap(),
1087                Some(BTreeMap::from_iter([
1088                    ("isFree".into(), RestrictedExpression::new_bool(true)),
1089                    (
1090                        "releaseDate".into(),
1091                        RestrictedExpression::new_datetime("2025-01-01"),
1092                    ),
1093                    (
1094                        "isEarlyAccess".into(),
1095                        RestrictedExpression::new_bool(false),
1096                    ),
1097                ])),
1098                None,
1099                None,
1100                &schema,
1101            )
1102            .unwrap();
1103            let e2 = PartialEntity::new(
1104                r#"Subscriber::"a""#.parse().unwrap(),
1105                None,
1106                None,
1107                None,
1108                &schema,
1109            )
1110            .unwrap();
1111            PartialEntities::from_partial_entities([e1.clone(), e2.clone()], &schema).unwrap();
1112            let e3 = PartialEntity::new(
1113                r#"Show::"foo""#.parse().unwrap(),
1114                Some(BTreeMap::from_iter([
1115                    ("isFree".into(), RestrictedExpression::new_bool(true)),
1116                    (
1117                        "releaseDate".into(),
1118                        RestrictedExpression::new_datetime("2025-01-01"),
1119                    ),
1120                    ("isEarlyAccess".into(), RestrictedExpression::new_bool(true)),
1121                ])),
1122                None,
1123                None,
1124                &schema,
1125            )
1126            .unwrap();
1127            assert_matches!(
1128                PartialEntities::from_partial_entities([e1, e2, e3], &schema),
1129                Err(EntitiesError::Duplicate(_)),
1130            );
1131        }
1132
1133        #[track_caller]
1134        fn schema() -> Schema {
1135            Schema::from_cedarschema_str(
1136                r"
1137            // Types
1138type Subscription = {
1139  tier: String
1140};
1141type Profile = {
1142  isKid: Bool
1143};
1144
1145// Entities
1146entity FreeMember;
1147entity Subscriber = {
1148  subscription: Subscription,
1149  profile: Profile
1150};
1151entity Movie = {
1152  isFree: Bool,
1153  needsRentOrBuy: Bool,
1154  isOscarNominated: Bool
1155};
1156entity Show = {
1157  isFree: Bool,
1158  releaseDate: datetime,
1159  isEarlyAccess: Bool
1160};
1161
1162// Actions for content in general
1163action watch
1164  appliesTo {
1165    principal: [FreeMember, Subscriber],
1166    resource: [Movie, Show],
1167    context: {
1168      now: {
1169        datetime: datetime,
1170        localTimeOffset: duration
1171      }
1172    }
1173  };
1174
1175// Actions for movies only
1176action rent, buy
1177  appliesTo {
1178    principal: [FreeMember, Subscriber],
1179    resource: Movie,
1180    context: {
1181      now: {
1182        datetime: datetime
1183      }
1184    }
1185  };
1186            ",
1187            )
1188            .unwrap()
1189            .0
1190        }
1191
1192        #[track_caller]
1193        fn policy_set() -> PolicySet {
1194            PolicySet::from_str(
1195                r#"
1196            // Subscriber Content Access (Shows)
1197@id("subscriber-content-access/show")
1198permit (
1199  principal is Subscriber,
1200  action == Action::"watch",
1201  resource is Show
1202)
1203unless
1204{ resource.isEarlyAccess && context.now.datetime < resource.releaseDate };
1205
1206// Subscriber Content Access (Movies)
1207@id("subscriber-content-access/movie")
1208permit (
1209  principal is Subscriber,
1210  action == Action::"watch",
1211  resource is Movie
1212)
1213unless { resource.needsRentOrBuy };
1214
1215// Free Content Access
1216@id("free-content-access")
1217permit (
1218  principal is FreeMember,
1219  action == Action::"watch",
1220  resource
1221)
1222when { resource.isFree };
1223
1224// Promo: Rent/Buy Oscar-Nominated Movies Until the Oscars
1225@id("rent-buy-oscar-movie")
1226permit (
1227  principal is Subscriber,
1228  action in [Action::"rent", Action::"buy"],
1229  resource is Movie
1230)
1231when
1232{
1233  resource.isOscarNominated &&
1234  context.now.datetime >= datetime("2025-02-02T19:00:00-0500") &&
1235  context.now.datetime < datetime(
1236      "2025-03-02T19:00:00-0500"
1237    ) // Oscars Night
1238};
1239
1240// Early Access (24h) to Shows for Premium Subscribers
1241@id("early-access-show")
1242permit (
1243  principal is Subscriber,
1244  action == Action::"watch",
1245  resource is Show
1246)
1247when
1248{
1249  resource.isEarlyAccess &&
1250  principal.subscription.tier == "premium" &&
1251  context.now.datetime >= resource.releaseDate.offset(duration("-24h"))
1252};
1253
1254// Forbid Bedtime Access to Kid Profile
1255@id("forbid-bedtime-watch-kid-profile")
1256forbid (
1257  principal is Subscriber,
1258  action == Action::"watch",
1259  resource
1260)
1261when { principal.profile.isKid }
1262unless
1263{
1264  // `toTime()` returns the duration modulo one day (i.e., it ignores the "date"
1265  // component). Here, we use it to calculate the subscriber's local time and
1266  // compare the result against durations that represent 6:00AM and 9:00PM.
1267  duration("6h") <= context.now
1268    .datetime
1269    .offset
1270    (
1271      context.now.localTimeOffset
1272    )
1273    .toTime
1274    (
1275    ) &&
1276  context.now.datetime.offset(context.now.localTimeOffset).toTime() <= duration(
1277      "21h"
1278    )
1279};
1280            "#,
1281            )
1282            .unwrap()
1283        }
1284
1285        #[track_caller]
1286        fn entities() -> Entities {
1287            Entities::from_json_value(
1288                serde_json::json!(
1289                                [
1290                    {
1291                        "uid": {
1292                            "type": "Subscriber",
1293                            "id": "Alice"
1294                        },
1295                        "attrs": {
1296                            "subscription" : {
1297                                "tier": "standard"
1298                            },
1299                            "profile" : {
1300                                "isKid": false
1301                            }
1302                        },
1303                        "parents": []
1304                    },
1305                    {
1306                        "uid": {
1307                            "type": "FreeMember",
1308                            "id": "Bob"
1309                        },
1310                        "attrs": {},
1311                        "parents": []
1312                    },
1313                    {
1314                        "uid": {
1315                            "type": "Subscriber",
1316                            "id": "Charlie"
1317                        },
1318                        "attrs": {
1319                            "subscription" : {
1320                                "tier": "premium"
1321                            },
1322                            "profile" : {
1323                                "isKid": false
1324                            }
1325                        },
1326                        "parents": []
1327                    },
1328                    {
1329                        "uid": {
1330                            "type": "Subscriber",
1331                            "id": "Dave"
1332                        },
1333                        "attrs": {
1334                            "subscription" : {
1335                                "tier": "standard"
1336                            },
1337                            "profile" : {
1338                                "isKid": true
1339                            }
1340                        },
1341                        "parents": []
1342                    },
1343                    {
1344                        "uid": {
1345                            "type": "Movie",
1346                            "id": "The Godparent"
1347                        },
1348                        "attrs": {
1349                            "isFree" : true,
1350                            "needsRentOrBuy" : false,
1351                            "isOscarNominated": true
1352                        },
1353                        "parents": []
1354                    },
1355                    {
1356                        "uid": {
1357                            "type": "Movie",
1358                            "id": "The Gleaming"
1359                        },
1360                        "attrs": {
1361                            "isFree" : false,
1362                            "needsRentOrBuy" : false,
1363                            "isOscarNominated": false
1364                        },
1365                        "parents": []
1366                    },
1367                    {
1368                        "uid": {
1369                            "type": "Movie",
1370                            "id": "Devilish"
1371                        },
1372                        "attrs": {
1373                            "isFree" : false,
1374                            "needsRentOrBuy" : true,
1375                            "isOscarNominated": true
1376                        },
1377                        "parents": []
1378                    },
1379                    {
1380                        "uid": {
1381                            "type": "Show",
1382                            "id": "Buddies"
1383                        },
1384                        "attrs": {
1385                            "isFree" : false,
1386                            "releaseDate": "2024-10-10",
1387                            "isEarlyAccess": false
1388                        },
1389                        "parents": []
1390                    },
1391                    {
1392                        "uid": {
1393                            "type": "Show",
1394                            "id": "Breach"
1395                        },
1396                        "attrs": {
1397                            "isFree" : false,
1398                            "releaseDate": "2025-02-21",
1399                            "isEarlyAccess": true
1400                        },
1401                        "parents": []
1402                    }
1403                ]
1404                            ),
1405                Some(&schema()),
1406            )
1407            .unwrap()
1408        }
1409
1410        #[test]
1411        fn run_tpe() {
1412            let schema = schema();
1413            let request = PartialRequest::new(
1414                PartialEntityUid::from_concrete(r#"Subscriber::"Alice""#.parse().unwrap()),
1415                r#"Action::"watch""#.parse().unwrap(),
1416                PartialEntityUid::new("Movie".parse().unwrap(), None),
1417                Some(
1418                    Context::from_pairs([(
1419                        "now".into(),
1420                        RestrictedExpression::new_record([
1421                            (
1422                                "datetime".into(),
1423                                RestrictedExpression::from_str(r#"datetime("2025-07-22")"#)
1424                                    .unwrap(),
1425                            ),
1426                            (
1427                                "localTimeOffset".into(),
1428                                RestrictedExpression::from_str(r#"duration("0h")"#).unwrap(),
1429                            ),
1430                        ])
1431                        .unwrap(),
1432                    )])
1433                    .unwrap(),
1434                ),
1435                &schema,
1436            )
1437            .unwrap();
1438            let policies = policy_set();
1439            let partial_entities = PartialEntities::from_concrete(entities(), &schema).unwrap();
1440
1441            let response = policies
1442                .tpe(&request, &partial_entities, &schema)
1443                .expect("tpe should succeed");
1444
1445            assert_eq!(response.policies().count(), policies.num_of_policies());
1446            for p in response.residual_policies() {
1447                assert_matches!(p.action_constraint(), ActionConstraint::Any);
1448                assert_matches!(p.principal_constraint(), PrincipalConstraint::Any);
1449                assert_matches!(p.resource_constraint(), ResourceConstraint::Any);
1450            }
1451            assert_eq!(
1452                response
1453                    .residual_policies()
1454                    .next()
1455                    .unwrap()
1456                    .annotation("id")
1457                    .unwrap(),
1458                "subscriber-content-access/movie"
1459            );
1460
1461            assert_eq!(response.decision(), None);
1462            assert!(response.reason().is_none());
1463
1464            let request = Request::new(
1465                EntityUid::from_type_name_and_id(
1466                    "Subscriber".parse().unwrap(),
1467                    EntityId::new("Alice"),
1468                ),
1469                r#"Action::"watch""#.parse().unwrap(),
1470                EntityUid::from_type_name_and_id(
1471                    "Movie".parse().unwrap(),
1472                    EntityId::new("The Godparent"),
1473                ),
1474                Context::from_pairs([(
1475                    "now".into(),
1476                    RestrictedExpression::new_record([
1477                        (
1478                            "datetime".into(),
1479                            RestrictedExpression::from_str(r#"datetime("2025-07-22")"#).unwrap(),
1480                        ),
1481                        (
1482                            "localTimeOffset".into(),
1483                            RestrictedExpression::from_str(r#"duration("0h")"#).unwrap(),
1484                        ),
1485                    ])
1486                    .unwrap(),
1487                )])
1488                .unwrap(),
1489                Some(&schema),
1490            )
1491            .unwrap();
1492            assert_matches!(response.reauthorize(&request, &entities()), Ok(res) => {
1493                assert_eq!(res.decision(), Decision::Allow);
1494            });
1495
1496            let request = Request::new(
1497                EntityUid::from_type_name_and_id(
1498                    "Subscriber".parse().unwrap(),
1499                    EntityId::new("Alice"),
1500                ),
1501                r#"Action::"watch""#.parse().unwrap(),
1502                EntityUid::from_type_name_and_id(
1503                    "Movie".parse().unwrap(),
1504                    EntityId::new("Devilish"),
1505                ),
1506                Context::from_pairs([(
1507                    "now".into(),
1508                    RestrictedExpression::new_record([
1509                        (
1510                            "datetime".into(),
1511                            RestrictedExpression::from_str(r#"datetime("2025-07-22")"#).unwrap(),
1512                        ),
1513                        (
1514                            "localTimeOffset".into(),
1515                            RestrictedExpression::from_str(r#"duration("0h")"#).unwrap(),
1516                        ),
1517                    ])
1518                    .unwrap(),
1519                )])
1520                .unwrap(),
1521                Some(&schema),
1522            )
1523            .unwrap();
1524            assert_matches!(response.reauthorize(&request, &entities()), Ok(res) => {
1525                assert_eq!(res.decision(), Decision::Deny);
1526            });
1527        }
1528
1529        #[test]
1530        fn query_resource() {
1531            let schema = schema();
1532            let policies = policy_set();
1533            let request = ResourceQueryRequest::new(
1534                r#"Subscriber::"Alice""#.parse().unwrap(),
1535                r#"Action::"watch""#.parse().unwrap(),
1536                "Movie".parse().unwrap(),
1537                Context::from_pairs([(
1538                    "now".into(),
1539                    RestrictedExpression::new_record([
1540                        (
1541                            "datetime".into(),
1542                            RestrictedExpression::from_str(r#"datetime("2025-07-22")"#).unwrap(),
1543                        ),
1544                        (
1545                            "localTimeOffset".into(),
1546                            RestrictedExpression::from_str(r#"duration("0h")"#).unwrap(),
1547                        ),
1548                    ])
1549                    .unwrap(),
1550                )])
1551                .unwrap(),
1552                &schema,
1553            )
1554            .unwrap();
1555
1556            // The two movies do not need rent or buy and hence satisfy the
1557            // residual policy
1558            let movies = policies
1559                .query_resource(&request, &entities(), &schema)
1560                .unwrap()
1561                .sorted()
1562                .collect_vec();
1563            assert_eq!(
1564                movies,
1565                &[
1566                    EntityUid::from_str(r#"Movie::"The Gleaming""#).unwrap(),
1567                    EntityUid::from_str(r#"Movie::"The Godparent""#).unwrap(),
1568                ]
1569            );
1570        }
1571
1572        #[test]
1573        fn query_principal() {
1574            let schema = schema();
1575            let policies = policy_set();
1576
1577            let request = PrincipalQueryRequest::new(
1578                "Subscriber".parse().unwrap(),
1579                r#"Action::"watch""#.parse().unwrap(),
1580                r#"Movie::"The Godparent""#.parse().unwrap(),
1581                Context::from_pairs([(
1582                    "now".into(),
1583                    RestrictedExpression::new_record([
1584                        (
1585                            "datetime".into(),
1586                            RestrictedExpression::from_str(r#"datetime("2025-07-22")"#).unwrap(),
1587                        ),
1588                        (
1589                            "localTimeOffset".into(),
1590                            RestrictedExpression::from_str(r#"duration("0h")"#).unwrap(),
1591                        ),
1592                    ])
1593                    .unwrap(),
1594                )])
1595                .unwrap(),
1596                &schema,
1597            )
1598            .unwrap();
1599
1600            let subscribers = policies
1601                .query_principal(&request, &entities(), &schema)
1602                .unwrap()
1603                .sorted()
1604                .collect_vec();
1605            assert_eq!(
1606                subscribers,
1607                &[
1608                    EntityUid::from_str(r#"Subscriber::"Alice""#).unwrap(),
1609                    EntityUid::from_str(r#"Subscriber::"Charlie""#).unwrap(),
1610                ]
1611            );
1612        }
1613
1614        #[test]
1615        fn query_action_alice() {
1616            let schema = schema();
1617            let request = ActionQueryRequest::new(
1618                PartialEntityUid::from_concrete(r#"Subscriber::"Alice""#.parse().unwrap()),
1619                PartialEntityUid::from_concrete(r#"Movie::"The Godparent""#.parse().unwrap()),
1620                None,
1621                schema.clone(),
1622            )
1623            .unwrap();
1624
1625            let policies = policy_set();
1626            let mut actions: Vec<_> = policies
1627                .query_action(
1628                    &request,
1629                    &PartialEntities::from_concrete(entities(), &schema).unwrap(),
1630                )
1631                .unwrap()
1632                .collect();
1633            actions.sort_by_key(|(a, _)| *a);
1634            assert_eq!(
1635                actions,
1636                vec![
1637                    (&r#"Action::"buy""#.parse().unwrap(), None),
1638                    (&r#"Action::"rent""#.parse().unwrap(), None),
1639                    (
1640                        &r#"Action::"watch""#.parse().unwrap(),
1641                        Some(Decision::Allow)
1642                    ),
1643                ]
1644            );
1645        }
1646
1647        #[test]
1648        fn query_action_bob_free() {
1649            let schema = schema();
1650            let request = ActionQueryRequest::new(
1651                PartialEntityUid::from_concrete(r#"FreeMember::"Bob""#.parse().unwrap()),
1652                PartialEntityUid::from_concrete(r#"Movie::"The Godparent""#.parse().unwrap()),
1653                None,
1654                schema.clone(),
1655            )
1656            .unwrap();
1657
1658            let policies = policy_set();
1659            let actions: Vec<_> = policies
1660                .query_action(
1661                    &request,
1662                    &PartialEntities::from_concrete(entities(), &schema).unwrap(),
1663                )
1664                .unwrap()
1665                .collect();
1666            assert_eq!(
1667                actions,
1668                vec![(
1669                    &r#"Action::"watch""#.parse().unwrap(),
1670                    Some(Decision::Allow)
1671                ),]
1672            );
1673        }
1674
1675        #[test]
1676        fn query_action_bob_not_free() {
1677            let schema = schema();
1678            let request = ActionQueryRequest::new(
1679                PartialEntityUid::from_concrete(r#"FreeMember::"Bob""#.parse().unwrap()),
1680                PartialEntityUid::from_concrete(r#"Movie::"The Gleaming""#.parse().unwrap()),
1681                None,
1682                schema.clone(),
1683            )
1684            .unwrap();
1685
1686            let policies = policy_set();
1687            let actions: Vec<_> = policies
1688                .query_action(
1689                    &request,
1690                    &PartialEntities::from_concrete(entities(), &schema).unwrap(),
1691                )
1692                .unwrap()
1693                .collect();
1694            assert_eq!(actions, vec![]);
1695        }
1696    }
1697
1698    mod github {
1699        use std::{
1700            collections::{HashMap, HashSet},
1701            str::FromStr,
1702        };
1703
1704        use cedar_policy_core::tpe::err::TpeError;
1705        use cedar_policy_core::{authorizer::Decision, batched_evaluator::err::BatchedEvalError};
1706        use cool_asserts::assert_matches;
1707        use itertools::Itertools;
1708        use similar_asserts::assert_eq;
1709
1710        use crate::{
1711            ActionQueryRequest, Context, Entities, EntityUid, PartialEntities, PartialEntityUid,
1712            PolicySet, PrincipalQueryRequest, Request, ResourceQueryRequest, RestrictedExpression,
1713            Schema, TestEntityLoader,
1714        };
1715
1716        #[track_caller]
1717        fn schema() -> Schema {
1718            Schema::from_str(
1719                r#"
1720            entity Team, UserGroup in [UserGroup];
1721entity Issue  = {
1722  "repo": Repository,
1723  "reporter": User,
1724};
1725entity Org  = {
1726  "members": UserGroup,
1727  "owners": UserGroup,
1728};
1729entity Repository  = {
1730  "admins": UserGroup,
1731  "maintainers": UserGroup,
1732  "readers": UserGroup,
1733  "triagers": UserGroup,
1734  "writers": UserGroup,
1735};
1736entity User in [UserGroup, Team];
1737
1738action push, pull, fork appliesTo {
1739  principal: [User],
1740  resource: [Repository]
1741};
1742action assign_issue, delete_issue, edit_issue appliesTo {
1743  principal: [User],
1744  resource: [Issue]
1745};
1746action add_reader, add_writer, add_maintainer, add_admin, add_triager appliesTo {
1747  principal: [User],
1748  resource: [Repository]
1749};
1750            "#,
1751            )
1752            .unwrap()
1753        }
1754
1755        fn policy_set() -> PolicySet {
1756            PolicySet::from_str(
1757                r#"
1758                //Actions for readers
1759permit (
1760  principal,
1761  action == Action::"pull",
1762  resource
1763)
1764when { principal in resource.readers };
1765
1766permit (
1767  principal,
1768  action == Action::"fork",
1769  resource
1770)
1771when { principal in resource.readers };
1772
1773permit (
1774  principal,
1775  action == Action::"delete_issue",
1776  resource
1777)
1778when { principal in resource.repo.readers && principal == resource.reporter };
1779
1780permit (
1781  principal,
1782  action == Action::"edit_issue",
1783  resource
1784)
1785when { principal in resource.repo.readers && principal == resource.reporter };
1786
1787//Actions for triagers
1788permit (
1789  principal,
1790  action == Action::"assign_issue",
1791  resource
1792)
1793when { principal in resource.repo.triagers };
1794
1795//Actions for writers
1796permit (
1797  principal,
1798  action == Action::"push",
1799  resource
1800)
1801when { principal in resource.writers };
1802
1803permit (
1804  principal,
1805  action == Action::"edit_issue",
1806  resource
1807)
1808when { principal in resource.repo.writers };
1809
1810//Actions for maintainers
1811permit (
1812  principal,
1813  action == Action::"delete_issue",
1814  resource
1815)
1816when { principal in resource.repo.maintainers };
1817
1818//Actions for admins
1819permit (
1820  principal,
1821  action in
1822    [Action::"add_reader",
1823     Action::"add_triager",
1824     Action::"add_writer",
1825     Action::"add_maintainer",
1826     Action::"add_admin"],
1827  resource
1828)
1829when { principal in resource.admins };
1830//We use the same permissions for org owners, and rely on placing them in the admins group for every repository in the org
1831//The other option is to duplicate all policies for the org base permissions (with a separate heirarchy for each org)
1832"#,
1833            )
1834            .unwrap()
1835        }
1836
1837        #[track_caller]
1838        fn entities() -> Entities {
1839            Entities::from_json_value(serde_json::json!(
1840
1841                [
1842    {
1843      "uid": { "__entity": { "type": "User", "id": "alice"} },
1844      "attrs": {},
1845      "parents": [{ "__entity": { "type": "UserGroup", "id": "common_knowledge_writers"} }, { "__entity": { "type": "UserGroup", "id": "uncommon_knowledge_writers"} } ]
1846    },
1847    {
1848      "uid": { "__entity": { "type": "User", "id": "jane"} },
1849      "attrs": {},
1850      "parents": [{ "__entity": { "type": "UserGroup", "id": "common_knowledge_maintainers"} },  { "__entity": { "type": "Team", "id": "team_that_can_read_everything"} }]
1851    },
1852    {
1853        "uid": { "__entity": { "type": "User", "id": "bob"} },
1854        "attrs": {},
1855        "parents": []
1856    },
1857    {
1858        "uid": { "__entity": { "type": "Repository", "id": "common_knowledge"} },
1859        "attrs": {
1860            "readers" : { "__entity": { "type": "UserGroup", "id": "common_knowledge_readers"} },
1861            "triagers" : { "__entity": { "type": "UserGroup", "id": "common_knowledge_triagers"} },
1862            "writers" : { "__entity": { "type": "UserGroup", "id": "common_knowledge_writers"} },
1863            "maintainers" : { "__entity": { "type": "UserGroup", "id": "common_knowledge_maintainers"} },
1864            "admins" : { "__entity": { "type": "UserGroup", "id": "common_knowledge_admins"} }
1865        },
1866        "parents": []
1867    },
1868    {
1869        "uid": { "__entity": { "type": "UserGroup", "id": "common_knowledge_readers"} },
1870        "attrs": {
1871        },
1872        "parents": [  ]
1873    },
1874    {
1875        "uid": { "__entity": { "type": "UserGroup", "id": "common_knowledge_triagers"} },
1876        "attrs": {
1877        },
1878        "parents": [ { "__entity": { "type": "UserGroup", "id": "common_knowledge_readers"} } ]
1879    },
1880    {
1881        "uid": { "__entity": { "type": "UserGroup", "id": "common_knowledge_writers"} },
1882        "attrs": {
1883        },
1884        "parents": [ {"__entity": { "type": "UserGroup", "id": "common_knowledge_triagers"}} ]
1885    },
1886    {
1887        "uid": { "__entity": { "type": "UserGroup", "id": "common_knowledge_maintainers"} },
1888        "attrs": {
1889        },
1890        "parents": [ {"__entity": { "type": "UserGroup", "id": "common_knowledge_writers"}} ]
1891    },
1892    {
1893        "uid": { "__entity": { "type": "UserGroup", "id": "common_knowledge_admins"} },
1894        "attrs": {
1895        },
1896        "parents": [ {"__entity": { "type": "UserGroup", "id": "common_knowledge_maintainers"}} ]
1897    },
1898    {
1899        "uid": { "__entity": { "type": "Repository", "id": "secret"} },
1900        "attrs": {
1901            "readers" : { "__entity": { "type": "UserGroup", "id": "secret_readers"} },
1902            "triagers" : { "__entity": { "type": "UserGroup", "id": "secret_triagers"} },
1903            "writers" : { "__entity": { "type": "UserGroup", "id": "secret_writers"} },
1904            "maintainers" : { "__entity": { "type": "UserGroup", "id": "secret_maintainers"} },
1905            "admins" : { "__entity": { "type": "UserGroup", "id": "secret_admins"} }
1906        },
1907        "parents": []
1908    },
1909    {
1910        "uid": { "__entity": { "type": "UserGroup", "id": "secret_readers"} },
1911        "attrs": {
1912        },
1913        "parents": [  ]
1914    },
1915    {
1916        "uid": { "__entity": { "type": "UserGroup", "id": "secret_triagers"} },
1917        "attrs": {
1918        },
1919        "parents": [ { "__entity": { "type": "UserGroup", "id": "secret_readers"} } ]
1920    },
1921    {
1922        "uid": { "__entity": { "type": "UserGroup", "id": "secret_writers"} },
1923        "attrs": {
1924        },
1925        "parents": [ {"__entity": { "type": "UserGroup", "id": "secret_triagers"}} ]
1926    },
1927    {
1928        "uid": { "__entity": { "type": "UserGroup", "id": "secret_maintainers"} },
1929        "attrs": {
1930        },
1931        "parents": [ {"__entity": { "type": "UserGroup", "id": "secret_writers"}} ]
1932    },
1933    {
1934        "uid": { "__entity": { "type": "UserGroup", "id": "secret_admins"} },
1935        "attrs": {
1936        },
1937        "parents": [ {"__entity": { "type": "UserGroup", "id": "secret_maintainers"}} ]
1938    },
1939    {
1940        "uid": { "__entity": { "type": "Repository", "id": "uncommon_knowledge"} },
1941        "attrs": {
1942            "readers" : { "__entity": { "type": "UserGroup", "id": "uncommon_knowledge_readers"} },
1943            "triagers" : { "__entity": { "type": "UserGroup", "id": "uncommon_knowledge_triagers"} },
1944            "writers" : { "__entity": { "type": "UserGroup", "id": "uncommon_knowledge_writers"} },
1945            "maintainers" : { "__entity": { "type": "UserGroup", "id": "uncommon_knowledge_maintainers"} },
1946            "admins" : { "__entity": { "type": "UserGroup", "id": "uncommon_knowledge_admins"} }
1947        },
1948        "parents": []
1949    },
1950    {
1951        "uid": { "__entity": { "type": "UserGroup", "id": "uncommon_knowledge_readers"} },
1952        "attrs": {
1953        },
1954        "parents": [  ]
1955    },
1956    {
1957        "uid": { "__entity": { "type": "UserGroup", "id": "uncommon_knowledge_triagers"} },
1958        "attrs": {
1959        },
1960        "parents": [ { "__entity": { "type": "UserGroup", "id": "uncommon_knowledge_readers"} } ]
1961    },
1962    {
1963        "uid": { "__entity": { "type": "UserGroup", "id": "uncommon_knowledge_writers"} },
1964        "attrs": {
1965        },
1966        "parents": [ {"__entity": { "type": "UserGroup", "id": "uncommon_knowledge_triagers"}} ]
1967    },
1968    {
1969        "uid": { "__entity": { "type": "UserGroup", "id": "uncommon_knowledge_maintainers"} },
1970        "attrs": {
1971        },
1972        "parents": [ {"__entity": { "type": "UserGroup", "id": "uncommon_knowledge_writers"}} ]
1973    },
1974    {
1975        "uid": { "__entity": { "type": "UserGroup", "id": "uncommon_knowledge_admins"} },
1976        "attrs": {
1977        },
1978        "parents": [ {"__entity": { "type": "UserGroup", "id": "uncommon_knowledge_maintainers"}} ]
1979    },
1980    {
1981        "uid": { "__entity": { "type": "Team", "id": "team_that_can_read_everything"} },
1982        "attrs": {},
1983        "parents": [{ "__entity": { "type": "UserGroup", "id": "common_knowledge_readers"} }, { "__entity": { "type": "UserGroup", "id": "secret_readers"} }, { "__entity": { "type": "UserGroup", "id": "uncommon_knowledge_readers"} }]
1984    },
1985]
1986            ), Some(&schema())).unwrap()
1987        }
1988
1989        #[test]
1990        fn query_resource() {
1991            let schema = schema();
1992            let request = ResourceQueryRequest::new(
1993                r#"User::"jane""#.parse().unwrap(),
1994                r#"Action::"push""#.parse().unwrap(),
1995                "Repository".parse().unwrap(),
1996                Context::empty(),
1997                &schema,
1998            )
1999            .unwrap();
2000            let policies = policy_set();
2001            assert_matches!(&policies.query_resource(&request, &entities(), &schema).unwrap().collect_vec(), [uid] => {
2002                assert_eq!(uid, &r#"Repository::"common_knowledge""#.parse().unwrap());
2003            });
2004        }
2005
2006        #[test]
2007        fn query_principal() {
2008            let schema = schema();
2009            let request = PrincipalQueryRequest::new(
2010                r"User".parse().unwrap(),
2011                r#"Action::"pull""#.parse().unwrap(),
2012                r#"Repository::"secret""#.parse().unwrap(),
2013                Context::empty(),
2014                &schema,
2015            )
2016            .unwrap();
2017            let policies = policy_set();
2018            assert_matches!(&policies.query_principal(&request, &entities(), &schema).unwrap().collect_vec(), [uid] => {
2019                assert_eq!(uid, &r#"User::"jane""#.parse().unwrap());
2020            });
2021        }
2022
2023        #[test]
2024        fn query_action() {
2025            let schema = schema();
2026            let request = ActionQueryRequest::new(
2027                PartialEntityUid::from_concrete(r#"User::"jane""#.parse().unwrap()),
2028                PartialEntityUid::from_concrete(r#"Repository::"secret""#.parse().unwrap()),
2029                None,
2030                schema.clone(),
2031            )
2032            .unwrap();
2033
2034            let policies = policy_set();
2035            let mut actions: Vec<_> = policies
2036                .query_action(
2037                    &request,
2038                    &PartialEntities::from_concrete(entities(), &schema).unwrap(),
2039                )
2040                .unwrap()
2041                .collect();
2042            actions.sort_by_key(|(a, _)| *a);
2043            assert_eq!(
2044                actions,
2045                vec![
2046                    (&r#"Action::"fork""#.parse().unwrap(), Some(Decision::Allow)),
2047                    (&r#"Action::"pull""#.parse().unwrap(), Some(Decision::Allow)),
2048                ]
2049            );
2050        }
2051
2052        #[test]
2053        fn test_is_authorized_vs_is_authorized_batched() {
2054            use crate::{Authorizer, Request};
2055
2056            let schema = schema();
2057            let policies = policy_set();
2058            let entities = entities();
2059            let authorizer = Authorizer::new();
2060
2061            // Create a set of test requests
2062            let test_requests = vec![
2063                // Request 1: alice can push to common_knowledge (should be allowed)
2064                Request::new(
2065                    r#"User::"alice""#.parse().unwrap(),
2066                    r#"Action::"push""#.parse().unwrap(),
2067                    r#"Repository::"common_knowledge""#.parse().unwrap(),
2068                    Context::empty(),
2069                    Some(&schema),
2070                )
2071                .unwrap(),
2072                // Request 2: jane can pull from secret (should be allowed)
2073                Request::new(
2074                    r#"User::"jane""#.parse().unwrap(),
2075                    r#"Action::"pull""#.parse().unwrap(),
2076                    r#"Repository::"secret""#.parse().unwrap(),
2077                    Context::empty(),
2078                    Some(&schema),
2079                )
2080                .unwrap(),
2081                // Request 3: bob cannot push to common_knowledge (should be denied)
2082                Request::new(
2083                    r#"User::"bob""#.parse().unwrap(),
2084                    r#"Action::"push""#.parse().unwrap(),
2085                    r#"Repository::"common_knowledge""#.parse().unwrap(),
2086                    Context::empty(),
2087                    Some(&schema),
2088                )
2089                .unwrap(),
2090                // Request 4: alice can fork common_knowledge (should be allowed)
2091                Request::new(
2092                    r#"User::"alice""#.parse().unwrap(),
2093                    r#"Action::"fork""#.parse().unwrap(),
2094                    r#"Repository::"common_knowledge""#.parse().unwrap(),
2095                    Context::empty(),
2096                    Some(&schema),
2097                )
2098                .unwrap(),
2099            ];
2100
2101            // Test each request with both methods and compare results
2102            for (i, request) in test_requests.iter().enumerate() {
2103                // Get result from is_authorized
2104                let standard_response = authorizer.is_authorized(request, &policies, &entities);
2105
2106                // Get result from is_authorized_batched (if TPE feature is enabled)
2107                let mut loader = TestEntityLoader::new(&entities);
2108                let batched_decision = policies
2109                    .is_authorized_batched(request, &schema, &mut loader, u32::MAX)
2110                    .unwrap();
2111
2112                // Compare decisions - they should be the same
2113                let standard_decision = standard_response.decision();
2114
2115                assert_eq!(
2116                        standard_decision,
2117                        batched_decision,
2118                        "Request {}: is_authorized returned {:?} but is_authorized_batched returned {:?}",
2119                        i + 1,
2120                        standard_decision,
2121                        batched_decision
2122                    );
2123            }
2124        }
2125
2126        #[test]
2127        fn test_batched_evaluation_error_validation() {
2128            let schema = schema();
2129            let policies = PolicySet::from_str(
2130                    r#"permit(principal, action, resource) when { principal.nonexistent_attr == "value" };"#
2131                ).unwrap();
2132
2133            let request = Request::new(
2134                EntityUid::from_str("User::\"alice\"").unwrap(),
2135                EntityUid::from_str("Action::\"push\"").unwrap(),
2136                EntityUid::from_str("Repository::\"repo\"").unwrap(),
2137                Context::empty(),
2138                Some(&schema),
2139            )
2140            .unwrap();
2141
2142            let entities = entities();
2143            let mut loader = TestEntityLoader::new(&entities);
2144            let result = policies.is_authorized_batched(&request, &schema, &mut loader, 10);
2145
2146            assert!(matches!(
2147                result,
2148                Err(BatchedEvalError::TPE(TpeError::Validation(_)))
2149            ));
2150        }
2151
2152        #[test]
2153        #[cfg(feature = "partial-eval")]
2154        fn test_batched_evaluation_error_partial_request() {
2155            let context_with_unknown = Context::from_pairs([(
2156                "key".to_string(),
2157                RestrictedExpression::new_unknown("test_unknown"),
2158            )])
2159            .unwrap();
2160
2161            let request = Request::new(
2162                EntityUid::from_str("User::\"alice\"").unwrap(),
2163                EntityUid::from_str("Action::\"view\"").unwrap(),
2164                EntityUid::from_str("Resource::\"doc\"").unwrap(),
2165                context_with_unknown,
2166                None,
2167            )
2168            .unwrap();
2169            let schema = schema();
2170
2171            let pset = PolicySet::from_str("permit(principal, action, resource);").unwrap();
2172            let entities = Entities::empty();
2173            let mut loader = TestEntityLoader::new(&entities);
2174            let result = pset.is_authorized_batched(&request, &schema, &mut loader, 10);
2175
2176            assert_matches!(result, Err(BatchedEvalError::PartialRequest(_)));
2177        }
2178
2179        #[test]
2180        fn test_batched_evaluation_error_invalid_entity() {
2181            // Create an entity loader that returns an invalid entity (wrong attribute type)
2182            struct InvalidEntityLoader;
2183            impl crate::EntityLoader for InvalidEntityLoader {
2184                fn load_entities(
2185                    &mut self,
2186                    _uids: &HashSet<EntityUid>,
2187                ) -> HashMap<EntityUid, Option<crate::Entity>> {
2188                    let mut result = HashMap::new();
2189                    let uid = EntityUid::from_strs("Org", "myorg");
2190                    let entity = crate::Entity::new(
2191                        uid.clone(),
2192                        [
2193                            (
2194                                "members".to_string(),
2195                                RestrictedExpression::new_string("not_a_usergroup".to_string()),
2196                            ),
2197                            (
2198                                "owners".to_string(),
2199                                RestrictedExpression::new_entity_uid(EntityUid::from_strs(
2200                                    "UserGroup",
2201                                    "2",
2202                                )),
2203                            ),
2204                        ]
2205                        .into(),
2206                        HashSet::new(),
2207                    )
2208                    .unwrap();
2209                    result.insert(uid, Some(entity));
2210                    result
2211                }
2212            }
2213
2214            let schema = schema();
2215            let pset = PolicySet::from_str(
2216                "permit(principal, action, resource) when { Org::\"myorg\".members == UserGroup::\"1\"};",
2217            )
2218            .unwrap();
2219
2220            let request = Request::new(
2221                r#"User::"alice""#.parse().unwrap(),
2222                r#"Action::"push""#.parse().unwrap(),
2223                r#"Repository::"common_knowledge""#.parse().unwrap(),
2224                Context::empty(),
2225                Some(&schema),
2226            )
2227            .unwrap();
2228
2229            let mut loader = InvalidEntityLoader;
2230            let result = pset.is_authorized_batched(&request, &schema, &mut loader, 10);
2231
2232            assert_matches!(result, Err(BatchedEvalError::Entities(_)));
2233        }
2234
2235        #[test]
2236        #[cfg(feature = "partial-eval")]
2237        fn test_batched_evaluation_error_partial_entity() {
2238            // Create an entity loader that returns a partial entity (contains unknowns)
2239            struct PartialEntityLoader;
2240            impl crate::EntityLoader for PartialEntityLoader {
2241                fn load_entities(
2242                    &mut self,
2243                    _uids: &HashSet<EntityUid>,
2244                ) -> HashMap<EntityUid, Option<crate::Entity>> {
2245                    let mut result = HashMap::new();
2246                    let uid = EntityUid::from_strs("Org", "myorg");
2247                    let entity = crate::Entity::new(
2248                        uid.clone(),
2249                        [
2250                            (
2251                                "members".to_string(),
2252                                RestrictedExpression::new_unknown("partial_members"),
2253                            ),
2254                            (
2255                                "owners".to_string(),
2256                                RestrictedExpression::new_entity_uid(EntityUid::from_strs(
2257                                    "UserGroup",
2258                                    "2",
2259                                )),
2260                            ),
2261                        ]
2262                        .into(),
2263                        HashSet::new(),
2264                    )
2265                    .unwrap();
2266                    result.insert(uid, Some(entity));
2267                    result
2268                }
2269            }
2270
2271            let schema = schema();
2272            let pset = PolicySet::from_str(
2273                "permit(principal, action, resource) when { Org::\"myorg\".members == UserGroup::\"1\"};",
2274            )
2275            .unwrap();
2276
2277            let request = Request::new(
2278                r#"User::"alice""#.parse().unwrap(),
2279                r#"Action::"push""#.parse().unwrap(),
2280                r#"Repository::"common_knowledge""#.parse().unwrap(),
2281                Context::empty(),
2282                Some(&schema),
2283            )
2284            .unwrap();
2285
2286            let mut loader = PartialEntityLoader;
2287            let result = pset.is_authorized_batched(&request, &schema, &mut loader, 10);
2288
2289            assert_matches!(result, Err(BatchedEvalError::PartialValueToValue(_)));
2290        }
2291
2292        #[test]
2293        fn test_batched_evaluation_error_insufficient_iters() {
2294            let schema = schema();
2295            let policies = policy_set();
2296            let entities = entities();
2297
2298            let request = Request::new(
2299                r#"User::"alice""#.parse().unwrap(),
2300                r#"Action::"push""#.parse().unwrap(),
2301                r#"Repository::"common_knowledge""#.parse().unwrap(),
2302                Context::empty(),
2303                Some(&schema),
2304            )
2305            .unwrap();
2306
2307            let mut loader = TestEntityLoader::new(&entities);
2308            let result = policies.is_authorized_batched(&request, &schema, &mut loader, 0);
2309
2310            assert_matches!(result, Err(BatchedEvalError::InsufficientIterations(_)));
2311        }
2312    }
2313
2314    mod trivial {
2315        use cedar_policy_core::authorizer::Decision;
2316        use itertools::Itertools;
2317
2318        use crate::{
2319            Context, Entities, PartialEntities, PartialEntityUid, PartialRequest, PolicyId,
2320            PolicySet, PrincipalQueryRequest, ResourceQueryRequest, Schema,
2321        };
2322        use std::{i64, str::FromStr};
2323
2324        fn schema() -> Schema {
2325            Schema::from_str("entity P, R; action A appliesTo { principal: P, resource: R };")
2326                .unwrap()
2327        }
2328
2329        fn entities() -> Entities {
2330            Entities::from_json_value(
2331                serde_json::json!([
2332                    { "uid": { "__entity": { "type": "P", "id": ""} }, "attrs": {}, "parents": [] },
2333                    { "uid": { "__entity": { "type": "R", "id": ""} }, "attrs": {}, "parents": [] },
2334                ]),
2335                None,
2336            )
2337            .unwrap()
2338        }
2339
2340        #[test]
2341        fn trivial_permit_tpe() {
2342            let schema = schema();
2343            let partial_entities = PartialEntities::from_concrete(entities(), &schema).unwrap();
2344            let req = PartialRequest::new(
2345                PartialEntityUid::new("P".parse().unwrap(), None),
2346                r#"Action::"A""#.parse().unwrap(),
2347                PartialEntityUid::new("R".parse().unwrap(), None),
2348                None,
2349                &schema,
2350            )
2351            .unwrap();
2352            let response = PolicySet::from_str(r"permit(principal, action, resource);")
2353                .unwrap()
2354                .tpe(&req, &partial_entities, &schema)
2355                .unwrap();
2356            assert_eq!(response.decision(), Some(Decision::Allow));
2357            assert_eq!(
2358                response.reason().unwrap().collect::<Vec<_>>(),
2359                vec![&PolicyId::new("policy0")]
2360            );
2361        }
2362
2363        #[test]
2364        fn trivial_permit_query_principal() {
2365            let schema = schema();
2366            let entities = entities();
2367            let req = PrincipalQueryRequest::new(
2368                "P".parse().unwrap(),
2369                r#"Action::"A""#.parse().unwrap(),
2370                r#"R::"""#.parse().unwrap(),
2371                Context::empty(),
2372                &schema,
2373            )
2374            .unwrap();
2375
2376            let principals = PolicySet::from_str(r#"permit(principal, action, resource);"#)
2377                .unwrap()
2378                .query_principal(&req, &entities, &schema)
2379                .unwrap()
2380                .collect_vec();
2381            assert_eq!(&principals, &[r#"P::"""#.parse().unwrap()]);
2382        }
2383
2384        #[test]
2385        fn trivial_permit_query_resource() {
2386            let schema = schema();
2387            let entities = entities();
2388            let req = ResourceQueryRequest::new(
2389                r#"P::"""#.parse().unwrap(),
2390                r#"Action::"A""#.parse().unwrap(),
2391                "R".parse().unwrap(),
2392                Context::empty(),
2393                &schema,
2394            )
2395            .unwrap();
2396
2397            let resources = PolicySet::from_str(r#"permit(principal, action, resource);"#)
2398                .unwrap()
2399                .query_resource(&req, &entities, &schema)
2400                .unwrap()
2401                .collect_vec();
2402            assert_eq!(&resources, &[r#"R::"""#.parse().unwrap()]);
2403        }
2404
2405        #[test]
2406        fn trivial_forbid_tpe() {
2407            let schema = schema();
2408            let partial_entities = PartialEntities::from_concrete(entities(), &schema).unwrap();
2409            let req = PartialRequest::new(
2410                PartialEntityUid::new("P".parse().unwrap(), None),
2411                r#"Action::"A""#.parse().unwrap(),
2412                PartialEntityUid::new("R".parse().unwrap(), None),
2413                None,
2414                &schema,
2415            )
2416            .unwrap();
2417            let response = PolicySet::from_str(r#"forbid(principal, action, resource);"#)
2418                .unwrap()
2419                .tpe(&req, &partial_entities, &schema)
2420                .unwrap();
2421            assert_eq!(response.decision(), Some(Decision::Deny));
2422            assert_eq!(
2423                response.reason().unwrap().collect::<Vec<_>>(),
2424                vec![&PolicyId::new("policy0")]
2425            );
2426            assert_eq!(
2427                response.true_forbids().collect::<Vec<_>>(),
2428                vec![&PolicyId::new("policy0")]
2429            );
2430        }
2431
2432        #[test]
2433        fn trivial_forbid_query_principal() {
2434            let schema = schema();
2435            let entities = entities();
2436            let req = PrincipalQueryRequest::new(
2437                "P".parse().unwrap(),
2438                r#"Action::"A""#.parse().unwrap(),
2439                r#"R::"""#.parse().unwrap(),
2440                Context::empty(),
2441                &schema,
2442            )
2443            .unwrap();
2444
2445            let principals = PolicySet::from_str(r#"forbid(principal, action, resource);"#)
2446                .unwrap()
2447                .query_principal(&req, &entities, &schema)
2448                .unwrap()
2449                .collect_vec();
2450            assert_eq!(&principals, &[]);
2451        }
2452
2453        #[test]
2454        fn trivial_forbid_query_resource() {
2455            let schema = schema();
2456            let entities = entities();
2457            let req = ResourceQueryRequest::new(
2458                r#"P::"""#.parse().unwrap(),
2459                r#"Action::"A""#.parse().unwrap(),
2460                "R".parse().unwrap(),
2461                Context::empty(),
2462                &schema,
2463            )
2464            .unwrap();
2465
2466            let resources = PolicySet::from_str(r#"forbid(principal, action, resource);"#)
2467                .unwrap()
2468                .query_resource(&req, &entities, &schema)
2469                .unwrap()
2470                .collect_vec();
2471            assert_eq!(&resources, &[]);
2472        }
2473
2474        #[test]
2475        fn error_tpe() {
2476            let schema = schema();
2477            let partial_entities = PartialEntities::from_concrete(entities(), &schema).unwrap();
2478            let req = PartialRequest::new(
2479                PartialEntityUid::new("P".parse().unwrap(), None),
2480                r#"Action::"A""#.parse().unwrap(),
2481                PartialEntityUid::new("R".parse().unwrap(), None),
2482                None,
2483                &schema,
2484            )
2485            .unwrap();
2486            let response = PolicySet::from_str(&format!(
2487                r#"permit(principal, action, resource) when {{ ({} + 1) == 0 || true }};"#,
2488                i64::MAX
2489            ))
2490            .unwrap()
2491            .tpe(&req, &partial_entities, &schema)
2492            .unwrap();
2493            assert_eq!(response.decision(), Some(Decision::Deny));
2494            assert_eq!(
2495                response.reason().unwrap().collect::<Vec<_>>(),
2496                Vec::<&PolicyId>::new()
2497            );
2498        }
2499
2500        #[test]
2501        fn error_query_principal() {
2502            let schema = schema();
2503            let entities = entities();
2504            let req = PrincipalQueryRequest::new(
2505                "P".parse().unwrap(),
2506                r#"Action::"A""#.parse().unwrap(),
2507                r#"R::"""#.parse().unwrap(),
2508                Context::empty(),
2509                &schema,
2510            )
2511            .unwrap();
2512
2513            let principals = PolicySet::from_str(&format!(
2514                r#"permit(principal, action, resource) when {{ ({} + 1) == 0 || true }};"#,
2515                i64::MAX
2516            ))
2517            .unwrap()
2518            .query_principal(&req, &entities, &schema)
2519            .unwrap()
2520            .collect_vec();
2521            assert_eq!(&principals, &[]);
2522        }
2523
2524        #[test]
2525        fn error_query_resource() {
2526            let schema = schema();
2527            let entities = entities();
2528            let req = ResourceQueryRequest::new(
2529                r#"P::"""#.parse().unwrap(),
2530                r#"Action::"A""#.parse().unwrap(),
2531                "R".parse().unwrap(),
2532                Context::empty(),
2533                &schema,
2534            )
2535            .unwrap();
2536
2537            let resources = PolicySet::from_str(&format!(
2538                r#"permit(principal, action, resource) when {{ ({} + 1) == 0 || true }};"#,
2539                i64::MAX
2540            ))
2541            .unwrap()
2542            .query_resource(&req, &entities, &schema)
2543            .unwrap()
2544            .collect_vec();
2545            assert_eq!(&resources, &[]);
2546        }
2547
2548        #[test]
2549        fn empty_tpe() {
2550            let schema = schema();
2551            let partial_entities = PartialEntities::from_concrete(entities(), &schema).unwrap();
2552            let req = PartialRequest::new(
2553                PartialEntityUid::new("P".parse().unwrap(), None),
2554                r#"Action::"A""#.parse().unwrap(),
2555                PartialEntityUid::new("R".parse().unwrap(), None),
2556                None,
2557                &schema,
2558            )
2559            .unwrap();
2560            let response = PolicySet::from_str(r#""#)
2561                .unwrap()
2562                .tpe(&req, &partial_entities, &schema)
2563                .unwrap();
2564            assert_eq!(response.decision(), Some(Decision::Deny));
2565            assert_eq!(
2566                response.reason().unwrap().collect::<Vec<_>>(),
2567                Vec::<&PolicyId>::new()
2568            );
2569        }
2570
2571        #[test]
2572        fn empty_query_principal() {
2573            let schema = schema();
2574            let entities = entities();
2575            let req = PrincipalQueryRequest::new(
2576                "P".parse().unwrap(),
2577                r#"Action::"A""#.parse().unwrap(),
2578                r#"R::"""#.parse().unwrap(),
2579                Context::empty(),
2580                &schema,
2581            )
2582            .unwrap();
2583
2584            let principals = PolicySet::from_str(r#""#)
2585                .unwrap()
2586                .query_principal(&req, &entities, &schema)
2587                .unwrap()
2588                .collect_vec();
2589            assert_eq!(&principals, &[]);
2590        }
2591
2592        #[test]
2593        fn empty_query_resource() {
2594            let schema = schema();
2595            let entities = entities();
2596            let req = ResourceQueryRequest::new(
2597                r#"P::"""#.parse().unwrap(),
2598                r#"Action::"A""#.parse().unwrap(),
2599                "R".parse().unwrap(),
2600                Context::empty(),
2601                &schema,
2602            )
2603            .unwrap();
2604
2605            let resources = PolicySet::from_str(r#""#)
2606                .unwrap()
2607                .query_resource(&req, &entities, &schema)
2608                .unwrap()
2609                .collect_vec();
2610            assert_eq!(&resources, &[]);
2611        }
2612    }
2613
2614    mod response_iterators {
2615        use std::{i64, str::FromStr};
2616
2617        use cedar_policy_core::authorizer::Decision;
2618
2619        use crate::{
2620            PartialEntities, PartialEntityUid, PartialRequest, PolicyId, PolicySet, Schema,
2621        };
2622
2623        #[test]
2624        fn all_policy_categories() {
2625            let schema = Schema::from_str(
2626                "entity P, R; action A appliesTo { principal: P, resource: R, context: { flag: Bool } };",
2627            )
2628            .unwrap();
2629            let req = PartialRequest::new(
2630                PartialEntityUid::new("P".parse().unwrap(), None),
2631                r#"Action::"A""#.parse().unwrap(),
2632                PartialEntityUid::new("R".parse().unwrap(), None),
2633                None,
2634                &schema,
2635            )
2636            .unwrap();
2637
2638            let policies = PolicySet::from_str(&format!(
2639                r#"
2640                permit(principal, action, resource);
2641                permit(principal, action, resource) when {{ false }};
2642                permit(principal, action, resource) when {{ ({} + 1) == 0 || true }};
2643                permit(principal, action, resource) when {{ context.flag }};
2644                forbid(principal, action, resource);
2645                forbid(principal, action, resource) when {{ false }};
2646                forbid(principal, action, resource) when {{ ({} + 1) == 0 || true }};
2647                forbid(principal, action, resource) when {{ context.flag }};
2648                "#,
2649                i64::MAX,
2650                i64::MAX
2651            ))
2652            .unwrap();
2653
2654            let entities = PartialEntities::empty();
2655            let response = policies.tpe(&req, &entities, &schema).unwrap();
2656
2657            assert_eq!(response.decision(), Some(Decision::Deny));
2658            assert_eq!(
2659                response.reason().unwrap().collect::<Vec<_>>(),
2660                vec![&PolicyId::new("policy4")]
2661            );
2662
2663            assert_eq!(
2664                response.true_permits().collect::<Vec<_>>(),
2665                vec![&PolicyId::new("policy0")]
2666            );
2667            assert_eq!(
2668                response.false_permits().collect::<Vec<_>>(),
2669                vec![&PolicyId::new("policy1")]
2670            );
2671            assert_eq!(
2672                response.error_permits().collect::<Vec<_>>(),
2673                vec![&PolicyId::new("policy2")]
2674            );
2675            assert_eq!(
2676                response.residual_permits().collect::<Vec<_>>(),
2677                vec![&PolicyId::new("policy3")]
2678            );
2679            assert_eq!(
2680                response.true_forbids().collect::<Vec<_>>(),
2681                vec![&PolicyId::new("policy4")]
2682            );
2683            assert_eq!(
2684                response.false_forbids().collect::<Vec<_>>(),
2685                vec![&PolicyId::new("policy5")]
2686            );
2687            assert_eq!(
2688                response.error_forbids().collect::<Vec<_>>(),
2689                vec![&PolicyId::new("policy6")]
2690            );
2691            assert_eq!(
2692                response.residual_forbids().collect::<Vec<_>>(),
2693                vec![&PolicyId::new("policy7")]
2694            );
2695        }
2696    }
2697
2698    mod query_action {
2699        use cedar_policy_core::authorizer::Decision;
2700
2701        use crate::{
2702            ActionQueryRequest, Context, PartialEntities, PartialEntityUid, PolicySet, Schema,
2703        };
2704        use similar_asserts::assert_eq;
2705        use std::str::FromStr;
2706
2707        #[test]
2708        fn test() {
2709            let policies = PolicySet::from_str(
2710                r#"
2711            // Edit might be alowed, depending on context
2712            permit(principal, action == Action::"edit", resource)
2713            when {
2714                context.ip.isInRange(resource.allowed_edit_range)
2715            };
2716
2717            // We pass a concrete resource, so we know this will be allowed
2718            permit(principal, action == Action::"view", resource)
2719            when {
2720                resource.public
2721            };
2722
2723            // never allowed for any request
2724            forbid(principal, action == Action::"delete", resource);
2725
2726            // allowed for this action, but it doesn't apply to the request types
2727            permit(principal, action == Action::"not_on_photo", resource);
2728        "#,
2729            )
2730            .unwrap();
2731            let schema = Schema::from_str(
2732                "
2733            entity User, Other;
2734            entity Photo {
2735              public: Bool,
2736              allowed_edit_range: ipaddr,
2737            };
2738            action view, edit, delete appliesTo {
2739              principal: User,
2740              resource: Photo,
2741              context: {
2742                ip: ipaddr,
2743              }
2744            };
2745            action not_on_photo appliesTo {
2746                principal: User,
2747                resource: Other
2748            };
2749        ",
2750            )
2751            .unwrap();
2752            let entities = PartialEntities::from_json_value(
2753                serde_json::json!([
2754                    {
2755                        "uid": { "__entity": { "type": "Photo", "id": "vacation.jpg"} },
2756                        "attrs": {
2757                            "public": true,
2758                            "allowed_edit_range": "192.0.2.0/24"
2759                        },
2760                        "parents": []
2761                    },
2762                ]),
2763                &schema,
2764            )
2765            .unwrap();
2766
2767            let request = ActionQueryRequest::new(
2768                PartialEntityUid::from_concrete(r#"User::"alice""#.parse().unwrap()),
2769                PartialEntityUid::from_concrete(r#"Photo::"vacation.jpg""#.parse().unwrap()),
2770                None,
2771                schema,
2772            )
2773            .unwrap();
2774
2775            let mut actions: Vec<_> = policies
2776                .query_action(&request, &entities)
2777                .unwrap()
2778                .collect();
2779            actions.sort_by_key(|(a, _)| *a);
2780            assert_eq!(
2781                actions,
2782                vec![
2783                    (&r#"Action::"edit""#.parse().unwrap(), None),
2784                    (&r#"Action::"view""#.parse().unwrap(), Some(Decision::Allow)),
2785                ]
2786            )
2787        }
2788
2789        #[test]
2790        fn permitted_action() {
2791            let policies = PolicySet::from_str("permit(principal, action, resource);").unwrap();
2792            let schema = Schema::from_str(
2793                "entity User, Photo; action view appliesTo { principal: User, resource: Photo};",
2794            )
2795            .unwrap();
2796            let entities = PartialEntities::empty();
2797
2798            let request = ActionQueryRequest::new(
2799                PartialEntityUid::from_concrete(r#"User::"alice""#.parse().unwrap()),
2800                PartialEntityUid::from_concrete(r#"Photo::"vacation.jpg""#.parse().unwrap()),
2801                None,
2802                schema,
2803            )
2804            .unwrap();
2805
2806            let actions: Vec<_> = policies
2807                .query_action(&request, &entities)
2808                .unwrap()
2809                .collect();
2810            assert_eq!(
2811                actions,
2812                vec![(&r#"Action::"view""#.parse().unwrap(), Some(Decision::Allow))]
2813            );
2814        }
2815
2816        #[test]
2817        fn maybe_permitted_action() {
2818            let policies = PolicySet::from_str(
2819                "permit(principal, action, resource) when { context.should_allow };",
2820            )
2821            .unwrap();
2822            let schema = Schema::from_str(
2823                "entity User, Photo; action view appliesTo { principal: User, resource: Photo, context: {should_allow: Bool}};",
2824            )
2825            .unwrap();
2826            let entities = PartialEntities::empty();
2827
2828            let request = ActionQueryRequest::new(
2829                PartialEntityUid::from_concrete(r#"User::"alice""#.parse().unwrap()),
2830                PartialEntityUid::from_concrete(r#"Photo::"vacation.jpg""#.parse().unwrap()),
2831                None,
2832                schema,
2833            )
2834            .unwrap();
2835
2836            let actions: Vec<_> = policies
2837                .query_action(&request, &entities)
2838                .unwrap()
2839                .collect();
2840            assert_eq!(actions, vec![(&r#"Action::"view""#.parse().unwrap(), None)]);
2841        }
2842
2843        #[test]
2844        fn forbidden_action() {
2845            let policies = PolicySet::from_str("forbid(principal, action, resource);").unwrap();
2846            let schema = Schema::from_str(
2847                "entity User, Photo; action view appliesTo { principal: User, resource: Photo};",
2848            )
2849            .unwrap();
2850            let entities = PartialEntities::empty();
2851
2852            let request = ActionQueryRequest::new(
2853                PartialEntityUid::from_concrete(r#"User::"alice""#.parse().unwrap()),
2854                PartialEntityUid::from_concrete(r#"Photo::"vacation.jpg""#.parse().unwrap()),
2855                None,
2856                schema,
2857            )
2858            .unwrap();
2859
2860            let actions: Vec<_> = policies
2861                .query_action(&request, &entities)
2862                .unwrap()
2863                .collect();
2864            assert_eq!(actions, Vec::new(),);
2865        }
2866
2867        #[test]
2868        fn invalid_permitted_action() {
2869            let policies = PolicySet::from_str("permit(principal, action, resource);").unwrap();
2870            let schema = Schema::from_str("entity User, Photo, Other; action view appliesTo { principal: User, resource: Other};").unwrap();
2871            let entities = PartialEntities::empty();
2872
2873            let request = ActionQueryRequest::new(
2874                PartialEntityUid::from_concrete(r#"User::"alice""#.parse().unwrap()),
2875                PartialEntityUid::from_concrete(r#"Photo::"vacation.jpg""#.parse().unwrap()),
2876                None,
2877                schema,
2878            )
2879            .unwrap();
2880
2881            let actions: Vec<_> = policies
2882                .query_action(&request, &entities)
2883                .unwrap()
2884                .collect();
2885            assert_eq!(actions, Vec::new());
2886        }
2887
2888        #[test]
2889        fn invalid_context_permitted_action() {
2890            let policies = PolicySet::from_str("permit(principal, action, resource);").unwrap();
2891            let schema = Schema::from_str("entity User, Photo; action view appliesTo { principal: User, resource: Photo, context: {a: Long}};").unwrap();
2892            let entities = PartialEntities::empty();
2893
2894            let request = ActionQueryRequest::new(
2895                PartialEntityUid::from_concrete(r#"User::"alice""#.parse().unwrap()),
2896                PartialEntityUid::from_concrete(r#"Photo::"vacation.jpg""#.parse().unwrap()),
2897                Some(Context::empty()),
2898                schema,
2899            )
2900            .unwrap();
2901
2902            let actions: Vec<_> = policies
2903                .query_action(&request, &entities)
2904                .unwrap()
2905                .collect();
2906            assert_eq!(actions, Vec::new());
2907        }
2908
2909        #[test]
2910        fn no_actions_in_schema() {
2911            let policies = PolicySet::from_str("permit(principal, action, resource);").unwrap();
2912            let schema = Schema::from_str("entity User, Photo;").unwrap();
2913            let entities = PartialEntities::empty();
2914
2915            let request = ActionQueryRequest::new(
2916                PartialEntityUid::from_concrete(r#"User::"alice""#.parse().unwrap()),
2917                PartialEntityUid::from_concrete(r#"Photo::"vacation.jpg""#.parse().unwrap()),
2918                None,
2919                schema,
2920            )
2921            .unwrap();
2922
2923            let actions: Vec<_> = policies
2924                .query_action(&request, &entities)
2925                .unwrap()
2926                .collect();
2927            assert_eq!(actions, Vec::new());
2928        }
2929
2930        #[test]
2931        fn permitted_action_error_permit() {
2932            let policies = PolicySet::from_str(&format!("permit(principal, action, resource);permit(principal, action, resource) when {{ {} + 1 == 0 || true }};", i64::MAX)).unwrap();
2933            let schema = Schema::from_str(
2934                "entity User, Photo; action view appliesTo { principal: User, resource: Photo};",
2935            )
2936            .unwrap();
2937            let entities = PartialEntities::empty();
2938
2939            let request = ActionQueryRequest::new(
2940                PartialEntityUid::from_concrete(r#"User::"alice""#.parse().unwrap()),
2941                PartialEntityUid::from_concrete(r#"Photo::"vacation.jpg""#.parse().unwrap()),
2942                None,
2943                schema,
2944            )
2945            .unwrap();
2946
2947            let actions: Vec<_> = policies
2948                .query_action(&request, &entities)
2949                .unwrap()
2950                .collect();
2951            assert_eq!(
2952                actions,
2953                vec![(&r#"Action::"view""#.parse().unwrap(), Some(Decision::Allow))]
2954            );
2955        }
2956
2957        #[test]
2958        fn permitted_action_error_forbid() {
2959            let policies = PolicySet::from_str(&format!("permit(principal, action, resource);forbid(principal, action, resource) when {{ {} + 1 == 0 || true }};", i64::MAX)).unwrap();
2960            let schema = Schema::from_str(
2961                "entity User, Photo; action view appliesTo { principal: User, resource: Photo};",
2962            )
2963            .unwrap();
2964            let entities = PartialEntities::empty();
2965
2966            let request = ActionQueryRequest::new(
2967                PartialEntityUid::from_concrete(r#"User::"alice""#.parse().unwrap()),
2968                PartialEntityUid::from_concrete(r#"Photo::"vacation.jpg""#.parse().unwrap()),
2969                None,
2970                schema,
2971            )
2972            .unwrap();
2973
2974            let actions: Vec<_> = policies
2975                .query_action(&request, &entities)
2976                .unwrap()
2977                .collect();
2978            assert_eq!(
2979                actions,
2980                vec![(&r#"Action::"view""#.parse().unwrap(), Some(Decision::Allow))]
2981            );
2982        }
2983
2984        #[test]
2985        fn forbidden_action_error_permit() {
2986            let policies = PolicySet::from_str(&format!(
2987                "permit(principal, action, resource) when {{ {} + 1 == 0 || true }};",
2988                i64::MAX
2989            ))
2990            .unwrap();
2991            let schema = Schema::from_str(
2992                "entity User, Photo; action view appliesTo { principal: User, resource: Photo};",
2993            )
2994            .unwrap();
2995            let entities = PartialEntities::empty();
2996
2997            let request = ActionQueryRequest::new(
2998                PartialEntityUid::from_concrete(r#"User::"alice""#.parse().unwrap()),
2999                PartialEntityUid::from_concrete(r#"Photo::"vacation.jpg""#.parse().unwrap()),
3000                None,
3001                schema,
3002            )
3003            .unwrap();
3004
3005            let actions: Vec<_> = policies
3006                .query_action(&request, &entities)
3007                .unwrap()
3008                .collect();
3009            assert_eq!(actions, Vec::new(),);
3010        }
3011    }
3012
3013    /// TPE produces `Residual::Error` when a concrete entity lacks an accessed
3014    /// attribute. The residual policy should be convertible to PST via
3015    /// `Policy::to_pst()`, with the error node represented as
3016    /// `pst::Expr::ResidualError`.
3017    #[test]
3018    fn residual_error_to_pst_and_json() {
3019        use cedar_policy_core::pst;
3020        use std::str::FromStr;
3021
3022        let (schema, _) = crate::Schema::from_cedarschema_str(
3023            r#"
3024            entity User = { name: String };
3025            entity Account = { name: String, assignedTo?: User };
3026            action RevealCredentials appliesTo {
3027                principal: [User],
3028                resource: [Account],
3029                context: { flag: Bool },
3030            };
3031            "#,
3032        )
3033        .unwrap();
3034
3035        let policies = crate::PolicySet::from_str(
3036            r#"
3037            permit(
3038                principal is User,
3039                action == Action::"RevealCredentials",
3040                resource is Account
3041            ) when {
3042                context.flag &&
3043                resource has assignedTo &&
3044                resource.assignedTo == principal
3045            };
3046            "#,
3047        )
3048        .unwrap();
3049
3050        // Account without assignedTo — TPE will produce an error node for
3051        // `resource.assignedTo`
3052        let entities = crate::Entities::from_json_value(
3053            serde_json::json!([
3054                {
3055                    "uid": { "type": "User", "id": "u1" },
3056                    "attrs": { "name": "alice" },
3057                    "parents": []
3058                },
3059                {
3060                    "uid": { "type": "Account", "id": "a1" },
3061                    "attrs": { "name": "shared" },
3062                    "parents": []
3063                }
3064            ]),
3065            Some(&schema),
3066        )
3067        .unwrap();
3068
3069        let partial_entities = crate::PartialEntities::from_concrete(entities, &schema).unwrap();
3070
3071        // Context is unknown — forces a residual on `context has flag`
3072        let request = crate::PartialRequest::new(
3073            crate::PartialEntityUid::from_concrete(r#"User::"u1""#.parse().unwrap()),
3074            r#"Action::"RevealCredentials""#.parse().unwrap(),
3075            crate::PartialEntityUid::from_concrete(r#"Account::"a1""#.parse().unwrap()),
3076            None,
3077            &schema,
3078        )
3079        .unwrap();
3080
3081        let response = policies
3082            .tpe(&request, &partial_entities, &schema)
3083            .expect("tpe should succeed");
3084        // There should be exactly one nontrivial residual
3085        let residual_policies: Vec<_> = response.residual_policies().collect();
3086        assert_eq!(
3087            residual_policies.len(),
3088            1,
3089            "decision={:?}, all residuals: {:?}",
3090            response.decision(),
3091            response
3092                .residual_policies()
3093                .map(|p| p.to_string())
3094                .collect::<Vec<_>>()
3095        );
3096
3097        let policy = &residual_policies[0];
3098
3099        // We can serialize a policy with residual error to json
3100        let json_res = policy.to_json();
3101        assert!(json_res.is_ok());
3102        assert!(json_res.unwrap().to_string().contains(r#"{"error":[]}"#));
3103
3104        // We can also convert it to PST
3105        let pst_policy = policy.to_pst().expect("to_pst should succeed");
3106        let clauses = pst_policy.body().clauses();
3107        assert_eq!(clauses.len(), 1);
3108
3109        let expr = match &clauses[0] {
3110            pst::Clause::When(e) => e,
3111            pst::Clause::Unless(_) => panic!("expected when clause"),
3112        };
3113
3114        // The expression should contain a ResidualError node (from
3115        // `resource.assignedTo` on an entity without that attribute)
3116        assert!(
3117            expr.has_error(),
3118            "residual expression should contain an error node"
3119        );
3120    }
3121
3122    mod template_links {
3123        use std::{collections::HashMap, str::FromStr};
3124
3125        use crate::{
3126            pst, Decision, EntityUid, PartialEntities, PartialEntityUid, PartialRequest, Policy,
3127            PolicyId, PolicySet, Schema, SlotId, Template,
3128        };
3129
3130        fn schema() -> Schema {
3131            Schema::from_str(
3132                "entity User { age: Long }; entity Photo; action view appliesTo { principal: User, resource: Photo};",
3133            )
3134            .unwrap()
3135        }
3136
3137        fn template_policy_set() -> PolicySet {
3138            let mut policies = PolicySet::new();
3139            let template = Template::parse(
3140                Some(PolicyId::new("t0").clone()),
3141                "permit(principal == ?principal, action, resource);",
3142            )
3143            .unwrap();
3144            policies.add_template(template).unwrap();
3145            let template = Template::parse(
3146                Some(PolicyId::new("t1").clone()),
3147                "permit(principal, action, resource == ?resource);",
3148            )
3149            .unwrap();
3150            policies.add_template(template).unwrap();
3151            policies
3152        }
3153
3154        fn partial_req() -> PartialRequest {
3155            PartialRequest::new(
3156                PartialEntityUid::from_concrete(r#"User::"alice""#.parse().unwrap()),
3157                r#"Action::"view""#.parse().unwrap(),
3158                PartialEntityUid::new("Photo".parse().unwrap(), None),
3159                None,
3160                &schema(),
3161            )
3162            .unwrap()
3163        }
3164
3165        #[test]
3166        fn concrete_allow() {
3167            let schema = schema();
3168            let mut policies = template_policy_set();
3169            policies
3170                .link(
3171                    PolicyId::new("t0"),
3172                    PolicyId::new("l"),
3173                    HashMap::from([(
3174                        SlotId::principal(),
3175                        EntityUid::from_str(r#"User::"alice""#).unwrap(),
3176                    )]),
3177                )
3178                .unwrap();
3179
3180            let request = partial_req();
3181            let es = PartialEntities::empty();
3182            let response = policies.tpe(&request, &es, &schema).unwrap();
3183
3184            assert_eq!(response.decision(), Some(Decision::Allow));
3185            assert_eq!(
3186                response.reason().unwrap().collect::<Vec<_>>(),
3187                vec![&PolicyId::new("l")]
3188            );
3189            assert_eq!(
3190                response.true_permits().collect::<Vec<_>>(),
3191                vec![&PolicyId::new("l")]
3192            );
3193        }
3194
3195        #[test]
3196        fn templates_no_links_deny() {
3197            let schema = schema();
3198            let policies = template_policy_set();
3199
3200            let request = partial_req();
3201            let es = PartialEntities::empty();
3202            let response = policies.tpe(&request, &es, &schema).unwrap();
3203
3204            assert_eq!(response.decision(), Some(Decision::Deny));
3205            assert_eq!(
3206                response.reason().unwrap().collect::<Vec<_>>(),
3207                Vec::<&PolicyId>::new()
3208            );
3209        }
3210
3211        #[test]
3212        fn concrete_deny() {
3213            let schema = schema();
3214            let mut policies = template_policy_set();
3215            policies
3216                .link(
3217                    PolicyId::new("t0"),
3218                    PolicyId::new("l"),
3219                    HashMap::from([(
3220                        SlotId::principal(),
3221                        EntityUid::from_str(r#"User::"bob""#).unwrap(),
3222                    )]),
3223                )
3224                .unwrap();
3225
3226            let request = partial_req();
3227            let es = PartialEntities::empty();
3228            let response = policies.tpe(&request, &es, &schema).unwrap();
3229
3230            assert_eq!(response.decision(), Some(Decision::Deny));
3231            assert_eq!(
3232                response.reason().unwrap().collect::<Vec<_>>(),
3233                Vec::<&PolicyId>::new()
3234            );
3235        }
3236
3237        #[test]
3238        fn residual() {
3239            let schema = schema();
3240            let mut policies = template_policy_set();
3241            policies
3242                .link(
3243                    PolicyId::new("t1"),
3244                    PolicyId::new("l"),
3245                    HashMap::from([(
3246                        SlotId::resource(),
3247                        EntityUid::from_str(r#"Photo::"p""#).unwrap(),
3248                    )]),
3249                )
3250                .unwrap();
3251
3252            let request = partial_req();
3253            let es = PartialEntities::empty();
3254            let response = policies.tpe(&request, &es, &schema).unwrap();
3255
3256            let expected: pst::Policy = Policy::parse(
3257                Some(PolicyId::new("l")),
3258                r#"permit(principal, action, resource) when { resource == Photo::"p" };"#,
3259            )
3260            .unwrap()
3261            .to_pst()
3262            .unwrap();
3263
3264            let residuals: Vec<_> = response.residual_policies().collect();
3265            assert_eq!(residuals[0].to_pst().unwrap().body(), expected.body());
3266            assert_eq!(response.decision(), None);
3267            assert!(response.reason().is_none());
3268            assert_eq!(residuals.len(), 1);
3269        }
3270    }
3271}