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