Skip to main content

cedar_policy/
api.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
17//! This module contains the public library api
18#![allow(
19    clippy::missing_panics_doc,
20    clippy::missing_errors_doc,
21    clippy::similar_names,
22    clippy::result_large_err, // see #878
23    reason = "this module doesn't currently comply with these lints"
24)]
25
26mod id;
27#[cfg(feature = "entity-manifest")]
28use cedar_policy_core::validator::entity_manifest;
29// TODO (#1157) implement wrappers for these structs before they become public
30#[cfg(feature = "entity-manifest")]
31pub use cedar_policy_core::validator::entity_manifest::{
32    AccessTrie, EntityManifest, EntityRoot, Fields, RootAccessTrie,
33};
34use cedar_policy_core::validator::json_schema;
35use cedar_policy_core::validator::typecheck::{PolicyCheck, Typechecker};
36pub use id::*;
37
38#[cfg(feature = "deprecated-schema-compat")]
39mod deprecated_schema_compat;
40
41mod err;
42pub use err::*;
43
44pub use ast::Effect;
45pub use authorizer::Decision;
46#[cfg(feature = "partial-eval")]
47use cedar_policy_core::ast::BorrowedRestrictedExpr;
48use cedar_policy_core::ast::{self, RequestSchema, RestrictedExpr};
49use cedar_policy_core::authorizer::{self};
50use cedar_policy_core::entities::{ContextSchema, Dereference};
51use cedar_policy_core::est::{self, TemplateLink};
52use cedar_policy_core::evaluator::Evaluator;
53#[cfg(feature = "partial-eval")]
54use cedar_policy_core::evaluator::RestrictedEvaluator;
55use cedar_policy_core::extensions::Extensions;
56use cedar_policy_core::parser;
57use cedar_policy_core::FromNormalizedStr;
58use itertools::{Either, Itertools};
59use linked_hash_map::LinkedHashMap;
60use miette::Diagnostic;
61use ref_cast::RefCast;
62use serde::{Deserialize, Serialize};
63use smol_str::SmolStr;
64use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
65use std::io::Read;
66use std::str::FromStr;
67use std::sync::Arc;
68
69#[expect(
70    clippy::unwrap_used,
71    reason = "`CARGO_PKG_VERSION` should return a valid SemVer version string"
72)]
73pub(crate) mod version {
74    use semver::Version;
75    use std::sync::LazyLock;
76
77    // Cedar Rust SDK Semantic Versioning version
78    static SDK_VERSION: LazyLock<Version> =
79        LazyLock::new(|| env!("CARGO_PKG_VERSION").parse().unwrap());
80    // Cedar language version
81    // The patch version field may be unnecessary
82    static LANG_VERSION: LazyLock<Version> = LazyLock::new(|| Version::new(4, 4, 0));
83
84    /// Get the Cedar SDK Semantic Versioning version
85    pub fn get_sdk_version() -> Version {
86        SDK_VERSION.clone()
87    }
88    /// Get the Cedar language version
89    pub fn get_lang_version() -> Version {
90        LANG_VERSION.clone()
91    }
92}
93
94/// Entity datatype
95#[repr(transparent)]
96#[derive(Debug, Clone, PartialEq, Eq, RefCast, Hash)]
97pub struct Entity(pub(crate) ast::Entity);
98
99#[doc(hidden)] // because this converts to a private/internal type
100impl AsRef<ast::Entity> for Entity {
101    fn as_ref(&self) -> &ast::Entity {
102        &self.0
103    }
104}
105
106#[doc(hidden)]
107impl From<ast::Entity> for Entity {
108    fn from(entity: ast::Entity) -> Self {
109        Self(entity)
110    }
111}
112
113impl Entity {
114    /// Create a new `Entity` with this Uid, attributes, and parents (and no tags).
115    ///
116    /// Attribute values are specified here as "restricted expressions".
117    /// See docs on `RestrictedExpression`
118    /// ```
119    /// # use cedar_policy::{Entity, EntityId, EntityTypeName, EntityUid, RestrictedExpression};
120    /// # use std::collections::{HashMap, HashSet};
121    /// # use std::str::FromStr;
122    /// let eid = EntityId::from_str("alice").unwrap();
123    /// let type_name = EntityTypeName::from_str("User").unwrap();
124    /// let euid = EntityUid::from_type_name_and_id(type_name, eid);
125    /// let attrs = HashMap::from([
126    ///     ("age".to_string(), RestrictedExpression::from_str("21").unwrap()),
127    ///     ("department".to_string(), RestrictedExpression::from_str("\"CS\"").unwrap()),
128    /// ]);
129    /// let parent_eid = EntityId::from_str("admin").unwrap();
130    /// let parent_type_name = EntityTypeName::from_str("Group").unwrap();
131    /// let parent_euid = EntityUid::from_type_name_and_id(parent_type_name, parent_eid);
132    /// let parents = HashSet::from([parent_euid]);
133    /// let entity = Entity::new(euid, attrs, parents);
134    ///```
135    pub fn new(
136        uid: EntityUid,
137        attrs: HashMap<String, RestrictedExpression>,
138        parents: HashSet<EntityUid>,
139    ) -> Result<Self, EntityAttrEvaluationError> {
140        Self::new_with_tags(uid, attrs, parents, [])
141    }
142
143    /// Create a new `Entity` with no attributes or tags.
144    ///
145    /// Unlike [`Entity::new()`], this constructor cannot error.
146    /// (The only source of errors in `Entity::new()` are attributes.)
147    pub fn new_no_attrs(uid: EntityUid, parents: HashSet<EntityUid>) -> Self {
148        // note that we take a "parents" parameter here; we will compute TC when
149        // the `Entities` object is created
150        Self(ast::Entity::new_with_attr_partial_value(
151            uid.into(),
152            [],
153            HashSet::new(),
154            parents.into_iter().map(EntityUid::into).collect(),
155            [],
156        ))
157    }
158
159    /// Create a new `Entity` with this Uid, attributes, parents, and tags.
160    ///
161    /// Attribute and tag values are specified here as "restricted expressions".
162    /// See docs on [`RestrictedExpression`].
163    pub fn new_with_tags(
164        uid: EntityUid,
165        attrs: impl IntoIterator<Item = (String, RestrictedExpression)>,
166        parents: impl IntoIterator<Item = EntityUid>,
167        tags: impl IntoIterator<Item = (String, RestrictedExpression)>,
168    ) -> Result<Self, EntityAttrEvaluationError> {
169        // note that we take a "parents" parameter here, not "ancestors"; we
170        // will compute TC when the `Entities` object is created
171        Ok(Self(ast::Entity::new(
172            uid.into(),
173            attrs.into_iter().map(|(k, v)| (k.into(), v.0)),
174            HashSet::new(),
175            parents.into_iter().map(EntityUid::into).collect(),
176            tags.into_iter().map(|(k, v)| (k.into(), v.0)),
177            Extensions::all_available(),
178        )?))
179    }
180
181    /// Create a new `Entity` with this Uid, no attributes, and no parents.
182    /// ```
183    /// # use cedar_policy::{Entity, EntityId, EntityTypeName, EntityUid};
184    /// # use std::str::FromStr;
185    /// let eid = EntityId::from_str("alice").unwrap();
186    /// let type_name = EntityTypeName::from_str("User").unwrap();
187    /// let euid = EntityUid::from_type_name_and_id(type_name, eid);
188    /// let alice = Entity::with_uid(euid);
189    /// # cool_asserts::assert_matches!(alice.attr("age"), None);
190    /// ```
191    pub fn with_uid(uid: EntityUid) -> Self {
192        Self(ast::Entity::with_uid(uid.into()))
193    }
194
195    /// Test if two entities are structurally equal. That is, not only do they
196    /// have the same UID, but they also have the same attributes and ancestors.
197    ///
198    /// Note that ancestor equality is determined by examining the ancestors
199    /// entities provided when constructing these objects, without computing
200    /// their transitive closure. For accurate comparison, entities should be
201    /// constructed with the transitive closure precomputed or be drawn from an
202    /// [`Entities`] object which will perform this computation.
203    pub fn deep_eq(&self, other: &Self) -> bool {
204        self.0.deep_eq(&other.0)
205    }
206
207    /// Get the Uid of this entity
208    /// ```
209    /// # use cedar_policy::{Entity, EntityId, EntityTypeName, EntityUid};
210    /// # use std::str::FromStr;
211    /// # let eid = EntityId::from_str("alice").unwrap();
212    /// let type_name = EntityTypeName::from_str("User").unwrap();
213    /// let euid = EntityUid::from_type_name_and_id(type_name, eid);
214    /// let alice = Entity::with_uid(euid.clone());
215    /// assert_eq!(alice.uid(), euid);
216    /// ```
217    pub fn uid(&self) -> EntityUid {
218        self.0.uid().clone().into()
219    }
220
221    /// Get the value for the given attribute, or `None` if not present.
222    ///
223    /// This can also return Some(Err) if the attribute is not a value (i.e., is
224    /// unknown due to partial evaluation).
225    /// ```
226    /// # use cedar_policy::{Entity, EntityId, EntityTypeName, EntityUid, EvalResult, RestrictedExpression};
227    /// # use std::collections::{HashMap, HashSet};
228    /// # use std::str::FromStr;
229    /// let eid = EntityId::from_str("alice").unwrap();
230    /// let type_name = EntityTypeName::from_str("User").unwrap();
231    /// let euid = EntityUid::from_type_name_and_id(type_name, eid);
232    /// let attrs = HashMap::from([
233    ///     ("age".to_string(), RestrictedExpression::from_str("21").unwrap()),
234    ///     ("department".to_string(), RestrictedExpression::from_str("\"CS\"").unwrap()),
235    /// ]);
236    /// let entity = Entity::new(euid, attrs, HashSet::new()).unwrap();
237    /// assert_eq!(entity.attr("age").unwrap().unwrap(), EvalResult::Long(21));
238    /// assert_eq!(entity.attr("department").unwrap().unwrap(), EvalResult::String("CS".to_string()));
239    /// assert!(entity.attr("foo").is_none());
240    /// ```
241    pub fn attr(&self, attr: &str) -> Option<Result<EvalResult, PartialValueToValueError>> {
242        match ast::Value::try_from(self.0.get(attr)?.clone()) {
243            Ok(v) => Some(Ok(EvalResult::from(v))),
244            Err(e) => Some(Err(e)),
245        }
246    }
247
248    /// Iterate over all attributes of the entity, as (name, value) pairs
249    ///
250    /// The value for any individual attribute may be `Err` if the attribute is
251    /// not a value (i.e., is unknown due to partial evaluation).
252    pub fn attrs(
253        &self,
254    ) -> impl Iterator<Item = (&str, Result<EvalResult, PartialValueToValueError>)> {
255        self.0.attrs().map(|(k, v)| {
256            (
257                k.as_ref(),
258                ast::Value::try_from(v.clone()).map(EvalResult::from),
259            )
260        })
261    }
262
263    /// Get the value for the given tag, or `None` if not present.
264    ///
265    /// This can also return Some(Err) if the tag is not a value (i.e., is
266    /// unknown due to partial evaluation).
267    pub fn tag(&self, tag: &str) -> Option<Result<EvalResult, PartialValueToValueError>> {
268        match ast::Value::try_from(self.0.get_tag(tag)?.clone()) {
269            Ok(v) => Some(Ok(EvalResult::from(v))),
270            Err(e) => Some(Err(e)),
271        }
272    }
273
274    /// Iterate over all tags of the entity, as (name, value) pairs
275    ///
276    /// The value for any individual tag may be `Err` if the tag is not a value
277    /// (i.e., is unknown due to partial evaluation).
278    pub fn tags(
279        &self,
280    ) -> impl Iterator<Item = (&str, Result<EvalResult, PartialValueToValueError>)> {
281        self.0.tags().map(|(k, v)| {
282            (
283                k.as_ref(),
284                ast::Value::try_from(v.clone()).map(EvalResult::from),
285            )
286        })
287    }
288
289    /// Consume the entity and return the entity's owned Uid, attributes and parents.
290    pub fn into_inner(
291        self,
292    ) -> (
293        EntityUid,
294        HashMap<String, RestrictedExpression>,
295        HashSet<EntityUid>,
296    ) {
297        let (uid, attrs, ancestors, mut parents, _) = self.0.into_inner();
298        parents.extend(ancestors);
299
300        let attrs = attrs
301            .into_iter()
302            .map(|(k, v)| {
303                (
304                    k.to_string(),
305                    match v {
306                        ast::PartialValue::Value(val) => {
307                            RestrictedExpression(ast::RestrictedExpr::from(val))
308                        }
309                        ast::PartialValue::Residual(exp) => {
310                            RestrictedExpression(ast::RestrictedExpr::new_unchecked(exp))
311                        }
312                    },
313                )
314            })
315            .collect();
316
317        (
318            uid.into(),
319            attrs,
320            parents.into_iter().map(Into::into).collect(),
321        )
322    }
323
324    /// Parse an entity from an in-memory JSON value
325    /// If a schema is provided, it is handled identically to [`Entities::from_json_str`]
326    pub fn from_json_value(
327        value: serde_json::Value,
328        schema: Option<&Schema>,
329    ) -> Result<Self, EntitiesError> {
330        let schema = schema.map(|s| cedar_policy_core::validator::CoreSchema::new(&s.0));
331        let eparser = cedar_policy_core::entities::EntityJsonParser::new(
332            schema.as_ref(),
333            Extensions::all_available(),
334            cedar_policy_core::entities::TCComputation::ComputeNow,
335        );
336        eparser.single_from_json_value(value).map(Self)
337    }
338
339    /// Parse an entity from a JSON string
340    /// If a schema is provided, it is handled identically to [`Entities::from_json_str`]
341    pub fn from_json_str(
342        src: impl AsRef<str>,
343        schema: Option<&Schema>,
344    ) -> Result<Self, EntitiesError> {
345        let schema = schema.map(|s| cedar_policy_core::validator::CoreSchema::new(&s.0));
346        let eparser = cedar_policy_core::entities::EntityJsonParser::new(
347            schema.as_ref(),
348            Extensions::all_available(),
349            cedar_policy_core::entities::TCComputation::ComputeNow,
350        );
351        eparser.single_from_json_str(src).map(Self)
352    }
353
354    /// Parse an entity from a JSON reader
355    /// If a schema is provided, it is handled identically to [`Entities::from_json_str`]
356    pub fn from_json_file(f: impl Read, schema: Option<&Schema>) -> Result<Self, EntitiesError> {
357        let schema = schema.map(|s| cedar_policy_core::validator::CoreSchema::new(&s.0));
358        let eparser = cedar_policy_core::entities::EntityJsonParser::new(
359            schema.as_ref(),
360            Extensions::all_available(),
361            cedar_policy_core::entities::TCComputation::ComputeNow,
362        );
363        eparser.single_from_json_file(f).map(Self)
364    }
365
366    /// Dump an `Entity` object into an entity JSON file.
367    ///
368    /// The resulting JSON will be suitable for parsing in via
369    /// `from_json_*`, and will be parse-able even with no [`Schema`].
370    ///
371    /// To read an `Entity` object from JSON , use
372    /// [`Self::from_json_file`], [`Self::from_json_value`], or [`Self::from_json_str`].
373    pub fn write_to_json(&self, f: impl std::io::Write) -> Result<(), EntitiesError> {
374        self.0.write_to_json(f)
375    }
376
377    /// Dump an `Entity` object into an in-memory JSON object.
378    ///
379    /// The resulting JSON will be suitable for parsing in via
380    /// `from_json_*`, and will be parse-able even with no `Schema`.
381    ///
382    /// To read an `Entity` object from JSON , use
383    /// [`Self::from_json_file`], [`Self::from_json_value`], or [`Self::from_json_str`].
384    pub fn to_json_value(&self) -> Result<serde_json::Value, EntitiesError> {
385        self.0.to_json_value()
386    }
387
388    /// Dump an `Entity` object into a JSON string.
389    ///
390    /// The resulting JSON will be suitable for parsing in via
391    /// `from_json_*`, and will be parse-able even with no `Schema`.
392    ///
393    /// To read an `Entity` object from JSON , use
394    /// [`Self::from_json_file`], [`Self::from_json_value`], or [`Self::from_json_str`].
395    pub fn to_json_string(&self) -> Result<String, EntitiesError> {
396        self.0.to_json_string()
397    }
398}
399
400impl std::fmt::Display for Entity {
401    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
402        write!(f, "{}", self.0)
403    }
404}
405
406/// Represents an entity hierarchy, and allows looking up `Entity` objects by
407/// Uid.
408#[repr(transparent)]
409#[derive(Debug, Clone, Default, PartialEq, Eq, RefCast)]
410pub struct Entities(pub(crate) cedar_policy_core::entities::Entities);
411
412#[doc(hidden)] // because this converts to a private/internal type
413impl AsRef<cedar_policy_core::entities::Entities> for Entities {
414    fn as_ref(&self) -> &cedar_policy_core::entities::Entities {
415        &self.0
416    }
417}
418
419#[doc(hidden)]
420impl From<cedar_policy_core::entities::Entities> for Entities {
421    fn from(entities: cedar_policy_core::entities::Entities) -> Self {
422        Self(entities)
423    }
424}
425
426use entities_errors::EntitiesError;
427
428impl Entities {
429    /// Create a fresh `Entities` with no entities
430    /// ```
431    /// # use cedar_policy::Entities;
432    /// let entities = Entities::empty();
433    /// # assert!(entities.is_empty());
434    /// ```
435    pub fn empty() -> Self {
436        Self(cedar_policy_core::entities::Entities::new())
437    }
438
439    /// Get the `Entity` with the given Uid, if any
440    pub fn get(&self, uid: &EntityUid) -> Option<&Entity> {
441        match self.0.entity(uid.as_ref()) {
442            Dereference::Residual(_) | Dereference::NoSuchEntity => None,
443            Dereference::Data(e) => Some(Entity::ref_cast(e)),
444        }
445    }
446
447    /// Transform the store into a partial store, where
448    /// attempting to dereference a non-existent `EntityUid` results in
449    /// a residual instead of an error.
450    #[doc = include_str!("../experimental_warning.md")]
451    #[must_use]
452    #[cfg(feature = "partial-eval")]
453    pub fn partial(self) -> Self {
454        Self(self.0.partial())
455    }
456
457    /// Iterate over the `Entity`'s in the `Entities`
458    pub fn iter(&self) -> impl Iterator<Item = &Entity> {
459        self.0.iter().map(Entity::ref_cast)
460    }
461
462    /// Test if two entity hierarchies are structurally equal. The hierarchies
463    /// must contain the same set of entity ids, and the entities with each id
464    /// must be structurally equal (decided by [`Entity::deep_eq`]). Ancestor
465    /// equality between entities is always decided by comparing the transitive
466    /// closure of ancestor and not direct parents.
467    pub fn deep_eq(&self, other: &Self) -> bool {
468        self.0.deep_eq(&other.0)
469    }
470
471    /// Create an `Entities` object with the given entities.
472    ///
473    /// `schema` represents a source of `Action` entities, which will be added
474    /// to the entities provided.
475    /// (If any `Action` entities are present in the provided entities, and a
476    /// `schema` is also provided, each `Action` entity in the provided entities
477    /// must exactly match its definition in the schema or an error is
478    /// returned.)
479    ///
480    /// If a `schema` is present, this function will also ensure that the
481    /// produced entities fully conform to the `schema` -- for instance, it will
482    /// error if attributes have the wrong types (e.g., string instead of
483    /// integer), or if required attributes are missing or superfluous
484    /// attributes are provided.
485    /// ## Errors
486    /// - [`EntitiesError::Duplicate`] if there are any duplicate entities in `entities`
487    /// - [`EntitiesError::InvalidEntity`] if `schema` is not none and any entities do not conform
488    ///   to the schema
489    pub fn from_entities(
490        entities: impl IntoIterator<Item = Entity>,
491        schema: Option<&Schema>,
492    ) -> Result<Self, EntitiesError> {
493        cedar_policy_core::entities::Entities::from_entities(
494            entities.into_iter().map(|e| e.0),
495            schema
496                .map(|s| cedar_policy_core::validator::CoreSchema::new(&s.0))
497                .as_ref(),
498            cedar_policy_core::entities::TCComputation::ComputeNow,
499            Extensions::all_available(),
500        )
501        .map(Entities)
502    }
503
504    /// Add all of the [`Entity`]s in the collection to this [`Entities`]
505    /// structure, re-computing the transitive closure.
506    ///
507    /// If a `schema` is provided, this method will ensure that the added
508    /// entities fully conform to the schema -- for instance, it will error if
509    /// attributes have the wrong types (e.g., string instead of integer), or if
510    /// required attributes are missing or superfluous attributes are provided.
511    /// (This method will not add action entities from the `schema`.)
512    ///
513    /// Re-computing the transitive closure can be expensive, so it is advised
514    /// to not call this method in a loop.
515    /// ## Errors
516    /// - [`EntitiesError::Duplicate`] if there is a pair of non-identical entities in `entities` with the same Entity UID,
517    ///   or there is an entity in `entities` with the same Entity UID as a non-identical entity in this structure
518    /// - [`EntitiesError::InvalidEntity`] if `schema` is not none and any entities do not conform
519    ///   to the schema
520    pub fn add_entities(
521        self,
522        entities: impl IntoIterator<Item = Entity>,
523        schema: Option<&Schema>,
524    ) -> Result<Self, EntitiesError> {
525        Ok(Self(
526            self.0.add_entities(
527                entities.into_iter().map(|e| Arc::new(e.0)),
528                schema
529                    .map(|s| cedar_policy_core::validator::CoreSchema::new(&s.0))
530                    .as_ref(),
531                cedar_policy_core::entities::TCComputation::ComputeNow,
532                Extensions::all_available(),
533            )?,
534        ))
535    }
536
537    /// Removes each of the [`EntityUid`]s in the iterator
538    /// from this [`Entities`] structure, re-computing the transitive
539    /// closure after removing all edges to/from the removed entities.
540    ///
541    /// Re-computing the transitive closure can be expensive, so it is
542    /// advised to not call this method in a loop.
543    pub fn remove_entities(
544        self,
545        entity_ids: impl IntoIterator<Item = EntityUid>,
546    ) -> Result<Self, EntitiesError> {
547        Ok(Self(self.0.remove_entities(
548            entity_ids.into_iter().map(|euid| euid.0),
549            cedar_policy_core::entities::TCComputation::ComputeNow,
550        )?))
551    }
552
553    /// Updates or adds all of the [`Entity`]s in the collection to this [`Entities`]
554    /// structure, re-computing the transitive closure.
555    ///
556    /// If a `schema` is provided, this method will ensure that the added
557    /// entities fully conform to the schema -- for instance, it will error if
558    /// attributes have the wrong types (e.g., string instead of integer), or if
559    /// required attributes are missing or superfluous attributes are provided.
560    /// (This method will not add action entities from the `schema`.)
561    ///
562    /// Re-computing the transitive closure can be expensive, so it is advised
563    /// to not call this method in a loop.
564    /// ## Errors
565    /// - [`EntitiesError::InvalidEntity`] if `schema` is not none and any entities do not conform
566    ///   to the schema
567    pub fn upsert_entities(
568        self,
569        entities: impl IntoIterator<Item = Entity>,
570        schema: Option<&Schema>,
571    ) -> Result<Self, EntitiesError> {
572        Ok(Self(
573            self.0.upsert_entities(
574                entities.into_iter().map(|e| Arc::new(e.0)),
575                schema
576                    .map(|s| cedar_policy_core::validator::CoreSchema::new(&s.0))
577                    .as_ref(),
578                cedar_policy_core::entities::TCComputation::ComputeNow,
579                Extensions::all_available(),
580            )?,
581        ))
582    }
583
584    /// Parse an entities JSON file (in [&str] form) and add them into this
585    /// [`Entities`] structure, re-computing the transitive closure
586    ///
587    /// If a `schema` is provided, this will inform the parsing: for instance, it
588    /// will allow `__entity` and `__extn` escapes to be implicit.
589    /// This method will also ensure that the added entities fully conform to the
590    /// schema -- for instance, it will error if attributes have the wrong types
591    /// (e.g., string instead of integer), or if required attributes are missing
592    /// or superfluous attributes are provided.
593    /// (This method will not add action entities from the `schema`.)
594    ///
595    /// Re-computing the transitive closure can be expensive, so it is advised
596    /// to not call this method in a loop.
597    /// ## Errors
598    /// - [`EntitiesError::Duplicate`] if there is a pair of non-identical entities in
599    ///   `entities` with the same Entity UID, or there is an entity in `entities` with the
600    ///   same Entity UID as a non-identical entity in this structure
601    /// - [`EntitiesError::InvalidEntity`] if `schema` is not none and any entities do not conform
602    ///   to the schema
603    /// - [`EntitiesError::Deserialization`] if there are errors while parsing the json
604    pub fn add_entities_from_json_str(
605        self,
606        json: &str,
607        schema: Option<&Schema>,
608    ) -> Result<Self, EntitiesError> {
609        let schema = schema.map(|s| cedar_policy_core::validator::CoreSchema::new(&s.0));
610        let eparser = cedar_policy_core::entities::EntityJsonParser::new(
611            schema.as_ref(),
612            Extensions::all_available(),
613            cedar_policy_core::entities::TCComputation::ComputeNow,
614        );
615        let new_entities = eparser.iter_from_json_str(json)?.map(Arc::new);
616        Ok(Self(self.0.add_entities(
617            new_entities,
618            schema.as_ref(),
619            cedar_policy_core::entities::TCComputation::ComputeNow,
620            Extensions::all_available(),
621        )?))
622    }
623
624    /// Parse an entities JSON file (in [`serde_json::Value`] form) and add them
625    /// into this [`Entities`] structure, re-computing the transitive closure
626    ///
627    /// If a `schema` is provided, this will inform the parsing: for instance, it
628    /// will allow `__entity` and `__extn` escapes to be implicit.
629    /// This method will also ensure that the added entities fully conform to the
630    /// schema -- for instance, it will error if attributes have the wrong types
631    /// (e.g., string instead of integer), or if required attributes are missing
632    /// or superfluous attributes are provided.
633    /// (This method will not add action entities from the `schema`.)
634    ///
635    /// Re-computing the transitive closure can be expensive, so it is advised
636    /// to not call this method in a loop.
637    /// ## Errors
638    /// - [`EntitiesError::Duplicate`] if there is a pair of non-identical entities in
639    ///   `entities` with the same Entity UID, or there is an entity in `entities` with the same
640    ///   Entity UID as a non-identical entity in this structure
641    /// - [`EntitiesError::InvalidEntity`] if `schema` is not none and any entities do not conform
642    ///   to the schema
643    /// - [`EntitiesError::Deserialization`] if there are errors while parsing the json
644    pub fn add_entities_from_json_value(
645        self,
646        json: serde_json::Value,
647        schema: Option<&Schema>,
648    ) -> Result<Self, EntitiesError> {
649        let schema = schema.map(|s| cedar_policy_core::validator::CoreSchema::new(&s.0));
650        let eparser = cedar_policy_core::entities::EntityJsonParser::new(
651            schema.as_ref(),
652            Extensions::all_available(),
653            cedar_policy_core::entities::TCComputation::ComputeNow,
654        );
655        let new_entities = eparser.iter_from_json_value(json)?.map(Arc::new);
656        Ok(Self(self.0.add_entities(
657            new_entities,
658            schema.as_ref(),
659            cedar_policy_core::entities::TCComputation::ComputeNow,
660            Extensions::all_available(),
661        )?))
662    }
663
664    /// Parse an entities JSON file (in [`std::io::Read`] form) and add them
665    /// into this [`Entities`] structure, re-computing the transitive closure
666    ///
667    /// If a `schema` is provided, this will inform the parsing: for instance, it
668    /// will allow `__entity` and `__extn` escapes to be implicit.
669    /// This method will also ensure that the added entities fully conform to the
670    /// schema -- for instance, it will error if attributes have the wrong types
671    /// (e.g., string instead of integer), or if required attributes are missing
672    /// or superfluous attributes are provided.
673    /// (This method will not add action entities from the `schema`.)
674    ///
675    /// Re-computing the transitive closure can be expensive, so it is advised
676    /// to not call this method in a loop.
677    ///
678    /// ## Errors
679    /// - [`EntitiesError::Duplicate`] if there is a pair of non-identical entities in `entities`
680    ///   with the same Entity UID, or there is an entity in `entities` with the same Entity UID as a
681    ///   non-identical entity in this structure
682    /// - [`EntitiesError::InvalidEntity`] if `schema` is not none and any entities do not conform
683    ///   to the schema
684    /// - [`EntitiesError::Deserialization`] if there are errors while parsing the json
685    pub fn add_entities_from_json_file(
686        self,
687        json: impl std::io::Read,
688        schema: Option<&Schema>,
689    ) -> Result<Self, EntitiesError> {
690        let schema = schema.map(|s| cedar_policy_core::validator::CoreSchema::new(&s.0));
691        let eparser = cedar_policy_core::entities::EntityJsonParser::new(
692            schema.as_ref(),
693            Extensions::all_available(),
694            cedar_policy_core::entities::TCComputation::ComputeNow,
695        );
696        let new_entities = eparser.iter_from_json_file(json)?.map(Arc::new);
697        Ok(Self(self.0.add_entities(
698            new_entities,
699            schema.as_ref(),
700            cedar_policy_core::entities::TCComputation::ComputeNow,
701            Extensions::all_available(),
702        )?))
703    }
704
705    /// Parse an entities JSON file (in `&str` form) into an `Entities` object
706    ///
707    /// `schema` represents a source of `Action` entities, which will be added
708    /// to the entities parsed from JSON.
709    /// (If any `Action` entities are present in the JSON, and a `schema` is
710    /// also provided, each `Action` entity in the JSON must exactly match its
711    /// definition in the schema or an error is returned.)
712    ///
713    /// If a `schema` is present, this will also inform the parsing: for
714    /// instance, it will allow `__entity` and `__extn` escapes to be implicit.
715    ///
716    /// Finally, if a `schema` is present, this function will ensure
717    /// that the produced entities fully conform to the `schema` -- for
718    /// instance, it will error if attributes have the wrong types (e.g., string
719    /// instead of integer), or if required attributes are missing or
720    /// superfluous attributes are provided.
721    ///
722    /// ## Errors
723    /// - [`EntitiesError::Duplicate`] if there are any duplicate entities in `entities`
724    /// - [`EntitiesError::InvalidEntity`] if `schema` is not none and any entities do not conform
725    ///   to the schema
726    /// - [`EntitiesError::Deserialization`] if there are errors while parsing the json
727    ///
728    /// ```
729    /// # use cedar_policy::{Entities, EntityId, EntityTypeName, EntityUid, EvalResult, Request,PolicySet};
730    /// # use std::str::FromStr;
731    /// let data =r#"
732    /// [
733    /// {
734    ///   "uid": {"type":"User","id":"alice"},
735    ///   "attrs": {
736    ///     "age":19,
737    ///     "ip_addr":{"__extn":{"fn":"ip", "arg":"10.0.1.101"}}
738    ///   },
739    ///   "parents": [{"type":"Group","id":"admin"}]
740    /// },
741    /// {
742    ///   "uid": {"type":"Group","id":"admin"},
743    ///   "attrs": {},
744    ///   "parents": []
745    /// }
746    /// ]
747    /// "#;
748    /// let entities = Entities::from_json_str(data, None).unwrap();
749    /// # let euid = EntityUid::from_str(r#"User::"alice""#).unwrap();
750    /// # let entity = entities.get(&euid).unwrap();
751    /// # assert_eq!(entity.attr("age").unwrap().unwrap(), EvalResult::Long(19));
752    /// # let ip = entity.attr("ip_addr").unwrap().unwrap();
753    /// # assert_eq!(ip, EvalResult::ExtensionValue("ip(\"10.0.1.101\")".to_string()));
754    /// ```
755    pub fn from_json_str(json: &str, schema: Option<&Schema>) -> Result<Self, EntitiesError> {
756        let schema = schema.map(|s| cedar_policy_core::validator::CoreSchema::new(&s.0));
757        let eparser = cedar_policy_core::entities::EntityJsonParser::new(
758            schema.as_ref(),
759            Extensions::all_available(),
760            cedar_policy_core::entities::TCComputation::ComputeNow,
761        );
762        eparser.from_json_str(json).map(Entities)
763    }
764
765    /// Parse an entities JSON file (in `serde_json::Value` form) into an
766    /// `Entities` object
767    ///
768    /// `schema` represents a source of `Action` entities, which will be added
769    /// to the entities parsed from JSON.
770    /// (If any `Action` entities are present in the JSON, and a `schema` is
771    /// also provided, each `Action` entity in the JSON must exactly match its
772    /// definition in the schema or an error is returned.)
773    ///
774    /// If a `schema` is present, this will also inform the parsing: for
775    /// instance, it will allow `__entity` and `__extn` escapes to be implicit.
776    ///
777    /// Finally, if a `schema` is present, this function will ensure
778    /// that the produced entities fully conform to the `schema` -- for
779    /// instance, it will error if attributes have the wrong types (e.g., string
780    /// instead of integer), or if required attributes are missing or
781    /// superfluous attributes are provided.
782    ///
783    /// ## Errors
784    /// - [`EntitiesError::Duplicate`] if there are any duplicate entities in `entities`
785    /// - [`EntitiesError::InvalidEntity`]if `schema` is not none and any entities do not conform
786    ///   to the schema
787    /// - [`EntitiesError::Deserialization`] if there are errors while parsing the json
788    ///
789    /// ```
790    /// # use cedar_policy::{Entities, EntityId, EntityTypeName, EntityUid, EvalResult, Request,PolicySet};
791    /// let data =serde_json::json!(
792    /// [
793    /// {
794    ///   "uid": {"type":"User","id":"alice"},
795    ///   "attrs": {
796    ///     "age":19,
797    ///     "ip_addr":{"__extn":{"fn":"ip", "arg":"10.0.1.101"}}
798    ///   },
799    ///   "parents": [{"type":"Group","id":"admin"}]
800    /// },
801    /// {
802    ///   "uid": {"type":"Group","id":"admin"},
803    ///   "attrs": {},
804    ///   "parents": []
805    /// }
806    /// ]
807    /// );
808    /// let entities = Entities::from_json_value(data, None).unwrap();
809    /// ```
810    pub fn from_json_value(
811        json: serde_json::Value,
812        schema: Option<&Schema>,
813    ) -> Result<Self, EntitiesError> {
814        let schema = schema.map(|s| cedar_policy_core::validator::CoreSchema::new(&s.0));
815        let eparser = cedar_policy_core::entities::EntityJsonParser::new(
816            schema.as_ref(),
817            Extensions::all_available(),
818            cedar_policy_core::entities::TCComputation::ComputeNow,
819        );
820        eparser.from_json_value(json).map(Entities)
821    }
822
823    /// Parse an entities JSON file (in `std::io::Read` form) into an `Entities`
824    /// object
825    ///
826    /// `schema` represents a source of `Action` entities, which will be added
827    /// to the entities parsed from JSON.
828    /// (If any `Action` entities are present in the JSON, and a `schema` is
829    /// also provided, each `Action` entity in the JSON must exactly match its
830    /// definition in the schema or an error is returned.)
831    ///
832    /// If a `schema` is present, this will also inform the parsing: for
833    /// instance, it will allow `__entity` and `__extn` escapes to be implicit.
834    ///
835    /// Finally, if a `schema` is present, this function will ensure
836    /// that the produced entities fully conform to the `schema` -- for
837    /// instance, it will error if attributes have the wrong types (e.g., string
838    /// instead of integer), or if required attributes are missing or
839    /// superfluous attributes are provided.
840    ///
841    /// ## Errors
842    /// - [`EntitiesError::Duplicate`] if there are any duplicate entities in `entities`
843    /// - [`EntitiesError::InvalidEntity`] if `schema` is not none and any entities do not conform
844    ///   to the schema
845    /// - [`EntitiesError::Deserialization`] if there are errors while parsing the json
846    pub fn from_json_file(
847        json: impl std::io::Read,
848        schema: Option<&Schema>,
849    ) -> Result<Self, EntitiesError> {
850        let schema = schema.map(|s| cedar_policy_core::validator::CoreSchema::new(&s.0));
851        let eparser = cedar_policy_core::entities::EntityJsonParser::new(
852            schema.as_ref(),
853            Extensions::all_available(),
854            cedar_policy_core::entities::TCComputation::ComputeNow,
855        );
856        eparser.from_json_file(json).map(Entities)
857    }
858
859    /// Is entity `a` an ancestor of entity `b`?
860    /// Same semantics as `b in a` in the Cedar language
861    pub fn is_ancestor_of(&self, a: &EntityUid, b: &EntityUid) -> bool {
862        match self.0.entity(b.as_ref()) {
863            Dereference::Data(b) => b.is_descendant_of(a.as_ref()),
864            _ => a == b, // if b doesn't exist, `b in a` is only true if `b == a`
865        }
866    }
867
868    /// Get an iterator over the ancestors of the given Euid.
869    /// Returns `None` if the given `Euid` does not exist.
870    pub fn ancestors<'a>(
871        &'a self,
872        euid: &EntityUid,
873    ) -> Option<impl Iterator<Item = &'a EntityUid>> {
874        let entity = match self.0.entity(euid.as_ref()) {
875            Dereference::Residual(_) | Dereference::NoSuchEntity => None,
876            Dereference::Data(e) => Some(e),
877        }?;
878        Some(entity.ancestors().map(EntityUid::ref_cast))
879    }
880
881    /// Returns the number of `Entity`s in the `Entities`
882    pub fn len(&self) -> usize {
883        self.0.len()
884    }
885
886    /// Returns true if the `Entities` contains no `Entity`s
887    pub fn is_empty(&self) -> bool {
888        self.0.is_empty()
889    }
890
891    /// Dump an `Entities` object into an entities JSON file.
892    ///
893    /// The resulting JSON will be suitable for parsing in via
894    /// `from_json_*`, and will be parse-able even with no `Schema`.
895    ///
896    /// To read an `Entities` object from an entities JSON file, use
897    /// `from_json_file`.
898    pub fn write_to_json(&self, f: impl std::io::Write) -> std::result::Result<(), EntitiesError> {
899        self.0.write_to_json(f)
900    }
901
902    /// Dump an `Entities` object into an in-memory JSON object.
903    ///
904    /// The resulting JSON will be suitable for parsing in via
905    /// `from_json_*`, and will be parse-able even with no `Schema`.
906    ///
907    /// To read an `Entities` object from JSON, use
908    /// [`Self::from_json_file`], [`Self::from_json_value`], or [`Self::from_json_str`].
909    pub fn to_json_value(&self) -> Result<serde_json::Value, EntitiesError> {
910        self.0.to_json_value()
911    }
912
913    #[doc = include_str!("../experimental_warning.md")]
914    /// Visualize an `Entities` object in the graphviz `dot`
915    /// format. Entity visualization is best-effort and not well tested.
916    /// Feel free to submit an issue if you are using this feature and would like it improved.
917    pub fn to_dot_str(&self) -> String {
918        let mut dot_str = String::new();
919        #[expect(clippy::unwrap_used, reason = "writing to a String cannot fail")]
920        self.0.to_dot_str(&mut dot_str).unwrap();
921        dot_str
922    }
923}
924
925/// Validates scope variables against the provided schema
926///
927/// Returns Ok(()) if the context is valid according to the schema, or an error otherwise
928///
929/// This validation is already handled by `Request::new`, so there is no need to separately call
930/// if you are validating the whole request
931pub fn validate_scope_variables(
932    principal: &EntityUid,
933    action: &EntityUid,
934    resource: &EntityUid,
935    schema: &Schema,
936) -> std::result::Result<(), RequestValidationError> {
937    Ok(RequestSchema::validate_scope_variables(
938        &schema.0,
939        Some(&principal.0),
940        Some(&action.0),
941        Some(&resource.0),
942    )?)
943}
944
945/// Utilities for defining `IntoIterator` over `Entities`
946pub mod entities {
947
948    /// `IntoIter` iterator for `Entities`
949    #[derive(Debug)]
950    pub struct IntoIter {
951        pub(super) inner: <cedar_policy_core::entities::Entities as IntoIterator>::IntoIter,
952    }
953
954    impl Iterator for IntoIter {
955        type Item = super::Entity;
956
957        fn next(&mut self) -> Option<Self::Item> {
958            self.inner.next().map(super::Entity)
959        }
960        fn size_hint(&self) -> (usize, Option<usize>) {
961            self.inner.size_hint()
962        }
963    }
964}
965
966impl IntoIterator for Entities {
967    type Item = Entity;
968    type IntoIter = entities::IntoIter;
969
970    fn into_iter(self) -> Self::IntoIter {
971        Self::IntoIter {
972            inner: self.0.into_iter(),
973        }
974    }
975}
976
977/// Authorizer object, which provides responses to authorization queries
978#[repr(transparent)]
979#[derive(Debug, Clone, RefCast)]
980pub struct Authorizer(authorizer::Authorizer);
981
982#[doc(hidden)] // because this converts to a private/internal type
983impl AsRef<authorizer::Authorizer> for Authorizer {
984    fn as_ref(&self) -> &authorizer::Authorizer {
985        &self.0
986    }
987}
988
989impl Default for Authorizer {
990    fn default() -> Self {
991        Self::new()
992    }
993}
994
995impl Authorizer {
996    /// Create a new `Authorizer`
997    ///
998    /// The authorizer uses the `stacker` crate to manage stack size and tries to use a sane default.
999    /// If the default is not right for you, you can try wrapping the authorizer or individual calls
1000    /// to `is_authorized` in `stacker::grow`.
1001    /// Note that on platforms not supported by `stacker` (e.g., Wasm, Android),
1002    /// the authorizer will simply assume that the stack size is sufficient. As a result, large inputs
1003    /// may result in stack overflows and crashing the process.
1004    /// But on all platforms supported by `stacker` (Linux, macOS, ...), Cedar will return the
1005    /// graceful error `RecursionLimit` instead of crashing.
1006    /// ```
1007    /// # use cedar_policy::{Authorizer, Context, Entities, EntityId, EntityTypeName,
1008    /// # EntityUid, Request,PolicySet};
1009    /// # use std::str::FromStr;
1010    /// # // create a request
1011    /// # let p_eid = EntityId::from_str("alice").unwrap();
1012    /// # let p_name: EntityTypeName = EntityTypeName::from_str("User").unwrap();
1013    /// # let p = EntityUid::from_type_name_and_id(p_name, p_eid);
1014    /// #
1015    /// # let a_eid = EntityId::from_str("view").unwrap();
1016    /// # let a_name: EntityTypeName = EntityTypeName::from_str("Action").unwrap();
1017    /// # let a = EntityUid::from_type_name_and_id(a_name, a_eid);
1018    /// #
1019    /// # let r_eid = EntityId::from_str("trip").unwrap();
1020    /// # let r_name: EntityTypeName = EntityTypeName::from_str("Album").unwrap();
1021    /// # let r = EntityUid::from_type_name_and_id(r_name, r_eid);
1022    /// #
1023    /// # let c = Context::empty();
1024    /// #
1025    /// # let request: Request = Request::new(p, a, r, c, None).unwrap();
1026    /// #
1027    /// # // create a policy
1028    /// # let s = r#"permit(
1029    /// #     principal == User::"alice",
1030    /// #     action == Action::"view",
1031    /// #     resource == Album::"trip"
1032    /// #   )when{
1033    /// #     principal.ip_addr.isIpv4()
1034    /// #   };
1035    /// # "#;
1036    /// # let policy = PolicySet::from_str(s).expect("policy error");
1037    /// # // create entities
1038    /// # let e = r#"[
1039    /// #     {
1040    /// #         "uid": {"type":"User","id":"alice"},
1041    /// #         "attrs": {
1042    /// #             "age":19,
1043    /// #             "ip_addr":{"__extn":{"fn":"ip", "arg":"10.0.1.101"}}
1044    /// #         },
1045    /// #         "parents": []
1046    /// #     }
1047    /// # ]"#;
1048    /// # let entities = Entities::from_json_str(e, None).expect("entity error");
1049    /// let authorizer = Authorizer::new();
1050    /// let r = authorizer.is_authorized(&request, &policy, &entities);
1051    /// ```
1052    pub fn new() -> Self {
1053        Self(authorizer::Authorizer::new())
1054    }
1055
1056    /// Returns an authorization response for `r` with respect to the given
1057    /// `PolicySet` and `Entities`.
1058    ///
1059    /// The language spec and formal model give a precise definition of how this
1060    /// is computed.
1061    /// ```
1062    /// # use cedar_policy::{Authorizer,Context,Decision,Entities,EntityId,EntityTypeName, EntityUid, Request,PolicySet};
1063    /// # use std::str::FromStr;
1064    /// // create a request
1065    /// let p_eid = EntityId::from_str("alice").unwrap();
1066    /// let p_name: EntityTypeName = EntityTypeName::from_str("User").unwrap();
1067    /// let p = EntityUid::from_type_name_and_id(p_name, p_eid);
1068    ///
1069    /// let a_eid = EntityId::from_str("view").unwrap();
1070    /// let a_name: EntityTypeName = EntityTypeName::from_str("Action").unwrap();
1071    /// let a = EntityUid::from_type_name_and_id(a_name, a_eid);
1072    ///
1073    /// let r_eid = EntityId::from_str("trip").unwrap();
1074    /// let r_name: EntityTypeName = EntityTypeName::from_str("Album").unwrap();
1075    /// let r = EntityUid::from_type_name_and_id(r_name, r_eid);
1076    ///
1077    /// let c = Context::empty();
1078    ///
1079    /// let request: Request = Request::new(p, a, r, c, None).unwrap();
1080    ///
1081    /// // create a policy
1082    /// let s = r#"
1083    /// permit (
1084    ///   principal == User::"alice",
1085    ///   action == Action::"view",
1086    ///   resource == Album::"trip"
1087    /// )
1088    /// when { principal.ip_addr.isIpv4() };
1089    /// "#;
1090    /// let policy = PolicySet::from_str(s).expect("policy error");
1091    ///
1092    /// // create entities
1093    /// let e = r#"[
1094    ///     {
1095    ///         "uid": {"type":"User","id":"alice"},
1096    ///         "attrs": {
1097    ///             "age":19,
1098    ///             "ip_addr":{"__extn":{"fn":"ip", "arg":"10.0.1.101"}}
1099    ///         },
1100    ///         "parents": []
1101    ///     }
1102    /// ]"#;
1103    /// let entities = Entities::from_json_str(e, None).expect("entity error");
1104    ///
1105    /// let authorizer = Authorizer::new();
1106    /// let response = authorizer.is_authorized(&request, &policy, &entities);
1107    /// assert_eq!(response.decision(), Decision::Allow);
1108    /// ```
1109    pub fn is_authorized(&self, r: &Request, p: &PolicySet, e: &Entities) -> Response {
1110        self.0.is_authorized(r.0.clone(), &p.ast, &e.0).into()
1111    }
1112
1113    /// A partially evaluated authorization request.
1114    /// The Authorizer will attempt to make as much progress as possible in the presence of unknowns.
1115    /// If the Authorizer can reach a response, it will return that response.
1116    /// Otherwise, it will return a list of residual policies that still need to be evaluated.
1117    #[doc = include_str!("../experimental_warning.md")]
1118    #[cfg(feature = "partial-eval")]
1119    pub fn is_authorized_partial(
1120        &self,
1121        query: &Request,
1122        policy_set: &PolicySet,
1123        entities: &Entities,
1124    ) -> PartialResponse {
1125        let response = self
1126            .0
1127            .is_authorized_core(query.0.clone(), &policy_set.ast, &entities.0);
1128        PartialResponse(response)
1129    }
1130}
1131
1132/// Authorization response returned from the `Authorizer`
1133#[derive(Debug, PartialEq, Eq, Clone)]
1134pub struct Response {
1135    /// Authorization decision
1136    pub(crate) decision: Decision,
1137    /// Diagnostics providing more information on how this decision was reached
1138    pub(crate) diagnostics: Diagnostics,
1139}
1140
1141/// A partially evaluated authorization response.
1142///
1143/// Splits the results into several categories: satisfied, false, and residual for each policy effect.
1144/// Also tracks all the errors that were encountered during evaluation.
1145#[doc = include_str!("../experimental_warning.md")]
1146#[cfg(feature = "partial-eval")]
1147#[repr(transparent)]
1148#[derive(Debug, Clone, RefCast)]
1149pub struct PartialResponse(cedar_policy_core::authorizer::PartialResponse);
1150
1151#[cfg(feature = "partial-eval")]
1152impl PartialResponse {
1153    /// Attempt to reach a partial decision; the presence of residuals may result in returning [`None`],
1154    /// indicating that a decision could not be reached given the unknowns
1155    pub fn decision(&self) -> Option<Decision> {
1156        self.0.decision()
1157    }
1158
1159    /// Convert this response into a concrete evaluation response.
1160    /// All residuals are treated as errors
1161    pub fn concretize(self) -> Response {
1162        self.0.concretize().into()
1163    }
1164
1165    /// Returns the set of [`Policy`]s that were definitely satisfied.
1166    /// This will be the set of policies (both `permit` and `forbid`) that evaluated to `true`
1167    pub fn definitely_satisfied(&self) -> impl Iterator<Item = Policy> + '_ {
1168        self.0.definitely_satisfied().map(Policy::from_ast)
1169    }
1170
1171    /// Returns the set of [`PolicyId`]s that encountered errors
1172    pub fn definitely_errored(&self) -> impl Iterator<Item = &PolicyId> {
1173        self.0.definitely_errored().map(PolicyId::ref_cast)
1174    }
1175
1176    /// Returns an over-approximation of the set of determining policies
1177    ///
1178    /// This is all policies that may be determining for any substitution of the unknowns.
1179    /// Policies not in this set will not affect the final decision, regardless of any
1180    /// substitutions.
1181    ///
1182    /// For more information on what counts as "determining" see: <https://docs.cedarpolicy.com/auth/authorization.html#request-authorization>
1183    pub fn may_be_determining(&self) -> impl Iterator<Item = Policy> + '_ {
1184        self.0.may_be_determining().map(Policy::from_ast)
1185    }
1186
1187    /// Returns an under-approximation of the set of determining policies
1188    ///
1189    /// This is all policies that must be determining for all possible substitutions of the unknowns.
1190    /// This set will include policies that evaluated to `true` and are guaranteed to be
1191    /// contributing to the final authorization decision.
1192    ///
1193    /// For more information on what counts as "determining" see: <https://docs.cedarpolicy.com/auth/authorization.html#request-authorization>
1194    pub fn must_be_determining(&self) -> impl Iterator<Item = Policy> + '_ {
1195        self.0.must_be_determining().map(Policy::from_ast)
1196    }
1197
1198    /// Returns the set of non-trivial (meaning more than just `true` or `false`) residuals expressions
1199    pub fn nontrivial_residuals(&'_ self) -> impl Iterator<Item = Policy> + '_ {
1200        self.0.nontrivial_residuals().map(Policy::from_ast)
1201    }
1202
1203    /// Returns every policy as a residual expression
1204    pub fn all_residuals(&'_ self) -> impl Iterator<Item = Policy> + '_ {
1205        self.0.all_residuals().map(Policy::from_ast)
1206    }
1207
1208    /// Returns all unknown entities during the evaluation of the response
1209    pub fn unknown_entities(&self) -> HashSet<EntityUid> {
1210        let mut entity_uids = HashSet::new();
1211        for policy in self.0.all_residuals() {
1212            entity_uids.extend(policy.unknown_entities().into_iter().map(Into::into));
1213        }
1214        entity_uids
1215    }
1216
1217    /// Return the residual for a given [`PolicyId`], if it exists in the response
1218    pub fn get(&self, id: &PolicyId) -> Option<Policy> {
1219        self.0.get(id.as_ref()).map(Policy::from_ast)
1220    }
1221
1222    /// Attempt to re-authorize this response given a mapping from unknowns to values.
1223    #[expect(
1224        clippy::needless_pass_by_value,
1225        reason = "don't want to change signature of deprecated public function"
1226    )]
1227    #[deprecated = "use reauthorize_with_bindings"]
1228    pub fn reauthorize(
1229        &self,
1230        mapping: HashMap<SmolStr, RestrictedExpression>,
1231        auth: &Authorizer,
1232        es: &Entities,
1233    ) -> Result<Self, ReauthorizationError> {
1234        self.reauthorize_with_bindings(mapping.iter().map(|(k, v)| (k.as_str(), v)), auth, es)
1235    }
1236
1237    /// Attempt to re-authorize this response given a mapping from unknowns to values, provided as an iterator.
1238    /// Exhausts the iterator, returning any evaluation errors in the restricted expressions, regardless whether there is a matching unknown.
1239    pub fn reauthorize_with_bindings<'m>(
1240        &self,
1241        mapping: impl IntoIterator<Item = (&'m str, &'m RestrictedExpression)>,
1242        auth: &Authorizer,
1243        es: &Entities,
1244    ) -> Result<Self, ReauthorizationError> {
1245        let exts = Extensions::all_available();
1246        let evaluator = RestrictedEvaluator::new(exts);
1247        let mapping = mapping
1248            .into_iter()
1249            .map(|(name, expr)| {
1250                evaluator
1251                    .interpret(BorrowedRestrictedExpr::new_unchecked(expr.0.as_ref()))
1252                    .map(|v| (name.into(), v))
1253            })
1254            .collect::<Result<HashMap<_, _>, EvaluationError>>()?;
1255        let r = self.0.reauthorize(&mapping, &auth.0, &es.0)?;
1256        Ok(Self(r))
1257    }
1258}
1259
1260#[cfg(feature = "partial-eval")]
1261#[doc(hidden)]
1262impl From<cedar_policy_core::authorizer::PartialResponse> for PartialResponse {
1263    fn from(pr: cedar_policy_core::authorizer::PartialResponse) -> Self {
1264        Self(pr)
1265    }
1266}
1267
1268/// Diagnostics providing more information on how a `Decision` was reached
1269#[derive(Debug, PartialEq, Eq, Clone)]
1270pub struct Diagnostics {
1271    /// `PolicyId`s of the policies that contributed to the decision.
1272    /// If no policies applied to the request, this set will be empty.
1273    reason: HashSet<PolicyId>,
1274    /// Errors that occurred during authorization. The errors should be
1275    /// treated as unordered, since policies may be evaluated in any order.
1276    errors: Vec<AuthorizationError>,
1277}
1278
1279#[doc(hidden)]
1280impl From<authorizer::Diagnostics> for Diagnostics {
1281    fn from(diagnostics: authorizer::Diagnostics) -> Self {
1282        Self {
1283            reason: diagnostics.reason.into_iter().map(PolicyId::new).collect(),
1284            errors: diagnostics.errors.into_iter().map(Into::into).collect(),
1285        }
1286    }
1287}
1288
1289impl Diagnostics {
1290    /// Get the `PolicyId`s of the policies that contributed to the decision.
1291    /// If no policies applied to the request, this set will be empty.
1292    /// ```
1293    /// # use cedar_policy::{Authorizer, Context, Decision, Entities, EntityId, EntityTypeName,
1294    /// # EntityUid, Request,PolicySet};
1295    /// # use std::str::FromStr;
1296    /// # // create a request
1297    /// # let p_eid = EntityId::from_str("alice").unwrap();
1298    /// # let p_name: EntityTypeName = EntityTypeName::from_str("User").unwrap();
1299    /// # let p = EntityUid::from_type_name_and_id(p_name, p_eid);
1300    /// #
1301    /// # let a_eid = EntityId::from_str("view").unwrap();
1302    /// # let a_name: EntityTypeName = EntityTypeName::from_str("Action").unwrap();
1303    /// # let a = EntityUid::from_type_name_and_id(a_name, a_eid);
1304    /// #
1305    /// # let r_eid = EntityId::from_str("trip").unwrap();
1306    /// # let r_name: EntityTypeName = EntityTypeName::from_str("Album").unwrap();
1307    /// # let r = EntityUid::from_type_name_and_id(r_name, r_eid);
1308    /// #
1309    /// # let c = Context::empty();
1310    /// #
1311    /// # let request: Request = Request::new(p, a, r, c, None).unwrap();
1312    /// #
1313    /// # // create a policy
1314    /// # let s = r#"permit(
1315    /// #     principal == User::"alice",
1316    /// #     action == Action::"view",
1317    /// #     resource == Album::"trip"
1318    /// #   )when{
1319    /// #     principal.ip_addr.isIpv4()
1320    /// #   };
1321    /// # "#;
1322    /// # let policy = PolicySet::from_str(s).expect("policy error");
1323    /// # // create entities
1324    /// # let e = r#"[
1325    /// #     {
1326    /// #         "uid": {"type":"User","id":"alice"},
1327    /// #         "attrs": {
1328    /// #             "age":19,
1329    /// #             "ip_addr":{"__extn":{"fn":"ip", "arg":"10.0.1.101"}}
1330    /// #         },
1331    /// #         "parents": []
1332    /// #     }
1333    /// # ]"#;
1334    /// # let entities = Entities::from_json_str(e, None).expect("entity error");
1335    /// let authorizer = Authorizer::new();
1336    /// let response = authorizer.is_authorized(&request, &policy, &entities);
1337    /// match response.decision() {
1338    ///     Decision::Allow => println!("ALLOW"),
1339    ///     Decision::Deny => println!("DENY"),
1340    /// }
1341    /// println!("note: this decision was due to the following policies:");
1342    /// for reason in response.diagnostics().reason() {
1343    ///     println!("{}", reason);
1344    /// }
1345    /// ```
1346    pub fn reason(&self) -> impl Iterator<Item = &PolicyId> {
1347        self.reason.iter()
1348    }
1349
1350    /// Get the errors that occurred during authorization. The errors should be
1351    /// treated as unordered, since policies may be evaluated in any order.
1352    /// ```
1353    /// # use cedar_policy::{Authorizer, Context, Decision, Entities, EntityId, EntityTypeName,
1354    /// # EntityUid, Request,PolicySet};
1355    /// # use std::str::FromStr;
1356    /// # // create a request
1357    /// # let p_eid = EntityId::from_str("alice").unwrap();
1358    /// # let p_name: EntityTypeName = EntityTypeName::from_str("User").unwrap();
1359    /// # let p = EntityUid::from_type_name_and_id(p_name, p_eid);
1360    /// #
1361    /// # let a_eid = EntityId::from_str("view").unwrap();
1362    /// # let a_name: EntityTypeName = EntityTypeName::from_str("Action").unwrap();
1363    /// # let a = EntityUid::from_type_name_and_id(a_name, a_eid);
1364    /// #
1365    /// # let r_eid = EntityId::from_str("trip").unwrap();
1366    /// # let r_name: EntityTypeName = EntityTypeName::from_str("Album").unwrap();
1367    /// # let r = EntityUid::from_type_name_and_id(r_name, r_eid);
1368    /// #
1369    /// # let c = Context::empty();
1370    /// #
1371    /// # let request: Request = Request::new(p, a, r, c, None).unwrap();
1372    /// #
1373    /// # // create a policy
1374    /// # let s = r#"permit(
1375    /// #     principal == User::"alice",
1376    /// #     action == Action::"view",
1377    /// #     resource == Album::"trip"
1378    /// #   )when{
1379    /// #     principal.ip_addr.isIpv4()
1380    /// #   };
1381    /// # "#;
1382    /// # let policy = PolicySet::from_str(s).expect("policy error");
1383    /// # // create entities
1384    /// # let e = r#"[
1385    /// #     {
1386    /// #         "uid": {"type":"User","id":"alice"},
1387    /// #         "attrs": {
1388    /// #             "age":19,
1389    /// #             "ip_addr":{"__extn":{"fn":"ip", "arg":"10.0.1.101"}}
1390    /// #         },
1391    /// #         "parents": []
1392    /// #     }
1393    /// # ]"#;
1394    /// # let entities = Entities::from_json_str(e, None).expect("entity error");
1395    /// let authorizer = Authorizer::new();
1396    /// let response = authorizer.is_authorized(&request, &policy, &entities);
1397    /// match response.decision() {
1398    ///     Decision::Allow => println!("ALLOW"),
1399    ///     Decision::Deny => println!("DENY"),
1400    /// }
1401    /// for err in response.diagnostics().errors() {
1402    ///     println!("{}", err);
1403    /// }
1404    /// ```
1405    pub fn errors(&self) -> impl Iterator<Item = &AuthorizationError> + '_ {
1406        self.errors.iter()
1407    }
1408
1409    /// Consume the `Diagnostics`, producing owned versions of `reason()` and `errors()`
1410    pub(crate) fn into_components(
1411        self,
1412    ) -> (
1413        impl Iterator<Item = PolicyId>,
1414        impl Iterator<Item = AuthorizationError>,
1415    ) {
1416        (self.reason.into_iter(), self.errors.into_iter())
1417    }
1418}
1419
1420impl Response {
1421    /// Create a new `Response`
1422    pub fn new(
1423        decision: Decision,
1424        reason: HashSet<PolicyId>,
1425        errors: Vec<AuthorizationError>,
1426    ) -> Self {
1427        Self {
1428            decision,
1429            diagnostics: Diagnostics { reason, errors },
1430        }
1431    }
1432
1433    /// Get the authorization decision
1434    pub fn decision(&self) -> Decision {
1435        self.decision
1436    }
1437
1438    /// Get the authorization diagnostics
1439    pub fn diagnostics(&self) -> &Diagnostics {
1440        &self.diagnostics
1441    }
1442}
1443
1444#[doc(hidden)]
1445impl From<authorizer::Response> for Response {
1446    fn from(a: authorizer::Response) -> Self {
1447        Self {
1448            decision: a.decision,
1449            diagnostics: a.diagnostics.into(),
1450        }
1451    }
1452}
1453
1454/// Used to select how a policy will be validated.
1455#[derive(Default, Eq, PartialEq, Copy, Clone, Debug, Serialize, Deserialize)]
1456#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
1457#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
1458#[serde(rename_all = "camelCase")]
1459#[non_exhaustive]
1460pub enum ValidationMode {
1461    /// Validate that policies do not contain any type errors, and additionally
1462    /// have a restricted form which is amenable for analysis.
1463    #[default]
1464    Strict,
1465    /// Validate that policies do not contain any type errors.
1466    #[doc = include_str!("../experimental_warning.md")]
1467    #[cfg(feature = "permissive-validate")]
1468    Permissive,
1469    /// Validate using a partial schema. Policies may contain type errors.
1470    #[doc = include_str!("../experimental_warning.md")]
1471    #[cfg(feature = "partial-validate")]
1472    Partial,
1473}
1474
1475#[doc(hidden)]
1476impl From<ValidationMode> for cedar_policy_core::validator::ValidationMode {
1477    fn from(mode: ValidationMode) -> Self {
1478        match mode {
1479            ValidationMode::Strict => Self::Strict,
1480            #[cfg(feature = "permissive-validate")]
1481            ValidationMode::Permissive => Self::Permissive,
1482            #[cfg(feature = "partial-validate")]
1483            ValidationMode::Partial => Self::Partial,
1484        }
1485    }
1486}
1487
1488/// Validator object, which provides policy validation and typechecking.
1489#[repr(transparent)]
1490#[derive(Debug, Clone, RefCast)]
1491pub struct Validator(cedar_policy_core::validator::Validator);
1492
1493#[doc(hidden)] // because this converts to a private/internal type
1494impl AsRef<cedar_policy_core::validator::Validator> for Validator {
1495    fn as_ref(&self) -> &cedar_policy_core::validator::Validator {
1496        &self.0
1497    }
1498}
1499
1500impl Validator {
1501    /// Construct a new `Validator` to validate policies using the given
1502    /// `Schema`.
1503    pub fn new(schema: Schema) -> Self {
1504        Self(cedar_policy_core::validator::Validator::new(schema.0))
1505    }
1506
1507    /// Get the `Schema` this `Validator` is using.
1508    pub fn schema(&self) -> &Schema {
1509        RefCast::ref_cast(self.0.schema())
1510    }
1511
1512    /// Validate all policies in a policy set, collecting all validation errors
1513    /// found into the returned `ValidationResult`. Each error is returned together with the
1514    /// policy id of the policy where the error was found. If a policy id
1515    /// included in the input policy set does not appear in the output iterator, then
1516    /// that policy passed the validator. If the function `validation_passed`
1517    /// returns true, then there were no validation errors found, so all
1518    /// policies in the policy set have passed the validator.
1519    pub fn validate(&self, pset: &PolicySet, mode: ValidationMode) -> ValidationResult {
1520        ValidationResult::from(self.0.validate(&pset.ast, mode.into()))
1521    }
1522
1523    /// Validate all policies in a policy set, collecting all validation errors
1524    /// found into the returned `ValidationResult`. If validation passes, run level
1525    /// validation (RFC 76). Each error is returned together with the policy id of the policy
1526    /// where the error was found. If a policy id included in the input policy set does not
1527    /// appear in the output iterator, then that policy passed the validator. If the function
1528    /// `validation_passed` returns true, then there were no validation errors found, so
1529    /// all policies in the policy set have passed the validator.
1530    pub fn validate_with_level(
1531        &self,
1532        pset: &PolicySet,
1533        mode: ValidationMode,
1534        max_deref_level: u32,
1535    ) -> ValidationResult {
1536        ValidationResult::from(
1537            self.0
1538                .validate_with_level(&pset.ast, mode.into(), max_deref_level),
1539        )
1540    }
1541}
1542
1543/// Contains all the type information used to construct a `Schema` that can be
1544/// used to validate a policy.
1545#[derive(Debug, Clone)]
1546pub struct SchemaFragment {
1547    value: cedar_policy_core::validator::ValidatorSchemaFragment<
1548        cedar_policy_core::validator::ConditionalName,
1549        cedar_policy_core::validator::ConditionalName,
1550    >,
1551    lossless:
1552        cedar_policy_core::validator::json_schema::Fragment<cedar_policy_core::validator::RawName>,
1553}
1554
1555#[doc(hidden)] // because this converts to a private/internal type
1556impl
1557    AsRef<
1558        cedar_policy_core::validator::ValidatorSchemaFragment<
1559            cedar_policy_core::validator::ConditionalName,
1560            cedar_policy_core::validator::ConditionalName,
1561        >,
1562    > for SchemaFragment
1563{
1564    fn as_ref(
1565        &self,
1566    ) -> &cedar_policy_core::validator::ValidatorSchemaFragment<
1567        cedar_policy_core::validator::ConditionalName,
1568        cedar_policy_core::validator::ConditionalName,
1569    > {
1570        &self.value
1571    }
1572}
1573
1574#[doc(hidden)] // because this converts from a private/internal type
1575impl
1576    TryFrom<
1577        cedar_policy_core::validator::json_schema::Fragment<cedar_policy_core::validator::RawName>,
1578    > for SchemaFragment
1579{
1580    type Error = SchemaError;
1581    fn try_from(
1582        json_frag: cedar_policy_core::validator::json_schema::Fragment<
1583            cedar_policy_core::validator::RawName,
1584        >,
1585    ) -> Result<Self, Self::Error> {
1586        Ok(Self {
1587            value: json_frag.clone().try_into()?,
1588            lossless: json_frag,
1589        })
1590    }
1591}
1592
1593fn get_annotation_by_key(
1594    annotations: &est::Annotations,
1595    annotation_key: impl AsRef<str>,
1596) -> Option<&str> {
1597    annotations
1598        .0
1599        .get(&annotation_key.as_ref().parse().ok()?)
1600        .map(|value| annotation_value_to_str_ref(value.as_ref()))
1601}
1602
1603fn annotation_value_to_str_ref(value: Option<&ast::Annotation>) -> &str {
1604    value.map_or("", |a| a.as_ref())
1605}
1606
1607fn annotations_to_pairs(annotations: &est::Annotations) -> impl Iterator<Item = (&str, &str)> {
1608    annotations
1609        .0
1610        .iter()
1611        .map(|(key, value)| (key.as_ref(), annotation_value_to_str_ref(value.as_ref())))
1612}
1613
1614impl SchemaFragment {
1615    /// Get annotations of a non-empty namespace.
1616    ///
1617    /// We do not allow namespace-level annotations on the empty namespace.
1618    ///
1619    /// Returns `None` if `namespace` is not found in the [`SchemaFragment`]
1620    pub fn namespace_annotations(
1621        &self,
1622        namespace: EntityNamespace,
1623    ) -> Option<impl Iterator<Item = (&str, &str)>> {
1624        self.lossless
1625            .0
1626            .get(&Some(namespace.0))
1627            .map(|ns_def| annotations_to_pairs(&ns_def.annotations))
1628    }
1629
1630    /// Get annotation value of a non-empty namespace by annotation key
1631    /// `annotation_key`
1632    ///
1633    /// We do not allow namespace-level annotations on the empty namespace.
1634    ///
1635    /// Returns `None` if `namespace` is not found in the [`SchemaFragment`]
1636    /// or `annotation_key` is not a valid annotation key
1637    /// or it does not exist
1638    pub fn namespace_annotation(
1639        &self,
1640        namespace: EntityNamespace,
1641        annotation_key: impl AsRef<str>,
1642    ) -> Option<&str> {
1643        let ns = self.lossless.0.get(&Some(namespace.0))?;
1644        get_annotation_by_key(&ns.annotations, annotation_key)
1645    }
1646
1647    /// Get annotations of a common type declaration
1648    ///
1649    /// Returns `None` if `namespace` is not found in the [`SchemaFragment`] or
1650    /// `ty` is not a valid common type ID or `ty` is not found in the
1651    /// corresponding namespace definition
1652    pub fn common_type_annotations(
1653        &self,
1654        namespace: Option<EntityNamespace>,
1655        ty: &str,
1656    ) -> Option<impl Iterator<Item = (&str, &str)>> {
1657        let ns_def = self.lossless.0.get(&namespace.map(|n| n.0))?;
1658        let ty = json_schema::CommonTypeId::new(ast::UnreservedId::from_normalized_str(ty).ok()?)
1659            .ok()?;
1660        ns_def
1661            .common_types
1662            .get(&ty)
1663            .map(|ty| annotations_to_pairs(&ty.annotations))
1664    }
1665
1666    /// Get annotation value of a common type declaration by annotation key
1667    /// `annotation_key`
1668    ///
1669    /// Returns `None` if `namespace` is not found in the [`SchemaFragment`]
1670    /// or `ty` is not a valid common type ID
1671    /// or `ty` is not found in the corresponding namespace definition
1672    /// or `annotation_key` is not a valid annotation key
1673    /// or it does not exist
1674    pub fn common_type_annotation(
1675        &self,
1676        namespace: Option<EntityNamespace>,
1677        ty: &str,
1678        annotation_key: impl AsRef<str>,
1679    ) -> Option<&str> {
1680        let ns_def = self.lossless.0.get(&namespace.map(|n| n.0))?;
1681        let ty = json_schema::CommonTypeId::new(ast::UnreservedId::from_normalized_str(ty).ok()?)
1682            .ok()?;
1683        get_annotation_by_key(&ns_def.common_types.get(&ty)?.annotations, annotation_key)
1684    }
1685
1686    /// Get annotations of an entity type declaration
1687    ///
1688    /// Returns `None` if `namespace` is not found in the [`SchemaFragment`] or
1689    /// `ty` is not a valid entity type name or `ty` is not found in the
1690    /// corresponding namespace definition
1691    pub fn entity_type_annotations(
1692        &self,
1693        namespace: Option<EntityNamespace>,
1694        ty: &str,
1695    ) -> Option<impl Iterator<Item = (&str, &str)>> {
1696        let ns_def = self.lossless.0.get(&namespace.map(|n| n.0))?;
1697        let ty = ast::UnreservedId::from_normalized_str(ty).ok()?;
1698        ns_def
1699            .entity_types
1700            .get(&ty)
1701            .map(|ty| annotations_to_pairs(&ty.annotations))
1702    }
1703
1704    /// Get annotation value of an entity type declaration by annotation key
1705    /// `annotation_key`
1706    ///
1707    /// Returns `None` if `namespace` is not found in the [`SchemaFragment`]
1708    /// or `ty` is not a valid entity type name
1709    /// or `ty` is not found in the corresponding namespace definition
1710    /// or `annotation_key` is not a valid annotation key
1711    /// or it does not exist
1712    pub fn entity_type_annotation(
1713        &self,
1714        namespace: Option<EntityNamespace>,
1715        ty: &str,
1716        annotation_key: impl AsRef<str>,
1717    ) -> Option<&str> {
1718        let ns_def = self.lossless.0.get(&namespace.map(|n| n.0))?;
1719        let ty = ast::UnreservedId::from_normalized_str(ty).ok()?;
1720        get_annotation_by_key(&ns_def.entity_types.get(&ty)?.annotations, annotation_key)
1721    }
1722
1723    /// Get annotations of an action declaration
1724    ///
1725    /// Returns `None` if `namespace` is not found in the [`SchemaFragment`] or
1726    /// `id` is not found in the corresponding namespace definition
1727    pub fn action_annotations(
1728        &self,
1729        namespace: Option<EntityNamespace>,
1730        id: &EntityId,
1731    ) -> Option<impl Iterator<Item = (&str, &str)>> {
1732        let ns_def = self.lossless.0.get(&namespace.map(|n| n.0))?;
1733        ns_def
1734            .actions
1735            .get(id.unescaped())
1736            .map(|a| annotations_to_pairs(&a.annotations))
1737    }
1738
1739    /// Get annotation value of an action declaration by annotation key
1740    /// `annotation_key`
1741    ///
1742    /// Returns `None` if `namespace` is not found in the [`SchemaFragment`]
1743    /// or `id` is not found in the corresponding namespace definition
1744    /// or `annotation_key` is not a valid annotation key
1745    /// or it does not exist
1746    pub fn action_annotation(
1747        &self,
1748        namespace: Option<EntityNamespace>,
1749        id: &EntityId,
1750        annotation_key: impl AsRef<str>,
1751    ) -> Option<&str> {
1752        let ns_def = self.lossless.0.get(&namespace.map(|n| n.0))?;
1753        get_annotation_by_key(
1754            &ns_def.actions.get(id.unescaped())?.annotations,
1755            annotation_key,
1756        )
1757    }
1758
1759    /// Extract namespaces defined in this [`SchemaFragment`].
1760    ///
1761    /// `None` indicates the empty namespace.
1762    pub fn namespaces(&self) -> impl Iterator<Item = Option<EntityNamespace>> + '_ {
1763        self.value.namespaces().filter_map(|ns| {
1764            match ns.map(|ns| ast::Name::try_from(ns.clone())) {
1765                Some(Ok(n)) => Some(Some(EntityNamespace(n))),
1766                None => Some(None), // empty namespace, which we want to surface to the user
1767                Some(Err(_)) => {
1768                    // if the `SchemaFragment` contains namespaces with
1769                    // reserved `__cedar` components, that's an internal
1770                    // implementation detail; hide that from the user.
1771                    // Also note that `EntityNamespace` is backed by `Name`
1772                    // which can't even contain names with reserved
1773                    // `__cedar` components.
1774                    None
1775                }
1776            }
1777        })
1778    }
1779
1780    /// Create a [`SchemaFragment`] from a string containing JSON in the
1781    /// JSON schema format.
1782    pub fn from_json_str(src: &str) -> Result<Self, SchemaError> {
1783        let lossless = cedar_policy_core::validator::json_schema::Fragment::from_json_str(src)?;
1784        Ok(Self {
1785            value: lossless.clone().try_into()?,
1786            lossless,
1787        })
1788    }
1789
1790    /// Create a [`SchemaFragment`] from a JSON value (which should be an
1791    /// object of the shape required for the JSON schema format).
1792    pub fn from_json_value(json: serde_json::Value) -> Result<Self, SchemaError> {
1793        let lossless = cedar_policy_core::validator::json_schema::Fragment::from_json_value(json)?;
1794        Ok(Self {
1795            value: lossless.clone().try_into()?,
1796            lossless,
1797        })
1798    }
1799
1800    /// Parse a [`SchemaFragment`] from a reader containing the Cedar schema syntax
1801    pub fn from_cedarschema_file(
1802        r: impl std::io::Read,
1803    ) -> Result<(Self, impl Iterator<Item = SchemaWarning>), CedarSchemaError> {
1804        let (lossless, warnings) =
1805            cedar_policy_core::validator::json_schema::Fragment::from_cedarschema_file(
1806                r,
1807                Extensions::all_available(),
1808            )?;
1809        Ok((
1810            Self {
1811                value: lossless.clone().try_into()?,
1812                lossless,
1813            },
1814            warnings,
1815        ))
1816    }
1817
1818    /// Parse a [`SchemaFragment`] from a string containing the Cedar schema syntax
1819    pub fn from_cedarschema_str(
1820        src: &str,
1821    ) -> Result<(Self, impl Iterator<Item = SchemaWarning>), CedarSchemaError> {
1822        let (lossless, warnings) =
1823            cedar_policy_core::validator::json_schema::Fragment::from_cedarschema_str(
1824                src,
1825                Extensions::all_available(),
1826            )?;
1827        Ok((
1828            Self {
1829                value: lossless.clone().try_into()?,
1830                lossless,
1831            },
1832            warnings,
1833        ))
1834    }
1835
1836    /// Create a [`SchemaFragment`] directly from a JSON file (which should
1837    /// contain an object of the shape required for the JSON schema format).
1838    pub fn from_json_file(file: impl std::io::Read) -> Result<Self, SchemaError> {
1839        let lossless = cedar_policy_core::validator::json_schema::Fragment::from_json_file(file)?;
1840        Ok(Self {
1841            value: lossless.clone().try_into()?,
1842            lossless,
1843        })
1844    }
1845
1846    /// Serialize this [`SchemaFragment`] as a JSON value
1847    pub fn to_json_value(self) -> Result<serde_json::Value, SchemaError> {
1848        serde_json::to_value(self.lossless).map_err(|e| SchemaError::JsonSerialization(e.into()))
1849    }
1850
1851    /// Serialize this [`SchemaFragment`] as a JSON string
1852    pub fn to_json_string(&self) -> Result<String, SchemaError> {
1853        serde_json::to_string(&self.lossless).map_err(|e| SchemaError::JsonSerialization(e.into()))
1854    }
1855
1856    /// Serialize this [`SchemaFragment`] into a string in the Cedar schema
1857    /// syntax
1858    pub fn to_cedarschema(&self) -> Result<String, ToCedarSchemaError> {
1859        let str = self.lossless.to_cedarschema()?;
1860        Ok(str)
1861    }
1862}
1863
1864impl TryInto<Schema> for SchemaFragment {
1865    type Error = SchemaError;
1866
1867    /// Convert [`SchemaFragment`] into a [`Schema`]. To build the [`Schema`] we
1868    /// need to have all entity types defined, so an error will be returned if
1869    /// any undeclared entity types are referenced in the schema fragment.
1870    fn try_into(self) -> Result<Schema, Self::Error> {
1871        Ok(Schema(
1872            cedar_policy_core::validator::ValidatorSchema::from_schema_fragments(
1873                [self.value],
1874                Extensions::all_available(),
1875            )?,
1876        ))
1877    }
1878}
1879
1880impl FromStr for SchemaFragment {
1881    type Err = CedarSchemaError;
1882    /// Construct [`SchemaFragment`] from a string containing a schema formatted
1883    /// in the Cedar schema format. This can fail if the string is not a valid
1884    /// schema. This function does not check for consistency in the schema
1885    /// (e.g., references to undefined entities) because this is not required
1886    /// until a `Schema` is constructed.
1887    fn from_str(src: &str) -> Result<Self, Self::Err> {
1888        Self::from_cedarschema_str(src).map(|(frag, _)| frag)
1889    }
1890}
1891
1892/// Object containing schema information used by the validator.
1893#[repr(transparent)]
1894#[derive(Debug, Clone, RefCast)]
1895pub struct Schema(pub(crate) cedar_policy_core::validator::ValidatorSchema);
1896
1897#[doc(hidden)] // because this converts to a private/internal type
1898impl AsRef<cedar_policy_core::validator::ValidatorSchema> for Schema {
1899    fn as_ref(&self) -> &cedar_policy_core::validator::ValidatorSchema {
1900        &self.0
1901    }
1902}
1903
1904#[doc(hidden)]
1905impl From<cedar_policy_core::validator::ValidatorSchema> for Schema {
1906    fn from(schema: cedar_policy_core::validator::ValidatorSchema) -> Self {
1907        Self(schema)
1908    }
1909}
1910
1911impl FromStr for Schema {
1912    type Err = CedarSchemaError;
1913
1914    /// Construct a [`Schema`] from a string containing a schema formatted in
1915    /// the Cedar schema format. This can fail if it is not possible to parse a
1916    /// schema from the string, or if errors in values in the schema are
1917    /// uncovered after parsing. For instance, when an entity attribute name is
1918    /// found to not be a valid attribute name according to the Cedar
1919    /// grammar.
1920    fn from_str(schema_src: &str) -> Result<Self, Self::Err> {
1921        Self::from_cedarschema_str(schema_src).map(|(schema, _)| schema)
1922    }
1923}
1924
1925impl Schema {
1926    /// Create a [`Schema`] from multiple [`SchemaFragment`]. The individual
1927    /// fragments may reference entity or common types that are not declared in that
1928    /// fragment, but all referenced entity and common types must be declared in some
1929    /// fragment.
1930    pub fn from_schema_fragments(
1931        fragments: impl IntoIterator<Item = SchemaFragment>,
1932    ) -> Result<Self, SchemaError> {
1933        Ok(Self(
1934            cedar_policy_core::validator::ValidatorSchema::from_schema_fragments(
1935                fragments.into_iter().map(|f| f.value),
1936                Extensions::all_available(),
1937            )?,
1938        ))
1939    }
1940
1941    /// Create a [`Schema`] from a JSON value (which should be an object of the
1942    /// shape required for the JSON schema format).
1943    pub fn from_json_value(json: serde_json::Value) -> Result<Self, SchemaError> {
1944        Ok(Self(
1945            cedar_policy_core::validator::ValidatorSchema::from_json_value(
1946                json,
1947                Extensions::all_available(),
1948            )?,
1949        ))
1950    }
1951
1952    /// Create a [`Schema`] from a string containing JSON in the appropriate
1953    /// shape.
1954    pub fn from_json_str(json: &str) -> Result<Self, SchemaError> {
1955        Ok(Self(
1956            cedar_policy_core::validator::ValidatorSchema::from_json_str(
1957                json,
1958                Extensions::all_available(),
1959            )?,
1960        ))
1961    }
1962
1963    /// Create a [`Schema`] directly from a file containing JSON in the
1964    /// appropriate shape.
1965    pub fn from_json_file(file: impl std::io::Read) -> Result<Self, SchemaError> {
1966        Ok(Self(
1967            cedar_policy_core::validator::ValidatorSchema::from_json_file(
1968                file,
1969                Extensions::all_available(),
1970            )?,
1971        ))
1972    }
1973
1974    /// Parse the schema from a reader, in the Cedar schema format.
1975    pub fn from_cedarschema_file(
1976        file: impl std::io::Read,
1977    ) -> Result<(Self, impl Iterator<Item = SchemaWarning> + 'static), CedarSchemaError> {
1978        let (schema, warnings) =
1979            cedar_policy_core::validator::ValidatorSchema::from_cedarschema_file(
1980                file,
1981                Extensions::all_available(),
1982            )?;
1983        Ok((Self(schema), warnings))
1984    }
1985
1986    /// Parse the schema from a string, in the Cedar schema format.
1987    pub fn from_cedarschema_str(
1988        src: &str,
1989    ) -> Result<(Self, impl Iterator<Item = SchemaWarning>), CedarSchemaError> {
1990        let (schema, warnings) =
1991            cedar_policy_core::validator::ValidatorSchema::from_cedarschema_str(
1992                src,
1993                Extensions::all_available(),
1994            )?;
1995        Ok((Self(schema), warnings))
1996    }
1997
1998    /// Extract from the schema an [`Entities`] containing the action entities
1999    /// declared in the schema.
2000    pub fn action_entities(&self) -> Result<Entities, EntitiesError> {
2001        Ok(Entities(self.0.action_entities()?))
2002    }
2003
2004    /// Returns an iterator over every entity type that can be a principal for any action in this schema
2005    ///
2006    /// Note: this iterator may contain duplicates.
2007    ///
2008    /// # Examples
2009    /// Here's an example of using a [`std::collections::HashSet`] to get a de-duplicated set of principals
2010    /// ```
2011    /// use std::collections::HashSet;
2012    /// use cedar_policy::Schema;
2013    /// let schema : Schema = r#"
2014    ///     entity User;
2015    ///     entity Folder;
2016    ///     action Access appliesTo {
2017    ///         principal : User,
2018    ///         resource : Folder,
2019    ///     };
2020    ///     action Delete appliesTo {
2021    ///         principal : User,
2022    ///         resource : Folder,
2023    ///     };
2024    /// "#.parse().unwrap();
2025    /// let principals = schema.principals().collect::<HashSet<_>>();
2026    /// assert_eq!(principals, HashSet::from([&"User".parse().unwrap()]));
2027    /// ```
2028    pub fn principals(&self) -> impl Iterator<Item = &EntityTypeName> {
2029        self.0.principals().map(RefCast::ref_cast)
2030    }
2031
2032    /// Returns an iterator over every entity type that can be a resource for any action in this schema
2033    ///
2034    /// Note: this iterator may contain duplicates.
2035    /// # Examples
2036    /// Here's an example of using a [`std::collections::HashSet`] to get a de-duplicated set of resources
2037    /// ```
2038    /// use std::collections::HashSet;
2039    /// use cedar_policy::Schema;
2040    /// let schema : Schema = r#"
2041    ///     entity User;
2042    ///     entity Folder;
2043    ///     action Access appliesTo {
2044    ///         principal : User,
2045    ///         resource : Folder,
2046    ///     };
2047    ///     action Delete appliesTo {
2048    ///         principal : User,
2049    ///         resource : Folder,
2050    ///     };
2051    /// "#.parse().unwrap();
2052    /// let resources = schema.resources().collect::<HashSet<_>>();
2053    /// assert_eq!(resources, HashSet::from([&"Folder".parse().unwrap()]));
2054    /// ```
2055    pub fn resources(&self) -> impl Iterator<Item = &EntityTypeName> {
2056        self.0.resources().map(RefCast::ref_cast)
2057    }
2058
2059    /// Returns an iterator over every entity type that can be a principal for `action` in this schema
2060    ///
2061    /// ## Errors
2062    ///
2063    /// Returns [`None`] if `action` is not found in the schema
2064    pub fn principals_for_action(
2065        &self,
2066        action: &EntityUid,
2067    ) -> Option<impl Iterator<Item = &EntityTypeName>> {
2068        self.0
2069            .principals_for_action(&action.0)
2070            .map(|iter| iter.map(RefCast::ref_cast))
2071    }
2072
2073    /// Returns an iterator over every entity type that can be a resource for `action` in this schema
2074    ///
2075    /// ## Errors
2076    ///
2077    /// Returns [`None`] if `action` is not found in the schema
2078    pub fn resources_for_action(
2079        &self,
2080        action: &EntityUid,
2081    ) -> Option<impl Iterator<Item = &EntityTypeName>> {
2082        self.0
2083            .resources_for_action(&action.0)
2084            .map(|iter| iter.map(RefCast::ref_cast))
2085    }
2086
2087    /// Returns an iterator over all the [`RequestEnv`]s that are valid
2088    /// according to this schema.
2089    pub fn request_envs(&self) -> impl Iterator<Item = RequestEnv> + '_ {
2090        self.0
2091            .unlinked_request_envs(cedar_policy_core::validator::ValidationMode::Strict)
2092            .map(Into::into)
2093    }
2094
2095    /// Returns an iterator over all the entity types that can be an ancestor of `ty`
2096    ///
2097    /// ## Errors
2098    ///
2099    /// Returns [`None`] if the `ty` is not found in the schema
2100    pub fn ancestors<'a>(
2101        &'a self,
2102        ty: &'a EntityTypeName,
2103    ) -> Option<impl Iterator<Item = &'a EntityTypeName> + 'a> {
2104        self.0
2105            .ancestors(&ty.0)
2106            .map(|iter| iter.map(RefCast::ref_cast))
2107    }
2108
2109    /// Returns an iterator over all the action groups defined in this schema
2110    pub fn action_groups(&self) -> impl Iterator<Item = &EntityUid> {
2111        self.0.action_groups().map(RefCast::ref_cast)
2112    }
2113
2114    /// Returns an iterator over all entity types defined in this schema
2115    pub fn entity_types(&self) -> impl Iterator<Item = &EntityTypeName> {
2116        self.0
2117            .entity_types()
2118            .map(|ety| RefCast::ref_cast(ety.name()))
2119    }
2120
2121    /// Returns an iterator over all actions defined in this schema
2122    pub fn actions(&self) -> impl Iterator<Item = &EntityUid> {
2123        self.0.actions().map(RefCast::ref_cast)
2124    }
2125
2126    /// Returns an iterator over the actions that apply to this principal and
2127    /// resource type, as specified by the `appliesTo` block for the action in
2128    /// this schema.
2129    pub fn actions_for_principal_and_resource<'a: 'b, 'b>(
2130        &'a self,
2131        principal_type: &'b EntityTypeName,
2132        resource_type: &'b EntityTypeName,
2133    ) -> impl Iterator<Item = &'a EntityUid> + 'b {
2134        self.0
2135            .actions_for_principal_and_resource(&principal_type.0, &resource_type.0)
2136            .map(RefCast::ref_cast)
2137    }
2138}
2139
2140/// Convert a Cedar schema string to JSON format with resolved types.
2141///
2142/// This function resolves ambiguous "`EntityOrCommon`" types to their specific
2143/// Entity or `CommonType` classifications using the schema's type definitions.
2144/// This is primarily meant to be used when working with schemas programmatically,
2145/// for example when creating a schema building UI.
2146///
2147/// Returns `Ok((json_value, warnings))` on success, or `Err(error)` on failure.
2148/// Fails if there are any types in the schema that are unresolved.
2149pub fn schema_str_to_json_with_resolved_types(
2150    schema_str: &str,
2151) -> Result<(serde_json::Value, Vec<SchemaWarning>), CedarSchemaError> {
2152    // Parse the Cedar schema string into a fragment
2153    let (json_schema_fragment, warnings) =
2154        json_schema::Fragment::from_cedarschema_str(schema_str, Extensions::all_available())
2155            .map_err(
2156                |e: cedar_policy_core::validator::CedarSchemaError| -> CedarSchemaError {
2157                    e.into()
2158                },
2159            )?;
2160
2161    let warnings_as_schema_warnings: Vec<SchemaWarning> = warnings.collect();
2162
2163    // Use the new method from json_schema.rs to get the resolved fragment
2164    let fully_resolved_fragment =
2165        match json_schema_fragment.to_internal_name_fragment_with_resolved_types() {
2166            Ok(fragment) => fragment,
2167            Err(e) => {
2168                // SchemaError can be directly converted to CedarSchemaError
2169                return Err(e.into());
2170            }
2171        };
2172
2173    // Serialize the resolved fragment to JSON
2174    let json_value = serde_json::to_value(&fully_resolved_fragment).map_err(|e| {
2175        let schema_error = SchemaError::JsonSerialization(
2176            cedar_policy_core::validator::schema_errors::JsonSerializationError::from(e),
2177        );
2178        CedarSchemaError::Schema(schema_error)
2179    })?;
2180
2181    Ok((json_value, warnings_as_schema_warnings))
2182}
2183
2184/// Contains the result of policy validation.
2185///
2186/// The result includes the list of issues found by validation and whether validation succeeds or fails.
2187
2188#[cfg(test)]
2189mod test_schema_str_to_json_with_resolved_types {
2190    use super::*;
2191
2192    #[test]
2193    fn test_unresolved_type_error() {
2194        let schema_str = r#"entity User = { "name": MyName };"#;
2195
2196        let result = schema_str_to_json_with_resolved_types(schema_str);
2197
2198        // Should return an error because MyName is not defined
2199        match result {
2200            Ok(_) => panic!("Expected error but got success - MyName should not be resolved"),
2201            Err(CedarSchemaError::Schema(SchemaError::TypeNotDefined(type_not_defined_error))) => {
2202                // Verify that the error message contains information about the undefined type "MyName"
2203                let error_message = format!("{}", type_not_defined_error);
2204                assert!(
2205                    error_message.contains("MyName"),
2206                    "Expected error message to contain 'MyName', but got: {}",
2207                    error_message
2208                );
2209
2210                // Verify it's specifically about failing to resolve types
2211                assert!(
2212                    error_message.contains("failed to resolve type"),
2213                    "Expected error message to mention 'failed to resolve type', but got: {}",
2214                    error_message
2215                );
2216            }
2217            Err(CedarSchemaError::Schema(other_schema_error)) => {
2218                panic!(
2219                    "Expected TypeNotDefined error, but got different SchemaError: {:?}",
2220                    other_schema_error
2221                );
2222            }
2223            Err(CedarSchemaError::Parse(parse_error)) => {
2224                panic!(
2225                    "Expected TypeNotDefined error, but got parse error: {:?}",
2226                    parse_error
2227                );
2228            }
2229            Err(CedarSchemaError::Io(io_error)) => {
2230                panic!(
2231                    "Expected TypeNotDefined error, but got IO error: {:?}",
2232                    io_error
2233                );
2234            }
2235        }
2236    }
2237
2238    #[test]
2239    fn test_successful_resolution() {
2240        let schema_str = r#"
2241            type MyName = String;
2242            entity User = { "name": MyName };
2243        "#;
2244
2245        let result = schema_str_to_json_with_resolved_types(schema_str);
2246
2247        match result {
2248            Ok((json_value, warnings)) => {
2249                // Verify we got a JSON value
2250                assert!(json_value.is_object(), "Expected JSON object");
2251
2252                // Verify the JSON doesn't contain "EntityOrCommon" (should be resolved)
2253                let json_str = serde_json::to_string(&json_value).unwrap();
2254                assert!(
2255                    !json_str.contains("EntityOrCommon"),
2256                    "JSON should not contain unresolved EntityOrCommon types: {}",
2257                    json_str
2258                );
2259
2260                // Verify MyName is resolved to a reference to the common type
2261                assert!(
2262                    json_str.contains("MyName"),
2263                    "JSON should contain resolved MyName type reference: {}",
2264                    json_str
2265                );
2266
2267                // Should have no warnings for this simple valid schema
2268                assert_eq!(warnings.len(), 0, "Expected no warnings for valid schema");
2269            }
2270            Err(e) => panic!("Expected success but got error: {:?}", e),
2271        }
2272    }
2273}
2274/// Validation succeeds if there are no fatal errors. There may still be
2275/// non-fatal warnings present when validation passes.
2276#[derive(Debug, Clone)]
2277pub struct ValidationResult {
2278    validation_errors: Vec<ValidationError>,
2279    validation_warnings: Vec<ValidationWarning>,
2280}
2281
2282impl ValidationResult {
2283    /// True when validation passes. There are no errors, but there may be
2284    /// non-fatal warnings. Use [`ValidationResult::validation_passed_without_warnings`]
2285    /// to check that there are also no warnings.
2286    pub fn validation_passed(&self) -> bool {
2287        self.validation_errors.is_empty()
2288    }
2289
2290    /// True when validation passes (i.e., there are no errors) and there are
2291    /// additionally no non-fatal warnings.
2292    pub fn validation_passed_without_warnings(&self) -> bool {
2293        self.validation_errors.is_empty() && self.validation_warnings.is_empty()
2294    }
2295
2296    /// Get an iterator over the errors found by the validator.
2297    pub fn validation_errors(&self) -> impl Iterator<Item = &ValidationError> {
2298        self.validation_errors.iter()
2299    }
2300
2301    /// Get an iterator over the warnings found by the validator.
2302    pub fn validation_warnings(&self) -> impl Iterator<Item = &ValidationWarning> {
2303        self.validation_warnings.iter()
2304    }
2305
2306    fn first_error_or_warning(&self) -> Option<&dyn Diagnostic> {
2307        self.validation_errors
2308            .first()
2309            .map(|e| e as &dyn Diagnostic)
2310            .or_else(|| {
2311                self.validation_warnings
2312                    .first()
2313                    .map(|w| w as &dyn Diagnostic)
2314            })
2315    }
2316
2317    pub(crate) fn into_errors_and_warnings(
2318        self,
2319    ) -> (
2320        impl Iterator<Item = ValidationError>,
2321        impl Iterator<Item = ValidationWarning>,
2322    ) {
2323        (
2324            self.validation_errors.into_iter(),
2325            self.validation_warnings.into_iter(),
2326        )
2327    }
2328}
2329
2330#[doc(hidden)]
2331impl From<cedar_policy_core::validator::ValidationResult> for ValidationResult {
2332    fn from(r: cedar_policy_core::validator::ValidationResult) -> Self {
2333        let (errors, warnings) = r.into_errors_and_warnings();
2334        Self {
2335            validation_errors: errors.map(ValidationError::from).collect(),
2336            validation_warnings: warnings.map(ValidationWarning::from).collect(),
2337        }
2338    }
2339}
2340
2341impl std::fmt::Display for ValidationResult {
2342    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2343        match self.first_error_or_warning() {
2344            Some(diagnostic) => write!(f, "{diagnostic}"),
2345            None => write!(f, "no errors or warnings"),
2346        }
2347    }
2348}
2349
2350impl std::error::Error for ValidationResult {
2351    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
2352        self.first_error_or_warning()
2353            .and_then(std::error::Error::source)
2354    }
2355
2356    fn description(&self) -> &str {
2357        #[expect(
2358            deprecated,
2359            reason = "description() is deprecated but we still want to forward it"
2360        )]
2361        self.first_error_or_warning()
2362            .map_or("no errors or warnings", std::error::Error::description)
2363    }
2364
2365    fn cause(&self) -> Option<&dyn std::error::Error> {
2366        #[expect(
2367            deprecated,
2368            reason = "cause() is deprecated but we still want to forward it"
2369        )]
2370        self.first_error_or_warning()
2371            .and_then(std::error::Error::cause)
2372    }
2373}
2374
2375// Except for `.related()`, and `.severity` everything is forwarded to the first
2376// error, or to the first warning if there are no errors. This is done for the
2377// same reason as policy parse errors.
2378impl Diagnostic for ValidationResult {
2379    fn related(&self) -> Option<Box<dyn Iterator<Item = &dyn Diagnostic> + '_>> {
2380        let mut related = self
2381            .validation_errors
2382            .iter()
2383            .map(|err| err as &dyn Diagnostic)
2384            .chain(
2385                self.validation_warnings
2386                    .iter()
2387                    .map(|warn| warn as &dyn Diagnostic),
2388            );
2389        related.next().map(move |first| match first.related() {
2390            Some(first_related) => Box::new(first_related.chain(related)),
2391            None => Box::new(related) as Box<dyn Iterator<Item = _>>,
2392        })
2393    }
2394
2395    fn severity(&self) -> Option<miette::Severity> {
2396        self.first_error_or_warning()
2397            .map_or(Some(miette::Severity::Advice), Diagnostic::severity)
2398    }
2399
2400    fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
2401        self.first_error_or_warning().and_then(Diagnostic::labels)
2402    }
2403
2404    fn source_code(&self) -> Option<&dyn miette::SourceCode> {
2405        self.first_error_or_warning()
2406            .and_then(Diagnostic::source_code)
2407    }
2408
2409    fn code(&self) -> Option<Box<dyn std::fmt::Display + '_>> {
2410        self.first_error_or_warning().and_then(Diagnostic::code)
2411    }
2412
2413    fn url(&self) -> Option<Box<dyn std::fmt::Display + '_>> {
2414        self.first_error_or_warning().and_then(Diagnostic::url)
2415    }
2416
2417    fn help(&self) -> Option<Box<dyn std::fmt::Display + '_>> {
2418        self.first_error_or_warning().and_then(Diagnostic::help)
2419    }
2420
2421    fn diagnostic_source(&self) -> Option<&dyn Diagnostic> {
2422        self.first_error_or_warning()
2423            .and_then(Diagnostic::diagnostic_source)
2424    }
2425}
2426
2427/// Scan a set of policies for potentially confusing/obfuscating text.
2428///
2429/// These checks are also provided through [`Validator::validate`] which provides more
2430/// comprehensive error detection, but this function can be used to check for
2431/// confusable strings without defining a schema.
2432pub fn confusable_string_checker<'a>(
2433    templates: impl Iterator<Item = &'a Template> + 'a,
2434) -> impl Iterator<Item = ValidationWarning> + 'a {
2435    cedar_policy_core::validator::confusable_string_checks(templates.map(|t| &t.ast))
2436        .map(std::convert::Into::into)
2437}
2438
2439/// Represents a namespace.
2440///
2441/// An `EntityNamespace` can can be constructed using
2442/// [`EntityNamespace::from_str`] or by calling `parse()` on a string.
2443/// _This can fail_, so it is important to properly handle an `Err` result.
2444///
2445/// ```
2446/// # use cedar_policy::EntityNamespace;
2447/// let id : Result<EntityNamespace, _> = "My::Name::Space".parse();
2448/// # assert_eq!(id.unwrap().to_string(), "My::Name::Space".to_string());
2449/// ```
2450#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
2451pub struct EntityNamespace(pub(crate) ast::Name);
2452
2453#[doc(hidden)] // because this converts to a private/internal type
2454impl AsRef<ast::Name> for EntityNamespace {
2455    fn as_ref(&self) -> &ast::Name {
2456        &self.0
2457    }
2458}
2459
2460/// This `FromStr` implementation requires the _normalized_ representation of the
2461/// namespace. See <https://github.com/cedar-policy/rfcs/pull/9/>.
2462impl FromStr for EntityNamespace {
2463    type Err = ParseErrors;
2464
2465    fn from_str(namespace_str: &str) -> Result<Self, Self::Err> {
2466        ast::Name::from_normalized_str(namespace_str)
2467            .map(EntityNamespace)
2468            .map_err(Into::into)
2469    }
2470}
2471
2472impl std::fmt::Display for EntityNamespace {
2473    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2474        write!(f, "{}", self.0)
2475    }
2476}
2477
2478#[derive(Debug, Clone, Default)]
2479/// A struct representing a `PolicySet` as a series of strings for ser/de.
2480/// A `PolicySet` that contains template-linked policies cannot be
2481/// represented as this struct.
2482pub(crate) struct StringifiedPolicySet {
2483    /// The static policies in the set
2484    pub policies: Vec<String>,
2485    /// The policy templates in the set
2486    pub policy_templates: Vec<String>,
2487}
2488
2489/// Represents a set of `Policy`s
2490#[derive(Debug, Clone, Default)]
2491pub struct PolicySet {
2492    /// AST representation. Technically partially redundant with the other fields.
2493    /// Internally, we ensure that the duplicated information remains consistent.
2494    pub(crate) ast: ast::PolicySet,
2495    /// Policies in the set (this includes both static policies and template linked-policies)
2496    policies: LinkedHashMap<PolicyId, Policy>,
2497    /// Templates in the set
2498    templates: LinkedHashMap<PolicyId, Template>,
2499}
2500
2501impl PartialEq for PolicySet {
2502    fn eq(&self, other: &Self) -> bool {
2503        // eq is based on just the `ast`
2504        self.ast.eq(&other.ast)
2505    }
2506}
2507impl Eq for PolicySet {}
2508
2509#[doc(hidden)] // because this converts to a private/internal type
2510impl AsRef<ast::PolicySet> for PolicySet {
2511    fn as_ref(&self) -> &ast::PolicySet {
2512        &self.ast
2513    }
2514}
2515
2516#[doc(hidden)]
2517impl TryFrom<ast::PolicySet> for PolicySet {
2518    type Error = PolicySetError;
2519    fn try_from(pset: ast::PolicySet) -> Result<Self, Self::Error> {
2520        Self::from_ast(pset)
2521    }
2522}
2523
2524impl FromStr for PolicySet {
2525    type Err = ParseErrors;
2526
2527    /// Create a policy set from multiple statements.
2528    ///
2529    /// Policy ids will default to "policy*" with numbers from 0.
2530    /// If you load more policies, do not use the default id, or there will be conflicts.
2531    ///
2532    /// See [`Policy`] for more.
2533    fn from_str(policies: &str) -> Result<Self, Self::Err> {
2534        let (texts, pset) = parser::parse_policyset_and_also_return_policy_text(policies)?;
2535        #[expect(clippy::expect_used, reason = "By the invariant on `parse_policyset_and_also_return_policy_text(policies)`, every `PolicyId` in `pset.policies()` occurs as a key in `text`.")]
2536        let policies = pset.policies().map(|p|
2537            (
2538                PolicyId::new(p.id().clone()),
2539                Policy { lossless: LosslessPolicy::policy_or_template_text(*texts.get(p.id()).expect("internal invariant violation: policy id exists in asts but not texts")), ast: p.clone() }
2540            )
2541        ).collect();
2542        #[expect(clippy::expect_used, reason = "By the invariant on `parse_policyset_and_also_return_policy_text(policies)`, every `PolicyId` in `pset.templates()` also occurs as a key in `text`.")]
2543        let templates = pset.templates().map(|t|
2544            (
2545                PolicyId::new(t.id().clone()),
2546                Template { lossless: LosslessPolicy::policy_or_template_text(*texts.get(t.id()).expect("internal invariant violation: template id exists in asts but not ests")), ast: t.clone() }
2547            )
2548        ).collect();
2549        Ok(Self {
2550            ast: pset,
2551            policies,
2552            templates,
2553        })
2554    }
2555}
2556
2557impl PolicySet {
2558    /// Build the policy set AST from the EST
2559    fn from_est(est: &est::PolicySet) -> Result<Self, PolicySetError> {
2560        let ast: ast::PolicySet = est.clone().try_into()?;
2561        #[expect(
2562            clippy::expect_used,
2563            reason = "Since conversion from EST to AST succeeded, every `PolicyId` in `ast.policies()` occurs in `est`"
2564        )]
2565        let policies = ast
2566            .policies()
2567            .map(|p| {
2568                (
2569                    PolicyId::new(p.id().clone()),
2570                    Policy {
2571                        lossless: LosslessPolicy::Est(est.get_policy(p.id()).expect(
2572                            "internal invariant violation: policy id exists in asts but not ests",
2573                        )),
2574                        ast: p.clone(),
2575                    },
2576                )
2577            })
2578            .collect();
2579        #[expect(
2580            clippy::expect_used,
2581            reason = "Since conversion from EST to AST succeeded, every `PolicyId` in `ast.templates()` occurs in `est`"
2582        )]
2583        let templates = ast
2584            .templates()
2585            .map(|t| {
2586                (
2587                    PolicyId::new(t.id().clone()),
2588                    Template {
2589                        lossless: LosslessPolicy::Est(est.get_template(t.id()).expect(
2590                            "internal invariant violation: template id exists in asts but not ests",
2591                        )),
2592                        ast: t.clone(),
2593                    },
2594                )
2595            })
2596            .collect();
2597        Ok(Self {
2598            ast,
2599            policies,
2600            templates,
2601        })
2602    }
2603
2604    /// Build the [`PolicySet`] from just the AST information
2605    pub(crate) fn from_ast(ast: ast::PolicySet) -> Result<Self, PolicySetError> {
2606        Self::from_policies(ast.into_policies().map(Policy::from_ast))
2607    }
2608
2609    /// Deserialize the [`PolicySet`] from a JSON string
2610    pub fn from_json_str(src: impl AsRef<str>) -> Result<Self, PolicySetError> {
2611        let est: est::PolicySet = serde_json::from_str(src.as_ref())
2612            .map_err(|e| policy_set_errors::JsonPolicySetError { inner: e })?;
2613        Self::from_est(&est)
2614    }
2615
2616    /// Deserialize the [`PolicySet`] from a JSON value
2617    pub fn from_json_value(src: serde_json::Value) -> Result<Self, PolicySetError> {
2618        let est: est::PolicySet = serde_json::from_value(src)
2619            .map_err(|e| policy_set_errors::JsonPolicySetError { inner: e })?;
2620        Self::from_est(&est)
2621    }
2622
2623    /// Deserialize the [`PolicySet`] from a JSON reader
2624    pub fn from_json_file(r: impl std::io::Read) -> Result<Self, PolicySetError> {
2625        let est: est::PolicySet = serde_json::from_reader(r)
2626            .map_err(|e| policy_set_errors::JsonPolicySetError { inner: e })?;
2627        Self::from_est(&est)
2628    }
2629
2630    /// Serialize the [`PolicySet`] as a JSON value
2631    pub fn to_json(self) -> Result<serde_json::Value, PolicySetError> {
2632        let est = self.est()?;
2633        let value = serde_json::to_value(est)
2634            .map_err(|e| policy_set_errors::JsonPolicySetError { inner: e })?;
2635        Ok(value)
2636    }
2637
2638    /// Get the EST representation of the [`PolicySet`]
2639    fn est(self) -> Result<est::PolicySet, PolicyToJsonError> {
2640        let (static_policies, template_links): (Vec<_>, Vec<_>) =
2641            fold_partition(self.policies, is_static_or_link)?;
2642        let static_policies = static_policies.into_iter().collect::<LinkedHashMap<_, _>>();
2643        let templates = self
2644            .templates
2645            .into_iter()
2646            .map(|(id, template)| {
2647                template
2648                    .lossless
2649                    .est(|| template.ast.clone().into())
2650                    .map(|est| (id.into(), est))
2651            })
2652            .collect::<Result<LinkedHashMap<_, _>, _>>()?;
2653        let est = est::PolicySet {
2654            templates,
2655            static_policies,
2656            template_links,
2657        };
2658
2659        Ok(est)
2660    }
2661
2662    /// Get the human-readable Cedar syntax representation of this policy set.
2663    /// This function is primarily intended for rendering JSON policies in the
2664    /// human-readable syntax, but it will also return the original policy text
2665    /// (though possibly re-ordering policies within the policy set) when the
2666    /// policy-set contains policies parsed from the human-readable syntax.
2667    ///
2668    /// This will return `None` if there are any linked policies in the policy
2669    /// set because they cannot be directly rendered in Cedar syntax. It also
2670    /// cannot record policy ids because these cannot be specified in the Cedar
2671    /// syntax. The policies may be reordered, so parsing the resulting string
2672    /// with [`PolicySet::from_str`] is likely to yield different policy id
2673    /// assignments. For these reasons you should prefer serializing as JSON (or protobuf) and
2674    /// only using this function to obtain a representation to display to human
2675    /// users.
2676    ///
2677    /// This function does not format the policy according to any particular
2678    /// rules.  Policy formatting can be done through the Cedar policy CLI or
2679    /// the `cedar-policy-formatter` crate.
2680    pub fn to_cedar(&self) -> Option<String> {
2681        match self.stringify() {
2682            Some(StringifiedPolicySet {
2683                policies,
2684                policy_templates,
2685            }) => {
2686                let policies_as_vec = policies
2687                    .into_iter()
2688                    .chain(policy_templates)
2689                    .collect::<Vec<_>>();
2690                Some(policies_as_vec.join("\n\n"))
2691            }
2692            None => None,
2693        }
2694    }
2695
2696    /// Get the human-readable Cedar syntax representation of this policy set,
2697    /// as a vec of strings. This function is useful to break up a large cedar
2698    /// file containing many policies into individual policies.
2699    ///
2700    /// This will return `None` if there are any linked policies in the policy
2701    /// set because they cannot be directly rendered in Cedar syntax. It also
2702    /// cannot record policy ids because these cannot be specified in the Cedar
2703    /// syntax. The policies may be reordered, so parsing the resulting string
2704    /// with [`PolicySet::from_str`] is likely to yield different policy id
2705    /// assignments. For these reasons you should prefer serializing as JSON (or protobuf) and
2706    /// only using this function to obtain a compact cedar representation,
2707    /// perhaps for storage purposes.
2708    ///
2709    /// This function does not format the policy according to any particular
2710    /// rules.  Policy formatting can be done through the Cedar policy CLI or
2711    /// the `cedar-policy-formatter` crate.
2712    pub(crate) fn stringify(&self) -> Option<StringifiedPolicySet> {
2713        let policies = self
2714            .policies
2715            .values()
2716            // We'd like to print policies in a deterministic order, so we sort
2717            // before printing, hoping that the size of policy sets is fairly
2718            // small.
2719            .sorted_by_key(|p| AsRef::<str>::as_ref(p.id()))
2720            .map(Policy::to_cedar)
2721            .collect::<Option<Vec<_>>>()?;
2722        let policy_templates = self
2723            .templates
2724            .values()
2725            .sorted_by_key(|t| AsRef::<str>::as_ref(t.id()))
2726            .map(Template::to_cedar)
2727            .collect_vec();
2728
2729        Some(StringifiedPolicySet {
2730            policies,
2731            policy_templates,
2732        })
2733    }
2734
2735    /// Create a fresh empty `PolicySet`
2736    pub fn new() -> Self {
2737        Self {
2738            ast: ast::PolicySet::new(),
2739            policies: LinkedHashMap::new(),
2740            templates: LinkedHashMap::new(),
2741        }
2742    }
2743
2744    /// Create a `PolicySet` from the given policies
2745    pub fn from_policies(
2746        policies: impl IntoIterator<Item = Policy>,
2747    ) -> Result<Self, PolicySetError> {
2748        let mut set = Self::new();
2749        for policy in policies {
2750            set.add(policy)?;
2751        }
2752        Ok(set)
2753    }
2754
2755    /// Merges this `PolicySet` with another `PolicySet`.
2756    /// This `PolicySet` is modified while the other `PolicySet`
2757    /// remains unchanged.
2758    ///
2759    /// The flag `rename_duplicates` controls the expected behavior
2760    /// when a `PolicyId` in this and the other `PolicySet` conflict.
2761    ///
2762    /// When `rename_duplicates` is false, conflicting `PolicyId`s result
2763    /// in a `PolicySetError::AlreadyDefined` error.
2764    ///
2765    /// Otherwise, when `rename_duplicates` is true, conflicting `PolicyId`s from
2766    /// the other `PolicySet` are automatically renamed to avoid conflict.
2767    /// This renaming is returned as a Hashmap from the old `PolicyId` to the
2768    /// renamed `PolicyId`.
2769    pub fn merge(
2770        &mut self,
2771        other: &Self,
2772        rename_duplicates: bool,
2773    ) -> Result<HashMap<PolicyId, PolicyId>, PolicySetError> {
2774        match self.ast.merge_policyset(&other.ast, rename_duplicates) {
2775            Ok(renaming) => {
2776                let renaming: HashMap<PolicyId, PolicyId> = renaming
2777                    .into_iter()
2778                    .map(|(old_pid, new_pid)| (PolicyId::new(old_pid), PolicyId::new(new_pid)))
2779                    .collect();
2780
2781                for (pid, op) in &other.policies {
2782                    let pid = renaming.get(pid).unwrap_or(pid);
2783                    if !self.policies.contains_key(pid) {
2784                        #[expect(
2785                            clippy::unwrap_used,
2786                            reason = "`pid` is the new id of a policy from `other`, so it will be in `self` after merging"
2787                        )]
2788                        let new_p = Policy {
2789                            // Use the representation from `self.ast` so that we get a version with internal references to
2790                            // policy ids updated to account for the renaming.
2791                            ast: self.ast.get(pid.as_ref()).unwrap().clone(),
2792                            lossless: op.lossless.clone(),
2793                        };
2794                        self.policies.insert(pid.clone(), new_p);
2795                    }
2796                }
2797                for (pid, ot) in &other.templates {
2798                    let pid = renaming.get(pid).unwrap_or(pid);
2799                    if !self.templates.contains_key(pid) {
2800                        #[expect(
2801                            clippy::unwrap_used,
2802                            reason = "`pid` is the new id of a template from `other`, so it will be in `self` after merging"
2803                        )]
2804                        let new_t = Template {
2805                            ast: self.ast.get_template(pid.as_ref()).unwrap().clone(),
2806                            lossless: ot.lossless.clone(),
2807                        };
2808                        self.templates.insert(pid.clone(), new_t);
2809                    }
2810                }
2811
2812                Ok(renaming)
2813            }
2814            Err(ast::PolicySetError::Occupied { id }) => Err(PolicySetError::AlreadyDefined(
2815                policy_set_errors::AlreadyDefined {
2816                    id: PolicyId::new(id),
2817                },
2818            )),
2819        }
2820    }
2821
2822    /// Add an static policy to the `PolicySet`. To add a template instance, use
2823    /// `link` instead. This function will return an error (and not modify
2824    /// the `PolicySet`) if a template-linked policy is passed in.
2825    pub fn add(&mut self, policy: Policy) -> Result<(), PolicySetError> {
2826        if policy.is_static() {
2827            let id = PolicyId::new(policy.ast.id().clone());
2828            self.ast.add(policy.ast.clone())?;
2829            self.policies.insert(id, policy);
2830            Ok(())
2831        } else {
2832            Err(PolicySetError::ExpectedStatic(
2833                policy_set_errors::ExpectedStatic::new(),
2834            ))
2835        }
2836    }
2837
2838    /// Remove a static `Policy` from the `PolicySet`.
2839    ///
2840    /// This will error if the policy is not a static policy.
2841    pub fn remove_static(&mut self, policy_id: PolicyId) -> Result<Policy, PolicySetError> {
2842        let Some(policy) = self.policies.remove(&policy_id) else {
2843            return Err(PolicySetError::PolicyNonexistent(
2844                policy_set_errors::PolicyNonexistentError { policy_id },
2845            ));
2846        };
2847        if self
2848            .ast
2849            .remove_static(&ast::PolicyID::from_string(&policy_id))
2850            .is_ok()
2851        {
2852            Ok(policy)
2853        } else {
2854            //Restore self.policies
2855            self.policies.insert(policy_id.clone(), policy);
2856            Err(PolicySetError::PolicyNonexistent(
2857                policy_set_errors::PolicyNonexistentError { policy_id },
2858            ))
2859        }
2860    }
2861
2862    /// Add a `Template` to the `PolicySet`
2863    pub fn add_template(&mut self, template: Template) -> Result<(), PolicySetError> {
2864        let id = PolicyId::new(template.ast.id().clone());
2865        self.ast.add_template(template.ast.clone())?;
2866        self.templates.insert(id, template);
2867        Ok(())
2868    }
2869
2870    /// Remove a `Template` from the `PolicySet`.
2871    ///
2872    /// This will error if any policy is linked to the template.
2873    /// This will error if `policy_id` is not a template.
2874    pub fn remove_template(&mut self, template_id: PolicyId) -> Result<Template, PolicySetError> {
2875        let Some(template) = self.templates.remove(&template_id) else {
2876            return Err(PolicySetError::TemplateNonexistent(
2877                policy_set_errors::TemplateNonexistentError { template_id },
2878            ));
2879        };
2880        // If self.templates and self.ast disagree, authorization cannot be trusted.
2881        #[expect(clippy::panic, reason = "We just found the policy in self.templates")]
2882        match self
2883            .ast
2884            .remove_template(&ast::PolicyID::from_string(&template_id))
2885        {
2886            Ok(_) => Ok(template),
2887            Err(ast::PolicySetTemplateRemovalError::RemoveTemplateWithLinksError(_)) => {
2888                self.templates.insert(template_id.clone(), template);
2889                Err(PolicySetError::RemoveTemplateWithActiveLinks(
2890                    policy_set_errors::RemoveTemplateWithActiveLinksError { template_id },
2891                ))
2892            }
2893            Err(ast::PolicySetTemplateRemovalError::NotTemplateError(_)) => {
2894                self.templates.insert(template_id.clone(), template);
2895                Err(PolicySetError::RemoveTemplateNotTemplate(
2896                    policy_set_errors::RemoveTemplateNotTemplateError { template_id },
2897                ))
2898            }
2899            Err(ast::PolicySetTemplateRemovalError::RemovePolicyNoTemplateError(_)) => {
2900                panic!("Found template policy in self.templates but not in self.ast");
2901            }
2902        }
2903    }
2904
2905    /// Get policies linked to a `Template` in the `PolicySet`.
2906    /// If any policy is linked to the template, this will error
2907    pub fn get_linked_policies(
2908        &self,
2909        template_id: PolicyId,
2910    ) -> Result<impl Iterator<Item = &PolicyId>, PolicySetError> {
2911        self.ast
2912            .get_linked_policies(&ast::PolicyID::from_string(&template_id))
2913            .map_or_else(
2914                |_| {
2915                    Err(PolicySetError::TemplateNonexistent(
2916                        policy_set_errors::TemplateNonexistentError { template_id },
2917                    ))
2918                },
2919                |v| Ok(v.map(PolicyId::ref_cast)),
2920            )
2921    }
2922
2923    /// Iterate over all the `Policy`s in the `PolicySet`.
2924    ///
2925    /// This will include both static and template-linked policies.
2926    pub fn policies(&self) -> impl Iterator<Item = &Policy> {
2927        self.policies.values()
2928    }
2929
2930    /// Iterate over the `Template`'s in the `PolicySet`.
2931    pub fn templates(&self) -> impl Iterator<Item = &Template> {
2932        self.templates.values()
2933    }
2934
2935    /// Get a `Template` by its `PolicyId`
2936    pub fn template(&self, id: &PolicyId) -> Option<&Template> {
2937        self.templates.get(id)
2938    }
2939
2940    /// Get a `Policy` by its `PolicyId`
2941    pub fn policy(&self, id: &PolicyId) -> Option<&Policy> {
2942        self.policies.get(id)
2943    }
2944
2945    /// Extract annotation data from a `Policy` by its `PolicyId` and annotation key.
2946    /// If the annotation is present without an explicit value (e.g., `@annotation`),
2947    /// then this function returns `Some("")`. It returns `None` only when the
2948    /// annotation is not present.
2949    pub fn annotation(&self, id: &PolicyId, key: impl AsRef<str>) -> Option<&str> {
2950        self.ast
2951            .get(id.as_ref())?
2952            .annotation(&key.as_ref().parse().ok()?)
2953            .map(AsRef::as_ref)
2954    }
2955
2956    /// Extract annotation data from a `Template` by its `PolicyId` and annotation key.
2957    /// If the annotation is present without an explicit value (e.g., `@annotation`),
2958    /// then this function returns `Some("")`. It returns `None` only when the
2959    /// annotation is not present.
2960    pub fn template_annotation(&self, id: &PolicyId, key: impl AsRef<str>) -> Option<&str> {
2961        self.ast
2962            .get_template(id.as_ref())?
2963            .annotation(&key.as_ref().parse().ok()?)
2964            .map(AsRef::as_ref)
2965    }
2966
2967    /// Returns true iff the `PolicySet` is empty
2968    pub fn is_empty(&self) -> bool {
2969        debug_assert_eq!(
2970            self.ast.is_empty(),
2971            self.policies.is_empty() && self.templates.is_empty()
2972        );
2973        self.ast.is_empty()
2974    }
2975
2976    /// Returns the number of `Policy`s in the `PolicySet`.
2977    ///
2978    /// This will include both static and template-linked policies.
2979    pub fn num_of_policies(&self) -> usize {
2980        self.policies.len()
2981    }
2982
2983    /// Returns the number of `Template`s in the `PolicySet`.
2984    pub fn num_of_templates(&self) -> usize {
2985        self.templates.len()
2986    }
2987
2988    /// Attempt to link a template and add the new template-linked policy to the policy set.
2989    /// If link fails, the `PolicySet` is not modified.
2990    /// Failure can happen for three reasons
2991    ///   1) The map passed in `vals` may not match the slots in the template
2992    ///   2) The `new_id` may conflict w/ a policy that already exists in the set
2993    ///   3) `template_id` does not correspond to a template. Either the id is
2994    ///      not in the policy set, or it is in the policy set but is either a
2995    ///      linked or static policy rather than a template
2996    pub fn link(
2997        &mut self,
2998        template_id: PolicyId,
2999        new_id: PolicyId,
3000        vals: HashMap<SlotId, EntityUid>,
3001    ) -> Result<(), PolicySetError> {
3002        let unwrapped_vals: HashMap<ast::SlotId, ast::EntityUID> = vals
3003            .into_iter()
3004            .map(|(key, value)| (key.into(), value.into()))
3005            .collect();
3006
3007        // Try to get the template with the id we're linking from.  We do this
3008        // _before_ calling `self.ast.link` because `link` mutates the policy
3009        // set by creating a new link entry in a hashmap. This happens even when
3010        // trying to link a static policy, which we want to error on here.
3011        let Some(template) = self.templates.get(&template_id) else {
3012            return Err(if self.policies.contains_key(&template_id) {
3013                policy_set_errors::ExpectedTemplate::new().into()
3014            } else {
3015                policy_set_errors::LinkingError {
3016                    inner: ast::LinkingError::NoSuchTemplate {
3017                        id: template_id.into(),
3018                    },
3019                }
3020                .into()
3021            });
3022        };
3023
3024        let linked_ast = self.ast.link(
3025            template_id.into(),
3026            new_id.clone().into(),
3027            unwrapped_vals.clone(),
3028        )?;
3029
3030        #[expect(
3031            clippy::expect_used,
3032            reason = "`lossless.link()` will not fail after `ast.link()` succeeds"
3033        )]
3034        let linked_lossless = template
3035            .lossless
3036            .clone()
3037            .link(unwrapped_vals.iter().map(|(k, v)| (*k, v)))
3038            // The only error case for `lossless.link()` is a template with
3039            // slots which are not filled by the provided values. `ast.link()`
3040            // will have already errored if there are any unfilled slots in the
3041            // template.
3042            .expect("ast.link() didn't fail above, so this shouldn't fail");
3043        self.policies.insert(
3044            new_id,
3045            Policy {
3046                ast: linked_ast.clone(),
3047                lossless: linked_lossless,
3048            },
3049        );
3050        Ok(())
3051    }
3052
3053    /// Get all the unknown entities from the policy set
3054    #[doc = include_str!("../experimental_warning.md")]
3055    #[cfg(feature = "partial-eval")]
3056    pub fn unknown_entities(&self) -> HashSet<EntityUid> {
3057        let mut entity_uids = HashSet::new();
3058        for policy in self.policies.values() {
3059            entity_uids.extend(policy.unknown_entities());
3060        }
3061        entity_uids
3062    }
3063
3064    /// Unlink a template-linked policy from the policy set.
3065    /// Returns the policy that was unlinked.
3066    pub fn unlink(&mut self, policy_id: PolicyId) -> Result<Policy, PolicySetError> {
3067        let Some(policy) = self.policies.remove(&policy_id) else {
3068            return Err(PolicySetError::LinkNonexistent(
3069                policy_set_errors::LinkNonexistentError { policy_id },
3070            ));
3071        };
3072        // If self.policies and self.ast disagree, authorization cannot be trusted.
3073        #[expect(clippy::panic, reason = "We just found the policy in self.policies")]
3074        match self.ast.unlink(&ast::PolicyID::from_string(&policy_id)) {
3075            Ok(_) => Ok(policy),
3076            Err(ast::PolicySetUnlinkError::NotLinkError(_)) => {
3077                //Restore self.policies
3078                self.policies.insert(policy_id.clone(), policy);
3079                Err(PolicySetError::UnlinkLinkNotLink(
3080                    policy_set_errors::UnlinkLinkNotLinkError { policy_id },
3081                ))
3082            }
3083            Err(ast::PolicySetUnlinkError::UnlinkingError(_)) => {
3084                panic!("Found linked policy in self.policies but not in self.ast")
3085            }
3086        }
3087    }
3088}
3089
3090impl std::fmt::Display for PolicySet {
3091    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3092        // prefer to display the lossless format
3093        let mut policies = self.policies().peekable();
3094        while let Some(policy) = policies.next() {
3095            policy.lossless.fmt(|| policy.ast.clone().into(), f)?;
3096            if policies.peek().is_some() {
3097                writeln!(f)?;
3098            }
3099        }
3100        Ok(())
3101    }
3102}
3103
3104/// Given a [`PolicyId`] and a [`Policy`], determine if the policy represents a static policy or a
3105/// link
3106fn is_static_or_link(
3107    (id, policy): (PolicyId, Policy),
3108) -> Result<Either<(ast::PolicyID, est::Policy), TemplateLink>, PolicyToJsonError> {
3109    match policy.template_id() {
3110        Some(template_id) => {
3111            let values = policy
3112                .ast
3113                .env()
3114                .iter()
3115                .map(|(id, euid)| (*id, euid.clone()))
3116                .collect();
3117            Ok(Either::Right(TemplateLink {
3118                new_id: id.into(),
3119                template_id: template_id.clone().into(),
3120                values,
3121            }))
3122        }
3123        None => policy
3124            .lossless
3125            .est(|| policy.ast.clone().into())
3126            .map(|est| Either::Left((id.into(), est))),
3127    }
3128}
3129
3130/// Like [`itertools::Itertools::partition_map`], but accepts a function that can fail.
3131/// The first invocation of `f` that fails causes the whole computation to fail
3132#[expect(
3133    clippy::redundant_pub_crate,
3134    reason = "can't be private because it's used in tests"
3135)]
3136pub(crate) fn fold_partition<T, A, B, E>(
3137    i: impl IntoIterator<Item = T>,
3138    f: impl Fn(T) -> Result<Either<A, B>, E>,
3139) -> Result<(Vec<A>, Vec<B>), E> {
3140    let mut lefts = vec![];
3141    let mut rights = vec![];
3142
3143    for item in i {
3144        match f(item)? {
3145            Either::Left(left) => lefts.push(left),
3146            Either::Right(right) => rights.push(right),
3147        }
3148    }
3149
3150    Ok((lefts, rights))
3151}
3152
3153/// The "type" of a [`Request`], i.e., the [`EntityTypeName`]s of principal
3154/// and resource, the [`EntityUid`] of action, and [`Option<EntityTypeName>`]s
3155/// of principal slot and resource slot
3156#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
3157pub struct RequestEnv {
3158    pub(crate) principal: EntityTypeName,
3159    pub(crate) action: EntityUid,
3160    pub(crate) resource: EntityTypeName,
3161    pub(crate) principal_slot: Option<EntityTypeName>,
3162    pub(crate) resource_slot: Option<EntityTypeName>,
3163}
3164
3165impl RequestEnv {
3166    /// Construct a [`RequestEnv`]
3167    pub fn new(principal: EntityTypeName, action: EntityUid, resource: EntityTypeName) -> Self {
3168        Self {
3169            principal,
3170            action,
3171            resource,
3172            principal_slot: None,
3173            resource_slot: None,
3174        }
3175    }
3176
3177    /// Construct a [`RequestEnv`] that contains slots in the scope
3178    pub fn new_request_env_with_slots(
3179        principal: EntityTypeName,
3180        action: EntityUid,
3181        resource: EntityTypeName,
3182        principal_slot: Option<EntityTypeName>,
3183        resource_slot: Option<EntityTypeName>,
3184    ) -> Self {
3185        Self {
3186            principal,
3187            action,
3188            resource,
3189            principal_slot,
3190            resource_slot,
3191        }
3192    }
3193
3194    /// Get the principal type name
3195    pub fn principal(&self) -> &EntityTypeName {
3196        &self.principal
3197    }
3198
3199    /// Get the action [`EntityUid`]
3200    pub fn action(&self) -> &EntityUid {
3201        &self.action
3202    }
3203
3204    /// Get the resource type name
3205    pub fn resource(&self) -> &EntityTypeName {
3206        &self.resource
3207    }
3208
3209    /// Get the principal slot type name
3210    pub fn principal_slot(&self) -> Option<&EntityTypeName> {
3211        self.principal_slot.as_ref()
3212    }
3213
3214    /// Get the resource slot type name
3215    pub fn resource_slot(&self) -> Option<&EntityTypeName> {
3216        self.resource_slot.as_ref()
3217    }
3218}
3219
3220#[doc(hidden)]
3221impl From<cedar_policy_core::validator::types::RequestEnv<'_>> for RequestEnv {
3222    fn from(renv: cedar_policy_core::validator::types::RequestEnv<'_>) -> Self {
3223        match renv {
3224            cedar_policy_core::validator::types::RequestEnv::DeclaredAction {
3225                principal,
3226                action,
3227                resource,
3228                principal_slot,
3229                resource_slot,
3230                ..
3231            } => Self {
3232                principal: principal.clone().into(),
3233                action: action.clone().into(),
3234                resource: resource.clone().into(),
3235                principal_slot: principal_slot.map(EntityTypeName::from),
3236                resource_slot: resource_slot.map(EntityTypeName::from),
3237            },
3238            #[expect(
3239                clippy::unreachable,
3240                reason = "partial validation is not enabled and hence `RequestEnv::UndeclaredAction` should not show up"
3241            )]
3242            cedar_policy_core::validator::types::RequestEnv::UndeclaredAction => {
3243                unreachable!("used unsupported feature")
3244            }
3245        }
3246    }
3247}
3248
3249/// Get valid request envs for an `ast::Template`
3250///
3251/// This function is called by [`Template::get_valid_request_envs`] and
3252/// [`Policy::get_valid_request_envs`]
3253fn get_valid_request_envs(ast: &ast::Template, s: &Schema) -> impl Iterator<Item = RequestEnv> {
3254    let tc = Typechecker::new(
3255        &s.0,
3256        cedar_policy_core::validator::ValidationMode::default(),
3257    );
3258    tc.typecheck_by_request_env(ast)
3259        .into_iter()
3260        .filter_map(|(env, pc)| {
3261            if matches!(pc, PolicyCheck::Success(_)) {
3262                Some(env.into())
3263            } else {
3264                None
3265            }
3266        })
3267        .collect::<BTreeSet<_>>()
3268        .into_iter()
3269}
3270
3271/// Policy template datatype
3272//
3273// NOTE: Unlike the internal type [`ast::Template`], this type only supports
3274// templates. The `Template` constructors will return an error if provided with
3275// a static policy.
3276#[derive(Debug, Clone)]
3277pub struct Template {
3278    /// AST representation of the template, used for most operations.
3279    /// In particular, the `ast` contains the authoritative `PolicyId` for the template.
3280    pub(crate) ast: ast::Template,
3281
3282    /// Some "lossless" representation of the template, whichever is most
3283    /// convenient to provide (and can be provided with the least overhead).
3284    /// This is used just for `to_json()`.
3285    /// We can't just derive this on-demand from `ast`, because the AST is lossy:
3286    /// we can't reconstruct an accurate CST/EST/policy-text from the AST, but
3287    /// we can from the EST (modulo whitespace and a few other things like the
3288    /// order of annotations).
3289    ///
3290    /// This is a `LosslessPolicy` (rather than something like `LosslessTemplate`)
3291    /// because the EST doesn't distinguish between static policies and templates.
3292    pub(crate) lossless: LosslessPolicy,
3293}
3294
3295impl PartialEq for Template {
3296    fn eq(&self, other: &Self) -> bool {
3297        // eq is based on just the `ast`
3298        self.ast.eq(&other.ast)
3299    }
3300}
3301impl Eq for Template {}
3302
3303#[doc(hidden)] // because this converts to a private/internal type
3304impl AsRef<ast::Template> for Template {
3305    fn as_ref(&self) -> &ast::Template {
3306        &self.ast
3307    }
3308}
3309
3310#[doc(hidden)]
3311impl From<ast::Template> for Template {
3312    fn from(template: ast::Template) -> Self {
3313        Self::from_ast(template)
3314    }
3315}
3316
3317impl Template {
3318    /// Attempt to parse a [`Template`] from source.
3319    /// Returns an error if the input is a static policy (i.e., has no slots).
3320    /// If `id` is Some, then the resulting template will have that `id`.
3321    /// If the `id` is None, the parser will use the default "policy0".
3322    /// The behavior around None may change in the future.
3323    pub fn parse(id: Option<PolicyId>, src: impl AsRef<str>) -> Result<Self, ParseErrors> {
3324        let ast = parser::parse_template(id.map(Into::into), src.as_ref())?;
3325        Ok(Self {
3326            ast,
3327            lossless: LosslessPolicy::policy_or_template_text(Some(src.as_ref())),
3328        })
3329    }
3330
3331    /// Get the `PolicyId` of this `Template`
3332    pub fn id(&self) -> &PolicyId {
3333        PolicyId::ref_cast(self.ast.id())
3334    }
3335
3336    /// Clone this `Template` with a new `PolicyId`
3337    #[must_use]
3338    pub fn new_id(&self, id: PolicyId) -> Self {
3339        Self {
3340            ast: self.ast.new_id(id.into()),
3341            lossless: self.lossless.clone(), // Lossless representation doesn't include the `PolicyId`
3342        }
3343    }
3344
3345    /// Get the `Effect` (`Forbid` or `Permit`) of this `Template`
3346    pub fn effect(&self) -> Effect {
3347        self.ast.effect()
3348    }
3349
3350    /// Returns `true` if this template has a `when` or `unless` clause.
3351    pub fn has_non_scope_constraint(&self) -> bool {
3352        self.ast.non_scope_constraints().is_some()
3353    }
3354
3355    /// Get an annotation value of this `Template`.
3356    /// If the annotation is present without an explicit value (e.g., `@annotation`),
3357    /// then this function returns `Some("")`. Returns `None` when the
3358    /// annotation is not present or when `key` is not a valid annotation identifier.
3359    pub fn annotation(&self, key: impl AsRef<str>) -> Option<&str> {
3360        self.ast
3361            .annotation(&key.as_ref().parse().ok()?)
3362            .map(AsRef::as_ref)
3363    }
3364
3365    /// Iterate through annotation data of this `Template` as key-value pairs.
3366    /// Annotations which do not have an explicit value (e.g., `@annotation`),
3367    /// are included in the iterator with the value `""`.
3368    pub fn annotations(&self) -> impl Iterator<Item = (&str, &str)> {
3369        self.ast
3370            .annotations()
3371            .map(|(k, v)| (k.as_ref(), v.as_ref()))
3372    }
3373
3374    /// Iterate over the open slots in this `Template`
3375    pub fn slots(&self) -> impl Iterator<Item = &SlotId> {
3376        self.ast.slots().map(|slot| SlotId::ref_cast(&slot.id))
3377    }
3378
3379    /// Get the scope constraint on this policy's principal
3380    pub fn principal_constraint(&self) -> TemplatePrincipalConstraint {
3381        match self.ast.principal_constraint().as_inner() {
3382            ast::PrincipalOrResourceConstraint::Any => TemplatePrincipalConstraint::Any,
3383            ast::PrincipalOrResourceConstraint::In(eref) => {
3384                TemplatePrincipalConstraint::In(match eref {
3385                    ast::EntityReference::EUID(e) => Some(e.as_ref().clone().into()),
3386                    ast::EntityReference::Slot(_) => None,
3387                })
3388            }
3389            ast::PrincipalOrResourceConstraint::Eq(eref) => {
3390                TemplatePrincipalConstraint::Eq(match eref {
3391                    ast::EntityReference::EUID(e) => Some(e.as_ref().clone().into()),
3392                    ast::EntityReference::Slot(_) => None,
3393                })
3394            }
3395            ast::PrincipalOrResourceConstraint::Is(entity_type) => {
3396                TemplatePrincipalConstraint::Is(entity_type.as_ref().clone().into())
3397            }
3398            ast::PrincipalOrResourceConstraint::IsIn(entity_type, eref) => {
3399                TemplatePrincipalConstraint::IsIn(
3400                    entity_type.as_ref().clone().into(),
3401                    match eref {
3402                        ast::EntityReference::EUID(e) => Some(e.as_ref().clone().into()),
3403                        ast::EntityReference::Slot(_) => None,
3404                    },
3405                )
3406            }
3407        }
3408    }
3409
3410    /// Get the scope constraint on this policy's action
3411    pub fn action_constraint(&self) -> ActionConstraint {
3412        // Clone the data from Core to be consistent with the other constraints
3413        match self.ast.action_constraint() {
3414            ast::ActionConstraint::Any => ActionConstraint::Any,
3415            ast::ActionConstraint::In(ids) => {
3416                ActionConstraint::In(ids.iter().map(|id| id.as_ref().clone().into()).collect())
3417            }
3418            ast::ActionConstraint::Eq(id) => ActionConstraint::Eq(id.as_ref().clone().into()),
3419            #[cfg(feature = "tolerant-ast")]
3420            ast::ActionConstraint::ErrorConstraint => {
3421                // We will only have an ErrorConstraint if we are using a parser that allows Error nodes
3422                // It is not recommended to evaluate an AST that allows error nodes
3423                // If somehow someone tries to evaluate an AST that includes an Action constraint error, we will
3424                // treat it as `Any`
3425                ActionConstraint::Any
3426            }
3427        }
3428    }
3429
3430    /// Get the scope constraint on this policy's resource
3431    pub fn resource_constraint(&self) -> TemplateResourceConstraint {
3432        match self.ast.resource_constraint().as_inner() {
3433            ast::PrincipalOrResourceConstraint::Any => TemplateResourceConstraint::Any,
3434            ast::PrincipalOrResourceConstraint::In(eref) => {
3435                TemplateResourceConstraint::In(match eref {
3436                    ast::EntityReference::EUID(e) => Some(e.as_ref().clone().into()),
3437                    ast::EntityReference::Slot(_) => None,
3438                })
3439            }
3440            ast::PrincipalOrResourceConstraint::Eq(eref) => {
3441                TemplateResourceConstraint::Eq(match eref {
3442                    ast::EntityReference::EUID(e) => Some(e.as_ref().clone().into()),
3443                    ast::EntityReference::Slot(_) => None,
3444                })
3445            }
3446            ast::PrincipalOrResourceConstraint::Is(entity_type) => {
3447                TemplateResourceConstraint::Is(entity_type.as_ref().clone().into())
3448            }
3449            ast::PrincipalOrResourceConstraint::IsIn(entity_type, eref) => {
3450                TemplateResourceConstraint::IsIn(
3451                    entity_type.as_ref().clone().into(),
3452                    match eref {
3453                        ast::EntityReference::EUID(e) => Some(e.as_ref().clone().into()),
3454                        ast::EntityReference::Slot(_) => None,
3455                    },
3456                )
3457            }
3458        }
3459    }
3460
3461    /// Create a [`Template`] from its JSON representation.
3462    /// Returns an error if the input is a static policy (i.e., has no slots).
3463    /// If `id` is Some, the policy will be given that Policy Id.
3464    /// If `id` is None, then "JSON policy" will be used.
3465    /// The behavior around None may change in the future.
3466    pub fn from_json(
3467        id: Option<PolicyId>,
3468        json: serde_json::Value,
3469    ) -> Result<Self, PolicyFromJsonError> {
3470        let est: est::Policy = serde_json::from_value(json)
3471            .map_err(|e| entities_json_errors::JsonDeserializationError::Serde(e.into()))
3472            .map_err(cedar_policy_core::est::FromJsonError::from)?;
3473        Self::from_est(id, est)
3474    }
3475
3476    fn from_est(id: Option<PolicyId>, est: est::Policy) -> Result<Self, PolicyFromJsonError> {
3477        Ok(Self {
3478            ast: est.clone().try_into_ast_template(id.map(PolicyId::into))?,
3479            lossless: LosslessPolicy::Est(est),
3480        })
3481    }
3482
3483    pub(crate) fn from_ast(ast: ast::Template) -> Self {
3484        Self {
3485            lossless: LosslessPolicy::Est(ast.clone().into()),
3486            ast,
3487        }
3488    }
3489
3490    /// Get the JSON representation of this `Template`.
3491    pub fn to_json(&self) -> Result<serde_json::Value, PolicyToJsonError> {
3492        let est = self.lossless.est(|| self.ast.clone().into())?;
3493        serde_json::to_value(est).map_err(Into::into)
3494    }
3495
3496    /// Get the human-readable Cedar syntax representation of this template.
3497    /// This function is primarily intended for rendering JSON policies in the
3498    /// human-readable syntax, but it will also return the original policy text
3499    /// when given a policy parsed from the human-readable syntax.
3500    ///
3501    /// It also does not format the policy according to any particular rules.
3502    /// Policy formatting can be done through the Cedar policy CLI or
3503    /// the `cedar-policy-formatter` crate.
3504    pub fn to_cedar(&self) -> String {
3505        match &self.lossless {
3506            LosslessPolicy::Empty | LosslessPolicy::Est(_) => self.ast.to_string(),
3507            LosslessPolicy::Text { text, .. } => text.clone(),
3508        }
3509    }
3510
3511    /// Get the valid [`RequestEnv`]s for this template, according to the schema.
3512    ///
3513    /// That is, all the [`RequestEnv`]s in the schema for which this template is
3514    /// not trivially false.
3515    pub fn get_valid_request_envs(&self, s: &Schema) -> impl Iterator<Item = RequestEnv> {
3516        get_valid_request_envs(&self.ast, s)
3517    }
3518}
3519
3520impl std::fmt::Display for Template {
3521    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3522        // prefer to display the lossless format
3523        self.lossless.fmt(|| self.ast.clone().into(), f)
3524    }
3525}
3526
3527impl FromStr for Template {
3528    type Err = ParseErrors;
3529
3530    fn from_str(src: &str) -> Result<Self, Self::Err> {
3531        Self::parse(None, src)
3532    }
3533}
3534
3535/// Scope constraint on policy principals.
3536#[derive(Debug, Clone, PartialEq, Eq)]
3537pub enum PrincipalConstraint {
3538    /// Un-constrained
3539    Any,
3540    /// Must be In the given [`EntityUid`]
3541    In(EntityUid),
3542    /// Must be equal to the given [`EntityUid`]
3543    Eq(EntityUid),
3544    /// Must be the given [`EntityTypeName`]
3545    Is(EntityTypeName),
3546    /// Must be the given [`EntityTypeName`], and `in` the [`EntityUid`]
3547    IsIn(EntityTypeName, EntityUid),
3548}
3549
3550/// Scope constraint on policy principals for templates.
3551#[derive(Debug, Clone, PartialEq, Eq)]
3552pub enum TemplatePrincipalConstraint {
3553    /// Un-constrained
3554    Any,
3555    /// Must be In the given [`EntityUid`].
3556    /// If [`None`], then it is a template slot.
3557    In(Option<EntityUid>),
3558    /// Must be equal to the given [`EntityUid`].
3559    /// If [`None`], then it is a template slot.
3560    Eq(Option<EntityUid>),
3561    /// Must be the given [`EntityTypeName`].
3562    Is(EntityTypeName),
3563    /// Must be the given [`EntityTypeName`], and `in` the [`EntityUid`].
3564    /// If the [`EntityUid`] is [`Option::None`], then it is a template slot.
3565    IsIn(EntityTypeName, Option<EntityUid>),
3566}
3567
3568impl TemplatePrincipalConstraint {
3569    /// Does this constraint contain a slot?
3570    pub fn has_slot(&self) -> bool {
3571        match self {
3572            Self::Any | Self::Is(_) => false,
3573            Self::In(o) | Self::Eq(o) | Self::IsIn(_, o) => o.is_none(),
3574        }
3575    }
3576}
3577
3578/// Scope constraint on policy actions.
3579#[derive(Debug, Clone, PartialEq, Eq)]
3580pub enum ActionConstraint {
3581    /// Un-constrained
3582    Any,
3583    /// Must be In the given [`EntityUid`]
3584    In(Vec<EntityUid>),
3585    /// Must be equal to the given [`EntityUid]`
3586    Eq(EntityUid),
3587}
3588
3589/// Scope constraint on policy resources.
3590#[derive(Debug, Clone, PartialEq, Eq)]
3591pub enum ResourceConstraint {
3592    /// Un-constrained
3593    Any,
3594    /// Must be In the given [`EntityUid`]
3595    In(EntityUid),
3596    /// Must be equal to the given [`EntityUid`]
3597    Eq(EntityUid),
3598    /// Must be the given [`EntityTypeName`]
3599    Is(EntityTypeName),
3600    /// Must be the given [`EntityTypeName`], and `in` the [`EntityUid`]
3601    IsIn(EntityTypeName, EntityUid),
3602}
3603
3604/// Scope constraint on policy resources for templates.
3605#[derive(Debug, Clone, PartialEq, Eq)]
3606pub enum TemplateResourceConstraint {
3607    /// Un-constrained
3608    Any,
3609    /// Must be In the given [`EntityUid`].
3610    /// If [`None`], then it is a template slot.
3611    In(Option<EntityUid>),
3612    /// Must be equal to the given [`EntityUid`].
3613    /// If [`None`], then it is a template slot.
3614    Eq(Option<EntityUid>),
3615    /// Must be the given [`EntityTypeName`].
3616    Is(EntityTypeName),
3617    /// Must be the given [`EntityTypeName`], and `in` the [`EntityUid`].
3618    /// If the [`EntityUid`] is [`Option::None`], then it is a template slot.
3619    IsIn(EntityTypeName, Option<EntityUid>),
3620}
3621
3622impl TemplateResourceConstraint {
3623    /// Does this constraint contain a slot?
3624    pub fn has_slot(&self) -> bool {
3625        match self {
3626            Self::Any | Self::Is(_) => false,
3627            Self::In(o) | Self::Eq(o) | Self::IsIn(_, o) => o.is_none(),
3628        }
3629    }
3630}
3631
3632/// Structure for a `Policy`. Includes both static policies and template-linked policies.
3633#[derive(Debug, Clone)]
3634pub struct Policy {
3635    /// AST representation of the policy, used for most operations.
3636    /// In particular, the `ast` contains the authoritative `PolicyId` for the policy.
3637    pub(crate) ast: ast::Policy,
3638    /// Some "lossless" representation of the policy, whichever is most
3639    /// convenient to provide (and can be provided with the least overhead).
3640    /// This is used just for `to_json()`.
3641    /// We can't just derive this on-demand from `ast`, because the AST is lossy:
3642    /// we can't reconstruct an accurate CST/EST/policy-text from the AST, but
3643    /// we can from the EST (modulo whitespace and a few other things like the
3644    /// order of annotations).
3645    pub(crate) lossless: LosslessPolicy,
3646}
3647
3648impl PartialEq for Policy {
3649    fn eq(&self, other: &Self) -> bool {
3650        // eq is based on just the `ast`
3651        self.ast.eq(&other.ast)
3652    }
3653}
3654impl Eq for Policy {}
3655
3656#[doc(hidden)] // because this converts to a private/internal type
3657impl AsRef<ast::Policy> for Policy {
3658    fn as_ref(&self) -> &ast::Policy {
3659        &self.ast
3660    }
3661}
3662
3663#[doc(hidden)]
3664impl From<ast::Policy> for Policy {
3665    fn from(policy: ast::Policy) -> Self {
3666        Self::from_ast(policy)
3667    }
3668}
3669
3670#[doc(hidden)]
3671impl From<ast::StaticPolicy> for Policy {
3672    fn from(policy: ast::StaticPolicy) -> Self {
3673        ast::Policy::from(policy).into()
3674    }
3675}
3676
3677impl Policy {
3678    /// Get the `PolicyId` of the `Template` this is linked to.
3679    /// If this is a static policy, this will return `None`.
3680    pub fn template_id(&self) -> Option<&PolicyId> {
3681        if self.is_static() {
3682            None
3683        } else {
3684            Some(PolicyId::ref_cast(self.ast.template().id()))
3685        }
3686    }
3687
3688    /// Get the values this `Template` is linked to, expressed as a map from `SlotId` to `EntityUid`.
3689    /// If this is a static policy, this will return `None`.
3690    pub fn template_links(&self) -> Option<HashMap<SlotId, EntityUid>> {
3691        if self.is_static() {
3692            None
3693        } else {
3694            let wrapped_vals: HashMap<SlotId, EntityUid> = self
3695                .ast
3696                .env()
3697                .iter()
3698                .map(|(key, value)| ((*key).into(), value.clone().into()))
3699                .collect();
3700            Some(wrapped_vals)
3701        }
3702    }
3703
3704    /// Get the `Effect` (`Permit` or `Forbid`) for this instance
3705    pub fn effect(&self) -> Effect {
3706        self.ast.effect()
3707    }
3708
3709    /// Returns `true` if this policy has a `when` or `unless` clause.
3710    pub fn has_non_scope_constraint(&self) -> bool {
3711        self.ast.non_scope_constraints().is_some()
3712    }
3713
3714    /// Get an annotation value of this template-linked or static policy.
3715    /// If the annotation is present without an explicit value (e.g., `@annotation`),
3716    /// then this function returns `Some("")`. Returns `None` when the
3717    /// annotation is not present or when `key` is not a valid annotations identifier.
3718    pub fn annotation(&self, key: impl AsRef<str>) -> Option<&str> {
3719        self.ast
3720            .annotation(&key.as_ref().parse().ok()?)
3721            .map(AsRef::as_ref)
3722    }
3723
3724    /// Iterate through annotation data of this template-linked or static policy.
3725    /// Annotations which do not have an explicit value (e.g., `@annotation`),
3726    /// are included in the iterator with the value `""`.
3727    pub fn annotations(&self) -> impl Iterator<Item = (&str, &str)> {
3728        self.ast
3729            .annotations()
3730            .map(|(k, v)| (k.as_ref(), v.as_ref()))
3731    }
3732
3733    /// Get the `PolicyId` for this template-linked or static policy
3734    pub fn id(&self) -> &PolicyId {
3735        PolicyId::ref_cast(self.ast.id())
3736    }
3737
3738    /// Clone this `Policy` with a new `PolicyId`
3739    #[must_use]
3740    pub fn new_id(&self, id: PolicyId) -> Self {
3741        Self {
3742            ast: self.ast.new_id(id.into()),
3743            lossless: self.lossless.clone(), // Lossless representation doesn't include the `PolicyId`
3744        }
3745    }
3746
3747    /// Returns `true` if this is a static policy, `false` otherwise.
3748    pub fn is_static(&self) -> bool {
3749        self.ast.is_static()
3750    }
3751
3752    /// Get the scope constraint on this policy's principal
3753    pub fn principal_constraint(&self) -> PrincipalConstraint {
3754        let slot_id = ast::SlotId::principal();
3755        match self.ast.template().principal_constraint().as_inner() {
3756            ast::PrincipalOrResourceConstraint::Any => PrincipalConstraint::Any,
3757            ast::PrincipalOrResourceConstraint::In(eref) => {
3758                PrincipalConstraint::In(self.convert_entity_reference(eref, slot_id).clone())
3759            }
3760            ast::PrincipalOrResourceConstraint::Eq(eref) => {
3761                PrincipalConstraint::Eq(self.convert_entity_reference(eref, slot_id).clone())
3762            }
3763            ast::PrincipalOrResourceConstraint::Is(entity_type) => {
3764                PrincipalConstraint::Is(entity_type.as_ref().clone().into())
3765            }
3766            ast::PrincipalOrResourceConstraint::IsIn(entity_type, eref) => {
3767                PrincipalConstraint::IsIn(
3768                    entity_type.as_ref().clone().into(),
3769                    self.convert_entity_reference(eref, slot_id).clone(),
3770                )
3771            }
3772        }
3773    }
3774
3775    /// Get the scope constraint on this policy's action
3776    pub fn action_constraint(&self) -> ActionConstraint {
3777        // Clone the data from Core to be consistant with the other constraints
3778        match self.ast.template().action_constraint() {
3779            ast::ActionConstraint::Any => ActionConstraint::Any,
3780            ast::ActionConstraint::In(ids) => ActionConstraint::In(
3781                ids.iter()
3782                    .map(|euid| EntityUid::ref_cast(euid.as_ref()))
3783                    .cloned()
3784                    .collect(),
3785            ),
3786            ast::ActionConstraint::Eq(id) => ActionConstraint::Eq(EntityUid::ref_cast(id).clone()),
3787            #[cfg(feature = "tolerant-ast")]
3788            ast::ActionConstraint::ErrorConstraint => {
3789                // We will only have an ErrorConstraint if we are using a parser that allows Error nodes
3790                // It is not recommended to evaluate an AST that allows error nodes
3791                // If somehow someone tries to evaluate an AST that includes an Action constraint error, we will
3792                // treat it as `Any`
3793                ActionConstraint::Any
3794            }
3795        }
3796    }
3797
3798    /// Get the scope constraint on this policy's resource
3799    pub fn resource_constraint(&self) -> ResourceConstraint {
3800        let slot_id = ast::SlotId::resource();
3801        match self.ast.template().resource_constraint().as_inner() {
3802            ast::PrincipalOrResourceConstraint::Any => ResourceConstraint::Any,
3803            ast::PrincipalOrResourceConstraint::In(eref) => {
3804                ResourceConstraint::In(self.convert_entity_reference(eref, slot_id).clone())
3805            }
3806            ast::PrincipalOrResourceConstraint::Eq(eref) => {
3807                ResourceConstraint::Eq(self.convert_entity_reference(eref, slot_id).clone())
3808            }
3809            ast::PrincipalOrResourceConstraint::Is(entity_type) => {
3810                ResourceConstraint::Is(entity_type.as_ref().clone().into())
3811            }
3812            ast::PrincipalOrResourceConstraint::IsIn(entity_type, eref) => {
3813                ResourceConstraint::IsIn(
3814                    entity_type.as_ref().clone().into(),
3815                    self.convert_entity_reference(eref, slot_id).clone(),
3816                )
3817            }
3818        }
3819    }
3820
3821    /// To avoid panicking, this function may only be called when `slot` is the
3822    /// `SlotId` corresponding to the scope constraint from which the entity
3823    /// reference `r` was extracted. I.e., If `r` is taken from the principal
3824    /// scope constraint, `slot` must be `?principal`. This ensures that the
3825    /// `SlotId` exists in the policy (and therefore the slot environment map)
3826    /// whenever the `EntityReference` `r` is the Slot variant.
3827    fn convert_entity_reference<'a>(
3828        &'a self,
3829        r: &'a ast::EntityReference,
3830        slot: ast::SlotId,
3831    ) -> &'a EntityUid {
3832        match r {
3833            ast::EntityReference::EUID(euid) => EntityUid::ref_cast(euid),
3834            #[expect(
3835                clippy::unwrap_used,
3836                reason = "This `unwrap` here is safe due the invariant (values total map) on policies"
3837            )]
3838            ast::EntityReference::Slot(_) => {
3839                EntityUid::ref_cast(self.ast.env().get(&slot).unwrap())
3840            }
3841        }
3842    }
3843
3844    /// Parse a single policy.
3845    /// If `id` is Some, the policy will be given that Policy Id.
3846    /// If `id` is None, then "policy0" will be used.
3847    /// The behavior around None may change in the future.
3848    ///
3849    /// This can fail if the policy fails to parse.
3850    /// It can also fail if a template was passed in, as this function only accepts static
3851    /// policies
3852    pub fn parse(id: Option<PolicyId>, policy_src: impl AsRef<str>) -> Result<Self, ParseErrors> {
3853        let inline_ast = parser::parse_policy(id.map(Into::into), policy_src.as_ref())?;
3854        let (_, ast) = ast::Template::link_static_policy(inline_ast);
3855        Ok(Self {
3856            ast,
3857            lossless: LosslessPolicy::policy_or_template_text(Some(policy_src.as_ref())),
3858        })
3859    }
3860
3861    /// Create a `Policy` from its JSON representation.
3862    /// If `id` is Some, the policy will be given that Policy Id.
3863    /// If `id` is None, then "JSON policy" will be used.
3864    /// The behavior around None may change in the future.
3865    ///
3866    /// ```
3867    /// # use cedar_policy::{Policy, PolicyId};
3868    ///
3869    /// let json: serde_json::Value = serde_json::json!(
3870    ///        {
3871    ///            "effect":"permit",
3872    ///            "principal":{
3873    ///            "op":"==",
3874    ///            "entity":{
3875    ///                "type":"User",
3876    ///                "id":"bob"
3877    ///            }
3878    ///            },
3879    ///            "action":{
3880    ///            "op":"==",
3881    ///            "entity":{
3882    ///                "type":"Action",
3883    ///                "id":"view"
3884    ///            }
3885    ///            },
3886    ///            "resource":{
3887    ///            "op":"==",
3888    ///            "entity":{
3889    ///                "type":"Album",
3890    ///                "id":"trip"
3891    ///            }
3892    ///            },
3893    ///            "conditions":[
3894    ///            {
3895    ///                "kind":"when",
3896    ///                "body":{
3897    ///                   ">":{
3898    ///                        "left":{
3899    ///                        ".":{
3900    ///                            "left":{
3901    ///                                "Var":"principal"
3902    ///                            },
3903    ///                            "attr":"age"
3904    ///                        }
3905    ///                        },
3906    ///                        "right":{
3907    ///                        "Value":18
3908    ///                        }
3909    ///                    }
3910    ///                }
3911    ///            }
3912    ///            ]
3913    ///        }
3914    /// );
3915    /// let json_policy = Policy::from_json(None, json).unwrap();
3916    /// let src = r#"
3917    ///   permit(
3918    ///     principal == User::"bob",
3919    ///     action == Action::"view",
3920    ///     resource == Album::"trip"
3921    ///   )
3922    ///   when { principal.age > 18 };"#;
3923    /// let text_policy = Policy::parse(None, src).unwrap();
3924    /// assert_eq!(json_policy.to_json().unwrap(), text_policy.to_json().unwrap());
3925    /// ```
3926    pub fn from_json(
3927        id: Option<PolicyId>,
3928        json: serde_json::Value,
3929    ) -> Result<Self, PolicyFromJsonError> {
3930        let est: est::Policy = serde_json::from_value(json)
3931            .map_err(|e| entities_json_errors::JsonDeserializationError::Serde(e.into()))
3932            .map_err(cedar_policy_core::est::FromJsonError::from)?;
3933        Self::from_est(id, est)
3934    }
3935
3936    /// Get the valid [`RequestEnv`]s for this policy, according to the schema.
3937    ///
3938    /// That is, all the [`RequestEnv`]s in the schema for which this policy is
3939    /// not trivially false.
3940    pub fn get_valid_request_envs(&self, s: &Schema) -> impl Iterator<Item = RequestEnv> {
3941        get_valid_request_envs(self.ast.template(), s)
3942    }
3943
3944    /// Get all entity literals occuring in a `Policy`
3945    pub fn entity_literals(&self) -> Vec<EntityUid> {
3946        self.ast
3947            .condition()
3948            .subexpressions()
3949            .filter_map(|e| match e.expr_kind() {
3950                cedar_policy_core::ast::ExprKind::Lit(
3951                    cedar_policy_core::ast::Literal::EntityUID(euid),
3952                ) => Some(EntityUid((*euid).as_ref().clone())),
3953                _ => None,
3954            })
3955            .collect()
3956    }
3957
3958    /// Return a new policy where all occurrences of key `EntityUid`s are replaced by value `EntityUid`
3959    /// (as a single, non-sequential substitution).
3960    pub fn sub_entity_literals(
3961        &self,
3962        mapping: BTreeMap<EntityUid, EntityUid>,
3963    ) -> Result<Self, PolicyFromJsonError> {
3964        #[expect(
3965            clippy::expect_used,
3966            reason = "This can't fail for a policy that was already constructed"
3967        )]
3968        let cloned_est = self
3969            .lossless
3970            .est(|| self.ast.clone().into())
3971            .expect("Internal error, failed to construct est.");
3972
3973        let mapping = mapping.into_iter().map(|(k, v)| (k.0, v.0)).collect();
3974
3975        #[expect(
3976            clippy::expect_used,
3977            reason = "This can't fail for a policy that was already constructed"
3978        )]
3979        let est = cloned_est
3980            .sub_entity_literals(&mapping)
3981            .expect("Internal error, failed to sub entity literals.");
3982
3983        let ast = est
3984            .clone()
3985            .try_into_ast_policy(Some(self.ast.id().clone()))?;
3986
3987        Ok(Self {
3988            ast,
3989            lossless: LosslessPolicy::Est(est),
3990        })
3991    }
3992
3993    fn from_est(id: Option<PolicyId>, est: est::Policy) -> Result<Self, PolicyFromJsonError> {
3994        Ok(Self {
3995            ast: est.clone().try_into_ast_policy(id.map(PolicyId::into))?,
3996            lossless: LosslessPolicy::Est(est),
3997        })
3998    }
3999
4000    /// Get the JSON representation of this `Policy`.
4001    ///  ```
4002    /// # use cedar_policy::Policy;
4003    /// let src = r#"
4004    ///   permit(
4005    ///     principal == User::"bob",
4006    ///     action == Action::"view",
4007    ///     resource == Album::"trip"
4008    ///   )
4009    ///   when { principal.age > 18 };"#;
4010    ///
4011    /// let policy = Policy::parse(None, src).unwrap();
4012    /// println!("{}", policy);
4013    /// // convert the policy to JSON
4014    /// let json = policy.to_json().unwrap();
4015    /// println!("{}", json);
4016    /// assert_eq!(json, Policy::from_json(None, json.clone()).unwrap().to_json().unwrap());
4017    /// ```
4018    pub fn to_json(&self) -> Result<serde_json::Value, PolicyToJsonError> {
4019        let est = self.lossless.est(|| self.ast.clone().into())?;
4020        serde_json::to_value(est).map_err(Into::into)
4021    }
4022
4023    /// Get the human-readable Cedar syntax representation of this policy. This
4024    /// function is primarily intended for rendering JSON policies in the
4025    /// human-readable syntax, but it will also return the original policy text
4026    /// when given a policy parsed from the human-readable syntax.
4027    ///
4028    /// It will return `None` for linked policies because they cannot be
4029    /// directly rendered in Cedar syntax. You can instead render the unlinked
4030    /// template if you do not need to preserve links. If serializing links is
4031    /// important, then you will need to serialize the whole policy set
4032    /// containing the template and link to JSON (or protobuf).
4033    ///
4034    /// It also does not format the policy according to any particular rules.
4035    /// Policy formatting can be done through the Cedar policy CLI or
4036    /// the `cedar-policy-formatter` crate.
4037    pub fn to_cedar(&self) -> Option<String> {
4038        match &self.lossless {
4039            LosslessPolicy::Empty | LosslessPolicy::Est(_) => Some(self.ast.to_string()),
4040            LosslessPolicy::Text { text, slots } => {
4041                if slots.is_empty() {
4042                    Some(text.clone())
4043                } else {
4044                    None
4045                }
4046            }
4047        }
4048    }
4049
4050    /// Get all the unknown entities from the policy
4051    #[doc = include_str!("../experimental_warning.md")]
4052    #[cfg(feature = "partial-eval")]
4053    pub fn unknown_entities(&self) -> HashSet<EntityUid> {
4054        self.ast
4055            .unknown_entities()
4056            .into_iter()
4057            .map(Into::into)
4058            .collect()
4059    }
4060
4061    /// Create a `Policy` from its AST representation only. The `LosslessPolicy`
4062    /// will reflect the AST structure. When possible, don't use this method and
4063    /// create the `Policy` from the policy text, CST, or EST instead, as the
4064    /// conversion to AST is lossy. ESTs for policies generated by this method
4065    /// will reflect the AST and not the original policy syntax.
4066    pub(crate) fn from_ast(ast: ast::Policy) -> Self {
4067        let text = ast.to_string(); // assume that pretty-printing is faster than `est::Policy::from(ast.clone())`; is that true?
4068        Self {
4069            ast,
4070            lossless: LosslessPolicy::policy_or_template_text(Some(text)),
4071        }
4072    }
4073}
4074
4075impl std::fmt::Display for Policy {
4076    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
4077        // prefer to display the lossless format
4078        self.lossless.fmt(|| self.ast.clone().into(), f)
4079    }
4080}
4081
4082impl FromStr for Policy {
4083    type Err = ParseErrors;
4084    /// Create a policy
4085    ///
4086    /// Important note: Policies have ids, but this interface does not
4087    /// allow them to be set. It will use the default "policy0", which
4088    /// may cause id conflicts if not handled. Use `Policy::parse` to set
4089    /// the id when parsing, or `Policy::new_id` to clone a policy with
4090    /// a new id.
4091    fn from_str(policy: &str) -> Result<Self, Self::Err> {
4092        Self::parse(None, policy)
4093    }
4094}
4095
4096/// See comments on `Policy` and `Template`.
4097///
4098/// This structure can be used for static policies, linked policies, and templates.
4099#[derive(Debug, Clone)]
4100pub(crate) enum LosslessPolicy {
4101    /// An empty representation
4102    Empty,
4103    /// EST representation
4104    Est(est::Policy),
4105    /// Text representation
4106    Text {
4107        /// actual policy text, of the policy or template
4108        text: String,
4109        /// For linked policies, map of slot to UID. Only linked policies have
4110        /// this; static policies and (unlinked) templates have an empty map
4111        /// here
4112        slots: HashMap<ast::SlotId, ast::EntityUID>,
4113    },
4114}
4115
4116impl LosslessPolicy {
4117    /// Create a new `LosslessPolicy` from the text of a policy or template.
4118    fn policy_or_template_text(text: Option<impl Into<String>>) -> Self {
4119        text.map_or(Self::Empty, |text| Self::Text {
4120            text: text.into(),
4121            slots: HashMap::new(),
4122        })
4123    }
4124
4125    /// Get the EST representation of this static policy, linked policy, or template.
4126    fn est(
4127        &self,
4128        fallback_est: impl FnOnce() -> est::Policy,
4129    ) -> Result<est::Policy, PolicyToJsonError> {
4130        match self {
4131            // Fall back to the `policy` AST if the lossless representation is empty
4132            Self::Empty => Ok(fallback_est()),
4133            Self::Est(est) => Ok(est.clone()),
4134            Self::Text { text, slots } => {
4135                let est =
4136                    parser::parse_policy_or_template_to_est(text).map_err(ParseErrors::from)?;
4137                if slots.is_empty() {
4138                    Ok(est)
4139                } else {
4140                    let unwrapped_vals = slots.iter().map(|(k, v)| (*k, v.into())).collect();
4141                    Ok(est.link(&unwrapped_vals)?)
4142                }
4143            }
4144        }
4145    }
4146
4147    fn link<'a>(
4148        self,
4149        vals: impl IntoIterator<Item = (ast::SlotId, &'a ast::EntityUID)>,
4150    ) -> Result<Self, est::LinkingError> {
4151        match self {
4152            Self::Empty => Ok(Self::Empty),
4153            Self::Est(est) => {
4154                let unwrapped_est_vals: HashMap<
4155                    ast::SlotId,
4156                    cedar_policy_core::entities::EntityUidJson,
4157                > = vals.into_iter().map(|(k, v)| (k, v.into())).collect();
4158                Ok(Self::Est(est.link(&unwrapped_est_vals)?))
4159            }
4160            Self::Text { text, slots } => {
4161                debug_assert!(
4162                    slots.is_empty(),
4163                    "shouldn't call link() on an already-linked policy"
4164                );
4165                let slots = vals.into_iter().map(|(k, v)| (k, v.clone())).collect();
4166                Ok(Self::Text { text, slots })
4167            }
4168        }
4169    }
4170
4171    fn fmt(
4172        &self,
4173        fallback_est: impl FnOnce() -> est::Policy,
4174        f: &mut std::fmt::Formatter<'_>,
4175    ) -> std::fmt::Result {
4176        match self {
4177            Self::Empty => match self.est(fallback_est) {
4178                Ok(est) => write!(f, "{est}"),
4179                Err(e) => write!(f, "<invalid policy: {e}>"),
4180            },
4181            Self::Est(est) => write!(f, "{est}"),
4182            Self::Text { text, slots } => {
4183                if slots.is_empty() {
4184                    write!(f, "{text}")
4185                } else {
4186                    // need to replace placeholders according to `slots`.
4187                    // just find-and-replace wouldn't be safe/perfect, we
4188                    // want to use the actual parser; right now we reuse
4189                    // another implementation by just converting to EST and
4190                    // printing that
4191                    match self.est(fallback_est) {
4192                        Ok(est) => write!(f, "{est}"),
4193                        Err(e) => write!(f, "<invalid linked policy: {e}>"),
4194                    }
4195                }
4196            }
4197        }
4198    }
4199}
4200
4201/// Expressions to be evaluated
4202#[repr(transparent)]
4203#[derive(Debug, Clone, RefCast)]
4204pub struct Expression(pub(crate) ast::Expr);
4205
4206#[doc(hidden)] // because this converts to a private/internal type
4207impl AsRef<ast::Expr> for Expression {
4208    fn as_ref(&self) -> &ast::Expr {
4209        &self.0
4210    }
4211}
4212
4213#[doc(hidden)]
4214impl From<ast::Expr> for Expression {
4215    fn from(expr: ast::Expr) -> Self {
4216        Self(expr)
4217    }
4218}
4219
4220impl Expression {
4221    /// Create an expression representing a literal string.
4222    pub fn new_string(value: String) -> Self {
4223        Self(ast::Expr::val(value))
4224    }
4225
4226    /// Create an expression representing a literal bool.
4227    pub fn new_bool(value: bool) -> Self {
4228        Self(ast::Expr::val(value))
4229    }
4230
4231    /// Create an expression representing a literal long.
4232    pub fn new_long(value: ast::Integer) -> Self {
4233        Self(ast::Expr::val(value))
4234    }
4235
4236    /// Create an expression representing a record.
4237    ///
4238    /// Error if any key appears two or more times in `fields`.
4239    pub fn new_record(
4240        fields: impl IntoIterator<Item = (String, Self)>,
4241    ) -> Result<Self, ExpressionConstructionError> {
4242        Ok(Self(ast::Expr::record(
4243            fields.into_iter().map(|(k, v)| (SmolStr::from(k), v.0)),
4244        )?))
4245    }
4246
4247    /// Create an expression representing a Set.
4248    pub fn new_set(values: impl IntoIterator<Item = Self>) -> Self {
4249        Self(ast::Expr::set(values.into_iter().map(|v| v.0)))
4250    }
4251
4252    /// Create an expression representing an ip address.
4253    /// This function does not perform error checking on the source string,
4254    /// it creates an expression that calls the `ip` constructor.
4255    pub fn new_ip(src: impl AsRef<str>) -> Self {
4256        let src_expr = ast::Expr::val(src.as_ref());
4257        Self(ast::Expr::call_extension_fn(
4258            ip_extension_name(),
4259            vec![src_expr],
4260        ))
4261    }
4262
4263    /// Create an expression representing a fixed precision decimal number.
4264    /// This function does not perform error checking on the source string,
4265    /// it creates an expression that calls the `decimal` constructor.
4266    pub fn new_decimal(src: impl AsRef<str>) -> Self {
4267        let src_expr = ast::Expr::val(src.as_ref());
4268        Self(ast::Expr::call_extension_fn(
4269            decimal_extension_name(),
4270            vec![src_expr],
4271        ))
4272    }
4273
4274    /// Create an expression representing a particular instant of time.
4275    /// This function does not perform error checking on the source string,
4276    /// it creates an expression that calls the `datetime` constructor.
4277    pub fn new_datetime(src: impl AsRef<str>) -> Self {
4278        let src_expr = ast::Expr::val(src.as_ref());
4279        Self(ast::Expr::call_extension_fn(
4280            datetime_extension_name(),
4281            vec![src_expr],
4282        ))
4283    }
4284
4285    /// Create an expression representing a duration of time.
4286    /// This function does not perform error checking on the source string,
4287    /// it creates an expression that calls the `datetime` constructor.
4288    pub fn new_duration(src: impl AsRef<str>) -> Self {
4289        let src_expr = ast::Expr::val(src.as_ref());
4290        Self(ast::Expr::call_extension_fn(
4291            duration_extension_name(),
4292            vec![src_expr],
4293        ))
4294    }
4295}
4296
4297#[cfg(test)]
4298impl Expression {
4299    /// Deconstruct an [`Expression`] to get the internal type.
4300    /// This function is only intended to be used internally.
4301    pub(crate) fn into_inner(self) -> ast::Expr {
4302        self.0
4303    }
4304}
4305
4306impl FromStr for Expression {
4307    type Err = ParseErrors;
4308
4309    /// create an Expression using Cedar syntax
4310    fn from_str(expression: &str) -> Result<Self, Self::Err> {
4311        ast::Expr::from_str(expression)
4312            .map(Expression)
4313            .map_err(Into::into)
4314    }
4315}
4316
4317impl std::fmt::Display for Expression {
4318    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
4319        write!(f, "{}", &self.0)
4320    }
4321}
4322
4323/// "Restricted" expressions are used for attribute values and `context`.
4324///
4325/// Restricted expressions can contain only the following:
4326///   - bool, int, and string literals
4327///   - literal `EntityUid`s such as `User::"alice"`
4328///   - extension function calls, where the arguments must be other things
4329///     on this list
4330///   - set and record literals, where the values must be other things on
4331///     this list
4332///
4333/// That means the following are not allowed in restricted expressions:
4334///   - `principal`, `action`, `resource`, `context`
4335///   - builtin operators and functions, including `.`, `in`, `has`, `like`,
4336///     `.contains()`
4337///   - if-then-else expressions
4338#[repr(transparent)]
4339#[derive(Debug, Clone, RefCast, PartialEq, Eq)]
4340pub struct RestrictedExpression(pub(crate) ast::RestrictedExpr);
4341
4342#[doc(hidden)] // because this converts to a private/internal type
4343impl AsRef<ast::RestrictedExpr> for RestrictedExpression {
4344    fn as_ref(&self) -> &ast::RestrictedExpr {
4345        &self.0
4346    }
4347}
4348
4349#[doc(hidden)]
4350impl From<ast::RestrictedExpr> for RestrictedExpression {
4351    fn from(expr: ast::RestrictedExpr) -> Self {
4352        Self(expr)
4353    }
4354}
4355
4356impl RestrictedExpression {
4357    /// Create an expression representing a literal string.
4358    pub fn new_string(value: String) -> Self {
4359        Self(ast::RestrictedExpr::val(value))
4360    }
4361
4362    /// Create an expression representing a literal bool.
4363    pub fn new_bool(value: bool) -> Self {
4364        Self(ast::RestrictedExpr::val(value))
4365    }
4366
4367    /// Create an expression representing a literal long.
4368    pub fn new_long(value: ast::Integer) -> Self {
4369        Self(ast::RestrictedExpr::val(value))
4370    }
4371
4372    /// Create an expression representing a literal `EntityUid`.
4373    pub fn new_entity_uid(value: EntityUid) -> Self {
4374        Self(ast::RestrictedExpr::val(ast::EntityUID::from(value)))
4375    }
4376
4377    /// Create an expression representing a record.
4378    ///
4379    /// Error if any key appears two or more times in `fields`.
4380    pub fn new_record(
4381        fields: impl IntoIterator<Item = (String, Self)>,
4382    ) -> Result<Self, ExpressionConstructionError> {
4383        Ok(Self(ast::RestrictedExpr::record(
4384            fields.into_iter().map(|(k, v)| (SmolStr::from(k), v.0)),
4385        )?))
4386    }
4387
4388    /// Create an expression representing a Set.
4389    pub fn new_set(values: impl IntoIterator<Item = Self>) -> Self {
4390        Self(ast::RestrictedExpr::set(values.into_iter().map(|v| v.0)))
4391    }
4392
4393    /// Create an expression representing an ip address.
4394    /// This function does not perform error checking on the source string,
4395    /// it creates an expression that calls the `ip` constructor.
4396    pub fn new_ip(src: impl AsRef<str>) -> Self {
4397        let src_expr = ast::RestrictedExpr::val(src.as_ref());
4398        Self(ast::RestrictedExpr::call_extension_fn(
4399            ip_extension_name(),
4400            [src_expr],
4401        ))
4402    }
4403
4404    /// Create an expression representing a fixed precision decimal number.
4405    /// This function does not perform error checking on the source string,
4406    /// it creates an expression that calls the `decimal` constructor.
4407    pub fn new_decimal(src: impl AsRef<str>) -> Self {
4408        let src_expr = ast::RestrictedExpr::val(src.as_ref());
4409        Self(ast::RestrictedExpr::call_extension_fn(
4410            decimal_extension_name(),
4411            [src_expr],
4412        ))
4413    }
4414
4415    /// Create an expression representing a particular instant of time.
4416    /// This function does not perform error checking on the source string,
4417    /// it creates an expression that calls the `datetime` constructor.
4418    pub fn new_datetime(src: impl AsRef<str>) -> Self {
4419        let src_expr = ast::RestrictedExpr::val(src.as_ref());
4420        Self(ast::RestrictedExpr::call_extension_fn(
4421            datetime_extension_name(),
4422            [src_expr],
4423        ))
4424    }
4425
4426    /// Create an expression representing a duration of time.
4427    /// This function does not perform error checking on the source string,
4428    /// it creates an expression that calls the `datetime` constructor.
4429    pub fn new_duration(src: impl AsRef<str>) -> Self {
4430        let src_expr = ast::RestrictedExpr::val(src.as_ref());
4431        Self(ast::RestrictedExpr::call_extension_fn(
4432            duration_extension_name(),
4433            [src_expr],
4434        ))
4435    }
4436
4437    /// Create an unknown expression
4438    #[cfg(feature = "partial-eval")]
4439    pub fn new_unknown(name: impl AsRef<str>) -> Self {
4440        Self(ast::RestrictedExpr::unknown(ast::Unknown::new_untyped(
4441            name.as_ref(),
4442        )))
4443    }
4444}
4445
4446#[cfg(test)]
4447impl RestrictedExpression {
4448    /// Deconstruct an [`RestrictedExpression`] to get the internal type.
4449    /// This function is only intended to be used internally.
4450    pub(crate) fn into_inner(self) -> ast::RestrictedExpr {
4451        self.0
4452    }
4453}
4454
4455fn decimal_extension_name() -> ast::Name {
4456    #[expect(
4457        clippy::unwrap_used,
4458        reason = "This is a constant and is known to be safe, verified by a test"
4459    )]
4460    ast::Name::unqualified_name("decimal".parse().unwrap())
4461}
4462
4463fn ip_extension_name() -> ast::Name {
4464    #[expect(
4465        clippy::unwrap_used,
4466        reason = "This is a constant and is known to be safe, verified by a test"
4467    )]
4468    ast::Name::unqualified_name("ip".parse().unwrap())
4469}
4470
4471fn datetime_extension_name() -> ast::Name {
4472    #[expect(
4473        clippy::unwrap_used,
4474        reason = "This is a constant and is known to be safe, verified by a test"
4475    )]
4476    ast::Name::unqualified_name("datetime".parse().unwrap())
4477}
4478
4479fn duration_extension_name() -> ast::Name {
4480    #[expect(
4481        clippy::unwrap_used,
4482        reason = "This is a constant and is known to be safe, verified by a test"
4483    )]
4484    ast::Name::unqualified_name("duration".parse().unwrap())
4485}
4486
4487impl FromStr for RestrictedExpression {
4488    type Err = RestrictedExpressionParseError;
4489
4490    /// create a `RestrictedExpression` using Cedar syntax
4491    fn from_str(expression: &str) -> Result<Self, Self::Err> {
4492        ast::RestrictedExpr::from_str(expression)
4493            .map(RestrictedExpression)
4494            .map_err(Into::into)
4495    }
4496}
4497
4498/// Builder for a [`Request`]
4499///
4500/// The default for principal, action, resource, and context fields is Unknown
4501/// for partial evaluation.
4502#[doc = include_str!("../experimental_warning.md")]
4503#[cfg(feature = "partial-eval")]
4504#[derive(Debug, Clone)]
4505pub struct RequestBuilder<S> {
4506    principal: ast::EntityUIDEntry,
4507    action: ast::EntityUIDEntry,
4508    resource: ast::EntityUIDEntry,
4509    /// Here, `None` means unknown
4510    context: Option<ast::Context>,
4511    schema: S,
4512}
4513
4514/// A marker type that indicates [`Schema`] is not set for a request
4515#[doc = include_str!("../experimental_warning.md")]
4516#[cfg(feature = "partial-eval")]
4517#[derive(Debug, Clone, Copy)]
4518pub struct UnsetSchema;
4519
4520#[cfg(feature = "partial-eval")]
4521impl Default for RequestBuilder<UnsetSchema> {
4522    fn default() -> Self {
4523        Self {
4524            principal: ast::EntityUIDEntry::unknown(),
4525            action: ast::EntityUIDEntry::unknown(),
4526            resource: ast::EntityUIDEntry::unknown(),
4527            context: None,
4528            schema: UnsetSchema,
4529        }
4530    }
4531}
4532
4533#[cfg(feature = "partial-eval")]
4534impl<S> RequestBuilder<S> {
4535    /// Set the principal.
4536    ///
4537    /// Note that you can create the `EntityUid` using `.parse()` on any
4538    /// string (via the `FromStr` implementation for `EntityUid`).
4539    #[must_use]
4540    pub fn principal(self, principal: EntityUid) -> Self {
4541        Self {
4542            principal: ast::EntityUIDEntry::known(principal.into(), None),
4543            ..self
4544        }
4545    }
4546
4547    /// Set the principal to be unknown, but known to belong to a certain entity type.
4548    ///
4549    /// This information is taken into account when evaluating 'is', '==' and '!=' expressions.
4550    #[must_use]
4551    pub fn unknown_principal_with_type(self, principal_type: EntityTypeName) -> Self {
4552        Self {
4553            principal: ast::EntityUIDEntry::unknown_with_type(principal_type.0, None),
4554            ..self
4555        }
4556    }
4557
4558    /// Set the action.
4559    ///
4560    /// Note that you can create the `EntityUid` using `.parse()` on any
4561    /// string (via the `FromStr` implementation for `EntityUid`).
4562    #[must_use]
4563    pub fn action(self, action: EntityUid) -> Self {
4564        Self {
4565            action: ast::EntityUIDEntry::known(action.into(), None),
4566            ..self
4567        }
4568    }
4569
4570    /// Set the resource.
4571    ///
4572    /// Note that you can create the `EntityUid` using `.parse()` on any
4573    /// string (via the `FromStr` implementation for `EntityUid`).
4574    #[must_use]
4575    pub fn resource(self, resource: EntityUid) -> Self {
4576        Self {
4577            resource: ast::EntityUIDEntry::known(resource.into(), None),
4578            ..self
4579        }
4580    }
4581
4582    /// Set the resource to be unknown, but known to belong to a certain entity type.
4583    ///
4584    /// This information is taken into account when evaluating 'is', '==' and '!=' expressions.
4585    #[must_use]
4586    pub fn unknown_resource_with_type(self, resource_type: EntityTypeName) -> Self {
4587        Self {
4588            resource: ast::EntityUIDEntry::unknown_with_type(resource_type.0, None),
4589            ..self
4590        }
4591    }
4592
4593    /// Set the context.
4594    #[must_use]
4595    pub fn context(self, context: Context) -> Self {
4596        Self {
4597            context: Some(context.0),
4598            ..self
4599        }
4600    }
4601}
4602
4603#[cfg(feature = "partial-eval")]
4604impl RequestBuilder<UnsetSchema> {
4605    /// Set the schema. If present, this will be used for request validation.
4606    #[must_use]
4607    pub fn schema(self, schema: &Schema) -> RequestBuilder<&Schema> {
4608        RequestBuilder {
4609            principal: self.principal,
4610            action: self.action,
4611            resource: self.resource,
4612            context: self.context,
4613            schema,
4614        }
4615    }
4616
4617    /// Create the [`Request`]
4618    pub fn build(self) -> Request {
4619        Request(ast::Request::new_unchecked(
4620            self.principal,
4621            self.action,
4622            self.resource,
4623            self.context,
4624        ))
4625    }
4626}
4627
4628#[cfg(feature = "partial-eval")]
4629impl RequestBuilder<&Schema> {
4630    /// Create the [`Request`]
4631    pub fn build(self) -> Result<Request, RequestValidationError> {
4632        Ok(Request(ast::Request::new_with_unknowns(
4633            self.principal,
4634            self.action,
4635            self.resource,
4636            self.context,
4637            Some(&self.schema.0),
4638            Extensions::all_available(),
4639        )?))
4640    }
4641}
4642
4643/// An authorization request is a tuple `<P, A, R, C>` where
4644/// * P is the principal [`EntityUid`],
4645/// * A is the action [`EntityUid`],
4646/// * R is the resource [`EntityUid`], and
4647/// * C is the request [`Context`] record.
4648///
4649/// It represents an authorization request asking the question, "Can this
4650/// principal take this action on this resource in this context?"
4651#[repr(transparent)]
4652#[derive(Debug, Clone, RefCast)]
4653pub struct Request(pub(crate) ast::Request);
4654
4655#[doc(hidden)] // because this converts to a private/internal type
4656impl AsRef<ast::Request> for Request {
4657    fn as_ref(&self) -> &ast::Request {
4658        &self.0
4659    }
4660}
4661
4662#[doc(hidden)]
4663impl From<ast::Request> for Request {
4664    fn from(req: ast::Request) -> Self {
4665        Self(req)
4666    }
4667}
4668
4669impl PartialEq for Request {
4670    fn eq(&self, other: &Self) -> bool {
4671        self.principal() == other.principal()
4672            && self.action() == other.action()
4673            && self.resource() == other.resource()
4674            && self.context() == other.context()
4675    }
4676}
4677
4678impl Request {
4679    /// Create a [`RequestBuilder`]
4680    #[doc = include_str!("../experimental_warning.md")]
4681    #[cfg(feature = "partial-eval")]
4682    pub fn builder() -> RequestBuilder<UnsetSchema> {
4683        RequestBuilder::default()
4684    }
4685
4686    /// Create a Request.
4687    ///
4688    /// Note that you can create the `EntityUid`s using `.parse()` on any
4689    /// string (via the `FromStr` implementation for `EntityUid`).
4690    /// The principal, action, and resource fields are optional to support
4691    /// the case where these fields do not contribute to authorization
4692    /// decisions (e.g., because they are not used in your policies).
4693    /// If any of the fields are `None`, we will automatically generate
4694    /// a unique entity UID that is not equal to any UID in the store.
4695    ///
4696    /// If `schema` is present, this constructor will validate that the
4697    /// `Request` complies with the given `schema`.
4698    pub fn new(
4699        principal: EntityUid,
4700        action: EntityUid,
4701        resource: EntityUid,
4702        context: Context,
4703        schema: Option<&Schema>,
4704    ) -> Result<Self, RequestValidationError> {
4705        Ok(Self(ast::Request::new(
4706            (principal.into(), None),
4707            (action.into(), None),
4708            (resource.into(), None),
4709            context.0,
4710            schema.map(|schema| &schema.0),
4711            Extensions::all_available(),
4712        )?))
4713    }
4714
4715    /// Get the context component of the request. Returns `None` if the context is
4716    /// "unknown" (i.e., constructed using the partial evaluation APIs).
4717    pub fn context(&self) -> Option<&Context> {
4718        self.0.context().map(Context::ref_cast)
4719    }
4720
4721    /// Get the principal component of the request. Returns `None` if the principal is
4722    /// "unknown" (i.e., constructed using the partial evaluation APIs).
4723    pub fn principal(&self) -> Option<&EntityUid> {
4724        match self.0.principal() {
4725            ast::EntityUIDEntry::Known { euid, .. } => Some(EntityUid::ref_cast(euid.as_ref())),
4726            ast::EntityUIDEntry::Unknown { .. } => None,
4727        }
4728    }
4729
4730    /// Get the action component of the request. Returns `None` if the action is
4731    /// "unknown" (i.e., constructed using the partial evaluation APIs).
4732    pub fn action(&self) -> Option<&EntityUid> {
4733        match self.0.action() {
4734            ast::EntityUIDEntry::Known { euid, .. } => Some(EntityUid::ref_cast(euid.as_ref())),
4735            ast::EntityUIDEntry::Unknown { .. } => None,
4736        }
4737    }
4738
4739    /// Get the resource component of the request. Returns `None` if the resource is
4740    /// "unknown" (i.e., constructed using the partial evaluation APIs).
4741    pub fn resource(&self) -> Option<&EntityUid> {
4742        match self.0.resource() {
4743            ast::EntityUIDEntry::Known { euid, .. } => Some(EntityUid::ref_cast(euid.as_ref())),
4744            ast::EntityUIDEntry::Unknown { .. } => None,
4745        }
4746    }
4747}
4748
4749/// the Context object for an authorization request
4750#[repr(transparent)]
4751#[derive(Debug, Clone, PartialEq, Eq, RefCast)]
4752pub struct Context(ast::Context);
4753
4754#[doc(hidden)] // because this converts to a private/internal type
4755impl AsRef<ast::Context> for Context {
4756    fn as_ref(&self) -> &ast::Context {
4757        &self.0
4758    }
4759}
4760
4761impl Context {
4762    /// Create an empty `Context`
4763    /// ```
4764    /// # use cedar_policy::Context;
4765    /// let context = Context::empty();
4766    /// # assert_eq!(context.into_iter().next(), None);
4767    /// ```
4768    pub fn empty() -> Self {
4769        Self(ast::Context::empty())
4770    }
4771
4772    /// Create a `Context` from a map of key to "restricted expression",
4773    /// or a Vec of `(key, restricted expression)` pairs, or any other iterator
4774    /// of `(key, restricted expression)` pairs.
4775    /// ```
4776    /// # use cedar_policy::{Context, EntityUid, RestrictedExpression, Request};
4777    /// # use std::str::FromStr;
4778    /// let context = Context::from_pairs([
4779    ///   ("key".to_string(), RestrictedExpression::from_str(r#""value""#).unwrap()),
4780    ///   ("age".to_string(), RestrictedExpression::from_str("18").unwrap()),
4781    /// ]).unwrap();
4782    /// # // create a request
4783    /// # let p = EntityUid::from_str(r#"User::"alice""#).unwrap();
4784    /// # let a = EntityUid::from_str(r#"Action::"view""#).unwrap();
4785    /// # let r = EntityUid::from_str(r#"Album::"trip""#).unwrap();
4786    /// # let request: Request = Request::new(p, a, r, context, None).unwrap();
4787    /// ```
4788    pub fn from_pairs(
4789        pairs: impl IntoIterator<Item = (String, RestrictedExpression)>,
4790    ) -> Result<Self, ContextCreationError> {
4791        Ok(Self(ast::Context::from_pairs(
4792            pairs.into_iter().map(|(k, v)| (SmolStr::from(k), v.0)),
4793            Extensions::all_available(),
4794        )?))
4795    }
4796
4797    /// Retrieves a value from the Context by its key.
4798    ///
4799    /// # Arguments
4800    ///
4801    /// * `key` - The key to look up in the context
4802    ///
4803    /// # Returns
4804    ///
4805    /// * `Some(EvalResult)` - If the key exists in the context, returns its value
4806    /// * `None` - If the key doesn't exist or if the context is not a Value type
4807    ///
4808    /// # Examples
4809    ///
4810    /// ```
4811    /// # use cedar_policy::{Context, Request, EntityUid};
4812    /// # use std::str::FromStr;
4813    /// let context = Context::from_json_str(r#"{"rayId": "abc123"}"#, None).unwrap();
4814    /// if let Some(value) = context.get("rayId") {
4815    ///     // value here is an EvalResult, convertible from the internal Value type
4816    ///     println!("Found value: {:?}", value);
4817    /// }
4818    /// assert_eq!(context.get("nonexistent"), None);
4819    /// ```
4820    pub fn get(&self, key: &str) -> Option<EvalResult> {
4821        match &self.0 {
4822            ast::Context::Value(map) => map.get(key).map(|v| EvalResult::from(v.clone())),
4823            ast::Context::RestrictedResidual(_) => None,
4824        }
4825    }
4826
4827    /// Create a `Context` from a string containing JSON (which must be a JSON
4828    /// object, not any other JSON type, or you will get an error here).
4829    /// JSON here must use the `__entity` and `__extn` escapes for entity
4830    /// references, extension values, etc.
4831    ///
4832    /// If a `schema` is provided, this will inform the parsing: for instance, it
4833    /// will allow `__entity` and `__extn` escapes to be implicit, and it will error
4834    /// if attributes have the wrong types (e.g., string instead of integer).
4835    /// Since different Actions have different schemas for `Context`, you also
4836    /// must specify the `Action` for schema-based parsing.
4837    /// ```
4838    /// # use cedar_policy::{Context, EntityUid, RestrictedExpression, Request};
4839    /// # use std::str::FromStr;
4840    /// let json_data = r#"{
4841    ///     "sub": "1234",
4842    ///     "groups": {
4843    ///         "1234": {
4844    ///             "group_id": "abcd",
4845    ///             "group_name": "test-group"
4846    ///         }
4847    ///     }
4848    /// }"#;
4849    /// let context = Context::from_json_str(json_data, None).unwrap();
4850    /// # // create a request
4851    /// # let p = EntityUid::from_str(r#"User::"alice""#).unwrap();
4852    /// # let a = EntityUid::from_str(r#"Action::"view""#).unwrap();
4853    /// # let r = EntityUid::from_str(r#"Album::"trip""#).unwrap();
4854    /// # let request: Request = Request::new(p, a, r, context, None).unwrap();
4855    /// ```
4856    pub fn from_json_str(
4857        json: &str,
4858        schema: Option<(&Schema, &EntityUid)>,
4859    ) -> Result<Self, ContextJsonError> {
4860        let schema = schema
4861            .map(|(s, uid)| Self::get_context_schema(s, uid))
4862            .transpose()?;
4863        let context = cedar_policy_core::entities::ContextJsonParser::new(
4864            schema.as_ref(),
4865            Extensions::all_available(),
4866        )
4867        .from_json_str(json)?;
4868        Ok(Self(context))
4869    }
4870
4871    /// Create a `Context` from a `serde_json::Value` (which must be a JSON object,
4872    /// not any other JSON type, or you will get an error here).
4873    /// JSON here must use the `__entity` and `__extn` escapes for entity
4874    /// references, extension values, etc.
4875    ///
4876    /// If a `schema` is provided, this will inform the parsing: for instance, it
4877    /// will allow `__entity` and `__extn` escapes to be implicit, and it will error
4878    /// if attributes have the wrong types (e.g., string instead of integer).
4879    /// Since different Actions have different schemas for `Context`, you also
4880    /// must specify the `Action` for schema-based parsing.
4881    /// ```
4882    /// # use cedar_policy::{Context, EntityUid, EntityId, EntityTypeName, RestrictedExpression, Request, Schema};
4883    /// # use std::str::FromStr;
4884    /// let schema_json = serde_json::json!(
4885    ///     {
4886    ///       "": {
4887    ///         "entityTypes": {
4888    ///           "User": {},
4889    ///           "Album": {},
4890    ///         },
4891    ///         "actions": {
4892    ///           "view": {
4893    ///              "appliesTo": {
4894    ///                "principalTypes": ["User"],
4895    ///                "resourceTypes": ["Album"],
4896    ///                "context": {
4897    ///                  "type": "Record",
4898    ///                  "attributes": {
4899    ///                    "sub": { "type": "Long" }
4900    ///                  }
4901    ///                }
4902    ///              }
4903    ///           }
4904    ///         }
4905    ///       }
4906    ///     });
4907    /// let schema = Schema::from_json_value(schema_json).unwrap();
4908    ///
4909    /// let a_eid = EntityId::from_str("view").unwrap();
4910    /// let a_name: EntityTypeName = EntityTypeName::from_str("Action").unwrap();
4911    /// let action = EntityUid::from_type_name_and_id(a_name, a_eid);
4912    /// let data = serde_json::json!({
4913    ///     "sub": 1234
4914    /// });
4915    /// let context = Context::from_json_value(data, Some((&schema, &action))).unwrap();
4916    /// # let p = EntityUid::from_str(r#"User::"alice""#).unwrap();
4917    /// # let r = EntityUid::from_str(r#"Album::"trip""#).unwrap();
4918    /// # let request: Request = Request::new(p, action, r, context, Some(&schema)).unwrap();
4919    /// ```
4920    pub fn from_json_value(
4921        json: serde_json::Value,
4922        schema: Option<(&Schema, &EntityUid)>,
4923    ) -> Result<Self, ContextJsonError> {
4924        let schema = schema
4925            .map(|(s, uid)| Self::get_context_schema(s, uid))
4926            .transpose()?;
4927        let context = cedar_policy_core::entities::ContextJsonParser::new(
4928            schema.as_ref(),
4929            Extensions::all_available(),
4930        )
4931        .from_json_value(json)?;
4932        Ok(Self(context))
4933    }
4934
4935    /// Create a `Context` from a JSON file.  The JSON file must contain a JSON
4936    /// object, not any other JSON type, or you will get an error here.
4937    /// JSON here must use the `__entity` and `__extn` escapes for entity
4938    /// references, extension values, etc.
4939    ///
4940    /// If a `schema` is provided, this will inform the parsing: for instance, it
4941    /// will allow `__entity` and `__extn` escapes to be implicit, and it will error
4942    /// if attributes have the wrong types (e.g., string instead of integer).
4943    /// Since different Actions have different schemas for `Context`, you also
4944    /// must specify the `Action` for schema-based parsing.
4945    /// ```no_run
4946    /// # use cedar_policy::{Context, RestrictedExpression};
4947    /// # use cedar_policy::{Entities, EntityId, EntityTypeName, EntityUid, Request,PolicySet};
4948    /// # use std::collections::HashMap;
4949    /// # use std::str::FromStr;
4950    /// # use std::fs::File;
4951    /// let mut json = File::open("json_file.json").unwrap();
4952    /// let context = Context::from_json_file(&json, None).unwrap();
4953    /// # // create a request
4954    /// # let p_eid = EntityId::from_str("alice").unwrap();
4955    /// # let p_name: EntityTypeName = EntityTypeName::from_str("User").unwrap();
4956    /// # let p = EntityUid::from_type_name_and_id(p_name, p_eid);
4957    /// #
4958    /// # let a_eid = EntityId::from_str("view").unwrap();
4959    /// # let a_name: EntityTypeName = EntityTypeName::from_str("Action").unwrap();
4960    /// # let a = EntityUid::from_type_name_and_id(a_name, a_eid);
4961    /// # let r_eid = EntityId::from_str("trip").unwrap();
4962    /// # let r_name: EntityTypeName = EntityTypeName::from_str("Album").unwrap();
4963    /// # let r = EntityUid::from_type_name_and_id(r_name, r_eid);
4964    /// # let request: Request = Request::new(p, a, r, context, None).unwrap();
4965    /// ```
4966    pub fn from_json_file(
4967        json: impl std::io::Read,
4968        schema: Option<(&Schema, &EntityUid)>,
4969    ) -> Result<Self, ContextJsonError> {
4970        let schema = schema
4971            .map(|(s, uid)| Self::get_context_schema(s, uid))
4972            .transpose()?;
4973        let context = cedar_policy_core::entities::ContextJsonParser::new(
4974            schema.as_ref(),
4975            Extensions::all_available(),
4976        )
4977        .from_json_file(json)?;
4978        Ok(Self(context))
4979    }
4980
4981    /// Convert this `Context` into a JSON value
4982    pub fn to_json_value(
4983        &self,
4984    ) -> Result<serde_json::Value, entities_json_errors::JsonSerializationError> {
4985        self.0.to_json_value()
4986    }
4987
4988    /// Internal helper function to convert `(&Schema, &EntityUid)` to `impl ContextSchema`
4989    fn get_context_schema(
4990        schema: &Schema,
4991        action: &EntityUid,
4992    ) -> Result<impl ContextSchema, ContextJsonError> {
4993        cedar_policy_core::validator::context_schema_for_action(&schema.0, action.as_ref())
4994            .ok_or_else(|| ContextJsonError::missing_action(action.clone()))
4995    }
4996
4997    /// Merge this [`Context`] with another context (or iterator over
4998    /// `(String, RestrictedExpression)` pairs), returning an error if the two
4999    /// contain overlapping keys
5000    pub fn merge(
5001        self,
5002        other_context: impl IntoIterator<Item = (String, RestrictedExpression)>,
5003    ) -> Result<Self, ContextCreationError> {
5004        Self::from_pairs(self.into_iter().chain(other_context))
5005    }
5006
5007    /// Validates this context against the provided schema
5008    ///
5009    /// Returns Ok(()) if the context is valid according to the schema, or an error otherwise
5010    ///
5011    /// This validation is already handled by `Request::new`, so there is no need to separately call
5012    /// if you are validating the whole request
5013    pub fn validate(
5014        &self,
5015        schema: &crate::Schema,
5016        action: &EntityUid,
5017    ) -> std::result::Result<(), RequestValidationError> {
5018        // Call the validate_context function from coreschema.rs
5019        Ok(RequestSchema::validate_context(
5020            &schema.0,
5021            &self.0,
5022            action.as_ref(),
5023            Extensions::all_available(),
5024        )?)
5025    }
5026}
5027
5028/// Utilities for implementing `IntoIterator` for `Context`
5029mod context {
5030    use super::{ast, RestrictedExpression};
5031
5032    /// `IntoIter` iterator for `Context`
5033    #[derive(Debug)]
5034    pub struct IntoIter {
5035        pub(super) inner: <ast::Context as IntoIterator>::IntoIter,
5036    }
5037
5038    impl Iterator for IntoIter {
5039        type Item = (String, RestrictedExpression);
5040
5041        fn next(&mut self) -> Option<Self::Item> {
5042            self.inner
5043                .next()
5044                .map(|(k, v)| (k.to_string(), RestrictedExpression(v)))
5045        }
5046    }
5047}
5048
5049impl IntoIterator for Context {
5050    type Item = (String, RestrictedExpression);
5051
5052    type IntoIter = context::IntoIter;
5053
5054    fn into_iter(self) -> Self::IntoIter {
5055        Self::IntoIter {
5056            inner: self.0.into_iter(),
5057        }
5058    }
5059}
5060
5061#[doc(hidden)]
5062impl From<ast::Context> for Context {
5063    fn from(c: ast::Context) -> Self {
5064        Self(c)
5065    }
5066}
5067
5068impl std::fmt::Display for Request {
5069    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5070        write!(f, "{}", self.0)
5071    }
5072}
5073
5074impl std::fmt::Display for Context {
5075    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5076        write!(f, "{}", self.0)
5077    }
5078}
5079
5080/// Result of Evaluation
5081#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
5082pub enum EvalResult {
5083    /// Boolean value
5084    Bool(bool),
5085    /// Signed integer value
5086    Long(ast::Integer),
5087    /// String value
5088    String(String),
5089    /// Entity Uid
5090    EntityUid(EntityUid),
5091    /// A first-class set
5092    Set(Set),
5093    /// A first-class anonymous record
5094    Record(Record),
5095    /// An extension value, currently limited to String results
5096    ExtensionValue(String),
5097    // ExtensionValue(std::sync::Arc<dyn InternalExtensionValue>),
5098}
5099
5100/// Sets of Cedar values
5101#[derive(Debug, Clone, Eq, PartialEq, PartialOrd, Ord)]
5102pub struct Set(BTreeSet<EvalResult>);
5103
5104impl Set {
5105    /// Iterate over the members of the set
5106    pub fn iter(&self) -> impl Iterator<Item = &EvalResult> {
5107        self.0.iter()
5108    }
5109
5110    /// Is a given element in the set
5111    pub fn contains(&self, elem: &EvalResult) -> bool {
5112        self.0.contains(elem)
5113    }
5114
5115    /// Get the number of members of the set
5116    pub fn len(&self) -> usize {
5117        self.0.len()
5118    }
5119
5120    /// Test if the set is empty
5121    pub fn is_empty(&self) -> bool {
5122        self.0.is_empty()
5123    }
5124}
5125
5126/// A record of Cedar values
5127#[derive(Debug, Clone, Eq, PartialEq, PartialOrd, Ord)]
5128pub struct Record(BTreeMap<String, EvalResult>);
5129
5130impl Record {
5131    /// Iterate over the attribute/value pairs in the record
5132    pub fn iter(&self) -> impl Iterator<Item = (&String, &EvalResult)> {
5133        self.0.iter()
5134    }
5135
5136    /// Check if a given attribute is in the record
5137    pub fn contains_attribute(&self, key: impl AsRef<str>) -> bool {
5138        self.0.contains_key(key.as_ref())
5139    }
5140
5141    /// Get a given attribute from the record
5142    pub fn get(&self, key: impl AsRef<str>) -> Option<&EvalResult> {
5143        self.0.get(key.as_ref())
5144    }
5145
5146    /// Get the number of attributes in the record
5147    pub fn len(&self) -> usize {
5148        self.0.len()
5149    }
5150
5151    /// Test if the record is empty
5152    pub fn is_empty(&self) -> bool {
5153        self.0.is_empty()
5154    }
5155}
5156
5157#[doc(hidden)]
5158impl From<ast::Value> for EvalResult {
5159    fn from(v: ast::Value) -> Self {
5160        match v.value {
5161            ast::ValueKind::Lit(ast::Literal::Bool(b)) => Self::Bool(b),
5162            ast::ValueKind::Lit(ast::Literal::Long(i)) => Self::Long(i),
5163            ast::ValueKind::Lit(ast::Literal::String(s)) => Self::String(s.to_string()),
5164            ast::ValueKind::Lit(ast::Literal::EntityUID(e)) => {
5165                Self::EntityUid(ast::EntityUID::clone(&e).into())
5166            }
5167            ast::ValueKind::Set(set) => Self::Set(Set(set
5168                .authoritative
5169                .iter()
5170                .map(|v| v.clone().into())
5171                .collect())),
5172            ast::ValueKind::Record(record) => Self::Record(Record(
5173                record
5174                    .iter()
5175                    .map(|(k, v)| (k.to_string(), v.clone().into()))
5176                    .collect(),
5177            )),
5178            ast::ValueKind::ExtensionValue(ev) => {
5179                Self::ExtensionValue(RestrictedExpr::from(ev.as_ref().clone()).to_string())
5180            }
5181        }
5182    }
5183}
5184
5185#[doc(hidden)]
5186#[expect(
5187    clippy::fallible_impl_from,
5188    reason = "see the panic safety comments below"
5189)]
5190impl From<EvalResult> for Expression {
5191    fn from(res: EvalResult) -> Self {
5192        match res {
5193            EvalResult::Bool(b) => Self::new_bool(b),
5194            EvalResult::Long(l) => Self::new_long(l),
5195            EvalResult::String(s) => Self::new_string(s),
5196            EvalResult::EntityUid(eid) => {
5197                Self::from(ast::Expr::from(ast::Value::from(ast::EntityUID::from(eid))))
5198            }
5199            EvalResult::Set(set) => Self::new_set(set.iter().cloned().map(Self::from)),
5200            EvalResult::Record(r) =>
5201            {
5202                #[expect(
5203                    clippy::unwrap_used,
5204                    reason = "record originates from EvalResult so should not panic when reconstructing as an Expression"
5205                )]
5206                Self::new_record(r.iter().map(|(k, v)| (k.clone(), Self::from(v.clone())))).unwrap()
5207            }
5208            EvalResult::ExtensionValue(s) => {
5209                #[expect(
5210                    clippy::unwrap_used,
5211                    reason = "the string s is constructed using RestrictedExpr::to_string() so should not panic when being parsed back into a RestrictedExpr"
5212                )]
5213                let expr: ast::Expr = ast::RestrictedExpr::from_str(&s).unwrap().into();
5214                Self::from(expr)
5215            }
5216        }
5217    }
5218}
5219
5220impl std::fmt::Display for EvalResult {
5221    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5222        match self {
5223            Self::Bool(b) => write!(f, "{b}"),
5224            Self::Long(l) => write!(f, "{l}"),
5225            Self::String(s) => write!(f, "\"{}\"", s.escape_debug()),
5226            Self::EntityUid(uid) => write!(f, "{uid}"),
5227            Self::Set(s) => {
5228                write!(f, "[")?;
5229                for (i, ev) in s.iter().enumerate() {
5230                    write!(f, "{ev}")?;
5231                    if (i + 1) < s.len() {
5232                        write!(f, ", ")?;
5233                    }
5234                }
5235                write!(f, "]")?;
5236                Ok(())
5237            }
5238            Self::Record(r) => {
5239                write!(f, "{{")?;
5240                for (i, (k, v)) in r.iter().enumerate() {
5241                    write!(f, "\"{}\": {v}", k.escape_debug())?;
5242                    if (i + 1) < r.len() {
5243                        write!(f, ", ")?;
5244                    }
5245                }
5246                write!(f, "}}")?;
5247                Ok(())
5248            }
5249            Self::ExtensionValue(s) => write!(f, "{s}"),
5250        }
5251    }
5252}
5253
5254/// Evaluates an expression.
5255///
5256/// If evaluation results in an error (e.g., attempting to access a non-existent Entity or Record,
5257/// passing the wrong number of arguments to a function etc.), that error is returned as a String
5258pub fn eval_expression(
5259    request: &Request,
5260    entities: &Entities,
5261    expr: &Expression,
5262) -> Result<EvalResult, EvaluationError> {
5263    let all_ext = Extensions::all_available();
5264    let eval = Evaluator::new(request.0.clone(), &entities.0, all_ext);
5265    Ok(EvalResult::from(
5266        // Evaluate under the empty slot map, as an expression should not have slots
5267        eval.interpret(&expr.0, &ast::SlotEnv::new())?,
5268    ))
5269}
5270
5271#[cfg(feature = "tpe")]
5272pub use tpe::*;
5273
5274#[cfg(feature = "tpe")]
5275mod tpe {
5276    use std::collections::{BTreeMap, HashMap, HashSet};
5277    use std::sync::Arc;
5278
5279    use cedar_policy_core::ast::{self, Value};
5280    use cedar_policy_core::authorizer::Decision;
5281    use cedar_policy_core::batched_evaluator::is_authorized_batched;
5282    use cedar_policy_core::batched_evaluator::{
5283        err::BatchedEvalError, EntityLoader as EntityLoaderInternal,
5284    };
5285    use cedar_policy_core::evaluator::{EvaluationError, RestrictedEvaluator};
5286    use cedar_policy_core::extensions::Extensions;
5287    use cedar_policy_core::tpe;
5288    use itertools::Itertools;
5289    use ref_cast::RefCast;
5290    use smol_str::SmolStr;
5291
5292    use crate::{
5293        api, tpe_err, Authorizer, Context, Entities, EntityId, EntityTypeName, EntityUid,
5294        PartialEntityError, PartialRequestCreationError, PermissionQueryError, Policy, PolicySet,
5295        Request, RequestValidationError, RestrictedExpression, Schema,
5296    };
5297    use crate::{Entity, TpeReauthorizationError};
5298
5299    /// A partial [`EntityUid`].
5300    /// That is, its [`EntityId`] could be unknown
5301    #[doc = include_str!("../experimental_warning.md")]
5302    #[repr(transparent)]
5303    #[derive(Debug, Clone, RefCast)]
5304    pub struct PartialEntityUid(pub(crate) tpe::request::PartialEntityUID);
5305
5306    #[doc(hidden)]
5307    impl AsRef<tpe::request::PartialEntityUID> for PartialEntityUid {
5308        fn as_ref(&self) -> &tpe::request::PartialEntityUID {
5309            &self.0
5310        }
5311    }
5312
5313    impl PartialEntityUid {
5314        /// Construct a [`PartialEntityUid`]
5315        pub fn new(ty: EntityTypeName, id: Option<EntityId>) -> Self {
5316            Self(tpe::request::PartialEntityUID {
5317                ty: ty.0,
5318                eid: id.map(|id| <EntityId as AsRef<ast::Eid>>::as_ref(&id).clone()),
5319            })
5320        }
5321
5322        /// Construct a [`PartialEntityUid`] from a concrete [`EntityUid`].
5323        pub fn from_concrete(euid: EntityUid) -> Self {
5324            let (ty, eid) = euid.0.components();
5325            Self(tpe::request::PartialEntityUID { ty, eid: Some(eid) })
5326        }
5327    }
5328
5329    /// A partial [`Request`]
5330    /// Its principal/resource types and action must be known and its context
5331    /// must either be fully known or unknown
5332    #[doc = include_str!("../experimental_warning.md")]
5333    #[repr(transparent)]
5334    #[derive(Debug, Clone, RefCast)]
5335    pub struct PartialRequest(pub(crate) tpe::request::PartialRequest);
5336
5337    #[doc(hidden)]
5338    impl AsRef<tpe::request::PartialRequest> for PartialRequest {
5339        fn as_ref(&self) -> &tpe::request::PartialRequest {
5340            &self.0
5341        }
5342    }
5343
5344    impl PartialRequest {
5345        /// Construct a valid [`PartialRequest`] according to a [`Schema`]
5346        pub fn new(
5347            principal: PartialEntityUid,
5348            action: EntityUid,
5349            resource: PartialEntityUid,
5350            context: Option<Context>,
5351            schema: &Schema,
5352        ) -> Result<Self, PartialRequestCreationError> {
5353            let context = context
5354                .map(|c| match c.0 {
5355                    ast::Context::RestrictedResidual(_) => {
5356                        Err(PartialRequestCreationError::ContextContainsUnknowns)
5357                    }
5358                    ast::Context::Value(m) => Ok(m),
5359                })
5360                .transpose()?;
5361            tpe::request::PartialRequest::new(principal.0, action.0, resource.0, context, &schema.0)
5362                .map(Self)
5363                .map_err(|e| PartialRequestCreationError::Validation(e.into()))
5364        }
5365    }
5366
5367    /// Like [`PartialRequest`] but only `resource` can be unknown
5368    #[doc = include_str!("../experimental_warning.md")]
5369    #[repr(transparent)]
5370    #[derive(Debug, Clone, RefCast)]
5371    pub struct ResourceQueryRequest(pub(crate) PartialRequest);
5372
5373    impl ResourceQueryRequest {
5374        /// Construct a valid [`ResourceQueryRequest`] according to a [`Schema`]
5375        pub fn new(
5376            principal: EntityUid,
5377            action: EntityUid,
5378            resource: EntityTypeName,
5379            context: Context,
5380            schema: &Schema,
5381        ) -> Result<Self, PartialRequestCreationError> {
5382            PartialRequest::new(
5383                PartialEntityUid(principal.0.into()),
5384                action,
5385                PartialEntityUid::new(resource, None),
5386                Some(context),
5387                schema,
5388            )
5389            .map(Self)
5390        }
5391
5392        /// Convert [`ResourceQueryRequest`] to a [`Request`] by providing the resource [`EntityId`]
5393        pub fn to_request(
5394            &self,
5395            resource_id: EntityId,
5396            schema: Option<&Schema>,
5397        ) -> Result<Request, RequestValidationError> {
5398            #[expect(
5399                clippy::unwrap_used,
5400                reason = "various fields are validated through the constructor"
5401            )]
5402            Request::new(
5403                EntityUid(self.0 .0.get_principal().try_into().unwrap()),
5404                EntityUid(self.0 .0.get_action()),
5405                EntityUid::from_type_name_and_id(
5406                    EntityTypeName(self.0 .0.get_resource_type()),
5407                    resource_id,
5408                ),
5409                Context::from_pairs(
5410                    self.0
5411                         .0
5412                        .get_context_attrs()
5413                        .unwrap()
5414                        .iter()
5415                        .map(|(a, v)| (a.to_string(), RestrictedExpression(v.clone().into()))),
5416                )
5417                .unwrap(),
5418                schema,
5419            )
5420        }
5421    }
5422
5423    /// Like [`PartialRequest`] but only `principal` can be unknown
5424    #[doc = include_str!("../experimental_warning.md")]
5425    #[repr(transparent)]
5426    #[derive(Debug, Clone, RefCast)]
5427    pub struct PrincipalQueryRequest(pub(crate) PartialRequest);
5428
5429    impl PrincipalQueryRequest {
5430        /// Construct a valid [`PrincipalQueryRequest`] according to a [`Schema`]
5431        pub fn new(
5432            principal: EntityTypeName,
5433            action: EntityUid,
5434            resource: EntityUid,
5435            context: Context,
5436            schema: &Schema,
5437        ) -> Result<Self, PartialRequestCreationError> {
5438            PartialRequest::new(
5439                PartialEntityUid::new(principal, None),
5440                action,
5441                PartialEntityUid(resource.0.into()),
5442                Some(context),
5443                schema,
5444            )
5445            .map(Self)
5446        }
5447
5448        /// Convert [`PrincipalQueryRequest`] to a [`Request`] by providing the principal [`EntityId`]
5449        pub fn to_request(
5450            &self,
5451            principal_id: EntityId,
5452            schema: Option<&Schema>,
5453        ) -> Result<Request, RequestValidationError> {
5454            #[expect(
5455                clippy::unwrap_used,
5456                reason = "various fields are validated through the constructor"
5457            )]
5458            Request::new(
5459                EntityUid::from_type_name_and_id(
5460                    EntityTypeName(self.0 .0.get_principal_type()),
5461                    principal_id,
5462                ),
5463                EntityUid(self.0 .0.get_action()),
5464                EntityUid(self.0 .0.get_resource().try_into().unwrap()),
5465                Context::from_pairs(
5466                    self.0
5467                         .0
5468                        .get_context_attrs()
5469                        .unwrap()
5470                        .iter()
5471                        .map(|(a, v)| (a.to_string(), RestrictedExpression(v.clone().into()))),
5472                )
5473                .unwrap(),
5474                schema,
5475            )
5476        }
5477    }
5478
5479    /// Defines a [`PartialRequest`] which additionally leaves the action
5480    /// undefined, enabling queries listing what actions might be authorized.
5481    ///
5482    /// See [`PolicySet::query_action`] for documentation and example usage.
5483    #[doc = include_str!("../experimental_warning.md")]
5484    #[derive(Debug, Clone)]
5485    pub struct ActionQueryRequest {
5486        principal: PartialEntityUid,
5487        resource: PartialEntityUid,
5488        context: Option<Arc<BTreeMap<SmolStr, Value>>>,
5489        schema: Schema,
5490    }
5491
5492    impl ActionQueryRequest {
5493        /// Construct a valid [`ActionQueryRequest`] according to a [`Schema`]
5494        pub fn new(
5495            principal: PartialEntityUid,
5496            resource: PartialEntityUid,
5497            context: Option<Context>,
5498            schema: Schema,
5499        ) -> Result<Self, PartialRequestCreationError> {
5500            let context = context
5501                .map(|c| match c.0 {
5502                    ast::Context::RestrictedResidual(_) => {
5503                        Err(PartialRequestCreationError::ContextContainsUnknowns)
5504                    }
5505                    ast::Context::Value(m) => Ok(m),
5506                })
5507                .transpose()?;
5508            Ok(Self {
5509                principal,
5510                resource,
5511                context,
5512                schema,
5513            })
5514        }
5515
5516        fn partial_request(
5517            &self,
5518            action: EntityUid,
5519        ) -> Result<PartialRequest, cedar_policy_core::validator::RequestValidationError> {
5520            tpe::request::PartialRequest::new(
5521                self.principal.0.clone(),
5522                action.0,
5523                self.resource.0.clone(),
5524                self.context.clone(),
5525                &self.schema.0,
5526            )
5527            .map(PartialRequest)
5528        }
5529    }
5530
5531    /// Partial [`Entity`]
5532    #[doc = include_str!("../experimental_warning.md")]
5533    #[repr(transparent)]
5534    #[derive(Debug, Clone, RefCast)]
5535    pub struct PartialEntity(pub(crate) tpe::entities::PartialEntity);
5536
5537    impl PartialEntity {
5538        /// Construct a [`PartialEntity`]
5539        pub fn new(
5540            uid: EntityUid,
5541            attrs: Option<BTreeMap<SmolStr, RestrictedExpression>>,
5542            ancestors: Option<HashSet<EntityUid>>,
5543            tags: Option<BTreeMap<SmolStr, RestrictedExpression>>,
5544            schema: &Schema,
5545        ) -> Result<Self, PartialEntityError> {
5546            Ok(Self(tpe::entities::PartialEntity::new(
5547                uid.0,
5548                attrs
5549                    .map(|ps| {
5550                        ps.into_iter()
5551                            .map(|(k, v)| {
5552                                Ok((
5553                                    k,
5554                                    RestrictedEvaluator::new(Extensions::all_available())
5555                                        .interpret(v.0.as_borrowed())?,
5556                                ))
5557                            })
5558                            .collect::<Result<BTreeMap<_, _>, EvaluationError>>()
5559                    })
5560                    .transpose()?,
5561                ancestors.map(|s| s.into_iter().map(|e| e.0).collect()),
5562                tags.map(|ps| {
5563                    ps.into_iter()
5564                        .map(|(k, v)| {
5565                            Ok((
5566                                k,
5567                                RestrictedEvaluator::new(Extensions::all_available())
5568                                    .interpret(v.0.as_borrowed())?,
5569                            ))
5570                        })
5571                        .collect::<Result<BTreeMap<_, _>, EvaluationError>>()
5572                })
5573                .transpose()?,
5574                &schema.0,
5575            )?))
5576        }
5577    }
5578
5579    /// Partial [`Entities`]
5580    #[doc = include_str!("../experimental_warning.md")]
5581    #[repr(transparent)]
5582    #[derive(Debug, Clone, RefCast)]
5583    pub struct PartialEntities(pub(crate) tpe::entities::PartialEntities);
5584
5585    #[doc(hidden)]
5586    impl AsRef<tpe::entities::PartialEntities> for PartialEntities {
5587        fn as_ref(&self) -> &tpe::entities::PartialEntities {
5588            &self.0
5589        }
5590    }
5591
5592    impl PartialEntities {
5593        /// Construct [`PartialEntities`] from a JSON value
5594        /// The `parent`, `attrs`, `tags` field must be either fully known or
5595        /// unknown. And parent entities cannot have unknown parents.
5596        pub fn from_json_value(
5597            value: serde_json::Value,
5598            schema: &Schema,
5599        ) -> Result<Self, tpe_err::EntitiesError> {
5600            tpe::entities::PartialEntities::from_json_value(value, &schema.0).map(Self)
5601        }
5602
5603        /// Construct [`PartialEntities`] given a fully concrete [`Entities`]
5604        pub fn from_concrete(
5605            entities: Entities,
5606            schema: &Schema,
5607        ) -> Result<Self, tpe_err::EntitiesError> {
5608            tpe::entities::PartialEntities::from_concrete(entities.0, &schema.0).map(Self)
5609        }
5610
5611        /// Create a `PartialEntities` with no entities
5612        pub fn empty() -> Self {
5613            Self(tpe::entities::PartialEntities::new())
5614        }
5615
5616        /// Construct [`PartialEntities`] from an iterator of [`PartialEntity`]
5617        pub fn from_partial_entities(
5618            entities: impl IntoIterator<Item = PartialEntity>,
5619            schema: &Schema,
5620        ) -> Result<Self, tpe_err::EntitiesError> {
5621            Ok(Self(tpe::entities::PartialEntities::from_entities(
5622                entities.into_iter().map(|entity| entity.0),
5623                &schema.0,
5624            )?))
5625        }
5626    }
5627
5628    /// A partial version of [`crate::Response`].
5629    #[doc = include_str!("../experimental_warning.md")]
5630    #[repr(transparent)]
5631    #[derive(Debug, Clone, RefCast)]
5632    pub struct TpeResponse<'a>(pub(crate) tpe::response::Response<'a>);
5633
5634    #[doc(hidden)]
5635    impl<'a> AsRef<tpe::response::Response<'a>> for TpeResponse<'a> {
5636        fn as_ref(&self) -> &tpe::response::Response<'a> {
5637            &self.0
5638        }
5639    }
5640
5641    impl TpeResponse<'_> {
5642        /// Attempt to get the authorization decision
5643        pub fn decision(&self) -> Option<Decision> {
5644            self.0.decision()
5645        }
5646
5647        /// Perform reauthorization
5648        pub fn reauthorize(
5649            &self,
5650            request: &Request,
5651            entities: &Entities,
5652        ) -> Result<api::Response, TpeReauthorizationError> {
5653            self.0
5654                .reauthorize(&request.0, &entities.0)
5655                .map(Into::into)
5656                .map_err(Into::into)
5657        }
5658
5659        /// Return residuals as [`Policy`]s
5660        /// A [`Policy`] returned inherits [`crate::PolicyId`] and annotations from
5661        /// the corresponding input policy
5662        /// Its scope is unconstrained and its condition is in the form of a
5663        /// single `when` clause with the residual as the expression
5664        /// Use [`TpeResponse::nontrivial_residual_policies`] to get non-trivial residual policies
5665        pub fn residual_policies(&self) -> impl Iterator<Item = Policy> + '_ {
5666            self.0
5667                .residual_policies()
5668                .map(|p| Policy::from_ast(p.clone().into()))
5669        }
5670
5671        /// Returns an iterator of non-trivial (meaning more than just `true`
5672        /// or `false`) residuals as [`Policy`]s
5673        /// A [`Policy`] returned inherits [`crate::PolicyId`] and annotations from
5674        /// the corresponding input policy
5675        /// Its scope is unconstrained and its condition is in the form of a
5676        /// single `when` clause with the residual as the expression
5677        pub fn nontrivial_residual_policies(&'_ self) -> impl Iterator<Item = Policy> + '_ {
5678            self.0
5679                .residual_permits()
5680                .chain(self.0.residual_forbids())
5681                .map(|p| Policy::from_ast(p.clone().into()))
5682        }
5683    }
5684
5685    /// Entity loader trait for batched evaluation.
5686    ///
5687    /// Loads entities on demand, returning `None` for missing entities.
5688    /// The `load_entities` function must load all requested entities,
5689    /// and must compute and include all ancestors of the requested entities.
5690    /// Loading more entities than requested is allowed.
5691    #[doc = include_str!("../experimental_warning.md")]
5692    pub trait EntityLoader {
5693        /// Load all entities for the given set of entity UIDs.
5694        /// Returns a map from [`EntityUid`] to [`Option<Entity>`], where `None` indicates
5695        /// the entity does not exist.
5696        fn load_entities(
5697            &mut self,
5698            uids: &HashSet<EntityUid>,
5699        ) -> HashMap<EntityUid, Option<Entity>>;
5700    }
5701
5702    /// Wrapper struct used to convert an [`EntityLoader`] to an `EntityLoaderInternal`
5703    struct EntityLoaderWrapper<'a>(&'a mut dyn EntityLoader);
5704
5705    impl EntityLoaderInternal for EntityLoaderWrapper<'_> {
5706        fn load_entities(
5707            &mut self,
5708            uids: &HashSet<ast::EntityUID>,
5709        ) -> HashMap<ast::EntityUID, Option<ast::Entity>> {
5710            let ids = uids
5711                .iter()
5712                .map(|id| EntityUid::ref_cast(id).clone())
5713                .collect();
5714            self.0
5715                .load_entities(&ids)
5716                .into_iter()
5717                .map(|(uid, entity)| (uid.0, entity.map(|e| e.0)))
5718                .collect()
5719        }
5720    }
5721
5722    /// Simple entity loader implementation that loads from a pre-existing Entities store
5723    #[doc = include_str!("../experimental_warning.md")]
5724    #[derive(Debug)]
5725
5726    pub struct TestEntityLoader<'a> {
5727        entities: &'a Entities,
5728    }
5729
5730    impl<'a> TestEntityLoader<'a> {
5731        /// Create a new [`TestEntityLoader`] from an existing Entities store
5732        pub fn new(entities: &'a Entities) -> Self {
5733            Self { entities }
5734        }
5735    }
5736
5737    impl EntityLoader for TestEntityLoader<'_> {
5738        fn load_entities(
5739            &mut self,
5740            uids: &HashSet<EntityUid>,
5741        ) -> HashMap<EntityUid, Option<Entity>> {
5742            uids.iter()
5743                .map(|uid| {
5744                    let entity = self.entities.get(uid).cloned();
5745                    (uid.clone(), entity)
5746                })
5747                .collect()
5748        }
5749    }
5750
5751    impl PolicySet {
5752        /// Perform type-aware partial evaluation on this [`PolicySet`]
5753        /// If successful, the result is a [`PolicySet`] containing residual
5754        /// policies ready for re-authorization
5755        #[doc = include_str!("../experimental_warning.md")]
5756        pub fn tpe<'a>(
5757            &self,
5758            request: &'a PartialRequest,
5759            entities: &'a PartialEntities,
5760            schema: &'a Schema,
5761        ) -> Result<TpeResponse<'a>, tpe_err::TpeError> {
5762            use cedar_policy_core::tpe::is_authorized;
5763            let ps = &self.ast;
5764            let res = is_authorized(ps, &request.0, &entities.0, &schema.0)?;
5765            Ok(TpeResponse(res))
5766        }
5767
5768        /// Like [`Authorizer::is_authorized`] but uses an [`EntityLoader`] to load
5769        /// entities on demand.
5770        ///
5771        /// Calls `loader` at most `max_iters` times, returning
5772        /// early if an authorization result is reached.
5773        /// Otherwise, it iterates `max_iters` times and returns
5774        /// a partial result.
5775        ///
5776        #[doc = include_str!("../experimental_warning.md")]
5777        pub fn is_authorized_batched(
5778            &self,
5779            query: &Request,
5780            schema: &Schema,
5781            loader: &mut dyn EntityLoader,
5782            max_iters: u32,
5783        ) -> Result<Decision, BatchedEvalError> {
5784            is_authorized_batched(
5785                &query.0,
5786                &self.ast,
5787                &schema.0,
5788                &mut EntityLoaderWrapper(loader),
5789                max_iters,
5790            )
5791        }
5792
5793        /// Perform a permission query on the resource
5794        #[doc = include_str!("../experimental_warning.md")]
5795        pub fn query_resource(
5796            &self,
5797            request: &ResourceQueryRequest,
5798            entities: &Entities,
5799            schema: &Schema,
5800        ) -> Result<impl Iterator<Item = EntityUid>, PermissionQueryError> {
5801            let partial_entities = PartialEntities::from_concrete(entities.clone(), schema)?;
5802            let residuals = self.tpe(&request.0, &partial_entities, schema)?;
5803            #[expect(
5804                clippy::unwrap_used,
5805                reason = "policy set construction should succeed because there shouldn't be any policy id conflicts"
5806            )]
5807            let policies = &Self::from_policies(
5808                residuals
5809                    .0
5810                    .residual_policies()
5811                    .map(|p| Policy::from_ast(p.clone().into())),
5812            )
5813            .unwrap();
5814            #[expect(
5815                clippy::unwrap_used,
5816                reason = "request construction should succeed because each entity passes validation"
5817            )]
5818            match residuals.decision() {
5819                Some(Decision::Allow) => Ok(entities
5820                    .iter()
5821                    .filter(|entity| {
5822                        entity.0.uid().entity_type() == &request.0 .0.get_resource_type()
5823                    })
5824                    .map(super::Entity::uid)
5825                    .collect_vec()
5826                    .into_iter()),
5827                Some(Decision::Deny) => Ok(vec![].into_iter()),
5828                None => Ok(entities
5829                    .iter()
5830                    .filter(|entity| {
5831                        entity.0.uid().entity_type() == &request.0 .0.get_resource_type()
5832                    })
5833                    .filter(|entity| {
5834                        let authorizer = Authorizer::new();
5835                        authorizer
5836                            .is_authorized(
5837                                &request.to_request(entity.uid().id().clone(), None).unwrap(),
5838                                policies,
5839                                entities,
5840                            )
5841                            .decision
5842                            == Decision::Allow
5843                    })
5844                    .map(super::Entity::uid)
5845                    .collect_vec()
5846                    .into_iter()),
5847            }
5848        }
5849
5850        /// Perform a permission query on the principal
5851        #[doc = include_str!("../experimental_warning.md")]
5852        pub fn query_principal(
5853            &self,
5854            request: &PrincipalQueryRequest,
5855            entities: &Entities,
5856            schema: &Schema,
5857        ) -> Result<impl Iterator<Item = EntityUid>, PermissionQueryError> {
5858            let partial_entities = PartialEntities::from_concrete(entities.clone(), schema)?;
5859            let residuals = self.tpe(&request.0, &partial_entities, schema)?;
5860            #[expect(
5861                clippy::unwrap_used,
5862                reason = "policy set construction should succeed because there shouldn't be any policy id conflicts"
5863            )]
5864            let policies = &Self::from_policies(
5865                residuals
5866                    .0
5867                    .residual_policies()
5868                    .map(|p| Policy::from_ast(p.clone().into())),
5869            )
5870            .unwrap();
5871            #[expect(
5872                clippy::unwrap_used,
5873                reason = "request construction should succeed because each entity passes validation"
5874            )]
5875            match residuals.decision() {
5876                Some(Decision::Allow) => Ok(entities
5877                    .iter()
5878                    .filter(|entity| {
5879                        entity.0.uid().entity_type() == &request.0 .0.get_principal_type()
5880                    })
5881                    .map(super::Entity::uid)
5882                    .collect_vec()
5883                    .into_iter()),
5884                Some(Decision::Deny) => Ok(vec![].into_iter()),
5885                None => Ok(entities
5886                    .iter()
5887                    .filter(|entity| {
5888                        entity.0.uid().entity_type() == &request.0 .0.get_principal_type()
5889                    })
5890                    .filter(|entity| {
5891                        let authorizer = Authorizer::new();
5892                        authorizer
5893                            .is_authorized(
5894                                &request.to_request(entity.uid().id().clone(), None).unwrap(),
5895                                policies,
5896                                entities,
5897                            )
5898                            .decision
5899                            == Decision::Allow
5900                    })
5901                    .map(super::Entity::uid)
5902                    .collect_vec()
5903                    .into_iter()),
5904            }
5905        }
5906
5907        /// Given a [`ActionQueryRequest`] (a partial request without a concrete
5908        /// action) enumerate actions in the schema which might be authorized
5909        /// for that request.
5910        ///
5911        /// Each action is returned with a partial authorization decision.  If
5912        /// the action is definitely authorized, then it is `Some(Decision::Allow)`.
5913        /// If we did not reach a concrete authorization decision, then it is
5914        /// `None`. Actions which are definitely not authorized (i.e., the
5915        /// decision is `Some(Decision::Deny)`) are not returned by this
5916        /// function. It is also possible that some actions without a concrete
5917        /// authorization decision are never authorized if the residual
5918        /// expressions after partial evaluation are not satisfiable.
5919        ///
5920        /// If the partial request for a particular action is invalid (e.g., the
5921        /// action does not apply to the type of principal and resource), then
5922        /// that action is not included in the result regardless of whether a
5923        /// request with that action would be authorized.
5924        ///
5925        /// ```
5926        /// # use cedar_policy::{PolicySet, Schema, ActionQueryRequest, PartialEntities, PartialEntityUid, Decision, EntityUid, Entities};
5927        /// # use std::str::FromStr;
5928        /// # let policies = PolicySet::from_str(r#"
5929        /// #     permit(principal, action == Action::"edit", resource) when { context.should_allow };
5930        /// #     permit(principal, action == Action::"view", resource);
5931        /// # "#).unwrap();
5932        /// # let schema = Schema::from_str("
5933        /// #     entity User, Photo;
5934        /// #     action view, edit appliesTo {
5935        /// #       principal: User,
5936        /// #       resource: Photo,
5937        /// #       context: { should_allow: Bool, }
5938        /// #     };
5939        /// # ").unwrap();
5940        /// # let entities = PartialEntities::empty();
5941        ///
5942        /// // Construct a request for a concrete principal and resource, but leaving the context unknown so
5943        /// // that we can see all actions that might be authorized for some context.
5944        /// let request = ActionQueryRequest::new(
5945        ///     PartialEntityUid::from_concrete(r#"User::"alice""#.parse().unwrap()),
5946        ///     PartialEntityUid::from_concrete(r#"Photo::"vacation.jpg""#.parse().unwrap()),
5947        ///     None,
5948        ///     schema,
5949        /// ).unwrap();
5950        ///
5951        /// // All actions which might be allowed for this principal and resource.
5952        /// // The exact authorization result may depend on currently unknown
5953        /// // context and entity data.
5954        /// let possibly_allowed_actions: Vec<&EntityUid> =
5955        ///     policies.query_action(&request, &entities)
5956        ///             .unwrap()
5957        ///             .map(|(a, _)| a)
5958        ///             .collect();
5959        /// # let mut possibly_allowed_actions = possibly_allowed_actions;
5960        /// # possibly_allowed_actions.sort();
5961        /// # assert_eq!(&possibly_allowed_actions, &[&r#"Action::"edit""#.parse().unwrap(), &r#"Action::"view""#.parse().unwrap()]);
5962        ///
5963        /// // These actions are definitely allowed for this principal and resource.
5964        /// // These will be allowed for _any_ context.
5965        /// let allowed_actions: Vec<&EntityUid> =
5966        ///     policies.query_action(&request, &entities).unwrap()
5967        ///             .filter(|(_, resp)| resp == &Some(Decision::Allow))
5968        ///             .map(|(a, _)| a)
5969        ///             .collect();
5970        /// # assert_eq!(&allowed_actions, &[&r#"Action::"view""#.parse().unwrap()]);
5971        /// ```
5972        #[doc = include_str!("../experimental_warning.md")]
5973        pub fn query_action<'a>(
5974            &self,
5975            request: &'a ActionQueryRequest,
5976            entities: &PartialEntities,
5977        ) -> Result<impl Iterator<Item = (&'a EntityUid, Option<Decision>)>, PermissionQueryError>
5978        {
5979            let mut authorized_actions = Vec::new();
5980            // We only consider actions that apply to the type of the requested
5981            // principal and resource. Any requests for different actions would
5982            // be invalid, so they should never be authorized. Not however that
5983            // an authorization request for _could_ return `Allow` if the caller
5984            // ignores the request validation error.
5985            for action in request
5986                .schema
5987                .0
5988                .actions_for_principal_and_resource(&request.principal.0.ty, &request.resource.0.ty)
5989            {
5990                // If we fail to construct a partial request, then the partial context is not valid for
5991                // the context type declared for this action. This action should never be authorized,
5992                // but with the same caveats about invalid requests.
5993                if let Ok(partial_request) = request.partial_request(action.clone().into()) {
5994                    let decision = self
5995                        .tpe(&partial_request, entities, &request.schema)?
5996                        .decision();
5997                    if decision != Some(Decision::Deny) {
5998                        authorized_actions.push((RefCast::ref_cast(action), decision));
5999                    }
6000                }
6001            }
6002            Ok(authorized_actions.into_iter())
6003        }
6004    }
6005}
6006
6007// These are the same tests in validator, just ensuring all the plumbing is done correctly
6008#[cfg(test)]
6009mod test_access {
6010    use cedar_policy_core::ast;
6011
6012    use super::*;
6013
6014    fn schema() -> Schema {
6015        let src = r#"
6016        type Task = {
6017    "id": Long,
6018    "name": String,
6019    "state": String,
6020};
6021
6022type T = String;
6023
6024type Tasks = Set<Task>;
6025entity List in [Application] = {
6026  "editors": Team,
6027  "name": String,
6028  "owner": User,
6029  "readers": Team,
6030  "tasks": Tasks,
6031};
6032entity Application;
6033entity User in [Team, Application] = {
6034  "joblevel": Long,
6035  "location": String,
6036};
6037
6038entity CoolList;
6039
6040entity Team in [Team, Application];
6041
6042action Read, Write, Create;
6043
6044action DeleteList, EditShare, UpdateList, CreateTask, UpdateTask, DeleteTask in Write appliesTo {
6045    principal: [User],
6046    resource : [List]
6047};
6048
6049action GetList in Read appliesTo {
6050    principal : [User],
6051    resource : [List, CoolList]
6052};
6053
6054action GetLists in Read appliesTo {
6055    principal : [User],
6056    resource : [Application]
6057};
6058
6059action CreateList in Create appliesTo {
6060    principal : [User],
6061    resource : [Application]
6062};
6063
6064        "#;
6065
6066        src.parse().unwrap()
6067    }
6068
6069    #[test]
6070    fn principals() {
6071        let schema = schema();
6072        let principals = schema.principals().collect::<HashSet<_>>();
6073        assert_eq!(principals.len(), 1);
6074        let user: EntityTypeName = "User".parse().unwrap();
6075        assert!(principals.contains(&user));
6076        let principals = schema.principals().collect::<Vec<_>>();
6077        assert!(principals.len() > 1);
6078        assert!(principals.iter().all(|ety| **ety == user));
6079        assert!(principals.iter().all(|ety| ety.0.loc().is_some()));
6080
6081        let et = ast::EntityType::EntityType(ast::Name::from_normalized_str("User").unwrap());
6082        let et = schema.0.get_entity_type(&et).unwrap();
6083        assert!(et.loc.is_some());
6084    }
6085
6086    #[cfg(feature = "extended-schema")]
6087    #[test]
6088    fn common_types_extended() {
6089        use cool_asserts::assert_matches;
6090
6091        use cedar_policy_core::validator::{types::Type, LocatedCommonType};
6092
6093        let schema = schema();
6094        assert_eq!(schema.0.common_types().collect::<HashSet<_>>().len(), 3);
6095        let task_type = LocatedCommonType {
6096            name: "Task".into(),
6097            name_loc: None,
6098            type_loc: None,
6099        };
6100        assert!(schema.0.common_types().contains(&task_type));
6101
6102        let tasks_type = LocatedCommonType {
6103            name: "Tasks".into(),
6104            name_loc: None,
6105            type_loc: None,
6106        };
6107        assert!(schema.0.common_types().contains(&tasks_type));
6108        assert!(schema.0.common_types().all(|ct| ct.name_loc.is_some()));
6109        assert!(schema.0.common_types().all(|ct| ct.type_loc.is_some()));
6110
6111        let tasks_type = LocatedCommonType {
6112            name: "T".into(),
6113            name_loc: None,
6114            type_loc: None,
6115        };
6116        assert!(schema.0.common_types().contains(&tasks_type));
6117
6118        let et = ast::EntityType::EntityType(ast::Name::from_normalized_str("List").unwrap());
6119        let et = schema.0.get_entity_type(&et).unwrap();
6120        let attrs = et.attributes();
6121
6122        // Assert that attributes that are resolved from common types still get source locations
6123        let t = attrs.get_attr("tasks").unwrap();
6124        assert!(t.loc.is_some());
6125        assert_matches!(t.attr_type.as_ref(), cedar_policy_core::validator::types::Type::Set { ref element_type } => {
6126            let el = element_type.as_ref().unwrap();
6127            assert_matches!(el.as_ref(), Type::Record{ attrs, .. } => {
6128                assert!(attrs.get_attr("name").unwrap().loc.is_some());
6129                assert!(attrs.get_attr("id").unwrap().loc.is_some());
6130                assert!(attrs.get_attr("state").unwrap().loc.is_some());
6131            });
6132        });
6133    }
6134
6135    #[cfg(feature = "extended-schema")]
6136    #[test]
6137    fn namespace_extended() {
6138        let schema = schema();
6139        assert_eq!(schema.0.namespaces().collect::<HashSet<_>>().len(), 1);
6140        let default_namespace = schema.0.namespaces().last().unwrap();
6141        assert_eq!(default_namespace.name, SmolStr::from("__cedar"));
6142        assert!(default_namespace.name_loc.is_none());
6143        assert!(default_namespace.def_loc.is_none());
6144    }
6145
6146    #[test]
6147    fn empty_schema_principals_and_resources() {
6148        let empty: Schema = "".parse().unwrap();
6149        assert!(empty.principals().next().is_none());
6150        assert!(empty.resources().next().is_none());
6151    }
6152
6153    #[test]
6154    fn resources() {
6155        let schema = schema();
6156        let resources = schema.resources().cloned().collect::<HashSet<_>>();
6157        let expected: HashSet<EntityTypeName> = HashSet::from([
6158            "List".parse().unwrap(),
6159            "Application".parse().unwrap(),
6160            "CoolList".parse().unwrap(),
6161        ]);
6162        assert_eq!(resources, expected);
6163        assert!(resources.iter().all(|ety| ety.0.loc().is_some()));
6164    }
6165
6166    #[test]
6167    fn principals_for_action() {
6168        let schema = schema();
6169        let delete_list: EntityUid = r#"Action::"DeleteList""#.parse().unwrap();
6170        let delete_user: EntityUid = r#"Action::"DeleteUser""#.parse().unwrap();
6171        let got = schema
6172            .principals_for_action(&delete_list)
6173            .unwrap()
6174            .cloned()
6175            .collect::<Vec<_>>();
6176        assert_eq!(got, vec!["User".parse().unwrap()]);
6177        assert!(got.iter().all(|ety| ety.0.loc().is_some()));
6178        assert!(schema.principals_for_action(&delete_user).is_none());
6179    }
6180
6181    #[test]
6182    fn resources_for_action() {
6183        let schema = schema();
6184        let delete_list: EntityUid = r#"Action::"DeleteList""#.parse().unwrap();
6185        let delete_user: EntityUid = r#"Action::"DeleteUser""#.parse().unwrap();
6186        let create_list: EntityUid = r#"Action::"CreateList""#.parse().unwrap();
6187        let get_list: EntityUid = r#"Action::"GetList""#.parse().unwrap();
6188        let got = schema
6189            .resources_for_action(&delete_list)
6190            .unwrap()
6191            .cloned()
6192            .collect::<Vec<_>>();
6193        assert_eq!(got, vec!["List".parse().unwrap()]);
6194        assert!(got.iter().all(|ety| ety.0.loc().is_some()));
6195        let got = schema
6196            .resources_for_action(&create_list)
6197            .unwrap()
6198            .cloned()
6199            .collect::<Vec<_>>();
6200        assert_eq!(got, vec!["Application".parse().unwrap()]);
6201        assert!(got.iter().all(|ety| ety.0.loc().is_some()));
6202        let got = schema
6203            .resources_for_action(&get_list)
6204            .unwrap()
6205            .cloned()
6206            .collect::<HashSet<_>>();
6207        assert_eq!(
6208            got,
6209            HashSet::from(["List".parse().unwrap(), "CoolList".parse().unwrap()])
6210        );
6211        assert!(got.iter().all(|ety| ety.0.loc().is_some()));
6212        assert!(schema.principals_for_action(&delete_user).is_none());
6213    }
6214
6215    #[test]
6216    fn principal_parents() {
6217        let schema = schema();
6218        let user: EntityTypeName = "User".parse().unwrap();
6219        let parents = schema
6220            .ancestors(&user)
6221            .unwrap()
6222            .cloned()
6223            .collect::<HashSet<_>>();
6224        assert!(parents.iter().all(|ety| ety.0.loc().is_some()));
6225        let expected = HashSet::from(["Team".parse().unwrap(), "Application".parse().unwrap()]);
6226        assert_eq!(parents, expected);
6227        let parents = schema
6228            .ancestors(&"List".parse().unwrap())
6229            .unwrap()
6230            .cloned()
6231            .collect::<HashSet<_>>();
6232        assert!(parents.iter().all(|ety| ety.0.loc().is_some()));
6233        let expected = HashSet::from(["Application".parse().unwrap()]);
6234        assert_eq!(parents, expected);
6235        assert!(schema.ancestors(&"Foo".parse().unwrap()).is_none());
6236        let parents = schema
6237            .ancestors(&"CoolList".parse().unwrap())
6238            .unwrap()
6239            .cloned()
6240            .collect::<HashSet<_>>();
6241        assert!(parents.iter().all(|ety| ety.0.loc().is_some()));
6242        let expected = HashSet::from([]);
6243        assert_eq!(parents, expected);
6244    }
6245
6246    #[test]
6247    fn action_groups() {
6248        let schema = schema();
6249        let groups = schema.action_groups().cloned().collect::<HashSet<_>>();
6250        let expected = ["Read", "Write", "Create"]
6251            .into_iter()
6252            .map(|ty| format!("Action::\"{ty}\"").parse().unwrap())
6253            .collect::<HashSet<EntityUid>>();
6254        #[cfg(feature = "extended-schema")]
6255        assert!(groups.iter().all(|ety| ety.0.loc().is_some()));
6256        assert_eq!(groups, expected);
6257    }
6258
6259    #[test]
6260    fn actions() {
6261        let schema = schema();
6262        let actions = schema.actions().cloned().collect::<HashSet<_>>();
6263        let expected = [
6264            "Read",
6265            "Write",
6266            "Create",
6267            "DeleteList",
6268            "EditShare",
6269            "UpdateList",
6270            "CreateTask",
6271            "UpdateTask",
6272            "DeleteTask",
6273            "GetList",
6274            "GetLists",
6275            "CreateList",
6276        ]
6277        .into_iter()
6278        .map(|ty| format!("Action::\"{ty}\"").parse().unwrap())
6279        .collect::<HashSet<EntityUid>>();
6280        assert_eq!(actions, expected);
6281        #[cfg(feature = "extended-schema")]
6282        assert!(actions.iter().all(|ety| ety.0.loc().is_some()));
6283    }
6284
6285    #[test]
6286    fn actions_for_principal_and_resource() {
6287        let schema = schema();
6288        let pty: EntityTypeName = "User".parse().unwrap();
6289        let rty: EntityTypeName = "Application".parse().unwrap();
6290        let actions = schema
6291            .actions_for_principal_and_resource(&pty, &rty)
6292            .cloned()
6293            .collect::<HashSet<EntityUid>>();
6294        let expected = ["GetLists", "CreateList"]
6295            .into_iter()
6296            .map(|ty| format!("Action::\"{ty}\"").parse().unwrap())
6297            .collect::<HashSet<EntityUid>>();
6298        assert_eq!(actions, expected);
6299    }
6300
6301    #[test]
6302    fn entities() {
6303        let schema = schema();
6304        let entities = schema.entity_types().cloned().collect::<HashSet<_>>();
6305        let expected = ["List", "Application", "User", "CoolList", "Team"]
6306            .into_iter()
6307            .map(|ty| ty.parse().unwrap())
6308            .collect::<HashSet<EntityTypeName>>();
6309        assert_eq!(entities, expected);
6310    }
6311}
6312
6313#[cfg(test)]
6314mod test_access_namespace {
6315    use super::*;
6316
6317    fn schema() -> Schema {
6318        let src = r#"
6319        namespace Foo {
6320        type Task = {
6321    "id": Long,
6322    "name": String,
6323    "state": String,
6324};
6325
6326type Tasks = Set<Task>;
6327entity List in [Application] = {
6328  "editors": Team,
6329  "name": String,
6330  "owner": User,
6331  "readers": Team,
6332  "tasks": Tasks,
6333};
6334entity Application;
6335entity User in [Team, Application] = {
6336  "joblevel": Long,
6337  "location": String,
6338};
6339
6340entity CoolList;
6341
6342entity Team in [Team, Application];
6343
6344action Read, Write, Create;
6345
6346action DeleteList, EditShare, UpdateList, CreateTask, UpdateTask, DeleteTask in Write appliesTo {
6347    principal: [User],
6348    resource : [List]
6349};
6350
6351action GetList in Read appliesTo {
6352    principal : [User],
6353    resource : [List, CoolList]
6354};
6355
6356action GetLists in Read appliesTo {
6357    principal : [User],
6358    resource : [Application]
6359};
6360
6361action CreateList in Create appliesTo {
6362    principal : [User],
6363    resource : [Application]
6364};
6365    }
6366
6367        "#;
6368
6369        src.parse().unwrap()
6370    }
6371
6372    #[test]
6373    fn principals() {
6374        let schema = schema();
6375        let principals = schema.principals().collect::<HashSet<_>>();
6376        assert_eq!(principals.len(), 1);
6377        let user: EntityTypeName = "Foo::User".parse().unwrap();
6378        assert!(principals.contains(&user));
6379        let principals = schema.principals().collect::<Vec<_>>();
6380        assert!(principals.len() > 1);
6381        assert!(principals.iter().all(|ety| **ety == user));
6382        assert!(principals.iter().all(|ety| ety.0.loc().is_some()));
6383    }
6384
6385    #[test]
6386    fn empty_schema_principals_and_resources() {
6387        let empty: Schema = "".parse().unwrap();
6388        assert!(empty.principals().next().is_none());
6389        assert!(empty.resources().next().is_none());
6390    }
6391
6392    #[test]
6393    fn resources() {
6394        let schema = schema();
6395        let resources = schema.resources().cloned().collect::<HashSet<_>>();
6396        let expected: HashSet<EntityTypeName> = HashSet::from([
6397            "Foo::List".parse().unwrap(),
6398            "Foo::Application".parse().unwrap(),
6399            "Foo::CoolList".parse().unwrap(),
6400        ]);
6401        assert_eq!(resources, expected);
6402        assert!(resources.iter().all(|ety| ety.0.loc().is_some()));
6403    }
6404
6405    #[test]
6406    fn principals_for_action() {
6407        let schema = schema();
6408        let delete_list: EntityUid = r#"Foo::Action::"DeleteList""#.parse().unwrap();
6409        let delete_user: EntityUid = r#"Foo::Action::"DeleteUser""#.parse().unwrap();
6410        let got = schema
6411            .principals_for_action(&delete_list)
6412            .unwrap()
6413            .cloned()
6414            .collect::<Vec<_>>();
6415        assert_eq!(got, vec!["Foo::User".parse().unwrap()]);
6416        assert!(schema.principals_for_action(&delete_user).is_none());
6417    }
6418
6419    #[test]
6420    fn resources_for_action() {
6421        let schema = schema();
6422        let delete_list: EntityUid = r#"Foo::Action::"DeleteList""#.parse().unwrap();
6423        let delete_user: EntityUid = r#"Foo::Action::"DeleteUser""#.parse().unwrap();
6424        let create_list: EntityUid = r#"Foo::Action::"CreateList""#.parse().unwrap();
6425        let get_list: EntityUid = r#"Foo::Action::"GetList""#.parse().unwrap();
6426        let got = schema
6427            .resources_for_action(&delete_list)
6428            .unwrap()
6429            .cloned()
6430            .collect::<Vec<_>>();
6431        assert!(got.iter().all(|ety| ety.0.loc().is_some()));
6432
6433        assert_eq!(got, vec!["Foo::List".parse().unwrap()]);
6434        let got = schema
6435            .resources_for_action(&create_list)
6436            .unwrap()
6437            .cloned()
6438            .collect::<Vec<_>>();
6439        assert_eq!(got, vec!["Foo::Application".parse().unwrap()]);
6440        assert!(got.iter().all(|ety| ety.0.loc().is_some()));
6441
6442        let got = schema
6443            .resources_for_action(&get_list)
6444            .unwrap()
6445            .cloned()
6446            .collect::<HashSet<_>>();
6447        assert_eq!(
6448            got,
6449            HashSet::from([
6450                "Foo::List".parse().unwrap(),
6451                "Foo::CoolList".parse().unwrap()
6452            ])
6453        );
6454        assert!(schema.principals_for_action(&delete_user).is_none());
6455    }
6456
6457    #[test]
6458    fn principal_parents() {
6459        let schema = schema();
6460        let user: EntityTypeName = "Foo::User".parse().unwrap();
6461        let parents = schema
6462            .ancestors(&user)
6463            .unwrap()
6464            .cloned()
6465            .collect::<HashSet<_>>();
6466        let expected = HashSet::from([
6467            "Foo::Team".parse().unwrap(),
6468            "Foo::Application".parse().unwrap(),
6469        ]);
6470        assert_eq!(parents, expected);
6471        let parents = schema
6472            .ancestors(&"Foo::List".parse().unwrap())
6473            .unwrap()
6474            .cloned()
6475            .collect::<HashSet<_>>();
6476        let expected = HashSet::from(["Foo::Application".parse().unwrap()]);
6477        assert_eq!(parents, expected);
6478        assert!(schema.ancestors(&"Foo::Foo".parse().unwrap()).is_none());
6479        let parents = schema
6480            .ancestors(&"Foo::CoolList".parse().unwrap())
6481            .unwrap()
6482            .cloned()
6483            .collect::<HashSet<_>>();
6484        let expected = HashSet::from([]);
6485        assert_eq!(parents, expected);
6486    }
6487
6488    #[test]
6489    fn action_groups() {
6490        let schema = schema();
6491        let groups = schema.action_groups().cloned().collect::<HashSet<_>>();
6492        let expected = ["Read", "Write", "Create"]
6493            .into_iter()
6494            .map(|ty| format!("Foo::Action::\"{ty}\"").parse().unwrap())
6495            .collect::<HashSet<EntityUid>>();
6496        assert_eq!(groups, expected);
6497    }
6498
6499    #[test]
6500    fn actions() {
6501        let schema = schema();
6502        let actions = schema.actions().cloned().collect::<HashSet<_>>();
6503        let expected = [
6504            "Read",
6505            "Write",
6506            "Create",
6507            "DeleteList",
6508            "EditShare",
6509            "UpdateList",
6510            "CreateTask",
6511            "UpdateTask",
6512            "DeleteTask",
6513            "GetList",
6514            "GetLists",
6515            "CreateList",
6516        ]
6517        .into_iter()
6518        .map(|ty| format!("Foo::Action::\"{ty}\"").parse().unwrap())
6519        .collect::<HashSet<EntityUid>>();
6520        assert_eq!(actions, expected);
6521    }
6522
6523    #[test]
6524    fn entities() {
6525        let schema = schema();
6526        let entities = schema.entity_types().cloned().collect::<HashSet<_>>();
6527        let expected = [
6528            "Foo::List",
6529            "Foo::Application",
6530            "Foo::User",
6531            "Foo::CoolList",
6532            "Foo::Team",
6533        ]
6534        .into_iter()
6535        .map(|ty| ty.parse().unwrap())
6536        .collect::<HashSet<EntityTypeName>>();
6537        assert_eq!(entities, expected);
6538    }
6539
6540    #[test]
6541    fn test_request_context() {
6542        // Create a context with some test data
6543        let context =
6544            Context::from_json_str(r#"{"testKey": "testValue", "numKey": 42}"#, None).unwrap();
6545
6546        // Create entity UIDs for the request
6547        let principal: EntityUid = "User::\"alice\"".parse().unwrap();
6548        let action: EntityUid = "Action::\"view\"".parse().unwrap();
6549        let resource: EntityUid = "Resource::\"doc123\"".parse().unwrap();
6550
6551        // Create the request
6552        let request = Request::new(
6553            principal, action, resource, context, None, // no schema validation for this test
6554        )
6555        .unwrap();
6556
6557        // Test context() method
6558        let retrieved_context = request.context().expect("Context should be present");
6559
6560        // Test get() method on the retrieved context
6561        assert!(retrieved_context.get("testKey").is_some());
6562        assert!(retrieved_context.get("numKey").is_some());
6563        assert!(retrieved_context.get("nonexistent").is_none());
6564    }
6565
6566    #[cfg(feature = "extended-schema")]
6567    #[test]
6568    fn namespace_extended() {
6569        let schema = schema();
6570        assert_eq!(schema.0.namespaces().collect::<HashSet<_>>().len(), 2);
6571        let default_namespace = schema
6572            .0
6573            .namespaces()
6574            .filter(|n| n.name == *"__cedar")
6575            .last()
6576            .unwrap();
6577        assert!(default_namespace.name_loc.is_none());
6578        assert!(default_namespace.def_loc.is_none());
6579
6580        let default_namespace = schema
6581            .0
6582            .namespaces()
6583            .filter(|n| n.name == *"Foo")
6584            .last()
6585            .unwrap();
6586        assert!(default_namespace.name_loc.is_some());
6587        assert!(default_namespace.def_loc.is_some());
6588    }
6589}
6590
6591#[cfg(test)]
6592mod test_lossless_empty {
6593    use super::{LosslessPolicy, Policy, PolicyId, Template};
6594
6595    #[test]
6596    fn test_lossless_empty_policy() {
6597        const STATIC_POLICY_TEXT: &str = "permit(principal,action,resource);";
6598        let policy0 = Policy::parse(Some(PolicyId::new("policy0")), STATIC_POLICY_TEXT)
6599            .expect("Failed to parse");
6600        let lossy_policy0 = Policy {
6601            ast: policy0.ast.clone(),
6602            lossless: LosslessPolicy::policy_or_template_text(None::<&str>),
6603        };
6604        // The `to_cedar` representation becomes lossy since we didn't provide text
6605        assert_eq!(
6606            lossy_policy0.to_cedar(),
6607            Some(String::from(
6608                "permit(\n  principal,\n  action,\n  resource\n);"
6609            ))
6610        );
6611        // The EST representation is obtained from the AST
6612        let lossy_policy0_est = lossy_policy0
6613            .lossless
6614            .est(|| policy0.ast.clone().into())
6615            .unwrap();
6616        assert_eq!(lossy_policy0_est, policy0.ast.into());
6617    }
6618
6619    #[test]
6620    fn test_lossless_empty_template() {
6621        const TEMPLATE_TEXT: &str = "permit(principal == ?principal,action,resource);";
6622        let template0 = Template::parse(Some(PolicyId::new("template0")), TEMPLATE_TEXT)
6623            .expect("Failed to parse");
6624        let lossy_template0 = Template {
6625            ast: template0.ast.clone(),
6626            lossless: LosslessPolicy::policy_or_template_text(None::<&str>),
6627        };
6628        // The `to_cedar` representation becomes lossy since we didn't provide text
6629        assert_eq!(
6630            lossy_template0.to_cedar(),
6631            String::from("permit(\n  principal == ?principal,\n  action,\n  resource\n);")
6632        );
6633        // The EST representation is obtained from the AST
6634        let lossy_template0_est = lossy_template0
6635            .lossless
6636            .est(|| template0.ast.clone().into())
6637            .unwrap();
6638        assert_eq!(lossy_template0_est, template0.ast.into());
6639    }
6640}
6641
6642/// Given a schema and policy set, compute an entity manifest.
6643///
6644/// The policies must validate against the schema in strict mode,
6645/// otherwise an error is returned.
6646/// The manifest describes the data required to answer requests
6647/// for each action.
6648#[doc = include_str!("../experimental_warning.md")]
6649#[deprecated = "The `entity-manifest` experimental feature and all associated functions are deprecated. Migrate to `PolicySet::is_authorized_batch` for efficient authorization with on-demand entity loading."]
6650#[cfg(feature = "entity-manifest")]
6651pub fn compute_entity_manifest(
6652    validator: &Validator,
6653    pset: &PolicySet,
6654) -> Result<EntityManifest, EntityManifestError> {
6655    entity_manifest::compute_entity_manifest(&validator.0, &pset.ast).map_err(Into::into)
6656}