cedar_policy/
api.rs

1/*
2 * Copyright 2022-2023 Amazon.com, Inc. or its affiliates. All Rights Reserved.
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)]
23pub use ast::Effect;
24pub use authorizer::Decision;
25use cedar_policy_core::ast;
26use cedar_policy_core::authorizer;
27use cedar_policy_core::entities;
28use cedar_policy_core::entities::JsonDeserializationErrorContext;
29use cedar_policy_core::entities::{ContextSchema, Dereference, JsonDeserializationError};
30use cedar_policy_core::est;
31use cedar_policy_core::evaluator::{Evaluator, RestrictedEvaluator};
32pub use cedar_policy_core::extensions;
33use cedar_policy_core::extensions::Extensions;
34use cedar_policy_core::parser;
35pub use cedar_policy_core::parser::err::ParseErrors;
36use cedar_policy_core::parser::SourceInfo;
37use cedar_policy_core::FromNormalizedStr;
38pub use cedar_policy_validator::{TypeErrorKind, ValidationErrorKind, ValidationWarningKind};
39use itertools::Itertools;
40use ref_cast::RefCast;
41use serde::{Deserialize, Serialize};
42use smol_str::SmolStr;
43use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
44use std::str::FromStr;
45use thiserror::Error;
46
47/// Identifier for a Template slot
48#[repr(transparent)]
49#[derive(Debug, Clone, Eq, PartialEq, PartialOrd, Ord, Hash, RefCast)]
50pub struct SlotId(ast::SlotId);
51
52impl SlotId {
53    /// Get the slot for `principal`
54    pub fn principal() -> Self {
55        Self(ast::SlotId::principal())
56    }
57
58    /// Get the slot for `resource`
59    pub fn resource() -> Self {
60        Self(ast::SlotId::resource())
61    }
62}
63
64impl std::fmt::Display for SlotId {
65    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66        write!(f, "{}", self.0)
67    }
68}
69
70impl From<ast::SlotId> for SlotId {
71    fn from(a: ast::SlotId) -> Self {
72        Self(a)
73    }
74}
75
76impl From<SlotId> for ast::SlotId {
77    fn from(s: SlotId) -> Self {
78        s.0
79    }
80}
81
82/// Entity datatype
83// INVARIANT The `EntityUid` of an `Entity` cannot be unspecified
84#[repr(transparent)]
85#[derive(Debug, Clone, PartialEq, Eq, RefCast)]
86pub struct Entity(ast::Entity);
87
88impl Entity {
89    /// Create a new `Entity` with this Uid, attributes, and parents.
90    ///
91    /// Attribute values are specified here as "restricted expressions".
92    /// See docs on `RestrictedExpression`
93    /// ```
94    /// # use cedar_policy::{Entity, EntityId, EntityTypeName, EntityUid, RestrictedExpression};
95    /// # use std::collections::{HashMap, HashSet};
96    /// # use std::str::FromStr;
97    /// let eid = EntityId::from_str("alice").unwrap();
98    /// let type_name = EntityTypeName::from_str("User").unwrap();
99    /// let euid = EntityUid::from_type_name_and_id(type_name, eid);
100    /// let attrs = HashMap::from([
101    ///     ("age".to_string(), RestrictedExpression::from_str("21").unwrap()),
102    ///     ("department".to_string(), RestrictedExpression::from_str("\"CS\"").unwrap()),
103    /// ]);
104    /// let parent_eid = EntityId::from_str("admin").unwrap();
105    /// let parent_type_name = EntityTypeName::from_str("Group").unwrap();
106    /// let parent_euid = EntityUid::from_type_name_and_id(parent_type_name, parent_eid);
107    /// let parents = HashSet::from([parent_euid]);
108    /// let entity = Entity::new(euid, attrs, parents);
109    ///```
110    pub fn new(
111        uid: EntityUid,
112        attrs: HashMap<String, RestrictedExpression>,
113        parents: HashSet<EntityUid>,
114    ) -> Self {
115        // note that we take a "parents" parameter here; we will compute TC when
116        // the `Entities` object is created
117        // INVARIANT by invariant on `EntityUid`
118        Self(ast::Entity::new(
119            uid.0,
120            attrs
121                .into_iter()
122                .map(|(k, v)| (SmolStr::from(k), v.0))
123                .collect(),
124            parents.into_iter().map(|uid| uid.0).collect(),
125        ))
126    }
127
128    /// Create a new `Entity` with this Uid, no attributes, and no parents.
129    /// ```
130    /// use cedar_policy::{Entity, EntityId, EntityTypeName, EntityUid};
131    /// # use std::str::FromStr;
132    /// let eid = EntityId::from_str("alice").unwrap();
133    /// let type_name = EntityTypeName::from_str("User").unwrap();
134    /// let euid = EntityUid::from_type_name_and_id(type_name, eid);
135    /// let alice = Entity::with_uid(euid);
136    /// # assert_eq!(alice.attr("age"), None);
137    /// ```
138    pub fn with_uid(uid: EntityUid) -> Self {
139        // INVARIANT: by invariant on `EntityUid`
140        Self(ast::Entity::with_uid(uid.0))
141    }
142
143    /// Get the Uid of this entity
144    /// ```
145    /// # use cedar_policy::{Entity, EntityId, EntityTypeName, EntityUid};
146    /// # use std::str::FromStr;
147    /// # let eid = EntityId::from_str("alice").unwrap();
148    /// let type_name = EntityTypeName::from_str("User").unwrap();
149    /// let euid = EntityUid::from_type_name_and_id(type_name, eid);
150    /// let alice = Entity::with_uid(euid.clone());
151    /// assert_eq!(alice.uid(), euid);
152    /// ```
153    pub fn uid(&self) -> EntityUid {
154        // INVARIANT: By invariant on self and `EntityUid`: Our Uid can't be unspecified
155        EntityUid(self.0.uid())
156    }
157
158    /// Get the value for the given attribute, or `None` if not present.
159    ///
160    /// This can also return Some(Err) if the attribute had an illegal value.
161    /// ```
162    /// use cedar_policy::{Entity, EntityId, EntityTypeName, EntityUid, EvalResult,
163    ///     RestrictedExpression};
164    /// use std::collections::{HashMap, HashSet};
165    /// use std::str::FromStr;
166    /// let eid = EntityId::from_str("alice").unwrap();
167    /// let type_name = EntityTypeName::from_str("User").unwrap();
168    /// let euid = EntityUid::from_type_name_and_id(type_name, eid);
169    /// let attrs = HashMap::from([
170    ///     ("age".to_string(), RestrictedExpression::from_str("21").unwrap()),
171    ///     ("department".to_string(), RestrictedExpression::from_str("\"CS\"").unwrap()),
172    /// ]);
173    /// let entity = Entity::new(euid, attrs, HashSet::new());
174    /// assert_eq!(entity.attr("age").unwrap(), Ok(EvalResult::Long(21)));
175    /// assert_eq!(entity.attr("department").unwrap(), Ok(EvalResult::String("CS".to_string())));
176    ///```
177    pub fn attr(&self, attr: &str) -> Option<Result<EvalResult, EvaluationError>> {
178        let expr = self.0.get(attr)?;
179        let all_ext = Extensions::all_available();
180        let evaluator = RestrictedEvaluator::new(&all_ext);
181        Some(
182            evaluator
183                .interpret(expr.as_borrowed())
184                .map(EvalResult::from)
185                .map_err(|e| EvaluationError::StringMessage(e.to_string())),
186        )
187    }
188}
189
190impl std::fmt::Display for Entity {
191    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
192        write!(f, "{}", self.0)
193    }
194}
195
196/// Represents an entity hierarchy, and allows looking up `Entity` objects by
197/// Uid.
198#[repr(transparent)]
199#[derive(Debug, Clone, Default, PartialEq, Eq, RefCast)]
200pub struct Entities(pub(crate) entities::Entities);
201
202pub use entities::EntitiesError;
203
204impl Entities {
205    /// Create a fresh `Entities` with no entities
206    /// ```
207    /// use cedar_policy::Entities;
208    /// let entities = Entities::empty();
209    /// ```
210    pub fn empty() -> Self {
211        Self(entities::Entities::new())
212    }
213
214    /// Get the `Entity` with the given Uid, if any
215    pub fn get(&self, uid: &EntityUid) -> Option<&Entity> {
216        match self.0.entity(&uid.0) {
217            Dereference::Residual(_) | Dereference::NoSuchEntity => None,
218            Dereference::Data(e) => Some(Entity::ref_cast(e)),
219        }
220    }
221
222    /// Transform the store into a partial store, where
223    /// attempting to dereference a non-existent `EntityUID` results in
224    /// a residual instead of an error.
225    #[must_use]
226    #[cfg(feature = "partial-eval")]
227    pub fn partial(self) -> Self {
228        Self(self.0.partial())
229    }
230
231    /// Iterate over the `Entity`'s in the `Entities`
232    pub fn iter(&self) -> impl Iterator<Item = &Entity> {
233        self.0.iter().map(Entity::ref_cast)
234    }
235
236    /// Create an `Entities` object with the given entities.
237    /// It will error if the entities cannot be read or if the entities hierarchy is cyclic
238    pub fn from_entities(
239        entities: impl IntoIterator<Item = Entity>,
240    ) -> Result<Self, entities::EntitiesError> {
241        entities::Entities::from_entities(
242            entities.into_iter().map(|e| e.0),
243            entities::TCComputation::ComputeNow,
244        )
245        .map(Entities)
246    }
247
248    /// Parse an entities JSON file (in `&str` form) into an `Entities` object
249    ///
250    /// If a `schema` is provided, this will inform the parsing: for instance, it
251    /// will allow `__entity` and `__extn` escapes to be implicit, and it will error
252    /// if attributes have the wrong types (e.g., string instead of integer).
253    /// ```
254    /// use std::collections::HashMap;
255    /// use std::str::FromStr;
256    /// # use cedar_policy::{Entities, EntityId, EntityTypeName, EntityUid, EvalResult, Request,PolicySet};
257    /// let data =r#"
258    /// [
259    /// {
260    ///   "uid": {"type":"User","id":"alice"},
261    ///   "attrs": {
262    ///     "age":19,
263    ///     "ip_addr":{"__extn":{"fn":"ip", "arg":"10.0.1.101"}}
264    ///   },
265    ///   "parents": [{"type":"Group","id":"admin"}]
266    /// },
267    /// {
268    ///   "uid": {"type":"Groupd","id":"admin"},
269    ///   "attrs": {},
270    ///   "parents": []
271    /// }
272    /// ]
273    /// "#;
274    /// let entities = Entities::from_json_str(data, None).unwrap();
275    /// let eid = EntityId::from_str("alice").unwrap();
276    /// let type_name: EntityTypeName = EntityTypeName::from_str("User").unwrap();
277    /// let euid = EntityUid::from_type_name_and_id(type_name, eid);
278    /// let entity = entities.get(&euid).unwrap();
279    /// assert_eq!(entity.attr("age").unwrap(), Ok(EvalResult::Long(19)));
280    /// let ip = entity.attr("ip_addr").unwrap().unwrap();
281    /// assert_eq!(ip, EvalResult::ExtensionValue("10.0.1.101/32".to_string()));
282    /// ```
283    pub fn from_json_str(
284        json: &str,
285        schema: Option<&Schema>,
286    ) -> Result<Self, entities::EntitiesError> {
287        let eparser = entities::EntityJsonParser::new(
288            schema.map(|s| cedar_policy_validator::CoreSchema::new(&s.0)),
289            Extensions::all_available(),
290            entities::TCComputation::ComputeNow,
291        );
292        eparser.from_json_str(json).map(Entities)
293    }
294
295    /// Parse an entities JSON file (in `serde_json::Value` form) into an
296    /// `Entities` object
297    ///
298    /// If a `schema` is provided, this will inform the parsing: for instance, it
299    /// will allow `__entity` and `__extn` escapes to be implicit, and it will error
300    /// if attributes have the wrong types (e.g., string instead of integer).
301    /// ```
302    /// use std::collections::HashMap;
303    /// use std::str::FromStr;
304    /// # use cedar_policy::{Entities, EntityId, EntityTypeName, EntityUid, EvalResult, Request,PolicySet};
305    /// let data =serde_json::json!(
306    /// [
307    /// {
308    ///   "uid": {"type":"User","id":"alice"},
309    ///   "attrs": {
310    ///     "age":19,
311    ///     "ip_addr":{"__extn":{"fn":"ip", "arg":"10.0.1.101"}}
312    ///   },
313    ///   "parents": [{"type":"Group","id":"admin"}]
314    /// },
315    /// {
316    ///   "uid": {"type":"Groupd","id":"admin"},
317    ///   "attrs": {},
318    ///   "parents": []
319    /// }
320    /// ]
321    /// );
322    /// let entities = Entities::from_json_value(data, None).unwrap();
323    /// ```
324    pub fn from_json_value(
325        json: serde_json::Value,
326        schema: Option<&Schema>,
327    ) -> Result<Self, entities::EntitiesError> {
328        let eparser = entities::EntityJsonParser::new(
329            schema.map(|s| cedar_policy_validator::CoreSchema::new(&s.0)),
330            Extensions::all_available(),
331            entities::TCComputation::ComputeNow,
332        );
333        eparser.from_json_value(json).map(Entities)
334    }
335
336    /// Parse an entities JSON file (in `std::io::Read` form) into an `Entities`
337    /// object
338    ///
339    /// If a `schema` is provided, this will inform the parsing: for instance, it
340    /// will allow `__entity` and `__extn` escapes to be implicit, and it will error
341    /// if attributes have the wrong types (e.g., string instead of integer).
342    pub fn from_json_file(
343        json: impl std::io::Read,
344        schema: Option<&Schema>,
345    ) -> Result<Self, entities::EntitiesError> {
346        let eparser = entities::EntityJsonParser::new(
347            schema.map(|s| cedar_policy_validator::CoreSchema::new(&s.0)),
348            Extensions::all_available(),
349            entities::TCComputation::ComputeNow,
350        );
351        eparser.from_json_file(json).map(Entities)
352    }
353
354    /// Is entity `a` an ancestor of entity `b`?
355    /// Same semantics as `b in a` in the Cedar language
356    pub fn is_ancestor_of(&self, a: &EntityUid, b: &EntityUid) -> bool {
357        match self.0.entity(&b.0) {
358            Dereference::Data(b) => b.is_descendant_of(&a.0),
359            _ => a == b, // if b doesn't exist, `b in a` is only true if `b == a`
360        }
361    }
362
363    /// Get an iterator over the ancestors of the given Euid.
364    /// Returns `None` if the given `Euid` does not exist.
365    pub fn ancestors<'a>(
366        &'a self,
367        euid: &EntityUid,
368    ) -> Option<impl Iterator<Item = &'a EntityUid>> {
369        let entity = match self.0.entity(&euid.0) {
370            Dereference::Residual(_) | Dereference::NoSuchEntity => None,
371            Dereference::Data(e) => Some(e),
372        }?;
373        // Invariant: No way to write down the unspecified EntityUid, so no way to have ancestors that are unspecified
374        Some(entity.ancestors().map(EntityUid::ref_cast))
375    }
376
377    /// Dump an `Entities` object into an entities JSON file.
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 `Entities` object from an entities JSON file, use
383    /// `from_json_file`.
384    pub fn write_to_json(
385        &self,
386        f: impl std::io::Write,
387    ) -> std::result::Result<(), entities::EntitiesError> {
388        self.0.write_to_json(f)
389    }
390}
391
392/// Authorizer object, which provides responses to authorization queries
393#[repr(transparent)]
394#[derive(Debug, RefCast)]
395pub struct Authorizer(authorizer::Authorizer);
396
397impl Default for Authorizer {
398    fn default() -> Self {
399        Self::new()
400    }
401}
402
403impl Authorizer {
404    /// Create a new `Authorizer`
405    ///
406    /// The authorizer uses the `stacker` crate to manage stack size and tries to use a sane default.
407    /// If the default is not right for you, you can try wrapping the authorizer or individual calls
408    /// to `is_authorized` in `stacker::grow`.
409    /// ```
410    /// # use cedar_policy::{Authorizer, Context, Entities, EntityId, EntityTypeName,
411    /// # EntityUid, Request,PolicySet};
412    /// # use std::str::FromStr;
413    /// # // create a request
414    /// # let p_eid = EntityId::from_str("alice").unwrap();
415    /// # let p_name: EntityTypeName = EntityTypeName::from_str("User").unwrap();
416    /// # let p = EntityUid::from_type_name_and_id(p_name, p_eid);
417    /// #
418    /// # let a_eid = EntityId::from_str("view").unwrap();
419    /// # let a_name: EntityTypeName = EntityTypeName::from_str("Action").unwrap();
420    /// # let a = EntityUid::from_type_name_and_id(a_name, a_eid);
421    /// #
422    /// # let r_eid = EntityId::from_str("trip").unwrap();
423    /// # let r_name: EntityTypeName = EntityTypeName::from_str("Album").unwrap();
424    /// # let r = EntityUid::from_type_name_and_id(r_name, r_eid);
425    /// #
426    /// # let c = Context::empty();
427    /// #
428    /// # let request: Request = Request::new(Some(p), Some(a), Some(r), c);
429    /// #
430    /// # // create a policy
431    /// # let s = r#"permit(
432    /// #     principal == User::"alice",
433    /// #     action == Action::"view",
434    /// #     resource == Album::"trip"
435    /// #   )when{
436    /// #     principal.ip_addr.isIpv4()
437    /// #   };
438    /// # "#;
439    /// # let policy = PolicySet::from_str(s).expect("policy error");
440    /// # // create entities
441    /// # let e = r#"[
442    /// #     {
443    /// #         "uid": {"type":"User","id":"alice"},
444    /// #         "attrs": {
445    /// #             "age":19,
446    /// #             "ip_addr":{"__extn":{"fn":"ip", "arg":"10.0.1.101"}}
447    /// #         },
448    /// #         "parents": []
449    /// #     }
450    /// # ]"#;
451    /// # let entities = Entities::from_json_str(e, None).expect("entity error");
452    /// let authorizer = Authorizer::new();
453    /// let r = authorizer.is_authorized(&request, &policy, &entities);
454    /// ```
455    pub fn new() -> Self {
456        Self(authorizer::Authorizer::new())
457    }
458
459    /// Returns an authorization response for `r` with respect to the given
460    /// `PolicySet` and `Entities`.
461    ///
462    /// The language spec and Dafny model give a precise definition of how this
463    /// is computed.
464    /// ```
465    /// use cedar_policy::{Authorizer,Context,Entities,EntityId,EntityTypeName,
466    /// EntityUid, Request,PolicySet};
467    /// use std::str::FromStr;
468    ///
469    /// // create a request
470    /// let p_eid = EntityId::from_str("alice").unwrap();
471    /// let p_name: EntityTypeName = EntityTypeName::from_str("User").unwrap();
472    /// let p = EntityUid::from_type_name_and_id(p_name, p_eid);
473    ///
474    /// let a_eid = EntityId::from_str("view").unwrap();
475    /// let a_name: EntityTypeName = EntityTypeName::from_str("Action").unwrap();
476    /// let a = EntityUid::from_type_name_and_id(a_name, a_eid);
477    ///
478    /// let r_eid = EntityId::from_str("trip").unwrap();
479    /// let r_name: EntityTypeName = EntityTypeName::from_str("Album").unwrap();
480    /// let r = EntityUid::from_type_name_and_id(r_name, r_eid);
481    ///
482    /// let c = Context::empty();
483    ///
484    /// let request: Request = Request::new(Some(p), Some(a), Some(r), c);
485    ///
486    /// // create a policy
487    /// let s = r#"
488    /// permit (
489    ///   principal == User::"alice",
490    ///   action == Action::"view",
491    ///   resource == Album::"trip"
492    /// )
493    /// when { principal.ip_addr.isIpv4() };
494    /// "#;
495    /// let policy = PolicySet::from_str(s).expect("policy error");
496
497    /// // create entities
498    /// let e = r#"[
499    ///     {
500    ///         "uid": {"type":"User","id":"alice"},
501    ///         "attrs": {
502    ///             "age":19,
503    ///             "ip_addr":{"__extn":{"fn":"ip", "arg":"10.0.1.101"}}
504    ///         },
505    ///         "parents": []
506    ///     }
507    /// ]"#;
508    /// let entities = Entities::from_json_str(e, None).expect("entity error");
509    ///
510    /// let authorizer = Authorizer::new();
511    /// let r = authorizer.is_authorized(&request, &policy, &entities);
512    /// println!("{:?}", r);
513    /// ```
514    pub fn is_authorized(&self, r: &Request, p: &PolicySet, e: &Entities) -> Response {
515        self.0.is_authorized(&r.0, &p.ast, &e.0).into()
516    }
517
518    /// A partially evaluated authorization request.
519    /// The Authorizer will attempt to make as much progress as possible in the presence of unknowns.
520    /// If the Authorizer can reach a response, it will return that response.
521    /// Otherwise, it will return a list of residual policies that still need to be evaluated.
522    #[cfg(feature = "partial-eval")]
523    pub fn is_authorized_partial(
524        &self,
525        query: &Request,
526        policy_set: &PolicySet,
527        entities: &Entities,
528    ) -> PartialResponse {
529        let response = self
530            .0
531            .is_authorized_core(&query.0, &policy_set.ast, &entities.0);
532        match response {
533            authorizer::ResponseKind::FullyEvaluated(a) => PartialResponse::Concrete(a.into()),
534            authorizer::ResponseKind::Partial(p) => PartialResponse::Residual(p.into()),
535        }
536    }
537}
538
539/// Authorization response returned from the `Authorizer`
540#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
541pub struct Response {
542    /// Authorization decision
543    decision: Decision,
544    /// Diagnostics providing more information on how this decision was reached
545    diagnostics: Diagnostics,
546}
547
548/// Authorization response returned from `is_authorized_partial`.
549/// It can either be a full concrete response, or a residual response.
550#[cfg(feature = "partial-eval")]
551#[derive(Debug, PartialEq, Clone)]
552pub enum PartialResponse {
553    /// A full, concrete response.
554    Concrete(Response),
555    /// A residual response. Determining the concrete response requires further processing.
556    Residual(ResidualResponse),
557}
558
559/// A residual response obtained from `is_authorized_partial`.
560#[cfg(feature = "partial-eval")]
561#[derive(Debug, PartialEq, Eq, Clone)]
562pub struct ResidualResponse {
563    /// Residual policies
564    residuals: PolicySet,
565    /// Diagnostics
566    diagnostics: Diagnostics,
567}
568
569/// Diagnostics providing more information on how a `Decision` was reached
570#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
571pub struct Diagnostics {
572    /// `PolicyId`s of the policies that contributed to the decision.
573    /// If no policies applied to the request, this set will be empty.
574    reason: HashSet<PolicyId>,
575    /// list of error messages which occurred
576    errors: HashSet<String>,
577}
578
579impl From<authorizer::Diagnostics> for Diagnostics {
580    fn from(diagnostics: authorizer::Diagnostics) -> Self {
581        Self {
582            reason: diagnostics.reason.into_iter().map(PolicyId).collect(),
583            errors: diagnostics.errors.iter().map(ToString::to_string).collect(),
584        }
585    }
586}
587
588impl Diagnostics {
589    /// Get the policies that contributed to the decision
590    /// ```
591    /// # use cedar_policy::{Authorizer, Context, Decision, Entities, EntityId, EntityTypeName,
592    /// # EntityUid, Request,PolicySet};
593    /// # use std::str::FromStr;
594    /// # // create a request
595    /// # let p_eid = EntityId::from_str("alice").unwrap();
596    /// # let p_name: EntityTypeName = EntityTypeName::from_str("User").unwrap();
597    /// # let p = EntityUid::from_type_name_and_id(p_name, p_eid);
598    /// #
599    /// # let a_eid = EntityId::from_str("view").unwrap();
600    /// # let a_name: EntityTypeName = EntityTypeName::from_str("Action").unwrap();
601    /// # let a = EntityUid::from_type_name_and_id(a_name, a_eid);
602    /// #
603    /// # let r_eid = EntityId::from_str("trip").unwrap();
604    /// # let r_name: EntityTypeName = EntityTypeName::from_str("Album").unwrap();
605    /// # let r = EntityUid::from_type_name_and_id(r_name, r_eid);
606    /// #
607    /// # let c = Context::empty();
608    /// #
609    /// # let request: Request = Request::new(Some(p), Some(a), Some(r), c);
610    /// #
611    /// # // create a policy
612    /// # let s = r#"permit(
613    /// #     principal == User::"alice",
614    /// #     action == Action::"view",
615    /// #     resource == Album::"trip"
616    /// #   )when{
617    /// #     principal.ip_addr.isIpv4()
618    /// #   };
619    /// # "#;
620    /// # let policy = PolicySet::from_str(s).expect("policy error");
621    /// # // create entities
622    /// # let e = r#"[
623    /// #     {
624    /// #         "uid": {"type":"User","id":"alice"},
625    /// #         "attrs": {
626    /// #             "age":19,
627    /// #             "ip_addr":{"__extn":{"fn":"ip", "arg":"10.0.1.101"}}
628    /// #         },
629    /// #         "parents": []
630    /// #     }
631    /// # ]"#;
632    /// # let entities = Entities::from_json_str(e, None).expect("entity error");
633    /// let authorizer = Authorizer::new();
634    /// let response = authorizer.is_authorized(&request, &policy, &entities);
635    /// match response.decision() {
636    ///     Decision::Allow => println!("ALLOW"),
637    ///     Decision::Deny => println!("DENY"),
638    /// }
639    /// println!("note: this decision was due to the following policies:");
640    /// for reason in response.diagnostics().reason() {
641    ///     println!("{}", reason);
642    /// }
643    /// ```
644    pub fn reason(&self) -> impl Iterator<Item = &PolicyId> {
645        self.reason.iter()
646    }
647
648    /// Get the error messages
649    /// ```
650    /// # use cedar_policy::{Authorizer, Context, Decision, Entities, EntityId, EntityTypeName,
651    /// # EntityUid, Request,PolicySet};
652    /// # use std::str::FromStr;
653    /// # // create a request
654    /// # let p_eid = EntityId::from_str("alice").unwrap();
655    /// # let p_name: EntityTypeName = EntityTypeName::from_str("User").unwrap();
656    /// # let p = EntityUid::from_type_name_and_id(p_name, p_eid);
657    /// #
658    /// # let a_eid = EntityId::from_str("view").unwrap();
659    /// # let a_name: EntityTypeName = EntityTypeName::from_str("Action").unwrap();
660    /// # let a = EntityUid::from_type_name_and_id(a_name, a_eid);
661    /// #
662    /// # let r_eid = EntityId::from_str("trip").unwrap();
663    /// # let r_name: EntityTypeName = EntityTypeName::from_str("Album").unwrap();
664    /// # let r = EntityUid::from_type_name_and_id(r_name, r_eid);
665    /// #
666    /// # let c = Context::empty();
667    /// #
668    /// # let request: Request = Request::new(Some(p), Some(a), Some(r), c);
669    /// #
670    /// # // create a policy
671    /// # let s = r#"permit(
672    /// #     principal == User::"alice",
673    /// #     action == Action::"view",
674    /// #     resource == Album::"trip"
675    /// #   )when{
676    /// #     principal.ip_addr.isIpv4()
677    /// #   };
678    /// # "#;
679    /// # let policy = PolicySet::from_str(s).expect("policy error");
680    /// # // create entities
681    /// # let e = r#"[
682    /// #     {
683    /// #         "uid": {"type":"User","id":"alice"},
684    /// #         "attrs": {
685    /// #             "age":19,
686    /// #             "ip_addr":{"__extn":{"fn":"ip", "arg":"10.0.1.101"}}
687    /// #         },
688    /// #         "parents": []
689    /// #     }
690    /// # ]"#;
691    /// # let entities = Entities::from_json_str(e, None).expect("entity error");
692    /// let authorizer = Authorizer::new();
693    /// let response = authorizer.is_authorized(&request, &policy, &entities);
694    /// match response.decision() {
695    ///     Decision::Allow => println!("ALLOW"),
696    ///     Decision::Deny => println!("DENY"),
697    /// }
698    /// for err in response.diagnostics().errors() {
699    ///     println!("{}", err);
700    /// }
701    /// ```
702    pub fn errors(&self) -> impl Iterator<Item = EvaluationError> + '_ {
703        self.errors
704            .iter()
705            .cloned()
706            .map(EvaluationError::StringMessage)
707    }
708}
709
710impl Response {
711    /// Create a new `Response`
712    pub fn new(decision: Decision, reason: HashSet<PolicyId>, errors: HashSet<String>) -> Self {
713        Self {
714            decision,
715            diagnostics: Diagnostics { reason, errors },
716        }
717    }
718
719    /// Get the authorization decision
720    pub fn decision(&self) -> Decision {
721        self.decision
722    }
723
724    /// Get the authorization diagnostics
725    pub fn diagnostics(&self) -> &Diagnostics {
726        &self.diagnostics
727    }
728}
729
730impl From<authorizer::Response> for Response {
731    fn from(a: authorizer::Response) -> Self {
732        Self {
733            decision: a.decision,
734            diagnostics: a.diagnostics.into(),
735        }
736    }
737}
738
739#[cfg(feature = "partial-eval")]
740impl ResidualResponse {
741    /// Create a new `ResidualResponse`
742    pub fn new(residuals: PolicySet, reason: HashSet<PolicyId>, errors: HashSet<String>) -> Self {
743        Self {
744            residuals,
745            diagnostics: Diagnostics { reason, errors },
746        }
747    }
748
749    /// Get the residual policies needed to reach an authorization decision.
750    pub fn residuals(&self) -> &PolicySet {
751        &self.residuals
752    }
753
754    /// Get the authorization diagnostics
755    pub fn diagnostics(&self) -> &Diagnostics {
756        &self.diagnostics
757    }
758}
759
760#[cfg(feature = "partial-eval")]
761impl From<authorizer::PartialResponse> for ResidualResponse {
762    fn from(p: authorizer::PartialResponse) -> Self {
763        Self {
764            residuals: PolicySet::from_ast(p.residuals),
765            diagnostics: p.diagnostics.into(),
766        }
767    }
768}
769
770/// Errors encountered while evaluating policies or expressions, or making
771/// authorization decisions.
772#[derive(Debug, Clone, PartialEq, Eq, Error)]
773pub enum EvaluationError {
774    /// Error message, as string.
775    /// TODO in the future this can/should be the actual Core `EvaluationError`
776    #[error("{0}")]
777    StringMessage(String),
778}
779
780/// Used to select how a policy will be validated.
781#[derive(Default, Eq, PartialEq, Copy, Clone, Debug)]
782#[non_exhaustive]
783pub enum ValidationMode {
784    /// Validate that policies do not contain any type errors, and additionally
785    /// have a restricted form which is amenable for analysis.
786    #[default]
787    Strict,
788    /// Validate that policies do not contain any type errors.
789    Permissive,
790}
791
792impl From<ValidationMode> for cedar_policy_validator::ValidationMode {
793    fn from(mode: ValidationMode) -> Self {
794        match mode {
795            ValidationMode::Strict => Self::Strict,
796            ValidationMode::Permissive => Self::Permissive,
797        }
798    }
799}
800
801/// Validator object, which provides policy validation and typechecking.
802#[repr(transparent)]
803#[derive(Debug, RefCast)]
804pub struct Validator(cedar_policy_validator::Validator);
805
806impl Validator {
807    /// Construct a new `Validator` to validate policies using the given
808    /// `Schema`.
809    pub fn new(schema: Schema) -> Self {
810        Self(cedar_policy_validator::Validator::new(schema.0))
811    }
812
813    /// Validate all policies in a policy set, collecting all validation errors
814    /// found into the returned `ValidationResult`. Each error is returned together with the
815    /// policy id of the policy where the error was found. If a policy id
816    /// included in the input policy set does not appear in the output iterator, then
817    /// that policy passed the validator. If the function `validation_passed`
818    /// returns true, then there were no validation errors found, so all
819    /// policies in the policy set have passed the validator.
820    pub fn validate<'a>(
821        &'a self,
822        pset: &'a PolicySet,
823        mode: ValidationMode,
824    ) -> ValidationResult<'a> {
825        ValidationResult::from(self.0.validate(&pset.ast, mode.into()))
826    }
827}
828
829/// Contains all the type information used to construct a `Schema` that can be
830/// used to validate a policy.
831#[derive(Debug)]
832pub struct SchemaFragment(cedar_policy_validator::ValidatorSchemaFragment);
833
834impl SchemaFragment {
835    /// Extract namespaces defined in this `SchemaFragment`. Each namespace
836    /// entry defines the name of the namespace and the entity types and actions
837    /// that exist in the namespace.
838    pub fn namespaces(&self) -> impl Iterator<Item = Option<EntityNamespace>> + '_ {
839        self.0
840            .namespaces()
841            .map(|ns| ns.as_ref().map(|ns| EntityNamespace(ns.clone())))
842    }
843
844    /// Create an `SchemaFragment` from a JSON value (which should be an
845    /// object of the shape required for Cedar schemas).
846    pub fn from_json_value(json: serde_json::Value) -> Result<Self, SchemaError> {
847        Ok(Self(
848            cedar_policy_validator::SchemaFragment::from_json_value(json)?.try_into()?,
849        ))
850    }
851
852    /// Create a `SchemaFragment` directly from a file.
853    pub fn from_file(file: impl std::io::Read) -> Result<Self, SchemaError> {
854        Ok(Self(
855            cedar_policy_validator::SchemaFragment::from_file(file)?.try_into()?,
856        ))
857    }
858}
859
860impl TryInto<Schema> for SchemaFragment {
861    type Error = SchemaError;
862
863    /// Convert `SchemaFragment` into a `Schema`. To build the `Schema` we
864    /// need to have all entity types defined, so an error will be returned if
865    /// any undeclared entity types are referenced in the schema fragment.
866    fn try_into(self) -> Result<Schema, Self::Error> {
867        Ok(Schema(
868            cedar_policy_validator::ValidatorSchema::from_schema_fragments([self.0])?,
869        ))
870    }
871}
872
873impl FromStr for SchemaFragment {
874    type Err = SchemaError;
875    /// Construct `SchemaFragment` from a string containing a schema formatted
876    /// in the cedar schema format. This can fail if the string is not valid
877    /// JSON, or if the JSON structure does not form a valid schema. This
878    /// function does not check for consistency in the schema (e.g., references
879    /// to undefined entities) because this is not required until a `Schema` is
880    /// constructed.
881    fn from_str(src: &str) -> Result<Self, Self::Err> {
882        Ok(Self(
883            serde_json::from_str::<cedar_policy_validator::SchemaFragment>(src)
884                .map_err(cedar_policy_validator::SchemaError::from)?
885                .try_into()?,
886        ))
887    }
888}
889
890/// Object containing schema information used by the validator.
891#[repr(transparent)]
892#[derive(Debug, Clone, RefCast)]
893pub struct Schema(pub(crate) cedar_policy_validator::ValidatorSchema);
894
895impl FromStr for Schema {
896    type Err = SchemaError;
897
898    /// Construct a schema from a string containing a schema formatted in the
899    /// Cedar schema format. This can fail if it is not possible to parse a
900    /// schema from the strings, or if errors in values in the schema are
901    /// uncovered after parsing. For instance, when an entity attribute name is
902    /// found to not be a valid attribute name according to the Cedar
903    /// grammar.
904    fn from_str(schema_src: &str) -> Result<Self, Self::Err> {
905        Ok(Self(schema_src.parse()?))
906    }
907}
908
909impl Schema {
910    /// Create a `Schema` from multiple `SchemaFragment`. The individual
911    /// fragments may references entity types that are not declared in that
912    /// fragment, but all referenced entity types must be declared in some
913    /// fragment.
914    pub fn from_schema_fragments(
915        fragments: impl IntoIterator<Item = SchemaFragment>,
916    ) -> Result<Self, SchemaError> {
917        Ok(Self(
918            cedar_policy_validator::ValidatorSchema::from_schema_fragments(
919                fragments.into_iter().map(|f| f.0),
920            )?,
921        ))
922    }
923
924    /// Create a `Schema` from a JSON value (which should be an object of the
925    /// shape required for Cedar schemas).
926    pub fn from_json_value(json: serde_json::Value) -> Result<Self, SchemaError> {
927        Ok(Self(
928            cedar_policy_validator::ValidatorSchema::from_json_value(json)?,
929        ))
930    }
931
932    /// Create a `Schema` directly from a file.
933    pub fn from_file(file: impl std::io::Read) -> Result<Self, SchemaError> {
934        Ok(Self(cedar_policy_validator::ValidatorSchema::from_file(
935            file,
936        )?))
937    }
938
939    /// Extract from the schema an `Entities` containing the action entities
940    /// declared in the schema.
941    pub fn action_entities(&self) -> Result<Entities, entities::EntitiesError> {
942        Ok(Entities(self.0.action_entities()?))
943    }
944}
945
946/// Errors encountered during construction of a Validation Schema
947#[derive(Debug, Error)]
948pub enum SchemaError {
949    /// Errors loading and parsing schema files
950    #[error("JSON Schema file could not be parsed: {0}")]
951    ParseJson(serde_json::Error),
952    /// Errors occurring while computing or enforcing transitive closure on
953    /// action id hierarchy.
954    #[error("Transitive closure error on action hierarchy: {0}")]
955    ActionTransitiveClosureError(String),
956    /// Errors occurring while computing or enforcing transitive closure on
957    /// entity type hierarchy.
958    #[error("Transitive closure error on entity hierarchy: {0}")]
959    EntityTransitiveClosureError(String),
960    /// Error generated when processing a schema file that uses features which
961    /// are not yet supported by the implementation.
962    #[error("Unsupported feature used in schema: {0}")]
963    UnsupportedSchemaFeature(String),
964    /// Undeclared entity type(s) used in an entity type's memberOf field or an
965    /// action's appliesTo fields.
966    #[error("Undeclared entity types: {0:?}")]
967    UndeclaredEntityTypes(HashSet<String>),
968    /// Undeclared action(s) used in an action's memberOf field.
969    #[error("Undeclared actions: {0:?}")]
970    UndeclaredActions(HashSet<String>),
971    /// Undeclared type used in entity or context attributes.
972    #[error("Undeclared common types: {0:?}")]
973    UndeclaredCommonType(HashSet<String>),
974    /// Duplicate specifications for an entity type. Argument is the name of
975    /// the duplicate entity type.
976    #[error("Duplicate entity type {0}")]
977    DuplicateEntityType(String),
978    /// Duplicate specifications for an action. Argument is the name of the
979    /// duplicate action.
980    #[error("Duplicate action {0}")]
981    DuplicateAction(String),
982    /// Duplicate specifications for a reusable common type. Argument is the
983    /// name of the duplicate type.
984    #[error("Duplicate common type {0}")]
985    DuplicateCommonType(String),
986    /// Cycle in the schema's action hierarchy.
987    #[error("Cycle in action hierarchy")]
988    CycleInActionHierarchy,
989    /// Parse errors occurring while parsing an entity type.
990    #[error("Parse error in entity type: {0}")]
991    EntityTypeParse(ParseErrors),
992    /// Parse errors occurring while parsing a namespace identifier.
993    #[error("Parse error in namespace identifier: {0}")]
994    NamespaceParse(ParseErrors),
995    /// Parse errors occurring while parsing a common type identifier.
996    #[error("Parse error in common type identifier: {0}")]
997    CommonTypeParseError(ParseErrors),
998    /// Parse errors occurring while parsing an extension type.
999    #[error("Parse error in extension type: {0}")]
1000    ExtensionTypeParse(ParseErrors),
1001    /// The schema file included an entity type `Action` in the entity type
1002    /// list. The `Action` entity type is always implicitly declared, and it
1003    /// cannot currently have attributes or be in any groups, so there is no
1004    /// purposes in adding an explicit entry.
1005    #[error("Entity type `Action` declared in `entityTypes` list.")]
1006    ActionEntityTypeDeclared,
1007    /// One or more action entities are declared with `attributes` lists, but
1008    /// action entities cannot have attributes.
1009    #[error("Actions declared with `attributes`: [{}]", .0.iter().map(String::as_str).join(", "))]
1010    ActionEntityAttributes(Vec<String>),
1011    /// An action context or entity type shape was declared to have a type other
1012    /// than `Record`.
1013    #[error("Action context or entity type shape is not a record")]
1014    ContextOrShapeNotRecord,
1015    /// An Action Entity (transitively) has an attribute that is an empty set
1016    #[error("Action attribute is an empty set")]
1017    ActionEntityAttributeEmptySet,
1018    /// An Action Entity (transitively) has an attribute of unsupported type (`ExprEscape`, `EntityEscape` or `ExtnEscape`)
1019    #[error(
1020        "Action has an attribute of unsupported type (escaped expression, entity or extension)"
1021    )]
1022    ActionEntityAttributeUnsupportedType,
1023}
1024
1025#[doc(hidden)]
1026impl From<cedar_policy_validator::SchemaError> for SchemaError {
1027    fn from(value: cedar_policy_validator::SchemaError) -> Self {
1028        match value {
1029            cedar_policy_validator::SchemaError::Serde(e) => Self::ParseJson(e),
1030            cedar_policy_validator::SchemaError::ActionTransitiveClosure(e) => {
1031                Self::ActionTransitiveClosureError(e.to_string())
1032            }
1033            cedar_policy_validator::SchemaError::EntityTypeTransitiveClosure(e) => {
1034                Self::EntityTransitiveClosureError(e.to_string())
1035            }
1036            cedar_policy_validator::SchemaError::UnsupportedFeature(e) => {
1037                Self::UnsupportedSchemaFeature(e.to_string())
1038            }
1039            cedar_policy_validator::SchemaError::UndeclaredEntityTypes(e) => {
1040                Self::UndeclaredEntityTypes(e)
1041            }
1042            cedar_policy_validator::SchemaError::UndeclaredActions(e) => Self::UndeclaredActions(e),
1043            cedar_policy_validator::SchemaError::UndeclaredCommonTypes(c) => {
1044                Self::UndeclaredCommonType(c)
1045            }
1046            cedar_policy_validator::SchemaError::DuplicateEntityType(e) => {
1047                Self::DuplicateEntityType(e)
1048            }
1049            cedar_policy_validator::SchemaError::DuplicateAction(e) => Self::DuplicateAction(e),
1050            cedar_policy_validator::SchemaError::DuplicateCommonType(c) => {
1051                Self::DuplicateCommonType(c)
1052            }
1053            cedar_policy_validator::SchemaError::CycleInActionHierarchy => {
1054                Self::CycleInActionHierarchy
1055            }
1056            cedar_policy_validator::SchemaError::EntityTypeParseError(e) => {
1057                Self::EntityTypeParse(e)
1058            }
1059            cedar_policy_validator::SchemaError::NamespaceParseError(e) => Self::NamespaceParse(e),
1060            cedar_policy_validator::SchemaError::CommonTypeParseError(e) => {
1061                Self::CommonTypeParseError(e)
1062            }
1063            cedar_policy_validator::SchemaError::ExtensionTypeParseError(e) => {
1064                Self::ExtensionTypeParse(e)
1065            }
1066            cedar_policy_validator::SchemaError::ActionEntityTypeDeclared => {
1067                Self::ActionEntityTypeDeclared
1068            }
1069            cedar_policy_validator::SchemaError::ActionHasAttributes(e) => {
1070                Self::ActionEntityAttributes(e)
1071            }
1072            cedar_policy_validator::SchemaError::ContextOrShapeNotRecord(_) => {
1073                Self::ContextOrShapeNotRecord
1074            }
1075            cedar_policy_validator::SchemaError::ActionAttributesContainEmptySet(_) => {
1076                Self::ActionEntityAttributeEmptySet
1077            }
1078            cedar_policy_validator::SchemaError::UnsupportedActionAttributeType(_) => {
1079                Self::ActionEntityAttributeUnsupportedType
1080            }
1081        }
1082    }
1083}
1084
1085/// Contains the result of policy validation. The result includes the list of of
1086/// issues found by the validation and whether validation succeeds or fails.
1087/// Validation succeeds if there are no fatal errors.  There are currently no
1088/// non-fatal warnings, so any issues found will cause validation to fail.
1089#[derive(Debug)]
1090pub struct ValidationResult<'a> {
1091    validation_errors: Vec<ValidationError<'a>>,
1092}
1093
1094impl<'a> ValidationResult<'a> {
1095    /// True when validation passes. There are no fatal errors.
1096    pub fn validation_passed(&self) -> bool {
1097        self.validation_errors.is_empty()
1098    }
1099
1100    /// Get the list of errors found by the validator.
1101    pub fn validation_errors(&self) -> impl Iterator<Item = &ValidationError<'a>> {
1102        self.validation_errors.iter()
1103    }
1104}
1105
1106impl<'a> From<cedar_policy_validator::ValidationResult<'a>> for ValidationResult<'a> {
1107    fn from(r: cedar_policy_validator::ValidationResult<'a>) -> Self {
1108        Self {
1109            validation_errors: r
1110                .into_validation_errors()
1111                .map(ValidationError::from)
1112                .collect(),
1113        }
1114    }
1115}
1116
1117/// An error generated by the validator when it finds a potential problem in a
1118/// policy. The error contains a enumeration that specifies the kind of problem,
1119/// and provides details specific to that kind of problem. The error also records
1120/// where the problem was encountered.
1121#[derive(Debug, Error)]
1122pub struct ValidationError<'a> {
1123    location: SourceLocation<'a>,
1124    error_kind: ValidationErrorKind,
1125}
1126
1127impl<'a> ValidationError<'a> {
1128    /// Extract details about the exact issue detected by the validator.
1129    pub fn error_kind(&self) -> &ValidationErrorKind {
1130        &self.error_kind
1131    }
1132
1133    /// Extract the location where the validator found the issue.
1134    pub fn location(&self) -> &SourceLocation<'a> {
1135        &self.location
1136    }
1137}
1138
1139impl<'a> From<cedar_policy_validator::ValidationError<'a>> for ValidationError<'a> {
1140    fn from(err: cedar_policy_validator::ValidationError<'a>) -> Self {
1141        let (location, error_kind) = err.into_location_and_error_kind();
1142        Self {
1143            location: SourceLocation::from(location),
1144            error_kind,
1145        }
1146    }
1147}
1148
1149impl<'a> std::fmt::Display for ValidationError<'a> {
1150    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1151        write!(f, "Validation error on policy {}", self.location.policy_id)?;
1152        if let (Some(range_start), Some(range_end)) =
1153            (self.location().range_start(), self.location().range_end())
1154        {
1155            write!(f, " at offset {range_start}-{range_end}")?;
1156        }
1157        write!(f, ": {}", self.error_kind())
1158    }
1159}
1160
1161/// Represents a location in Cedar policy source.
1162#[derive(Debug, Clone, Eq, PartialEq)]
1163pub struct SourceLocation<'a> {
1164    policy_id: &'a PolicyId,
1165    source_range: Option<SourceInfo>,
1166}
1167
1168impl<'a> SourceLocation<'a> {
1169    /// Get the `PolicyId` for the policy at this source location.
1170    pub fn policy_id(&self) -> &'a PolicyId {
1171        self.policy_id
1172    }
1173
1174    /// Get the start of the location. Returns `None` if this location does not
1175    /// have a range.
1176    pub fn range_start(&self) -> Option<usize> {
1177        self.source_range.as_ref().map(SourceInfo::range_start)
1178    }
1179
1180    /// Get the end of the location. Returns `None` if this location does not
1181    /// have a range.
1182    pub fn range_end(&self) -> Option<usize> {
1183        self.source_range.as_ref().map(SourceInfo::range_end)
1184    }
1185}
1186
1187impl<'a> From<cedar_policy_validator::SourceLocation<'a>> for SourceLocation<'a> {
1188    fn from(loc: cedar_policy_validator::SourceLocation<'a>) -> SourceLocation<'a> {
1189        let policy_id: &'a PolicyId = PolicyId::ref_cast(loc.policy_id());
1190        let source_range = loc.into_source_info();
1191        Self {
1192            policy_id,
1193            source_range,
1194        }
1195    }
1196}
1197
1198/// Scan a set of policies for potentially confusing/obfuscating text.
1199pub fn confusable_string_checker<'a>(
1200    templates: impl Iterator<Item = &'a Template>,
1201) -> impl Iterator<Item = ValidationWarning<'a>> {
1202    cedar_policy_validator::confusable_string_checks(templates.map(|t| &t.ast))
1203        .map(std::convert::Into::into)
1204}
1205
1206#[derive(Debug, Error)]
1207#[error("Warning on policy {}: {}", .location.policy_id, .kind)]
1208/// Warnings found in Cedar policies
1209pub struct ValidationWarning<'a> {
1210    location: SourceLocation<'a>,
1211    kind: ValidationWarningKind,
1212}
1213
1214impl<'a> ValidationWarning<'a> {
1215    /// Extract details about the exact issue detected by the validator.
1216    pub fn warning_kind(&self) -> &ValidationWarningKind {
1217        &self.kind
1218    }
1219
1220    /// Extract the location where the validator found the issue.
1221    pub fn location(&self) -> &SourceLocation<'a> {
1222        &self.location
1223    }
1224}
1225
1226#[doc(hidden)]
1227impl<'a> From<cedar_policy_validator::ValidationWarning<'a>> for ValidationWarning<'a> {
1228    fn from(w: cedar_policy_validator::ValidationWarning<'a>) -> Self {
1229        let (loc, kind) = w.to_kind_and_location();
1230        ValidationWarning {
1231            location: SourceLocation {
1232                policy_id: PolicyId::ref_cast(loc),
1233                source_range: None,
1234            },
1235            kind,
1236        }
1237    }
1238}
1239
1240/// unique identifier portion of the `EntityUid` type
1241#[repr(transparent)]
1242#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, RefCast)]
1243pub struct EntityId(ast::Eid);
1244
1245impl FromStr for EntityId {
1246    type Err = ParseErrors;
1247    fn from_str(eid_str: &str) -> Result<Self, Self::Err> {
1248        Ok(Self(ast::Eid::new(eid_str)))
1249    }
1250}
1251
1252impl AsRef<str> for EntityId {
1253    fn as_ref(&self) -> &str {
1254        self.0.as_ref()
1255    }
1256}
1257
1258// Note that this Display formatter will format the EntityId as it would be expected
1259// in the EntityUid string form. For instance, the `"alice"` in `User::"alice"`.
1260// This means it adds quotes and potentially performs some escaping.
1261impl std::fmt::Display for EntityId {
1262    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1263        write!(f, "{}", self.0)
1264    }
1265}
1266
1267/// Represents a concatenation of Namespaces and `TypeName`
1268#[repr(transparent)]
1269#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, RefCast)]
1270pub struct EntityTypeName(ast::Name);
1271
1272impl EntityTypeName {
1273    /// Get the basename of the `EntityTypeName` (ie, with namespaces stripped).
1274    /// ```
1275    /// use cedar_policy::EntityTypeName;
1276    /// use std::str::FromStr;
1277    /// let type_name = EntityTypeName::from_str("MySpace::User").unwrap();
1278    /// assert_eq!(type_name.basename(), "User");
1279    /// ```
1280    pub fn basename(&self) -> &str {
1281        self.0.basename().as_ref()
1282    }
1283
1284    /// Get the namespace of the `EntityTypeName`, as components
1285    /// ```
1286    /// use cedar_policy::EntityTypeName;
1287    /// use std::str::FromStr;
1288    /// let type_name = EntityTypeName::from_str("Namespace::MySpace::User").unwrap();
1289    /// let mut components = type_name.namespace_components();
1290    /// assert_eq!(components.next(), Some("Namespace"));
1291    /// assert_eq!(components.next(), Some("MySpace"));
1292    /// assert_eq!(components.next(), None);
1293    /// ```
1294    pub fn namespace_components(&self) -> impl Iterator<Item = &str> {
1295        self.0.namespace_components().map(AsRef::as_ref)
1296    }
1297
1298    /// Get the full namespace of the `EntityTypeName`, as a single string.
1299    /// ```
1300    /// use cedar_policy::EntityTypeName;
1301    /// use std::str::FromStr;
1302    /// let type_name = EntityTypeName::from_str("Namespace::MySpace::User").unwrap();
1303    /// let components = type_name.namespace();
1304    /// assert_eq!(components,"Namespace::MySpace");
1305    /// ```
1306    pub fn namespace(&self) -> String {
1307        self.0.namespace()
1308    }
1309}
1310
1311impl FromStr for EntityTypeName {
1312    type Err = ParseErrors;
1313
1314    fn from_str(namespace_type_str: &str) -> Result<Self, Self::Err> {
1315        ast::Name::from_normalized_str(namespace_type_str).map(EntityTypeName)
1316    }
1317}
1318
1319impl std::fmt::Display for EntityTypeName {
1320    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1321        write!(f, "{}", self.0)
1322    }
1323}
1324
1325/// Represents a namespace
1326#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
1327pub struct EntityNamespace(ast::Name);
1328
1329// This FromStr implementation requires the _normalized_ representation of the
1330// namespace. See https://github.com/cedar-policy/rfcs/pull/9/.
1331impl FromStr for EntityNamespace {
1332    type Err = ParseErrors;
1333
1334    fn from_str(namespace_str: &str) -> Result<Self, Self::Err> {
1335        ast::Name::from_normalized_str(namespace_str).map(EntityNamespace)
1336    }
1337}
1338
1339impl std::fmt::Display for EntityNamespace {
1340    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1341        write!(f, "{}", self.0)
1342    }
1343}
1344
1345/// Unique Id for an entity, such as `User::"alice"`
1346// INVARIANT: this can never be an `ast::EntityType::Unspecified`
1347#[repr(transparent)]
1348#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, RefCast)]
1349pub struct EntityUid(ast::EntityUID);
1350
1351impl EntityUid {
1352    /// Returns the portion of the Euid that represents namespace and entity type
1353    /// ```
1354    /// use cedar_policy::{Entity, EntityId, EntityTypeName, EntityUid};
1355    /// use std::str::FromStr;
1356    /// let json_data = serde_json::json!({ "__entity": { "type": "User", "id": "alice" } });
1357    /// let euid = EntityUid::from_json(json_data).unwrap();
1358    /// assert_eq!(euid.type_name(), &EntityTypeName::from_str("User").unwrap());
1359    /// ```
1360    pub fn type_name(&self) -> &EntityTypeName {
1361        // PANIC SAFETY by invariant on struct
1362        #[allow(clippy::panic)]
1363        match self.0.entity_type() {
1364            ast::EntityType::Unspecified => panic!("Impossible to have an unspecified entity"),
1365            ast::EntityType::Concrete(name) => EntityTypeName::ref_cast(name),
1366        }
1367    }
1368
1369    /// Returns the id portion of the Euid
1370    /// ```
1371    /// use cedar_policy::{Entity, EntityId, EntityTypeName, EntityUid};
1372    /// use std::str::FromStr;
1373    /// let json_data = serde_json::json!({ "__entity": { "type": "User", "id": "alice" } });
1374    /// let euid = EntityUid::from_json(json_data).unwrap();
1375    /// assert_eq!(euid.id(), &EntityId::from_str("alice").unwrap());
1376    /// ```
1377    pub fn id(&self) -> &EntityId {
1378        EntityId::ref_cast(self.0.eid())
1379    }
1380
1381    /// Creates `EntityUid` from `EntityTypeName` and `EntityId`
1382    ///```
1383    /// use cedar_policy::{Entity, EntityId, EntityTypeName, EntityUid};
1384    /// use std::str::FromStr;
1385    /// let eid = EntityId::from_str("alice").unwrap();
1386    /// let type_name: EntityTypeName = EntityTypeName::from_str("User").unwrap();
1387    /// let euid = EntityUid::from_type_name_and_id(type_name, eid);
1388    /// assert_eq!(euid.type_name(), &EntityTypeName::from_str("User").unwrap());
1389    /// assert_eq!(euid.id(), &EntityId::from_str("alice").unwrap());
1390    ///
1391    /// ```
1392    pub fn from_type_name_and_id(name: EntityTypeName, id: EntityId) -> Self {
1393        // INVARIANT: `from_components` always constructs a Concrete id
1394        Self(ast::EntityUID::from_components(name.0, id.0))
1395    }
1396
1397    /// Creates `EntityUid` from a JSON value, which should have
1398    /// either the implicit or explicit `__entity` form.
1399    /// ```
1400    /// # use cedar_policy::{Entity, EntityId, EntityTypeName, EntityUid};
1401    /// # use std::str::FromStr;
1402    /// let json_data = serde_json::json!({ "__entity": { "type": "User", "id": "123abc" } });
1403    /// let euid = EntityUid::from_json(json_data).unwrap();
1404    /// assert_eq!(euid.type_name(), &EntityTypeName::from_str("User").unwrap());
1405    /// ```
1406    pub fn from_json(json: serde_json::Value) -> Result<Self, impl std::error::Error> {
1407        let parsed: entities::EntityUidJSON = serde_json::from_value(json)?;
1408        // INVARIANT: There is no way to write down the unspecified entityuid
1409        Ok::<Self, entities::JsonDeserializationError>(Self(
1410            parsed.into_euid(|| JsonDeserializationErrorContext::EntityUid)?,
1411        ))
1412    }
1413
1414    /// Testing utility for creating `EntityUids` a bit easier
1415    #[cfg(test)]
1416    pub(crate) fn from_strs(typename: &str, id: &str) -> Self {
1417        Self::from_type_name_and_id(
1418            EntityTypeName::from_str(typename).unwrap(),
1419            EntityId::from_str(id).unwrap(),
1420        )
1421    }
1422}
1423
1424impl FromStr for EntityUid {
1425    type Err = ParseErrors;
1426
1427    /// Parse an [`EntityUid`].
1428    ///
1429    /// An [`EntityUid`] consists of an [`EntityTypeName`] followed by a quoted [`EntityId`].
1430    /// The two are joined by a `::`.
1431    /// For the formal grammar, see <https://docs.cedarpolicy.com/policies/syntax-grammar.html#entity>
1432    ///
1433    /// Examples:
1434    /// ```
1435    ///  use cedar_policy::EntityUid;
1436    ///  let euid: EntityUid = r#"Foo::Bar::"george""#.parse().unwrap();
1437    ///  // Get the type of this euid (`Foo::Bar`)
1438    ///  euid.type_name();
1439    ///  // Or the id
1440    ///  euid.id();
1441    /// ```
1442    ///
1443    /// This [`FromStr`] implementation requires the _normalized_ representation of the
1444    /// UID. See <https://github.com/cedar-policy/rfcs/pull/9/>.
1445    ///
1446    /// A note on safety:
1447    ///
1448    /// __DO NOT__ create [`EntityUid`]'s via string concatenation.
1449    /// If you have separate components of an [`EntityUid`], use [`EntityUid::from_type_name_and_id`]
1450    fn from_str(uid_str: &str) -> Result<Self, Self::Err> {
1451        // INVARIANT there is no way to write down the unspecified entity
1452        ast::EntityUID::from_normalized_str(uid_str).map(EntityUid)
1453    }
1454}
1455
1456impl std::fmt::Display for EntityUid {
1457    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1458        write!(f, "{}", self.0)
1459    }
1460}
1461
1462/// Potential errors when adding to a `PolicySet`.
1463#[derive(Error, Debug)]
1464#[non_exhaustive]
1465pub enum PolicySetError {
1466    /// There was a duplicate [`PolicyId`] encountered in either the set of
1467    /// templates or the set of policies.
1468    #[error("duplicate template or policy id")]
1469    AlreadyDefined,
1470    /// Error when linking a template
1471    #[error("unable to link template: {0}")]
1472    LinkingError(#[from] ast::LinkingError),
1473    /// Expected a static policy, but a template-linked policy was provided.
1474    #[error("expected a static policy, but a template-linked policy was provided")]
1475    ExpectedStatic,
1476    /// Expected a template, but a static policy was provided.
1477    #[error("expected a template, but a static policy was provided")]
1478    ExpectedTemplate,
1479}
1480
1481impl From<ast::PolicySetError> for PolicySetError {
1482    fn from(e: ast::PolicySetError) -> Self {
1483        match e {
1484            ast::PolicySetError::Occupied => Self::AlreadyDefined,
1485        }
1486    }
1487}
1488
1489impl From<ast::UnexpectedSlotError> for PolicySetError {
1490    fn from(_: ast::UnexpectedSlotError) -> Self {
1491        Self::ExpectedStatic
1492    }
1493}
1494
1495/// Represents a set of `Policy`s
1496#[derive(Debug, Clone, Default)]
1497pub struct PolicySet {
1498    /// AST representation. Technically partially redundant with the other fields.
1499    /// Internally, we ensure that the duplicated information remains consistent.
1500    pub(crate) ast: ast::PolicySet,
1501    /// Policies in the set (this includes both static policies and template linked-policies)
1502    policies: HashMap<PolicyId, Policy>,
1503    /// Templates in the set
1504    templates: HashMap<PolicyId, Template>,
1505}
1506
1507impl PartialEq for PolicySet {
1508    fn eq(&self, other: &Self) -> bool {
1509        // eq is based on just the `ast`
1510        self.ast.eq(&other.ast)
1511    }
1512}
1513impl Eq for PolicySet {}
1514
1515impl FromStr for PolicySet {
1516    type Err = ParseErrors;
1517
1518    /// Create a policy set from multiple statements.
1519    ///
1520    /// Policy ids will default to "policy*" with numbers from 0.
1521    /// If you load more policies, do not use the default id, or there will be conflicts.
1522    ///
1523    /// See [`Policy`] for more.
1524    fn from_str(policies: &str) -> Result<Self, Self::Err> {
1525        let (texts, pset) = parser::parse_policyset_and_also_return_policy_text(policies)?;
1526        // PANIC SAFETY: By the invariant on `parse_policyset_and_also_return_policy_text(policies)`, every `PolicyId` in `pset.policies()` occurs as a key in `text`.
1527        #[allow(clippy::expect_used)]
1528        let policies = pset.policies().map(|p|
1529            (
1530                PolicyId(p.id().clone()),
1531                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() }
1532            )
1533        ).collect();
1534        // PANIC SAFETY: By the same invariant, every `PolicyId` in `pset.templates()` also occurs as a key in `text`.
1535        #[allow(clippy::expect_used)]
1536        let templates = pset.templates().map(|t|
1537            (
1538                PolicyId(t.id().clone()),
1539                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() }
1540            )
1541        ).collect();
1542        Ok(Self {
1543            ast: pset,
1544            policies,
1545            templates,
1546        })
1547    }
1548}
1549
1550impl PolicySet {
1551    /// Create a fresh empty `PolicySet`
1552    pub fn new() -> Self {
1553        Self {
1554            ast: ast::PolicySet::new(),
1555            policies: HashMap::new(),
1556            templates: HashMap::new(),
1557        }
1558    }
1559
1560    /// Create a `PolicySet` from the given policies
1561    pub fn from_policies(
1562        policies: impl IntoIterator<Item = Policy>,
1563    ) -> Result<Self, PolicySetError> {
1564        let mut set = Self::new();
1565        for policy in policies {
1566            set.add(policy)?;
1567        }
1568        Ok(set)
1569    }
1570
1571    /// Add an static policy to the `PolicySet`. To add a template instance, use
1572    /// `link` instead. This function will return an error (and not modify
1573    /// the `PolicySet`) if a template-linked policy is passed in.
1574    /// If a link, template or static policy with this ID already exists, this will error.
1575    pub fn add(&mut self, policy: Policy) -> Result<(), PolicySetError> {
1576        if policy.is_static() {
1577            let id = PolicyId(policy.ast.id().clone());
1578            self.ast.add(policy.ast.clone())?;
1579            self.policies.insert(id, policy);
1580            Ok(())
1581        } else {
1582            Err(PolicySetError::ExpectedStatic)
1583        }
1584    }
1585
1586    /// Add a `Template` to the `PolicySet`
1587    /// If a link, template or static policy with this ID already exists, this will error.
1588    pub fn add_template(&mut self, template: Template) -> Result<(), PolicySetError> {
1589        let id = PolicyId(template.ast.id().clone());
1590        self.ast.add_template(template.ast.clone())?;
1591        self.templates.insert(id, template);
1592        Ok(())
1593    }
1594
1595    /// Iterate over all the `Policy`s in the `PolicySet`.
1596    ///
1597    /// This will include both static and template-linked policies.
1598    pub fn policies(&self) -> impl Iterator<Item = &Policy> {
1599        self.policies.values()
1600    }
1601
1602    /// Iterate over the `Template`'s in the `PolicySet`.
1603    pub fn templates(&self) -> impl Iterator<Item = &Template> {
1604        self.templates.values()
1605    }
1606
1607    /// Get a `Template` by its `PolicyId`
1608    pub fn template(&self, id: &PolicyId) -> Option<&Template> {
1609        self.templates.get(id)
1610    }
1611
1612    /// Get a `Policy` by its `PolicyId`
1613    pub fn policy(&self, id: &PolicyId) -> Option<&Policy> {
1614        self.policies.get(id)
1615    }
1616
1617    /// Extract annotation data from a `Policy` by its `PolicyId` and annotation key
1618    pub fn annotation<'a>(&'a self, id: &PolicyId, key: impl AsRef<str>) -> Option<&'a str> {
1619        self.ast
1620            .get(&id.0)?
1621            .annotation(&key.as_ref().parse().ok()?)
1622            .map(smol_str::SmolStr::as_str)
1623    }
1624
1625    /// Extract annotation data from a `Template` by its `PolicyId` and annotation key.
1626    pub fn template_annotation(&self, id: &PolicyId, key: impl AsRef<str>) -> Option<String> {
1627        self.ast
1628            .get_template(&id.0)?
1629            .annotation(&key.as_ref().parse().ok()?)
1630            .map(smol_str::SmolStr::to_string)
1631    }
1632
1633    /// Returns true iff the `PolicySet` is empty
1634    pub fn is_empty(&self) -> bool {
1635        debug_assert_eq!(
1636            self.ast.is_empty(),
1637            self.policies.is_empty() && self.templates.is_empty()
1638        );
1639        self.ast.is_empty()
1640    }
1641
1642    /// Attempt to link a template and add the new template-linked policy to the policy set.
1643    /// If link fails, the `PolicySet` is not modified.
1644    /// Failure can happen for three reasons
1645    ///   1) The map passed in `vals` may not match the slots in the template
1646    ///   2) The `new_id` may conflict w/ a policy that already exists in the set
1647    /// If a link, template or static policy with this ID already exists, this will error.
1648    #[allow(clippy::needless_pass_by_value)]
1649    pub fn link(
1650        &mut self,
1651        template_id: PolicyId,
1652        new_id: PolicyId,
1653        vals: HashMap<SlotId, EntityUid>,
1654    ) -> Result<(), PolicySetError> {
1655        let unwrapped_vals: HashMap<ast::SlotId, ast::EntityUID> = vals
1656            .into_iter()
1657            .map(|(key, value)| (key.into(), value.0))
1658            .collect();
1659
1660        // Try to get the template with the id we're linking from.  We do this
1661        // _before_ calling `self.ast.link` because `link` mutates the policy
1662        // set by creating a new link entry in a hashmap. This happens even when
1663        // trying to link a static policy, which we want to error on here.
1664        let template = match self.templates.get(&template_id) {
1665            Some(template) => template,
1666            None => {
1667                return Err(if self.policies.contains_key(&template_id) {
1668                    PolicySetError::ExpectedTemplate
1669                } else {
1670                    PolicySetError::LinkingError(ast::LinkingError::NoSuchTemplate(template_id.0))
1671                });
1672            }
1673        };
1674
1675        let linked_ast = self
1676            .ast
1677            .link(
1678                template_id.0.clone(),
1679                new_id.0.clone(),
1680                unwrapped_vals.clone(),
1681            )
1682            .map_err(PolicySetError::LinkingError)?;
1683
1684        // PANIC SAFETY: `lossless.link()` will not fail after `ast.link()` succeeds
1685        #[allow(clippy::expect_used)]
1686        let linked_lossless = template
1687            .lossless
1688            .clone()
1689            .link(unwrapped_vals.iter().map(|(k, v)| (*k, v)))
1690            // The only error case for `est.link()` is a template with
1691            // slots which are not filled by the provided values. `ast.link()`
1692            // will have already errored if there are any unfilled slots in the
1693            // template.
1694            .expect("ast.link() didn't fail above, so this shouldn't fail");
1695        self.policies.insert(
1696            new_id,
1697            Policy {
1698                ast: linked_ast.clone(),
1699                lossless: linked_lossless,
1700            },
1701        );
1702        Ok(())
1703    }
1704
1705    /// Create a `PolicySet` from its AST representation only. The EST will
1706    /// reflect the AST structure. When possible, don't use this method and
1707    /// create the ESTs from the policy text or CST instead, as the conversion
1708    /// to AST is lossy. ESTs generated by this method will reflect the AST and
1709    /// not the original policy syntax.
1710    #[cfg_attr(not(feature = "partial-eval"), allow(unused))]
1711    fn from_ast(ast: ast::PolicySet) -> Self {
1712        let policies = ast
1713            .policies()
1714            .map(|p| (PolicyId(p.id().clone()), Policy::from_ast(p.clone())))
1715            .collect();
1716        let templates = ast
1717            .templates()
1718            .map(|t| (PolicyId(t.id().clone()), Template::from_ast(t.clone())))
1719            .collect();
1720        Self {
1721            ast,
1722            policies,
1723            templates,
1724        }
1725    }
1726}
1727
1728impl std::fmt::Display for PolicySet {
1729    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1730        write!(f, "{}", self.ast)
1731    }
1732}
1733
1734/// Policy template datatype
1735#[derive(Debug, Clone)]
1736pub struct Template {
1737    /// AST representation of the template, used for most operations.
1738    /// In particular, the `ast` contains the authoritative `PolicyId` for the template.
1739    ast: ast::Template,
1740
1741    /// Some "lossless" representation of the template, whichever is most
1742    /// convenient to provide (and can be provided with the least overhead).
1743    /// This is used just for `to_json()`.
1744    /// We can't just derive this on-demand from `ast`, because the AST is lossy:
1745    /// we can't reconstruct an accurate CST/EST/policy-text from the AST, but
1746    /// we can from the EST (modulo whitespace and a few other things like the
1747    /// order of annotations).
1748    ///
1749    /// This is a `LosslessPolicy` (rather than something like `LosslessTemplate`)
1750    /// because the EST doesn't distinguish between static policies and templates.
1751    lossless: LosslessPolicy,
1752}
1753
1754impl PartialEq for Template {
1755    fn eq(&self, other: &Self) -> bool {
1756        // eq is based on just the `ast`
1757        self.ast.eq(&other.ast)
1758    }
1759}
1760impl Eq for Template {}
1761
1762impl Template {
1763    /// Attempt to parse a `Template` from source.
1764    /// If `id` is Some, then the resulting template will have that `id`.
1765    /// If the `id` is None, the parser will use the default "policy0".
1766    /// The behavior around None may change in the future.
1767    pub fn parse(id: Option<String>, src: impl AsRef<str>) -> Result<Self, ParseErrors> {
1768        let ast = parser::parse_policy_template(id, src.as_ref())?;
1769        Ok(Self {
1770            ast,
1771            lossless: LosslessPolicy::policy_or_template_text(src.as_ref()),
1772        })
1773    }
1774
1775    /// Get the `PolicyId` of this `Template`
1776    pub fn id(&self) -> &PolicyId {
1777        PolicyId::ref_cast(self.ast.id())
1778    }
1779
1780    /// Clone this `Template` with a new `PolicyId`
1781    #[must_use]
1782    pub fn new_id(&self, id: PolicyId) -> Self {
1783        Self {
1784            ast: self.ast.new_id(id.0),
1785            lossless: self.lossless.clone(), // Lossless representation doesn't include the `PolicyId`
1786        }
1787    }
1788
1789    /// Get the `Effect` (`Forbid` or `Permit`) of this `Template`
1790    pub fn effect(&self) -> Effect {
1791        self.ast.effect()
1792    }
1793
1794    /// Get an annotation value of this `Template`
1795    pub fn annotation(&self, key: impl AsRef<str>) -> Option<&str> {
1796        self.ast
1797            .annotation(&key.as_ref().parse().ok()?)
1798            .map(smol_str::SmolStr::as_str)
1799    }
1800
1801    /// Iterate through annotation data of this `Template` as key-value pairs
1802    pub fn annotations(&self) -> impl Iterator<Item = (&str, &str)> {
1803        self.ast
1804            .annotations()
1805            .map(|(k, v)| (k.as_ref(), v.as_str()))
1806    }
1807
1808    /// Iterate over the open slots in this `Template`
1809    pub fn slots(&self) -> impl Iterator<Item = &SlotId> {
1810        self.ast.slots().map(SlotId::ref_cast)
1811    }
1812
1813    /// Get the head constraint on this policy's principal
1814    pub fn principal_constraint(&self) -> TemplatePrincipalConstraint {
1815        match self.ast.principal_constraint().as_inner() {
1816            ast::PrincipalOrResourceConstraint::Any => TemplatePrincipalConstraint::Any,
1817            ast::PrincipalOrResourceConstraint::In(eref) => {
1818                TemplatePrincipalConstraint::In(match eref {
1819                    ast::EntityReference::EUID(e) => Some(EntityUid(e.as_ref().clone())),
1820                    ast::EntityReference::Slot => None,
1821                })
1822            }
1823            ast::PrincipalOrResourceConstraint::Eq(eref) => {
1824                TemplatePrincipalConstraint::Eq(match eref {
1825                    ast::EntityReference::EUID(e) => Some(EntityUid(e.as_ref().clone())),
1826                    ast::EntityReference::Slot => None,
1827                })
1828            }
1829        }
1830    }
1831
1832    /// Get the head constraint on this policy's action
1833    pub fn action_constraint(&self) -> ActionConstraint {
1834        // Clone the data from Core to be consistent with the other constraints
1835        match self.ast.action_constraint() {
1836            ast::ActionConstraint::Any => ActionConstraint::Any,
1837            ast::ActionConstraint::In(ids) => ActionConstraint::In(
1838                ids.iter()
1839                    .map(|id| EntityUid(id.as_ref().clone()))
1840                    .collect(),
1841            ),
1842            ast::ActionConstraint::Eq(id) => ActionConstraint::Eq(EntityUid(id.as_ref().clone())),
1843        }
1844    }
1845
1846    /// Get the head constraint on this policy's resource
1847    pub fn resource_constraint(&self) -> TemplateResourceConstraint {
1848        match self.ast.resource_constraint().as_inner() {
1849            ast::PrincipalOrResourceConstraint::Any => TemplateResourceConstraint::Any,
1850            ast::PrincipalOrResourceConstraint::In(eref) => {
1851                TemplateResourceConstraint::In(match eref {
1852                    ast::EntityReference::EUID(e) => Some(EntityUid(e.as_ref().clone())),
1853                    ast::EntityReference::Slot => None,
1854                })
1855            }
1856            ast::PrincipalOrResourceConstraint::Eq(eref) => {
1857                TemplateResourceConstraint::Eq(match eref {
1858                    ast::EntityReference::EUID(e) => Some(EntityUid(e.as_ref().clone())),
1859                    ast::EntityReference::Slot => None,
1860                })
1861            }
1862        }
1863    }
1864
1865    /// Create a `Template` from its JSON representation.
1866    /// If `id` is Some, the policy will be given that Policy Id.
1867    /// If `id` is None, then "JSON policy" will be used.
1868    /// The behavior around None may change in the future.
1869    #[allow(dead_code)] // planned to be a public method in the future
1870    fn from_json(
1871        id: Option<PolicyId>,
1872        json: serde_json::Value,
1873    ) -> Result<Self, cedar_policy_core::est::EstToAstError> {
1874        let est: est::Policy =
1875            serde_json::from_value(json).map_err(JsonDeserializationError::Serde)?;
1876        Ok(Self {
1877            ast: est.clone().try_into_ast_template(id.map(|id| id.0))?,
1878            lossless: LosslessPolicy::Est(est),
1879        })
1880    }
1881
1882    /// Get the JSON representation of this `Template`.
1883    #[allow(dead_code)] // planned to be a public method in the future
1884    fn to_json(&self) -> Result<serde_json::Value, impl std::error::Error> {
1885        let est = self.lossless.est()?;
1886        let json = serde_json::to_value(est)?;
1887        Ok::<_, PolicyToJsonError>(json)
1888    }
1889
1890    /// Create a `Template` from its AST representation only. The EST will
1891    /// reflect the AST structure. When possible, don't use this method and
1892    /// create the EST from the policy text or CST instead, as the conversion
1893    /// to AST is lossy. ESTs generated by this method will reflect the AST and
1894    /// not the original policy syntax.
1895    #[cfg_attr(not(feature = "partial-eval"), allow(unused))]
1896    fn from_ast(ast: ast::Template) -> Self {
1897        let text = ast.to_string(); // assume that pretty-printing is faster than `est::Policy::from(ast.clone())`; is that true?
1898        Self {
1899            ast,
1900            lossless: LosslessPolicy::policy_or_template_text(text),
1901        }
1902    }
1903}
1904
1905impl FromStr for Template {
1906    type Err = ParseErrors;
1907
1908    fn from_str(src: &str) -> Result<Self, Self::Err> {
1909        Self::parse(None, src)
1910    }
1911}
1912
1913/// Head constraint on policy principals.
1914#[derive(Debug, Clone, PartialEq, Eq)]
1915pub enum PrincipalConstraint {
1916    /// Un-constrained
1917    Any,
1918    /// Must be In the given `EntityUid`
1919    In(EntityUid),
1920    /// Must be equal to the given `EntityUid`
1921    Eq(EntityUid),
1922}
1923
1924/// Head constraint on policy principals for templates.
1925#[derive(Debug, Clone, PartialEq, Eq)]
1926pub enum TemplatePrincipalConstraint {
1927    /// Un-constrained
1928    Any,
1929    /// Must be In the given `EntityUid`.
1930    /// If [`None`], then it is a template slot.
1931    In(Option<EntityUid>),
1932    /// Must be equal to the given `EntityUid`.
1933    /// If [`None`], then it is a template slot.
1934    Eq(Option<EntityUid>),
1935}
1936
1937impl TemplatePrincipalConstraint {
1938    /// Does this constraint contain a slot?
1939    pub fn has_slot(&self) -> bool {
1940        match self {
1941            Self::Any => false,
1942            Self::In(o) | Self::Eq(o) => o.is_none(),
1943        }
1944    }
1945}
1946
1947/// Head constraint on policy actions.
1948#[derive(Debug, Clone, PartialEq, Eq)]
1949pub enum ActionConstraint {
1950    /// Un-constrained
1951    Any,
1952    /// Must be In the given `EntityUid`
1953    In(Vec<EntityUid>),
1954    /// Must be equal to the given `EntityUid`
1955    Eq(EntityUid),
1956}
1957
1958/// Head constraint on policy resources.
1959#[derive(Debug, Clone, PartialEq, Eq)]
1960pub enum ResourceConstraint {
1961    /// Un-constrained
1962    Any,
1963    /// Must be In the given `EntityUid`
1964    In(EntityUid),
1965    /// Must be equal to the given `EntityUid`
1966    Eq(EntityUid),
1967}
1968
1969/// Head constraint on policy resources for templates.
1970#[derive(Debug, Clone, PartialEq, Eq)]
1971pub enum TemplateResourceConstraint {
1972    /// Un-constrained
1973    Any,
1974    /// Must be In the given `EntityUid`.
1975    /// If [`None`], then it is a template slot.
1976    In(Option<EntityUid>),
1977    /// Must be equal to the given `EntityUid`.
1978    /// If [`None`], then it is a template slot.
1979    Eq(Option<EntityUid>),
1980}
1981
1982impl TemplateResourceConstraint {
1983    /// Does this constraint contain a slot?
1984    pub fn has_slot(&self) -> bool {
1985        match self {
1986            Self::Any => false,
1987            Self::In(o) | Self::Eq(o) => o.is_none(),
1988        }
1989    }
1990}
1991
1992/// Unique Ids assigned to policies and templates
1993#[repr(transparent)]
1994#[derive(Debug, PartialEq, Eq, Clone, Hash, Serialize, Deserialize, RefCast)]
1995pub struct PolicyId(ast::PolicyID);
1996
1997impl FromStr for PolicyId {
1998    type Err = ParseErrors;
1999
2000    /// Create a `PolicyId` from a string. Currently always returns `Ok()`.
2001    fn from_str(id: &str) -> Result<Self, Self::Err> {
2002        Ok(Self(ast::PolicyID::from_string(id)))
2003    }
2004}
2005
2006impl std::fmt::Display for PolicyId {
2007    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2008        write!(f, "{}", self.0)
2009    }
2010}
2011
2012/// Structure for a `Policy`. Includes both static policies and template-linked policies.
2013#[derive(Debug, Clone)]
2014pub struct Policy {
2015    /// AST representation of the policy, used for most operations.
2016    /// In particular, the `ast` contains the authoritative `PolicyId` for the policy.
2017    ast: ast::Policy,
2018    /// Some "lossless" representation of the policy, whichever is most
2019    /// convenient to provide (and can be provided with the least overhead).
2020    /// This is used just for `to_json()`.
2021    /// We can't just derive this on-demand from `ast`, because the AST is lossy:
2022    /// we can't reconstruct an accurate CST/EST/policy-text from the AST, but
2023    /// we can from the EST (modulo whitespace and a few other things like the
2024    /// order of annotations).
2025    lossless: LosslessPolicy,
2026}
2027
2028impl PartialEq for Policy {
2029    fn eq(&self, other: &Self) -> bool {
2030        // eq is based on just the `ast`
2031        self.ast.eq(&other.ast)
2032    }
2033}
2034impl Eq for Policy {}
2035
2036impl Policy {
2037    /// Get the `PolicyId` of the `Template` this is linked to.
2038    /// If this is a static policy, this will return `None`.
2039    pub fn template_id(&self) -> Option<&PolicyId> {
2040        if self.is_static() {
2041            None
2042        } else {
2043            Some(PolicyId::ref_cast(self.ast.template().id()))
2044        }
2045    }
2046
2047    /// Get the `Effect` (`Permit` or `Forbid`) for this instance
2048    pub fn effect(&self) -> Effect {
2049        self.ast.effect()
2050    }
2051
2052    /// Get an annotation value of this template-linked or static policy
2053    pub fn annotation(&self, key: impl AsRef<str>) -> Option<&str> {
2054        self.ast
2055            .annotation(&key.as_ref().parse().ok()?)
2056            .map(smol_str::SmolStr::as_str)
2057    }
2058
2059    /// Iterate through annotation data of this template-linked or static policy
2060    pub fn annotations(&self) -> impl Iterator<Item = (&str, &str)> {
2061        self.ast
2062            .annotations()
2063            .map(|(k, v)| (k.as_ref(), v.as_str()))
2064    }
2065
2066    /// Get the `PolicyId` for this template-linked or static policy
2067    pub fn id(&self) -> &PolicyId {
2068        PolicyId::ref_cast(self.ast.id())
2069    }
2070
2071    /// Clone this `Policy` with a new `PolicyId`
2072    #[must_use]
2073    pub fn new_id(&self, id: PolicyId) -> Self {
2074        Self {
2075            ast: self.ast.new_id(id.0),
2076            lossless: self.lossless.clone(), // Lossless representation doesn't include the `PolicyId`
2077        }
2078    }
2079
2080    /// Returns `true` if this is a static policy, `false` otherwise.
2081    pub fn is_static(&self) -> bool {
2082        self.ast.is_static()
2083    }
2084
2085    /// Get the head constraint on this policy's principal
2086    pub fn principal_constraint(&self) -> PrincipalConstraint {
2087        let slot_id = ast::SlotId::principal();
2088        match self.ast.template().principal_constraint().as_inner() {
2089            ast::PrincipalOrResourceConstraint::Any => PrincipalConstraint::Any,
2090            ast::PrincipalOrResourceConstraint::In(eref) => {
2091                PrincipalConstraint::In(self.convert_entity_reference(eref, slot_id).clone())
2092            }
2093            ast::PrincipalOrResourceConstraint::Eq(eref) => {
2094                PrincipalConstraint::Eq(self.convert_entity_reference(eref, slot_id).clone())
2095            }
2096        }
2097    }
2098
2099    /// Get the head constraint on this policy's action
2100    pub fn action_constraint(&self) -> ActionConstraint {
2101        // Clone the data from Core to be consistant with the other constraints
2102        // INVARIANT: all of the EntityUids come from a policy, which must have Concrete EntityUids
2103        match self.ast.template().action_constraint() {
2104            ast::ActionConstraint::Any => ActionConstraint::Any,
2105            ast::ActionConstraint::In(ids) => ActionConstraint::In(
2106                ids.iter()
2107                    .map(|euid| EntityUid::ref_cast(euid.as_ref()))
2108                    .cloned()
2109                    .collect(),
2110            ),
2111            ast::ActionConstraint::Eq(id) => ActionConstraint::Eq(EntityUid::ref_cast(id).clone()),
2112        }
2113    }
2114
2115    /// Get the head constraint on this policy's resource
2116    pub fn resource_constraint(&self) -> ResourceConstraint {
2117        let slot_id = ast::SlotId::resource();
2118        match self.ast.template().resource_constraint().as_inner() {
2119            ast::PrincipalOrResourceConstraint::Any => ResourceConstraint::Any,
2120            ast::PrincipalOrResourceConstraint::In(eref) => {
2121                ResourceConstraint::In(self.convert_entity_reference(eref, slot_id).clone())
2122            }
2123            ast::PrincipalOrResourceConstraint::Eq(eref) => {
2124                ResourceConstraint::Eq(self.convert_entity_reference(eref, slot_id).clone())
2125            }
2126        }
2127    }
2128
2129    /// To avoid panicking, this function may only be called when `slot` is the
2130    /// `SlotId` corresponding to the scope constraint from which the entity
2131    /// reference `r` was extracted. I.e., If `r` is taken from the principal
2132    /// scope constraint, `slot` must be `?principal`. This ensures that the
2133    /// `SlotId` exists in the policy (and therefore the slot environment map)
2134    /// whenever the `EntityReference` `r` is the Slot variant.
2135    fn convert_entity_reference<'a>(
2136        &'a self,
2137        r: &'a ast::EntityReference,
2138        slot: ast::SlotId,
2139    ) -> &'a EntityUid {
2140        match r {
2141            // INVARIANT: this comes from policy source, so must be concrete
2142            ast::EntityReference::EUID(euid) => EntityUid::ref_cast(euid),
2143            // PANIC SAFETY: This `unwrap` here is safe due the invariant (values total map) on policies.
2144            #[allow(clippy::unwrap_used)]
2145            ast::EntityReference::Slot => EntityUid::ref_cast(self.ast.env().get(&slot).unwrap()),
2146        }
2147    }
2148
2149    /// Parse a single policy.
2150    /// If `id` is Some, the policy will be given that Policy Id.
2151    /// If `id` is None, then "policy0" will be used.
2152    /// The behavior around None may change in the future.
2153    pub fn parse(id: Option<String>, policy_src: impl AsRef<str>) -> Result<Self, ParseErrors> {
2154        let inline_ast = parser::parse_policy(id, policy_src.as_ref())?;
2155        let (_, ast) = ast::Template::link_static_policy(inline_ast);
2156        Ok(Self {
2157            ast,
2158            lossless: LosslessPolicy::policy_or_template_text(policy_src.as_ref()),
2159        })
2160    }
2161
2162    /// Create a `Policy` from its JSON representation.
2163    /// If `id` is Some, the policy will be given that Policy Id.
2164    /// If `id` is None, then "JSON policy" will be used.
2165    /// The behavior around None may change in the future.
2166    ///
2167    /// ```
2168    /// use cedar_policy::{Policy, PolicyId};
2169    /// use std::str::FromStr;
2170    ///
2171    /// let data : serde_json::Value = serde_json::json!(
2172    ///        {
2173    ///            "effect":"permit",
2174    ///            "principal":{
2175    ///            "op":"==",
2176    ///            "entity":{
2177    ///                "type":"User",
2178    ///                "id":"bob"
2179    ///            }
2180    ///            },
2181    ///            "action":{
2182    ///            "op":"==",
2183    ///            "entity":{
2184    ///                "type":"Action",
2185    ///                "id":"view"
2186    ///            }
2187    ///            },
2188    ///            "resource":{
2189    ///            "op":"==",
2190    ///            "entity":{
2191    ///                "type":"Album",
2192    ///                "id":"trip"
2193    ///            }
2194    ///            },
2195    ///            "conditions":[
2196    ///            {
2197    ///                "kind":"when",
2198    ///                "body":{
2199    ///                   ">":{
2200    ///                        "left":{
2201    ///                        ".":{
2202    ///                            "left":{
2203    ///                                "Var":"principal"
2204    ///                            },
2205    ///                            "attr":"age"
2206    ///                        }
2207    ///                        },
2208    ///                        "right":{
2209    ///                        "Value":18
2210    ///                        }
2211    ///                    }
2212    ///                }
2213    ///            }
2214    ///            ]
2215    ///        }
2216    /// );
2217    /// let policy = Policy::from_json(None, data).unwrap();
2218    /// let src = r#"
2219    ///   permit(
2220    ///     principal == User::"bob",
2221    ///     action == Action::"view",
2222    ///     resource == Album::"trip"
2223    ///   )
2224    ///   when { principal.age > 18 };"#;
2225    /// let expected_output = Policy::parse(None, src).unwrap();
2226    /// assert_eq!(policy.to_string(), expected_output.to_string());
2227    /// ```
2228    pub fn from_json(
2229        id: Option<PolicyId>,
2230        json: serde_json::Value,
2231    ) -> Result<Self, cedar_policy_core::est::EstToAstError> {
2232        let est: est::Policy =
2233            serde_json::from_value(json).map_err(JsonDeserializationError::Serde)?;
2234        Ok(Self {
2235            ast: est.clone().try_into_ast_policy(id.map(|id| id.0))?,
2236            lossless: LosslessPolicy::Est(est),
2237        })
2238    }
2239
2240    /// Get the JSON representation of this `Policy`.
2241    ///  ```
2242    /// use cedar_policy::Policy;
2243    /// let src = r#"
2244    ///   permit(
2245    ///     principal == User::"bob",
2246    ///     action == Action::"view",
2247    ///     resource == Album::"trip"
2248    ///   )
2249    ///   when { principal.age > 18 };"#;
2250
2251    /// let policy = Policy::parse(None, src).unwrap();
2252    /// println!("{}", policy);
2253    /// // convert the policy to JSON
2254    /// let json = policy.to_json().unwrap();
2255    /// println!("{}", json);
2256    /// assert_eq!(policy.to_string(), Policy::from_json(None, json).unwrap().to_string());
2257    /// ```
2258    pub fn to_json(&self) -> Result<serde_json::Value, impl std::error::Error> {
2259        let est = self.lossless.est()?;
2260        let json = serde_json::to_value(est)?;
2261        Ok::<_, PolicyToJsonError>(json)
2262    }
2263
2264    /// Create a `Policy` from its AST representation only. The `LosslessPolicy`
2265    /// will reflect the AST structure. When possible, don't use this method and
2266    /// create the `Policy` from the policy text, CST, or EST instead, as the
2267    /// conversion to AST is lossy. ESTs for policies generated by this method
2268    /// will reflect the AST and not the original policy syntax.
2269    #[cfg_attr(not(feature = "partial-eval"), allow(unused))]
2270    fn from_ast(ast: ast::Policy) -> Self {
2271        let text = ast.to_string(); // assume that pretty-printing is faster than `est::Policy::from(ast.clone())`; is that true?
2272        Self {
2273            ast,
2274            lossless: LosslessPolicy::policy_or_template_text(text),
2275        }
2276    }
2277}
2278
2279impl std::fmt::Display for Policy {
2280    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2281        self.ast.fmt(f)
2282    }
2283}
2284
2285impl FromStr for Policy {
2286    type Err = ParseErrors;
2287    /// Create a policy
2288    ///
2289    /// Important note: Policies have ids, but this interface does not
2290    /// allow them to be set. It will use the default "policy0", which
2291    /// may cause id conflicts if not handled. Use `Policy::parse` to set
2292    /// the id when parsing, or `Policy::new_id` to clone a policy with
2293    /// a new id.
2294    fn from_str(policy: &str) -> Result<Self, Self::Err> {
2295        Self::parse(None, policy)
2296    }
2297}
2298
2299/// See comments on `Policy` and `Template`.
2300///
2301/// This structure can be used for static policies, linked policies, and templates.
2302#[derive(Debug, Clone)]
2303enum LosslessPolicy {
2304    /// EST representation
2305    Est(est::Policy),
2306    /// Text representation
2307    Text {
2308        /// actual policy text, of the policy or template
2309        text: String,
2310        /// For linked policies, map of slot to UID. Only linked policies have
2311        /// this; static policies and (unlinked) templates have an empty map
2312        /// here
2313        slots: HashMap<ast::SlotId, ast::EntityUID>,
2314    },
2315}
2316
2317impl LosslessPolicy {
2318    /// Create a new `LosslessPolicy` from the text of a policy or template.
2319    fn policy_or_template_text(text: impl Into<String>) -> Self {
2320        Self::Text {
2321            text: text.into(),
2322            slots: HashMap::new(),
2323        }
2324    }
2325
2326    /// Get the EST representation of this static policy, linked policy, or template
2327    fn est(&self) -> Result<est::Policy, PolicyToJsonError> {
2328        match self {
2329            Self::Est(est) => Ok(est.clone()),
2330            Self::Text { text, slots } => {
2331                let est = parser::parse_policy_or_template_to_est(text)?;
2332                if slots.is_empty() {
2333                    Ok(est)
2334                } else {
2335                    let unwrapped_vals = slots.iter().map(|(k, v)| (*k, v.into())).collect();
2336                    Ok(est.link(&unwrapped_vals)?)
2337                }
2338            }
2339        }
2340    }
2341
2342    fn link<'a>(
2343        self,
2344        vals: impl IntoIterator<Item = (ast::SlotId, &'a ast::EntityUID)>,
2345    ) -> Result<Self, est::InstantiationError> {
2346        match self {
2347            Self::Est(est) => {
2348                let unwrapped_est_vals: HashMap<ast::SlotId, entities::EntityUidJSON> =
2349                    vals.into_iter().map(|(k, v)| (k, v.into())).collect();
2350                Ok(Self::Est(est.link(&unwrapped_est_vals)?))
2351            }
2352            Self::Text { text, slots } => {
2353                debug_assert!(
2354                    slots.is_empty(),
2355                    "shouldn't call link() on an already-linked policy"
2356                );
2357                let slots = vals.into_iter().map(|(k, v)| (k, v.clone())).collect();
2358                Ok(Self::Text { text, slots })
2359            }
2360        }
2361    }
2362}
2363
2364/// Errors that can happen when getting the JSON representation of a policy
2365#[derive(Debug, Error)]
2366pub enum PolicyToJsonError {
2367    /// Parse error in the policy text
2368    #[error(transparent)]
2369    Parse(#[from] ParseErrors),
2370    /// For linked policies, error linking the JSON representation
2371    #[error(transparent)]
2372    Link(#[from] est::InstantiationError),
2373    /// Error in the JSON serialization
2374    #[error(transparent)]
2375    Serde(#[from] serde_json::Error),
2376}
2377
2378/// Expressions to be evaluated
2379#[repr(transparent)]
2380#[derive(Debug, Clone, RefCast)]
2381pub struct Expression(ast::Expr);
2382
2383impl Expression {
2384    /// Create an expression representing a literal string.
2385    pub fn new_string(value: String) -> Self {
2386        Self(ast::Expr::val(value))
2387    }
2388
2389    /// Create an expression representing a literal bool.
2390    pub fn new_bool(value: bool) -> Self {
2391        Self(ast::Expr::val(value))
2392    }
2393
2394    /// Create an expression representing a literal long.
2395    pub fn new_long(value: i64) -> Self {
2396        Self(ast::Expr::val(value))
2397    }
2398
2399    /// Create an expression representing a record.
2400    pub fn new_record(fields: impl IntoIterator<Item = (String, Self)>) -> Self {
2401        Self(ast::Expr::record(
2402            fields.into_iter().map(|(k, v)| (SmolStr::from(k), v.0)),
2403        ))
2404    }
2405
2406    /// Create an expression representing a Set.
2407    pub fn new_set(values: impl IntoIterator<Item = Self>) -> Self {
2408        Self(ast::Expr::set(values.into_iter().map(|v| v.0)))
2409    }
2410}
2411
2412impl FromStr for Expression {
2413    type Err = ParseErrors;
2414
2415    /// create an Expression using Cedar syntax
2416    fn from_str(expression: &str) -> Result<Self, Self::Err> {
2417        ast::Expr::from_str(expression).map(Expression)
2418    }
2419}
2420
2421/// "Restricted" expressions are used for attribute values and `context`.
2422///
2423/// Restricted expressions can contain only the following:
2424///   - bool, int, and string literals
2425///   - literal `EntityUid`s such as `User::"alice"`
2426///   - extension function calls, where the arguments must be other things
2427///       on this list
2428///   - set and record literals, where the values must be other things on
2429///       this list
2430///
2431/// That means the following are not allowed in restricted expressions:
2432///   - `principal`, `action`, `resource`, `context`
2433///   - builtin operators and functions, including `.`, `in`, `has`, `like`,
2434///       `.contains()`
2435///   - if-then-else expressions
2436#[repr(transparent)]
2437#[derive(Debug, Clone, RefCast)]
2438pub struct RestrictedExpression(ast::RestrictedExpr);
2439
2440impl RestrictedExpression {
2441    /// Create an expression representing a literal string.
2442    pub fn new_string(value: String) -> Self {
2443        Self(ast::RestrictedExpr::val(value))
2444    }
2445
2446    /// Create an expression representing a literal bool.
2447    pub fn new_bool(value: bool) -> Self {
2448        Self(ast::RestrictedExpr::val(value))
2449    }
2450
2451    /// Create an expression representing a literal long.
2452    pub fn new_long(value: i64) -> Self {
2453        Self(ast::RestrictedExpr::val(value))
2454    }
2455
2456    /// Create an expression representing a record.
2457    pub fn new_record(fields: impl IntoIterator<Item = (String, Self)>) -> Self {
2458        Self(ast::RestrictedExpr::record(
2459            fields.into_iter().map(|(k, v)| (SmolStr::from(k), v.0)),
2460        ))
2461    }
2462
2463    /// Create an expression representing a Set.
2464    pub fn new_set(values: impl IntoIterator<Item = Self>) -> Self {
2465        Self(ast::RestrictedExpr::set(values.into_iter().map(|v| v.0)))
2466    }
2467}
2468
2469impl FromStr for RestrictedExpression {
2470    type Err = ParseErrors;
2471
2472    /// create a `RestrictedExpression` using Cedar syntax
2473    fn from_str(expression: &str) -> Result<Self, Self::Err> {
2474        ast::RestrictedExpr::from_str(expression).map(RestrictedExpression)
2475    }
2476}
2477
2478/// Builder for a [`Request`]
2479///
2480/// Note that you can create the `EntityUid`s using `.parse()` on any
2481/// string (via the `FromStr` implementation for `EntityUid`).
2482/// The principal, action, and resource fields are optional to support
2483/// the case where these fields do not contribute to authorization
2484/// decisions (e.g., because they are not used in your policies).
2485/// If any of the fields are `None`, we will automatically generate
2486/// a unique entity UID that is not equal to any UID in the store.
2487///
2488/// The default for principal, action and resource fields is Unknown.
2489#[cfg(feature = "partial-eval")]
2490#[derive(Debug, Default)]
2491pub struct RequestBuilder {
2492    principal: Option<ast::EntityUIDEntry>,
2493    action: Option<ast::EntityUIDEntry>,
2494    resource: Option<ast::EntityUIDEntry>,
2495    context: Option<ast::Context>,
2496}
2497
2498#[cfg(feature = "partial-eval")]
2499impl RequestBuilder {
2500    /// Set the principal
2501    pub fn principal(self, principal: Option<EntityUid>) -> Self {
2502        Self {
2503            principal: Some(match principal {
2504                Some(p) => ast::EntityUIDEntry::concrete(p.0),
2505                None => ast::EntityUIDEntry::concrete(ast::EntityUID::unspecified_from_eid(
2506                    ast::Eid::new("principal"),
2507                )),
2508            }),
2509            ..self
2510        }
2511    }
2512
2513    /// Set the action
2514    pub fn action(self, action: Option<EntityUid>) -> Self {
2515        Self {
2516            action: Some(match action {
2517                Some(a) => ast::EntityUIDEntry::concrete(a.0),
2518                None => ast::EntityUIDEntry::concrete(ast::EntityUID::unspecified_from_eid(
2519                    ast::Eid::new("action"),
2520                )),
2521            }),
2522            ..self
2523        }
2524    }
2525
2526    /// Set the resource
2527    pub fn resource(self, resource: Option<EntityUid>) -> Self {
2528        Self {
2529            resource: Some(match resource {
2530                Some(r) => ast::EntityUIDEntry::concrete(r.0),
2531                None => ast::EntityUIDEntry::concrete(ast::EntityUID::unspecified_from_eid(
2532                    ast::Eid::new("resource"),
2533                )),
2534            }),
2535            ..self
2536        }
2537    }
2538
2539    /// Set the context
2540    pub fn context(self, context: Context) -> Self {
2541        Self {
2542            context: Some(context.0),
2543            ..self
2544        }
2545    }
2546
2547    /// Create the [`Request`]
2548    pub fn build(self) -> Request {
2549        let p = match self.principal {
2550            Some(p) => p,
2551            None => ast::EntityUIDEntry::Unknown,
2552        };
2553        let a = match self.action {
2554            Some(a) => a,
2555            None => ast::EntityUIDEntry::Unknown,
2556        };
2557        let r = match self.resource {
2558            Some(r) => r,
2559            None => ast::EntityUIDEntry::Unknown,
2560        };
2561        Request(ast::Request::new_with_unknowns(p, a, r, self.context))
2562    }
2563}
2564
2565/// Represents the request tuple <P, A, R, C> (see the Cedar design doc).
2566#[repr(transparent)]
2567#[derive(Debug, RefCast)]
2568pub struct Request(pub(crate) ast::Request);
2569
2570impl Request {
2571    /// Create a [`RequestBuilder`]
2572    #[cfg(feature = "partial-eval")]
2573    pub fn builder() -> RequestBuilder {
2574        RequestBuilder::default()
2575    }
2576
2577    /// Create a Request.
2578    ///
2579    /// Note that you can create the `EntityUid`s using `.parse()` on any
2580    /// string (via the `FromStr` implementation for `EntityUid`).
2581    /// The principal, action, and resource fields are optional to support
2582    /// the case where these fields do not contribute to authorization
2583    /// decisions (e.g., because they are not used in your policies).
2584    /// If any of the fields are `None`, we will automatically generate
2585    /// a unique entity UID that is not equal to any UID in the store.
2586    pub fn new(
2587        principal: Option<EntityUid>,
2588        action: Option<EntityUid>,
2589        resource: Option<EntityUid>,
2590        context: Context,
2591    ) -> Self {
2592        let p = match principal {
2593            Some(p) => p.0,
2594            None => ast::EntityUID::unspecified_from_eid(ast::Eid::new("principal")),
2595        };
2596        let a = match action {
2597            Some(a) => a.0,
2598            None => ast::EntityUID::unspecified_from_eid(ast::Eid::new("action")),
2599        };
2600        let r = match resource {
2601            Some(r) => r.0,
2602            None => ast::EntityUID::unspecified_from_eid(ast::Eid::new("resource")),
2603        };
2604        Self(ast::Request::new(p, a, r, context.0))
2605    }
2606
2607    /// Get the principal component of the request. Returns `None` if the principal is
2608    /// "unspecified" (i.e., constructed by passing `None` into the constructor) or
2609    /// "unknown" (i.e., constructed using the partial evaluation APIs).
2610    pub fn principal(&self) -> Option<&EntityUid> {
2611        match self.0.principal() {
2612            ast::EntityUIDEntry::Concrete(euid) => match euid.entity_type() {
2613                // INVARIANT: we ensure Concrete-ness here
2614                ast::EntityType::Concrete(_) => Some(EntityUid::ref_cast(euid.as_ref())),
2615                ast::EntityType::Unspecified => None,
2616            },
2617            ast::EntityUIDEntry::Unknown => None,
2618        }
2619    }
2620
2621    /// Get the action component of the request. Returns `None` if the action is
2622    /// "unspecified" (i.e., constructed by passing `None` into the constructor) or
2623    /// "unknown" (i.e., constructed using the partial evaluation APIs).
2624    pub fn action(&self) -> Option<&EntityUid> {
2625        match self.0.action() {
2626            ast::EntityUIDEntry::Concrete(euid) => match euid.entity_type() {
2627                // INVARIANT: we ensure Concrete-ness here
2628                ast::EntityType::Concrete(_) => Some(EntityUid::ref_cast(euid.as_ref())),
2629                ast::EntityType::Unspecified => None,
2630            },
2631            ast::EntityUIDEntry::Unknown => None,
2632        }
2633    }
2634
2635    /// Get the resource component of the request. Returns `None` if the resource is
2636    /// "unspecified" (i.e., constructed by passing `None` into the constructor) or
2637    /// "unknown" (i.e., constructed using the partial evaluation APIs).
2638    pub fn resource(&self) -> Option<&EntityUid> {
2639        match self.0.resource() {
2640            ast::EntityUIDEntry::Concrete(euid) => match euid.entity_type() {
2641                // INVARIANT: we ensure Concrete-ness here
2642                ast::EntityType::Concrete(_) => Some(EntityUid::ref_cast(euid.as_ref())),
2643                ast::EntityType::Unspecified => None,
2644            },
2645            ast::EntityUIDEntry::Unknown => None,
2646        }
2647    }
2648}
2649
2650/// the Context object for an authorization request
2651#[repr(transparent)]
2652#[derive(Debug, Clone, RefCast)]
2653pub struct Context(ast::Context);
2654
2655impl Context {
2656    /// Create an empty `Context`
2657    /// ```
2658    /// use cedar_policy::Context;
2659    /// let c = Context::empty();
2660    /// // let request: Request = Request::new(Some(principal), Some(action), Some(resource), c);
2661    /// ```
2662    pub fn empty() -> Self {
2663        Self(ast::Context::empty())
2664    }
2665
2666    /// Create a `Context` from a map of key to "restricted expression",
2667    /// or a Vec of `(key, restricted expression)` pairs, or any other iterator
2668    /// of `(key, restricted expression)` pairs.
2669    /// ```
2670    /// use cedar_policy::{Context, RestrictedExpression};
2671    /// use std::collections::HashMap;
2672    /// use std::str::FromStr;
2673    /// # use cedar_policy::{Entities, EntityId, EntityTypeName, EntityUid, Request,PolicySet};
2674    /// let data : serde_json::Value = serde_json::json!({
2675    ///     "sub": "1234",
2676    ///     "groups": {
2677    ///         "1234": {
2678    ///             "group_id": "abcd",
2679    ///             "group_name": "test-group"
2680    ///         }
2681    ///     }
2682    /// });
2683    /// let mut groups: HashMap<String, RestrictedExpression> = HashMap::new();
2684    /// groups.insert("key".to_string(), RestrictedExpression::from_str(&data.to_string()).unwrap());
2685    /// groups.insert("age".to_string(), RestrictedExpression::from_str("18").unwrap());
2686    /// let context = Context::from_pairs(groups);
2687    /// # // create a request
2688    /// # let p_eid = EntityId::from_str("alice").unwrap();
2689    /// # let p_name: EntityTypeName = EntityTypeName::from_str("User").unwrap();
2690    /// # let p = EntityUid::from_type_name_and_id(p_name, p_eid);
2691    /// #
2692    /// # let a_eid = EntityId::from_str("view").unwrap();
2693    /// # let a_name: EntityTypeName = EntityTypeName::from_str("Action").unwrap();
2694    /// # let a = EntityUid::from_type_name_and_id(a_name, a_eid);
2695    /// # let r_eid = EntityId::from_str("trip").unwrap();
2696    /// # let r_name: EntityTypeName = EntityTypeName::from_str("Album").unwrap();
2697    /// # let r = EntityUid::from_type_name_and_id(r_name, r_eid);
2698    /// let request: Request = Request::new(Some(p), Some(a), Some(r), context);
2699    /// ```
2700    pub fn from_pairs(pairs: impl IntoIterator<Item = (String, RestrictedExpression)>) -> Self {
2701        Self(ast::Context::from_pairs(
2702            pairs.into_iter().map(|(k, v)| (SmolStr::from(k), v.0)),
2703        ))
2704    }
2705
2706    /// Create a `Context` from a string containing JSON (which must be a JSON
2707    /// object, not any other JSON type, or you will get an error here).
2708    /// JSON here must use the `__entity` and `__extn` escapes for entity
2709    /// references, extension values, etc.
2710    ///
2711    /// If a `schema` is provided, this will inform the parsing: for instance, it
2712    /// will allow `__entity` and `__extn` escapes to be implicit, and it will error
2713    /// if attributes have the wrong types (e.g., string instead of integer).
2714    /// Since different Actions have different schemas for `Context`, you also
2715    /// must specify the `Action` for schema-based parsing.
2716    /// ```
2717    /// use cedar_policy::{Context, RestrictedExpression};
2718    /// use std::collections::HashMap;
2719    /// use std::str::FromStr;
2720    /// # use cedar_policy::{Entities, EntityId, EntityTypeName, EntityUid, Request,PolicySet};
2721    /// let data =r#"{
2722    ///     "sub": "1234",
2723    ///     "groups": {
2724    ///         "1234": {
2725    ///             "group_id": "abcd",
2726    ///             "group_name": "test-group"
2727    ///         }
2728    ///     }
2729    /// }"#;
2730    /// let context = Context::from_json_str(data, None).unwrap();
2731    /// # // create a request
2732    /// # let p_eid = EntityId::from_str("alice").unwrap();
2733    /// # let p_name: EntityTypeName = EntityTypeName::from_str("User").unwrap();
2734    /// # let p = EntityUid::from_type_name_and_id(p_name, p_eid);
2735    /// #
2736    /// # let a_eid = EntityId::from_str("view").unwrap();
2737    /// # let a_name: EntityTypeName = EntityTypeName::from_str("Action").unwrap();
2738    /// # let a = EntityUid::from_type_name_and_id(a_name, a_eid);
2739    /// # let r_eid = EntityId::from_str("trip").unwrap();
2740    /// # let r_name: EntityTypeName = EntityTypeName::from_str("Album").unwrap();
2741    /// # let r = EntityUid::from_type_name_and_id(r_name, r_eid);
2742    /// let request: Request = Request::new(Some(p), Some(a), Some(r), context);
2743    /// ```
2744    pub fn from_json_str(
2745        json: &str,
2746        schema: Option<(&Schema, &EntityUid)>,
2747    ) -> Result<Self, ContextJsonError> {
2748        let schema = schema
2749            .map(|(s, uid)| Self::get_context_schema(s, uid))
2750            .transpose()?;
2751        let context =
2752            entities::ContextJsonParser::new(schema.as_ref(), Extensions::all_available())
2753                .from_json_str(json)?;
2754        Ok(Self(context))
2755    }
2756
2757    /// Create a `Context` from a `serde_json::Value` (which must be a JSON object,
2758    /// not any other JSON type, or you will get an error here).
2759    /// JSON here must use the `__entity` and `__extn` escapes for entity
2760    /// references, extension values, etc.
2761    ///
2762    /// If a `schema` is provided, this will inform the parsing: for instance, it
2763    /// will allow `__entity` and `__extn` escapes to be implicit, and it will error
2764    /// if attributes have the wrong types (e.g., string instead of integer).
2765    /// Since different Actions have different schemas for `Context`, you also
2766    /// must specify the `Action` for schema-based parsing.
2767    /// ```
2768    /// use cedar_policy::{Context, RestrictedExpression, Schema};
2769    /// use std::collections::HashMap;
2770    /// use std::str::FromStr;
2771    /// # use cedar_policy::{Entities, EntityId, EntityTypeName, EntityUid, Request,PolicySet};
2772    /// let data = serde_json::json!(
2773    /// {
2774    ///     "sub": "1234"
2775    /// });
2776    /// let schema_data =r#"
2777    ///     {
2778    ///       "": {
2779    ///         "entityTypes": {},
2780    ///           "actions": {
2781    ///             "view": {
2782    ///                "appliesTo": {
2783    ///                  "principalTypes": [],
2784    ///                   "resourceTypes": [],
2785    ///                   "context": {
2786    ///                     "type": "Record",
2787    ///                     "attributes": {
2788    ///                       "sub": { "type": "Long" }
2789    ///                     }
2790    ///                   }
2791    ///                 }
2792    ///               }
2793    ///           }
2794    ///       }
2795    ///     }"#;
2796    /// # // create a request
2797    /// # let p_eid = EntityId::from_str("alice").unwrap();
2798    /// # let p_name: EntityTypeName = EntityTypeName::from_str("User").unwrap();
2799    /// # let principal = EntityUid::from_type_name_and_id(p_name, p_eid);
2800    /// let a_eid = EntityId::from_str("view").unwrap();
2801    /// let a_name: EntityTypeName = EntityTypeName::from_str("Action").unwrap();
2802    /// let action = EntityUid::from_type_name_and_id(a_name, a_eid);
2803    /// # let r_eid = EntityId::from_str("trip").unwrap();
2804    /// # let r_name: EntityTypeName = EntityTypeName::from_str("Album").unwrap();
2805    /// # let resource = EntityUid::from_type_name_and_id(r_name, r_eid);
2806    /// let schema = Schema::from_str(schema_data).unwrap();
2807    /// let context = Context::from_json_value(data, Some((&schema, &action))).unwrap();
2808    /// let request: Request = Request::new(Some(principal), Some(action), Some(resource), context);
2809    /// ```
2810    pub fn from_json_value(
2811        json: serde_json::Value,
2812        schema: Option<(&Schema, &EntityUid)>,
2813    ) -> Result<Self, ContextJsonError> {
2814        let schema = schema
2815            .map(|(s, uid)| Self::get_context_schema(s, uid))
2816            .transpose()?;
2817        let context =
2818            entities::ContextJsonParser::new(schema.as_ref(), Extensions::all_available())
2819                .from_json_value(json)?;
2820        Ok(Self(context))
2821    }
2822
2823    /// Create a `Context` from a JSON file.  The JSON file must contain a JSON
2824    /// object, not any other JSON type, or you will get an error here.
2825    /// JSON here must use the `__entity` and `__extn` escapes for entity
2826    /// references, extension values, etc.
2827    ///
2828    /// If a `schema` is provided, this will inform the parsing: for instance, it
2829    /// will allow `__entity` and `__extn` escapes to be implicit, and it will error
2830    /// if attributes have the wrong types (e.g., string instead of integer).
2831    /// Since different Actions have different schemas for `Context`, you also
2832    /// must specify the `Action` for schema-based parsing.
2833    /// ```no_run
2834    /// # use cedar_policy::{Context, RestrictedExpression};
2835    /// # use cedar_policy::{Entities, EntityId, EntityTypeName, EntityUid, Request,PolicySet};
2836    /// # use std::collections::HashMap;
2837    /// # use std::str::FromStr;
2838    /// # use std::fs::File;
2839    /// let mut json = File::open("json_file.txt").expect("failed");
2840    /// let context = Context::from_json_file(&json, None).unwrap();
2841    /// # // create a request
2842    /// # let p_eid = EntityId::from_str("alice").unwrap();
2843    /// # let p_name: EntityTypeName = EntityTypeName::from_str("User").unwrap();
2844    /// # let p = EntityUid::from_type_name_and_id(p_name, p_eid);
2845    /// #
2846    /// # let a_eid = EntityId::from_str("view").unwrap();
2847    /// # let a_name: EntityTypeName = EntityTypeName::from_str("Action").unwrap();
2848    /// # let a = EntityUid::from_type_name_and_id(a_name, a_eid);
2849    /// # let r_eid = EntityId::from_str("trip").unwrap();
2850    /// # let r_name: EntityTypeName = EntityTypeName::from_str("Album").unwrap();
2851    /// # let r = EntityUid::from_type_name_and_id(r_name, r_eid);
2852    /// let request: Request = Request::new(Some(p), Some(a), Some(r), context);
2853    /// ```
2854    pub fn from_json_file(
2855        json: impl std::io::Read,
2856        schema: Option<(&Schema, &EntityUid)>,
2857    ) -> Result<Self, ContextJsonError> {
2858        let schema = schema
2859            .map(|(s, uid)| Self::get_context_schema(s, uid))
2860            .transpose()?;
2861        let context =
2862            entities::ContextJsonParser::new(schema.as_ref(), Extensions::all_available())
2863                .from_json_file(json)?;
2864        Ok(Self(context))
2865    }
2866
2867    /// Internal helper function to convert `(&Schema, &EntityUid)` to `impl ContextSchema`
2868    fn get_context_schema(
2869        schema: &Schema,
2870        action: &EntityUid,
2871    ) -> Result<impl ContextSchema, ContextJsonError> {
2872        schema
2873            .0
2874            .get_context_schema(&action.0)
2875            .ok_or_else(|| ContextJsonError::ActionDoesNotExist {
2876                action: action.clone(),
2877            })
2878    }
2879}
2880
2881/// Error type for parsing `Context` from JSON
2882#[derive(Debug, Error)]
2883pub enum ContextJsonError {
2884    /// Error deserializing the JSON into a Context
2885    #[error(transparent)]
2886    JsonDeserializationError(#[from] JsonDeserializationError),
2887    /// The supplied action doesn't exist in the supplied schema
2888    #[error("Action {action} doesn't exist in the supplied schema")]
2889    ActionDoesNotExist {
2890        /// UID of the action which doesn't exist
2891        action: EntityUid,
2892    },
2893}
2894
2895impl std::fmt::Display for Request {
2896    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2897        write!(f, "{}", self.0)
2898    }
2899}
2900
2901/// Result of Evaluation
2902#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
2903pub enum EvalResult {
2904    /// Boolean value
2905    Bool(bool),
2906    /// Signed integer value
2907    Long(i64),
2908    /// String value
2909    String(String),
2910    /// Entity Uid
2911    EntityUid(EntityUid),
2912    /// A first-class set
2913    Set(Set),
2914    /// A first-class anonymous record
2915    Record(Record),
2916    /// An extension value, currently limited to String results
2917    ExtensionValue(String),
2918    // ExtensionValue(std::sync::Arc<dyn InternalExtensionValue>),
2919}
2920
2921/// Sets of Cedar values
2922#[derive(Debug, Eq, PartialEq, PartialOrd, Ord)]
2923pub struct Set(BTreeSet<EvalResult>);
2924
2925impl Set {
2926    /// Iterate over the members of the set
2927    pub fn iter(&self) -> impl Iterator<Item = &EvalResult> {
2928        self.0.iter()
2929    }
2930
2931    /// Is a given element in the set
2932    pub fn contains(&self, elem: &EvalResult) -> bool {
2933        self.0.contains(elem)
2934    }
2935
2936    /// Get the number of members of the set
2937    pub fn len(&self) -> usize {
2938        self.0.len()
2939    }
2940
2941    /// Test if the set is empty
2942    pub fn is_empty(&self) -> bool {
2943        self.0.is_empty()
2944    }
2945}
2946
2947/// A record of Cedar values
2948#[derive(Debug, Eq, PartialEq, PartialOrd, Ord)]
2949pub struct Record(BTreeMap<String, EvalResult>);
2950
2951impl Record {
2952    /// Iterate over the attribute/value pairs in the record
2953    pub fn iter(&self) -> impl Iterator<Item = (&String, &EvalResult)> {
2954        self.0.iter()
2955    }
2956
2957    /// Check if a given attribute is in the record
2958    pub fn contains_attribute(&self, key: impl AsRef<str>) -> bool {
2959        self.0.contains_key(key.as_ref())
2960    }
2961
2962    /// Get a given attribute from the record
2963    pub fn get(&self, key: impl AsRef<str>) -> Option<&EvalResult> {
2964        self.0.get(key.as_ref())
2965    }
2966
2967    /// Get the number of attributes in the record
2968    pub fn len(&self) -> usize {
2969        self.0.len()
2970    }
2971
2972    /// Test if the record is empty
2973    pub fn is_empty(&self) -> bool {
2974        self.0.is_empty()
2975    }
2976}
2977
2978#[doc(hidden)]
2979impl From<ast::Value> for EvalResult {
2980    fn from(v: ast::Value) -> Self {
2981        match v {
2982            ast::Value::Lit(ast::Literal::Bool(b)) => Self::Bool(b),
2983            ast::Value::Lit(ast::Literal::Long(i)) => Self::Long(i),
2984            ast::Value::Lit(ast::Literal::String(s)) => Self::String(s.to_string()),
2985            ast::Value::Lit(ast::Literal::EntityUID(e)) => {
2986                Self::EntityUid(EntityUid(ast::EntityUID::clone(&e)))
2987            }
2988            ast::Value::Set(s) => Self::Set(Set(s
2989                .authoritative
2990                .iter()
2991                .map(|v| v.clone().into())
2992                .collect())),
2993            ast::Value::Record(r) => Self::Record(Record(
2994                r.iter()
2995                    .map(|(k, v)| (k.to_string(), v.clone().into()))
2996                    .collect(),
2997            )),
2998            ast::Value::ExtensionValue(v) => Self::ExtensionValue(v.to_string()),
2999        }
3000    }
3001}
3002impl std::fmt::Display for EvalResult {
3003    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3004        match self {
3005            Self::Bool(b) => write!(f, "{b}"),
3006            Self::Long(l) => write!(f, "{l}"),
3007            Self::String(s) => write!(f, "\"{}\"", s.escape_debug()),
3008            Self::EntityUid(uid) => write!(f, "{uid}"),
3009            Self::Set(s) => {
3010                write!(f, "[")?;
3011                for (i, ev) in s.iter().enumerate() {
3012                    write!(f, "{ev}")?;
3013                    if (i + 1) < s.len() {
3014                        write!(f, ", ")?;
3015                    }
3016                }
3017                write!(f, "]")?;
3018                Ok(())
3019            }
3020            Self::Record(r) => {
3021                write!(f, "{{")?;
3022                for (i, (k, v)) in r.iter().enumerate() {
3023                    write!(f, "\"{}\": {v}", k.escape_debug())?;
3024                    if (i + 1) < r.len() {
3025                        write!(f, ", ")?;
3026                    }
3027                }
3028                write!(f, "}}")?;
3029                Ok(())
3030            }
3031            Self::ExtensionValue(s) => write!(f, "{s}"),
3032        }
3033    }
3034}
3035
3036/// Evaluates an expression.
3037/// If evaluation results in an error (e.g., attempting to access a non-existent Entity or Record,
3038/// passing the wrong number of arguments to a function etc.), that error is returned as a String
3039pub fn eval_expression(
3040    request: &Request,
3041    entities: &Entities,
3042    expr: &Expression,
3043) -> Result<EvalResult, EvaluationError> {
3044    let all_ext = Extensions::all_available();
3045    let eval = Evaluator::new(&request.0, &entities.0, &all_ext)
3046        .map_err(|e| EvaluationError::StringMessage(e.to_string()))?;
3047    Ok(EvalResult::from(
3048        // Evaluate under the empty slot map, as an expression should not have slots
3049        eval.interpret(&expr.0, &ast::SlotEnv::new())
3050            .map_err(|e| EvaluationError::StringMessage(e.to_string()))?,
3051    ))
3052}
3053
3054#[cfg(test)]
3055#[cfg(feature = "partial-eval")]
3056mod partial_eval_test {
3057    use std::collections::HashSet;
3058
3059    use crate::{PolicyId, PolicySet, ResidualResponse};
3060
3061    #[test]
3062    fn test_pe_response_constructor() {
3063        let p: PolicySet = "permit(principal, action, resource);".parse().unwrap();
3064        let reason: HashSet<PolicyId> = std::iter::once("id1".parse().unwrap()).collect();
3065        let errors: HashSet<String> = std::iter::once("error".to_string()).collect();
3066        let a = ResidualResponse::new(p.clone(), reason.clone(), errors.clone());
3067        assert_eq!(a.diagnostics().errors, errors);
3068        assert_eq!(a.diagnostics().reason, reason);
3069        assert_eq!(a.residuals(), &p);
3070    }
3071}
3072
3073// PANIC SAFETY unit tests
3074#[allow(clippy::panic)]
3075#[cfg(test)]
3076mod entity_uid_tests {
3077    use super::*;
3078
3079    /// building an `EntityUid` from components
3080    #[test]
3081    fn entity_uid_from_parts() {
3082        let entity_id = EntityId::from_str("bobby").expect("failed at constructing EntityId");
3083        let entity_type_name = EntityTypeName::from_str("Chess::Master")
3084            .expect("failed at constructing EntityTypeName");
3085        let euid = EntityUid::from_type_name_and_id(entity_type_name, entity_id);
3086        assert_eq!(euid.id().as_ref(), "bobby");
3087        assert_eq!(euid.type_name().to_string(), "Chess::Master");
3088        assert_eq!(euid.type_name().basename(), "Master");
3089        assert_eq!(euid.type_name().namespace(), "Chess");
3090        assert_eq!(euid.type_name().namespace_components().count(), 1);
3091    }
3092
3093    /// building an `EntityUid` from components, with no namespace
3094    #[test]
3095    fn entity_uid_no_namespace() {
3096        let entity_id = EntityId::from_str("bobby").expect("failed at constructing EntityId");
3097        let entity_type_name =
3098            EntityTypeName::from_str("User").expect("failed at constructing EntityTypeName");
3099        let euid = EntityUid::from_type_name_and_id(entity_type_name, entity_id);
3100        assert_eq!(euid.id().as_ref(), "bobby");
3101        assert_eq!(euid.type_name().to_string(), "User");
3102        assert_eq!(euid.type_name().basename(), "User");
3103        assert_eq!(euid.type_name().namespace(), String::new());
3104        assert_eq!(euid.type_name().namespace_components().count(), 0);
3105    }
3106
3107    /// building an `EntityUid` from components, with many nested namespaces
3108    #[test]
3109    fn entity_uid_nested_namespaces() {
3110        let entity_id = EntityId::from_str("bobby").expect("failed at constructing EntityId");
3111        let entity_type_name = EntityTypeName::from_str("A::B::C::D::Z")
3112            .expect("failed at constructing EntityTypeName");
3113        let euid = EntityUid::from_type_name_and_id(entity_type_name, entity_id);
3114        assert_eq!(euid.id().as_ref(), "bobby");
3115        assert_eq!(euid.type_name().to_string(), "A::B::C::D::Z");
3116        assert_eq!(euid.type_name().basename(), "Z");
3117        assert_eq!(euid.type_name().namespace(), "A::B::C::D");
3118        assert_eq!(euid.type_name().namespace_components().count(), 4);
3119    }
3120
3121    /// building an `EntityUid` from components, including escapes
3122    #[test]
3123    fn entity_uid_with_escape() {
3124        // EntityId contains some things that look like escapes
3125        let entity_id = EntityId::from_str(r"bobby\'s sister:\nVeronica")
3126            .expect("failed at constructing EntityId");
3127        let entity_type_name = EntityTypeName::from_str("Hockey::Master")
3128            .expect("failed at constructing EntityTypeName");
3129        let euid = EntityUid::from_type_name_and_id(entity_type_name, entity_id);
3130        // these are passed through (no escape interpretation):
3131        //   the EntityId has the literal backslash characters in it
3132        assert_eq!(euid.id().as_ref(), r"bobby\'s sister:\nVeronica");
3133        assert_eq!(euid.type_name().to_string(), "Hockey::Master");
3134        assert_eq!(euid.type_name().basename(), "Master");
3135        assert_eq!(euid.type_name().namespace(), "Hockey");
3136        assert_eq!(euid.type_name().namespace_components().count(), 1);
3137    }
3138
3139    /// building an `EntityUid` from components, including backslashes
3140    #[test]
3141    fn entity_uid_with_backslashes() {
3142        // backslashes preceding a variety of characters
3143        let entity_id =
3144            EntityId::from_str(r#"\ \a \b \' \" \\"#).expect("failed at constructing EntityId");
3145        let entity_type_name =
3146            EntityTypeName::from_str("Test::User").expect("failed at constructing EntityTypeName");
3147        let euid = EntityUid::from_type_name_and_id(entity_type_name, entity_id);
3148        // the backslashes appear the same way in the EntityId
3149        assert_eq!(euid.id().as_ref(), r#"\ \a \b \' \" \\"#);
3150        assert_eq!(euid.type_name().to_string(), "Test::User");
3151    }
3152
3153    /// building an `EntityUid` from components, including single and double quotes (and backslashes)
3154    #[test]
3155    fn entity_uid_with_quotes() {
3156        let euid: EntityUid = EntityUid::from_type_name_and_id(
3157            EntityTypeName::from_str("Test::User").unwrap(),
3158            EntityId::from_str(r#"b'ob"by\'s sis\"ter"#).unwrap(),
3159        );
3160        // EntityId is passed through (no escape interpretation):
3161        //   the EntityId has all the same literal characters in it
3162        assert_eq!(euid.id().as_ref(), r#"b'ob"by\'s sis\"ter"#);
3163        assert_eq!(euid.type_name().to_string(), r"Test::User");
3164    }
3165
3166    /// building an `EntityUid` from components, including whitespace in various places
3167    #[test]
3168    fn entity_uid_with_whitespace() {
3169        EntityTypeName::from_str("A ::   B::C").expect_err("should fail due to RFC 9");
3170        EntityTypeName::from_str(" A :: B\n::C \n  ::D\n").expect_err("should fail due to RFC 9");
3171
3172        // but embedded whitespace should be OK when parsing an actual policy
3173        let policy = Policy::from_str(
3174            r#"permit(principal == A ::   B::C :: " hi there are spaces ", action, resource);"#,
3175        )
3176        .expect("should succeed, see RFC 9");
3177        let euid = match policy.principal_constraint() {
3178            PrincipalConstraint::Eq(euid) => euid,
3179            _ => panic!("expected Eq constraint"),
3180        };
3181        assert_eq!(euid.id().as_ref(), " hi there are spaces ");
3182        assert_eq!(euid.type_name().to_string(), "A::B::C"); // expect to have been normalized
3183        assert_eq!(euid.type_name().basename(), "C");
3184        assert_eq!(euid.type_name().namespace(), "A::B");
3185        assert_eq!(euid.type_name().namespace_components().count(), 2);
3186
3187        let policy = Policy::from_str(
3188            r#"
3189permit(principal ==  A :: B
3190    ::C
3191    :: D
3192    ::  " hi there are
3193    spaces and
3194    newlines ", action, resource);"#,
3195        )
3196        .expect("should succeed, see RFC 9");
3197        let euid = match policy.principal_constraint() {
3198            PrincipalConstraint::Eq(euid) => euid,
3199            _ => panic!("expected Eq constraint"),
3200        };
3201        assert_eq!(
3202            euid.id().as_ref(),
3203            " hi there are\n    spaces and\n    newlines "
3204        );
3205        assert_eq!(euid.type_name().to_string(), "A::B::C::D"); // expect to have been normalized
3206        assert_eq!(euid.type_name().basename(), "D");
3207        assert_eq!(euid.type_name().namespace(), "A::B::C");
3208        assert_eq!(euid.type_name().namespace_components().count(), 3);
3209    }
3210
3211    #[test]
3212    fn malformed_entity_type_name_should_fail() {
3213        let result = EntityTypeName::from_str("I'm an invalid name");
3214
3215        assert!(matches!(result, Err(ParseErrors(_))));
3216        let error = result.err().unwrap();
3217        assert!(error.to_string().contains("invalid token"));
3218    }
3219
3220    /// parsing an `EntityUid` from string
3221    #[test]
3222    fn parse_euid() {
3223        let parsed_eid: EntityUid = r#"Test::User::"bobby""#.parse().expect("Failed to parse");
3224        assert_eq!(parsed_eid.id().as_ref(), r"bobby");
3225        assert_eq!(parsed_eid.type_name().to_string(), r"Test::User");
3226    }
3227
3228    /// parsing an `EntityUid` from string, including escapes
3229    #[test]
3230    fn parse_euid_with_escape() {
3231        // the EntityUid string has an escaped single-quote and escaped double-quote
3232        let parsed_eid: EntityUid = r#"Test::User::"b\'ob\"by""#.parse().expect("Failed to parse");
3233        // the escapes were interpreted:
3234        //   the EntityId has single-quote and double-quote characters (but no backslash characters)
3235        assert_eq!(parsed_eid.id().as_ref(), r#"b'ob"by"#);
3236        assert_eq!(parsed_eid.type_name().to_string(), r"Test::User");
3237    }
3238
3239    /// parsing an `EntityUid` from string, including both escaped and unescaped single-quotes
3240    #[test]
3241    fn parse_euid_single_quotes() {
3242        // the EntityUid string has an unescaped and escaped single-quote
3243        let euid_str = r#"Test::User::"b'obby\'s sister""#;
3244        EntityUid::from_str(euid_str).expect_err("Should fail, not normalized -- see RFC 9");
3245        // but this should be accepted in an actual policy
3246        let policy_str = "permit(principal == ".to_string() + euid_str + ", action, resource);";
3247        let policy = Policy::from_str(&policy_str).expect("Should parse; see RFC 9");
3248        let parsed_euid = match policy.principal_constraint() {
3249            PrincipalConstraint::Eq(euid) => euid,
3250            _ => panic!("Expected an Eq constraint"),
3251        };
3252        // the escape was interpreted:
3253        //   the EntityId has both single-quote characters (but no backslash characters)
3254        assert_eq!(parsed_euid.id().as_ref(), r"b'obby's sister");
3255        assert_eq!(parsed_euid.type_name().to_string(), r"Test::User");
3256    }
3257
3258    /// parsing an `EntityUid` from string, including whitespace
3259    #[test]
3260    fn parse_euid_whitespace() {
3261        let euid_str = " A ::B :: C:: D \n :: \n E\n :: \"hi\"";
3262        EntityUid::from_str(euid_str).expect_err("Should fail, not normalized -- see RFC 9");
3263        // but this should be accepted in an actual policy
3264        let policy_str = "permit(principal == ".to_string() + euid_str + ", action, resource);";
3265        let policy = Policy::from_str(&policy_str).expect("Should parse; see RFC 9");
3266        let parsed_euid = match policy.principal_constraint() {
3267            PrincipalConstraint::Eq(euid) => euid,
3268            _ => panic!("Expected an Eq constraint"),
3269        };
3270        assert_eq!(parsed_euid.id().as_ref(), "hi");
3271        assert_eq!(parsed_euid.type_name().to_string(), "A::B::C::D::E"); // expect to have been normalized
3272        assert_eq!(parsed_euid.type_name().basename(), "E");
3273        assert_eq!(parsed_euid.type_name().namespace(), "A::B::C::D");
3274        assert_eq!(parsed_euid.type_name().namespace_components().count(), 4);
3275    }
3276
3277    /// test that we can parse the `Display` output of `EntityUid`
3278    #[test]
3279    fn euid_roundtrip() {
3280        let parsed_euid: EntityUid = r#"Test::User::"b\'ob""#.parse().expect("Failed to parse");
3281        assert_eq!(parsed_euid.id().as_ref(), r"b'ob");
3282        let reparsed: EntityUid = format!("{parsed_euid}")
3283            .parse()
3284            .expect("failed to roundtrip");
3285        assert_eq!(reparsed.id().as_ref(), r"b'ob");
3286    }
3287
3288    #[test]
3289    fn accessing_unspecified_entity_returns_none() {
3290        let c = Context::empty();
3291        let request: Request = Request::new(None, None, None, c);
3292        let p = request.principal();
3293        let a = request.action();
3294        let r = request.resource();
3295        assert!(p.is_none());
3296        assert!(a.is_none());
3297        assert!(r.is_none());
3298    }
3299}
3300
3301#[cfg(test)]
3302mod head_constraints_tests {
3303    use super::*;
3304
3305    #[test]
3306    fn principal_constraint_inline() {
3307        let p = Policy::from_str("permit(principal,action,resource);").unwrap();
3308        assert_eq!(p.principal_constraint(), PrincipalConstraint::Any);
3309        let euid = EntityUid::from_strs("T", "a");
3310        assert_eq!(euid.id().as_ref(), "a");
3311        assert_eq!(
3312            euid.type_name(),
3313            &EntityTypeName::from_str("T").expect("Failed to parse EntityTypeName")
3314        );
3315        let p =
3316            Policy::from_str("permit(principal == T::\"a\",action,resource == T::\"b\");").unwrap();
3317        assert_eq!(
3318            p.principal_constraint(),
3319            PrincipalConstraint::Eq(euid.clone())
3320        );
3321        let p = Policy::from_str("permit(principal in T::\"a\",action,resource);").unwrap();
3322        assert_eq!(p.principal_constraint(), PrincipalConstraint::In(euid));
3323    }
3324
3325    #[test]
3326    fn action_constraint_inline() {
3327        let p = Policy::from_str("permit(principal,action,resource);").unwrap();
3328        assert_eq!(p.action_constraint(), ActionConstraint::Any);
3329        let euid = EntityUid::from_strs("NN::N::Action", "a");
3330        assert_eq!(
3331            euid.type_name(),
3332            &EntityTypeName::from_str("NN::N::Action").expect("Failed to parse EntityTypeName")
3333        );
3334        let p = Policy::from_str(
3335            "permit(principal == T::\"b\",action == NN::N::Action::\"a\",resource == T::\"c\");",
3336        )
3337        .unwrap();
3338        assert_eq!(p.action_constraint(), ActionConstraint::Eq(euid.clone()));
3339        let p = Policy::from_str("permit(principal,action in [NN::N::Action::\"a\"],resource);")
3340            .unwrap();
3341        assert_eq!(p.action_constraint(), ActionConstraint::In(vec![euid]));
3342    }
3343
3344    #[test]
3345    fn resource_constraint_inline() {
3346        let p = Policy::from_str("permit(principal,action,resource);").unwrap();
3347        assert_eq!(p.resource_constraint(), ResourceConstraint::Any);
3348        let euid = EntityUid::from_strs("NN::N::T", "a");
3349        assert_eq!(
3350            euid.type_name(),
3351            &EntityTypeName::from_str("NN::N::T").expect("Failed to parse EntityTypeName")
3352        );
3353        let p =
3354            Policy::from_str("permit(principal == T::\"b\",action,resource == NN::N::T::\"a\");")
3355                .unwrap();
3356        assert_eq!(
3357            p.resource_constraint(),
3358            ResourceConstraint::Eq(euid.clone())
3359        );
3360        let p = Policy::from_str("permit(principal,action,resource in NN::N::T::\"a\");").unwrap();
3361        assert_eq!(p.resource_constraint(), ResourceConstraint::In(euid));
3362    }
3363
3364    #[test]
3365    fn principal_constraint_link() {
3366        let p = link("permit(principal,action,resource);", HashMap::new());
3367        assert_eq!(p.principal_constraint(), PrincipalConstraint::Any);
3368        let euid = EntityUid::from_strs("T", "a");
3369        let p = link(
3370            "permit(principal == T::\"a\",action,resource);",
3371            HashMap::new(),
3372        );
3373        assert_eq!(
3374            p.principal_constraint(),
3375            PrincipalConstraint::Eq(euid.clone())
3376        );
3377        let p = link(
3378            "permit(principal in T::\"a\",action,resource);",
3379            HashMap::new(),
3380        );
3381        assert_eq!(
3382            p.principal_constraint(),
3383            PrincipalConstraint::In(euid.clone())
3384        );
3385        let map: HashMap<SlotId, EntityUid> =
3386            std::iter::once((SlotId::principal(), euid.clone())).collect();
3387        let p = link(
3388            "permit(principal in ?principal,action,resource);",
3389            map.clone(),
3390        );
3391        assert_eq!(
3392            p.principal_constraint(),
3393            PrincipalConstraint::In(euid.clone())
3394        );
3395        let p = link("permit(principal == ?principal,action,resource);", map);
3396        assert_eq!(p.principal_constraint(), PrincipalConstraint::Eq(euid));
3397    }
3398
3399    #[test]
3400    fn action_constraint_link() {
3401        let p = link("permit(principal,action,resource);", HashMap::new());
3402        assert_eq!(p.action_constraint(), ActionConstraint::Any);
3403        let euid = EntityUid::from_strs("Action", "a");
3404        let p = link(
3405            "permit(principal,action == Action::\"a\",resource);",
3406            HashMap::new(),
3407        );
3408        assert_eq!(p.action_constraint(), ActionConstraint::Eq(euid.clone()));
3409        let p = link(
3410            "permit(principal,action in [Action::\"a\",Action::\"b\"],resource);",
3411            HashMap::new(),
3412        );
3413        assert_eq!(
3414            p.action_constraint(),
3415            ActionConstraint::In(vec![euid, EntityUid::from_strs("Action", "b"),])
3416        );
3417    }
3418
3419    #[test]
3420    fn resource_constraint_link() {
3421        let p = link("permit(principal,action,resource);", HashMap::new());
3422        assert_eq!(p.resource_constraint(), ResourceConstraint::Any);
3423        let euid = EntityUid::from_strs("T", "a");
3424        let p = link(
3425            "permit(principal,action,resource == T::\"a\");",
3426            HashMap::new(),
3427        );
3428        assert_eq!(
3429            p.resource_constraint(),
3430            ResourceConstraint::Eq(euid.clone())
3431        );
3432        let p = link(
3433            "permit(principal,action,resource in T::\"a\");",
3434            HashMap::new(),
3435        );
3436        assert_eq!(
3437            p.resource_constraint(),
3438            ResourceConstraint::In(euid.clone())
3439        );
3440        let map: HashMap<SlotId, EntityUid> =
3441            std::iter::once((SlotId::resource(), euid.clone())).collect();
3442        let p = link(
3443            "permit(principal,action,resource in ?resource);",
3444            map.clone(),
3445        );
3446        assert_eq!(
3447            p.resource_constraint(),
3448            ResourceConstraint::In(euid.clone())
3449        );
3450        let p = link("permit(principal,action,resource == ?resource);", map);
3451        assert_eq!(p.resource_constraint(), ResourceConstraint::Eq(euid));
3452    }
3453
3454    fn link(src: &str, values: HashMap<SlotId, EntityUid>) -> Policy {
3455        let mut pset = PolicySet::new();
3456        let template = Template::parse(Some("Id".to_string()), src).unwrap();
3457
3458        pset.add_template(template).unwrap();
3459
3460        let link_id = PolicyId::from_str("link").unwrap();
3461        pset.link(PolicyId::from_str("Id").unwrap(), link_id.clone(), values)
3462            .unwrap();
3463        pset.policy(&link_id).unwrap().clone()
3464    }
3465}
3466
3467// PANIC SAFETY unit tests
3468#[allow(clippy::panic)]
3469/// Tests in this module are adapted from Core's `policy_set.rs` tests
3470#[cfg(test)]
3471mod policy_set_tests {
3472    use super::*;
3473    use ast::LinkingError;
3474    use cool_asserts::assert_matches;
3475
3476    #[test]
3477    fn link_conflicts() {
3478        let mut pset = PolicySet::new();
3479        let p1 = Policy::parse(Some("id".into()), "permit(principal,action,resource);")
3480            .expect("Failed to parse");
3481        pset.add(p1).expect("Failed to add");
3482        let template = Template::parse(
3483            Some("t".into()),
3484            "permit(principal == ?principal, action, resource);",
3485        )
3486        .expect("Failed to parse");
3487        pset.add_template(template).expect("Add failed");
3488
3489        let env: HashMap<SlotId, EntityUid> =
3490            std::iter::once((SlotId::principal(), EntityUid::from_strs("Test", "test"))).collect();
3491
3492        let before_link = pset.clone();
3493        let r = pset.link(
3494            PolicyId::from_str("t").unwrap(),
3495            PolicyId::from_str("id").unwrap(),
3496            env,
3497        );
3498
3499        assert_matches!(
3500            r,
3501        Err(PolicySetError::LinkingError(LinkingError::PolicyIdConflict)) => (),
3502        );
3503        assert_eq!(
3504            pset, before_link,
3505            "A failed link shouldn't mutate the policy set"
3506        );
3507    }
3508
3509    #[test]
3510    fn policyset_add() {
3511        let mut pset = PolicySet::new();
3512        let static_policy = Policy::parse(Some("id".into()), "permit(principal,action,resource);")
3513            .expect("Failed to parse");
3514        pset.add(static_policy).expect("Failed to add");
3515
3516        let template = Template::parse(
3517            Some("t".into()),
3518            "permit(principal == ?principal, action, resource);",
3519        )
3520        .expect("Failed to parse");
3521        pset.add_template(template).expect("Failed to add");
3522
3523        let env1: HashMap<SlotId, EntityUid> =
3524            std::iter::once((SlotId::principal(), EntityUid::from_strs("Test", "test1"))).collect();
3525        pset.link(
3526            PolicyId::from_str("t").unwrap(),
3527            PolicyId::from_str("link").unwrap(),
3528            env1,
3529        )
3530        .expect("Failed to link");
3531
3532        let env2: HashMap<SlotId, EntityUid> =
3533            std::iter::once((SlotId::principal(), EntityUid::from_strs("Test", "test2"))).collect();
3534
3535        let err = pset
3536            .link(
3537                PolicyId::from_str("t").unwrap(),
3538                PolicyId::from_str("link").unwrap(),
3539                env2.clone(),
3540            )
3541            .expect_err("Should have failed due to conflict with existing link id");
3542        match err {
3543            PolicySetError::LinkingError(_) => (),
3544            e => panic!("Wrong error: {e}"),
3545        }
3546
3547        pset.link(
3548            PolicyId::from_str("t").unwrap(),
3549            PolicyId::from_str("link2").unwrap(),
3550            env2,
3551        )
3552        .expect("Failed to link");
3553
3554        let template2 = Template::parse(
3555            Some("t".into()),
3556            "forbid(principal, action, resource == ?resource);",
3557        )
3558        .expect("Failed to parse");
3559        pset.add_template(template2)
3560            .expect_err("should have failed due to conflict on template id");
3561        let template2 = Template::parse(
3562            Some("t2".into()),
3563            "forbid(principal, action, resource == ?resource);",
3564        )
3565        .expect("Failed to parse");
3566        pset.add_template(template2)
3567            .expect("Failed to add template");
3568        let env3: HashMap<SlotId, EntityUid> =
3569            std::iter::once((SlotId::resource(), EntityUid::from_strs("Test", "test3"))).collect();
3570
3571        pset.link(
3572            PolicyId::from_str("t").unwrap(),
3573            PolicyId::from_str("unique3").unwrap(),
3574            env3.clone(),
3575        )
3576        .expect_err("should have failed due to conflict on template id");
3577
3578        pset.link(
3579            PolicyId::from_str("t2").unwrap(),
3580            PolicyId::from_str("unique3").unwrap(),
3581            env3,
3582        )
3583        .expect("should succeed with unique ids");
3584    }
3585
3586    #[test]
3587    fn pset_requests() {
3588        let template = Template::parse(
3589            Some("template".into()),
3590            "permit(principal == ?principal, action, resource);",
3591        )
3592        .expect("Template Parse Failure");
3593        let static_policy = Policy::parse(
3594            Some("static".into()),
3595            "permit(principal, action, resource);",
3596        )
3597        .expect("Static parse failure");
3598        let mut pset = PolicySet::new();
3599        pset.add_template(template).unwrap();
3600        pset.add(static_policy).unwrap();
3601        pset.link(
3602            PolicyId::from_str("template").unwrap(),
3603            PolicyId::from_str("linked").unwrap(),
3604            std::iter::once((SlotId::principal(), EntityUid::from_strs("Test", "test"))).collect(),
3605        )
3606        .expect("Link failure");
3607
3608        assert_eq!(pset.templates().count(), 1);
3609        assert_eq!(pset.policies().count(), 2);
3610        assert_eq!(pset.policies().filter(|p| p.is_static()).count(), 1);
3611
3612        assert_eq!(
3613            pset.template(&"template".parse().unwrap())
3614                .expect("lookup failed")
3615                .id(),
3616            &"template".parse().unwrap()
3617        );
3618        assert_eq!(
3619            pset.policy(&"static".parse().unwrap())
3620                .expect("lookup failed")
3621                .id(),
3622            &"static".parse().unwrap()
3623        );
3624        assert_eq!(
3625            pset.policy(&"linked".parse().unwrap())
3626                .expect("lookup failed")
3627                .id(),
3628            &"linked".parse().unwrap()
3629        );
3630    }
3631
3632    #[test]
3633    fn link_static_policy() {
3634        // Linking the `PolicyId` of a static policy should not be allowed.
3635        // Attempting it should cause an `ExpectedTemplate` error.
3636        let static_policy = Policy::parse(
3637            Some("static".into()),
3638            "permit(principal, action, resource);",
3639        )
3640        .expect("Static parse failure");
3641        let mut pset = PolicySet::new();
3642        pset.add(static_policy).unwrap();
3643
3644        let before_link = pset.clone();
3645        let result = pset.link(
3646            PolicyId::from_str("static").unwrap(),
3647            PolicyId::from_str("linked").unwrap(),
3648            HashMap::new(),
3649        );
3650        assert_matches!(result, Err(PolicySetError::ExpectedTemplate));
3651        assert_eq!(
3652            pset, before_link,
3653            "A failed link shouldn't mutate the policy set"
3654        );
3655    }
3656
3657    #[test]
3658    fn link_linked_policy() {
3659        let template = Template::parse(
3660            Some("template".into()),
3661            "permit(principal == ?principal, action, resource);",
3662        )
3663        .expect("Template Parse Failure");
3664        let mut pset = PolicySet::new();
3665        pset.add_template(template).unwrap();
3666
3667        pset.link(
3668            PolicyId::from_str("template").unwrap(),
3669            PolicyId::from_str("linked").unwrap(),
3670            std::iter::once((SlotId::principal(), EntityUid::from_strs("Test", "test"))).collect(),
3671        )
3672        .unwrap();
3673
3674        let before_link = pset.clone();
3675        let result = pset.link(
3676            PolicyId::from_str("linked").unwrap(),
3677            PolicyId::from_str("linked2").unwrap(),
3678            HashMap::new(),
3679        );
3680        assert_matches!(result, Err(PolicySetError::ExpectedTemplate));
3681        assert_eq!(
3682            pset, before_link,
3683            "A failed link shouldn't mutate the policy set"
3684        );
3685    }
3686
3687    #[test]
3688    fn pset_add_conflict() {
3689        let template = Template::parse(
3690            Some("policy0".into()),
3691            "permit(principal == ?principal, action, resource);",
3692        )
3693        .expect("Template Parse Failure");
3694        let mut pset = PolicySet::new();
3695        pset.add_template(template).unwrap();
3696        let env: HashMap<SlotId, EntityUid> =
3697            std::iter::once((SlotId::principal(), EntityUid::from_strs("Test", "test"))).collect();
3698        pset.link(
3699            PolicyId::from_str("policy0").unwrap(),
3700            PolicyId::from_str("policy1").unwrap(),
3701            env,
3702        )
3703        .unwrap();
3704
3705        //fails for template; static
3706        let static_policy = Policy::parse(
3707            Some("policy0".into()),
3708            "permit(principal, action, resource);",
3709        )
3710        .expect("Static parse failure");
3711        assert_matches!(pset.add(static_policy), Err(PolicySetError::AlreadyDefined));
3712
3713        //fails for link; static
3714        let static_policy = Policy::parse(
3715            Some("policy1".into()),
3716            "permit(principal, action, resource);",
3717        )
3718        .expect("Static parse failure");
3719        assert_matches!(pset.add(static_policy), Err(PolicySetError::AlreadyDefined));
3720
3721        //fails for static; static
3722        let static_policy = Policy::parse(
3723            Some("policy2".into()),
3724            "permit(principal, action, resource);",
3725        )
3726        .expect("Static parse failure");
3727        pset.add(static_policy.clone()).unwrap();
3728        assert_matches!(pset.add(static_policy), Err(PolicySetError::AlreadyDefined));
3729    }
3730
3731    #[test]
3732    fn pset_add_template_conflict() {
3733        let template = Template::parse(
3734            Some("policy0".into()),
3735            "permit(principal == ?principal, action, resource);",
3736        )
3737        .expect("Template Parse Failure");
3738        let mut pset = PolicySet::new();
3739        pset.add_template(template).unwrap();
3740        let env: HashMap<SlotId, EntityUid> =
3741            std::iter::once((SlotId::principal(), EntityUid::from_strs("Test", "test"))).collect();
3742        pset.link(
3743            PolicyId::from_str("policy0").unwrap(),
3744            PolicyId::from_str("policy3").unwrap(),
3745            env,
3746        )
3747        .unwrap();
3748
3749        //fails for link; template
3750        let template = Template::parse(
3751            Some("policy3".into()),
3752            "permit(principal == ?principal, action, resource);",
3753        )
3754        .expect("Template Parse Failure");
3755        assert_matches!(
3756            pset.add_template(template),
3757            Err(PolicySetError::AlreadyDefined)
3758        );
3759
3760        //fails for template; template
3761        let template = Template::parse(
3762            Some("policy0".into()),
3763            "permit(principal == ?principal, action, resource);",
3764        )
3765        .expect("Template Parse Failure");
3766        assert_matches!(
3767            pset.add_template(template),
3768            Err(PolicySetError::AlreadyDefined)
3769        );
3770
3771        //fails for static; template
3772        let static_policy = Policy::parse(
3773            Some("policy1".into()),
3774            "permit(principal, action, resource);",
3775        )
3776        .expect("Static parse failure");
3777        pset.add(static_policy).unwrap();
3778        let template = Template::parse(
3779            Some("policy1".into()),
3780            "permit(principal == ?principal, action, resource);",
3781        )
3782        .expect("Template Parse Failure");
3783        assert_matches!(
3784            pset.add_template(template),
3785            Err(PolicySetError::AlreadyDefined)
3786        );
3787    }
3788
3789    #[test]
3790    fn pset_link_conflict() {
3791        let template = Template::parse(
3792            Some("policy0".into()),
3793            "permit(principal == ?principal, action, resource);",
3794        )
3795        .expect("Template Parse Failure");
3796        let mut pset = PolicySet::new();
3797        pset.add_template(template).unwrap();
3798        let env: HashMap<SlotId, EntityUid> =
3799            std::iter::once((SlotId::principal(), EntityUid::from_strs("Test", "test"))).collect();
3800
3801        //fails for link; link
3802        pset.link(
3803            PolicyId::from_str("policy0").unwrap(),
3804            PolicyId::from_str("policy3").unwrap(),
3805            env.clone(),
3806        )
3807        .unwrap();
3808        assert_matches!(
3809            pset.link(
3810                PolicyId::from_str("policy0").unwrap(),
3811                PolicyId::from_str("policy3").unwrap(),
3812                env.clone(),
3813            ),
3814            Err(PolicySetError::LinkingError(LinkingError::PolicyIdConflict))
3815        );
3816
3817        //fails for template; link
3818        assert_matches!(
3819            pset.link(
3820                PolicyId::from_str("policy0").unwrap(),
3821                PolicyId::from_str("policy0").unwrap(),
3822                env.clone(),
3823            ),
3824            Err(PolicySetError::LinkingError(LinkingError::PolicyIdConflict))
3825        );
3826
3827        //fails for static; link
3828        let static_policy = Policy::parse(
3829            Some("policy1".into()),
3830            "permit(principal, action, resource);",
3831        )
3832        .expect("Static parse failure");
3833        pset.add(static_policy).unwrap();
3834        assert_matches!(
3835            pset.link(
3836                PolicyId::from_str("policy0").unwrap(),
3837                PolicyId::from_str("policy1").unwrap(),
3838                env,
3839            ),
3840            Err(PolicySetError::LinkingError(LinkingError::PolicyIdConflict))
3841        );
3842    }
3843}
3844
3845#[cfg(test)]
3846mod schema_tests {
3847    use super::*;
3848    use cool_asserts::assert_matches;
3849    use serde_json::json;
3850
3851    /// A minimal test that a valid Schema parses
3852    #[test]
3853    fn valid_schema() {
3854        Schema::from_json_value(json!(
3855        { "": {
3856            "entityTypes": {
3857                "Photo": {
3858                    "memberOfTypes": [ "Album" ],
3859                    "shape": {
3860                        "type": "Record",
3861                        "attributes": {
3862                            "foo": {
3863                                "type": "Boolean",
3864                                "required": false
3865                            }
3866                        }
3867                    }
3868                },
3869                "Album": {
3870                    "memberOfTypes": [ ],
3871                    "shape": {
3872                        "type": "Record",
3873                        "attributes": {
3874                            "foo": {
3875                                "type": "Boolean",
3876                                "required": false
3877                            }
3878                        }
3879                    }
3880                }
3881            },
3882            "actions": {
3883                "view": {
3884                    "appliesTo": {
3885                        "principalTypes": ["Photo", "Album"],
3886                        "resourceTypes": ["Photo"]
3887                    }
3888                }
3889            }
3890        }}))
3891        .expect("schema should be valid");
3892    }
3893
3894    /// Test that an invalid schema returns the appropriate error
3895    #[test]
3896    fn invalid_schema() {
3897        assert_matches!(
3898            Schema::from_json_value(json!(
3899                // Written as a string because duplicate entity types are detected
3900                // by the serde-json string parser.
3901                r#""{"": {
3902                "entityTypes": {
3903                    "Photo": {
3904                        "memberOfTypes": [ "Album" ],
3905                        "shape": {
3906                            "type": "Record",
3907                            "attributes": {
3908                                "foo": {
3909                                    "type": "Boolean",
3910                                    "required": false
3911                                }
3912                            }
3913                        }
3914                    },
3915                    "Album": {
3916                        "memberOfTypes": [ ],
3917                        "shape": {
3918                            "type": "Record",
3919                            "attributes": {
3920                                "foo": {
3921                                    "type": "Boolean",
3922                                    "required": false
3923                                }
3924                            }
3925                        }
3926                    },
3927                    "Photo": {
3928                        "memberOfTypes": [ "Album" ],
3929                        "shape": {
3930                            "type": "Record",
3931                            "attributes": {
3932                                "foo": {
3933                                    "type": "Boolean",
3934                                    "required": false
3935                                }
3936                            }
3937                        }
3938                    }
3939                },
3940                "actions": {
3941                    "view": {
3942                        "appliesTo": {
3943                            "principalTypes": ["Photo", "Album"],
3944                            "resourceTypes": ["Photo"]
3945                        }
3946                    }
3947                }
3948            }}"#
3949            )),
3950            Err(SchemaError::ParseJson(_))
3951        );
3952    }
3953}
3954
3955#[cfg(test)]
3956mod ancestors_tests {
3957    use super::*;
3958
3959    #[test]
3960    fn test_ancestors() {
3961        let a_euid: EntityUid = EntityUid::from_strs("test", "A");
3962        let b_euid: EntityUid = EntityUid::from_strs("test", "b");
3963        let c_euid: EntityUid = EntityUid::from_strs("test", "C");
3964        let a = Entity::new(a_euid.clone(), HashMap::new(), HashSet::new());
3965        let b = Entity::new(
3966            b_euid.clone(),
3967            HashMap::new(),
3968            std::iter::once(a_euid.clone()).collect(),
3969        );
3970        let c = Entity::new(
3971            c_euid.clone(),
3972            HashMap::new(),
3973            std::iter::once(b_euid.clone()).collect(),
3974        );
3975        let es = Entities::from_entities([a, b, c]).unwrap();
3976        let ans = es.ancestors(&c_euid).unwrap().collect::<HashSet<_>>();
3977        assert_eq!(ans.len(), 2);
3978        assert!(ans.contains(&b_euid));
3979        assert!(ans.contains(&a_euid));
3980    }
3981}
3982
3983/// The main unit tests for schema-based parsing live here, as they require both
3984/// the Validator and Core packages working together.
3985///
3986/// (Core has similar tests, but using a stubbed implementation of Schema.)
3987// PANIC SAFETY unit tests
3988#[allow(clippy::panic)]
3989#[cfg(test)]
3990mod schema_based_parsing_tests {
3991    use std::assert_eq;
3992
3993    use super::*;
3994    use cool_asserts::assert_matches;
3995    use serde_json::json;
3996
3997    /// Simple test that exercises a variety of attribute types.
3998    #[test]
3999    #[allow(clippy::too_many_lines)]
4000    #[allow(clippy::cognitive_complexity)]
4001    fn attr_types() {
4002        let schema = Schema::from_json_value(json!(
4003        {"": {
4004            "entityTypes": {
4005                "Employee": {
4006                    "memberOfTypes": [],
4007                    "shape": {
4008                        "type": "Record",
4009                        "attributes": {
4010                            "isFullTime": { "type": "Boolean" },
4011                            "numDirectReports": { "type": "Long" },
4012                            "department": { "type": "String" },
4013                            "manager": { "type": "Entity", "name": "Employee" },
4014                            "hr_contacts": { "type": "Set", "element": {
4015                                "type": "Entity", "name": "HR" } },
4016                            "json_blob": { "type": "Record", "attributes": {
4017                                "inner1": { "type": "Boolean" },
4018                                "inner2": { "type": "String" },
4019                                "inner3": { "type": "Record", "attributes": {
4020                                    "innerinner": { "type": "Entity", "name": "Employee" }
4021                                }}
4022                            }},
4023                            "home_ip": { "type": "Extension", "name": "ipaddr" },
4024                            "work_ip": { "type": "Extension", "name": "ipaddr" },
4025                            "trust_score": { "type": "Extension", "name": "decimal" },
4026                            "tricky": { "type": "Record", "attributes": {
4027                                "type": { "type": "String" },
4028                                "id": { "type": "String" }
4029                            }}
4030                        }
4031                    }
4032                },
4033                "HR": {
4034                    "memberOfTypes": []
4035                }
4036            },
4037            "actions": {
4038                "view": { }
4039            }
4040        }}
4041        ))
4042        .expect("should be a valid schema");
4043
4044        let entitiesjson = json!(
4045            [
4046                {
4047                    "uid": { "type": "Employee", "id": "12UA45" },
4048                    "attrs": {
4049                        "isFullTime": true,
4050                        "numDirectReports": 3,
4051                        "department": "Sales",
4052                        "manager": { "type": "Employee", "id": "34FB87" },
4053                        "hr_contacts": [
4054                            { "type": "HR", "id": "aaaaa" },
4055                            { "type": "HR", "id": "bbbbb" }
4056                        ],
4057                        "json_blob": {
4058                            "inner1": false,
4059                            "inner2": "-*/",
4060                            "inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
4061                        },
4062                        "home_ip": "222.222.222.101",
4063                        "work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
4064                        "trust_score": "5.7",
4065                        "tricky": { "type": "Employee", "id": "34FB87" }
4066                    },
4067                    "parents": []
4068                }
4069            ]
4070        );
4071        // without schema-based parsing, `home_ip` and `trust_score` are
4072        // strings, `manager` and `work_ip` are Records, `hr_contacts` contains
4073        // Records, and `json_blob.inner3.innerinner` is a Record
4074        let parsed = Entities::from_json_value(entitiesjson.clone(), None)
4075            .expect("Should parse without error");
4076        assert_eq!(parsed.iter().count(), 1);
4077        let parsed = parsed
4078            .get(&EntityUid::from_strs("Employee", "12UA45"))
4079            .expect("that should be the employee id");
4080        assert_eq!(
4081            parsed.attr("home_ip"),
4082            Some(Ok(EvalResult::String("222.222.222.101".into())))
4083        );
4084        assert_eq!(
4085            parsed.attr("trust_score"),
4086            Some(Ok(EvalResult::String("5.7".into())))
4087        );
4088        assert!(matches!(
4089            parsed.attr("manager"),
4090            Some(Ok(EvalResult::Record(_)))
4091        ));
4092        assert!(matches!(
4093            parsed.attr("work_ip"),
4094            Some(Ok(EvalResult::Record(_)))
4095        ));
4096        {
4097            let Some(Ok(EvalResult::Set(set))) = parsed.attr("hr_contacts") else {
4098                panic!("expected hr_contacts attr to exist and be a Set")
4099            };
4100            let contact = set.iter().next().expect("should be at least one contact");
4101            assert!(matches!(contact, EvalResult::Record(_)));
4102        };
4103        {
4104            let Some(Ok(EvalResult::Record(rec))) = parsed.attr("json_blob") else {
4105                panic!("expected json_blob attr to exist and be a Record")
4106            };
4107            let inner3 = rec.get("inner3").expect("expected inner3 attr to exist");
4108            let EvalResult::Record(rec) = inner3 else {
4109                panic!("expected inner3 to be a Record")
4110            };
4111            let innerinner = rec
4112                .get("innerinner")
4113                .expect("expected innerinner attr to exist");
4114            assert!(matches!(innerinner, EvalResult::Record(_)));
4115        };
4116        // but with schema-based parsing, we get these other types
4117        let parsed = Entities::from_json_value(entitiesjson, Some(&schema))
4118            .expect("Should parse without error");
4119        assert_eq!(parsed.iter().count(), 1);
4120        let parsed = parsed
4121            .get(&EntityUid::from_strs("Employee", "12UA45"))
4122            .expect("that should be the employee id");
4123        assert_eq!(parsed.attr("isFullTime"), Some(Ok(EvalResult::Bool(true))));
4124        assert_eq!(
4125            parsed.attr("numDirectReports"),
4126            Some(Ok(EvalResult::Long(3)))
4127        );
4128        assert_eq!(
4129            parsed.attr("department"),
4130            Some(Ok(EvalResult::String("Sales".into())))
4131        );
4132        assert_eq!(
4133            parsed.attr("manager"),
4134            Some(Ok(EvalResult::EntityUid(EntityUid::from_strs(
4135                "Employee", "34FB87"
4136            ))))
4137        );
4138        {
4139            let Some(Ok(EvalResult::Set(set))) = parsed.attr("hr_contacts") else {
4140                panic!("expected hr_contacts attr to exist and be a Set")
4141            };
4142            let contact = set.iter().next().expect("should be at least one contact");
4143            assert!(matches!(contact, EvalResult::EntityUid(_)));
4144        };
4145        {
4146            let Some(Ok(EvalResult::Record(rec))) = parsed.attr("json_blob") else {
4147                panic!("expected json_blob attr to exist and be a Record")
4148            };
4149            let inner3 = rec.get("inner3").expect("expected inner3 attr to exist");
4150            let EvalResult::Record(rec) = inner3 else {
4151                panic!("expected inner3 to be a Record")
4152            };
4153            let innerinner = rec
4154                .get("innerinner")
4155                .expect("expected innerinner attr to exist");
4156            assert!(matches!(innerinner, EvalResult::EntityUid(_)));
4157        };
4158        assert_eq!(
4159            parsed.attr("home_ip"),
4160            Some(Ok(EvalResult::ExtensionValue("222.222.222.101/32".into())))
4161        );
4162        assert_eq!(
4163            parsed.attr("work_ip"),
4164            Some(Ok(EvalResult::ExtensionValue("2.2.2.0/24".into())))
4165        );
4166        assert_eq!(
4167            parsed.attr("trust_score"),
4168            Some(Ok(EvalResult::ExtensionValue("5.7000".into())))
4169        );
4170
4171        // simple type mismatch with expected type
4172        let entitiesjson = json!(
4173            [
4174                {
4175                    "uid": { "type": "Employee", "id": "12UA45" },
4176                    "attrs": {
4177                        "isFullTime": true,
4178                        "numDirectReports": "3",
4179                        "department": "Sales",
4180                        "manager": { "type": "Employee", "id": "34FB87" },
4181                        "hr_contacts": [
4182                            { "type": "HR", "id": "aaaaa" },
4183                            { "type": "HR", "id": "bbbbb" }
4184                        ],
4185                        "json_blob": {
4186                            "inner1": false,
4187                            "inner2": "-*/",
4188                            "inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
4189                        },
4190                        "home_ip": "222.222.222.101",
4191                        "work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
4192                        "trust_score": "5.7",
4193                        "tricky": { "type": "Employee", "id": "34FB87" }
4194                    },
4195                    "parents": []
4196                }
4197            ]
4198        );
4199        let err = Entities::from_json_value(entitiesjson, Some(&schema))
4200            .expect_err("should fail due to type mismatch on numDirectReports");
4201        assert!(
4202            err.to_string().contains(r#"in attribute "numDirectReports" on Employee::"12UA45", type mismatch: attribute was expected to have type long, but actually has type string"#),
4203            "actual error message was {err}"
4204        );
4205
4206        // another simple type mismatch with expected type
4207        let entitiesjson = json!(
4208            [
4209                {
4210                    "uid": { "type": "Employee", "id": "12UA45" },
4211                    "attrs": {
4212                        "isFullTime": true,
4213                        "numDirectReports": 3,
4214                        "department": "Sales",
4215                        "manager": "34FB87",
4216                        "hr_contacts": [
4217                            { "type": "HR", "id": "aaaaa" },
4218                            { "type": "HR", "id": "bbbbb" }
4219                        ],
4220                        "json_blob": {
4221                            "inner1": false,
4222                            "inner2": "-*/",
4223                            "inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
4224                        },
4225                        "home_ip": "222.222.222.101",
4226                        "work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
4227                        "trust_score": "5.7",
4228                        "tricky": { "type": "Employee", "id": "34FB87" }
4229                    },
4230                    "parents": []
4231                }
4232            ]
4233        );
4234        let err = Entities::from_json_value(entitiesjson, Some(&schema))
4235            .expect_err("should fail due to type mismatch on manager");
4236        assert!(
4237            err.to_string()
4238                .contains(r#"in attribute "manager" on Employee::"12UA45", expected a literal entity reference, but got: "34FB87""#),
4239            "actual error message was {err}"
4240        );
4241
4242        // type mismatch where we expect a set and get just a single element
4243        let entitiesjson = json!(
4244            [
4245                {
4246                    "uid": { "type": "Employee", "id": "12UA45" },
4247                    "attrs": {
4248                        "isFullTime": true,
4249                        "numDirectReports": 3,
4250                        "department": "Sales",
4251                        "manager": { "type": "Employee", "id": "34FB87" },
4252                        "hr_contacts": { "type": "HR", "id": "aaaaa" },
4253                        "json_blob": {
4254                            "inner1": false,
4255                            "inner2": "-*/",
4256                            "inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
4257                        },
4258                        "home_ip": "222.222.222.101",
4259                        "work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
4260                        "trust_score": "5.7",
4261                        "tricky": { "type": "Employee", "id": "34FB87" }
4262                    },
4263                    "parents": []
4264                }
4265            ]
4266        );
4267        let err = Entities::from_json_value(entitiesjson, Some(&schema))
4268            .expect_err("should fail due to type mismatch on hr_contacts");
4269        assert!(
4270            err.to_string().contains(r#"in attribute "hr_contacts" on Employee::"12UA45", type mismatch: attribute was expected to have type (set of (entity of type HR)), but actually has type record with attributes: ("#),
4271            "actual error message was {err}"
4272        );
4273
4274        // type mismatch where we just get the wrong entity type
4275        let entitiesjson = json!(
4276            [
4277                {
4278                    "uid": { "type": "Employee", "id": "12UA45" },
4279                    "attrs": {
4280                        "isFullTime": true,
4281                        "numDirectReports": 3,
4282                        "department": "Sales",
4283                        "manager": { "type": "HR", "id": "34FB87" },
4284                        "hr_contacts": [
4285                            { "type": "HR", "id": "aaaaa" },
4286                            { "type": "HR", "id": "bbbbb" }
4287                        ],
4288                        "json_blob": {
4289                            "inner1": false,
4290                            "inner2": "-*/",
4291                            "inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
4292                        },
4293                        "home_ip": "222.222.222.101",
4294                        "work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
4295                        "trust_score": "5.7",
4296                        "tricky": { "type": "Employee", "id": "34FB87" }
4297                    },
4298                    "parents": []
4299                }
4300            ]
4301        );
4302        let err = Entities::from_json_value(entitiesjson, Some(&schema))
4303            .expect_err("should fail due to type mismatch on manager");
4304        assert!(
4305            err.to_string().contains(r#"in attribute "manager" on Employee::"12UA45", type mismatch: attribute was expected to have type (entity of type Employee), but actually has type (entity of type HR)"#),
4306            "actual error message was {err}"
4307        );
4308
4309        // type mismatch where we're expecting an extension type and get a
4310        // different extension type
4311        let entitiesjson = json!(
4312            [
4313                {
4314                    "uid": { "type": "Employee", "id": "12UA45" },
4315                    "attrs": {
4316                        "isFullTime": true,
4317                        "numDirectReports": 3,
4318                        "department": "Sales",
4319                        "manager": { "type": "Employee", "id": "34FB87" },
4320                        "hr_contacts": [
4321                            { "type": "HR", "id": "aaaaa" },
4322                            { "type": "HR", "id": "bbbbb" }
4323                        ],
4324                        "json_blob": {
4325                            "inner1": false,
4326                            "inner2": "-*/",
4327                            "inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
4328                        },
4329                        "home_ip": { "fn": "decimal", "arg": "3.33" },
4330                        "work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
4331                        "trust_score": "5.7",
4332                        "tricky": { "type": "Employee", "id": "34FB87" }
4333                    },
4334                    "parents": []
4335                }
4336            ]
4337        );
4338        let err = Entities::from_json_value(entitiesjson, Some(&schema))
4339            .expect_err("should fail due to type mismatch on home_ip");
4340        assert!(
4341            err.to_string().contains(r#"in attribute "home_ip" on Employee::"12UA45", type mismatch: attribute was expected to have type ipaddr, but actually has type decimal"#),
4342            "actual error message was {err}"
4343        );
4344
4345        // missing a record attribute entirely
4346        let entitiesjson = json!(
4347            [
4348                {
4349                    "uid": { "type": "Employee", "id": "12UA45" },
4350                    "attrs": {
4351                        "isFullTime": true,
4352                        "numDirectReports": 3,
4353                        "department": "Sales",
4354                        "manager": { "type": "Employee", "id": "34FB87" },
4355                        "hr_contacts": [
4356                            { "type": "HR", "id": "aaaaa" },
4357                            { "type": "HR", "id": "bbbbb" }
4358                        ],
4359                        "json_blob": {
4360                            "inner1": false,
4361                            "inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
4362                        },
4363                        "home_ip": "222.222.222.101",
4364                        "work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
4365                        "trust_score": "5.7",
4366                        "tricky": { "type": "Employee", "id": "34FB87" }
4367                    },
4368                    "parents": []
4369                }
4370            ]
4371        );
4372        let err = Entities::from_json_value(entitiesjson, Some(&schema))
4373            .expect_err("should fail due to missing attribute \"inner2\"");
4374        assert!(
4375            err.to_string().contains(r#"in attribute "json_blob" on Employee::"12UA45", expected the record to have an attribute "inner2", but it doesn't"#),
4376            "actual error message was {err}"
4377        );
4378
4379        // record attribute has the wrong type
4380        let entitiesjson = json!(
4381            [
4382                {
4383                    "uid": { "type": "Employee", "id": "12UA45" },
4384                    "attrs": {
4385                        "isFullTime": true,
4386                        "numDirectReports": 3,
4387                        "department": "Sales",
4388                        "manager": { "type": "Employee", "id": "34FB87" },
4389                        "hr_contacts": [
4390                            { "type": "HR", "id": "aaaaa" },
4391                            { "type": "HR", "id": "bbbbb" }
4392                        ],
4393                        "json_blob": {
4394                            "inner1": 33,
4395                            "inner2": "-*/",
4396                            "inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
4397                        },
4398                        "home_ip": "222.222.222.101",
4399                        "work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
4400                        "trust_score": "5.7",
4401                        "tricky": { "type": "Employee", "id": "34FB87" }
4402                    },
4403                    "parents": []
4404                }
4405            ]
4406        );
4407        let err = Entities::from_json_value(entitiesjson, Some(&schema))
4408            .expect_err("should fail due to type mismatch on attribute \"inner1\"");
4409        assert!(
4410            err.to_string().contains(r#"in attribute "json_blob" on Employee::"12UA45", type mismatch: attribute was expected to have type record with attributes: "#),
4411            "actual error message was {err}"
4412        );
4413
4414        let entitiesjson = json!(
4415            [
4416                {
4417                    "uid": { "__entity": { "type": "Employee", "id": "12UA45" } },
4418                    "attrs": {
4419                        "isFullTime": true,
4420                        "numDirectReports": 3,
4421                        "department": "Sales",
4422                        "manager": { "__entity": { "type": "Employee", "id": "34FB87" } },
4423                        "hr_contacts": [
4424                            { "type": "HR", "id": "aaaaa" },
4425                            { "type": "HR", "id": "bbbbb" }
4426                        ],
4427                        "json_blob": {
4428                            "inner1": false,
4429                            "inner2": "-*/",
4430                            "inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
4431                        },
4432                        "home_ip": { "__extn": { "fn": "ip", "arg": "222.222.222.101" } },
4433                        "work_ip": { "__extn": { "fn": "ip", "arg": "2.2.2.0/24" } },
4434                        "trust_score": { "__extn": { "fn": "decimal", "arg": "5.7" } },
4435                        "tricky": { "type": "Employee", "id": "34FB87" }
4436                    },
4437                    "parents": []
4438                }
4439            ]
4440        );
4441
4442        Entities::from_json_value(entitiesjson, Some(&schema))
4443            .expect("this version with explicit __entity and __extn escapes should also pass");
4444    }
4445
4446    /// Test that involves namespaced entity types
4447    #[test]
4448    fn namespaces() {
4449        let schema = Schema::from_str(
4450            r#"
4451        {"XYZCorp": {
4452            "entityTypes": {
4453                "Employee": {
4454                    "memberOfTypes": [],
4455                    "shape": {
4456                        "type": "Record",
4457                        "attributes": {
4458                            "isFullTime": { "type": "Boolean" },
4459                            "department": { "type": "String" },
4460                            "manager": {
4461                                "type": "Entity",
4462                                "name": "XYZCorp::Employee"
4463                            }
4464                        }
4465                    }
4466                }
4467            },
4468            "actions": {
4469                "view": {}
4470            }
4471        }}
4472        "#,
4473        )
4474        .expect("should be a valid schema");
4475
4476        let entitiesjson = json!(
4477            [
4478                {
4479                    "uid": { "type": "XYZCorp::Employee", "id": "12UA45" },
4480                    "attrs": {
4481                        "isFullTime": true,
4482                        "department": "Sales",
4483                        "manager": { "type": "XYZCorp::Employee", "id": "34FB87" }
4484                    },
4485                    "parents": []
4486                }
4487            ]
4488        );
4489        let parsed = Entities::from_json_value(entitiesjson, Some(&schema))
4490            .expect("Should parse without error");
4491        assert_eq!(parsed.iter().count(), 1);
4492        let parsed = parsed
4493            .get(&EntityUid::from_strs("XYZCorp::Employee", "12UA45"))
4494            .expect("that should be the employee type and id");
4495        assert_eq!(parsed.attr("isFullTime"), Some(Ok(EvalResult::Bool(true))));
4496        assert_eq!(
4497            parsed.attr("department"),
4498            Some(Ok(EvalResult::String("Sales".into())))
4499        );
4500        assert_eq!(
4501            parsed.attr("manager"),
4502            Some(Ok(EvalResult::EntityUid(EntityUid::from_strs(
4503                "XYZCorp::Employee",
4504                "34FB87"
4505            ))))
4506        );
4507
4508        let entitiesjson = json!(
4509            [
4510                {
4511                    "uid": { "type": "XYZCorp::Employee", "id": "12UA45" },
4512                    "attrs": {
4513                        "isFullTime": true,
4514                        "department": "Sales",
4515                        "manager": { "type": "Employee", "id": "34FB87" }
4516                    },
4517                    "parents": []
4518                }
4519            ]
4520        );
4521        let err = Entities::from_json_value(entitiesjson, Some(&schema))
4522            .expect_err("should fail due to manager being wrong entity type (missing namespace)");
4523        assert!(
4524            err.to_string().contains(r#"in attribute "manager" on XYZCorp::Employee::"12UA45", type mismatch: attribute was expected to have type (entity of type XYZCorp::Employee), but actually has type (entity of type Employee)"#),
4525            "actual error message was {err}"
4526        );
4527    }
4528
4529    /// Test that involves optional attributes
4530    #[test]
4531    fn optional_attrs() {
4532        let schema = Schema::from_str(
4533            r#"
4534        {"": {
4535            "entityTypes": {
4536                "Employee": {
4537                    "memberOfTypes": [],
4538                    "shape": {
4539                        "type": "Record",
4540                        "attributes": {
4541                            "isFullTime": { "type": "Boolean" },
4542                            "department": { "type": "String", "required": false },
4543                            "manager": { "type": "Entity", "name": "Employee" }
4544                        }
4545                    }
4546                }
4547            },
4548            "actions": {
4549                "view": {}
4550            }
4551        }}
4552        "#,
4553        )
4554        .expect("should be a valid schema");
4555
4556        // all good here
4557        let entitiesjson = json!(
4558            [
4559                {
4560                    "uid": { "type": "Employee", "id": "12UA45" },
4561                    "attrs": {
4562                        "isFullTime": true,
4563                        "department": "Sales",
4564                        "manager": { "type": "Employee", "id": "34FB87" }
4565                    },
4566                    "parents": []
4567                }
4568            ]
4569        );
4570        let parsed = Entities::from_json_value(entitiesjson, Some(&schema))
4571            .expect("Should parse without error");
4572        assert_eq!(parsed.iter().count(), 1);
4573
4574        // "department" shouldn't be required
4575        let entitiesjson = json!(
4576            [
4577                {
4578                    "uid": { "type": "Employee", "id": "12UA45" },
4579                    "attrs": {
4580                        "isFullTime": true,
4581                        "manager": { "type": "Employee", "id": "34FB87" }
4582                    },
4583                    "parents": []
4584                }
4585            ]
4586        );
4587        let parsed = Entities::from_json_value(entitiesjson, Some(&schema))
4588            .expect("Should parse without error");
4589        assert_eq!(parsed.iter().count(), 1);
4590    }
4591
4592    /// Test that involves open entities
4593    #[test]
4594    #[should_panic(
4595        expected = "UnsupportedSchemaFeature(\"Records and entities with additional attributes are not yet implemented.\")"
4596    )]
4597    fn open_entities() {
4598        let schema = Schema::from_str(
4599            r#"
4600        {"": {
4601            "entityTypes": {
4602                "Employee": {
4603                    "memberOfTypes": [],
4604                    "shape": {
4605                        "type": "Record",
4606                        "attributes": {
4607                            "isFullTime": { "type": "Boolean" },
4608                            "department": { "type": "String", "required": false },
4609                            "manager": { "type": "Entity", "name": "Employee" }
4610                        },
4611                        "additionalAttributes": true
4612                    }
4613                }
4614            },
4615            "actions": {
4616                "view": {}
4617            }
4618        }}
4619        "#,
4620        )
4621        .expect("should be a valid schema");
4622
4623        // all good here
4624        let entitiesjson = json!(
4625            [
4626                {
4627                    "uid": { "type": "Employee", "id": "12UA45" },
4628                    "attrs": {
4629                        "isFullTime": true,
4630                        "department": "Sales",
4631                        "manager": { "type": "Employee", "id": "34FB87" }
4632                    },
4633                    "parents": []
4634                }
4635            ]
4636        );
4637        let parsed = Entities::from_json_value(entitiesjson, Some(&schema))
4638            .expect("Should parse without error");
4639        assert_eq!(parsed.iter().count(), 1);
4640
4641        // providing another attribute "foobar" should be OK
4642        let entitiesjson = json!(
4643            [
4644                {
4645                    "uid": { "type": "Employee", "id": "12UA45" },
4646                    "attrs": {
4647                        "isFullTime": true,
4648                        "foobar": 234,
4649                        "manager": { "type": "Employee", "id": "34FB87" }
4650                    },
4651                    "parents": []
4652                }
4653            ]
4654        );
4655        let parsed = Entities::from_json_value(entitiesjson, Some(&schema))
4656            .expect("Should parse without error");
4657        assert_eq!(parsed.iter().count(), 1);
4658    }
4659
4660    #[test]
4661    fn schema_sanity_check() {
4662        let src = "{ , .. }";
4663        assert_matches!(Schema::from_str(src), Err(super::SchemaError::ParseJson(_)));
4664    }
4665
4666    #[test]
4667    fn template_constraint_sanity_checks() {
4668        assert!(!TemplatePrincipalConstraint::Any.has_slot());
4669        assert!(!TemplatePrincipalConstraint::In(Some(EntityUid::from_strs("a", "a"))).has_slot());
4670        assert!(!TemplatePrincipalConstraint::Eq(Some(EntityUid::from_strs("a", "a"))).has_slot());
4671        assert!(TemplatePrincipalConstraint::In(None).has_slot());
4672        assert!(TemplatePrincipalConstraint::Eq(None).has_slot());
4673        assert!(!TemplateResourceConstraint::Any.has_slot());
4674        assert!(!TemplateResourceConstraint::In(Some(EntityUid::from_strs("a", "a"))).has_slot());
4675        assert!(!TemplateResourceConstraint::Eq(Some(EntityUid::from_strs("a", "a"))).has_slot());
4676        assert!(TemplateResourceConstraint::In(None).has_slot());
4677        assert!(TemplateResourceConstraint::Eq(None).has_slot());
4678    }
4679
4680    #[test]
4681    fn template_principal_constraints() {
4682        let src = r"
4683            permit(principal, action, resource);
4684        ";
4685        let t = Template::parse(None, src).unwrap();
4686        assert_eq!(t.principal_constraint(), TemplatePrincipalConstraint::Any);
4687
4688        let src = r"
4689            permit(principal == ?principal, action, resource);
4690        ";
4691        let t = Template::parse(None, src).unwrap();
4692        assert_eq!(
4693            t.principal_constraint(),
4694            TemplatePrincipalConstraint::Eq(None)
4695        );
4696
4697        let src = r#"
4698            permit(principal == A::"a", action, resource);
4699        "#;
4700        let t = Template::parse(None, src).unwrap();
4701        assert_eq!(
4702            t.principal_constraint(),
4703            TemplatePrincipalConstraint::Eq(Some(EntityUid::from_strs("A", "a")))
4704        );
4705
4706        let src = r"
4707            permit(principal in ?principal, action, resource);
4708        ";
4709        let t = Template::parse(None, src).unwrap();
4710        assert_eq!(
4711            t.principal_constraint(),
4712            TemplatePrincipalConstraint::In(None)
4713        );
4714
4715        let src = r#"
4716            permit(principal in A::"a", action, resource);
4717        "#;
4718        let t = Template::parse(None, src).unwrap();
4719        assert_eq!(
4720            t.principal_constraint(),
4721            TemplatePrincipalConstraint::In(Some(EntityUid::from_strs("A", "a")))
4722        );
4723    }
4724
4725    #[test]
4726    fn template_action_constraints() {
4727        let src = r"
4728            permit(principal, action, resource);
4729        ";
4730        let t = Template::parse(None, src).unwrap();
4731        assert_eq!(t.action_constraint(), ActionConstraint::Any);
4732
4733        let src = r#"
4734            permit(principal, action == Action::"A", resource);
4735        "#;
4736        let t = Template::parse(None, src).unwrap();
4737        assert_eq!(
4738            t.action_constraint(),
4739            ActionConstraint::Eq(EntityUid::from_strs("Action", "A"))
4740        );
4741
4742        let src = r#"
4743            permit(principal, action in [Action::"A", Action::"B"], resource);
4744        "#;
4745        let t = Template::parse(None, src).unwrap();
4746        assert_eq!(
4747            t.action_constraint(),
4748            ActionConstraint::In(vec![
4749                EntityUid::from_strs("Action", "A"),
4750                EntityUid::from_strs("Action", "B")
4751            ])
4752        );
4753    }
4754
4755    #[test]
4756    fn template_resource_constraints() {
4757        let src = r"
4758            permit(principal, action, resource);
4759        ";
4760        let t = Template::parse(None, src).unwrap();
4761        assert_eq!(t.resource_constraint(), TemplateResourceConstraint::Any);
4762
4763        let src = r"
4764            permit(principal, action, resource == ?resource);
4765        ";
4766        let t = Template::parse(None, src).unwrap();
4767        assert_eq!(
4768            t.resource_constraint(),
4769            TemplateResourceConstraint::Eq(None)
4770        );
4771
4772        let src = r#"
4773            permit(principal, action, resource == A::"a");
4774        "#;
4775        let t = Template::parse(None, src).unwrap();
4776        assert_eq!(
4777            t.resource_constraint(),
4778            TemplateResourceConstraint::Eq(Some(EntityUid::from_strs("A", "a")))
4779        );
4780
4781        let src = r"
4782            permit(principal, action, resource in ?resource);
4783        ";
4784        let t = Template::parse(None, src).unwrap();
4785        assert_eq!(
4786            t.resource_constraint(),
4787            TemplateResourceConstraint::In(None)
4788        );
4789
4790        let src = r#"
4791            permit(principal, action, resource in A::"a");
4792        "#;
4793        let t = Template::parse(None, src).unwrap();
4794        assert_eq!(
4795            t.resource_constraint(),
4796            TemplateResourceConstraint::In(Some(EntityUid::from_strs("A", "a")))
4797        );
4798    }
4799
4800    #[test]
4801    fn schema_namespace() {
4802        let fragment: SchemaFragment = r#"
4803        {
4804            "Foo::Bar": {
4805                "entityTypes": {},
4806                "actions": {}
4807            }
4808        }
4809        "#
4810        .parse()
4811        .unwrap();
4812        let namespaces = fragment.namespaces().next().unwrap();
4813        assert_eq!(
4814            namespaces.map(|ns| ns.to_string()),
4815            Some("Foo::Bar".to_string())
4816        );
4817        let _schema: Schema = fragment.try_into().expect("Should convert to schema");
4818
4819        let fragment: SchemaFragment = r#"
4820        {
4821            "": {
4822                "entityTypes": {},
4823                "actions": {}
4824            }
4825        }
4826        "#
4827        .parse()
4828        .unwrap();
4829        let namespaces = fragment.namespaces().next().unwrap();
4830        assert_eq!(namespaces, None);
4831        let _schema: Schema = fragment.try_into().expect("Should convert to schema");
4832    }
4833
4834    #[test]
4835    fn load_multiple_namespaces() {
4836        let fragment = SchemaFragment::from_json_value(json!({
4837            "Foo::Bar": {
4838                "entityTypes": {
4839                    "Baz": {
4840                        "memberOfTypes": ["Bar::Foo::Baz"]
4841                    }
4842                },
4843                "actions": {}
4844            },
4845            "Bar::Foo": {
4846                "entityTypes": {
4847                    "Baz": {
4848                        "memberOfTypes": ["Foo::Bar::Baz"]
4849                    }
4850                },
4851                "actions": {}
4852            }
4853        }))
4854        .unwrap();
4855
4856        let schema = Schema::from_schema_fragments([fragment]).unwrap();
4857
4858        assert!(schema
4859            .0
4860            .get_entity_type(&"Foo::Bar::Baz".parse().unwrap())
4861            .is_some());
4862        assert!(schema
4863            .0
4864            .get_entity_type(&"Bar::Foo::Baz".parse().unwrap())
4865            .is_some());
4866    }
4867
4868    #[test]
4869    fn get_attributes_from_schema() {
4870        let fragment: SchemaFragment = SchemaFragment::from_json_value(json!({
4871        "": {
4872            "entityTypes": {},
4873            "actions": {
4874                "A": {},
4875                "B": {
4876                    "memberOf": [{"id": "A"}]
4877                },
4878                "C": {
4879                    "memberOf": [{"id": "A"}]
4880                },
4881                "D": {
4882                    "memberOf": [{"id": "B"}, {"id": "C"}]
4883                },
4884                "E": {
4885                    "memberOf": [{"id": "D"}]
4886                }
4887            }
4888        }}))
4889        .unwrap();
4890
4891        let schema = Schema::from_schema_fragments([fragment]).unwrap();
4892        let action_entities = schema.action_entities().unwrap();
4893
4894        let a_euid = EntityUid::from_strs("Action", "A");
4895        let b_euid = EntityUid::from_strs("Action", "B");
4896        let c_euid = EntityUid::from_strs("Action", "C");
4897        let d_euid = EntityUid::from_strs("Action", "D");
4898        let e_euid = EntityUid::from_strs("Action", "E");
4899
4900        assert_eq!(
4901            action_entities,
4902            Entities::from_entities([
4903                Entity::new(a_euid.clone(), HashMap::new(), HashSet::new()),
4904                Entity::new(
4905                    b_euid.clone(),
4906                    HashMap::new(),
4907                    HashSet::from([a_euid.clone()])
4908                ),
4909                Entity::new(
4910                    c_euid.clone(),
4911                    HashMap::new(),
4912                    HashSet::from([a_euid.clone()])
4913                ),
4914                Entity::new(
4915                    d_euid.clone(),
4916                    HashMap::new(),
4917                    HashSet::from([a_euid.clone(), b_euid.clone(), c_euid.clone()])
4918                ),
4919                Entity::new(
4920                    e_euid,
4921                    HashMap::new(),
4922                    HashSet::from([a_euid, b_euid, c_euid, d_euid])
4923                ),
4924            ])
4925            .unwrap()
4926        );
4927    }
4928
4929    /// If a user passes actions through both the schema and the entities, then
4930    /// those actions should exactly match _unless_ the `TCComputation::ComputeNow`
4931    /// option is used, in which case only the TC has to match.
4932    #[test]
4933    fn issue_285() {
4934        let schema = Schema::from_json_value(json!(
4935        {"": {
4936            "entityTypes": {},
4937            "actions": {
4938                "A": {},
4939                "B": {
4940                    "memberOf": [{"id": "A"}]
4941                },
4942                "C": {
4943                    "memberOf": [{"id": "B"}]
4944                }
4945            }
4946        }}
4947        ))
4948        .expect("should be a valid schema");
4949
4950        let entitiesjson_tc = json!(
4951            [
4952                {
4953                    "uid": { "type": "Action", "id": "A" },
4954                    "attrs": {},
4955                    "parents": []
4956                },
4957                {
4958                    "uid": { "type": "Action", "id": "B" },
4959                    "attrs": {},
4960                    "parents": [
4961                        { "type": "Action", "id": "A" }
4962                    ]
4963                },
4964                {
4965                    "uid": { "type": "Action", "id": "C" },
4966                    "attrs": {},
4967                    "parents": [
4968                        { "type": "Action", "id": "A" },
4969                        { "type": "Action", "id": "B" }
4970                    ]
4971                }
4972            ]
4973        );
4974
4975        let entitiesjson_no_tc = json!(
4976            [
4977                {
4978                    "uid": { "type": "Action", "id": "A" },
4979                    "attrs": {},
4980                    "parents": []
4981                },
4982                {
4983                    "uid": { "type": "Action", "id": "B" },
4984                    "attrs": {},
4985                    "parents": [
4986                        { "type": "Action", "id": "A" }
4987                    ]
4988                },
4989                {
4990                    "uid": { "type": "Action", "id": "C" },
4991                    "attrs": {},
4992                    "parents": [
4993                        { "type": "Action", "id": "B" }
4994                    ]
4995                }
4996            ]
4997        );
4998
4999        // Both entity jsons are ok (the default TC setting is `ComputeNow`)
5000        assert!(Entities::from_json_value(entitiesjson_tc, Some(&schema)).is_ok());
5001        assert!(Entities::from_json_value(entitiesjson_no_tc.clone(), Some(&schema)).is_ok());
5002
5003        // Parsing will fail if the TC doesn't match
5004        let entitiesjson_bad = json!(
5005            [
5006                {
5007                    "uid": { "type": "Action", "id": "A" },
5008                    "attrs": {},
5009                    "parents": []
5010                },
5011                {
5012                    "uid": { "type": "Action", "id": "B" },
5013                    "attrs": {},
5014                    "parents": [
5015                        { "type": "Action", "id": "A" }
5016                    ]
5017                },
5018                {
5019                    "uid": { "type": "Action", "id": "C" },
5020                    "attrs": {},
5021                    "parents": [
5022                        { "type": "Action", "id": "A" }
5023                    ]
5024                }
5025            ]
5026        );
5027        assert!(matches!(
5028            Entities::from_json_value(entitiesjson_bad, Some(&schema)),
5029            Err(EntitiesError::Deserialization(
5030                entities::JsonDeserializationError::ActionDeclarationMismatch { uid: _ }
5031            ))
5032        ));
5033
5034        // Parsing will fail if we change the TC setting
5035        let parser_assume_computed = entities::EntityJsonParser::new(
5036            Some(cedar_policy_validator::CoreSchema::new(&schema.0)),
5037            Extensions::all_available(),
5038            entities::TCComputation::AssumeAlreadyComputed,
5039        );
5040        assert!(matches!(
5041            parser_assume_computed.from_json_value(entitiesjson_no_tc.clone()),
5042            Err(EntitiesError::Deserialization(
5043                entities::JsonDeserializationError::ActionDeclarationMismatch { uid: _ }
5044            ))
5045        ));
5046
5047        let parser_enforce_computed = entities::EntityJsonParser::new(
5048            Some(cedar_policy_validator::CoreSchema::new(&schema.0)),
5049            extensions::Extensions::all_available(),
5050            entities::TCComputation::EnforceAlreadyComputed,
5051        );
5052        assert!(matches!(
5053            parser_enforce_computed.from_json_value(entitiesjson_no_tc),
5054            Err(EntitiesError::TransitiveClosureError(_))
5055        ));
5056    }
5057}
5058#[cfg(test)]
5059// PANIC SAFETY: unit tests
5060#[allow(clippy::unwrap_used)]
5061mod test {
5062    use super::*;
5063
5064    #[test]
5065    fn test_all_ints() {
5066        test_single_int(0);
5067        test_single_int(i64::MAX);
5068        test_single_int(i64::MIN);
5069        test_single_int(7);
5070        test_single_int(-7);
5071    }
5072
5073    fn test_single_int(x: i64) {
5074        for i in 0..4 {
5075            test_single_int_with_dashes(x, i);
5076        }
5077    }
5078
5079    fn test_single_int_with_dashes(x: i64, num_dashes: usize) {
5080        let dashes = vec!['-'; num_dashes].into_iter().collect::<String>();
5081        let src = format!(r#"permit(principal, action, resource) when {{ {dashes}{x} }};"#);
5082        let p: Policy = src.parse().unwrap();
5083        let json = p.to_json().unwrap();
5084        let round_trip = Policy::from_json(None, json).unwrap();
5085        let pretty_print = format!("{round_trip}");
5086        assert!(pretty_print.contains(&x.to_string()));
5087        if x != 0 {
5088            let expected_dashes = if x < 0 { num_dashes + 1 } else { num_dashes };
5089            assert_eq!(
5090                pretty_print.chars().filter(|c| *c == '-').count(),
5091                expected_dashes
5092            );
5093        }
5094    }
5095
5096    // Serializing a valid 64-bit int that can't be represented in double precision float
5097    #[test]
5098    fn json_bignum_1() {
5099        let src = r#"
5100        permit(
5101            principal,
5102            action == Action::"action",
5103            resource
5104          ) when {
5105            -9223372036854775808
5106          };"#;
5107        let p: Policy = src.parse().unwrap();
5108        p.to_json().unwrap();
5109    }
5110
5111    #[test]
5112    fn json_bignum_1a() {
5113        let src = r"
5114        permit(principal, action, resource) when { 
5115            (true && (-90071992547409921)) && principal
5116        };";
5117        let p: Policy = src.parse().unwrap();
5118        let v = p.to_json().unwrap();
5119        let s = serde_json::to_string(&v).unwrap();
5120        assert!(s.contains("90071992547409921"));
5121    }
5122
5123    // Deserializing a valid 64-bit int that can't be represented in double precision float
5124    #[test]
5125    fn json_bignum_2() {
5126        let src = r#"{"effect":"permit","principal":{"op":"All"},"action":{"op":"All"},"resource":{"op":"All"},"conditions":[{"kind":"when","body":{"==":{"left":{".":{"left":{"Var":"principal"},"attr":"x"}},"right":{"Value":90071992547409921}}}}]}"#;
5127        let v: serde_json::Value = serde_json::from_str(src).unwrap();
5128        let p = Policy::from_json(None, v).unwrap();
5129        let pretty = format!("{p}");
5130        // Ensure the number didn't get rounded
5131        assert!(pretty.contains("90071992547409921"));
5132    }
5133
5134    // Deserializing a valid 64-bit int that can't be represented in double precision float
5135    #[test]
5136    fn json_bignum_2a() {
5137        let src = r#"{"effect":"permit","principal":{"op":"All"},"action":{"op":"All"},"resource":{"op":"All"},"conditions":[{"kind":"when","body":{"==":{"left":{".":{"left":{"Var":"principal"},"attr":"x"}},"right":{"Value":-9223372036854775808}}}}]}"#;
5138        let v: serde_json::Value = serde_json::from_str(src).unwrap();
5139        let p = Policy::from_json(None, v).unwrap();
5140        let pretty = format!("{p}");
5141        // Ensure the number didn't get rounded
5142        assert!(pretty.contains("-9223372036854775808"));
5143    }
5144
5145    // Deserializing a number that doesn't fit in 64 bit integer
5146    // This _should_ fail, as there's no way to do this w/out loss of precision
5147    #[test]
5148    fn json_bignum_3() {
5149        let src = r#"{"effect":"permit","principal":{"op":"All"},"action":{"op":"All"},"resource":{"op":"All"},"conditions":[{"kind":"when","body":{"==":{"left":{".":{"left":{"Var":"principal"},"attr":"x"}},"right":{"Value":9223372036854775808}}}}]}"#;
5150        let v: serde_json::Value = serde_json::from_str(src).unwrap();
5151        assert!(Policy::from_json(None, v).is_err());
5152    }
5153}
5154
5155#[cfg(test)]
5156mod issue_606 {
5157    use std::str::FromStr;
5158
5159    use cedar_policy_core::est::EstToAstError;
5160
5161    use crate::{PolicyId, Template};
5162
5163    #[test]
5164    fn est_template() {
5165        let est_json = serde_json::json!({
5166            "effect": "permit",
5167            "principal": { "op": "All" },
5168            "action": { "op": "All" },
5169            "resource": { "op": "All" },
5170            "conditions": [
5171                {
5172                    "kind": "when",
5173                    "body": {
5174                        "==": {
5175                            "left": { "Var": "principal" },
5176                            "right": { "Slot": "?principal" }
5177                        }
5178                    }
5179                }
5180            ]
5181        });
5182
5183        let tid = PolicyId::from_str("t0").unwrap();
5184        // We should get an error here after trying to construct a template with a slot in the condition
5185        let template = Template::from_json(Some(tid), est_json);
5186        assert!(matches!(
5187            template,
5188            Err(EstToAstError::SlotsInConditionClause {
5189                slot: _,
5190                clausetype: "when"
5191            })
5192        ));
5193    }
5194}
5195
5196#[cfg(test)]
5197mod issue_604 {
5198    use crate::Policy;
5199    use cedar_policy_core::parser::parse_policy_or_template_to_est;
5200    use cool_asserts::assert_matches;
5201    #[track_caller]
5202    fn to_json_is_ok(text: &str) {
5203        let policy = Policy::parse(None, text).unwrap();
5204        let json = policy.to_json();
5205        assert_matches!(json, Ok(_));
5206    }
5207
5208    #[track_caller]
5209    fn make_policy_with_get_attr(attr: &str) -> String {
5210        format!(
5211            r#"
5212        permit(principal, action, resource) when {{ principal == resource.{attr} }};
5213        "#
5214        )
5215    }
5216
5217    #[track_caller]
5218    fn make_policy_with_has_attr(attr: &str) -> String {
5219        format!(
5220            r#"
5221        permit(principal, action, resource) when {{ resource has {attr} }};
5222        "#
5223        )
5224    }
5225
5226    #[test]
5227    fn var_as_attribute_name() {
5228        for attr in ["principal", "action", "resource", "context"] {
5229            to_json_is_ok(&make_policy_with_get_attr(attr));
5230            to_json_is_ok(&make_policy_with_has_attr(attr));
5231        }
5232    }
5233
5234    #[track_caller]
5235    fn is_valid_est(text: &str) {
5236        let est = parse_policy_or_template_to_est(text);
5237        assert_matches!(est, Ok(_));
5238    }
5239
5240    #[track_caller]
5241    fn is_invalid_est(text: &str) {
5242        let est = parse_policy_or_template_to_est(text);
5243        assert_matches!(est, Err(_));
5244    }
5245
5246    #[test]
5247    fn keyword_as_attribute_name_err() {
5248        for attr in ["true", "false", "if", "then", "else", "in", "like", "has"] {
5249            is_invalid_est(&make_policy_with_get_attr(attr));
5250            is_invalid_est(&make_policy_with_has_attr(attr));
5251        }
5252    }
5253
5254    #[test]
5255    fn keyword_as_attribute_name_ok() {
5256        for attr in ["permit", "forbid", "when", "unless", "_"] {
5257            is_valid_est(&make_policy_with_get_attr(attr));
5258            is_valid_est(&make_policy_with_has_attr(attr));
5259        }
5260    }
5261}