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    /// This function is meant to allow inspecting non-trivial residuals. To get the complete set
587    /// of residual policies that will be used for reauthorization, use [`TpeResponse::policies`]
588    /// or [`TpeResponse::policy_set`].
589    /// To find policies that reached a concrete value, use, e.g., [`TpeResponse::true_permits`].
590    ///
591    /// Each returned [`Policy`] inherits its [`PolicyId`] and
592    /// annotations from the corresponding input policy. Its scope is
593    /// unconstrained and its condition is a single `when` clause containing
594    /// the residual expression.
595    ///
596    /// Call [`Policy::to_pst()`] on each result to get a [`pst::Policy`](crate::pst::Policy)
597    /// for structured inspection.
598    ///
599    /// ```no_run
600    /// # use cedar_policy::{PolicySet, PartialRequest, PartialEntities, Schema};
601    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
602    /// # let (policy_set, request, entities, schema) : (&PolicySet, &PartialRequest, &PartialEntities, &Schema) = panic!();
603    /// let response = policy_set.tpe(&request, &entities, &schema)?;
604    /// for policy in response.residual_policies() {
605    ///     let pst_policy = policy.to_pst()?;
606    ///     for clause in pst_policy.body().clauses() {
607    ///         // inspect the residual expression via pst::Clause / pst::Expr
608    ///     }
609    /// }
610    /// # Ok(())
611    /// # }
612    /// ```
613    ///
614    /// When inspecting these policies, be aware that they may contain
615    /// [`pst::Expr::ResidualError`](crate::pst::Expr::ResidualError) nodes
616    /// which do not normally exist in Cedar expressions. These represent
617    /// subexpressions which are statically known to error; however, the whole
618    /// residual policy might or might not error, regardless of whether it
619    /// contains these nodes.
620    pub fn residual_policies(&self) -> impl Iterator<Item = Policy> + '_ {
621        self.0
622            .residual_permits()
623            .chain(self.0.residual_forbids())
624            .map(|p| Policy::from_ast(p.clone().into()))
625    }
626
627    /// Return all residuals as [`Policy`]s, including concretely `true`, `false`, and error residuals.
628    ///
629    /// Each returned [`Policy`] inherits its [`PolicyId`](crate::PolicyId) and
630    /// annotations from the corresponding input policy. Its scope is
631    /// unconstrained and its condition is a single `when` clause containing
632    /// the residual expression.
633    ///
634    /// Use [`TpeResponse::residual_policies`] to skip `true`, `false`, and error residuals.
635    ///
636    /// See [`TpeResponse::residual_policies`] for documentation on how to inspect policies using the PST.
637    pub fn policies(&self) -> impl Iterator<Item = Policy> + '_ {
638        self.0
639            .policies()
640            .map(|p| Policy::from_ast(p.clone().into()))
641    }
642
643    /// Return all residuals as a [`PolicySet`], including concretely `true`, `false`, and error residuals.
644    ///
645    /// This returns exactly the same policies as [`TpeResponse::policies`], but collected into a policy set.
646    pub fn policy_set(&self) -> PolicySet {
647        PolicySet::from_ast(self.0.policy_set())
648    }
649
650    /// Deprecated alias for [`TpeResponse::residual_policies`]
651    #[deprecated(
652        since = "4.12.0",
653        note = "TpeResponse::residual_policies now returns only non-trivial residual policies"
654    )]
655    pub fn nontrivial_residual_policies(&'_ self) -> impl Iterator<Item = Policy> + '_ {
656        self.residual_policies()
657    }
658
659    /// Get the residual policy for a specific [`PolicyId`], if it exists.
660    ///
661    /// See [`TpeResponse::residual_policies`] for documentation on how to inspect policies using the PST.
662    pub fn get_policy(&self, id: &PolicyId) -> Option<Policy> {
663        self.0
664            .get_residual_policy(id.as_ref())
665            .map(|p| Policy::from_ast(p.clone().into()))
666    }
667}
668
669/// Entity loader trait for batched evaluation.
670///
671/// Loads entities on demand, returning `None` for missing entities.
672/// The `load_entities` function must load all requested entities,
673/// and must compute and include all ancestors of the requested entities.
674/// Loading more entities than requested is allowed.
675#[doc = include_str!("../../experimental_warning.md")]
676pub trait EntityLoader {
677    /// Load all entities for the given set of entity UIDs.
678    /// Returns a map from [`EntityUid`] to [`Option<Entity>`], where `None` indicates
679    /// the entity does not exist.
680    fn load_entities(&mut self, uids: &HashSet<EntityUid>) -> HashMap<EntityUid, Option<Entity>>;
681}
682
683/// Wrapper struct used to convert an [`EntityLoader`] to an `EntityLoaderInternal`
684struct EntityLoaderWrapper<'a>(&'a mut dyn EntityLoader);
685
686impl EntityLoaderInternal for EntityLoaderWrapper<'_> {
687    fn load_entities(
688        &mut self,
689        uids: &HashSet<ast::EntityUID>,
690    ) -> HashMap<ast::EntityUID, Option<ast::Entity>> {
691        let ids = uids
692            .iter()
693            .map(|id| EntityUid::ref_cast(id).clone())
694            .collect();
695        self.0
696            .load_entities(&ids)
697            .into_iter()
698            .map(|(uid, entity)| (uid.0, entity.map(|e| e.0)))
699            .collect()
700    }
701}
702
703/// Simple entity loader implementation that loads from a pre-existing Entities store
704#[doc = include_str!("../../experimental_warning.md")]
705#[derive(Debug)]
706
707pub struct TestEntityLoader<'a> {
708    entities: &'a Entities,
709}
710
711impl<'a> TestEntityLoader<'a> {
712    /// Create a new [`TestEntityLoader`] from an existing Entities store
713    pub fn new(entities: &'a Entities) -> Self {
714        Self { entities }
715    }
716}
717
718impl EntityLoader for TestEntityLoader<'_> {
719    fn load_entities(&mut self, uids: &HashSet<EntityUid>) -> HashMap<EntityUid, Option<Entity>> {
720        uids.iter()
721            .map(|uid| {
722                let entity = self.entities.get(uid).cloned();
723                (uid.clone(), entity)
724            })
725            .collect()
726    }
727}
728
729impl PolicySet {
730    /// Perform type-aware partial evaluation on this [`PolicySet`].
731    ///
732    /// If successful, the result is a [`TpeResponse`] containing the authorization decision, if
733    /// one was reached, and residual policies ready for re-authorization. Use [`TpeResponse::decision`]
734    /// to check the decision and [`TpeResponse::residual_policies`] to get the residuals as
735    /// [`Policy`] objects. You can then call [`Policy::to_pst`] to convert them to [`pst::Policy`](crate::pst::Policy)
736    /// for structured inspection of the residual expression tree.
737    #[doc = include_str!("../../experimental_warning.md")]
738    pub fn tpe<'a>(
739        &self,
740        request: &'a PartialRequest,
741        entities: &'a PartialEntities,
742        schema: &'a Schema,
743    ) -> Result<TpeResponse<'a>, tpe_err::TpeError> {
744        use cedar_policy_core::tpe::is_authorized;
745        let ps = &self.ast;
746        let res = is_authorized(ps, &request.0, &entities.0, &schema.0)?;
747        Ok(TpeResponse(res))
748    }
749
750    /// Like [`Authorizer::is_authorized`] but uses an [`EntityLoader`] to load
751    /// entities on demand.
752    ///
753    /// Calls `loader` at most `max_iters` times, returning
754    /// early if an authorization result is reached.
755    /// Otherwise, it iterates `max_iters` times and returns
756    /// a partial result.
757    ///
758    #[doc = include_str!("../../experimental_warning.md")]
759    pub fn is_authorized_batched(
760        &self,
761        query: &Request,
762        schema: &Schema,
763        loader: &mut dyn EntityLoader,
764        max_iters: u32,
765    ) -> Result<Decision, BatchedEvalError> {
766        is_authorized_batched(
767            &query.0,
768            &self.ast,
769            &schema.0,
770            &mut EntityLoaderWrapper(loader),
771            max_iters,
772        )
773    }
774
775    /// Perform a permission query on the resource
776    #[doc = include_str!("../../experimental_warning.md")]
777    pub fn query_resource(
778        &self,
779        request: &ResourceQueryRequest,
780        entities: &Entities,
781        schema: &Schema,
782    ) -> Result<impl Iterator<Item = EntityUid>, PermissionQueryError> {
783        let partial_entities = PartialEntities::from_concrete(entities.clone(), schema)?;
784        let tpe_response = self.tpe(&request.0, &partial_entities, schema)?;
785        let policies = tpe_response.policy_set();
786        match tpe_response.decision() {
787            Some(Decision::Allow) => Ok(entities
788                .iter()
789                .filter(|entity| entity.0.uid().entity_type() == request.0.0.resource_type())
790                .map(Entity::uid)
791                .collect_vec()
792                .into_iter()),
793            Some(Decision::Deny) => Ok(vec![].into_iter()),
794            None => Ok(entities
795                .iter()
796                .filter(|entity| entity.0.uid().entity_type() == request.0.0.resource_type())
797                .filter(|entity| {
798                    #[expect(
799                        clippy::unwrap_used, reason = "`to_request` cannot panic because we do not pass a schema. However, the correctness of the authorization
800                        decision depends on having valid a request and entities, but we do not do any validation here. Entities were already validated by
801                        `PartialEntities::from_concrete`. The request was _mostly_ validated by its constructor, but the concrete request could still be invalid
802                        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
803                        resources from the entities, which we know are valid."
804                    )]
805                    let req = request.to_request(entity.uid().id().clone(), None).unwrap();
806                    let authorizer = Authorizer::new();
807                    let auth_response = authorizer
808                        .is_authorized(
809                            &req,
810                            &policies,
811                            entities,
812                        );
813                    auth_response.decision() == Decision::Allow
814                })
815                .map(Entity::uid)
816                .collect_vec()
817                .into_iter()),
818        }
819    }
820
821    /// Perform a permission query on the principal
822    #[doc = include_str!("../../experimental_warning.md")]
823    pub fn query_principal(
824        &self,
825        request: &PrincipalQueryRequest,
826        entities: &Entities,
827        schema: &Schema,
828    ) -> Result<impl Iterator<Item = EntityUid>, PermissionQueryError> {
829        let partial_entities = PartialEntities::from_concrete(entities.clone(), schema)?;
830        let tpe_response = self.tpe(&request.0, &partial_entities, schema)?;
831        let policies = tpe_response.policy_set();
832        match tpe_response.decision() {
833            Some(Decision::Allow) => Ok(entities
834                .iter()
835                .filter(|entity| entity.0.uid().entity_type() == request.0.0.principal_type())
836                .map(Entity::uid)
837                .collect_vec()
838                .into_iter()),
839            Some(Decision::Deny) => Ok(vec![].into_iter()),
840            None => Ok(entities
841                .iter()
842                .filter(|entity| entity.0.uid().entity_type() == request.0.0.principal_type())
843                .filter(|entity| {
844                    #[expect(
845                        clippy::unwrap_used, reason = "`to_request` cannot panic because we do not pass a schema. However, the correctness of the authorization
846                        decision depends on having valid a request and entities, but we do not do any validation here. Entities were already validated by
847                        `PartialEntities::from_concrete`. The request was _mostly_ validated by its constructor, but the concrete request could still be invalid
848                        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
849                        principals from the entities, which we know are valid."
850                    )]
851                    let req = request.to_request(entity.uid().id().clone(), None).unwrap();
852                    let authorizer = Authorizer::new();
853                    let auth_response = authorizer
854                        .is_authorized(
855                            &req,
856                            &policies,
857                            entities,
858                        );
859                    auth_response.decision() == Decision::Allow
860                })
861                .map(Entity::uid)
862                .collect_vec()
863                .into_iter()),
864        }
865    }
866
867    /// Given a [`ActionQueryRequest`] (a partial request without a concrete
868    /// action) enumerate actions in the schema which might be authorized
869    /// for that request.
870    ///
871    /// Each action is returned with a partial authorization decision.  If
872    /// the action is definitely authorized, then it is `Some(Decision::Allow)`.
873    /// If we did not reach a concrete authorization decision, then it is
874    /// `None`. Actions which are definitely not authorized (i.e., the
875    /// decision is `Some(Decision::Deny)`) are not returned by this
876    /// function. It is also possible that some actions without a concrete
877    /// authorization decision are never authorized if the residual
878    /// expressions after partial evaluation are not satisfiable.
879    ///
880    /// If the partial request for a particular action is invalid (e.g., the
881    /// action does not apply to the type of principal and resource), then
882    /// that action is not included in the result regardless of whether a
883    /// request with that action would be authorized.
884    ///
885    /// ```
886    /// # use cedar_policy::{PolicySet, Schema, ActionQueryRequest, PartialEntities, PartialEntityUid, Decision, EntityUid, Entities};
887    /// # use std::str::FromStr;
888    /// # let policies = PolicySet::from_str(r#"
889    /// #     permit(principal, action == Action::"edit", resource) when { context.should_allow };
890    /// #     permit(principal, action == Action::"view", resource);
891    /// # "#).unwrap();
892    /// # let schema = Schema::from_str("
893    /// #     entity User, Photo;
894    /// #     action view, edit appliesTo {
895    /// #       principal: User,
896    /// #       resource: Photo,
897    /// #       context: { should_allow: Bool, }
898    /// #     };
899    /// # ").unwrap();
900    /// # let entities = PartialEntities::empty();
901    ///
902    /// // Construct a request for a concrete principal and resource, but leaving the context unknown so
903    /// // that we can see all actions that might be authorized for some context.
904    /// let request = ActionQueryRequest::new(
905    ///     PartialEntityUid::from_concrete(r#"User::"alice""#.parse().unwrap()),
906    ///     PartialEntityUid::from_concrete(r#"Photo::"vacation.jpg""#.parse().unwrap()),
907    ///     None,
908    ///     schema,
909    /// ).unwrap();
910    ///
911    /// // All actions which might be allowed for this principal and resource.
912    /// // The exact authorization result may depend on currently unknown
913    /// // context and entity data.
914    /// let possibly_allowed_actions: Vec<&EntityUid> =
915    ///     policies.query_action(&request, &entities)
916    ///             .unwrap()
917    ///             .map(|(a, _)| a)
918    ///             .collect();
919    /// # let mut possibly_allowed_actions = possibly_allowed_actions;
920    /// # possibly_allowed_actions.sort();
921    /// # assert_eq!(&possibly_allowed_actions, &[&r#"Action::"edit""#.parse().unwrap(), &r#"Action::"view""#.parse().unwrap()]);
922    ///
923    /// // These actions are definitely allowed for this principal and resource.
924    /// // These will be allowed for _any_ context.
925    /// let allowed_actions: Vec<&EntityUid> =
926    ///     policies.query_action(&request, &entities).unwrap()
927    ///             .filter(|(_, resp)| resp == &Some(Decision::Allow))
928    ///             .map(|(a, _)| a)
929    ///             .collect();
930    /// # assert_eq!(&allowed_actions, &[&r#"Action::"view""#.parse().unwrap()]);
931    /// ```
932    #[doc = include_str!("../../experimental_warning.md")]
933    pub fn query_action<'a>(
934        &self,
935        request: &'a ActionQueryRequest,
936        entities: &PartialEntities,
937    ) -> Result<impl Iterator<Item = (&'a EntityUid, Option<Decision>)>, PermissionQueryError> {
938        let mut authorized_actions = Vec::new();
939        // We only consider actions that apply to the type of the requested
940        // principal and resource. Any requests for different actions would
941        // be invalid, so they should never be authorized. Not however that
942        // an authorization request for _could_ return `Allow` if the caller
943        // ignores the request validation error.
944        for action in request
945            .schema
946            .0
947            .actions_for_principal_and_resource(&request.principal.0.ty, &request.resource.0.ty)
948        {
949            // If we fail to construct a partial request, then the partial context is not valid for
950            // the context type declared for this action. This action should never be authorized,
951            // but with the same caveats about invalid requests.
952            if let Ok(partial_request) = request.partial_request(action.clone().into()) {
953                let decision = self
954                    .tpe(&partial_request, entities, &request.schema)?
955                    .decision();
956                if decision != Some(Decision::Deny) {
957                    authorized_actions.push((RefCast::ref_cast(action), decision));
958                }
959            }
960        }
961        Ok(authorized_actions.into_iter())
962    }
963}
964
965#[cfg(test)]
966mod tpe_tests {
967    use std::{
968        collections::{BTreeMap, HashSet},
969        str::FromStr,
970    };
971
972    use cedar_policy_core::tpe::err::EntitiesError;
973    use cool_asserts::assert_matches;
974
975    use crate::{PartialEntity, PartialEntityError, RestrictedExpression, Schema};
976
977    #[test]
978    fn entity_construction() {
979        let schema = Schema::from_str(
980            r"
981            entity A in B tags Long;
982            entity B;
983        ",
984        )
985        .unwrap();
986        PartialEntity::new(
987            r#"A::"foo""#.parse().unwrap(),
988            None,
989            Some(HashSet::from_iter([r#"B::"b""#.parse().unwrap()])),
990            Some(BTreeMap::from_iter([(
991                "".into(),
992                RestrictedExpression::new_long(1),
993            )])),
994            &schema,
995        )
996        .unwrap();
997        assert_matches!(
998            PartialEntity::new(
999                r#"A::"foo""#.parse().unwrap(),
1000                None,
1001                Some(HashSet::from_iter([r#"C::"c""#.parse().unwrap()])),
1002                Some(BTreeMap::from_iter([(
1003                    "".into(),
1004                    RestrictedExpression::new_long(1)
1005                )])),
1006                &schema
1007            ),
1008            Err(PartialEntityError::Entities(EntitiesError::Validation(_)))
1009        );
1010
1011        assert_matches!(
1012            PartialEntity::new(
1013                r#"A::"foo""#.parse().unwrap(),
1014                None,
1015                Some(HashSet::from_iter([r#"B::"b""#.parse().unwrap()])),
1016                Some(BTreeMap::from_iter([(
1017                    "".into(),
1018                    RestrictedExpression::new_bool(true)
1019                )])),
1020                &schema
1021            ),
1022            Err(PartialEntityError::Entities(EntitiesError::Validation(_)))
1023        );
1024    }
1025
1026    mod streaming_service {
1027        use std::{collections::BTreeMap, str::FromStr};
1028
1029        use cedar_policy_core::{authorizer::Decision, tpe::err::EntitiesError};
1030        use cool_asserts::assert_matches;
1031        use itertools::Itertools;
1032        use similar_asserts::assert_eq;
1033
1034        use crate::{
1035            ActionConstraint, ActionQueryRequest, Context, Entities, EntityId, EntityUid,
1036            PartialEntities, PartialEntity, PartialEntityError, PartialEntityUid, PartialRequest,
1037            PolicySet, PrincipalConstraint, PrincipalQueryRequest, Request, ResourceConstraint,
1038            ResourceQueryRequest, RestrictedExpression, Schema,
1039        };
1040
1041        #[test]
1042        fn entities_construction() {
1043            let schema = schema();
1044            PartialEntity::new(
1045                r#"Movie::"foo""#.parse().unwrap(),
1046                None,
1047                None,
1048                None,
1049                &schema,
1050            )
1051            .unwrap();
1052            PartialEntity::new(
1053                r#"Show::"foo""#.parse().unwrap(),
1054                Some(BTreeMap::from_iter([
1055                    ("isFree".into(), RestrictedExpression::new_bool(true)),
1056                    (
1057                        "releaseDate".into(),
1058                        RestrictedExpression::new_datetime("2025-01-01"),
1059                    ),
1060                    (
1061                        "isEarlyAccess".into(),
1062                        RestrictedExpression::new_bool(false),
1063                    ),
1064                ])),
1065                None,
1066                None,
1067                &schema,
1068            )
1069            .unwrap();
1070
1071            assert_matches!(
1072                PartialEntity::new(
1073                    r#"Show::"foo""#.parse().unwrap(),
1074                    Some(BTreeMap::from_iter([
1075                        ("isFree".into(), RestrictedExpression::new_bool(true)),
1076                        (
1077                            "isEarlyAccess".into(),
1078                            RestrictedExpression::new_bool(false)
1079                        ),
1080                    ])),
1081                    None,
1082                    None,
1083                    &schema
1084                ),
1085                Err(PartialEntityError::Entities(EntitiesError::Validation(_)))
1086            );
1087
1088            let e1 = PartialEntity::new(
1089                r#"Show::"foo""#.parse().unwrap(),
1090                Some(BTreeMap::from_iter([
1091                    ("isFree".into(), RestrictedExpression::new_bool(true)),
1092                    (
1093                        "releaseDate".into(),
1094                        RestrictedExpression::new_datetime("2025-01-01"),
1095                    ),
1096                    (
1097                        "isEarlyAccess".into(),
1098                        RestrictedExpression::new_bool(false),
1099                    ),
1100                ])),
1101                None,
1102                None,
1103                &schema,
1104            )
1105            .unwrap();
1106            let e2 = PartialEntity::new(
1107                r#"Subscriber::"a""#.parse().unwrap(),
1108                None,
1109                None,
1110                None,
1111                &schema,
1112            )
1113            .unwrap();
1114            PartialEntities::from_partial_entities([e1.clone(), e2.clone()], &schema).unwrap();
1115            let e3 = PartialEntity::new(
1116                r#"Show::"foo""#.parse().unwrap(),
1117                Some(BTreeMap::from_iter([
1118                    ("isFree".into(), RestrictedExpression::new_bool(true)),
1119                    (
1120                        "releaseDate".into(),
1121                        RestrictedExpression::new_datetime("2025-01-01"),
1122                    ),
1123                    ("isEarlyAccess".into(), RestrictedExpression::new_bool(true)),
1124                ])),
1125                None,
1126                None,
1127                &schema,
1128            )
1129            .unwrap();
1130            assert_matches!(
1131                PartialEntities::from_partial_entities([e1, e2, e3], &schema),
1132                Err(EntitiesError::Duplicate(_)),
1133            );
1134        }
1135
1136        #[track_caller]
1137        fn schema() -> Schema {
1138            Schema::from_cedarschema_str(
1139                r"
1140            // Types
1141type Subscription = {
1142  tier: String
1143};
1144type Profile = {
1145  isKid: Bool
1146};
1147
1148// Entities
1149entity FreeMember;
1150entity Subscriber = {
1151  subscription: Subscription,
1152  profile: Profile
1153};
1154entity Movie = {
1155  isFree: Bool,
1156  needsRentOrBuy: Bool,
1157  isOscarNominated: Bool
1158};
1159entity Show = {
1160  isFree: Bool,
1161  releaseDate: datetime,
1162  isEarlyAccess: Bool
1163};
1164
1165// Actions for content in general
1166action watch
1167  appliesTo {
1168    principal: [FreeMember, Subscriber],
1169    resource: [Movie, Show],
1170    context: {
1171      now: {
1172        datetime: datetime,
1173        localTimeOffset: duration
1174      }
1175    }
1176  };
1177
1178// Actions for movies only
1179action rent, buy
1180  appliesTo {
1181    principal: [FreeMember, Subscriber],
1182    resource: Movie,
1183    context: {
1184      now: {
1185        datetime: datetime
1186      }
1187    }
1188  };
1189            ",
1190            )
1191            .unwrap()
1192            .0
1193        }
1194
1195        #[track_caller]
1196        fn policy_set() -> PolicySet {
1197            PolicySet::from_str(
1198                r#"
1199            // Subscriber Content Access (Shows)
1200@id("subscriber-content-access/show")
1201permit (
1202  principal is Subscriber,
1203  action == Action::"watch",
1204  resource is Show
1205)
1206unless
1207{ resource.isEarlyAccess && context.now.datetime < resource.releaseDate };
1208
1209// Subscriber Content Access (Movies)
1210@id("subscriber-content-access/movie")
1211permit (
1212  principal is Subscriber,
1213  action == Action::"watch",
1214  resource is Movie
1215)
1216unless { resource.needsRentOrBuy };
1217
1218// Free Content Access
1219@id("free-content-access")
1220permit (
1221  principal is FreeMember,
1222  action == Action::"watch",
1223  resource
1224)
1225when { resource.isFree };
1226
1227// Promo: Rent/Buy Oscar-Nominated Movies Until the Oscars
1228@id("rent-buy-oscar-movie")
1229permit (
1230  principal is Subscriber,
1231  action in [Action::"rent", Action::"buy"],
1232  resource is Movie
1233)
1234when
1235{
1236  resource.isOscarNominated &&
1237  context.now.datetime >= datetime("2025-02-02T19:00:00-0500") &&
1238  context.now.datetime < datetime(
1239      "2025-03-02T19:00:00-0500"
1240    ) // Oscars Night
1241};
1242
1243// Early Access (24h) to Shows for Premium Subscribers
1244@id("early-access-show")
1245permit (
1246  principal is Subscriber,
1247  action == Action::"watch",
1248  resource is Show
1249)
1250when
1251{
1252  resource.isEarlyAccess &&
1253  principal.subscription.tier == "premium" &&
1254  context.now.datetime >= resource.releaseDate.offset(duration("-24h"))
1255};
1256
1257// Forbid Bedtime Access to Kid Profile
1258@id("forbid-bedtime-watch-kid-profile")
1259forbid (
1260  principal is Subscriber,
1261  action == Action::"watch",
1262  resource
1263)
1264when { principal.profile.isKid }
1265unless
1266{
1267  // `toTime()` returns the duration modulo one day (i.e., it ignores the "date"
1268  // component). Here, we use it to calculate the subscriber's local time and
1269  // compare the result against durations that represent 6:00AM and 9:00PM.
1270  duration("6h") <= context.now
1271    .datetime
1272    .offset
1273    (
1274      context.now.localTimeOffset
1275    )
1276    .toTime
1277    (
1278    ) &&
1279  context.now.datetime.offset(context.now.localTimeOffset).toTime() <= duration(
1280      "21h"
1281    )
1282};
1283            "#,
1284            )
1285            .unwrap()
1286        }
1287
1288        #[track_caller]
1289        fn entities() -> Entities {
1290            Entities::from_json_value(
1291                serde_json::json!(
1292                                [
1293                    {
1294                        "uid": {
1295                            "type": "Subscriber",
1296                            "id": "Alice"
1297                        },
1298                        "attrs": {
1299                            "subscription" : {
1300                                "tier": "standard"
1301                            },
1302                            "profile" : {
1303                                "isKid": false
1304                            }
1305                        },
1306                        "parents": []
1307                    },
1308                    {
1309                        "uid": {
1310                            "type": "FreeMember",
1311                            "id": "Bob"
1312                        },
1313                        "attrs": {},
1314                        "parents": []
1315                    },
1316                    {
1317                        "uid": {
1318                            "type": "Subscriber",
1319                            "id": "Charlie"
1320                        },
1321                        "attrs": {
1322                            "subscription" : {
1323                                "tier": "premium"
1324                            },
1325                            "profile" : {
1326                                "isKid": false
1327                            }
1328                        },
1329                        "parents": []
1330                    },
1331                    {
1332                        "uid": {
1333                            "type": "Subscriber",
1334                            "id": "Dave"
1335                        },
1336                        "attrs": {
1337                            "subscription" : {
1338                                "tier": "standard"
1339                            },
1340                            "profile" : {
1341                                "isKid": true
1342                            }
1343                        },
1344                        "parents": []
1345                    },
1346                    {
1347                        "uid": {
1348                            "type": "Movie",
1349                            "id": "The Godparent"
1350                        },
1351                        "attrs": {
1352                            "isFree" : true,
1353                            "needsRentOrBuy" : false,
1354                            "isOscarNominated": true
1355                        },
1356                        "parents": []
1357                    },
1358                    {
1359                        "uid": {
1360                            "type": "Movie",
1361                            "id": "The Gleaming"
1362                        },
1363                        "attrs": {
1364                            "isFree" : false,
1365                            "needsRentOrBuy" : false,
1366                            "isOscarNominated": false
1367                        },
1368                        "parents": []
1369                    },
1370                    {
1371                        "uid": {
1372                            "type": "Movie",
1373                            "id": "Devilish"
1374                        },
1375                        "attrs": {
1376                            "isFree" : false,
1377                            "needsRentOrBuy" : true,
1378                            "isOscarNominated": true
1379                        },
1380                        "parents": []
1381                    },
1382                    {
1383                        "uid": {
1384                            "type": "Show",
1385                            "id": "Buddies"
1386                        },
1387                        "attrs": {
1388                            "isFree" : false,
1389                            "releaseDate": "2024-10-10",
1390                            "isEarlyAccess": false
1391                        },
1392                        "parents": []
1393                    },
1394                    {
1395                        "uid": {
1396                            "type": "Show",
1397                            "id": "Breach"
1398                        },
1399                        "attrs": {
1400                            "isFree" : false,
1401                            "releaseDate": "2025-02-21",
1402                            "isEarlyAccess": true
1403                        },
1404                        "parents": []
1405                    }
1406                ]
1407                            ),
1408                Some(&schema()),
1409            )
1410            .unwrap()
1411        }
1412
1413        #[test]
1414        fn run_tpe() {
1415            let schema = schema();
1416            let request = PartialRequest::new(
1417                PartialEntityUid::from_concrete(r#"Subscriber::"Alice""#.parse().unwrap()),
1418                r#"Action::"watch""#.parse().unwrap(),
1419                PartialEntityUid::new("Movie".parse().unwrap(), None),
1420                Some(
1421                    Context::from_pairs([(
1422                        "now".into(),
1423                        RestrictedExpression::new_record([
1424                            (
1425                                "datetime".into(),
1426                                RestrictedExpression::from_str(r#"datetime("2025-07-22")"#)
1427                                    .unwrap(),
1428                            ),
1429                            (
1430                                "localTimeOffset".into(),
1431                                RestrictedExpression::from_str(r#"duration("0h")"#).unwrap(),
1432                            ),
1433                        ])
1434                        .unwrap(),
1435                    )])
1436                    .unwrap(),
1437                ),
1438                &schema,
1439            )
1440            .unwrap();
1441            let policies = policy_set();
1442            let partial_entities = PartialEntities::from_concrete(entities(), &schema).unwrap();
1443
1444            let response = policies
1445                .tpe(&request, &partial_entities, &schema)
1446                .expect("tpe should succeed");
1447
1448            assert_eq!(response.policies().count(), policies.num_of_policies());
1449            for p in response.residual_policies() {
1450                assert_matches!(p.action_constraint(), ActionConstraint::Any);
1451                assert_matches!(p.principal_constraint(), PrincipalConstraint::Any);
1452                assert_matches!(p.resource_constraint(), ResourceConstraint::Any);
1453            }
1454            assert_eq!(
1455                response
1456                    .residual_policies()
1457                    .next()
1458                    .unwrap()
1459                    .annotation("id")
1460                    .unwrap(),
1461                "subscriber-content-access/movie"
1462            );
1463
1464            assert_eq!(response.decision(), None);
1465            assert!(response.reason().is_none());
1466
1467            let request = Request::new(
1468                EntityUid::from_type_name_and_id(
1469                    "Subscriber".parse().unwrap(),
1470                    EntityId::new("Alice"),
1471                ),
1472                r#"Action::"watch""#.parse().unwrap(),
1473                EntityUid::from_type_name_and_id(
1474                    "Movie".parse().unwrap(),
1475                    EntityId::new("The Godparent"),
1476                ),
1477                Context::from_pairs([(
1478                    "now".into(),
1479                    RestrictedExpression::new_record([
1480                        (
1481                            "datetime".into(),
1482                            RestrictedExpression::from_str(r#"datetime("2025-07-22")"#).unwrap(),
1483                        ),
1484                        (
1485                            "localTimeOffset".into(),
1486                            RestrictedExpression::from_str(r#"duration("0h")"#).unwrap(),
1487                        ),
1488                    ])
1489                    .unwrap(),
1490                )])
1491                .unwrap(),
1492                Some(&schema),
1493            )
1494            .unwrap();
1495            assert_matches!(response.reauthorize(&request, &entities()), Ok(res) => {
1496                assert_eq!(res.decision(), Decision::Allow);
1497            });
1498
1499            let request = Request::new(
1500                EntityUid::from_type_name_and_id(
1501                    "Subscriber".parse().unwrap(),
1502                    EntityId::new("Alice"),
1503                ),
1504                r#"Action::"watch""#.parse().unwrap(),
1505                EntityUid::from_type_name_and_id(
1506                    "Movie".parse().unwrap(),
1507                    EntityId::new("Devilish"),
1508                ),
1509                Context::from_pairs([(
1510                    "now".into(),
1511                    RestrictedExpression::new_record([
1512                        (
1513                            "datetime".into(),
1514                            RestrictedExpression::from_str(r#"datetime("2025-07-22")"#).unwrap(),
1515                        ),
1516                        (
1517                            "localTimeOffset".into(),
1518                            RestrictedExpression::from_str(r#"duration("0h")"#).unwrap(),
1519                        ),
1520                    ])
1521                    .unwrap(),
1522                )])
1523                .unwrap(),
1524                Some(&schema),
1525            )
1526            .unwrap();
1527            assert_matches!(response.reauthorize(&request, &entities()), Ok(res) => {
1528                assert_eq!(res.decision(), Decision::Deny);
1529            });
1530        }
1531
1532        // Api test: `TpeResponse::policy_set` agrees with `TpeResponse::policies`, as
1533        // documented. It returns residuals.
1534        #[test]
1535        fn policy_set_returns_residuals() {
1536            let schema = schema();
1537            let request = PartialRequest::new(
1538                PartialEntityUid::from_concrete(r#"Subscriber::"Alice""#.parse().unwrap()),
1539                r#"Action::"watch""#.parse().unwrap(),
1540                // Unknown resource of type `Movie`.
1541                PartialEntityUid::new("Movie".parse().unwrap(), None),
1542                Some(
1543                    Context::from_pairs([(
1544                        "now".into(),
1545                        RestrictedExpression::new_record([
1546                            (
1547                                "datetime".into(),
1548                                RestrictedExpression::from_str(r#"datetime("2025-07-22")"#)
1549                                    .unwrap(),
1550                            ),
1551                            (
1552                                "localTimeOffset".into(),
1553                                RestrictedExpression::from_str(r#"duration("0h")"#).unwrap(),
1554                            ),
1555                        ])
1556                        .unwrap(),
1557                    )])
1558                    .unwrap(),
1559                ),
1560                &schema,
1561            )
1562            .unwrap();
1563            let policies = policy_set();
1564            let partial_entities = PartialEntities::from_json_value(
1565                serde_json::json!([
1566                    {
1567                        "uid": { "type": "Subscriber", "id": "Alice" },
1568                        "attrs": {
1569                            "subscription": { "tier": "standard" },
1570                            "profile": { "isKid": false }
1571                        },
1572                        "parents": []
1573                    }
1574                ]),
1575                &schema,
1576            )
1577            .unwrap();
1578
1579            let response = policies
1580                .tpe(&request, &partial_entities, &schema)
1581                .expect("tpe should succeed");
1582            assert_eq!(response.decision(), None);
1583
1584            let residual_set = response.policy_set();
1585
1586            // `policy_set` includes all residuals (true/false/error + non-trivial).
1587            assert_eq!(residual_set.num_of_policies(), policies.num_of_policies());
1588
1589            // Every policy in the set must have unconstrained scopes (given the current
1590            // partial evaluation's implementation)
1591            for p in residual_set.policies() {
1592                assert_matches!(p.action_constraint(), ActionConstraint::Any);
1593                assert_matches!(p.principal_constraint(), PrincipalConstraint::Any);
1594                assert_matches!(p.resource_constraint(), ResourceConstraint::Any);
1595            }
1596
1597            // `policy_set` returns exactly the same policies as `policies`.
1598            let mut from_policies: Vec<(String, String)> = response
1599                .policies()
1600                .map(|p| (p.id().to_string(), p.to_cedar().unwrap()))
1601                .collect();
1602            from_policies.sort();
1603            let mut from_set: Vec<(String, String)> = residual_set
1604                .policies()
1605                .map(|p| (p.id().to_string(), p.to_cedar().unwrap()))
1606                .collect();
1607            from_set.sort();
1608            assert_eq!(from_set, from_policies);
1609        }
1610
1611        #[test]
1612        fn query_resource() {
1613            let schema = schema();
1614            let policies = policy_set();
1615            let request = ResourceQueryRequest::new(
1616                r#"Subscriber::"Alice""#.parse().unwrap(),
1617                r#"Action::"watch""#.parse().unwrap(),
1618                "Movie".parse().unwrap(),
1619                Context::from_pairs([(
1620                    "now".into(),
1621                    RestrictedExpression::new_record([
1622                        (
1623                            "datetime".into(),
1624                            RestrictedExpression::from_str(r#"datetime("2025-07-22")"#).unwrap(),
1625                        ),
1626                        (
1627                            "localTimeOffset".into(),
1628                            RestrictedExpression::from_str(r#"duration("0h")"#).unwrap(),
1629                        ),
1630                    ])
1631                    .unwrap(),
1632                )])
1633                .unwrap(),
1634                &schema,
1635            )
1636            .unwrap();
1637
1638            // The two movies do not need rent or buy and hence satisfy the
1639            // residual policy
1640            let movies = policies
1641                .query_resource(&request, &entities(), &schema)
1642                .unwrap()
1643                .sorted()
1644                .collect_vec();
1645            assert_eq!(
1646                movies,
1647                &[
1648                    EntityUid::from_str(r#"Movie::"The Gleaming""#).unwrap(),
1649                    EntityUid::from_str(r#"Movie::"The Godparent""#).unwrap(),
1650                ]
1651            );
1652        }
1653
1654        #[test]
1655        fn query_principal() {
1656            let schema = schema();
1657            let policies = policy_set();
1658
1659            let request = PrincipalQueryRequest::new(
1660                "Subscriber".parse().unwrap(),
1661                r#"Action::"watch""#.parse().unwrap(),
1662                r#"Movie::"The Godparent""#.parse().unwrap(),
1663                Context::from_pairs([(
1664                    "now".into(),
1665                    RestrictedExpression::new_record([
1666                        (
1667                            "datetime".into(),
1668                            RestrictedExpression::from_str(r#"datetime("2025-07-22")"#).unwrap(),
1669                        ),
1670                        (
1671                            "localTimeOffset".into(),
1672                            RestrictedExpression::from_str(r#"duration("0h")"#).unwrap(),
1673                        ),
1674                    ])
1675                    .unwrap(),
1676                )])
1677                .unwrap(),
1678                &schema,
1679            )
1680            .unwrap();
1681
1682            let subscribers = policies
1683                .query_principal(&request, &entities(), &schema)
1684                .unwrap()
1685                .sorted()
1686                .collect_vec();
1687            assert_eq!(
1688                subscribers,
1689                &[
1690                    EntityUid::from_str(r#"Subscriber::"Alice""#).unwrap(),
1691                    EntityUid::from_str(r#"Subscriber::"Charlie""#).unwrap(),
1692                ]
1693            );
1694        }
1695
1696        #[test]
1697        fn query_action_alice() {
1698            let schema = schema();
1699            let request = ActionQueryRequest::new(
1700                PartialEntityUid::from_concrete(r#"Subscriber::"Alice""#.parse().unwrap()),
1701                PartialEntityUid::from_concrete(r#"Movie::"The Godparent""#.parse().unwrap()),
1702                None,
1703                schema.clone(),
1704            )
1705            .unwrap();
1706
1707            let policies = policy_set();
1708            let mut actions: Vec<_> = policies
1709                .query_action(
1710                    &request,
1711                    &PartialEntities::from_concrete(entities(), &schema).unwrap(),
1712                )
1713                .unwrap()
1714                .collect();
1715            actions.sort_by_key(|(a, _)| *a);
1716            assert_eq!(
1717                actions,
1718                vec![
1719                    (&r#"Action::"buy""#.parse().unwrap(), None),
1720                    (&r#"Action::"rent""#.parse().unwrap(), None),
1721                    (
1722                        &r#"Action::"watch""#.parse().unwrap(),
1723                        Some(Decision::Allow)
1724                    ),
1725                ]
1726            );
1727        }
1728
1729        #[test]
1730        fn query_action_bob_free() {
1731            let schema = schema();
1732            let request = ActionQueryRequest::new(
1733                PartialEntityUid::from_concrete(r#"FreeMember::"Bob""#.parse().unwrap()),
1734                PartialEntityUid::from_concrete(r#"Movie::"The Godparent""#.parse().unwrap()),
1735                None,
1736                schema.clone(),
1737            )
1738            .unwrap();
1739
1740            let policies = policy_set();
1741            let actions: Vec<_> = policies
1742                .query_action(
1743                    &request,
1744                    &PartialEntities::from_concrete(entities(), &schema).unwrap(),
1745                )
1746                .unwrap()
1747                .collect();
1748            assert_eq!(
1749                actions,
1750                vec![(
1751                    &r#"Action::"watch""#.parse().unwrap(),
1752                    Some(Decision::Allow)
1753                ),]
1754            );
1755        }
1756
1757        #[test]
1758        fn query_action_bob_not_free() {
1759            let schema = schema();
1760            let request = ActionQueryRequest::new(
1761                PartialEntityUid::from_concrete(r#"FreeMember::"Bob""#.parse().unwrap()),
1762                PartialEntityUid::from_concrete(r#"Movie::"The Gleaming""#.parse().unwrap()),
1763                None,
1764                schema.clone(),
1765            )
1766            .unwrap();
1767
1768            let policies = policy_set();
1769            let actions: Vec<_> = policies
1770                .query_action(
1771                    &request,
1772                    &PartialEntities::from_concrete(entities(), &schema).unwrap(),
1773                )
1774                .unwrap()
1775                .collect();
1776            assert_eq!(actions, vec![]);
1777        }
1778    }
1779
1780    mod github {
1781        use std::{
1782            collections::{HashMap, HashSet},
1783            str::FromStr,
1784        };
1785
1786        use cedar_policy_core::tpe::err::TpeError;
1787        use cedar_policy_core::{authorizer::Decision, batched_evaluator::err::BatchedEvalError};
1788        use cool_asserts::assert_matches;
1789        use itertools::Itertools;
1790        use similar_asserts::assert_eq;
1791
1792        use crate::{
1793            ActionQueryRequest, Context, Entities, EntityUid, PartialEntities, PartialEntityUid,
1794            PolicySet, PrincipalQueryRequest, Request, ResourceQueryRequest, RestrictedExpression,
1795            Schema, TestEntityLoader,
1796        };
1797
1798        #[track_caller]
1799        fn schema() -> Schema {
1800            Schema::from_str(
1801                r#"
1802            entity Team, UserGroup in [UserGroup];
1803entity Issue  = {
1804  "repo": Repository,
1805  "reporter": User,
1806};
1807entity Org  = {
1808  "members": UserGroup,
1809  "owners": UserGroup,
1810};
1811entity Repository  = {
1812  "admins": UserGroup,
1813  "maintainers": UserGroup,
1814  "readers": UserGroup,
1815  "triagers": UserGroup,
1816  "writers": UserGroup,
1817};
1818entity User in [UserGroup, Team];
1819
1820action push, pull, fork appliesTo {
1821  principal: [User],
1822  resource: [Repository]
1823};
1824action assign_issue, delete_issue, edit_issue appliesTo {
1825  principal: [User],
1826  resource: [Issue]
1827};
1828action add_reader, add_writer, add_maintainer, add_admin, add_triager appliesTo {
1829  principal: [User],
1830  resource: [Repository]
1831};
1832            "#,
1833            )
1834            .unwrap()
1835        }
1836
1837        fn policy_set() -> PolicySet {
1838            PolicySet::from_str(
1839                r#"
1840                //Actions for readers
1841permit (
1842  principal,
1843  action == Action::"pull",
1844  resource
1845)
1846when { principal in resource.readers };
1847
1848permit (
1849  principal,
1850  action == Action::"fork",
1851  resource
1852)
1853when { principal in resource.readers };
1854
1855permit (
1856  principal,
1857  action == Action::"delete_issue",
1858  resource
1859)
1860when { principal in resource.repo.readers && principal == resource.reporter };
1861
1862permit (
1863  principal,
1864  action == Action::"edit_issue",
1865  resource
1866)
1867when { principal in resource.repo.readers && principal == resource.reporter };
1868
1869//Actions for triagers
1870permit (
1871  principal,
1872  action == Action::"assign_issue",
1873  resource
1874)
1875when { principal in resource.repo.triagers };
1876
1877//Actions for writers
1878permit (
1879  principal,
1880  action == Action::"push",
1881  resource
1882)
1883when { principal in resource.writers };
1884
1885permit (
1886  principal,
1887  action == Action::"edit_issue",
1888  resource
1889)
1890when { principal in resource.repo.writers };
1891
1892//Actions for maintainers
1893permit (
1894  principal,
1895  action == Action::"delete_issue",
1896  resource
1897)
1898when { principal in resource.repo.maintainers };
1899
1900//Actions for admins
1901permit (
1902  principal,
1903  action in
1904    [Action::"add_reader",
1905     Action::"add_triager",
1906     Action::"add_writer",
1907     Action::"add_maintainer",
1908     Action::"add_admin"],
1909  resource
1910)
1911when { principal in resource.admins };
1912//We use the same permissions for org owners, and rely on placing them in the admins group for every repository in the org
1913//The other option is to duplicate all policies for the org base permissions (with a separate heirarchy for each org)
1914"#,
1915            )
1916            .unwrap()
1917        }
1918
1919        #[track_caller]
1920        fn entities() -> Entities {
1921            Entities::from_json_value(serde_json::json!(
1922
1923                [
1924    {
1925      "uid": { "__entity": { "type": "User", "id": "alice"} },
1926      "attrs": {},
1927      "parents": [{ "__entity": { "type": "UserGroup", "id": "common_knowledge_writers"} }, { "__entity": { "type": "UserGroup", "id": "uncommon_knowledge_writers"} } ]
1928    },
1929    {
1930      "uid": { "__entity": { "type": "User", "id": "jane"} },
1931      "attrs": {},
1932      "parents": [{ "__entity": { "type": "UserGroup", "id": "common_knowledge_maintainers"} },  { "__entity": { "type": "Team", "id": "team_that_can_read_everything"} }]
1933    },
1934    {
1935        "uid": { "__entity": { "type": "User", "id": "bob"} },
1936        "attrs": {},
1937        "parents": []
1938    },
1939    {
1940        "uid": { "__entity": { "type": "Repository", "id": "common_knowledge"} },
1941        "attrs": {
1942            "readers" : { "__entity": { "type": "UserGroup", "id": "common_knowledge_readers"} },
1943            "triagers" : { "__entity": { "type": "UserGroup", "id": "common_knowledge_triagers"} },
1944            "writers" : { "__entity": { "type": "UserGroup", "id": "common_knowledge_writers"} },
1945            "maintainers" : { "__entity": { "type": "UserGroup", "id": "common_knowledge_maintainers"} },
1946            "admins" : { "__entity": { "type": "UserGroup", "id": "common_knowledge_admins"} }
1947        },
1948        "parents": []
1949    },
1950    {
1951        "uid": { "__entity": { "type": "UserGroup", "id": "common_knowledge_readers"} },
1952        "attrs": {
1953        },
1954        "parents": [  ]
1955    },
1956    {
1957        "uid": { "__entity": { "type": "UserGroup", "id": "common_knowledge_triagers"} },
1958        "attrs": {
1959        },
1960        "parents": [ { "__entity": { "type": "UserGroup", "id": "common_knowledge_readers"} } ]
1961    },
1962    {
1963        "uid": { "__entity": { "type": "UserGroup", "id": "common_knowledge_writers"} },
1964        "attrs": {
1965        },
1966        "parents": [ {"__entity": { "type": "UserGroup", "id": "common_knowledge_triagers"}} ]
1967    },
1968    {
1969        "uid": { "__entity": { "type": "UserGroup", "id": "common_knowledge_maintainers"} },
1970        "attrs": {
1971        },
1972        "parents": [ {"__entity": { "type": "UserGroup", "id": "common_knowledge_writers"}} ]
1973    },
1974    {
1975        "uid": { "__entity": { "type": "UserGroup", "id": "common_knowledge_admins"} },
1976        "attrs": {
1977        },
1978        "parents": [ {"__entity": { "type": "UserGroup", "id": "common_knowledge_maintainers"}} ]
1979    },
1980    {
1981        "uid": { "__entity": { "type": "Repository", "id": "secret"} },
1982        "attrs": {
1983            "readers" : { "__entity": { "type": "UserGroup", "id": "secret_readers"} },
1984            "triagers" : { "__entity": { "type": "UserGroup", "id": "secret_triagers"} },
1985            "writers" : { "__entity": { "type": "UserGroup", "id": "secret_writers"} },
1986            "maintainers" : { "__entity": { "type": "UserGroup", "id": "secret_maintainers"} },
1987            "admins" : { "__entity": { "type": "UserGroup", "id": "secret_admins"} }
1988        },
1989        "parents": []
1990    },
1991    {
1992        "uid": { "__entity": { "type": "UserGroup", "id": "secret_readers"} },
1993        "attrs": {
1994        },
1995        "parents": [  ]
1996    },
1997    {
1998        "uid": { "__entity": { "type": "UserGroup", "id": "secret_triagers"} },
1999        "attrs": {
2000        },
2001        "parents": [ { "__entity": { "type": "UserGroup", "id": "secret_readers"} } ]
2002    },
2003    {
2004        "uid": { "__entity": { "type": "UserGroup", "id": "secret_writers"} },
2005        "attrs": {
2006        },
2007        "parents": [ {"__entity": { "type": "UserGroup", "id": "secret_triagers"}} ]
2008    },
2009    {
2010        "uid": { "__entity": { "type": "UserGroup", "id": "secret_maintainers"} },
2011        "attrs": {
2012        },
2013        "parents": [ {"__entity": { "type": "UserGroup", "id": "secret_writers"}} ]
2014    },
2015    {
2016        "uid": { "__entity": { "type": "UserGroup", "id": "secret_admins"} },
2017        "attrs": {
2018        },
2019        "parents": [ {"__entity": { "type": "UserGroup", "id": "secret_maintainers"}} ]
2020    },
2021    {
2022        "uid": { "__entity": { "type": "Repository", "id": "uncommon_knowledge"} },
2023        "attrs": {
2024            "readers" : { "__entity": { "type": "UserGroup", "id": "uncommon_knowledge_readers"} },
2025            "triagers" : { "__entity": { "type": "UserGroup", "id": "uncommon_knowledge_triagers"} },
2026            "writers" : { "__entity": { "type": "UserGroup", "id": "uncommon_knowledge_writers"} },
2027            "maintainers" : { "__entity": { "type": "UserGroup", "id": "uncommon_knowledge_maintainers"} },
2028            "admins" : { "__entity": { "type": "UserGroup", "id": "uncommon_knowledge_admins"} }
2029        },
2030        "parents": []
2031    },
2032    {
2033        "uid": { "__entity": { "type": "UserGroup", "id": "uncommon_knowledge_readers"} },
2034        "attrs": {
2035        },
2036        "parents": [  ]
2037    },
2038    {
2039        "uid": { "__entity": { "type": "UserGroup", "id": "uncommon_knowledge_triagers"} },
2040        "attrs": {
2041        },
2042        "parents": [ { "__entity": { "type": "UserGroup", "id": "uncommon_knowledge_readers"} } ]
2043    },
2044    {
2045        "uid": { "__entity": { "type": "UserGroup", "id": "uncommon_knowledge_writers"} },
2046        "attrs": {
2047        },
2048        "parents": [ {"__entity": { "type": "UserGroup", "id": "uncommon_knowledge_triagers"}} ]
2049    },
2050    {
2051        "uid": { "__entity": { "type": "UserGroup", "id": "uncommon_knowledge_maintainers"} },
2052        "attrs": {
2053        },
2054        "parents": [ {"__entity": { "type": "UserGroup", "id": "uncommon_knowledge_writers"}} ]
2055    },
2056    {
2057        "uid": { "__entity": { "type": "UserGroup", "id": "uncommon_knowledge_admins"} },
2058        "attrs": {
2059        },
2060        "parents": [ {"__entity": { "type": "UserGroup", "id": "uncommon_knowledge_maintainers"}} ]
2061    },
2062    {
2063        "uid": { "__entity": { "type": "Team", "id": "team_that_can_read_everything"} },
2064        "attrs": {},
2065        "parents": [{ "__entity": { "type": "UserGroup", "id": "common_knowledge_readers"} }, { "__entity": { "type": "UserGroup", "id": "secret_readers"} }, { "__entity": { "type": "UserGroup", "id": "uncommon_knowledge_readers"} }]
2066    },
2067]
2068            ), Some(&schema())).unwrap()
2069        }
2070
2071        #[test]
2072        fn query_resource() {
2073            let schema = schema();
2074            let request = ResourceQueryRequest::new(
2075                r#"User::"jane""#.parse().unwrap(),
2076                r#"Action::"push""#.parse().unwrap(),
2077                "Repository".parse().unwrap(),
2078                Context::empty(),
2079                &schema,
2080            )
2081            .unwrap();
2082            let policies = policy_set();
2083            assert_matches!(&policies.query_resource(&request, &entities(), &schema).unwrap().collect_vec(), [uid] => {
2084                assert_eq!(uid, &r#"Repository::"common_knowledge""#.parse().unwrap());
2085            });
2086        }
2087
2088        #[test]
2089        fn query_principal() {
2090            let schema = schema();
2091            let request = PrincipalQueryRequest::new(
2092                r"User".parse().unwrap(),
2093                r#"Action::"pull""#.parse().unwrap(),
2094                r#"Repository::"secret""#.parse().unwrap(),
2095                Context::empty(),
2096                &schema,
2097            )
2098            .unwrap();
2099            let policies = policy_set();
2100            assert_matches!(&policies.query_principal(&request, &entities(), &schema).unwrap().collect_vec(), [uid] => {
2101                assert_eq!(uid, &r#"User::"jane""#.parse().unwrap());
2102            });
2103        }
2104
2105        #[test]
2106        fn query_action() {
2107            let schema = schema();
2108            let request = ActionQueryRequest::new(
2109                PartialEntityUid::from_concrete(r#"User::"jane""#.parse().unwrap()),
2110                PartialEntityUid::from_concrete(r#"Repository::"secret""#.parse().unwrap()),
2111                None,
2112                schema.clone(),
2113            )
2114            .unwrap();
2115
2116            let policies = policy_set();
2117            let mut actions: Vec<_> = policies
2118                .query_action(
2119                    &request,
2120                    &PartialEntities::from_concrete(entities(), &schema).unwrap(),
2121                )
2122                .unwrap()
2123                .collect();
2124            actions.sort_by_key(|(a, _)| *a);
2125            assert_eq!(
2126                actions,
2127                vec![
2128                    (&r#"Action::"fork""#.parse().unwrap(), Some(Decision::Allow)),
2129                    (&r#"Action::"pull""#.parse().unwrap(), Some(Decision::Allow)),
2130                ]
2131            );
2132        }
2133
2134        #[test]
2135        fn test_is_authorized_vs_is_authorized_batched() {
2136            use crate::{Authorizer, Request};
2137
2138            let schema = schema();
2139            let policies = policy_set();
2140            let entities = entities();
2141            let authorizer = Authorizer::new();
2142
2143            // Create a set of test requests
2144            let test_requests = vec![
2145                // Request 1: alice can push to common_knowledge (should be allowed)
2146                Request::new(
2147                    r#"User::"alice""#.parse().unwrap(),
2148                    r#"Action::"push""#.parse().unwrap(),
2149                    r#"Repository::"common_knowledge""#.parse().unwrap(),
2150                    Context::empty(),
2151                    Some(&schema),
2152                )
2153                .unwrap(),
2154                // Request 2: jane can pull from secret (should be allowed)
2155                Request::new(
2156                    r#"User::"jane""#.parse().unwrap(),
2157                    r#"Action::"pull""#.parse().unwrap(),
2158                    r#"Repository::"secret""#.parse().unwrap(),
2159                    Context::empty(),
2160                    Some(&schema),
2161                )
2162                .unwrap(),
2163                // Request 3: bob cannot push to common_knowledge (should be denied)
2164                Request::new(
2165                    r#"User::"bob""#.parse().unwrap(),
2166                    r#"Action::"push""#.parse().unwrap(),
2167                    r#"Repository::"common_knowledge""#.parse().unwrap(),
2168                    Context::empty(),
2169                    Some(&schema),
2170                )
2171                .unwrap(),
2172                // Request 4: alice can fork common_knowledge (should be allowed)
2173                Request::new(
2174                    r#"User::"alice""#.parse().unwrap(),
2175                    r#"Action::"fork""#.parse().unwrap(),
2176                    r#"Repository::"common_knowledge""#.parse().unwrap(),
2177                    Context::empty(),
2178                    Some(&schema),
2179                )
2180                .unwrap(),
2181            ];
2182
2183            // Test each request with both methods and compare results
2184            for (i, request) in test_requests.iter().enumerate() {
2185                // Get result from is_authorized
2186                let standard_response = authorizer.is_authorized(request, &policies, &entities);
2187
2188                // Get result from is_authorized_batched (if TPE feature is enabled)
2189                let mut loader = TestEntityLoader::new(&entities);
2190                let batched_decision = policies
2191                    .is_authorized_batched(request, &schema, &mut loader, u32::MAX)
2192                    .unwrap();
2193
2194                // Compare decisions - they should be the same
2195                let standard_decision = standard_response.decision();
2196
2197                assert_eq!(
2198                        standard_decision,
2199                        batched_decision,
2200                        "Request {}: is_authorized returned {:?} but is_authorized_batched returned {:?}",
2201                        i + 1,
2202                        standard_decision,
2203                        batched_decision
2204                    );
2205            }
2206        }
2207
2208        #[test]
2209        fn test_batched_evaluation_error_validation() {
2210            let schema = schema();
2211            let policies = PolicySet::from_str(
2212                    r#"permit(principal, action, resource) when { principal.nonexistent_attr == "value" };"#
2213                ).unwrap();
2214
2215            let request = Request::new(
2216                EntityUid::from_str("User::\"alice\"").unwrap(),
2217                EntityUid::from_str("Action::\"push\"").unwrap(),
2218                EntityUid::from_str("Repository::\"repo\"").unwrap(),
2219                Context::empty(),
2220                Some(&schema),
2221            )
2222            .unwrap();
2223
2224            let entities = entities();
2225            let mut loader = TestEntityLoader::new(&entities);
2226            let result = policies.is_authorized_batched(&request, &schema, &mut loader, 10);
2227
2228            assert!(matches!(
2229                result,
2230                Err(BatchedEvalError::TPE(TpeError::Validation(_)))
2231            ));
2232        }
2233
2234        #[test]
2235        #[cfg(feature = "partial-eval")]
2236        fn test_batched_evaluation_error_partial_request() {
2237            let context_with_unknown = Context::from_pairs([(
2238                "key".to_string(),
2239                RestrictedExpression::new_unknown("test_unknown"),
2240            )])
2241            .unwrap();
2242
2243            let request = Request::new(
2244                EntityUid::from_str("User::\"alice\"").unwrap(),
2245                EntityUid::from_str("Action::\"view\"").unwrap(),
2246                EntityUid::from_str("Resource::\"doc\"").unwrap(),
2247                context_with_unknown,
2248                None,
2249            )
2250            .unwrap();
2251            let schema = schema();
2252
2253            let pset = PolicySet::from_str("permit(principal, action, resource);").unwrap();
2254            let entities = Entities::empty();
2255            let mut loader = TestEntityLoader::new(&entities);
2256            let result = pset.is_authorized_batched(&request, &schema, &mut loader, 10);
2257
2258            assert_matches!(result, Err(BatchedEvalError::PartialRequest(_)));
2259        }
2260
2261        #[test]
2262        fn test_batched_evaluation_error_invalid_entity() {
2263            // Create an entity loader that returns an invalid entity (wrong attribute type)
2264            struct InvalidEntityLoader;
2265            impl crate::EntityLoader for InvalidEntityLoader {
2266                fn load_entities(
2267                    &mut self,
2268                    _uids: &HashSet<EntityUid>,
2269                ) -> HashMap<EntityUid, Option<crate::Entity>> {
2270                    let mut result = HashMap::new();
2271                    let uid = EntityUid::from_strs("Org", "myorg");
2272                    let entity = crate::Entity::new(
2273                        uid.clone(),
2274                        [
2275                            (
2276                                "members".to_string(),
2277                                RestrictedExpression::new_string("not_a_usergroup".to_string()),
2278                            ),
2279                            (
2280                                "owners".to_string(),
2281                                RestrictedExpression::new_entity_uid(EntityUid::from_strs(
2282                                    "UserGroup",
2283                                    "2",
2284                                )),
2285                            ),
2286                        ]
2287                        .into(),
2288                        HashSet::new(),
2289                    )
2290                    .unwrap();
2291                    result.insert(uid, Some(entity));
2292                    result
2293                }
2294            }
2295
2296            let schema = schema();
2297            let pset = PolicySet::from_str(
2298                "permit(principal, action, resource) when { Org::\"myorg\".members == UserGroup::\"1\"};",
2299            )
2300            .unwrap();
2301
2302            let request = Request::new(
2303                r#"User::"alice""#.parse().unwrap(),
2304                r#"Action::"push""#.parse().unwrap(),
2305                r#"Repository::"common_knowledge""#.parse().unwrap(),
2306                Context::empty(),
2307                Some(&schema),
2308            )
2309            .unwrap();
2310
2311            let mut loader = InvalidEntityLoader;
2312            let result = pset.is_authorized_batched(&request, &schema, &mut loader, 10);
2313
2314            assert_matches!(result, Err(BatchedEvalError::Entities(_)));
2315        }
2316
2317        #[test]
2318        #[cfg(feature = "partial-eval")]
2319        fn test_batched_evaluation_error_partial_entity() {
2320            // Create an entity loader that returns a partial entity (contains unknowns)
2321            struct PartialEntityLoader;
2322            impl crate::EntityLoader for PartialEntityLoader {
2323                fn load_entities(
2324                    &mut self,
2325                    _uids: &HashSet<EntityUid>,
2326                ) -> HashMap<EntityUid, Option<crate::Entity>> {
2327                    let mut result = HashMap::new();
2328                    let uid = EntityUid::from_strs("Org", "myorg");
2329                    let entity = crate::Entity::new(
2330                        uid.clone(),
2331                        [
2332                            (
2333                                "members".to_string(),
2334                                RestrictedExpression::new_unknown("partial_members"),
2335                            ),
2336                            (
2337                                "owners".to_string(),
2338                                RestrictedExpression::new_entity_uid(EntityUid::from_strs(
2339                                    "UserGroup",
2340                                    "2",
2341                                )),
2342                            ),
2343                        ]
2344                        .into(),
2345                        HashSet::new(),
2346                    )
2347                    .unwrap();
2348                    result.insert(uid, Some(entity));
2349                    result
2350                }
2351            }
2352
2353            let schema = schema();
2354            let pset = PolicySet::from_str(
2355                "permit(principal, action, resource) when { Org::\"myorg\".members == UserGroup::\"1\"};",
2356            )
2357            .unwrap();
2358
2359            let request = Request::new(
2360                r#"User::"alice""#.parse().unwrap(),
2361                r#"Action::"push""#.parse().unwrap(),
2362                r#"Repository::"common_knowledge""#.parse().unwrap(),
2363                Context::empty(),
2364                Some(&schema),
2365            )
2366            .unwrap();
2367
2368            let mut loader = PartialEntityLoader;
2369            let result = pset.is_authorized_batched(&request, &schema, &mut loader, 10);
2370
2371            assert_matches!(result, Err(BatchedEvalError::PartialValueToValue(_)));
2372        }
2373
2374        #[test]
2375        fn test_batched_evaluation_error_insufficient_iters() {
2376            let schema = schema();
2377            let policies = policy_set();
2378            let entities = entities();
2379
2380            let request = Request::new(
2381                r#"User::"alice""#.parse().unwrap(),
2382                r#"Action::"push""#.parse().unwrap(),
2383                r#"Repository::"common_knowledge""#.parse().unwrap(),
2384                Context::empty(),
2385                Some(&schema),
2386            )
2387            .unwrap();
2388
2389            let mut loader = TestEntityLoader::new(&entities);
2390            let result = policies.is_authorized_batched(&request, &schema, &mut loader, 0);
2391
2392            assert_matches!(result, Err(BatchedEvalError::InsufficientIterations(_)));
2393        }
2394    }
2395
2396    mod trivial {
2397        use cedar_policy_core::authorizer::Decision;
2398        use itertools::Itertools;
2399
2400        use crate::{
2401            Context, Entities, PartialEntities, PartialEntityUid, PartialRequest, PolicyId,
2402            PolicySet, PrincipalQueryRequest, ResourceQueryRequest, Schema,
2403        };
2404        use std::{i64, str::FromStr};
2405
2406        fn schema() -> Schema {
2407            Schema::from_str("entity P, R; action A appliesTo { principal: P, resource: R };")
2408                .unwrap()
2409        }
2410
2411        fn entities() -> Entities {
2412            Entities::from_json_value(
2413                serde_json::json!([
2414                    { "uid": { "__entity": { "type": "P", "id": ""} }, "attrs": {}, "parents": [] },
2415                    { "uid": { "__entity": { "type": "R", "id": ""} }, "attrs": {}, "parents": [] },
2416                ]),
2417                None,
2418            )
2419            .unwrap()
2420        }
2421
2422        #[test]
2423        fn trivial_permit_tpe() {
2424            let schema = schema();
2425            let partial_entities = PartialEntities::from_concrete(entities(), &schema).unwrap();
2426            let req = PartialRequest::new(
2427                PartialEntityUid::new("P".parse().unwrap(), None),
2428                r#"Action::"A""#.parse().unwrap(),
2429                PartialEntityUid::new("R".parse().unwrap(), None),
2430                None,
2431                &schema,
2432            )
2433            .unwrap();
2434            let response = PolicySet::from_str(r"permit(principal, action, resource);")
2435                .unwrap()
2436                .tpe(&req, &partial_entities, &schema)
2437                .unwrap();
2438            assert_eq!(response.decision(), Some(Decision::Allow));
2439            assert_eq!(
2440                response.reason().unwrap().collect::<Vec<_>>(),
2441                vec![&PolicyId::new("policy0")]
2442            );
2443        }
2444
2445        #[test]
2446        fn trivial_permit_query_principal() {
2447            let schema = schema();
2448            let entities = entities();
2449            let req = PrincipalQueryRequest::new(
2450                "P".parse().unwrap(),
2451                r#"Action::"A""#.parse().unwrap(),
2452                r#"R::"""#.parse().unwrap(),
2453                Context::empty(),
2454                &schema,
2455            )
2456            .unwrap();
2457
2458            let principals = PolicySet::from_str(r#"permit(principal, action, resource);"#)
2459                .unwrap()
2460                .query_principal(&req, &entities, &schema)
2461                .unwrap()
2462                .collect_vec();
2463            assert_eq!(&principals, &[r#"P::"""#.parse().unwrap()]);
2464        }
2465
2466        #[test]
2467        fn trivial_permit_query_resource() {
2468            let schema = schema();
2469            let entities = entities();
2470            let req = ResourceQueryRequest::new(
2471                r#"P::"""#.parse().unwrap(),
2472                r#"Action::"A""#.parse().unwrap(),
2473                "R".parse().unwrap(),
2474                Context::empty(),
2475                &schema,
2476            )
2477            .unwrap();
2478
2479            let resources = PolicySet::from_str(r#"permit(principal, action, resource);"#)
2480                .unwrap()
2481                .query_resource(&req, &entities, &schema)
2482                .unwrap()
2483                .collect_vec();
2484            assert_eq!(&resources, &[r#"R::"""#.parse().unwrap()]);
2485        }
2486
2487        #[test]
2488        fn trivial_forbid_tpe() {
2489            let schema = schema();
2490            let partial_entities = PartialEntities::from_concrete(entities(), &schema).unwrap();
2491            let req = PartialRequest::new(
2492                PartialEntityUid::new("P".parse().unwrap(), None),
2493                r#"Action::"A""#.parse().unwrap(),
2494                PartialEntityUid::new("R".parse().unwrap(), None),
2495                None,
2496                &schema,
2497            )
2498            .unwrap();
2499            let response = PolicySet::from_str(r#"forbid(principal, action, resource);"#)
2500                .unwrap()
2501                .tpe(&req, &partial_entities, &schema)
2502                .unwrap();
2503            assert_eq!(response.decision(), Some(Decision::Deny));
2504            assert_eq!(
2505                response.reason().unwrap().collect::<Vec<_>>(),
2506                vec![&PolicyId::new("policy0")]
2507            );
2508            assert_eq!(
2509                response.true_forbids().collect::<Vec<_>>(),
2510                vec![&PolicyId::new("policy0")]
2511            );
2512        }
2513
2514        #[test]
2515        fn trivial_forbid_query_principal() {
2516            let schema = schema();
2517            let entities = entities();
2518            let req = PrincipalQueryRequest::new(
2519                "P".parse().unwrap(),
2520                r#"Action::"A""#.parse().unwrap(),
2521                r#"R::"""#.parse().unwrap(),
2522                Context::empty(),
2523                &schema,
2524            )
2525            .unwrap();
2526
2527            let principals = PolicySet::from_str(r#"forbid(principal, action, resource);"#)
2528                .unwrap()
2529                .query_principal(&req, &entities, &schema)
2530                .unwrap()
2531                .collect_vec();
2532            assert_eq!(&principals, &[]);
2533        }
2534
2535        #[test]
2536        fn trivial_forbid_query_resource() {
2537            let schema = schema();
2538            let entities = entities();
2539            let req = ResourceQueryRequest::new(
2540                r#"P::"""#.parse().unwrap(),
2541                r#"Action::"A""#.parse().unwrap(),
2542                "R".parse().unwrap(),
2543                Context::empty(),
2544                &schema,
2545            )
2546            .unwrap();
2547
2548            let resources = PolicySet::from_str(r#"forbid(principal, action, resource);"#)
2549                .unwrap()
2550                .query_resource(&req, &entities, &schema)
2551                .unwrap()
2552                .collect_vec();
2553            assert_eq!(&resources, &[]);
2554        }
2555
2556        #[test]
2557        fn error_tpe() {
2558            let schema = schema();
2559            let partial_entities = PartialEntities::from_concrete(entities(), &schema).unwrap();
2560            let req = PartialRequest::new(
2561                PartialEntityUid::new("P".parse().unwrap(), None),
2562                r#"Action::"A""#.parse().unwrap(),
2563                PartialEntityUid::new("R".parse().unwrap(), None),
2564                None,
2565                &schema,
2566            )
2567            .unwrap();
2568            let response = PolicySet::from_str(&format!(
2569                r#"permit(principal, action, resource) when {{ ({} + 1) == 0 || true }};"#,
2570                i64::MAX
2571            ))
2572            .unwrap()
2573            .tpe(&req, &partial_entities, &schema)
2574            .unwrap();
2575            assert_eq!(response.decision(), Some(Decision::Deny));
2576            assert_eq!(
2577                response.reason().unwrap().collect::<Vec<_>>(),
2578                Vec::<&PolicyId>::new()
2579            );
2580        }
2581
2582        #[test]
2583        fn error_query_principal() {
2584            let schema = schema();
2585            let entities = entities();
2586            let req = PrincipalQueryRequest::new(
2587                "P".parse().unwrap(),
2588                r#"Action::"A""#.parse().unwrap(),
2589                r#"R::"""#.parse().unwrap(),
2590                Context::empty(),
2591                &schema,
2592            )
2593            .unwrap();
2594
2595            let principals = PolicySet::from_str(&format!(
2596                r#"permit(principal, action, resource) when {{ ({} + 1) == 0 || true }};"#,
2597                i64::MAX
2598            ))
2599            .unwrap()
2600            .query_principal(&req, &entities, &schema)
2601            .unwrap()
2602            .collect_vec();
2603            assert_eq!(&principals, &[]);
2604        }
2605
2606        #[test]
2607        fn error_query_resource() {
2608            let schema = schema();
2609            let entities = entities();
2610            let req = ResourceQueryRequest::new(
2611                r#"P::"""#.parse().unwrap(),
2612                r#"Action::"A""#.parse().unwrap(),
2613                "R".parse().unwrap(),
2614                Context::empty(),
2615                &schema,
2616            )
2617            .unwrap();
2618
2619            let resources = PolicySet::from_str(&format!(
2620                r#"permit(principal, action, resource) when {{ ({} + 1) == 0 || true }};"#,
2621                i64::MAX
2622            ))
2623            .unwrap()
2624            .query_resource(&req, &entities, &schema)
2625            .unwrap()
2626            .collect_vec();
2627            assert_eq!(&resources, &[]);
2628        }
2629
2630        #[test]
2631        fn empty_tpe() {
2632            let schema = schema();
2633            let partial_entities = PartialEntities::from_concrete(entities(), &schema).unwrap();
2634            let req = PartialRequest::new(
2635                PartialEntityUid::new("P".parse().unwrap(), None),
2636                r#"Action::"A""#.parse().unwrap(),
2637                PartialEntityUid::new("R".parse().unwrap(), None),
2638                None,
2639                &schema,
2640            )
2641            .unwrap();
2642            let response = PolicySet::from_str(r#""#)
2643                .unwrap()
2644                .tpe(&req, &partial_entities, &schema)
2645                .unwrap();
2646            assert_eq!(response.decision(), Some(Decision::Deny));
2647            assert_eq!(
2648                response.reason().unwrap().collect::<Vec<_>>(),
2649                Vec::<&PolicyId>::new()
2650            );
2651        }
2652
2653        #[test]
2654        fn empty_query_principal() {
2655            let schema = schema();
2656            let entities = entities();
2657            let req = PrincipalQueryRequest::new(
2658                "P".parse().unwrap(),
2659                r#"Action::"A""#.parse().unwrap(),
2660                r#"R::"""#.parse().unwrap(),
2661                Context::empty(),
2662                &schema,
2663            )
2664            .unwrap();
2665
2666            let principals = PolicySet::from_str(r#""#)
2667                .unwrap()
2668                .query_principal(&req, &entities, &schema)
2669                .unwrap()
2670                .collect_vec();
2671            assert_eq!(&principals, &[]);
2672        }
2673
2674        #[test]
2675        fn empty_query_resource() {
2676            let schema = schema();
2677            let entities = entities();
2678            let req = ResourceQueryRequest::new(
2679                r#"P::"""#.parse().unwrap(),
2680                r#"Action::"A""#.parse().unwrap(),
2681                "R".parse().unwrap(),
2682                Context::empty(),
2683                &schema,
2684            )
2685            .unwrap();
2686
2687            let resources = PolicySet::from_str(r#""#)
2688                .unwrap()
2689                .query_resource(&req, &entities, &schema)
2690                .unwrap()
2691                .collect_vec();
2692            assert_eq!(&resources, &[]);
2693        }
2694    }
2695
2696    mod response_iterators {
2697        use std::{i64, str::FromStr};
2698
2699        use cedar_policy_core::authorizer::Decision;
2700
2701        use crate::{
2702            PartialEntities, PartialEntityUid, PartialRequest, PolicyId, PolicySet, Schema,
2703        };
2704
2705        #[test]
2706        fn all_policy_categories() {
2707            let schema = Schema::from_str(
2708                "entity P, R; action A appliesTo { principal: P, resource: R, context: { flag: Bool } };",
2709            )
2710            .unwrap();
2711            let req = PartialRequest::new(
2712                PartialEntityUid::new("P".parse().unwrap(), None),
2713                r#"Action::"A""#.parse().unwrap(),
2714                PartialEntityUid::new("R".parse().unwrap(), None),
2715                None,
2716                &schema,
2717            )
2718            .unwrap();
2719
2720            let policies = PolicySet::from_str(&format!(
2721                r#"
2722                permit(principal, action, resource);
2723                permit(principal, action, resource) when {{ false }};
2724                permit(principal, action, resource) when {{ ({} + 1) == 0 || true }};
2725                permit(principal, action, resource) when {{ context.flag }};
2726                forbid(principal, action, resource);
2727                forbid(principal, action, resource) when {{ false }};
2728                forbid(principal, action, resource) when {{ ({} + 1) == 0 || true }};
2729                forbid(principal, action, resource) when {{ context.flag }};
2730                "#,
2731                i64::MAX,
2732                i64::MAX
2733            ))
2734            .unwrap();
2735
2736            let entities = PartialEntities::empty();
2737            let response = policies.tpe(&req, &entities, &schema).unwrap();
2738
2739            assert_eq!(response.decision(), Some(Decision::Deny));
2740            assert_eq!(
2741                response.reason().unwrap().collect::<Vec<_>>(),
2742                vec![&PolicyId::new("policy4")]
2743            );
2744
2745            assert_eq!(
2746                response.true_permits().collect::<Vec<_>>(),
2747                vec![&PolicyId::new("policy0")]
2748            );
2749            assert_eq!(
2750                response.false_permits().collect::<Vec<_>>(),
2751                vec![&PolicyId::new("policy1")]
2752            );
2753            assert_eq!(
2754                response.error_permits().collect::<Vec<_>>(),
2755                vec![&PolicyId::new("policy2")]
2756            );
2757            assert_eq!(
2758                response.residual_permits().collect::<Vec<_>>(),
2759                vec![&PolicyId::new("policy3")]
2760            );
2761            assert_eq!(
2762                response.true_forbids().collect::<Vec<_>>(),
2763                vec![&PolicyId::new("policy4")]
2764            );
2765            assert_eq!(
2766                response.false_forbids().collect::<Vec<_>>(),
2767                vec![&PolicyId::new("policy5")]
2768            );
2769            assert_eq!(
2770                response.error_forbids().collect::<Vec<_>>(),
2771                vec![&PolicyId::new("policy6")]
2772            );
2773            assert_eq!(
2774                response.residual_forbids().collect::<Vec<_>>(),
2775                vec![&PolicyId::new("policy7")]
2776            );
2777        }
2778    }
2779
2780    mod query_action {
2781        use cedar_policy_core::authorizer::Decision;
2782
2783        use crate::{
2784            ActionQueryRequest, Context, PartialEntities, PartialEntityUid, PolicySet, Schema,
2785        };
2786        use similar_asserts::assert_eq;
2787        use std::str::FromStr;
2788
2789        #[test]
2790        fn test() {
2791            let policies = PolicySet::from_str(
2792                r#"
2793            // Edit might be alowed, depending on context
2794            permit(principal, action == Action::"edit", resource)
2795            when {
2796                context.ip.isInRange(resource.allowed_edit_range)
2797            };
2798
2799            // We pass a concrete resource, so we know this will be allowed
2800            permit(principal, action == Action::"view", resource)
2801            when {
2802                resource.public
2803            };
2804
2805            // never allowed for any request
2806            forbid(principal, action == Action::"delete", resource);
2807
2808            // allowed for this action, but it doesn't apply to the request types
2809            permit(principal, action == Action::"not_on_photo", resource);
2810        "#,
2811            )
2812            .unwrap();
2813            let schema = Schema::from_str(
2814                "
2815            entity User, Other;
2816            entity Photo {
2817              public: Bool,
2818              allowed_edit_range: ipaddr,
2819            };
2820            action view, edit, delete appliesTo {
2821              principal: User,
2822              resource: Photo,
2823              context: {
2824                ip: ipaddr,
2825              }
2826            };
2827            action not_on_photo appliesTo {
2828                principal: User,
2829                resource: Other
2830            };
2831        ",
2832            )
2833            .unwrap();
2834            let entities = PartialEntities::from_json_value(
2835                serde_json::json!([
2836                    {
2837                        "uid": { "__entity": { "type": "Photo", "id": "vacation.jpg"} },
2838                        "attrs": {
2839                            "public": true,
2840                            "allowed_edit_range": "192.0.2.0/24"
2841                        },
2842                        "parents": []
2843                    },
2844                ]),
2845                &schema,
2846            )
2847            .unwrap();
2848
2849            let request = ActionQueryRequest::new(
2850                PartialEntityUid::from_concrete(r#"User::"alice""#.parse().unwrap()),
2851                PartialEntityUid::from_concrete(r#"Photo::"vacation.jpg""#.parse().unwrap()),
2852                None,
2853                schema,
2854            )
2855            .unwrap();
2856
2857            let mut actions: Vec<_> = policies
2858                .query_action(&request, &entities)
2859                .unwrap()
2860                .collect();
2861            actions.sort_by_key(|(a, _)| *a);
2862            assert_eq!(
2863                actions,
2864                vec![
2865                    (&r#"Action::"edit""#.parse().unwrap(), None),
2866                    (&r#"Action::"view""#.parse().unwrap(), Some(Decision::Allow)),
2867                ]
2868            )
2869        }
2870
2871        #[test]
2872        fn permitted_action() {
2873            let policies = PolicySet::from_str("permit(principal, action, resource);").unwrap();
2874            let schema = Schema::from_str(
2875                "entity User, Photo; action view appliesTo { principal: User, resource: Photo};",
2876            )
2877            .unwrap();
2878            let entities = PartialEntities::empty();
2879
2880            let request = ActionQueryRequest::new(
2881                PartialEntityUid::from_concrete(r#"User::"alice""#.parse().unwrap()),
2882                PartialEntityUid::from_concrete(r#"Photo::"vacation.jpg""#.parse().unwrap()),
2883                None,
2884                schema,
2885            )
2886            .unwrap();
2887
2888            let actions: Vec<_> = policies
2889                .query_action(&request, &entities)
2890                .unwrap()
2891                .collect();
2892            assert_eq!(
2893                actions,
2894                vec![(&r#"Action::"view""#.parse().unwrap(), Some(Decision::Allow))]
2895            );
2896        }
2897
2898        #[test]
2899        fn maybe_permitted_action() {
2900            let policies = PolicySet::from_str(
2901                "permit(principal, action, resource) when { context.should_allow };",
2902            )
2903            .unwrap();
2904            let schema = Schema::from_str(
2905                "entity User, Photo; action view appliesTo { principal: User, resource: Photo, context: {should_allow: Bool}};",
2906            )
2907            .unwrap();
2908            let entities = PartialEntities::empty();
2909
2910            let request = ActionQueryRequest::new(
2911                PartialEntityUid::from_concrete(r#"User::"alice""#.parse().unwrap()),
2912                PartialEntityUid::from_concrete(r#"Photo::"vacation.jpg""#.parse().unwrap()),
2913                None,
2914                schema,
2915            )
2916            .unwrap();
2917
2918            let actions: Vec<_> = policies
2919                .query_action(&request, &entities)
2920                .unwrap()
2921                .collect();
2922            assert_eq!(actions, vec![(&r#"Action::"view""#.parse().unwrap(), None)]);
2923        }
2924
2925        #[test]
2926        fn forbidden_action() {
2927            let policies = PolicySet::from_str("forbid(principal, action, resource);").unwrap();
2928            let schema = Schema::from_str(
2929                "entity User, Photo; action view appliesTo { principal: User, resource: Photo};",
2930            )
2931            .unwrap();
2932            let entities = PartialEntities::empty();
2933
2934            let request = ActionQueryRequest::new(
2935                PartialEntityUid::from_concrete(r#"User::"alice""#.parse().unwrap()),
2936                PartialEntityUid::from_concrete(r#"Photo::"vacation.jpg""#.parse().unwrap()),
2937                None,
2938                schema,
2939            )
2940            .unwrap();
2941
2942            let actions: Vec<_> = policies
2943                .query_action(&request, &entities)
2944                .unwrap()
2945                .collect();
2946            assert_eq!(actions, Vec::new(),);
2947        }
2948
2949        #[test]
2950        fn invalid_permitted_action() {
2951            let policies = PolicySet::from_str("permit(principal, action, resource);").unwrap();
2952            let schema = Schema::from_str("entity User, Photo, Other; action view appliesTo { principal: User, resource: Other};").unwrap();
2953            let entities = PartialEntities::empty();
2954
2955            let request = ActionQueryRequest::new(
2956                PartialEntityUid::from_concrete(r#"User::"alice""#.parse().unwrap()),
2957                PartialEntityUid::from_concrete(r#"Photo::"vacation.jpg""#.parse().unwrap()),
2958                None,
2959                schema,
2960            )
2961            .unwrap();
2962
2963            let actions: Vec<_> = policies
2964                .query_action(&request, &entities)
2965                .unwrap()
2966                .collect();
2967            assert_eq!(actions, Vec::new());
2968        }
2969
2970        #[test]
2971        fn invalid_context_permitted_action() {
2972            let policies = PolicySet::from_str("permit(principal, action, resource);").unwrap();
2973            let schema = Schema::from_str("entity User, Photo; action view appliesTo { principal: User, resource: Photo, context: {a: Long}};").unwrap();
2974            let entities = PartialEntities::empty();
2975
2976            let request = ActionQueryRequest::new(
2977                PartialEntityUid::from_concrete(r#"User::"alice""#.parse().unwrap()),
2978                PartialEntityUid::from_concrete(r#"Photo::"vacation.jpg""#.parse().unwrap()),
2979                Some(Context::empty()),
2980                schema,
2981            )
2982            .unwrap();
2983
2984            let actions: Vec<_> = policies
2985                .query_action(&request, &entities)
2986                .unwrap()
2987                .collect();
2988            assert_eq!(actions, Vec::new());
2989        }
2990
2991        #[test]
2992        fn no_actions_in_schema() {
2993            let policies = PolicySet::from_str("permit(principal, action, resource);").unwrap();
2994            let schema = Schema::from_str("entity User, Photo;").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        #[test]
3013        fn permitted_action_error_permit() {
3014            let policies = PolicySet::from_str(&format!("permit(principal, action, resource);permit(principal, action, resource) when {{ {} + 1 == 0 || true }};", i64::MAX)).unwrap();
3015            let schema = Schema::from_str(
3016                "entity User, Photo; action view appliesTo { principal: User, resource: Photo};",
3017            )
3018            .unwrap();
3019            let entities = PartialEntities::empty();
3020
3021            let request = ActionQueryRequest::new(
3022                PartialEntityUid::from_concrete(r#"User::"alice""#.parse().unwrap()),
3023                PartialEntityUid::from_concrete(r#"Photo::"vacation.jpg""#.parse().unwrap()),
3024                None,
3025                schema,
3026            )
3027            .unwrap();
3028
3029            let actions: Vec<_> = policies
3030                .query_action(&request, &entities)
3031                .unwrap()
3032                .collect();
3033            assert_eq!(
3034                actions,
3035                vec![(&r#"Action::"view""#.parse().unwrap(), Some(Decision::Allow))]
3036            );
3037        }
3038
3039        #[test]
3040        fn permitted_action_error_forbid() {
3041            let policies = PolicySet::from_str(&format!("permit(principal, action, resource);forbid(principal, action, resource) when {{ {} + 1 == 0 || true }};", i64::MAX)).unwrap();
3042            let schema = Schema::from_str(
3043                "entity User, Photo; action view appliesTo { principal: User, resource: Photo};",
3044            )
3045            .unwrap();
3046            let entities = PartialEntities::empty();
3047
3048            let request = ActionQueryRequest::new(
3049                PartialEntityUid::from_concrete(r#"User::"alice""#.parse().unwrap()),
3050                PartialEntityUid::from_concrete(r#"Photo::"vacation.jpg""#.parse().unwrap()),
3051                None,
3052                schema,
3053            )
3054            .unwrap();
3055
3056            let actions: Vec<_> = policies
3057                .query_action(&request, &entities)
3058                .unwrap()
3059                .collect();
3060            assert_eq!(
3061                actions,
3062                vec![(&r#"Action::"view""#.parse().unwrap(), Some(Decision::Allow))]
3063            );
3064        }
3065
3066        #[test]
3067        fn forbidden_action_error_permit() {
3068            let policies = PolicySet::from_str(&format!(
3069                "permit(principal, action, resource) when {{ {} + 1 == 0 || true }};",
3070                i64::MAX
3071            ))
3072            .unwrap();
3073            let schema = Schema::from_str(
3074                "entity User, Photo; action view appliesTo { principal: User, resource: Photo};",
3075            )
3076            .unwrap();
3077            let entities = PartialEntities::empty();
3078
3079            let request = ActionQueryRequest::new(
3080                PartialEntityUid::from_concrete(r#"User::"alice""#.parse().unwrap()),
3081                PartialEntityUid::from_concrete(r#"Photo::"vacation.jpg""#.parse().unwrap()),
3082                None,
3083                schema,
3084            )
3085            .unwrap();
3086
3087            let actions: Vec<_> = policies
3088                .query_action(&request, &entities)
3089                .unwrap()
3090                .collect();
3091            assert_eq!(actions, Vec::new(),);
3092        }
3093    }
3094
3095    /// TPE produces `Residual::Error` when a concrete entity lacks an accessed
3096    /// attribute. The residual policy should be convertible to PST via
3097    /// `Policy::to_pst()`, with the error node represented as
3098    /// `pst::Expr::ResidualError`.
3099    #[test]
3100    fn residual_error_to_pst_and_json() {
3101        use cedar_policy_core::pst;
3102        use std::str::FromStr;
3103
3104        let (schema, _) = crate::Schema::from_cedarschema_str(
3105            r#"
3106            entity User = { name: String };
3107            entity Account = { name: String, assignedTo?: User };
3108            action RevealCredentials appliesTo {
3109                principal: [User],
3110                resource: [Account],
3111                context: { flag: Bool },
3112            };
3113            "#,
3114        )
3115        .unwrap();
3116
3117        let policies = crate::PolicySet::from_str(
3118            r#"
3119            permit(
3120                principal is User,
3121                action == Action::"RevealCredentials",
3122                resource is Account
3123            ) when {
3124                context.flag &&
3125                resource has assignedTo &&
3126                resource.assignedTo == principal
3127            };
3128            "#,
3129        )
3130        .unwrap();
3131
3132        // Account without assignedTo — TPE will produce an error node for
3133        // `resource.assignedTo`
3134        let entities = crate::Entities::from_json_value(
3135            serde_json::json!([
3136                {
3137                    "uid": { "type": "User", "id": "u1" },
3138                    "attrs": { "name": "alice" },
3139                    "parents": []
3140                },
3141                {
3142                    "uid": { "type": "Account", "id": "a1" },
3143                    "attrs": { "name": "shared" },
3144                    "parents": []
3145                }
3146            ]),
3147            Some(&schema),
3148        )
3149        .unwrap();
3150
3151        let partial_entities = crate::PartialEntities::from_concrete(entities, &schema).unwrap();
3152
3153        // Context is unknown — forces a residual on `context has flag`
3154        let request = crate::PartialRequest::new(
3155            crate::PartialEntityUid::from_concrete(r#"User::"u1""#.parse().unwrap()),
3156            r#"Action::"RevealCredentials""#.parse().unwrap(),
3157            crate::PartialEntityUid::from_concrete(r#"Account::"a1""#.parse().unwrap()),
3158            None,
3159            &schema,
3160        )
3161        .unwrap();
3162
3163        let response = policies
3164            .tpe(&request, &partial_entities, &schema)
3165            .expect("tpe should succeed");
3166        // There should be exactly one nontrivial residual
3167        let residual_policies: Vec<_> = response.residual_policies().collect();
3168        assert_eq!(
3169            residual_policies.len(),
3170            1,
3171            "decision={:?}, all residuals: {:?}",
3172            response.decision(),
3173            response
3174                .residual_policies()
3175                .map(|p| p.to_string())
3176                .collect::<Vec<_>>()
3177        );
3178
3179        let policy = &residual_policies[0];
3180
3181        // We can serialize a policy with residual error to json
3182        let json_res = policy.to_json();
3183        assert!(json_res.is_ok());
3184        assert!(json_res.unwrap().to_string().contains(r#"{"error":[]}"#));
3185
3186        // We can also convert it to PST
3187        let pst_policy = policy.to_pst().expect("to_pst should succeed");
3188        let clauses = pst_policy.body().clauses();
3189        assert_eq!(clauses.len(), 1);
3190
3191        let expr = match &clauses[0] {
3192            pst::Clause::When(e) => e,
3193            pst::Clause::Unless(_) => panic!("expected when clause"),
3194        };
3195
3196        // The expression should contain a ResidualError node (from
3197        // `resource.assignedTo` on an entity without that attribute)
3198        assert!(
3199            expr.has_error(),
3200            "residual expression should contain an error node"
3201        );
3202    }
3203
3204    mod template_links {
3205        use std::{collections::HashMap, str::FromStr};
3206
3207        use crate::{
3208            pst, Decision, EntityUid, PartialEntities, PartialEntityUid, PartialRequest, Policy,
3209            PolicyId, PolicySet, Schema, SlotId, Template,
3210        };
3211
3212        fn schema() -> Schema {
3213            Schema::from_str(
3214                "entity User { age: Long }; entity Photo; action view appliesTo { principal: User, resource: Photo};",
3215            )
3216            .unwrap()
3217        }
3218
3219        fn template_policy_set() -> PolicySet {
3220            let mut policies = PolicySet::new();
3221            let template = Template::parse(
3222                Some(PolicyId::new("t0").clone()),
3223                "permit(principal == ?principal, action, resource);",
3224            )
3225            .unwrap();
3226            policies.add_template(template).unwrap();
3227            let template = Template::parse(
3228                Some(PolicyId::new("t1").clone()),
3229                "permit(principal, action, resource == ?resource);",
3230            )
3231            .unwrap();
3232            policies.add_template(template).unwrap();
3233            policies
3234        }
3235
3236        fn partial_req() -> PartialRequest {
3237            PartialRequest::new(
3238                PartialEntityUid::from_concrete(r#"User::"alice""#.parse().unwrap()),
3239                r#"Action::"view""#.parse().unwrap(),
3240                PartialEntityUid::new("Photo".parse().unwrap(), None),
3241                None,
3242                &schema(),
3243            )
3244            .unwrap()
3245        }
3246
3247        #[test]
3248        fn concrete_allow() {
3249            let schema = schema();
3250            let mut policies = template_policy_set();
3251            policies
3252                .link(
3253                    PolicyId::new("t0"),
3254                    PolicyId::new("l"),
3255                    HashMap::from([(
3256                        SlotId::principal(),
3257                        EntityUid::from_str(r#"User::"alice""#).unwrap(),
3258                    )]),
3259                )
3260                .unwrap();
3261
3262            let request = partial_req();
3263            let es = PartialEntities::empty();
3264            let response = policies.tpe(&request, &es, &schema).unwrap();
3265
3266            assert_eq!(response.decision(), Some(Decision::Allow));
3267            assert_eq!(
3268                response.reason().unwrap().collect::<Vec<_>>(),
3269                vec![&PolicyId::new("l")]
3270            );
3271            assert_eq!(
3272                response.true_permits().collect::<Vec<_>>(),
3273                vec![&PolicyId::new("l")]
3274            );
3275        }
3276
3277        #[test]
3278        fn templates_no_links_deny() {
3279            let schema = schema();
3280            let policies = template_policy_set();
3281
3282            let request = partial_req();
3283            let es = PartialEntities::empty();
3284            let response = policies.tpe(&request, &es, &schema).unwrap();
3285
3286            assert_eq!(response.decision(), Some(Decision::Deny));
3287            assert_eq!(
3288                response.reason().unwrap().collect::<Vec<_>>(),
3289                Vec::<&PolicyId>::new()
3290            );
3291        }
3292
3293        #[test]
3294        fn concrete_deny() {
3295            let schema = schema();
3296            let mut policies = template_policy_set();
3297            policies
3298                .link(
3299                    PolicyId::new("t0"),
3300                    PolicyId::new("l"),
3301                    HashMap::from([(
3302                        SlotId::principal(),
3303                        EntityUid::from_str(r#"User::"bob""#).unwrap(),
3304                    )]),
3305                )
3306                .unwrap();
3307
3308            let request = partial_req();
3309            let es = PartialEntities::empty();
3310            let response = policies.tpe(&request, &es, &schema).unwrap();
3311
3312            assert_eq!(response.decision(), Some(Decision::Deny));
3313            assert_eq!(
3314                response.reason().unwrap().collect::<Vec<_>>(),
3315                Vec::<&PolicyId>::new()
3316            );
3317        }
3318
3319        #[test]
3320        fn residual() {
3321            let schema = schema();
3322            let mut policies = template_policy_set();
3323            policies
3324                .link(
3325                    PolicyId::new("t1"),
3326                    PolicyId::new("l"),
3327                    HashMap::from([(
3328                        SlotId::resource(),
3329                        EntityUid::from_str(r#"Photo::"p""#).unwrap(),
3330                    )]),
3331                )
3332                .unwrap();
3333
3334            let request = partial_req();
3335            let es = PartialEntities::empty();
3336            let response = policies.tpe(&request, &es, &schema).unwrap();
3337
3338            let expected: pst::Policy = Policy::parse(
3339                Some(PolicyId::new("l")),
3340                r#"permit(principal, action, resource) when { resource == Photo::"p" };"#,
3341            )
3342            .unwrap()
3343            .to_pst()
3344            .unwrap();
3345
3346            let residuals: Vec<_> = response.residual_policies().collect();
3347            assert_eq!(residuals[0].to_pst().unwrap().body(), expected.body());
3348            assert_eq!(response.decision(), None);
3349            assert!(response.reason().is_none());
3350            assert_eq!(residuals.len(), 1);
3351        }
3352    }
3353}