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#[repr(transparent)]
84#[derive(Debug, Clone, PartialEq, Eq, RefCast)]
85pub struct Entity(ast::Entity);
86
87impl Entity {
88    /// Create a new `Entity` with this Uid, attributes, and parents.
89    ///
90    /// Attribute values are specified here as "restricted expressions".
91    /// See docs on `RestrictedExpression`
92    pub fn new(
93        uid: EntityUid,
94        attrs: HashMap<String, RestrictedExpression>,
95        parents: HashSet<EntityUid>,
96    ) -> Self {
97        // note that we take a "parents" parameter here; we will compute TC when
98        // the `Entities` object is created
99        Self(ast::Entity::new(
100            uid.0,
101            attrs
102                .into_iter()
103                .map(|(k, v)| (SmolStr::from(k), v.0))
104                .collect(),
105            parents.into_iter().map(|uid| uid.0).collect(),
106        ))
107    }
108
109    /// Create a new `Entity` with this Uid, no attributes, and no parents.
110    pub fn with_uid(uid: EntityUid) -> Self {
111        Self(ast::Entity::with_uid(uid.0))
112    }
113
114    /// Get the Uid of this entity
115    pub fn uid(&self) -> EntityUid {
116        EntityUid(self.0.uid())
117    }
118
119    /// Get the value for the given attribute, or `None` if not present.
120    ///
121    /// This can also return Some(Err) if the attribute had an illegal value.
122    pub fn attr(&self, attr: &str) -> Option<Result<EvalResult, EvaluationError>> {
123        let expr = self.0.get(attr)?;
124        let all_ext = Extensions::all_available();
125        let evaluator = RestrictedEvaluator::new(&all_ext);
126        Some(
127            evaluator
128                .interpret(expr.as_borrowed())
129                .map(EvalResult::from)
130                .map_err(|e| EvaluationError::StringMessage(e.to_string())),
131        )
132    }
133}
134
135impl std::fmt::Display for Entity {
136    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
137        write!(f, "{}", self.0)
138    }
139}
140
141/// Represents an entity hierarchy, and allows looking up `Entity` objects by
142/// Uid.
143#[repr(transparent)]
144#[derive(Debug, Clone, Default, PartialEq, Eq, RefCast)]
145pub struct Entities(pub(crate) entities::Entities);
146
147pub use entities::EntitiesError;
148
149impl Entities {
150    /// Create a fresh `Entities` with no entities
151    pub fn empty() -> Self {
152        Self(entities::Entities::new())
153    }
154
155    /// Get the `Entity` with the given Uid, if any
156    pub fn get(&self, uid: &EntityUid) -> Option<&Entity> {
157        match self.0.entity(&uid.0) {
158            Dereference::Residual(_) | Dereference::NoSuchEntity => None,
159            Dereference::Data(e) => Some(Entity::ref_cast(e)),
160        }
161    }
162
163    /// Transform the store into a partial store, where
164    /// attempting to dereference a non-existent `EntityUID` results in
165    /// a residual instead of an error.
166    #[must_use]
167    #[cfg(feature = "partial-eval")]
168    pub fn partial(self) -> Self {
169        Self(self.0.partial())
170    }
171
172    /// Iterate over the `Entity`'s in the `Entities`
173    pub fn iter(&self) -> impl Iterator<Item = &Entity> {
174        self.0.iter().map(Entity::ref_cast)
175    }
176
177    /// Create an `Entities` object with the given entities
178    /// It will error if the entities cannot be read or if the entities hierarchy is cyclic
179    pub fn from_entities(
180        entities: impl IntoIterator<Item = Entity>,
181    ) -> Result<Self, entities::EntitiesError> {
182        entities::Entities::from_entities(
183            entities.into_iter().map(|e| e.0),
184            entities::TCComputation::ComputeNow,
185        )
186        .map(Entities)
187    }
188
189    /// Parse an entities JSON file (in `&str` form) into an `Entities` object
190    ///
191    /// If a `schema` is provided, this will inform the parsing: for instance, it
192    /// will allow `__entity` and `__extn` escapes to be implicit, and it will error
193    /// if attributes have the wrong types (e.g., string instead of integer).
194    pub fn from_json_str(
195        json: &str,
196        schema: Option<&Schema>,
197    ) -> Result<Self, entities::EntitiesError> {
198        let eparser = entities::EntityJsonParser::new(
199            schema.map(|s| cedar_policy_validator::CoreSchema::new(&s.0)),
200            Extensions::all_available(),
201            entities::TCComputation::ComputeNow,
202        );
203        eparser.from_json_str(json).map(Entities)
204    }
205
206    /// Parse an entities JSON file (in `serde_json::Value` form) into an
207    /// `Entities` object
208    ///
209    /// If a `schema` is provided, this will inform the parsing: for instance, it
210    /// will allow `__entity` and `__extn` escapes to be implicit, and it will error
211    /// if attributes have the wrong types (e.g., string instead of integer).
212    pub fn from_json_value(
213        json: serde_json::Value,
214        schema: Option<&Schema>,
215    ) -> Result<Self, entities::EntitiesError> {
216        let eparser = entities::EntityJsonParser::new(
217            schema.map(|s| cedar_policy_validator::CoreSchema::new(&s.0)),
218            Extensions::all_available(),
219            entities::TCComputation::ComputeNow,
220        );
221        eparser.from_json_value(json).map(Entities)
222    }
223
224    /// Parse an entities JSON file (in `std::io::Read` form) into an `Entities`
225    /// object
226    ///
227    /// If a `schema` is provided, this will inform the parsing: for instance, it
228    /// will allow `__entity` and `__extn` escapes to be implicit, and it will error
229    /// if attributes have the wrong types (e.g., string instead of integer).
230    pub fn from_json_file(
231        json: impl std::io::Read,
232        schema: Option<&Schema>,
233    ) -> Result<Self, entities::EntitiesError> {
234        let eparser = entities::EntityJsonParser::new(
235            schema.map(|s| cedar_policy_validator::CoreSchema::new(&s.0)),
236            Extensions::all_available(),
237            entities::TCComputation::ComputeNow,
238        );
239        eparser.from_json_file(json).map(Entities)
240    }
241
242    /// Is entity `a` an ancestor of entity `b`?
243    /// Same semantics as `b in a` in the Cedar language
244    pub fn is_ancestor_of(&self, a: &EntityUid, b: &EntityUid) -> bool {
245        match self.0.entity(&b.0) {
246            Dereference::Data(b) => b.is_descendant_of(&a.0),
247            _ => a == b, // if b doesn't exist, `b in a` is only true if `b == a`
248        }
249    }
250
251    /// Get an iterator over the ancestors of the given Euid.
252    /// Returns `None` if the given `Euid` does not exist.
253    pub fn ancestors<'a>(
254        &'a self,
255        euid: &EntityUid,
256    ) -> Option<impl Iterator<Item = &'a EntityUid>> {
257        let entity = match self.0.entity(&euid.0) {
258            Dereference::Residual(_) | Dereference::NoSuchEntity => None,
259            Dereference::Data(e) => Some(e),
260        }?;
261        Some(entity.ancestors().map(EntityUid::ref_cast))
262    }
263
264    /// Dump an `Entities` object into an entities JSON file.
265    ///
266    /// The resulting JSON will be suitable for parsing in via
267    /// `from_json_*`, and will be parse-able even with no `Schema`.
268    ///
269    /// To read an `Entities` object from an entities JSON file, use
270    /// `from_json_file`.
271    pub fn write_to_json(
272        &self,
273        f: impl std::io::Write,
274    ) -> std::result::Result<(), entities::EntitiesError> {
275        self.0.write_to_json(f)
276    }
277}
278
279/// Authorizer object, which provides responses to authorization queries
280#[repr(transparent)]
281#[derive(Debug, RefCast)]
282pub struct Authorizer(authorizer::Authorizer);
283
284impl Default for Authorizer {
285    fn default() -> Self {
286        Self::new()
287    }
288}
289
290impl Authorizer {
291    /// Create a new `Authorizer`
292    pub fn new() -> Self {
293        Self(authorizer::Authorizer::new())
294    }
295
296    /// Returns an authorization response for `r` with respect to the given
297    /// `PolicySet` and `Entities`.
298    ///
299    /// The language spec and Dafny model give a precise definition of how this
300    /// is computed.
301    pub fn is_authorized(&self, r: &Request, p: &PolicySet, e: &Entities) -> Response {
302        self.0.is_authorized(&r.0, &p.ast, &e.0).into()
303    }
304
305    /// A partially evaluated authorization request.
306    /// The Authorizer will attempt to make as much progress as possible in the presence of unknowns.
307    /// If the Authorizer can reach a response, it will return that response.
308    /// Otherwise, it will return a list of residual policies that still need to be evaluated.
309    #[cfg(feature = "partial-eval")]
310    pub fn is_authorized_partial(
311        &self,
312        query: &Request,
313        policy_set: &PolicySet,
314        entities: &Entities,
315    ) -> PartialResponse {
316        let response = self
317            .0
318            .is_authorized_core(&query.0, &policy_set.ast, &entities.0);
319        match response {
320            authorizer::ResponseKind::FullyEvaluated(a) => PartialResponse::Concrete(a.into()),
321            authorizer::ResponseKind::Partial(p) => PartialResponse::Residual(p.into()),
322        }
323    }
324}
325
326/// Authorization response returned from the `Authorizer`
327#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
328pub struct Response {
329    /// Authorization decision
330    decision: Decision,
331    /// Diagnostics providing more information on how this decision was reached
332    diagnostics: Diagnostics,
333}
334
335/// Authorization response returned from `is_authorized_partial`
336/// It can either be a full concrete response, or a residual response.
337#[cfg(feature = "partial-eval")]
338#[derive(Debug, PartialEq, Clone)]
339pub enum PartialResponse {
340    /// A full, concrete response.
341    Concrete(Response),
342    /// A residual response. Determining the concrete response requires further processing.
343    Residual(ResidualResponse),
344}
345
346/// A residual response obtained from `is_authorized_partial`.
347#[cfg(feature = "partial-eval")]
348#[derive(Debug, PartialEq, Eq, Clone)]
349pub struct ResidualResponse {
350    /// Residual policies
351    residuals: PolicySet,
352    /// Diagnostics
353    diagnostics: Diagnostics,
354}
355
356/// Diagnostics providing more information on how a `Decision` was reached
357#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
358pub struct Diagnostics {
359    /// `PolicyId`s of the policies that contributed to the decision.
360    /// If no policies applied to the request, this set will be empty.
361    reason: HashSet<PolicyId>,
362    /// list of error messages which occurred
363    errors: HashSet<String>,
364}
365
366impl From<authorizer::Diagnostics> for Diagnostics {
367    fn from(diagnostics: authorizer::Diagnostics) -> Self {
368        Self {
369            reason: diagnostics.reason.into_iter().map(PolicyId).collect(),
370            errors: diagnostics.errors.iter().map(ToString::to_string).collect(),
371        }
372    }
373}
374
375impl Diagnostics {
376    /// Get the policies that contributed to the decision
377    pub fn reason(&self) -> impl Iterator<Item = &PolicyId> {
378        self.reason.iter()
379    }
380
381    /// Get the error messages
382    pub fn errors(&self) -> impl Iterator<Item = EvaluationError> + '_ {
383        self.errors
384            .iter()
385            .cloned()
386            .map(EvaluationError::StringMessage)
387    }
388}
389
390impl Response {
391    /// Create a new `Response`
392    pub fn new(decision: Decision, reason: HashSet<PolicyId>, errors: HashSet<String>) -> Self {
393        Self {
394            decision,
395            diagnostics: Diagnostics { reason, errors },
396        }
397    }
398
399    /// Get the authorization decision
400    pub fn decision(&self) -> Decision {
401        self.decision
402    }
403
404    /// Get the authorization diagnostics
405    pub fn diagnostics(&self) -> &Diagnostics {
406        &self.diagnostics
407    }
408}
409
410impl From<authorizer::Response> for Response {
411    fn from(a: authorizer::Response) -> Self {
412        Self {
413            decision: a.decision,
414            diagnostics: a.diagnostics.into(),
415        }
416    }
417}
418
419#[cfg(feature = "partial-eval")]
420impl ResidualResponse {
421    /// Create a new `ResidualResponse`
422    pub fn new(residuals: PolicySet, reason: HashSet<PolicyId>, errors: HashSet<String>) -> Self {
423        Self {
424            residuals,
425            diagnostics: Diagnostics { reason, errors },
426        }
427    }
428
429    /// Get the residual policies needed to reach an authorization decision.
430    pub fn residuals(&self) -> &PolicySet {
431        &self.residuals
432    }
433
434    /// Get the authorization diagnostics
435    pub fn diagnostics(&self) -> &Diagnostics {
436        &self.diagnostics
437    }
438}
439
440#[cfg(feature = "partial-eval")]
441impl From<authorizer::PartialResponse> for ResidualResponse {
442    fn from(p: authorizer::PartialResponse) -> Self {
443        Self {
444            residuals: PolicySet::from_ast(p.residuals),
445            diagnostics: p.diagnostics.into(),
446        }
447    }
448}
449
450/// Errors encountered while evaluating policies or expressions, or making
451/// authorization decisions.
452#[derive(Debug, Clone, PartialEq, Eq, Error)]
453pub enum EvaluationError {
454    /// Error message, as string.
455    /// TODO in the future this can/should be the actual Core `EvaluationError`
456    #[error("{0}")]
457    StringMessage(String),
458}
459
460/// Used to select how a policy will be validated.
461#[derive(Default, Eq, PartialEq, Copy, Clone, Debug)]
462#[non_exhaustive]
463pub enum ValidationMode {
464    /// Validate that policies do not contain any type errors, and additionally
465    /// have a restricted form which is amenable for analysis.
466    #[default]
467    Strict,
468    /// Validate that policies do not contain any type errors.
469    Permissive,
470}
471
472impl From<ValidationMode> for cedar_policy_validator::ValidationMode {
473    fn from(mode: ValidationMode) -> Self {
474        match mode {
475            ValidationMode::Strict => Self::Strict,
476            ValidationMode::Permissive => Self::Permissive,
477        }
478    }
479}
480
481/// Validator object, which provides policy validation and typechecking.
482#[repr(transparent)]
483#[derive(Debug, RefCast)]
484pub struct Validator(cedar_policy_validator::Validator);
485
486impl Validator {
487    /// Construct a new `Validator` to validate policies using the given
488    /// `Schema`.
489    pub fn new(schema: Schema) -> Self {
490        Self(cedar_policy_validator::Validator::new(schema.0))
491    }
492
493    /// Validate all policies in a policy set, collecting all validation errors
494    /// found into the returned `ValidationResult`. Each error is returned together with the
495    /// policy id of the policy where the error was found. If a policy id
496    /// included in the input policy set does not appear in the output iterator, then
497    /// that policy passed the validator. If the function `validation_passed`
498    /// returns true, then there were no validation errors found, so all
499    /// policies in the policy set have passed the validator.
500    pub fn validate<'a>(
501        &'a self,
502        pset: &'a PolicySet,
503        mode: ValidationMode,
504    ) -> ValidationResult<'a> {
505        ValidationResult::from(self.0.validate(&pset.ast, mode.into()))
506    }
507}
508
509/// Contains all the type information used to construct a `Schema` that can be
510/// used to validate a policy.
511#[derive(Debug)]
512pub struct SchemaFragment(cedar_policy_validator::ValidatorSchemaFragment);
513
514impl SchemaFragment {
515    /// Extract namespaces defined in this `SchemaFragment`. Each namespace
516    /// entry defines the name of the namespace and the entity types and actions
517    /// that exist in the namespace.
518    pub fn namespaces(&self) -> impl Iterator<Item = Option<EntityNamespace>> + '_ {
519        self.0
520            .namespaces()
521            .map(|ns| ns.as_ref().map(|ns| EntityNamespace(ns.clone())))
522    }
523
524    /// Create an `SchemaFragment` from a JSON value (which should be an
525    /// object of the shape required for Cedar schemas).
526    pub fn from_json_value(json: serde_json::Value) -> Result<Self, SchemaError> {
527        Ok(Self(
528            cedar_policy_validator::SchemaFragment::from_json_value(json)?.try_into()?,
529        ))
530    }
531
532    /// Create a `SchemaFragment` directly from a file.
533    pub fn from_file(file: impl std::io::Read) -> Result<Self, SchemaError> {
534        Ok(Self(
535            cedar_policy_validator::SchemaFragment::from_file(file)?.try_into()?,
536        ))
537    }
538}
539
540impl TryInto<Schema> for SchemaFragment {
541    type Error = SchemaError;
542
543    /// Convert `SchemaFragment` into a `Schema`. To build the `Schema` we
544    /// need to have all entity types defined, so an error will be returned if
545    /// any undeclared entity types are referenced in the schema fragment.
546    fn try_into(self) -> Result<Schema, Self::Error> {
547        Ok(Schema(
548            cedar_policy_validator::ValidatorSchema::from_schema_fragments([self.0])?,
549        ))
550    }
551}
552
553impl FromStr for SchemaFragment {
554    type Err = SchemaError;
555    /// Construct `SchemaFragment` from a string containing a schema formatted
556    /// in the cedar schema format. This can fail if the string is not valid
557    /// JSON, or if the JSON structure does not form a valid schema. This
558    /// function does not check for consistency in the schema (e.g., references
559    /// to undefined entities) because this is not required until a `Schema` is
560    /// constructed.
561    fn from_str(src: &str) -> Result<Self, Self::Err> {
562        Ok(Self(
563            serde_json::from_str::<cedar_policy_validator::SchemaFragment>(src)
564                .map_err(cedar_policy_validator::SchemaError::from)?
565                .try_into()?,
566        ))
567    }
568}
569
570/// Object containing schema information used by the validator.
571#[repr(transparent)]
572#[derive(Debug, Clone, RefCast)]
573pub struct Schema(pub(crate) cedar_policy_validator::ValidatorSchema);
574
575impl FromStr for Schema {
576    type Err = SchemaError;
577
578    /// Construct a schema from a string containing a schema formatted in the
579    /// Cedar schema format. This can fail if it is not possible to parse a
580    /// schema from the strings, or if errors in values in the schema are
581    /// uncovered after parsing. For instance, when an entity attribute name is
582    /// found to not be a valid attribute name according to the Cedar
583    /// grammar.
584    fn from_str(schema_src: &str) -> Result<Self, Self::Err> {
585        Ok(Self(schema_src.parse()?))
586    }
587}
588
589impl Schema {
590    /// Create a `Schema` from multiple `SchemaFragment`. The individual
591    /// fragments may references entity types that are not declared in that
592    /// fragment, but all referenced entity types must be declared in some
593    /// fragment.
594    pub fn from_schema_fragments(
595        fragments: impl IntoIterator<Item = SchemaFragment>,
596    ) -> Result<Self, SchemaError> {
597        Ok(Self(
598            cedar_policy_validator::ValidatorSchema::from_schema_fragments(
599                fragments.into_iter().map(|f| f.0),
600            )?,
601        ))
602    }
603
604    /// Create a `Schema` from a JSON value (which should be an object of the
605    /// shape required for Cedar schemas).
606    pub fn from_json_value(json: serde_json::Value) -> Result<Self, SchemaError> {
607        Ok(Self(
608            cedar_policy_validator::ValidatorSchema::from_json_value(json)?,
609        ))
610    }
611
612    /// Create a `Schema` directly from a file.
613    pub fn from_file(file: impl std::io::Read) -> Result<Self, SchemaError> {
614        Ok(Self(cedar_policy_validator::ValidatorSchema::from_file(
615            file,
616        )?))
617    }
618
619    /// Extract from the schema an `Entities` containing the action entities
620    /// declared in the schema.
621    pub fn action_entities(&self) -> Result<Entities, entities::EntitiesError> {
622        Ok(Entities(self.0.action_entities()?))
623    }
624}
625
626/// Errors encountered during construction of a Validation Schema
627#[derive(Debug, Error)]
628pub enum SchemaError {
629    /// Errors loading and parsing schema files
630    #[error("JSON Schema file could not be parsed: {0}")]
631    ParseJson(serde_json::Error),
632    /// Errors occurring while computing or enforcing transitive closure on
633    /// action id hierarchy.
634    #[error("Transitive closure error on action hierarchy: {0}")]
635    ActionTransitiveClosureError(String),
636    /// Errors occurring while computing or enforcing transitive closure on
637    /// entity type hierarchy.
638    #[error("Transitive closure error on entity hierarchy: {0}")]
639    EntityTransitiveClosureError(String),
640    /// Error generated when processing a schema file that uses features which
641    /// are not yet supported by the implementation.
642    #[error("Unsupported feature used in schema: {0}")]
643    UnsupportedSchemaFeature(String),
644    /// Undeclared entity type(s) used in an entity type's memberOf field or an
645    /// action's appliesTo fields.
646    #[error("Undeclared entity types: {0:?}")]
647    UndeclaredEntityTypes(HashSet<String>),
648    /// Undeclared action(s) used in an action's memberOf field.
649    #[error("Undeclared actions: {0:?}")]
650    UndeclaredActions(HashSet<String>),
651    /// Undeclared type used in entity or context attributes.
652    #[error("Undeclared common types: {0:?}")]
653    UndeclaredCommonType(HashSet<String>),
654    /// Duplicate specifications for an entity type. Argument is the name of
655    /// the duplicate entity type.
656    #[error("Duplicate entity type {0}")]
657    DuplicateEntityType(String),
658    /// Duplicate specifications for an action. Argument is the name of the
659    /// duplicate action.
660    #[error("Duplicate action {0}")]
661    DuplicateAction(String),
662    /// Duplicate specifications for a reusable common type. Argument is the
663    /// name of the duplicate type.
664    #[error("Duplicate common type {0}")]
665    DuplicateCommonType(String),
666    /// Cycle in the schema's action hierarchy.
667    #[error("Cycle in action hierarchy")]
668    CycleInActionHierarchy,
669    /// Parse errors occurring while parsing an entity type.
670    #[error("Parse error in entity type: {0}")]
671    EntityTypeParse(ParseErrors),
672    /// Parse errors occurring while parsing a namespace identifier.
673    #[error("Parse error in namespace identifier: {0}")]
674    NamespaceParse(ParseErrors),
675    /// Parse errors occurring while parsing a common type identifier.
676    #[error("Parse error in common type identifier: {0}")]
677    CommonTypeParseError(ParseErrors),
678    /// Parse errors occurring while parsing an extension type.
679    #[error("Parse error in extension type: {0}")]
680    ExtensionTypeParse(ParseErrors),
681    /// The schema file included an entity type `Action` in the entity type
682    /// list. The `Action` entity type is always implicitly declared, and it
683    /// cannot currently have attributes or be in any groups, so there is no
684    /// purposes in adding an explicit entry.
685    #[error("Entity type `Action` declared in `entityTypes` list.")]
686    ActionEntityTypeDeclared,
687    /// One or more action entities are declared with `attributes` lists, but
688    /// action entities cannot have attributes.
689    #[error("Actions declared with `attributes`: [{}]", .0.iter().map(String::as_str).join(", "))]
690    ActionEntityAttributes(Vec<String>),
691    /// An action context or entity type shape was declared to have a type other
692    /// than `Record`.
693    #[error("Action context or entity type shape is not a record")]
694    ContextOrShapeNotRecord,
695    /// An Action Entity (transitively) has an attribute that is an empty set
696    #[error("Action attribute is an empty set")]
697    ActionEntityAttributeEmptySet,
698    /// An Action Entity (transitively) has an attribute of unsupported type (ExprEscape, EntityEscape or ExtnEscape)
699    #[error(
700        "Action has an attribute of unsupported type (escaped expression, entity or extension)"
701    )]
702    ActionEntityAttributeUnsupportedType,
703}
704
705#[doc(hidden)]
706impl From<cedar_policy_validator::SchemaError> for SchemaError {
707    fn from(value: cedar_policy_validator::SchemaError) -> Self {
708        match value {
709            cedar_policy_validator::SchemaError::ParseFileFormat(e) => Self::ParseJson(e),
710            cedar_policy_validator::SchemaError::ActionTransitiveClosureError(e) => {
711                Self::ActionTransitiveClosureError(e.to_string())
712            }
713            cedar_policy_validator::SchemaError::EntityTransitiveClosureError(e) => {
714                Self::EntityTransitiveClosureError(e.to_string())
715            }
716            cedar_policy_validator::SchemaError::UnsupportedSchemaFeature(e) => {
717                Self::UnsupportedSchemaFeature(e.to_string())
718            }
719            cedar_policy_validator::SchemaError::UndeclaredEntityTypes(e) => {
720                Self::UndeclaredEntityTypes(e)
721            }
722            cedar_policy_validator::SchemaError::UndeclaredActions(e) => Self::UndeclaredActions(e),
723            cedar_policy_validator::SchemaError::UndeclaredCommonType(c) => {
724                Self::UndeclaredCommonType(c)
725            }
726            cedar_policy_validator::SchemaError::DuplicateEntityType(e) => {
727                Self::DuplicateEntityType(e)
728            }
729            cedar_policy_validator::SchemaError::DuplicateAction(e) => Self::DuplicateAction(e),
730            cedar_policy_validator::SchemaError::DuplicateCommonType(c) => {
731                Self::DuplicateCommonType(c)
732            }
733            cedar_policy_validator::SchemaError::CycleInActionHierarchy => {
734                Self::CycleInActionHierarchy
735            }
736            cedar_policy_validator::SchemaError::EntityTypeParseError(e) => {
737                Self::EntityTypeParse(ParseErrors(e))
738            }
739            cedar_policy_validator::SchemaError::NamespaceParseError(e) => {
740                Self::NamespaceParse(ParseErrors(e))
741            }
742            cedar_policy_validator::SchemaError::CommonTypeParseError(e) => {
743                Self::CommonTypeParseError(ParseErrors(e))
744            }
745            cedar_policy_validator::SchemaError::ExtensionTypeParseError(e) => {
746                Self::ExtensionTypeParse(ParseErrors(e))
747            }
748            cedar_policy_validator::SchemaError::ActionEntityTypeDeclared => {
749                Self::ActionEntityTypeDeclared
750            }
751            cedar_policy_validator::SchemaError::ActionEntityAttributes(e) => {
752                Self::ActionEntityAttributes(e)
753            }
754            cedar_policy_validator::SchemaError::ContextOrShapeNotRecord(_) => {
755                Self::ContextOrShapeNotRecord
756            }
757            cedar_policy_validator::SchemaError::ActionEntityAttributeEmptySet => {
758                Self::ActionEntityAttributeEmptySet
759            }
760            cedar_policy_validator::SchemaError::ActionEntityAttributeUnsupportedType => {
761                Self::ActionEntityAttributeUnsupportedType
762            }
763        }
764    }
765}
766
767/// Contains the result of policy validation. The result includes the list of of
768/// issues found by the validation and whether validation succeeds or fails.
769/// Validation succeeds if there are no fatal errors.  There are currently no
770/// non-fatal warnings, so any issues found will cause validation to fail.
771#[derive(Debug)]
772pub struct ValidationResult<'a> {
773    validation_errors: Vec<ValidationError<'a>>,
774}
775
776impl<'a> ValidationResult<'a> {
777    /// True when validation passes. There are no fatal errors.
778    pub fn validation_passed(&self) -> bool {
779        self.validation_errors.is_empty()
780    }
781
782    /// Get the list of errors found by the validator.
783    pub fn validation_errors(&self) -> impl Iterator<Item = &ValidationError<'a>> {
784        self.validation_errors.iter()
785    }
786}
787
788impl<'a> From<cedar_policy_validator::ValidationResult<'a>> for ValidationResult<'a> {
789    fn from(r: cedar_policy_validator::ValidationResult<'a>) -> Self {
790        Self {
791            validation_errors: r
792                .into_validation_errors()
793                .map(ValidationError::from)
794                .collect(),
795        }
796    }
797}
798
799/// An error generated by the validator when it finds a potential problem in a
800/// policy. The error contains a enumeration that specifies the kind of problem,
801/// and provides details specific to that kind of problem. The error also records
802/// where the problem was encountered.
803#[derive(Debug, Error)]
804pub struct ValidationError<'a> {
805    location: SourceLocation<'a>,
806    error_kind: ValidationErrorKind,
807}
808
809impl<'a> ValidationError<'a> {
810    /// Extract details about the exact issue detected by the validator.
811    pub fn error_kind(&self) -> &ValidationErrorKind {
812        &self.error_kind
813    }
814
815    /// Extract the location where the validator found the issue.
816    pub fn location(&self) -> &SourceLocation<'a> {
817        &self.location
818    }
819}
820
821impl<'a> From<cedar_policy_validator::ValidationError<'a>> for ValidationError<'a> {
822    fn from(err: cedar_policy_validator::ValidationError<'a>) -> Self {
823        let (location, error_kind) = err.into_location_and_error_kind();
824        Self {
825            location: SourceLocation::from(location),
826            error_kind,
827        }
828    }
829}
830
831impl<'a> std::fmt::Display for ValidationError<'a> {
832    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
833        write!(f, "Validation error on policy {}", self.location.policy_id)?;
834        if let (Some(range_start), Some(range_end)) =
835            (self.location().range_start(), self.location().range_end())
836        {
837            write!(f, " at offset {range_start}-{range_end}")?;
838        }
839        write!(f, ": {}", self.error_kind())
840    }
841}
842
843/// Represents a location in Cedar policy source.
844#[derive(Debug, Clone, Eq, PartialEq)]
845pub struct SourceLocation<'a> {
846    policy_id: &'a PolicyId,
847    source_range: Option<SourceInfo>,
848}
849
850impl<'a> SourceLocation<'a> {
851    /// Get the `PolicyId` for the policy at this source location.
852    pub fn policy_id(&self) -> &'a PolicyId {
853        self.policy_id
854    }
855
856    /// Get the start of the location. Returns `None` if this location does not
857    /// have a range.
858    pub fn range_start(&self) -> Option<usize> {
859        self.source_range.as_ref().map(SourceInfo::range_start)
860    }
861
862    /// Get the end of the location. Returns `None` if this location does not
863    /// have a range.
864    pub fn range_end(&self) -> Option<usize> {
865        self.source_range.as_ref().map(SourceInfo::range_end)
866    }
867}
868
869impl<'a> From<cedar_policy_validator::SourceLocation<'a>> for SourceLocation<'a> {
870    fn from(loc: cedar_policy_validator::SourceLocation<'a>) -> SourceLocation<'a> {
871        let policy_id: &'a PolicyId = PolicyId::ref_cast(loc.policy_id());
872        let source_range = loc.into_source_info();
873        Self {
874            policy_id,
875            source_range,
876        }
877    }
878}
879
880/// Scan a set of policies for potentially confusing/obfuscating text.
881pub fn confusable_string_checker<'a>(
882    templates: impl Iterator<Item = &'a Template>,
883) -> impl Iterator<Item = ValidationWarning<'a>> {
884    cedar_policy_validator::confusable_string_checks(templates.map(|t| &t.ast))
885        .map(std::convert::Into::into)
886}
887
888#[derive(Debug, Error)]
889#[error("Warning on policy {}: {}", .location.policy_id, .kind)]
890/// Warnings found in Cedar policies
891pub struct ValidationWarning<'a> {
892    location: SourceLocation<'a>,
893    kind: ValidationWarningKind,
894}
895
896impl<'a> ValidationWarning<'a> {
897    /// Extract details about the exact issue detected by the validator.
898    pub fn warning_kind(&self) -> &ValidationWarningKind {
899        &self.kind
900    }
901
902    /// Extract the location where the validator found the issue.
903    pub fn location(&self) -> &SourceLocation<'a> {
904        &self.location
905    }
906}
907
908#[doc(hidden)]
909impl<'a> From<cedar_policy_validator::ValidationWarning<'a>> for ValidationWarning<'a> {
910    fn from(w: cedar_policy_validator::ValidationWarning<'a>) -> Self {
911        let (loc, kind) = w.to_kind_and_location();
912        ValidationWarning {
913            location: SourceLocation {
914                policy_id: PolicyId::ref_cast(loc),
915                source_range: None,
916            },
917            kind,
918        }
919    }
920}
921
922/// unique identifier portion of the `EntityUid` type
923#[repr(transparent)]
924#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, RefCast)]
925pub struct EntityId(ast::Eid);
926
927impl FromStr for EntityId {
928    type Err = ParseErrors;
929    fn from_str(eid_str: &str) -> Result<Self, Self::Err> {
930        Ok(Self(ast::Eid::new(eid_str)))
931    }
932}
933
934impl AsRef<str> for EntityId {
935    fn as_ref(&self) -> &str {
936        self.0.as_ref()
937    }
938}
939
940// Note that this Display formatter will format the EntityId as it would be expected
941// in the EntityUid string form. For instance, the `"alice"` in `User::"alice"`.
942// This means it adds quotes and potentially performs some escaping.
943impl std::fmt::Display for EntityId {
944    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
945        write!(f, "{}", self.0)
946    }
947}
948
949/// Represents a concatenation of Namespaces and `TypeName`
950#[repr(transparent)]
951#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, RefCast)]
952pub struct EntityTypeName(ast::Name);
953
954impl FromStr for EntityTypeName {
955    type Err = ParseErrors;
956
957    fn from_str(namespace_type_str: &str) -> Result<Self, Self::Err> {
958        ast::Name::from_normalized_str(namespace_type_str)
959            .map(EntityTypeName)
960            .map_err(ParseErrors)
961    }
962}
963
964impl std::fmt::Display for EntityTypeName {
965    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
966        write!(f, "{}", self.0)
967    }
968}
969
970/// Represents a namespace
971#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
972pub struct EntityNamespace(ast::Name);
973
974// This FromStr implementation requires the _normalized_ representation of the
975// namespace. See https://github.com/cedar-policy/rfcs/pull/9/.
976impl FromStr for EntityNamespace {
977    type Err = ParseErrors;
978
979    fn from_str(namespace_str: &str) -> Result<Self, Self::Err> {
980        ast::Name::from_normalized_str(namespace_str)
981            .map(EntityNamespace)
982            .map_err(ParseErrors)
983    }
984}
985
986impl std::fmt::Display for EntityNamespace {
987    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
988        write!(f, "{}", self.0)
989    }
990}
991
992/// Unique Id for an entity, such as `User::"alice"`
993#[repr(transparent)]
994#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, RefCast)]
995pub struct EntityUid(ast::EntityUID);
996
997impl EntityUid {
998    /// Returns the portion of the Euid that represents namespace and entity type
999    pub fn type_name(&self) -> &EntityTypeName {
1000        match self.0.entity_type() {
1001            ast::EntityType::Unspecified => panic!("Impossible to have an unspecified entity"),
1002            ast::EntityType::Concrete(name) => EntityTypeName::ref_cast(name),
1003        }
1004    }
1005
1006    /// Returns the id portion of the Euid
1007    pub fn id(&self) -> &EntityId {
1008        EntityId::ref_cast(self.0.eid())
1009    }
1010
1011    /// Creates `EntityUid` from `EntityTypeName` and `EntityId`
1012    pub fn from_type_name_and_id(name: EntityTypeName, id: EntityId) -> Self {
1013        Self(ast::EntityUID::from_components(name.0, id.0))
1014    }
1015
1016    /// Creates `EntityUid` from a JSON value, which should have
1017    /// either the implicit or explicit `__entity` form.
1018    ///
1019    /// Examples:
1020    /// * `{ "__entity": { "type": "User", "id": "123abc" } }`
1021    /// * `{ "type": "User", "id": "123abc" }`
1022    pub fn from_json(json: serde_json::Value) -> Result<Self, impl std::error::Error> {
1023        let parsed: entities::EntityUidJSON = serde_json::from_value(json)?;
1024        Ok::<Self, entities::JsonDeserializationError>(Self(
1025            parsed.into_euid(|| JsonDeserializationErrorContext::EntityUid)?,
1026        ))
1027    }
1028
1029    /// Testing utility for creating `EntityUids` a bit easier
1030    #[cfg(test)]
1031    pub(crate) fn from_strs(typename: &str, id: &str) -> Self {
1032        Self::from_type_name_and_id(
1033            EntityTypeName::from_str(typename).unwrap(),
1034            EntityId::from_str(id).unwrap(),
1035        )
1036    }
1037}
1038
1039// This FromStr implementation requires the _normalized_ representation of the
1040// UID. See https://github.com/cedar-policy/rfcs/pull/9/.
1041impl FromStr for EntityUid {
1042    type Err = ParseErrors;
1043
1044    /// This is deprecated (starting with Cedar 1.2); use
1045    /// `EntityUid::from_type_name_and_id()` or `EntityUid::from_json()`
1046    /// instead.
1047    //
1048    // You can't actually `#[deprecated]` a trait implementation or trait
1049    // method.
1050    fn from_str(uid_str: &str) -> Result<Self, Self::Err> {
1051        ast::EntityUID::from_normalized_str(uid_str)
1052            .map(EntityUid)
1053            .map_err(ParseErrors)
1054    }
1055}
1056
1057impl std::fmt::Display for EntityUid {
1058    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1059        write!(f, "{}", self.0)
1060    }
1061}
1062
1063/// Potential errors when adding to a `PolicySet`.
1064#[derive(Error, Debug)]
1065#[non_exhaustive]
1066pub enum PolicySetError {
1067    /// There was a duplicate [`PolicyId`] encountered in either the set of
1068    /// templates or the set of policies.
1069    #[error("duplicate template or policy id")]
1070    AlreadyDefined,
1071    /// Error when linking a template
1072    #[error("unable to link template: {0}")]
1073    LinkingError(#[from] ast::LinkingError),
1074    /// Expected a static policy, but a template-linked policy was provided.
1075    #[error("expected a static policy, but a template-linked policy was provided")]
1076    ExpectedStatic,
1077    /// Expected a template, but a static policy was provided.
1078    #[error("expected a template, but a static policy was provided")]
1079    ExpectedTemplate,
1080}
1081
1082impl From<ast::PolicySetError> for PolicySetError {
1083    fn from(e: ast::PolicySetError) -> Self {
1084        match e {
1085            ast::PolicySetError::Occupied => Self::AlreadyDefined,
1086        }
1087    }
1088}
1089
1090impl From<ast::UnexpectedSlotError> for PolicySetError {
1091    fn from(_: ast::UnexpectedSlotError) -> Self {
1092        Self::ExpectedStatic
1093    }
1094}
1095
1096/// Represents a set of `Policy`s
1097#[derive(Debug, Clone, Default)]
1098pub struct PolicySet {
1099    /// AST representation. Technically partially redundant with the other fields.
1100    /// Internally, we ensure that the duplicated information remains consistent.
1101    pub(crate) ast: ast::PolicySet,
1102    /// Policies in the set (this includes both static policies and template linked-policies)
1103    policies: HashMap<PolicyId, Policy>,
1104    /// Templates in the set
1105    templates: HashMap<PolicyId, Template>,
1106}
1107
1108impl PartialEq for PolicySet {
1109    fn eq(&self, other: &Self) -> bool {
1110        // eq is based on just the `ast`
1111        self.ast.eq(&other.ast)
1112    }
1113}
1114impl Eq for PolicySet {}
1115
1116impl FromStr for PolicySet {
1117    type Err = ParseErrors;
1118
1119    /// Create a policy set from multiple statements.
1120    ///
1121    /// Policy ids will default to "policy*" with numbers from 0
1122    /// If you load more policies, do not use the default id, or there will be conflicts.
1123    ///
1124    /// See [`Policy`] for more.
1125    fn from_str(policies: &str) -> Result<Self, Self::Err> {
1126        let (texts, pset) = parser::parse_policyset_and_also_return_policy_text(policies)?;
1127        let policies = pset.policies().map(|p|
1128            (
1129                PolicyId(p.id().clone()),
1130                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() }
1131            )
1132        ).collect();
1133        let templates = pset.templates().map(|t|
1134            (
1135                PolicyId(t.id().clone()),
1136                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() }
1137            )
1138        ).collect();
1139        Ok(Self {
1140            ast: pset,
1141            policies,
1142            templates,
1143        })
1144    }
1145}
1146
1147impl PolicySet {
1148    /// Create a fresh empty `PolicySet`
1149    pub fn new() -> Self {
1150        Self {
1151            ast: ast::PolicySet::new(),
1152            policies: HashMap::new(),
1153            templates: HashMap::new(),
1154        }
1155    }
1156
1157    /// Create a `PolicySet` from the given policies
1158    pub fn from_policies(
1159        policies: impl IntoIterator<Item = Policy>,
1160    ) -> Result<Self, PolicySetError> {
1161        let mut set = Self::new();
1162        for policy in policies {
1163            set.add(policy)?;
1164        }
1165        Ok(set)
1166    }
1167
1168    /// Add an static policy to the `PolicySet`. To add a template instance, use
1169    /// `link` instead. This function will return an error (and not modify
1170    /// the `PolicySet`) if a template-linked policy is passed in.
1171    pub fn add(&mut self, policy: Policy) -> Result<(), PolicySetError> {
1172        if policy.is_static() {
1173            let id = PolicyId(policy.ast.id().clone());
1174            self.ast.add(policy.ast.clone())?;
1175            self.policies.insert(id, policy);
1176            Ok(())
1177        } else {
1178            Err(PolicySetError::ExpectedStatic)
1179        }
1180    }
1181
1182    /// Add a `Template` to the `PolicySet`
1183    pub fn add_template(&mut self, template: Template) -> Result<(), PolicySetError> {
1184        let id = PolicyId(template.ast.id().clone());
1185        self.ast.add_template(template.ast.clone())?;
1186        self.templates.insert(id, template);
1187        Ok(())
1188    }
1189
1190    /// Iterate over all the `Policy`s in the `PolicySet`.
1191    ///
1192    /// This will include both static and template-linked policies.
1193    pub fn policies(&self) -> impl Iterator<Item = &Policy> {
1194        self.policies.values()
1195    }
1196
1197    /// Iterate over the `Template`'s in the `PolicySet`.
1198    pub fn templates(&self) -> impl Iterator<Item = &Template> {
1199        self.templates.values()
1200    }
1201
1202    /// Get a `Template` by its `PolicyId`
1203    pub fn template(&self, id: &PolicyId) -> Option<&Template> {
1204        self.templates.get(id)
1205    }
1206
1207    /// Get a `Policy` by its `PolicyId`
1208    pub fn policy(&self, id: &PolicyId) -> Option<&Policy> {
1209        self.policies.get(id)
1210    }
1211
1212    /// Extract annotation data from a `Policy` by its `PolicyId` and annotation key
1213    pub fn annotation<'a>(&'a self, id: &PolicyId, key: impl AsRef<str>) -> Option<&'a str> {
1214        self.ast
1215            .get(&id.0)?
1216            .annotation(&key.as_ref().parse().ok()?)
1217            .map(smol_str::SmolStr::as_str)
1218    }
1219
1220    /// Extract annotation data from a `Template` by its `PolicyId` and annotation key.
1221    pub fn template_annotation(&self, id: &PolicyId, key: impl AsRef<str>) -> Option<String> {
1222        self.ast
1223            .get_template(&id.0)?
1224            .annotation(&key.as_ref().parse().ok()?)
1225            .map(smol_str::SmolStr::to_string)
1226    }
1227
1228    /// Returns true iff the `PolicySet` is empty
1229    pub fn is_empty(&self) -> bool {
1230        debug_assert_eq!(
1231            self.ast.is_empty(),
1232            self.policies.is_empty() && self.templates.is_empty()
1233        );
1234        self.ast.is_empty()
1235    }
1236
1237    /// Attempt to link a template and add the new template-linked policy to the policy set.
1238    /// If link fails, the `PolicySet` is not modified.
1239    /// Failure can happen for three reasons
1240    ///   1) The map passed in `vals` may not match the slots in the template
1241    ///   2) The `new_id` may conflict w/ a policy that already exists in the set
1242    #[allow(clippy::needless_pass_by_value)]
1243    pub fn link(
1244        &mut self,
1245        template_id: PolicyId,
1246        new_id: PolicyId,
1247        vals: HashMap<SlotId, EntityUid>,
1248    ) -> Result<(), PolicySetError> {
1249        let unwrapped_vals: HashMap<ast::SlotId, ast::EntityUID> = vals
1250            .into_iter()
1251            .map(|(key, value)| (key.into(), value.0))
1252            .collect();
1253        self.ast
1254            .link(
1255                template_id.0.clone(),
1256                new_id.0.clone(),
1257                unwrapped_vals.clone(),
1258            )
1259            .map_err(PolicySetError::LinkingError)?;
1260        let linked_ast = self
1261            .ast
1262            .get(&new_id.0)
1263            .expect("ast.link() didn't fail above, so this shouldn't fail");
1264        let linked_lossless = self
1265            .templates
1266            .get(&template_id)
1267            // We know `template_id` exists in the policy set as either a
1268            // template or a static policy because otherwise `ast.link()` would
1269            // have errored. If `ast.link()` did not error, then it could still
1270            // be that the id corresponds to a static policy. This function
1271            // should only be used to link templates, so this is an error.
1272            .ok_or(PolicySetError::ExpectedTemplate)?
1273            .lossless
1274            .clone()
1275            .link(unwrapped_vals.iter().map(|(k, v)| (*k, v)))
1276            // The only error case for `est.link()` is a template with
1277            // slots which are not filled by the provided values. `ast.link()`
1278            // will have already errored if there are any unfilled slots in the
1279            // template.
1280            .expect("ast.link() didn't fail above, so this shouldn't fail");
1281        self.policies.insert(
1282            new_id,
1283            Policy {
1284                ast: linked_ast.clone(),
1285                lossless: linked_lossless,
1286            },
1287        );
1288        Ok(())
1289    }
1290
1291    /// Create a `PolicySet` from its AST representation only. The EST will
1292    /// reflect the AST structure. When possible, don't use this method and
1293    /// create the ESTs from the policy text or CST instead, as the conversion
1294    /// to AST is lossy. ESTs generated by this method will reflect the AST and
1295    /// not the original policy syntax.
1296    #[cfg_attr(not(feature = "partial-eval"), allow(unused))]
1297    fn from_ast(ast: ast::PolicySet) -> Self {
1298        let policies = ast
1299            .policies()
1300            .map(|p| (PolicyId(p.id().clone()), Policy::from_ast(p.clone())))
1301            .collect();
1302        let templates = ast
1303            .templates()
1304            .map(|t| (PolicyId(t.id().clone()), Template::from_ast(t.clone())))
1305            .collect();
1306        Self {
1307            ast,
1308            policies,
1309            templates,
1310        }
1311    }
1312}
1313
1314impl std::fmt::Display for PolicySet {
1315    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1316        write!(f, "{}", self.ast)
1317    }
1318}
1319
1320/// Policy template datatype
1321#[derive(Debug, Clone)]
1322pub struct Template {
1323    /// AST representation of the template, used for most operations.
1324    /// In particular, the `ast` contains the authoritative `PolicyId` for the template.
1325    ast: ast::Template,
1326
1327    /// Some "lossless" representation of the template, whichever is most
1328    /// convenient to provide (and can be provided with the least overhead).
1329    /// This is used just for `to_json()`.
1330    /// We can't just derive this on-demand from `ast`, because the AST is lossy:
1331    /// we can't reconstruct an accurate CST/EST/policy-text from the AST, but
1332    /// we can from the EST (modulo whitespace and a few other things like the
1333    /// order of annotations).
1334    ///
1335    /// This is a `LosslessPolicy` (rather than something like `LosslessTemplate`)
1336    /// because the EST doesn't distinguish between static policies and templates.
1337    lossless: LosslessPolicy,
1338}
1339
1340impl PartialEq for Template {
1341    fn eq(&self, other: &Self) -> bool {
1342        // eq is based on just the `ast`
1343        self.ast.eq(&other.ast)
1344    }
1345}
1346impl Eq for Template {}
1347
1348impl Template {
1349    /// Attempt to parse a `Template` from source.
1350    /// If `id` is Some, then the resulting template will have that `id`.
1351    /// If the `id` is None, the parser will use the default "policy0".
1352    /// The behavior around None may change in the future.
1353    pub fn parse(id: Option<String>, src: impl AsRef<str>) -> Result<Self, ParseErrors> {
1354        let ast = parser::parse_policy_template(id, src.as_ref()).map_err(ParseErrors)?;
1355        Ok(Self {
1356            ast,
1357            lossless: LosslessPolicy::policy_or_template_text(src.as_ref()),
1358        })
1359    }
1360
1361    /// Get the `PolicyId` of this `Template`
1362    pub fn id(&self) -> &PolicyId {
1363        PolicyId::ref_cast(self.ast.id())
1364    }
1365
1366    /// Clone this `Template` with a new `PolicyId`
1367    #[must_use]
1368    pub fn new_id(&self, id: PolicyId) -> Self {
1369        Self {
1370            ast: self.ast.new_id(id.0),
1371            lossless: self.lossless.clone(), // Lossless representation doesn't include the `PolicyId`
1372        }
1373    }
1374
1375    /// Get the `Effect` (`Forbid` or `Permit`) of this `Template`
1376    pub fn effect(&self) -> Effect {
1377        self.ast.effect()
1378    }
1379
1380    /// Get an annotation value of this `Template`
1381    pub fn annotation(&self, key: impl AsRef<str>) -> Option<&str> {
1382        self.ast
1383            .annotation(&key.as_ref().parse().ok()?)
1384            .map(smol_str::SmolStr::as_str)
1385    }
1386
1387    /// Iterate through annotation data of this `Template` as key-value pairs
1388    pub fn annotations(&self) -> impl Iterator<Item = (&str, &str)> {
1389        self.ast
1390            .annotations()
1391            .map(|(k, v)| (k.as_ref(), v.as_str()))
1392    }
1393
1394    /// Iterate over the open slots in this `Template`
1395    pub fn slots(&self) -> impl Iterator<Item = &SlotId> {
1396        self.ast.slots().map(SlotId::ref_cast)
1397    }
1398
1399    /// Get the head constraint on this policy's principal
1400    pub fn principal_constraint(&self) -> TemplatePrincipalConstraint {
1401        match self.ast.principal_constraint().as_inner() {
1402            ast::PrincipalOrResourceConstraint::Any => TemplatePrincipalConstraint::Any,
1403            ast::PrincipalOrResourceConstraint::In(eref) => {
1404                TemplatePrincipalConstraint::In(match eref {
1405                    ast::EntityReference::EUID(e) => Some(EntityUid(e.as_ref().clone())),
1406                    ast::EntityReference::Slot => None,
1407                })
1408            }
1409            ast::PrincipalOrResourceConstraint::Eq(eref) => {
1410                TemplatePrincipalConstraint::Eq(match eref {
1411                    ast::EntityReference::EUID(e) => Some(EntityUid(e.as_ref().clone())),
1412                    ast::EntityReference::Slot => None,
1413                })
1414            }
1415        }
1416    }
1417
1418    /// Get the head constraint on this policy's action
1419    pub fn action_constraint(&self) -> ActionConstraint {
1420        // Clone the data from Core to be consistent with the other constraints
1421        match self.ast.action_constraint() {
1422            ast::ActionConstraint::Any => ActionConstraint::Any,
1423            ast::ActionConstraint::In(ids) => ActionConstraint::In(
1424                ids.iter()
1425                    .map(|id| EntityUid(id.as_ref().clone()))
1426                    .collect(),
1427            ),
1428            ast::ActionConstraint::Eq(id) => ActionConstraint::Eq(EntityUid(id.as_ref().clone())),
1429        }
1430    }
1431
1432    /// Get the head constraint on this policy's resource
1433    pub fn resource_constraint(&self) -> TemplateResourceConstraint {
1434        match self.ast.resource_constraint().as_inner() {
1435            ast::PrincipalOrResourceConstraint::Any => TemplateResourceConstraint::Any,
1436            ast::PrincipalOrResourceConstraint::In(eref) => {
1437                TemplateResourceConstraint::In(match eref {
1438                    ast::EntityReference::EUID(e) => Some(EntityUid(e.as_ref().clone())),
1439                    ast::EntityReference::Slot => None,
1440                })
1441            }
1442            ast::PrincipalOrResourceConstraint::Eq(eref) => {
1443                TemplateResourceConstraint::Eq(match eref {
1444                    ast::EntityReference::EUID(e) => Some(EntityUid(e.as_ref().clone())),
1445                    ast::EntityReference::Slot => None,
1446                })
1447            }
1448        }
1449    }
1450
1451    /// Create a `Template` from its JSON representation.
1452    /// If `id` is Some, the policy will be given that Policy Id.
1453    /// If `id` is None, then "JSON policy" will be used.
1454    /// The behavior around None may change in the future.
1455    #[allow(dead_code)] // planned to be a public method in the future
1456    fn from_json(
1457        id: Option<PolicyId>,
1458        json: serde_json::Value,
1459    ) -> Result<Self, cedar_policy_core::est::EstToAstError> {
1460        let est: est::Policy =
1461            serde_json::from_value(json).map_err(JsonDeserializationError::Serde)?;
1462        Ok(Self {
1463            ast: est.clone().try_into_ast_template(id.map(|id| id.0))?,
1464            lossless: LosslessPolicy::Est(est),
1465        })
1466    }
1467
1468    /// Get the JSON representation of this `Template`.
1469    #[allow(dead_code)] // planned to be a public method in the future
1470    fn to_json(&self) -> Result<serde_json::Value, impl std::error::Error> {
1471        let est = self.lossless.est()?;
1472        let json = serde_json::to_value(est)?;
1473        Ok::<_, PolicyToJsonError>(json)
1474    }
1475
1476    /// Create a `Template` from its AST representation only. The EST will
1477    /// reflect the AST structure. When possible, don't use this method and
1478    /// create the EST from the policy text or CST instead, as the conversion
1479    /// to AST is lossy. ESTs generated by this method will reflect the AST and
1480    /// not the original policy syntax.
1481    #[cfg_attr(not(feature = "partial-eval"), allow(unused))]
1482    fn from_ast(ast: ast::Template) -> Self {
1483        let text = ast.to_string(); // assume that pretty-printing is faster than `est::Policy::from(ast.clone())`; is that true?
1484        Self {
1485            ast,
1486            lossless: LosslessPolicy::policy_or_template_text(text),
1487        }
1488    }
1489}
1490
1491impl FromStr for Template {
1492    type Err = ParseErrors;
1493
1494    fn from_str(src: &str) -> Result<Self, Self::Err> {
1495        Self::parse(None, src)
1496    }
1497}
1498
1499/// Head constraint on policy principals.
1500#[derive(Debug, Clone, PartialEq, Eq)]
1501pub enum PrincipalConstraint {
1502    /// Un-constrained
1503    Any,
1504    /// Must be In the given EntityUid
1505    In(EntityUid),
1506    /// Must be equal to the given EntityUid
1507    Eq(EntityUid),
1508}
1509
1510/// Head constraint on policy principals for templates.
1511#[derive(Debug, Clone, PartialEq, Eq)]
1512pub enum TemplatePrincipalConstraint {
1513    /// Un-constrained
1514    Any,
1515    /// Must be In the given EntityUid.
1516    /// If [`None`], then it is a template slot.
1517    In(Option<EntityUid>),
1518    /// Must be equal to the given EntityUid
1519    /// If [`None`], then it is a template slot.
1520    Eq(Option<EntityUid>),
1521}
1522
1523impl TemplatePrincipalConstraint {
1524    /// Does this constraint contain a slot?
1525    pub fn has_slot(&self) -> bool {
1526        match self {
1527            Self::Any => false,
1528            Self::In(o) | Self::Eq(o) => o.is_none(),
1529        }
1530    }
1531}
1532
1533/// Head constraint on policy actions.
1534#[derive(Debug, Clone, PartialEq, Eq)]
1535pub enum ActionConstraint {
1536    /// Un-constrained
1537    Any,
1538    /// Must be In the given EntityUid
1539    In(Vec<EntityUid>),
1540    /// Must be equal to the given EntityUid
1541    Eq(EntityUid),
1542}
1543
1544/// Head constraint on policy resources.
1545#[derive(Debug, Clone, PartialEq, Eq)]
1546pub enum ResourceConstraint {
1547    /// Un-constrained
1548    Any,
1549    /// Must be In the given EntityUid
1550    In(EntityUid),
1551    /// Must be equal to the given EntityUid
1552    Eq(EntityUid),
1553}
1554
1555/// Head constraint on policy resources for templates.
1556#[derive(Debug, Clone, PartialEq, Eq)]
1557pub enum TemplateResourceConstraint {
1558    /// Un-constrained
1559    Any,
1560    /// Must be In the given EntityUid.
1561    /// If [`None`], then it is a template slot.
1562    In(Option<EntityUid>),
1563    /// Must be equal to the given EntityUid
1564    /// If [`None`], then it is a template slot.
1565    Eq(Option<EntityUid>),
1566}
1567
1568impl TemplateResourceConstraint {
1569    /// Does this constraint contain a slot?
1570    pub fn has_slot(&self) -> bool {
1571        match self {
1572            Self::Any => false,
1573            Self::In(o) | Self::Eq(o) => o.is_none(),
1574        }
1575    }
1576}
1577
1578/// Unique Ids assigned to policies and templates
1579#[repr(transparent)]
1580#[derive(Debug, PartialEq, Eq, Clone, Hash, Serialize, Deserialize, RefCast)]
1581pub struct PolicyId(ast::PolicyID);
1582
1583impl FromStr for PolicyId {
1584    type Err = ParseErrors;
1585
1586    /// Create a `PolicyId` from a string. Currently always returns Ok().
1587    fn from_str(id: &str) -> Result<Self, Self::Err> {
1588        Ok(Self(ast::PolicyID::from_string(id)))
1589    }
1590}
1591
1592impl std::fmt::Display for PolicyId {
1593    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1594        write!(f, "{}", self.0)
1595    }
1596}
1597
1598/// Structure for a `Policy`. Includes both static policies and template-linked policies.
1599#[derive(Debug, Clone)]
1600pub struct Policy {
1601    /// AST representation of the policy, used for most operations.
1602    /// In particular, the `ast` contains the authoritative `PolicyId` for the policy.
1603    ast: ast::Policy,
1604    /// Some "lossless" representation of the policy, whichever is most
1605    /// convenient to provide (and can be provided with the least overhead).
1606    /// This is used just for `to_json()`.
1607    /// We can't just derive this on-demand from `ast`, because the AST is lossy:
1608    /// we can't reconstruct an accurate CST/EST/policy-text from the AST, but
1609    /// we can from the EST (modulo whitespace and a few other things like the
1610    /// order of annotations).
1611    lossless: LosslessPolicy,
1612}
1613
1614impl PartialEq for Policy {
1615    fn eq(&self, other: &Self) -> bool {
1616        // eq is based on just the `ast`
1617        self.ast.eq(&other.ast)
1618    }
1619}
1620impl Eq for Policy {}
1621
1622impl Policy {
1623    /// Get the `PolicyId` of the `Template` this is linked to.
1624    /// If this is a static policy, this will return `None`.
1625    pub fn template_id(&self) -> Option<&PolicyId> {
1626        if self.is_static() {
1627            None
1628        } else {
1629            Some(PolicyId::ref_cast(self.ast.template().id()))
1630        }
1631    }
1632
1633    /// Get the `Effect` (`Permit` or `Forbid`) for this instance
1634    pub fn effect(&self) -> Effect {
1635        self.ast.effect()
1636    }
1637
1638    /// Get an annotation value of this template-linked or static policy
1639    pub fn annotation(&self, key: impl AsRef<str>) -> Option<&str> {
1640        self.ast
1641            .annotation(&key.as_ref().parse().ok()?)
1642            .map(smol_str::SmolStr::as_str)
1643    }
1644
1645    /// Iterate through annotation data of this template-linked or static policy
1646    pub fn annotations(&self) -> impl Iterator<Item = (&str, &str)> {
1647        self.ast
1648            .annotations()
1649            .map(|(k, v)| (k.as_ref(), v.as_str()))
1650    }
1651
1652    /// Get the `PolicyId` for this template-linked or static policy
1653    pub fn id(&self) -> &PolicyId {
1654        PolicyId::ref_cast(self.ast.id())
1655    }
1656
1657    /// Clone this `Policy` with a new `PolicyId`
1658    #[must_use]
1659    pub fn new_id(&self, id: PolicyId) -> Self {
1660        Self {
1661            ast: self.ast.new_id(id.0),
1662            lossless: self.lossless.clone(), // Lossless representation doesn't include the `PolicyId`
1663        }
1664    }
1665
1666    /// Returns `true` if this is a static policy, `false` otherwise.
1667    pub fn is_static(&self) -> bool {
1668        self.ast.is_static()
1669    }
1670
1671    /// Get the head constraint on this policy's principal
1672    pub fn principal_constraint(&self) -> PrincipalConstraint {
1673        let slot_id = ast::SlotId::principal();
1674        match self.ast.template().principal_constraint().as_inner() {
1675            ast::PrincipalOrResourceConstraint::Any => PrincipalConstraint::Any,
1676            ast::PrincipalOrResourceConstraint::In(eref) => {
1677                PrincipalConstraint::In(self.convert_entity_reference(eref, slot_id).clone())
1678            }
1679            ast::PrincipalOrResourceConstraint::Eq(eref) => {
1680                PrincipalConstraint::Eq(self.convert_entity_reference(eref, slot_id).clone())
1681            }
1682        }
1683    }
1684
1685    /// Get the head constraint on this policy's action
1686    pub fn action_constraint(&self) -> ActionConstraint {
1687        // Clone the data from Core to be consistant with the other constraints
1688        match self.ast.template().action_constraint() {
1689            ast::ActionConstraint::Any => ActionConstraint::Any,
1690            ast::ActionConstraint::In(ids) => ActionConstraint::In(
1691                ids.iter()
1692                    .map(|euid| EntityUid::ref_cast(euid.as_ref()))
1693                    .cloned()
1694                    .collect(),
1695            ),
1696            ast::ActionConstraint::Eq(id) => ActionConstraint::Eq(EntityUid::ref_cast(id).clone()),
1697        }
1698    }
1699
1700    /// Get the head constraint on this policy's resource
1701    pub fn resource_constraint(&self) -> ResourceConstraint {
1702        let slot_id = ast::SlotId::resource();
1703        match self.ast.template().resource_constraint().as_inner() {
1704            ast::PrincipalOrResourceConstraint::Any => ResourceConstraint::Any,
1705            ast::PrincipalOrResourceConstraint::In(eref) => {
1706                ResourceConstraint::In(self.convert_entity_reference(eref, slot_id).clone())
1707            }
1708            ast::PrincipalOrResourceConstraint::Eq(eref) => {
1709                ResourceConstraint::Eq(self.convert_entity_reference(eref, slot_id).clone())
1710            }
1711        }
1712    }
1713
1714    fn convert_entity_reference<'a>(
1715        &'a self,
1716        r: &'a ast::EntityReference,
1717        slot: ast::SlotId,
1718    ) -> &'a EntityUid {
1719        match r {
1720            ast::EntityReference::EUID(euid) => EntityUid::ref_cast(euid),
1721            // This `unwrap` here is safe due the invariant (values total map) on policies.
1722            ast::EntityReference::Slot => EntityUid::ref_cast(self.ast.env().get(&slot).unwrap()),
1723        }
1724    }
1725
1726    /// Parse a single policy.
1727    /// If `id` is Some, the policy will be given that Policy Id.
1728    /// If `id` is None, then "policy0" will be used.
1729    /// The behavior around None may change in the future.
1730    pub fn parse(id: Option<String>, policy_src: impl AsRef<str>) -> Result<Self, ParseErrors> {
1731        let inline_ast = parser::parse_policy(id, policy_src.as_ref()).map_err(ParseErrors)?;
1732        let (_, ast) = ast::Template::link_static_policy(inline_ast);
1733        Ok(Self {
1734            ast,
1735            lossless: LosslessPolicy::policy_or_template_text(policy_src.as_ref()),
1736        })
1737    }
1738
1739    /// Create a `Policy` from its JSON representation.
1740    /// If `id` is Some, the policy will be given that Policy Id.
1741    /// If `id` is None, then "JSON policy" will be used.
1742    /// The behavior around None may change in the future.
1743    pub fn from_json(
1744        id: Option<PolicyId>,
1745        json: serde_json::Value,
1746    ) -> Result<Self, cedar_policy_core::est::EstToAstError> {
1747        let est: est::Policy =
1748            serde_json::from_value(json).map_err(JsonDeserializationError::Serde)?;
1749        Ok(Self {
1750            ast: est.clone().try_into_ast_policy(id.map(|id| id.0))?,
1751            lossless: LosslessPolicy::Est(est),
1752        })
1753    }
1754
1755    /// Get the JSON representation of this `Policy`.
1756    pub fn to_json(&self) -> Result<serde_json::Value, impl std::error::Error> {
1757        let est = self.lossless.est()?;
1758        let json = serde_json::to_value(est)?;
1759        Ok::<_, PolicyToJsonError>(json)
1760    }
1761
1762    /// Create a `Policy` from its AST representation only. The `LosslessPolicy`
1763    /// will reflect the AST structure. When possible, don't use this method and
1764    /// create the `Policy` from the policy text, CST, or EST instead, as the
1765    /// conversion to AST is lossy. ESTs for policies generated by this method
1766    /// will reflect the AST and not the original policy syntax.
1767    #[cfg_attr(not(feature = "partial-eval"), allow(unused))]
1768    fn from_ast(ast: ast::Policy) -> Self {
1769        let text = ast.to_string(); // assume that pretty-printing is faster than `est::Policy::from(ast.clone())`; is that true?
1770        Self {
1771            ast,
1772            lossless: LosslessPolicy::policy_or_template_text(text),
1773        }
1774    }
1775}
1776
1777impl std::fmt::Display for Policy {
1778    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1779        self.ast.fmt(f)
1780    }
1781}
1782
1783impl FromStr for Policy {
1784    type Err = ParseErrors;
1785    /// Create a policy
1786    ///
1787    /// Important note: Policies have ids, but this interface does not
1788    /// allow them to be set. It will use the default "policy0", which
1789    /// may cause id conflicts if not handled. Use `Policy::parse` to set
1790    /// the id when parsing, or `Policy::new_id` to clone a policy with
1791    /// a new id.
1792    fn from_str(policy: &str) -> Result<Self, Self::Err> {
1793        Self::parse(None, policy)
1794    }
1795}
1796
1797/// See comments on `Policy` and `Template`.
1798///
1799/// This structure can be used for static policies, linked policies, and templates.
1800#[derive(Debug, Clone)]
1801enum LosslessPolicy {
1802    /// EST representation
1803    Est(est::Policy),
1804    /// Text representation
1805    Text {
1806        /// actual policy text, of the policy or template
1807        text: String,
1808        /// For linked policies, map of slot to UID. Only linked policies have
1809        /// this; static policies and (unlinked) templates have an empty map
1810        /// here
1811        slots: HashMap<ast::SlotId, ast::EntityUID>,
1812    },
1813}
1814
1815impl LosslessPolicy {
1816    /// Create a new `LosslessPolicy` from the text of a policy or template.
1817    fn policy_or_template_text(text: impl Into<String>) -> Self {
1818        Self::Text {
1819            text: text.into(),
1820            slots: HashMap::new(),
1821        }
1822    }
1823
1824    /// Get the EST representation of this static policy, linked policy, or template
1825    fn est(&self) -> Result<est::Policy, PolicyToJsonError> {
1826        match self {
1827            Self::Est(est) => Ok(est.clone()),
1828            Self::Text { text, slots } => {
1829                let est = parser::parse_policy_or_template_to_est(text)?;
1830                if slots.is_empty() {
1831                    Ok(est)
1832                } else {
1833                    let unwrapped_vals = slots.iter().map(|(k, v)| (*k, v.into())).collect();
1834                    Ok(est.link(&unwrapped_vals)?)
1835                }
1836            }
1837        }
1838    }
1839
1840    fn link<'a>(
1841        self,
1842        vals: impl IntoIterator<Item = (ast::SlotId, &'a ast::EntityUID)>,
1843    ) -> Result<Self, est::InstantiationError> {
1844        match self {
1845            Self::Est(est) => {
1846                let unwrapped_est_vals: HashMap<ast::SlotId, entities::EntityUidJSON> =
1847                    vals.into_iter().map(|(k, v)| (k, v.into())).collect();
1848                Ok(Self::Est(est.link(&unwrapped_est_vals)?))
1849            }
1850            Self::Text { text, slots } => {
1851                debug_assert!(
1852                    slots.is_empty(),
1853                    "shouldn't call link() on an already-linked policy"
1854                );
1855                let slots = vals.into_iter().map(|(k, v)| (k, v.clone())).collect();
1856                Ok(Self::Text { text, slots })
1857            }
1858        }
1859    }
1860}
1861
1862/// Errors that can happen when getting the JSON representation of a policy
1863#[derive(Debug, Error)]
1864pub enum PolicyToJsonError {
1865    /// Parse error in the policy text
1866    #[error(transparent)]
1867    Parse(#[from] ParseErrors),
1868    /// For linked policies, error linking the JSON representation
1869    #[error(transparent)]
1870    Link(#[from] est::InstantiationError),
1871    /// Error in the JSON serialization
1872    #[error(transparent)]
1873    Serde(#[from] serde_json::Error),
1874}
1875
1876/// Expressions to be evaluated
1877#[repr(transparent)]
1878#[derive(Debug, Clone, RefCast)]
1879pub struct Expression(ast::Expr);
1880
1881impl Expression {
1882    /// Create an expression representing a literal string.
1883    pub fn new_string(value: String) -> Self {
1884        Self(ast::Expr::val(value))
1885    }
1886
1887    /// Create an expression representing a literal bool.
1888    pub fn new_bool(value: bool) -> Self {
1889        Self(ast::Expr::val(value))
1890    }
1891
1892    /// Create an expression representing a literal long.
1893    pub fn new_long(value: i64) -> Self {
1894        Self(ast::Expr::val(value))
1895    }
1896
1897    /// Create an expression representing a record.
1898    pub fn new_record(fields: impl IntoIterator<Item = (String, Self)>) -> Self {
1899        Self(ast::Expr::record(
1900            fields.into_iter().map(|(k, v)| (SmolStr::from(k), v.0)),
1901        ))
1902    }
1903
1904    /// Create an expression representing a Set.
1905    pub fn new_set(values: impl IntoIterator<Item = Self>) -> Self {
1906        Self(ast::Expr::set(values.into_iter().map(|v| v.0)))
1907    }
1908}
1909
1910impl FromStr for Expression {
1911    type Err = ParseErrors;
1912
1913    /// create an Expression using Cedar syntax
1914    fn from_str(expression: &str) -> Result<Self, Self::Err> {
1915        ast::Expr::from_str(expression)
1916            .map_err(ParseErrors)
1917            .map(Expression)
1918    }
1919}
1920
1921/// "Restricted" expressions are used for attribute values and `context`.
1922///
1923/// Restricted expressions can contain only the following:
1924///   - bool, int, and string literals
1925///   - literal `EntityUid`s such as `User::"alice"`
1926///   - extension function calls, where the arguments must be other things
1927///       on this list
1928///   - set and record literals, where the values must be other things on
1929///       this list
1930///
1931/// That means the following are not allowed in restricted expressions:
1932///   - `principal`, `action`, `resource`, `context`
1933///   - builtin operators and functions, including `.`, `in`, `has`, `like`,
1934///       `.contains()`
1935///   - if-then-else expressions
1936#[repr(transparent)]
1937#[derive(Debug, Clone, RefCast)]
1938pub struct RestrictedExpression(ast::RestrictedExpr);
1939
1940impl RestrictedExpression {
1941    /// Create an expression representing a literal string.
1942    pub fn new_string(value: String) -> Self {
1943        Self(ast::RestrictedExpr::val(value))
1944    }
1945
1946    /// Create an expression representing a literal bool.
1947    pub fn new_bool(value: bool) -> Self {
1948        Self(ast::RestrictedExpr::val(value))
1949    }
1950
1951    /// Create an expression representing a literal long.
1952    pub fn new_long(value: i64) -> Self {
1953        Self(ast::RestrictedExpr::val(value))
1954    }
1955
1956    /// Create an expression representing a record.
1957    pub fn new_record(fields: impl IntoIterator<Item = (String, Self)>) -> Self {
1958        Self(ast::RestrictedExpr::record(
1959            fields.into_iter().map(|(k, v)| (SmolStr::from(k), v.0)),
1960        ))
1961    }
1962
1963    /// Create an expression representing a Set.
1964    pub fn new_set(values: impl IntoIterator<Item = Self>) -> Self {
1965        Self(ast::RestrictedExpr::set(values.into_iter().map(|v| v.0)))
1966    }
1967}
1968
1969impl FromStr for RestrictedExpression {
1970    type Err = ParseErrors;
1971
1972    /// create a `RestrictedExpression` using Cedar syntax
1973    fn from_str(expression: &str) -> Result<Self, Self::Err> {
1974        ast::RestrictedExpr::from_str(expression)
1975            .map_err(ParseErrors)
1976            .map(RestrictedExpression)
1977    }
1978}
1979
1980/// Represents the request tuple <P, A, R, C> (see the Cedar design doc).
1981#[repr(transparent)]
1982#[derive(Debug, RefCast)]
1983pub struct Request(pub(crate) ast::Request);
1984
1985impl Request {
1986    /// Create a Request.
1987    ///
1988    /// Note that you can create the `EntityUid`s using `.parse()` on any
1989    /// string (via the `FromStr` implementation for `EntityUid`).
1990    /// The principal, action, and resource fields are optional to support
1991    /// the case where these fields do not contribute to authorization
1992    /// decisions (e.g., because they are not used in your policies).
1993    /// If any of the fields are `None`, we will automatically generate
1994    /// a unique entity UID that is not equal to any UID in the store.
1995    pub fn new(
1996        principal: Option<EntityUid>,
1997        action: Option<EntityUid>,
1998        resource: Option<EntityUid>,
1999        context: Context,
2000    ) -> Self {
2001        let p = match principal {
2002            Some(p) => p.0,
2003            None => ast::EntityUID::unspecified_from_eid(ast::Eid::new("principal")),
2004        };
2005        let a = match action {
2006            Some(a) => a.0,
2007            None => ast::EntityUID::unspecified_from_eid(ast::Eid::new("action")),
2008        };
2009        let r = match resource {
2010            Some(r) => r.0,
2011            None => ast::EntityUID::unspecified_from_eid(ast::Eid::new("resource")),
2012        };
2013        Self(ast::Request::new(p, a, r, context.0))
2014    }
2015
2016    ///Get the principal component of the request
2017    pub fn principal(&self) -> Option<&EntityUid> {
2018        match self.0.principal() {
2019            ast::EntityUIDEntry::Concrete(euid) => Some(EntityUid::ref_cast(euid.as_ref())),
2020            ast::EntityUIDEntry::Unknown => None,
2021        }
2022    }
2023
2024    ///Get the action component of the request
2025    pub fn action(&self) -> Option<&EntityUid> {
2026        match self.0.action() {
2027            ast::EntityUIDEntry::Concrete(euid) => Some(EntityUid::ref_cast(euid.as_ref())),
2028            ast::EntityUIDEntry::Unknown => None,
2029        }
2030    }
2031
2032    ///Get the resource component of the request
2033    pub fn resource(&self) -> Option<&EntityUid> {
2034        match self.0.resource() {
2035            ast::EntityUIDEntry::Concrete(euid) => Some(EntityUid::ref_cast(euid.as_ref())),
2036            ast::EntityUIDEntry::Unknown => None,
2037        }
2038    }
2039}
2040
2041/// the Context object for an authorization request
2042#[repr(transparent)]
2043#[derive(Debug, Clone, RefCast)]
2044pub struct Context(ast::Context);
2045
2046impl Context {
2047    /// Create an empty `Context`
2048    pub fn empty() -> Self {
2049        Self(ast::Context::empty())
2050    }
2051
2052    /// Create a `Context` from a map of key to "restricted expression",
2053    /// or a Vec of `(key, restricted expression)` pairs, or any other iterator
2054    /// of `(key, restricted expression)` pairs.
2055    pub fn from_pairs(pairs: impl IntoIterator<Item = (String, RestrictedExpression)>) -> Self {
2056        Self(ast::Context::from_pairs(
2057            pairs.into_iter().map(|(k, v)| (SmolStr::from(k), v.0)),
2058        ))
2059    }
2060
2061    /// Create a `Context` from a string containing JSON (which must be a JSON
2062    /// object, not any other JSON type, or you will get an error here).
2063    /// JSON here must use the `__entity` and `__extn` escapes for entity
2064    /// references, extension values, etc.
2065    ///
2066    /// If a `schema` is provided, this will inform the parsing: for instance, it
2067    /// will allow `__entity` and `__extn` escapes to be implicit, and it will error
2068    /// if attributes have the wrong types (e.g., string instead of integer).
2069    /// Since different Actions have different schemas for `Context`, you also
2070    /// must specify the `Action` for schema-based parsing.
2071    pub fn from_json_str(
2072        json: &str,
2073        schema: Option<(&Schema, &EntityUid)>,
2074    ) -> Result<Self, ContextJsonError> {
2075        let schema = schema
2076            .map(|(s, uid)| Self::get_context_schema(s, uid))
2077            .transpose()?;
2078        let context =
2079            entities::ContextJsonParser::new(schema.as_ref(), Extensions::all_available())
2080                .from_json_str(json)?;
2081        Ok(Self(context))
2082    }
2083
2084    /// Create a `Context` from a `serde_json::Value` (which must be a JSON object,
2085    /// not any other JSON type, or you will get an error here).
2086    /// JSON here must use the `__entity` and `__extn` escapes for entity
2087    /// references, extension values, etc.
2088    ///
2089    /// If a `schema` is provided, this will inform the parsing: for instance, it
2090    /// will allow `__entity` and `__extn` escapes to be implicit, and it will error
2091    /// if attributes have the wrong types (e.g., string instead of integer).
2092    /// Since different Actions have different schemas for `Context`, you also
2093    /// must specify the `Action` for schema-based parsing.
2094    pub fn from_json_value(
2095        json: serde_json::Value,
2096        schema: Option<(&Schema, &EntityUid)>,
2097    ) -> Result<Self, ContextJsonError> {
2098        let schema = schema
2099            .map(|(s, uid)| Self::get_context_schema(s, uid))
2100            .transpose()?;
2101        let context =
2102            entities::ContextJsonParser::new(schema.as_ref(), Extensions::all_available())
2103                .from_json_value(json)?;
2104        Ok(Self(context))
2105    }
2106
2107    /// Create a `Context` from a JSON file.  The JSON file must contain a JSON
2108    /// object, not any other JSON type, or you will get an error here.
2109    /// JSON here must use the `__entity` and `__extn` escapes for entity
2110    /// references, extension values, etc.
2111    ///
2112    /// If a `schema` is provided, this will inform the parsing: for instance, it
2113    /// will allow `__entity` and `__extn` escapes to be implicit, and it will error
2114    /// if attributes have the wrong types (e.g., string instead of integer).
2115    /// Since different Actions have different schemas for `Context`, you also
2116    /// must specify the `Action` for schema-based parsing.
2117    pub fn from_json_file(
2118        json: impl std::io::Read,
2119        schema: Option<(&Schema, &EntityUid)>,
2120    ) -> Result<Self, ContextJsonError> {
2121        let schema = schema
2122            .map(|(s, uid)| Self::get_context_schema(s, uid))
2123            .transpose()?;
2124        let context =
2125            entities::ContextJsonParser::new(schema.as_ref(), Extensions::all_available())
2126                .from_json_file(json)?;
2127        Ok(Self(context))
2128    }
2129
2130    /// Internal helper function to convert `(&Schema, &EntityUid)` to `impl ContextSchema`
2131    fn get_context_schema(
2132        schema: &Schema,
2133        action: &EntityUid,
2134    ) -> Result<impl ContextSchema, ContextJsonError> {
2135        schema
2136            .0
2137            .get_context_schema(&action.0)
2138            .ok_or_else(|| ContextJsonError::ActionDoesNotExist {
2139                action: action.clone(),
2140            })
2141    }
2142}
2143
2144/// Error type for parsing `Context` from JSON
2145#[derive(Debug, Error)]
2146pub enum ContextJsonError {
2147    /// Error deserializing the JSON into a Context
2148    #[error(transparent)]
2149    JsonDeserializationError(#[from] JsonDeserializationError),
2150    /// The supplied action doesn't exist in the supplied schema
2151    #[error("Action {action} doesn't exist in the supplied schema")]
2152    ActionDoesNotExist {
2153        /// UID of the action which doesn't exist
2154        action: EntityUid,
2155    },
2156}
2157
2158impl std::fmt::Display for Request {
2159    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2160        write!(f, "{}", self.0)
2161    }
2162}
2163
2164/// Result of Evaluation
2165#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
2166pub enum EvalResult {
2167    /// Boolean value
2168    Bool(bool),
2169    /// Signed integer value
2170    Long(i64),
2171    /// String value
2172    String(String),
2173    /// Entity Uid
2174    EntityUid(EntityUid),
2175    /// A first-class set
2176    Set(Set),
2177    /// A first-class anonymous record
2178    Record(Record),
2179    /// An extension value, currently limited to String results
2180    ExtensionValue(String),
2181    // ExtensionValue(std::sync::Arc<dyn InternalExtensionValue>),
2182}
2183
2184/// Sets of Cedar values
2185#[derive(Debug, Eq, PartialEq, PartialOrd, Ord)]
2186pub struct Set(BTreeSet<EvalResult>);
2187
2188impl Set {
2189    /// Iterate over the members of the set
2190    pub fn iter(&self) -> impl Iterator<Item = &EvalResult> {
2191        self.0.iter()
2192    }
2193
2194    /// Is a given element in the set
2195    pub fn contains(&self, elem: &EvalResult) -> bool {
2196        self.0.contains(elem)
2197    }
2198
2199    /// Get the number of members of the set
2200    pub fn len(&self) -> usize {
2201        self.0.len()
2202    }
2203
2204    /// Test if the set is empty
2205    pub fn is_empty(&self) -> bool {
2206        self.0.is_empty()
2207    }
2208}
2209
2210/// A record of Cedar values
2211#[derive(Debug, Eq, PartialEq, PartialOrd, Ord)]
2212pub struct Record(BTreeMap<String, EvalResult>);
2213
2214impl Record {
2215    /// Iterate over the attribute/value pairs in the record
2216    pub fn iter(&self) -> impl Iterator<Item = (&String, &EvalResult)> {
2217        self.0.iter()
2218    }
2219
2220    /// Check if a given attribute is in the record
2221    pub fn contains_attribute(&self, key: impl AsRef<str>) -> bool {
2222        self.0.contains_key(key.as_ref())
2223    }
2224
2225    /// Get a given attribute from the record
2226    pub fn get(&self, key: impl AsRef<str>) -> Option<&EvalResult> {
2227        self.0.get(key.as_ref())
2228    }
2229
2230    /// Get the number of attributes in the record
2231    pub fn len(&self) -> usize {
2232        self.0.len()
2233    }
2234
2235    /// Test if the record is empty
2236    pub fn is_empty(&self) -> bool {
2237        self.0.is_empty()
2238    }
2239}
2240
2241#[doc(hidden)]
2242impl From<ast::Value> for EvalResult {
2243    fn from(v: ast::Value) -> Self {
2244        match v {
2245            ast::Value::Lit(ast::Literal::Bool(b)) => Self::Bool(b),
2246            ast::Value::Lit(ast::Literal::Long(i)) => Self::Long(i),
2247            ast::Value::Lit(ast::Literal::String(s)) => Self::String(s.to_string()),
2248            ast::Value::Lit(ast::Literal::EntityUID(e)) => {
2249                Self::EntityUid(EntityUid(ast::EntityUID::clone(&e)))
2250            }
2251            ast::Value::Set(s) => Self::Set(Set(s
2252                .authoritative
2253                .iter()
2254                .map(|v| v.clone().into())
2255                .collect())),
2256            ast::Value::Record(r) => Self::Record(Record(
2257                r.iter()
2258                    .map(|(k, v)| (k.to_string(), v.clone().into()))
2259                    .collect(),
2260            )),
2261            ast::Value::ExtensionValue(v) => Self::ExtensionValue(v.to_string()),
2262        }
2263    }
2264}
2265impl std::fmt::Display for EvalResult {
2266    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2267        match self {
2268            Self::Bool(b) => write!(f, "{b}"),
2269            Self::Long(l) => write!(f, "{l}"),
2270            Self::String(s) => write!(f, "\"{}\"", s.escape_debug()),
2271            Self::EntityUid(uid) => write!(f, "{uid}"),
2272            Self::Set(s) => {
2273                write!(f, "[")?;
2274                for (i, ev) in s.iter().enumerate() {
2275                    write!(f, "{ev}")?;
2276                    if (i + 1) < s.len() {
2277                        write!(f, ", ")?;
2278                    }
2279                }
2280                write!(f, "]")?;
2281                Ok(())
2282            }
2283            Self::Record(r) => {
2284                write!(f, "{{")?;
2285                for (i, (k, v)) in r.iter().enumerate() {
2286                    write!(f, "\"{}\": {v}", k.escape_debug())?;
2287                    if (i + 1) < r.len() {
2288                        write!(f, ", ")?;
2289                    }
2290                }
2291                write!(f, "}}")?;
2292                Ok(())
2293            }
2294            Self::ExtensionValue(s) => write!(f, "{s}"),
2295        }
2296    }
2297}
2298
2299/// Evaluate
2300/// If evaluation results in an error (e.g., attempting to access a non-existent Entity or Record,
2301/// passing the wrong number of arguments to a function etc.), that error is returned as a String
2302pub fn eval_expression(
2303    request: &Request,
2304    entities: &Entities,
2305    expr: &Expression,
2306) -> Result<EvalResult, EvaluationError> {
2307    let all_ext = Extensions::all_available();
2308    let eval = Evaluator::new(&request.0, &entities.0, &all_ext)
2309        .map_err(|e| EvaluationError::StringMessage(e.to_string()))?;
2310    Ok(EvalResult::from(
2311        // Evaluate under the empty slot map, as an expression should not have slots
2312        eval.interpret(&expr.0, &ast::SlotEnv::new())
2313            .map_err(|e| EvaluationError::StringMessage(e.to_string()))?,
2314    ))
2315}
2316
2317#[cfg(test)]
2318#[cfg(feature = "partial-eval")]
2319mod partial_eval_test {
2320    use std::collections::HashSet;
2321
2322    use crate::{PolicyId, PolicySet, ResidualResponse};
2323
2324    #[test]
2325    fn test_pe_response_constructor() {
2326        let p: PolicySet = "permit(principal, action, resource);".parse().unwrap();
2327        let reason: HashSet<PolicyId> = std::iter::once("id1".parse().unwrap()).collect();
2328        let errors: HashSet<String> = std::iter::once("error".to_string()).collect();
2329        let a = ResidualResponse::new(p.clone(), reason.clone(), errors.clone());
2330        assert_eq!(a.diagnostics().errors, errors);
2331        assert_eq!(a.diagnostics().reason, reason);
2332        assert_eq!(a.residuals(), &p);
2333    }
2334}
2335
2336#[cfg(test)]
2337mod entity_uid_tests {
2338    use super::*;
2339
2340    /// building an `EntityUid` from components
2341    #[test]
2342    fn entity_uid_from_parts() {
2343        let entity_id = EntityId::from_str("bobby").expect("failed at constructing EntityId");
2344        let entity_type_name = EntityTypeName::from_str("Chess::Master")
2345            .expect("failed at constructing EntityTypeName");
2346        let euid = EntityUid::from_type_name_and_id(entity_type_name, entity_id);
2347        assert_eq!(euid.id().as_ref(), "bobby");
2348        assert_eq!(euid.type_name().to_string(), "Chess::Master");
2349    }
2350
2351    /// building an `EntityUid` from components, including escapes
2352    #[test]
2353    fn entity_uid_with_escape() {
2354        // EntityId contains some things that look like escapes
2355        let entity_id = EntityId::from_str(r#"bobby\'s sister:\nVeronica"#)
2356            .expect("failed at constructing EntityId");
2357        let entity_type_name = EntityTypeName::from_str("Hockey::Master")
2358            .expect("failed at constructing EntityTypeName");
2359        let euid = EntityUid::from_type_name_and_id(entity_type_name, entity_id);
2360        // these are passed through (no escape interpretation):
2361        //   the EntityId has the literal backslash characters in it
2362        assert_eq!(euid.id().as_ref(), r#"bobby\'s sister:\nVeronica"#);
2363        assert_eq!(euid.type_name().to_string(), "Hockey::Master");
2364    }
2365
2366    /// building an `EntityUid` from components, including backslashes
2367    #[test]
2368    fn entity_uid_with_backslashes() {
2369        // backslashes preceding a variety of characters
2370        let entity_id =
2371            EntityId::from_str(r#"\ \a \b \' \" \\"#).expect("failed at constructing EntityId");
2372        let entity_type_name =
2373            EntityTypeName::from_str("Test::User").expect("failed at constructing EntityTypeName");
2374        let euid = EntityUid::from_type_name_and_id(entity_type_name, entity_id);
2375        // the backslashes appear the same way in the EntityId
2376        assert_eq!(euid.id().as_ref(), r#"\ \a \b \' \" \\"#);
2377        assert_eq!(euid.type_name().to_string(), "Test::User");
2378    }
2379
2380    /// building an `EntityUid` from components, including single and double quotes (and backslashes)
2381    #[test]
2382    fn entity_uid_with_quotes() {
2383        let euid: EntityUid = EntityUid::from_type_name_and_id(
2384            EntityTypeName::from_str("Test::User").unwrap(),
2385            EntityId::from_str(r#"b'ob"by\'s sis\"ter"#).unwrap(),
2386        );
2387        // EntityId is passed through (no escape interpretation):
2388        //   the EntityId has all the same literal characters in it
2389        assert_eq!(euid.id().as_ref(), r#"b'ob"by\'s sis\"ter"#);
2390        assert_eq!(euid.type_name().to_string(), r#"Test::User"#);
2391    }
2392
2393    /// building an `EntityUid` from components, including whitespace in various places
2394    #[test]
2395    fn entity_uid_with_whitespace() {
2396        EntityTypeName::from_str("A ::   B::C").expect_err("should fail due to RFC 9");
2397        EntityTypeName::from_str(" A :: B\n::C \n  ::D\n").expect_err("should fail due to RFC 9");
2398
2399        // but embedded whitespace should be OK when parsing an actual policy
2400        let policy = Policy::from_str(
2401            r#"permit(principal == A ::   B::C :: " hi there are spaces ", action, resource);"#,
2402        )
2403        .expect("should succeed, see RFC 9");
2404        let euid = match policy.principal_constraint() {
2405            PrincipalConstraint::Eq(euid) => euid,
2406            _ => panic!("expected Eq constraint"),
2407        };
2408        assert_eq!(euid.id().as_ref(), " hi there are spaces ");
2409        assert_eq!(euid.type_name().to_string(), "A::B::C"); // expect to have been normalized
2410
2411        let policy = Policy::from_str(
2412            r#"
2413permit(principal ==  A :: B
2414    ::C
2415    :: D
2416    ::  " hi there are
2417    spaces and
2418    newlines ", action, resource);"#,
2419        )
2420        .expect("should succeed, see RFC 9");
2421        let euid = match policy.principal_constraint() {
2422            PrincipalConstraint::Eq(euid) => euid,
2423            _ => panic!("expected Eq constraint"),
2424        };
2425        assert_eq!(
2426            euid.id().as_ref(),
2427            " hi there are\n    spaces and\n    newlines "
2428        );
2429        assert_eq!(euid.type_name().to_string(), "A::B::C::D"); // expect to have been normalized
2430    }
2431
2432    #[test]
2433    fn malformed_entity_type_name_should_fail() {
2434        let result = EntityTypeName::from_str("I'm an invalid name");
2435
2436        assert!(matches!(result, Err(ParseErrors(_))));
2437        let error = result.err().unwrap();
2438        assert!(error.to_string().contains("Unrecognized token `'`"));
2439    }
2440
2441    /// parsing an `EntityUid` from string
2442    #[test]
2443    fn parse_euid() {
2444        let parsed_eid: EntityUid = r#"Test::User::"bobby""#.parse().expect("Failed to parse");
2445        assert_eq!(parsed_eid.id().as_ref(), r#"bobby"#);
2446        assert_eq!(parsed_eid.type_name().to_string(), r#"Test::User"#);
2447    }
2448
2449    /// parsing an `EntityUid` from string, including escapes
2450    #[test]
2451    fn parse_euid_with_escape() {
2452        // the EntityUid string has an escaped single-quote and escaped double-quote
2453        let parsed_eid: EntityUid = r#"Test::User::"b\'ob\"by""#.parse().expect("Failed to parse");
2454        // the escapes were interpreted:
2455        //   the EntityId has single-quote and double-quote characters (but no backslash characters)
2456        assert_eq!(parsed_eid.id().as_ref(), r#"b'ob"by"#);
2457        assert_eq!(parsed_eid.type_name().to_string(), r#"Test::User"#);
2458    }
2459
2460    /// parsing an `EntityUid` from string, including both escaped and unescaped single-quotes
2461    #[test]
2462    fn parse_euid_single_quotes() {
2463        // the EntityUid string has an unescaped and escaped single-quote
2464        let euid_str = r#"Test::User::"b'obby\'s sister""#;
2465        EntityUid::from_str(euid_str).expect_err("Should fail, not normalized -- see RFC 9");
2466        // but this should be accepted in an actual policy
2467        let policy_str = "permit(principal == ".to_string() + euid_str + ", action, resource);";
2468        let policy = Policy::from_str(&policy_str).expect("Should parse; see RFC 9");
2469        let parsed_euid = match policy.principal_constraint() {
2470            PrincipalConstraint::Eq(euid) => euid,
2471            _ => panic!("Expected an Eq constraint"),
2472        };
2473        // the escape was interpreted:
2474        //   the EntityId has both single-quote characters (but no backslash characters)
2475        assert_eq!(parsed_euid.id().as_ref(), r#"b'obby's sister"#);
2476        assert_eq!(parsed_euid.type_name().to_string(), r#"Test::User"#);
2477    }
2478
2479    /// parsing an `EntityUid` from string, including whitespace
2480    #[test]
2481    fn parse_euid_whitespace() {
2482        let euid_str = " A ::B :: C:: D \n :: \n E\n :: \"hi\"";
2483        EntityUid::from_str(euid_str).expect_err("Should fail, not normalized -- see RFC 9");
2484        // but this should be accepted in an actual policy
2485        let policy_str = "permit(principal == ".to_string() + euid_str + ", action, resource);";
2486        let policy = Policy::from_str(&policy_str).expect("Should parse; see RFC 9");
2487        let parsed_euid = match policy.principal_constraint() {
2488            PrincipalConstraint::Eq(euid) => euid,
2489            _ => panic!("Expected an Eq constraint"),
2490        };
2491        assert_eq!(parsed_euid.id().as_ref(), "hi");
2492        assert_eq!(parsed_euid.type_name().to_string(), "A::B::C::D::E"); // expect to have been normalized
2493    }
2494
2495    /// test that we can parse the `Display` output of `EntityUid`
2496    #[test]
2497    fn euid_roundtrip() {
2498        let parsed_euid: EntityUid = r#"Test::User::"b\'ob""#.parse().expect("Failed to parse");
2499        assert_eq!(parsed_euid.id().as_ref(), r#"b'ob"#);
2500        let reparsed: EntityUid = format!("{parsed_euid}")
2501            .parse()
2502            .expect("failed to roundtrip");
2503        assert_eq!(reparsed.id().as_ref(), r#"b'ob"#);
2504    }
2505}
2506
2507#[cfg(test)]
2508mod head_constraints_tests {
2509    use super::*;
2510
2511    #[test]
2512    fn principal_constraint_inline() {
2513        let p = Policy::from_str("permit(principal,action,resource);").unwrap();
2514        assert_eq!(p.principal_constraint(), PrincipalConstraint::Any);
2515        let euid = EntityUid::from_strs("T", "a");
2516        assert_eq!(euid.id().as_ref(), "a");
2517        assert_eq!(
2518            euid.type_name(),
2519            &EntityTypeName::from_str("T").expect("Failed to parse EntityTypeName")
2520        );
2521        let p =
2522            Policy::from_str("permit(principal == T::\"a\",action,resource == T::\"b\");").unwrap();
2523        assert_eq!(
2524            p.principal_constraint(),
2525            PrincipalConstraint::Eq(euid.clone())
2526        );
2527        let p = Policy::from_str("permit(principal in T::\"a\",action,resource);").unwrap();
2528        assert_eq!(p.principal_constraint(), PrincipalConstraint::In(euid));
2529    }
2530
2531    #[test]
2532    fn action_constraint_inline() {
2533        let p = Policy::from_str("permit(principal,action,resource);").unwrap();
2534        assert_eq!(p.action_constraint(), ActionConstraint::Any);
2535        let euid = EntityUid::from_strs("NN::N::Action", "a");
2536        assert_eq!(
2537            euid.type_name(),
2538            &EntityTypeName::from_str("NN::N::Action").expect("Failed to parse EntityTypeName")
2539        );
2540        let p = Policy::from_str(
2541            "permit(principal == T::\"b\",action == NN::N::Action::\"a\",resource == T::\"c\");",
2542        )
2543        .unwrap();
2544        assert_eq!(p.action_constraint(), ActionConstraint::Eq(euid.clone()));
2545        let p = Policy::from_str("permit(principal,action in [NN::N::Action::\"a\"],resource);")
2546            .unwrap();
2547        assert_eq!(p.action_constraint(), ActionConstraint::In(vec![euid]));
2548    }
2549
2550    #[test]
2551    fn resource_constraint_inline() {
2552        let p = Policy::from_str("permit(principal,action,resource);").unwrap();
2553        assert_eq!(p.resource_constraint(), ResourceConstraint::Any);
2554        let euid = EntityUid::from_strs("NN::N::T", "a");
2555        assert_eq!(
2556            euid.type_name(),
2557            &EntityTypeName::from_str("NN::N::T").expect("Failed to parse EntityTypeName")
2558        );
2559        let p =
2560            Policy::from_str("permit(principal == T::\"b\",action,resource == NN::N::T::\"a\");")
2561                .unwrap();
2562        assert_eq!(
2563            p.resource_constraint(),
2564            ResourceConstraint::Eq(euid.clone())
2565        );
2566        let p = Policy::from_str("permit(principal,action,resource in NN::N::T::\"a\");").unwrap();
2567        assert_eq!(p.resource_constraint(), ResourceConstraint::In(euid));
2568    }
2569
2570    #[test]
2571    fn principal_constraint_link() {
2572        let p = link("permit(principal,action,resource);", HashMap::new());
2573        assert_eq!(p.principal_constraint(), PrincipalConstraint::Any);
2574        let euid = EntityUid::from_strs("T", "a");
2575        let p = link(
2576            "permit(principal == T::\"a\",action,resource);",
2577            HashMap::new(),
2578        );
2579        assert_eq!(
2580            p.principal_constraint(),
2581            PrincipalConstraint::Eq(euid.clone())
2582        );
2583        let p = link(
2584            "permit(principal in T::\"a\",action,resource);",
2585            HashMap::new(),
2586        );
2587        assert_eq!(
2588            p.principal_constraint(),
2589            PrincipalConstraint::In(euid.clone())
2590        );
2591        let map: HashMap<SlotId, EntityUid> =
2592            std::iter::once((SlotId::principal(), euid.clone())).collect();
2593        let p = link(
2594            "permit(principal in ?principal,action,resource);",
2595            map.clone(),
2596        );
2597        assert_eq!(
2598            p.principal_constraint(),
2599            PrincipalConstraint::In(euid.clone())
2600        );
2601        let p = link("permit(principal == ?principal,action,resource);", map);
2602        assert_eq!(p.principal_constraint(), PrincipalConstraint::Eq(euid));
2603    }
2604
2605    #[test]
2606    fn action_constraint_link() {
2607        let p = link("permit(principal,action,resource);", HashMap::new());
2608        assert_eq!(p.action_constraint(), ActionConstraint::Any);
2609        let euid = EntityUid::from_strs("Action", "a");
2610        let p = link(
2611            "permit(principal,action == Action::\"a\",resource);",
2612            HashMap::new(),
2613        );
2614        assert_eq!(p.action_constraint(), ActionConstraint::Eq(euid.clone()));
2615        let p = link(
2616            "permit(principal,action in [Action::\"a\",Action::\"b\"],resource);",
2617            HashMap::new(),
2618        );
2619        assert_eq!(
2620            p.action_constraint(),
2621            ActionConstraint::In(vec![euid, EntityUid::from_strs("Action", "b"),])
2622        );
2623    }
2624
2625    #[test]
2626    fn resource_constraint_link() {
2627        let p = link("permit(principal,action,resource);", HashMap::new());
2628        assert_eq!(p.resource_constraint(), ResourceConstraint::Any);
2629        let euid = EntityUid::from_strs("T", "a");
2630        let p = link(
2631            "permit(principal,action,resource == T::\"a\");",
2632            HashMap::new(),
2633        );
2634        assert_eq!(
2635            p.resource_constraint(),
2636            ResourceConstraint::Eq(euid.clone())
2637        );
2638        let p = link(
2639            "permit(principal,action,resource in T::\"a\");",
2640            HashMap::new(),
2641        );
2642        assert_eq!(
2643            p.resource_constraint(),
2644            ResourceConstraint::In(euid.clone())
2645        );
2646        let map: HashMap<SlotId, EntityUid> =
2647            std::iter::once((SlotId::resource(), euid.clone())).collect();
2648        let p = link(
2649            "permit(principal,action,resource in ?resource);",
2650            map.clone(),
2651        );
2652        assert_eq!(
2653            p.resource_constraint(),
2654            ResourceConstraint::In(euid.clone())
2655        );
2656        let p = link("permit(principal,action,resource == ?resource);", map);
2657        assert_eq!(p.resource_constraint(), ResourceConstraint::Eq(euid));
2658    }
2659
2660    fn link(src: &str, values: HashMap<SlotId, EntityUid>) -> Policy {
2661        let mut pset = PolicySet::new();
2662        let template = Template::parse(Some("Id".to_string()), src).unwrap();
2663
2664        pset.add_template(template).unwrap();
2665
2666        let link_id = PolicyId::from_str("link").unwrap();
2667        pset.link(PolicyId::from_str("Id").unwrap(), link_id.clone(), values)
2668            .unwrap();
2669        pset.policy(&link_id).unwrap().clone()
2670    }
2671}
2672
2673/// Tests in this module are adapted from Core's `policy_set.rs` tests
2674#[cfg(test)]
2675mod policy_set_tests {
2676    use super::*;
2677    use ast::LinkingError;
2678    use cool_asserts::assert_matches;
2679
2680    #[test]
2681    fn link_conflicts() {
2682        let mut pset = PolicySet::new();
2683        let p1 = Policy::parse(Some("id".into()), "permit(principal,action,resource);")
2684            .expect("Failed to parse");
2685        pset.add(p1).expect("Failed to add");
2686        let template = Template::parse(
2687            Some("t".into()),
2688            "permit(principal == ?principal, action, resource);",
2689        )
2690        .expect("Failed to parse");
2691        pset.add_template(template).expect("Add failed");
2692
2693        let env: HashMap<SlotId, EntityUid> =
2694            std::iter::once((SlotId::principal(), EntityUid::from_strs("Test", "test"))).collect();
2695
2696        let r = pset.link(
2697            PolicyId::from_str("t").unwrap(),
2698            PolicyId::from_str("id").unwrap(),
2699            env,
2700        );
2701
2702        match r {
2703            Ok(_) => panic!("Should have failed due to conflict"),
2704            Err(PolicySetError::LinkingError(LinkingError::PolicyIdConflict)) => (),
2705            Err(e) => panic!("Incorrect error: {e}"),
2706        };
2707    }
2708
2709    #[test]
2710    fn policyset_add() {
2711        let mut pset = PolicySet::new();
2712        let static_policy = Policy::parse(Some("id".into()), "permit(principal,action,resource);")
2713            .expect("Failed to parse");
2714        pset.add(static_policy).expect("Failed to add");
2715
2716        let template = Template::parse(
2717            Some("t".into()),
2718            "permit(principal == ?principal, action, resource);",
2719        )
2720        .expect("Failed to parse");
2721        pset.add_template(template).expect("Failed to add");
2722
2723        let env1: HashMap<SlotId, EntityUid> =
2724            std::iter::once((SlotId::principal(), EntityUid::from_strs("Test", "test1"))).collect();
2725        pset.link(
2726            PolicyId::from_str("t").unwrap(),
2727            PolicyId::from_str("link").unwrap(),
2728            env1,
2729        )
2730        .expect("Failed to link");
2731
2732        let env2: HashMap<SlotId, EntityUid> =
2733            std::iter::once((SlotId::principal(), EntityUid::from_strs("Test", "test2"))).collect();
2734
2735        let err = pset
2736            .link(
2737                PolicyId::from_str("t").unwrap(),
2738                PolicyId::from_str("link").unwrap(),
2739                env2.clone(),
2740            )
2741            .expect_err("Should have failed due to conflict with existing link id");
2742        match err {
2743            PolicySetError::LinkingError(_) => (),
2744            e => panic!("Wrong error: {e}"),
2745        }
2746
2747        pset.link(
2748            PolicyId::from_str("t").unwrap(),
2749            PolicyId::from_str("link2").unwrap(),
2750            env2,
2751        )
2752        .expect("Failed to link");
2753
2754        let template2 = Template::parse(
2755            Some("t".into()),
2756            "forbid(principal, action, resource == ?resource);",
2757        )
2758        .expect("Failed to parse");
2759        pset.add_template(template2)
2760            .expect_err("should have failed due to conflict on template id");
2761        let template2 = Template::parse(
2762            Some("t2".into()),
2763            "forbid(principal, action, resource == ?resource);",
2764        )
2765        .expect("Failed to parse");
2766        pset.add_template(template2)
2767            .expect("Failed to add template");
2768        let env3: HashMap<SlotId, EntityUid> =
2769            std::iter::once((SlotId::resource(), EntityUid::from_strs("Test", "test3"))).collect();
2770
2771        pset.link(
2772            PolicyId::from_str("t").unwrap(),
2773            PolicyId::from_str("unique3").unwrap(),
2774            env3.clone(),
2775        )
2776        .expect_err("should have failed due to conflict on template id");
2777
2778        pset.link(
2779            PolicyId::from_str("t2").unwrap(),
2780            PolicyId::from_str("unique3").unwrap(),
2781            env3,
2782        )
2783        .expect("should succeed with unique ids");
2784    }
2785
2786    #[test]
2787    fn pset_requests() {
2788        let template = Template::parse(
2789            Some("template".into()),
2790            "permit(principal == ?principal, action, resource);",
2791        )
2792        .expect("Template Parse Failure");
2793        let static_policy = Policy::parse(
2794            Some("static".into()),
2795            "permit(principal, action, resource);",
2796        )
2797        .expect("Static parse failure");
2798        let mut pset = PolicySet::new();
2799        pset.add_template(template).unwrap();
2800        pset.add(static_policy).unwrap();
2801        pset.link(
2802            PolicyId::from_str("template").unwrap(),
2803            PolicyId::from_str("linked").unwrap(),
2804            std::iter::once((SlotId::principal(), EntityUid::from_strs("Test", "test"))).collect(),
2805        )
2806        .expect("Link failure");
2807
2808        assert_eq!(pset.templates().count(), 1);
2809        assert_eq!(pset.policies().count(), 2);
2810        assert_eq!(pset.policies().filter(|p| p.is_static()).count(), 1);
2811
2812        assert_eq!(
2813            pset.template(&"template".parse().unwrap())
2814                .expect("lookup failed")
2815                .id(),
2816            &"template".parse().unwrap()
2817        );
2818        assert_eq!(
2819            pset.policy(&"static".parse().unwrap())
2820                .expect("lookup failed")
2821                .id(),
2822            &"static".parse().unwrap()
2823        );
2824        assert_eq!(
2825            pset.policy(&"linked".parse().unwrap())
2826                .expect("lookup failed")
2827                .id(),
2828            &"linked".parse().unwrap()
2829        );
2830    }
2831
2832    #[test]
2833    fn link_static_policy() {
2834        // Linking the `PolicyId` of a static policy should not be allowed.
2835        // Attempting it should cause an `ExpectedTemplate` error.
2836        let static_policy = Policy::parse(
2837            Some("static".into()),
2838            "permit(principal, action, resource);",
2839        )
2840        .expect("Static parse failure");
2841        let mut pset = PolicySet::new();
2842        pset.add(static_policy).unwrap();
2843
2844        let result = pset.link(
2845            PolicyId::from_str("static").unwrap(),
2846            PolicyId::from_str("linked").unwrap(),
2847            HashMap::new(),
2848        );
2849        assert_matches!(result, Err(PolicySetError::ExpectedTemplate));
2850    }
2851
2852    #[test]
2853    fn link_linked_policy() {
2854        let template = Template::parse(
2855            Some("template".into()),
2856            "permit(principal == ?principal, action, resource);",
2857        )
2858        .expect("Template Parse Failure");
2859        let mut pset = PolicySet::new();
2860        pset.add_template(template).unwrap();
2861
2862        pset.link(
2863            PolicyId::from_str("template").unwrap(),
2864            PolicyId::from_str("linked").unwrap(),
2865            std::iter::once((SlotId::principal(), EntityUid::from_strs("Test", "test"))).collect(),
2866        )
2867        .unwrap();
2868
2869        let result = pset.link(
2870            PolicyId::from_str("linked").unwrap(),
2871            PolicyId::from_str("linked2").unwrap(),
2872            HashMap::new(),
2873        );
2874        assert_matches!(
2875            result,
2876            Err(PolicySetError::LinkingError(LinkingError::NoSuchTemplate(
2877                _
2878            )))
2879        );
2880    }
2881}
2882
2883#[cfg(test)]
2884mod schema_tests {
2885    use super::*;
2886    use cool_asserts::assert_matches;
2887    use serde_json::json;
2888
2889    /// A minimal test that a valid Schema parses
2890    #[test]
2891    fn valid_schema() {
2892        Schema::from_json_value(json!(
2893        { "": {
2894            "entityTypes": {
2895                "Photo": {
2896                    "memberOfTypes": [ "Album" ],
2897                    "shape": {
2898                        "type": "Record",
2899                        "attributes": {
2900                            "foo": {
2901                                "type": "Boolean",
2902                                "required": false
2903                            }
2904                        }
2905                    }
2906                },
2907                "Album": {
2908                    "memberOfTypes": [ ],
2909                    "shape": {
2910                        "type": "Record",
2911                        "attributes": {
2912                            "foo": {
2913                                "type": "Boolean",
2914                                "required": false
2915                            }
2916                        }
2917                    }
2918                }
2919            },
2920            "actions": {
2921                "view": {
2922                    "appliesTo": {
2923                        "principalTypes": ["Photo", "Album"],
2924                        "resourceTypes": ["Photo"]
2925                    }
2926                }
2927            }
2928        }}))
2929        .expect("schema should be valid");
2930    }
2931
2932    /// Test that an invalid schema returns the appropriate error
2933    #[test]
2934    fn invalid_schema() {
2935        assert_matches!(
2936            Schema::from_json_value(json!(
2937                // Written as a string because duplicate entity types are detected
2938                // by the serde-json string parser.
2939                r#""{"": {
2940                "entityTypes": {
2941                    "Photo": {
2942                        "memberOfTypes": [ "Album" ],
2943                        "shape": {
2944                            "type": "Record",
2945                            "attributes": {
2946                                "foo": {
2947                                    "type": "Boolean",
2948                                    "required": false
2949                                }
2950                            }
2951                        }
2952                    },
2953                    "Album": {
2954                        "memberOfTypes": [ ],
2955                        "shape": {
2956                            "type": "Record",
2957                            "attributes": {
2958                                "foo": {
2959                                    "type": "Boolean",
2960                                    "required": false
2961                                }
2962                            }
2963                        }
2964                    },
2965                    "Photo": {
2966                        "memberOfTypes": [ "Album" ],
2967                        "shape": {
2968                            "type": "Record",
2969                            "attributes": {
2970                                "foo": {
2971                                    "type": "Boolean",
2972                                    "required": false
2973                                }
2974                            }
2975                        }
2976                    }
2977                },
2978                "actions": {
2979                    "view": {
2980                        "appliesTo": {
2981                            "principalTypes": ["Photo", "Album"],
2982                            "resourceTypes": ["Photo"]
2983                        }
2984                    }
2985                }
2986            }}"#
2987            )),
2988            Err(SchemaError::ParseJson(_))
2989        );
2990    }
2991}
2992
2993#[cfg(test)]
2994mod ancestors_tests {
2995    use super::*;
2996
2997    #[test]
2998    fn test_ancestors() {
2999        let a_euid: EntityUid = EntityUid::from_strs("test", "A");
3000        let b_euid: EntityUid = EntityUid::from_strs("test", "b");
3001        let c_euid: EntityUid = EntityUid::from_strs("test", "C");
3002        let a = Entity::new(a_euid.clone(), HashMap::new(), HashSet::new());
3003        let b = Entity::new(
3004            b_euid.clone(),
3005            HashMap::new(),
3006            std::iter::once(a_euid.clone()).collect(),
3007        );
3008        let c = Entity::new(
3009            c_euid.clone(),
3010            HashMap::new(),
3011            std::iter::once(b_euid.clone()).collect(),
3012        );
3013        let es = Entities::from_entities([a, b, c]).unwrap();
3014        let ans = es.ancestors(&c_euid).unwrap().collect::<HashSet<_>>();
3015        assert_eq!(ans.len(), 2);
3016        assert!(ans.contains(&b_euid));
3017        assert!(ans.contains(&a_euid));
3018    }
3019}
3020
3021/// The main unit tests for schema-based parsing live here, as they require both
3022/// the Validator and Core packages working together.
3023///
3024/// (Core has similar tests, but using a stubbed implementation of Schema.)
3025#[cfg(test)]
3026mod schema_based_parsing_tests {
3027    use std::assert_eq;
3028
3029    use super::*;
3030    use cool_asserts::assert_matches;
3031    use serde_json::json;
3032
3033    /// Simple test that exercises a variety of attribute types.
3034    #[test]
3035    #[allow(clippy::too_many_lines)]
3036    #[allow(clippy::cognitive_complexity)]
3037    fn attr_types() {
3038        let schema = Schema::from_json_value(json!(
3039        {"": {
3040            "entityTypes": {
3041                "Employee": {
3042                    "memberOfTypes": [],
3043                    "shape": {
3044                        "type": "Record",
3045                        "attributes": {
3046                            "isFullTime": { "type": "Boolean" },
3047                            "numDirectReports": { "type": "Long" },
3048                            "department": { "type": "String" },
3049                            "manager": { "type": "Entity", "name": "Employee" },
3050                            "hr_contacts": { "type": "Set", "element": {
3051                                "type": "Entity", "name": "HR" } },
3052                            "json_blob": { "type": "Record", "attributes": {
3053                                "inner1": { "type": "Boolean" },
3054                                "inner2": { "type": "String" },
3055                                "inner3": { "type": "Record", "attributes": {
3056                                    "innerinner": { "type": "Entity", "name": "Employee" }
3057                                }}
3058                            }},
3059                            "home_ip": { "type": "Extension", "name": "ipaddr" },
3060                            "work_ip": { "type": "Extension", "name": "ipaddr" },
3061                            "trust_score": { "type": "Extension", "name": "decimal" },
3062                            "tricky": { "type": "Record", "attributes": {
3063                                "type": { "type": "String" },
3064                                "id": { "type": "String" }
3065                            }}
3066                        }
3067                    }
3068                },
3069                "HR": {
3070                    "memberOfTypes": []
3071                }
3072            },
3073            "actions": {
3074                "view": { }
3075            }
3076        }}
3077        ))
3078        .expect("should be a valid schema");
3079
3080        let entitiesjson = json!(
3081            [
3082                {
3083                    "uid": { "type": "Employee", "id": "12UA45" },
3084                    "attrs": {
3085                        "isFullTime": true,
3086                        "numDirectReports": 3,
3087                        "department": "Sales",
3088                        "manager": { "type": "Employee", "id": "34FB87" },
3089                        "hr_contacts": [
3090                            { "type": "HR", "id": "aaaaa" },
3091                            { "type": "HR", "id": "bbbbb" }
3092                        ],
3093                        "json_blob": {
3094                            "inner1": false,
3095                            "inner2": "-*/",
3096                            "inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
3097                        },
3098                        "home_ip": "222.222.222.101",
3099                        "work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
3100                        "trust_score": "5.7",
3101                        "tricky": { "type": "Employee", "id": "34FB87" }
3102                    },
3103                    "parents": []
3104                }
3105            ]
3106        );
3107        // without schema-based parsing, `home_ip` and `trust_score` are
3108        // strings, `manager` and `work_ip` are Records, `hr_contacts` contains
3109        // Records, and `json_blob.inner3.innerinner` is a Record
3110        let parsed = Entities::from_json_value(entitiesjson.clone(), None)
3111            .expect("Should parse without error");
3112        assert_eq!(parsed.iter().count(), 1);
3113        let parsed = parsed
3114            .get(&EntityUid::from_strs("Employee", "12UA45"))
3115            .expect("that should be the employee id");
3116        assert_eq!(
3117            parsed.attr("home_ip"),
3118            Some(Ok(EvalResult::String("222.222.222.101".into())))
3119        );
3120        assert_eq!(
3121            parsed.attr("trust_score"),
3122            Some(Ok(EvalResult::String("5.7".into())))
3123        );
3124        assert!(matches!(
3125            parsed.attr("manager"),
3126            Some(Ok(EvalResult::Record(_)))
3127        ));
3128        assert!(matches!(
3129            parsed.attr("work_ip"),
3130            Some(Ok(EvalResult::Record(_)))
3131        ));
3132        {
3133            let Some(Ok(EvalResult::Set(set))) = parsed.attr("hr_contacts") else {
3134                panic!("expected hr_contacts attr to exist and be a Set")
3135            };
3136            let contact = set.iter().next().expect("should be at least one contact");
3137            assert!(matches!(contact, EvalResult::Record(_)));
3138        };
3139        {
3140            let Some(Ok(EvalResult::Record(rec))) = parsed.attr("json_blob") else {
3141                panic!("expected json_blob attr to exist and be a Record")
3142            };
3143            let inner3 = rec.get("inner3").expect("expected inner3 attr to exist");
3144            let EvalResult::Record(rec) = inner3 else {
3145                panic!("expected inner3 to be a Record")
3146            };
3147            let innerinner = rec
3148                .get("innerinner")
3149                .expect("expected innerinner attr to exist");
3150            assert!(matches!(innerinner, EvalResult::Record(_)));
3151        };
3152        // but with schema-based parsing, we get these other types
3153        let parsed = Entities::from_json_value(entitiesjson, Some(&schema))
3154            .expect("Should parse without error");
3155        assert_eq!(parsed.iter().count(), 1);
3156        let parsed = parsed
3157            .get(&EntityUid::from_strs("Employee", "12UA45"))
3158            .expect("that should be the employee id");
3159        assert_eq!(parsed.attr("isFullTime"), Some(Ok(EvalResult::Bool(true))));
3160        assert_eq!(
3161            parsed.attr("numDirectReports"),
3162            Some(Ok(EvalResult::Long(3)))
3163        );
3164        assert_eq!(
3165            parsed.attr("department"),
3166            Some(Ok(EvalResult::String("Sales".into())))
3167        );
3168        assert_eq!(
3169            parsed.attr("manager"),
3170            Some(Ok(EvalResult::EntityUid(EntityUid::from_strs(
3171                "Employee", "34FB87"
3172            ))))
3173        );
3174        {
3175            let Some(Ok(EvalResult::Set(set))) = parsed.attr("hr_contacts") else {
3176                panic!("expected hr_contacts attr to exist and be a Set")
3177            };
3178            let contact = set.iter().next().expect("should be at least one contact");
3179            assert!(matches!(contact, EvalResult::EntityUid(_)));
3180        };
3181        {
3182            let Some(Ok(EvalResult::Record(rec))) = parsed.attr("json_blob") else {
3183                panic!("expected json_blob attr to exist and be a Record")
3184            };
3185            let inner3 = rec.get("inner3").expect("expected inner3 attr to exist");
3186            let EvalResult::Record(rec) = inner3 else {
3187                panic!("expected inner3 to be a Record")
3188            };
3189            let innerinner = rec
3190                .get("innerinner")
3191                .expect("expected innerinner attr to exist");
3192            assert!(matches!(innerinner, EvalResult::EntityUid(_)));
3193        };
3194        assert_eq!(
3195            parsed.attr("home_ip"),
3196            Some(Ok(EvalResult::ExtensionValue("222.222.222.101/32".into())))
3197        );
3198        assert_eq!(
3199            parsed.attr("work_ip"),
3200            Some(Ok(EvalResult::ExtensionValue("2.2.2.0/24".into())))
3201        );
3202        assert_eq!(
3203            parsed.attr("trust_score"),
3204            Some(Ok(EvalResult::ExtensionValue("5.7000".into())))
3205        );
3206
3207        // simple type mismatch with expected type
3208        let entitiesjson = json!(
3209            [
3210                {
3211                    "uid": { "type": "Employee", "id": "12UA45" },
3212                    "attrs": {
3213                        "isFullTime": true,
3214                        "numDirectReports": "3",
3215                        "department": "Sales",
3216                        "manager": { "type": "Employee", "id": "34FB87" },
3217                        "hr_contacts": [
3218                            { "type": "HR", "id": "aaaaa" },
3219                            { "type": "HR", "id": "bbbbb" }
3220                        ],
3221                        "json_blob": {
3222                            "inner1": false,
3223                            "inner2": "-*/",
3224                            "inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
3225                        },
3226                        "home_ip": "222.222.222.101",
3227                        "work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
3228                        "trust_score": "5.7",
3229                        "tricky": { "type": "Employee", "id": "34FB87" }
3230                    },
3231                    "parents": []
3232                }
3233            ]
3234        );
3235        let err = Entities::from_json_value(entitiesjson, Some(&schema))
3236            .expect_err("should fail due to type mismatch on numDirectReports");
3237        assert!(
3238            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"#),
3239            "actual error message was {err}"
3240        );
3241
3242        // another simple type mismatch with expected type
3243        let entitiesjson = json!(
3244            [
3245                {
3246                    "uid": { "type": "Employee", "id": "12UA45" },
3247                    "attrs": {
3248                        "isFullTime": true,
3249                        "numDirectReports": 3,
3250                        "department": "Sales",
3251                        "manager": "34FB87",
3252                        "hr_contacts": [
3253                            { "type": "HR", "id": "aaaaa" },
3254                            { "type": "HR", "id": "bbbbb" }
3255                        ],
3256                        "json_blob": {
3257                            "inner1": false,
3258                            "inner2": "-*/",
3259                            "inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
3260                        },
3261                        "home_ip": "222.222.222.101",
3262                        "work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
3263                        "trust_score": "5.7",
3264                        "tricky": { "type": "Employee", "id": "34FB87" }
3265                    },
3266                    "parents": []
3267                }
3268            ]
3269        );
3270        let err = Entities::from_json_value(entitiesjson, Some(&schema))
3271            .expect_err("should fail due to type mismatch on manager");
3272        assert!(
3273            err.to_string()
3274                .contains(r#"in attribute "manager" on Employee::"12UA45", expected a literal entity reference, but got "34FB87""#),
3275            "actual error message was {err}"
3276        );
3277
3278        // type mismatch where we expect a set and get just a single element
3279        let entitiesjson = json!(
3280            [
3281                {
3282                    "uid": { "type": "Employee", "id": "12UA45" },
3283                    "attrs": {
3284                        "isFullTime": true,
3285                        "numDirectReports": 3,
3286                        "department": "Sales",
3287                        "manager": { "type": "Employee", "id": "34FB87" },
3288                        "hr_contacts": { "type": "HR", "id": "aaaaa" },
3289                        "json_blob": {
3290                            "inner1": false,
3291                            "inner2": "-*/",
3292                            "inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
3293                        },
3294                        "home_ip": "222.222.222.101",
3295                        "work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
3296                        "trust_score": "5.7",
3297                        "tricky": { "type": "Employee", "id": "34FB87" }
3298                    },
3299                    "parents": []
3300                }
3301            ]
3302        );
3303        let err = Entities::from_json_value(entitiesjson, Some(&schema))
3304            .expect_err("should fail due to type mismatch on hr_contacts");
3305        assert!(
3306            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: ("#),
3307            "actual error message was {err}"
3308        );
3309
3310        // type mismatch where we just get the wrong entity type
3311        let entitiesjson = json!(
3312            [
3313                {
3314                    "uid": { "type": "Employee", "id": "12UA45" },
3315                    "attrs": {
3316                        "isFullTime": true,
3317                        "numDirectReports": 3,
3318                        "department": "Sales",
3319                        "manager": { "type": "HR", "id": "34FB87" },
3320                        "hr_contacts": [
3321                            { "type": "HR", "id": "aaaaa" },
3322                            { "type": "HR", "id": "bbbbb" }
3323                        ],
3324                        "json_blob": {
3325                            "inner1": false,
3326                            "inner2": "-*/",
3327                            "inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
3328                        },
3329                        "home_ip": "222.222.222.101",
3330                        "work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
3331                        "trust_score": "5.7",
3332                        "tricky": { "type": "Employee", "id": "34FB87" }
3333                    },
3334                    "parents": []
3335                }
3336            ]
3337        );
3338        let err = Entities::from_json_value(entitiesjson, Some(&schema))
3339            .expect_err("should fail due to type mismatch on manager");
3340        assert!(
3341            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)"#),
3342            "actual error message was {err}"
3343        );
3344
3345        // type mismatch where we're expecting an extension type and get a
3346        // different extension type
3347        let entitiesjson = json!(
3348            [
3349                {
3350                    "uid": { "type": "Employee", "id": "12UA45" },
3351                    "attrs": {
3352                        "isFullTime": true,
3353                        "numDirectReports": 3,
3354                        "department": "Sales",
3355                        "manager": { "type": "Employee", "id": "34FB87" },
3356                        "hr_contacts": [
3357                            { "type": "HR", "id": "aaaaa" },
3358                            { "type": "HR", "id": "bbbbb" }
3359                        ],
3360                        "json_blob": {
3361                            "inner1": false,
3362                            "inner2": "-*/",
3363                            "inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
3364                        },
3365                        "home_ip": { "fn": "decimal", "arg": "3.33" },
3366                        "work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
3367                        "trust_score": "5.7",
3368                        "tricky": { "type": "Employee", "id": "34FB87" }
3369                    },
3370                    "parents": []
3371                }
3372            ]
3373        );
3374        let err = Entities::from_json_value(entitiesjson, Some(&schema))
3375            .expect_err("should fail due to type mismatch on home_ip");
3376        assert!(
3377            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"#),
3378            "actual error message was {err}"
3379        );
3380
3381        // missing a record attribute entirely
3382        let entitiesjson = json!(
3383            [
3384                {
3385                    "uid": { "type": "Employee", "id": "12UA45" },
3386                    "attrs": {
3387                        "isFullTime": true,
3388                        "numDirectReports": 3,
3389                        "department": "Sales",
3390                        "manager": { "type": "Employee", "id": "34FB87" },
3391                        "hr_contacts": [
3392                            { "type": "HR", "id": "aaaaa" },
3393                            { "type": "HR", "id": "bbbbb" }
3394                        ],
3395                        "json_blob": {
3396                            "inner1": false,
3397                            "inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
3398                        },
3399                        "home_ip": "222.222.222.101",
3400                        "work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
3401                        "trust_score": "5.7",
3402                        "tricky": { "type": "Employee", "id": "34FB87" }
3403                    },
3404                    "parents": []
3405                }
3406            ]
3407        );
3408        let err = Entities::from_json_value(entitiesjson, Some(&schema))
3409            .expect_err("should fail due to missing attribute \"inner2\"");
3410        assert!(
3411            err.to_string().contains(r#"in attribute "json_blob" on Employee::"12UA45", expected the record to have an attribute "inner2", but it didn't"#),
3412            "actual error message was {err}"
3413        );
3414
3415        // record attribute has the wrong type
3416        let entitiesjson = json!(
3417            [
3418                {
3419                    "uid": { "type": "Employee", "id": "12UA45" },
3420                    "attrs": {
3421                        "isFullTime": true,
3422                        "numDirectReports": 3,
3423                        "department": "Sales",
3424                        "manager": { "type": "Employee", "id": "34FB87" },
3425                        "hr_contacts": [
3426                            { "type": "HR", "id": "aaaaa" },
3427                            { "type": "HR", "id": "bbbbb" }
3428                        ],
3429                        "json_blob": {
3430                            "inner1": 33,
3431                            "inner2": "-*/",
3432                            "inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
3433                        },
3434                        "home_ip": "222.222.222.101",
3435                        "work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
3436                        "trust_score": "5.7",
3437                        "tricky": { "type": "Employee", "id": "34FB87" }
3438                    },
3439                    "parents": []
3440                }
3441            ]
3442        );
3443        let err = Entities::from_json_value(entitiesjson, Some(&schema))
3444            .expect_err("should fail due to type mismatch on attribute \"inner1\"");
3445        assert!(
3446            err.to_string().contains(r#"in attribute "json_blob" on Employee::"12UA45", type mismatch: attribute was expected to have type record with attributes: "#),
3447            "actual error message was {err}"
3448        );
3449
3450        let entitiesjson = json!(
3451            [
3452                {
3453                    "uid": { "__entity": { "type": "Employee", "id": "12UA45" } },
3454                    "attrs": {
3455                        "isFullTime": true,
3456                        "numDirectReports": 3,
3457                        "department": "Sales",
3458                        "manager": { "__entity": { "type": "Employee", "id": "34FB87" } },
3459                        "hr_contacts": [
3460                            { "type": "HR", "id": "aaaaa" },
3461                            { "type": "HR", "id": "bbbbb" }
3462                        ],
3463                        "json_blob": {
3464                            "inner1": false,
3465                            "inner2": "-*/",
3466                            "inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
3467                        },
3468                        "home_ip": { "__extn": { "fn": "ip", "arg": "222.222.222.101" } },
3469                        "work_ip": { "__extn": { "fn": "ip", "arg": "2.2.2.0/24" } },
3470                        "trust_score": { "__extn": { "fn": "decimal", "arg": "5.7" } },
3471                        "tricky": { "type": "Employee", "id": "34FB87" }
3472                    },
3473                    "parents": []
3474                }
3475            ]
3476        );
3477
3478        Entities::from_json_value(entitiesjson, Some(&schema))
3479            .expect("this version with explicit __entity and __extn escapes should also pass");
3480    }
3481
3482    /// Test that involves namespaced entity types
3483    #[test]
3484    fn namespaces() {
3485        let schema = Schema::from_str(
3486            r#"
3487        {"XYZCorp": {
3488            "entityTypes": {
3489                "Employee": {
3490                    "memberOfTypes": [],
3491                    "shape": {
3492                        "type": "Record",
3493                        "attributes": {
3494                            "isFullTime": { "type": "Boolean" },
3495                            "department": { "type": "String" },
3496                            "manager": {
3497                                "type": "Entity",
3498                                "name": "XYZCorp::Employee"
3499                            }
3500                        }
3501                    }
3502                }
3503            },
3504            "actions": {
3505                "view": {}
3506            }
3507        }}
3508        "#,
3509        )
3510        .expect("should be a valid schema");
3511
3512        let entitiesjson = json!(
3513            [
3514                {
3515                    "uid": { "type": "XYZCorp::Employee", "id": "12UA45" },
3516                    "attrs": {
3517                        "isFullTime": true,
3518                        "department": "Sales",
3519                        "manager": { "type": "XYZCorp::Employee", "id": "34FB87" }
3520                    },
3521                    "parents": []
3522                }
3523            ]
3524        );
3525        let parsed = Entities::from_json_value(entitiesjson, Some(&schema))
3526            .expect("Should parse without error");
3527        assert_eq!(parsed.iter().count(), 1);
3528        let parsed = parsed
3529            .get(&EntityUid::from_strs("XYZCorp::Employee", "12UA45"))
3530            .expect("that should be the employee type and id");
3531        assert_eq!(parsed.attr("isFullTime"), Some(Ok(EvalResult::Bool(true))));
3532        assert_eq!(
3533            parsed.attr("department"),
3534            Some(Ok(EvalResult::String("Sales".into())))
3535        );
3536        assert_eq!(
3537            parsed.attr("manager"),
3538            Some(Ok(EvalResult::EntityUid(EntityUid::from_strs(
3539                "XYZCorp::Employee",
3540                "34FB87"
3541            ))))
3542        );
3543
3544        let entitiesjson = json!(
3545            [
3546                {
3547                    "uid": { "type": "XYZCorp::Employee", "id": "12UA45" },
3548                    "attrs": {
3549                        "isFullTime": true,
3550                        "department": "Sales",
3551                        "manager": { "type": "Employee", "id": "34FB87" }
3552                    },
3553                    "parents": []
3554                }
3555            ]
3556        );
3557        let err = Entities::from_json_value(entitiesjson, Some(&schema))
3558            .expect_err("should fail due to manager being wrong entity type (missing namespace)");
3559        assert!(
3560            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)"#),
3561            "actual error message was {err}"
3562        );
3563    }
3564
3565    /// Test that involves optional attributes
3566    #[test]
3567    fn optional_attrs() {
3568        let schema = Schema::from_str(
3569            r#"
3570        {"": {
3571            "entityTypes": {
3572                "Employee": {
3573                    "memberOfTypes": [],
3574                    "shape": {
3575                        "type": "Record",
3576                        "attributes": {
3577                            "isFullTime": { "type": "Boolean" },
3578                            "department": { "type": "String", "required": false },
3579                            "manager": { "type": "Entity", "name": "Employee" }
3580                        }
3581                    }
3582                }
3583            },
3584            "actions": {
3585                "view": {}
3586            }
3587        }}
3588        "#,
3589        )
3590        .expect("should be a valid schema");
3591
3592        // all good here
3593        let entitiesjson = json!(
3594            [
3595                {
3596                    "uid": { "type": "Employee", "id": "12UA45" },
3597                    "attrs": {
3598                        "isFullTime": true,
3599                        "department": "Sales",
3600                        "manager": { "type": "Employee", "id": "34FB87" }
3601                    },
3602                    "parents": []
3603                }
3604            ]
3605        );
3606        let parsed = Entities::from_json_value(entitiesjson, Some(&schema))
3607            .expect("Should parse without error");
3608        assert_eq!(parsed.iter().count(), 1);
3609
3610        // "department" shouldn't be required
3611        let entitiesjson = json!(
3612            [
3613                {
3614                    "uid": { "type": "Employee", "id": "12UA45" },
3615                    "attrs": {
3616                        "isFullTime": true,
3617                        "manager": { "type": "Employee", "id": "34FB87" }
3618                    },
3619                    "parents": []
3620                }
3621            ]
3622        );
3623        let parsed = Entities::from_json_value(entitiesjson, Some(&schema))
3624            .expect("Should parse without error");
3625        assert_eq!(parsed.iter().count(), 1);
3626    }
3627
3628    /// Test that involves open entities
3629    #[test]
3630    #[should_panic(
3631        expected = "UnsupportedSchemaFeature(\"Records and entities with additional attributes are not yet implemented.\")"
3632    )]
3633    fn open_entities() {
3634        let schema = Schema::from_str(
3635            r#"
3636        {"": {
3637            "entityTypes": {
3638                "Employee": {
3639                    "memberOfTypes": [],
3640                    "shape": {
3641                        "type": "Record",
3642                        "attributes": {
3643                            "isFullTime": { "type": "Boolean" },
3644                            "department": { "type": "String", "required": false },
3645                            "manager": { "type": "Entity", "name": "Employee" }
3646                        },
3647                        "additionalAttributes": true
3648                    }
3649                }
3650            },
3651            "actions": {
3652                "view": {}
3653            }
3654        }}
3655        "#,
3656        )
3657        .expect("should be a valid schema");
3658
3659        // all good here
3660        let entitiesjson = json!(
3661            [
3662                {
3663                    "uid": { "type": "Employee", "id": "12UA45" },
3664                    "attrs": {
3665                        "isFullTime": true,
3666                        "department": "Sales",
3667                        "manager": { "type": "Employee", "id": "34FB87" }
3668                    },
3669                    "parents": []
3670                }
3671            ]
3672        );
3673        let parsed = Entities::from_json_value(entitiesjson, Some(&schema))
3674            .expect("Should parse without error");
3675        assert_eq!(parsed.iter().count(), 1);
3676
3677        // providing another attribute "foobar" should be OK
3678        let entitiesjson = json!(
3679            [
3680                {
3681                    "uid": { "type": "Employee", "id": "12UA45" },
3682                    "attrs": {
3683                        "isFullTime": true,
3684                        "foobar": 234,
3685                        "manager": { "type": "Employee", "id": "34FB87" }
3686                    },
3687                    "parents": []
3688                }
3689            ]
3690        );
3691        let parsed = Entities::from_json_value(entitiesjson, Some(&schema))
3692            .expect("Should parse without error");
3693        assert_eq!(parsed.iter().count(), 1);
3694    }
3695
3696    #[test]
3697    fn schema_sanity_check() {
3698        let src = "{ , .. }";
3699        assert_matches!(Schema::from_str(src), Err(super::SchemaError::ParseJson(_)));
3700    }
3701
3702    #[test]
3703    fn template_constraint_sanity_checks() {
3704        assert!(!TemplatePrincipalConstraint::Any.has_slot());
3705        assert!(!TemplatePrincipalConstraint::In(Some(EntityUid::from_strs("a", "a"))).has_slot());
3706        assert!(!TemplatePrincipalConstraint::Eq(Some(EntityUid::from_strs("a", "a"))).has_slot());
3707        assert!(TemplatePrincipalConstraint::In(None).has_slot());
3708        assert!(TemplatePrincipalConstraint::Eq(None).has_slot());
3709        assert!(!TemplateResourceConstraint::Any.has_slot());
3710        assert!(!TemplateResourceConstraint::In(Some(EntityUid::from_strs("a", "a"))).has_slot());
3711        assert!(!TemplateResourceConstraint::Eq(Some(EntityUid::from_strs("a", "a"))).has_slot());
3712        assert!(TemplateResourceConstraint::In(None).has_slot());
3713        assert!(TemplateResourceConstraint::Eq(None).has_slot());
3714    }
3715
3716    #[test]
3717    fn template_principal_constraints() {
3718        let src = r#"
3719            permit(principal, action, resource);
3720        "#;
3721        let t = Template::parse(None, src).unwrap();
3722        assert_eq!(t.principal_constraint(), TemplatePrincipalConstraint::Any);
3723
3724        let src = r#"
3725            permit(principal == ?principal, action, resource);
3726        "#;
3727        let t = Template::parse(None, src).unwrap();
3728        assert_eq!(
3729            t.principal_constraint(),
3730            TemplatePrincipalConstraint::Eq(None)
3731        );
3732
3733        let src = r#"
3734            permit(principal == A::"a", action, resource);
3735        "#;
3736        let t = Template::parse(None, src).unwrap();
3737        assert_eq!(
3738            t.principal_constraint(),
3739            TemplatePrincipalConstraint::Eq(Some(EntityUid::from_strs("A", "a")))
3740        );
3741
3742        let src = r#"
3743            permit(principal in ?principal, action, resource);
3744        "#;
3745        let t = Template::parse(None, src).unwrap();
3746        assert_eq!(
3747            t.principal_constraint(),
3748            TemplatePrincipalConstraint::In(None)
3749        );
3750
3751        let src = r#"
3752            permit(principal in A::"a", action, resource);
3753        "#;
3754        let t = Template::parse(None, src).unwrap();
3755        assert_eq!(
3756            t.principal_constraint(),
3757            TemplatePrincipalConstraint::In(Some(EntityUid::from_strs("A", "a")))
3758        );
3759    }
3760
3761    #[test]
3762    fn template_action_constraints() {
3763        let src = r#"
3764            permit(principal, action, resource);
3765        "#;
3766        let t = Template::parse(None, src).unwrap();
3767        assert_eq!(t.action_constraint(), ActionConstraint::Any);
3768
3769        let src = r#"
3770            permit(principal, action == Action::"A", resource);
3771        "#;
3772        let t = Template::parse(None, src).unwrap();
3773        assert_eq!(
3774            t.action_constraint(),
3775            ActionConstraint::Eq(EntityUid::from_strs("Action", "A"))
3776        );
3777
3778        let src = r#"
3779            permit(principal, action in [Action::"A", Action::"B"], resource);
3780        "#;
3781        let t = Template::parse(None, src).unwrap();
3782        assert_eq!(
3783            t.action_constraint(),
3784            ActionConstraint::In(vec![
3785                EntityUid::from_strs("Action", "A"),
3786                EntityUid::from_strs("Action", "B")
3787            ])
3788        );
3789    }
3790
3791    #[test]
3792    fn template_resource_constraints() {
3793        let src = r#"
3794            permit(principal, action, resource);
3795        "#;
3796        let t = Template::parse(None, src).unwrap();
3797        assert_eq!(t.resource_constraint(), TemplateResourceConstraint::Any);
3798
3799        let src = r#"
3800            permit(principal, action, resource == ?resource);
3801        "#;
3802        let t = Template::parse(None, src).unwrap();
3803        assert_eq!(
3804            t.resource_constraint(),
3805            TemplateResourceConstraint::Eq(None)
3806        );
3807
3808        let src = r#"
3809            permit(principal, action, resource == A::"a");
3810        "#;
3811        let t = Template::parse(None, src).unwrap();
3812        assert_eq!(
3813            t.resource_constraint(),
3814            TemplateResourceConstraint::Eq(Some(EntityUid::from_strs("A", "a")))
3815        );
3816
3817        let src = r#"
3818            permit(principal, action, resource in ?resource);
3819        "#;
3820        let t = Template::parse(None, src).unwrap();
3821        assert_eq!(
3822            t.resource_constraint(),
3823            TemplateResourceConstraint::In(None)
3824        );
3825
3826        let src = r#"
3827            permit(principal, action, resource in A::"a");
3828        "#;
3829        let t = Template::parse(None, src).unwrap();
3830        assert_eq!(
3831            t.resource_constraint(),
3832            TemplateResourceConstraint::In(Some(EntityUid::from_strs("A", "a")))
3833        );
3834    }
3835
3836    #[test]
3837    fn schema_namespace() {
3838        let fragment: SchemaFragment = r#"
3839        {
3840            "Foo::Bar": {
3841                "entityTypes": {},
3842                "actions": {}
3843            }
3844        }
3845        "#
3846        .parse()
3847        .unwrap();
3848        let namespaces = fragment.namespaces().next().unwrap();
3849        assert_eq!(
3850            namespaces.map(|ns| ns.to_string()),
3851            Some("Foo::Bar".to_string())
3852        );
3853        let _schema: Schema = fragment.try_into().expect("Should convert to schema");
3854
3855        let fragment: SchemaFragment = r#"
3856        {
3857            "": {
3858                "entityTypes": {},
3859                "actions": {}
3860            }
3861        }
3862        "#
3863        .parse()
3864        .unwrap();
3865        let namespaces = fragment.namespaces().next().unwrap();
3866        assert_eq!(namespaces, None);
3867        let _schema: Schema = fragment.try_into().expect("Should convert to schema");
3868    }
3869
3870    #[test]
3871    fn load_multiple_namespaces() {
3872        let fragment = SchemaFragment::from_json_value(json!({
3873            "Foo::Bar": {
3874                "entityTypes": {
3875                    "Baz": {
3876                        "memberOfTypes": ["Bar::Foo::Baz"]
3877                    }
3878                },
3879                "actions": {}
3880            },
3881            "Bar::Foo": {
3882                "entityTypes": {
3883                    "Baz": {
3884                        "memberOfTypes": ["Foo::Bar::Baz"]
3885                    }
3886                },
3887                "actions": {}
3888            }
3889        }))
3890        .unwrap();
3891
3892        let schema = Schema::from_schema_fragments([fragment]).unwrap();
3893
3894        assert!(schema
3895            .0
3896            .get_entity_type(&"Foo::Bar::Baz".parse().unwrap())
3897            .is_some());
3898        assert!(schema
3899            .0
3900            .get_entity_type(&"Bar::Foo::Baz".parse().unwrap())
3901            .is_some());
3902    }
3903
3904    #[test]
3905    fn get_attributes_from_schema() {
3906        let fragment: SchemaFragment = SchemaFragment::from_json_value(json!({
3907        "": {
3908            "entityTypes": {},
3909            "actions": {
3910                "A": {},
3911                "B": {
3912                    "memberOf": [{"id": "A"}]
3913                },
3914                "C": {
3915                    "memberOf": [{"id": "A"}]
3916                },
3917                "D": {
3918                    "memberOf": [{"id": "B"}, {"id": "C"}]
3919                },
3920                "E": {
3921                    "memberOf": [{"id": "D"}]
3922                }
3923            }
3924        }}))
3925        .unwrap();
3926
3927        let schema = Schema::from_schema_fragments([fragment]).unwrap();
3928        let action_entities = schema.action_entities().unwrap();
3929
3930        let a_euid = EntityUid::from_strs("Action", "A");
3931        let b_euid = EntityUid::from_strs("Action", "B");
3932        let c_euid = EntityUid::from_strs("Action", "C");
3933        let d_euid = EntityUid::from_strs("Action", "D");
3934        let e_euid = EntityUid::from_strs("Action", "E");
3935
3936        assert_eq!(
3937            action_entities,
3938            Entities::from_entities([
3939                Entity::new(a_euid.clone(), HashMap::new(), HashSet::new()),
3940                Entity::new(
3941                    b_euid.clone(),
3942                    HashMap::new(),
3943                    HashSet::from([a_euid.clone()])
3944                ),
3945                Entity::new(
3946                    c_euid.clone(),
3947                    HashMap::new(),
3948                    HashSet::from([a_euid.clone()])
3949                ),
3950                Entity::new(
3951                    d_euid.clone(),
3952                    HashMap::new(),
3953                    HashSet::from([a_euid.clone(), b_euid.clone(), c_euid.clone()])
3954                ),
3955                Entity::new(
3956                    e_euid,
3957                    HashMap::new(),
3958                    HashSet::from([a_euid, b_euid, c_euid, d_euid])
3959                ),
3960            ])
3961            .unwrap()
3962        );
3963    }
3964}