Skip to main content

cedar_policy_core/tpe/
entities.rs

1/*
2 * Copyright Cedar Contributors
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      https://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17//! This module contains partial entities.
18
19use crate::ast::{Entity, PartialValueToValueError};
20use crate::entities::conformance::err::EntitySchemaConformanceError;
21use crate::entities::err::Duplicate;
22use crate::entities::{Dereference, Entities, TCComputation};
23use crate::tpe::err::{
24    AncestorValidationError, EntitiesConsistencyError, EntitiesError, EntityConsistencyError,
25    EntityValidationError, JsonDeserializationError, MismatchedActionAncestorsError,
26    MismatchedAncestorError, MismatchedAttributeError, MismatchedTagError, MissingEntityError,
27    UnexpectedActionError, UnknownActionComponentError, UnknownAttributeError, UnknownEntityError,
28    UnknownTagError,
29};
30use crate::transitive_closure::{enforce_tc_and_dag, TcError};
31use crate::validator::{CoreSchema, ValidatorSchema};
32use crate::{
33    ast::PartialValue,
34    entities::{conformance::EntitySchemaConformanceChecker, Schema},
35};
36use crate::{
37    ast::{EntityUID, Value},
38    entities::{
39        json::{err::JsonDeserializationErrorContext, ValueParser},
40        EntityUidJson,
41    },
42    evaluator::RestrictedEvaluator,
43    extensions::Extensions,
44    jsonvalue::JsonValueWithNoDuplicateKeys,
45};
46use crate::{
47    entities::{
48        conformance::{err::UnexpectedEntityTypeError, validate_euid},
49        EntityTypeDescription,
50    },
51    transitive_closure::{compute_tc, repair_tc, TCNode},
52};
53use itertools::Itertools;
54use serde::{Deserialize, Serialize};
55use serde_with::serde_as;
56use smol_str::SmolStr;
57use std::collections::hash_map::Entry;
58use std::collections::{BTreeMap, HashMap, HashSet};
59
60#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
61#[serde_as]
62#[serde(transparent)]
63struct DeduplicatedMap {
64    #[serde_as(as = "serde_with::MapPreventDuplicates<_,_>")]
65    pub map: HashMap<SmolStr, JsonValueWithNoDuplicateKeys>,
66}
67
68/// Serde JSON format for a single entity
69#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
70pub struct EntityJson {
71    /// UID of the entity, specified in any form accepted by `EntityUidJson`
72    uid: EntityUidJson,
73    /// attributes, whose values can be any JSON value.
74    /// (Probably a `CedarValueJson`, but for schema-based parsing, it could for
75    /// instance be an `EntityUidJson` if we're expecting an entity reference,
76    /// so for now we leave it in its raw json-value form, albeit not allowing
77    /// any duplicate keys in any records that may occur in an attribute value
78    /// (even nested).)
79    #[serde(default)]
80    // the annotation covers duplicates in this `HashMap` itself, while the `JsonValueWithNoDuplicateKeys` covers duplicates in any records contained in attribute values (including recursively)
81    attrs: Option<DeduplicatedMap>,
82    #[serde(default)]
83    /// Parents of the entity, specified in any form accepted by `EntityUidJson`
84    parents: Option<Vec<EntityUidJson>>,
85    #[serde(default)]
86    // the annotation covers duplicates in this `HashMap` itself, while the `JsonValueWithNoDuplicateKeys` covers duplicates in any records contained in tag values (including recursively)
87    // Note that unlike the concrete JSON entity format, when the `tags` field
88    // is missing, it means `tags` are unknown
89    // This is because we need to represent `tags` being unknowns
90    tags: Option<DeduplicatedMap>,
91}
92
93/// The partial entity
94#[derive(Debug, Clone, PartialEq, Eq)]
95pub struct PartialEntity {
96    // The uid of the partial entity
97    pub(crate) uid: EntityUID,
98    // Optional attributes
99    pub(crate) attrs: Option<BTreeMap<SmolStr, Value>>,
100    // Optional ancestors
101    pub(crate) ancestors: Option<HashSet<EntityUID>>,
102    // Optional tags
103    pub(crate) tags: Option<BTreeMap<SmolStr, Value>>,
104}
105
106// An `Entity` without unknowns is a `PartialEntity`
107impl TryFrom<Entity> for PartialEntity {
108    type Error = PartialValueToValueError;
109    fn try_from(value: Entity) -> Result<Self, Self::Error> {
110        let uid = value.uid().clone();
111        let attrs = value
112            .attrs()
113            .map(|(a, v)| Ok((a.clone(), Value::try_from(v.clone())?)))
114            .collect::<Result<BTreeMap<_, _>, PartialValueToValueError>>()?;
115        let ancestors = value.ancestors().cloned().collect();
116        let tags = value
117            .tags()
118            .map(|(a, v)| Ok((a.clone(), Value::try_from(v.clone())?)))
119            .collect::<Result<BTreeMap<_, _>, PartialValueToValueError>>()?;
120        Ok(Self {
121            uid,
122            attrs: Some(attrs),
123            ancestors: Some(ancestors),
124            tags: Some(tags),
125        })
126    }
127}
128
129impl PartialEntity {
130    /// Construct a new [`PartialEntity`]
131    pub fn new(
132        uid: EntityUID,
133        attrs: Option<BTreeMap<SmolStr, Value>>,
134        ancestors: Option<HashSet<EntityUID>>,
135        tags: Option<BTreeMap<SmolStr, Value>>,
136        schema: &ValidatorSchema,
137    ) -> std::result::Result<Self, EntitiesError> {
138        let e = Self {
139            uid,
140            attrs,
141            ancestors,
142            tags,
143        };
144        e.validate(schema)?;
145        Ok(e)
146    }
147
148    /// Get the uid of this partial entity
149    pub fn uid(&self) -> &EntityUID {
150        &self.uid
151    }
152
153    /// Get the optional attributes of this partial entity
154    pub fn attrs(&self) -> Option<&BTreeMap<SmolStr, Value>> {
155        self.attrs.as_ref()
156    }
157
158    /// Get the optional ancestors of this partial entity
159    pub fn ancestors(&self) -> Option<&HashSet<EntityUID>> {
160        self.ancestors.as_ref()
161    }
162
163    /// Get the optional tags of this partial entity
164    pub fn tags(&self) -> Option<&BTreeMap<SmolStr, Value>> {
165        self.tags.as_ref()
166    }
167
168    /// Check if an [`Entity`] is consistent with a [`PartialEntity`]
169    pub(crate) fn check_consistency(
170        &self,
171        entity: &Entity,
172    ) -> std::result::Result<(), EntityConsistencyError> {
173        if let Some(attrs) = &self.attrs {
174            let other_attrs = entity
175                .attrs()
176                .map(|(a, pv)| match pv {
177                    PartialValue::Value(v) => Ok((a.clone(), v.clone())),
178                    PartialValue::Residual(_) => Err(UnknownAttributeError {
179                        uid: self.uid.clone(),
180                        attr: a.clone(),
181                    }
182                    .into()),
183                })
184                .collect::<std::result::Result<BTreeMap<_, _>, EntityConsistencyError>>()?;
185
186            if attrs != &other_attrs {
187                return Err(MismatchedAttributeError {
188                    uid: self.uid.clone(),
189                }
190                .into());
191            }
192        }
193        if let Some(ancestors) = &self.ancestors {
194            let other_ancestors: HashSet<EntityUID> = entity.ancestors().cloned().collect();
195            if ancestors != &other_ancestors {
196                return Err(MismatchedAncestorError {
197                    uid: self.uid.clone(),
198                }
199                .into());
200            }
201        }
202        if let Some(tags) = &self.tags {
203            let other_tags = entity
204                .tags()
205                .map(|(a, pv)| match pv {
206                    PartialValue::Value(v) => Ok((a.clone(), v.clone())),
207                    PartialValue::Residual(_) => Err(UnknownTagError {
208                        uid: self.uid.clone(),
209                        tag: a.clone(),
210                    }
211                    .into()),
212                })
213                .collect::<std::result::Result<BTreeMap<_, _>, EntityConsistencyError>>()?;
214            if tags != &other_tags {
215                return Err(MismatchedTagError {
216                    uid: self.uid.clone(),
217                }
218                .into());
219            }
220        }
221        Ok(())
222    }
223}
224
225/// Parse an [`EntityJson`] into a [`PartialEntity`] according to `schema`
226pub fn parse_ejson(
227    e: EntityJson,
228    schema: &ValidatorSchema,
229) -> std::result::Result<PartialEntity, JsonDeserializationError> {
230    let uid = e
231        .uid
232        .into_euid(&|| JsonDeserializationErrorContext::EntityUid)?;
233    let core_schema = CoreSchema::new(schema);
234
235    if uid.is_action() {
236        return Err(UnexpectedActionError { action: uid }.into());
237    }
238    let vparser = ValueParser::new(Extensions::all_available());
239    let eval = RestrictedEvaluator::new(Extensions::all_available());
240    let attrs = e
241        .attrs
242        .map(|m| {
243            m.map
244                .into_iter()
245                .map(|(k, v)| {
246                    if let Some(ty) = core_schema.entity_type(uid.entity_type()) {
247                        Ok((
248                            k.clone(),
249                            eval.interpret(
250                                vparser
251                                    .val_into_restricted_expr(
252                                        v.into(),
253                                        ty.attr_type(&k).as_ref(),
254                                        &|| JsonDeserializationErrorContext::EntityAttribute {
255                                            uid: uid.clone(),
256                                            attr: k.clone(),
257                                        },
258                                    )?
259                                    .as_borrowed(),
260                            )?,
261                        ))
262                    } else {
263                        Err(JsonDeserializationError::Concrete(
264                            crate::entities::json::err::JsonDeserializationError::from(
265                                EntitySchemaConformanceError::UnexpectedEntityType(
266                                    UnexpectedEntityTypeError {
267                                        uid: uid.clone(),
268                                        suggested_types: core_schema
269                                            .entity_types_with_basename(
270                                                &uid.entity_type().name().basename(),
271                                            )
272                                            .collect(),
273                                    },
274                                ),
275                            ),
276                        ))
277                    }
278                })
279                .collect::<std::result::Result<BTreeMap<_, _>, _>>()
280        })
281        .transpose()?;
282
283    let ancestors = e
284        .parents
285        .map(|parents| {
286            parents
287                .into_iter()
288                .map(|parent| {
289                    parent
290                        .into_euid(&|| JsonDeserializationErrorContext::EntityParents {
291                            uid: uid.clone(),
292                        })
293                        .map_err(JsonDeserializationError::Concrete)
294                })
295                .collect::<std::result::Result<HashSet<_>, _>>()
296        })
297        .transpose()?;
298
299    let tags = e
300        .tags
301        .map(|m| {
302            m.map
303                .into_iter()
304                .map(|(k, v)| {
305                    if let Some(ty) = core_schema.entity_type(uid.entity_type()) {
306                        Ok((
307                            k.clone(),
308                            eval.interpret(
309                                vparser
310                                    .val_into_restricted_expr(
311                                        v.into(),
312                                        ty.tag_type().as_ref(),
313                                        &|| JsonDeserializationErrorContext::EntityAttribute {
314                                            uid: uid.clone(),
315                                            attr: k.clone(),
316                                        },
317                                    )?
318                                    .as_borrowed(),
319                            )?,
320                        ))
321                    } else {
322                        Err(JsonDeserializationError::Concrete(
323                            crate::entities::json::err::JsonDeserializationError::from(
324                                EntitySchemaConformanceError::UnexpectedEntityType(
325                                    UnexpectedEntityTypeError {
326                                        uid: uid.clone(),
327                                        suggested_types: core_schema
328                                            .entity_types_with_basename(
329                                                &uid.entity_type().name().basename(),
330                                            )
331                                            .collect(),
332                                    },
333                                ),
334                            ),
335                        ))
336                    }
337                })
338                .collect::<std::result::Result<BTreeMap<_, _>, _>>()
339        })
340        .transpose()?;
341
342    Ok(PartialEntity {
343        uid,
344        attrs,
345        ancestors,
346        tags,
347    })
348}
349
350impl TCNode<EntityUID> for PartialEntity {
351    fn add_edge_to(&mut self, k: EntityUID) {
352        self.add_ancestor(k);
353    }
354
355    fn get_key(&self) -> EntityUID {
356        self.uid.clone()
357    }
358
359    fn has_edge_to(&self, k: &EntityUID) -> bool {
360        match self.ancestors.as_ref() {
361            Some(ancestors) => ancestors.contains(k),
362            None => false,
363        }
364    }
365
366    fn out_edges(&self) -> Box<dyn Iterator<Item = &EntityUID> + '_> {
367        match self.ancestors.as_ref() {
368            Some(ancestors) => Box::new(ancestors.iter()),
369            None => Box::new(std::iter::empty()),
370        }
371    }
372
373    fn reset_edges(&mut self) {}
374}
375
376impl PartialEntity {
377    /// This method should be only called on entities that have known ancestors
378    pub(crate) fn add_ancestor(&mut self, uid: EntityUID) {
379        #[expect(
380            clippy::expect_used,
381            reason = "this method should be only called on entities that have known ancestors"
382        )]
383        self.ancestors
384            .as_mut()
385            .expect("should not be unknown")
386            .insert(uid);
387    }
388
389    /// Validate `self` according to `schema`
390    pub fn validate(
391        &self,
392        schema: &ValidatorSchema,
393    ) -> std::result::Result<(), EntityValidationError> {
394        let core_schema = CoreSchema::new(schema);
395        let uid = &self.uid;
396        let etype = uid.entity_type();
397
398        if self.uid.is_action() {
399            if self.attrs.is_none() || self.tags.is_none() {
400                return Err(UnknownActionComponentError {
401                    action: uid.clone(),
402                }
403                .into());
404            }
405            if let Some(attrs) = &self.attrs {
406                if let Some((attr, _)) = attrs.first_key_value() {
407                    return Err(EntitySchemaConformanceError::unexpected_entity_attr(
408                        uid.clone(),
409                        attr.clone(),
410                    )
411                    .into());
412                }
413            }
414            if let Some(tags) = &self.tags {
415                if let Some((tag, _)) = tags.first_key_value() {
416                    return Err(EntitySchemaConformanceError::unexpected_entity_tag(
417                        uid.clone(),
418                        tag.clone(),
419                    )
420                    .into());
421                }
422            }
423            if let Some(action) = core_schema.action(uid) {
424                if let Some(ancestors) = &self.ancestors {
425                    let schema_ancestors: HashSet<EntityUID> =
426                        action.ancestors().cloned().collect();
427                    if &schema_ancestors != ancestors {
428                        return Err(MismatchedActionAncestorsError {
429                            action: uid.clone(),
430                        }
431                        .into());
432                    }
433                } else {
434                    return Err(UnknownActionComponentError {
435                        action: uid.clone(),
436                    }
437                    .into());
438                }
439            } else {
440                return Err(EntitySchemaConformanceError::UndeclaredAction(
441                    crate::entities::conformance::err::UndeclaredAction { uid: uid.clone() },
442                )
443                .into());
444            }
445            return Ok(());
446        }
447        validate_euid(&core_schema, uid).map_err(EntitySchemaConformanceError::from)?;
448        let schema_etype = core_schema
449            .entity_type(etype)
450            .ok_or_else(|| {
451                let suggested_types = core_schema
452                    .entity_types_with_basename(&etype.name().basename())
453                    .collect();
454                UnexpectedEntityTypeError {
455                    uid: uid.clone(),
456                    suggested_types,
457                }
458            })
459            .map_err(EntitySchemaConformanceError::from)?;
460        let checker =
461            EntitySchemaConformanceChecker::new(&core_schema, Extensions::all_available());
462        if let Some(ancestors) = &self.ancestors {
463            checker.validate_entity_ancestors(uid, ancestors.iter(), &schema_etype)?;
464        }
465        if let Some(attrs) = &self.attrs {
466            let attrs: BTreeMap<_, PartialValue> = attrs
467                .iter()
468                .map(|(a, v)| (a.clone(), v.clone().into()))
469                .collect();
470            checker.validate_entity_attributes(uid, attrs.iter(), &schema_etype)?;
471        }
472        if let Some(tags) = &self.tags {
473            let tags: BTreeMap<_, PartialValue> = tags
474                .iter()
475                .map(|(a, v)| (a.clone(), v.clone().into()))
476                .collect();
477            checker.validate_tags(uid, tags.iter(), &schema_etype)?;
478        }
479        Ok(())
480    }
481}
482
483// Validate if ancestors are well-formed
484// i.e., ancestors of any ancestor of a `PartialEntity` should not be unknown
485// This ensures that we can always compute a TC for entities with concrete
486// ancestors
487pub(crate) fn validate_ancestors(
488    entities: &HashMap<EntityUID, PartialEntity>,
489) -> std::result::Result<(), AncestorValidationError> {
490    for e in entities.values() {
491        if let Some(ancestors) = e.ancestors.as_ref() {
492            for ancestor in ancestors {
493                if let Some(ancestor_entity) = entities.get(ancestor) {
494                    if ancestor_entity.ancestors.is_none() {
495                        return Err(AncestorValidationError {
496                            uid: e.uid.clone(),
497                            ancestor: ancestor.clone(),
498                        });
499                    }
500                }
501            }
502        }
503    }
504    Ok(())
505}
506
507/// The partial entity store
508#[derive(Clone, Debug, Default, PartialEq, Eq)]
509pub struct PartialEntities {
510    /// Important internal invariant: for any `Entities` object that exists,
511    /// the `ancestor` relation is transitively closed.
512    entities: HashMap<EntityUID, PartialEntity>,
513}
514
515impl PartialEntities {
516    /// Get an empty partial entities
517    pub fn new() -> Self {
518        Self::default()
519    }
520
521    /// Get an iterator of entities
522    pub fn entities(&self) -> impl Iterator<Item = &PartialEntity> {
523        self.entities.values()
524    }
525
526    /// Compute transitive closure
527    pub fn compute_tc(&mut self) -> std::result::Result<(), TcError<EntityUID>> {
528        compute_tc(&mut self.entities, true)
529    }
530
531    /// Check that the tc is computed and forms a dag
532    pub fn enforce_tc_and_dag(&self) -> std::result::Result<(), TcError<EntityUID>> {
533        enforce_tc_and_dag(&self.entities)
534    }
535
536    /// Get the `PartialEntity` with this identifier
537    pub fn get(&self, euid: &EntityUID) -> Option<&PartialEntity> {
538        self.entities.get(euid)
539    }
540
541    /// Check if there is a `PartialEntity` with identifier
542    pub fn contains_entity(&self, euid: &EntityUID) -> bool {
543        self.entities.contains_key(euid)
544    }
545
546    fn from_entities_map(
547        entities: HashMap<EntityUID, PartialEntity>,
548        schema: &ValidatorSchema,
549    ) -> std::result::Result<Self, EntitiesError> {
550        entities.values().try_for_each(|e| e.validate(schema))?;
551        validate_ancestors(&entities)?;
552        let mut entities = Self { entities };
553        entities.compute_tc()?;
554        entities.insert_actions(schema);
555        Ok(entities)
556    }
557
558    /// Construct `PartialEntities` from `Entities`, ensuring that the entities are valid.
559    /// TC is already computed in the source `Entities`, so we skip recomputation.
560    pub fn from_concrete(
561        entities: Entities,
562        schema: &ValidatorSchema,
563    ) -> std::result::Result<Self, EntitiesError> {
564        let entities_map: HashMap<EntityUID, PartialEntity> = entities
565            .into_iter()
566            .map(|e| e.try_into().map(|e: PartialEntity| (e.uid.clone(), e)))
567            .try_collect()?;
568        entities_map.values().try_for_each(|e| e.validate(schema))?;
569        validate_ancestors(&entities_map)?;
570        // TC is already computed in the source Entities — the conversion to
571        // PartialEntity preserves all ancestors (direct + indirect).
572        let mut entities = Self {
573            entities: entities_map,
574        };
575        entities.insert_actions(schema);
576        Ok(entities)
577    }
578
579    /// Construct `PartialEntities` from an iterator
580    pub fn from_entities(
581        entity_mappings: impl Iterator<Item = PartialEntity>,
582        schema: &ValidatorSchema,
583    ) -> std::result::Result<Self, EntitiesError> {
584        let mut entities: HashMap<EntityUID, PartialEntity> = HashMap::new();
585        for entity in entity_mappings {
586            use std::collections::hash_map::Entry;
587            match entities.entry(entity.uid.clone()) {
588                Entry::Vacant(e) => {
589                    e.insert(entity);
590                }
591                Entry::Occupied(e) => {
592                    return Err(Duplicate {
593                        euid: e.key().clone(),
594                    }
595                    .into())
596                }
597            }
598        }
599        Self::from_entities_map(entities, schema)
600    }
601
602    /// Add a partial entity without checking if it conforms to the schema,
603    /// assuming the TC is already computed.
604    /// Errors on duplicate entries.
605    pub(crate) fn add_entity_trusted(
606        &mut self,
607        uid: EntityUID,
608        entity: PartialEntity,
609    ) -> std::result::Result<(), EntitiesError> {
610        match self.entities.entry(uid) {
611            Entry::Vacant(e) => {
612                e.insert(entity);
613            }
614            Entry::Occupied(e) => {
615                return Err(Duplicate {
616                    euid: e.key().clone(),
617                }
618                .into())
619            }
620        }
621
622        Ok(())
623    }
624
625    /// Add a set of partial entities to this store,
626    /// erroring on duplicates.
627    pub fn add_entities(
628        &mut self,
629        entity_mappings: impl Iterator<Item = (EntityUID, PartialEntity)>,
630        schema: &ValidatorSchema,
631        tc_computation: TCComputation,
632    ) -> std::result::Result<(), EntitiesError> {
633        let mut entities_touched: HashSet<EntityUID> = HashSet::new();
634        for (id, entity) in entity_mappings {
635            entity.validate(schema)?;
636            entities_touched.insert(id.clone());
637            self.add_entity_trusted(id, entity)?;
638        }
639
640        validate_ancestors(&self.entities)?;
641
642        match tc_computation {
643            TCComputation::AssumeAlreadyComputed => (),
644            TCComputation::EnforceAlreadyComputed => {
645                self.enforce_tc_and_dag()?;
646            }
647            TCComputation::ComputeNow => {
648                for entity in self.entities.values() {
649                    if let Some(ancestors) = entity.ancestors.as_ref() {
650                        if !entities_touched.is_disjoint(ancestors) {
651                            entities_touched.insert(entity.uid.clone());
652                        }
653                    }
654                }
655                repair_tc(entities_touched, &mut self.entities, true)?;
656            }
657        }
658        Ok(())
659    }
660
661    /// Like `from_entities` but do not perform any validation and tc
662    /// computation. Callers must ensure these invariants are maintained.
663    pub fn from_entities_unchecked(
664        entities: impl Iterator<Item = (EntityUID, PartialEntity)>,
665    ) -> Self {
666        Self {
667            entities: entities.collect(),
668        }
669    }
670
671    // Insert action entities from the schema
672    // Overwriting existing action entities is fine because they should come
673    // from schema or be consistent with schema anyways
674    fn insert_actions(&mut self, schema: &ValidatorSchema) {
675        for (uid, action) in &schema.actions {
676            self.entities.insert(
677                uid.clone(),
678                #[expect(
679                    clippy::unwrap_used,
680                    reason = "action entities do not contain unknowns"
681                )]
682                action.as_ref().clone().try_into().unwrap(),
683            );
684        }
685    }
686
687    /// Construct [`PartialEntities`] from a JSON list
688    pub fn from_json_value(
689        value: serde_json::Value,
690        schema: &ValidatorSchema,
691    ) -> std::result::Result<Self, EntitiesError> {
692        let entities: Vec<EntityJson> = serde_json::from_value(value)
693            .map_err(|e| JsonDeserializationError::Concrete(e.into()))?;
694        let mut partial_entities = PartialEntities::default();
695        for e in entities {
696            let partial_entity = parse_ejson(e, schema)?;
697            partial_entity.validate(schema)?;
698            partial_entities
699                .entities
700                .insert(partial_entity.uid.clone(), partial_entity);
701        }
702        validate_ancestors(&partial_entities.entities)?;
703        partial_entities.compute_tc()?;
704
705        // Insert actions from the schema
706        partial_entities.insert_actions(schema);
707        Ok(partial_entities)
708    }
709
710    /// Check if [`PartialEntities`] are consistent with [`Entities`]
711    pub fn check_consistency(
712        &self,
713        concrete: &Entities,
714    ) -> std::result::Result<(), EntitiesConsistencyError> {
715        for (uid, e) in &self.entities {
716            match concrete.entity(uid) {
717                Dereference::NoSuchEntity => {
718                    return Err(MissingEntityError { uid: uid.clone() }.into());
719                }
720                Dereference::Residual(_) => {
721                    return Err(UnknownEntityError { uid: uid.clone() }.into());
722                }
723                Dereference::Data(entity) => e.check_consistency(entity)?,
724            }
725        }
726        Ok(())
727    }
728}
729
730#[cfg(test)]
731mod tests {
732    use std::collections::{BTreeMap, HashMap, HashSet};
733
734    use crate::tpe::err::AncestorValidationError;
735    use crate::validator::ValidatorSchema;
736    use crate::{
737        ast::{EntityUID, Value},
738        extensions::Extensions,
739    };
740    use cool_asserts::assert_matches;
741
742    use super::{parse_ejson, validate_ancestors, EntityJson, PartialEntities, PartialEntity};
743
744    #[track_caller]
745    fn basic_schema() -> ValidatorSchema {
746        ValidatorSchema::from_cedarschema_str(
747            r#"
748        entity A {
749            a? : String,
750            b? : Long,
751            c? : {"x" : Bool}
752        } tags Long;
753         action a appliesTo {
754           principal : A,
755           resource : A
756         };
757        "#,
758            Extensions::all_available(),
759        )
760        .unwrap()
761        .0
762    }
763
764    #[test]
765    fn basic() {
766        let schema = basic_schema();
767        // unlike the existing JSON format, absence of `tags` or `tags` being
768        // `null` means unknown tags, as opposed to empty tags
769        let json = serde_json::json!(
770            {
771                "uid" : {
772                    "type" : "A",
773                    "id" : "",
774                },
775                "tags" : null,
776            }
777        );
778        let ejson: EntityJson = serde_json::from_value(json).expect("should parse");
779        assert_matches!(parse_ejson(ejson, &schema), Ok(e) => {
780            assert_eq!(e, PartialEntity { uid: r#"A::"""#.parse().unwrap(), attrs: None, ancestors: None, tags: None });
781        });
782
783        // empty tags need to be specified explicitly
784        let schema = basic_schema();
785        let json = serde_json::json!(
786            {
787                "uid" : {
788                    "type" : "A",
789                    "id" : "",
790                },
791                "tags" : {},
792            }
793        );
794        let ejson: EntityJson = serde_json::from_value(json).expect("should parse");
795        assert_matches!(parse_ejson(ejson, &schema), Ok(e) => {
796            assert_eq!(e, PartialEntity { uid: r#"A::"""#.parse().unwrap(), attrs: None, ancestors: None, tags: Some(BTreeMap::default()) });
797        });
798
799        let schema = basic_schema();
800        let json = serde_json::json!(
801            {
802                "uid" : {
803                    "type" : "A",
804                    "id" : "",
805                },
806                "parents" : [],
807                "attrs" : {},
808                "tags" : {},
809            }
810        );
811        let ejson: EntityJson = serde_json::from_value(json).expect("should parse");
812        assert_matches!(parse_ejson(ejson, &schema), Ok(e) => {
813            assert_eq!(e, PartialEntity { uid: r#"A::"""#.parse().unwrap(), attrs: Some(BTreeMap::new()), ancestors: Some(HashSet::default()), tags: Some(BTreeMap::default()) });
814        });
815
816        let schema = basic_schema();
817        let json = serde_json::json!(
818            {
819                "uid" : {
820                    "type" : "A",
821                    "id" : "",
822                },
823                "parents" : [],
824                "attrs" : {
825                    "b" : 1,
826                    "c" : {"x": false},
827                },
828                "tags" : {},
829            }
830        );
831        let ejson: EntityJson = serde_json::from_value(json).expect("should parse");
832        assert_matches!(parse_ejson(ejson, &schema), Ok(e) => {
833            assert_eq!(e, PartialEntity { uid: r#"A::"""#.parse().unwrap(), attrs: Some(BTreeMap::from_iter([("b".into(), 1.into()), ("c".into(), Value::record(std::iter::once(("x", false)), None)
834            )])), ancestors: Some(HashSet::default()), tags: Some(BTreeMap::default()) });
835        });
836    }
837
838    #[test]
839    fn invalid_hierarchy() {
840        let uid_a: EntityUID = r#"A::"a""#.parse().unwrap();
841        let uid_b: EntityUID = r#"A::"b""#.parse().unwrap();
842        assert_matches!(
843            validate_ancestors(&HashMap::from_iter([
844                (
845                    uid_a.clone(),
846                    PartialEntity {
847                        uid: uid_a,
848                        ancestors: Some(HashSet::from_iter([uid_b.clone()])),
849                        attrs: None,
850                        tags: None
851                    }
852                ),
853                (
854                    uid_b.clone(),
855                    PartialEntity {
856                        uid: uid_b,
857                        ancestors: None,
858                        attrs: None,
859                        tags: None
860                    }
861                )
862            ])),
863            Err(AncestorValidationError { .. })
864        )
865    }
866
867    #[test]
868    fn tc_computation() {
869        let a = PartialEntity {
870            uid: r#"E::"a""#.parse().unwrap(),
871            attrs: None,
872            ancestors: Some(HashSet::from_iter([
873                r#"E::"b""#.parse().unwrap(),
874                r#"E::"c""#.parse().unwrap(),
875            ])),
876            tags: None,
877        };
878        let b = PartialEntity {
879            uid: r#"E::"b""#.parse().unwrap(),
880            attrs: None,
881            ancestors: Some(HashSet::from_iter([r#"E::"d""#.parse().unwrap()])),
882            tags: None,
883        };
884        let c = PartialEntity {
885            uid: r#"E::"c""#.parse().unwrap(),
886            attrs: None,
887            ancestors: Some(HashSet::from_iter([r#"E::"e""#.parse().unwrap()])),
888            tags: None,
889        };
890        let e = PartialEntity {
891            uid: r#"E::"e""#.parse().unwrap(),
892            attrs: None,
893            ancestors: Some(HashSet::from_iter([r#"E::"f""#.parse().unwrap()])),
894            tags: None,
895        };
896        let x = PartialEntity {
897            uid: r#"E::"x""#.parse().unwrap(),
898            attrs: None,
899            ancestors: None,
900            tags: None,
901        };
902        let mut entities = PartialEntities {
903            entities: vec![a, b, c, e, x]
904                .into_iter()
905                .map(|e| (e.uid.clone(), e))
906                .collect(),
907        };
908        entities.compute_tc().expect("should compute tc");
909        assert_eq!(
910            entities
911                .entities
912                .get(&r#"E::"a""#.parse().unwrap())
913                .as_ref()
914                .unwrap()
915                .ancestors
916                .clone()
917                .unwrap(),
918            HashSet::from_iter([
919                r#"E::"b""#.parse().unwrap(),
920                r#"E::"c""#.parse().unwrap(),
921                r#"E::"d""#.parse().unwrap(),
922                r#"E::"e""#.parse().unwrap(),
923                r#"E::"f""#.parse().unwrap()
924            ])
925        );
926        assert_eq!(
927            entities
928                .entities
929                .get(&r#"E::"b""#.parse().unwrap())
930                .as_ref()
931                .unwrap()
932                .ancestors
933                .clone()
934                .unwrap(),
935            HashSet::from_iter([r#"E::"d""#.parse().unwrap(),])
936        );
937        assert_eq!(
938            entities
939                .entities
940                .get(&r#"E::"c""#.parse().unwrap())
941                .as_ref()
942                .unwrap()
943                .ancestors
944                .clone()
945                .unwrap(),
946            HashSet::from_iter([r#"E::"e""#.parse().unwrap(), r#"E::"f""#.parse().unwrap()])
947        );
948        assert_eq!(
949            entities
950                .entities
951                .get(&r#"E::"e""#.parse().unwrap())
952                .as_ref()
953                .unwrap()
954                .ancestors
955                .clone()
956                .unwrap(),
957            HashSet::from_iter([r#"E::"f""#.parse().unwrap()])
958        );
959        assert_eq!(
960            entities
961                .entities
962                .get(&r#"E::"x""#.parse().unwrap())
963                .as_ref()
964                .unwrap()
965                .ancestors,
966            None
967        );
968    }
969}
970
971#[cfg(test)]
972mod test_validate {
973    use super::*;
974    use crate::entities::conformance::err::EntitySchemaConformanceError;
975    use crate::tpe::err::{
976        EntityValidationError, MismatchedActionAncestorsError, UnknownActionComponentError,
977    };
978    use cool_asserts::assert_matches;
979
980    fn test_schema() -> ValidatorSchema {
981        ValidatorSchema::from_cedarschema_str(
982            r#"
983            entity User {
984                name: String,
985            } tags String;
986
987            entity Resource;
988
989            action view appliesTo {
990                principal: User,
991                resource: Resource
992            };
993            "#,
994            Extensions::all_available(),
995        )
996        .unwrap()
997        .0
998    }
999
1000    #[test]
1001    fn valid_entity() {
1002        let schema = test_schema();
1003        let entity = PartialEntity {
1004            uid: "User::\"alice\"".parse().unwrap(),
1005            attrs: Some(BTreeMap::from_iter([("name".into(), Value::from("Alice"))])),
1006            ancestors: Some(HashSet::new()),
1007            tags: Some(BTreeMap::from_iter([(
1008                "department".into(),
1009                Value::from("Engineering"),
1010            )])),
1011        };
1012
1013        assert_matches!(entity.validate(&schema), Ok(()));
1014    }
1015
1016    #[test]
1017    fn valid_action() {
1018        let schema = test_schema();
1019        let action = PartialEntity {
1020            uid: "Action::\"view\"".parse().unwrap(),
1021            attrs: Some(BTreeMap::new()),
1022            ancestors: Some(HashSet::new()),
1023            tags: Some(BTreeMap::new()),
1024        };
1025
1026        assert_matches!(action.validate(&schema), Ok(()));
1027    }
1028
1029    #[test]
1030    fn invalid_action_with_unknown_ancestors() {
1031        let schema = test_schema();
1032        let action = PartialEntity {
1033            uid: "Action::\"view\"".parse().unwrap(),
1034            attrs: Some(BTreeMap::new()),
1035            ancestors: None,
1036            tags: Some(BTreeMap::new()),
1037        };
1038
1039        assert_matches!(
1040            action.validate(&schema),
1041            Err(EntityValidationError::UnknownActionComponent(
1042                UnknownActionComponentError { .. }
1043            ))
1044        );
1045    }
1046
1047    #[test]
1048    fn invalid_action_with_unknown_tags() {
1049        let schema = test_schema();
1050        let action = PartialEntity {
1051            uid: "Action::\"view\"".parse().unwrap(),
1052            attrs: Some(BTreeMap::new()),
1053            ancestors: Some(HashSet::new()),
1054            tags: None,
1055        };
1056
1057        assert_matches!(
1058            action.validate(&schema),
1059            Err(EntityValidationError::UnknownActionComponent(
1060                UnknownActionComponentError { .. }
1061            ))
1062        );
1063    }
1064
1065    #[test]
1066    fn invalid_action_with_unknown_attrs() {
1067        let schema = test_schema();
1068        let action = PartialEntity {
1069            uid: "Action::\"view\"".parse().unwrap(),
1070            attrs: None,
1071            ancestors: Some(HashSet::new()),
1072            tags: Some(BTreeMap::new()),
1073        };
1074
1075        assert_matches!(
1076            action.validate(&schema),
1077            Err(EntityValidationError::UnknownActionComponent(
1078                UnknownActionComponentError { .. }
1079            ))
1080        );
1081    }
1082
1083    #[test]
1084    fn invalid_action_with_unexpected_attr() {
1085        let schema = test_schema();
1086        let action = PartialEntity {
1087            uid: "Action::\"view\"".parse().unwrap(),
1088            attrs: Some(BTreeMap::from_iter([(
1089                "unexpected_attr".into(),
1090                Value::from("value"),
1091            )])),
1092            ancestors: Some(HashSet::new()),
1093            tags: Some(BTreeMap::new()),
1094        };
1095
1096        assert_matches!(
1097            action.validate(&schema),
1098            Err(EntityValidationError::Concrete(
1099                EntitySchemaConformanceError::UnexpectedEntityAttr(_)
1100            ))
1101        );
1102    }
1103
1104    #[test]
1105    fn invalid_action_with_unexpected_tag() {
1106        let schema = test_schema();
1107        let action = PartialEntity {
1108            uid: "Action::\"view\"".parse().unwrap(),
1109            attrs: Some(BTreeMap::new()),
1110            ancestors: Some(HashSet::new()),
1111            tags: Some(BTreeMap::from_iter([(
1112                "unexpected_tag".into(),
1113                Value::from("value"),
1114            )])),
1115        };
1116
1117        assert_matches!(
1118            action.validate(&schema),
1119            Err(EntityValidationError::Concrete(
1120                EntitySchemaConformanceError::UnexpectedEntityTag(_)
1121            ))
1122        );
1123    }
1124
1125    #[test]
1126    fn invalid_action_with_incorrect_ancestors() {
1127        let schema = test_schema();
1128        let action = PartialEntity {
1129            uid: "Action::\"view\"".parse().unwrap(),
1130            attrs: Some(BTreeMap::new()),
1131            ancestors: Some(HashSet::from_iter(["Action::\"other\"".parse().unwrap()])),
1132            tags: Some(BTreeMap::new()),
1133        };
1134
1135        assert_matches!(
1136            action.validate(&schema),
1137            Err(EntityValidationError::MismatchedActionAncestors(
1138                MismatchedActionAncestorsError { .. }
1139            ))
1140        );
1141    }
1142
1143    #[test]
1144    fn invalid_unexpected_action() {
1145        let schema = test_schema();
1146        let action = PartialEntity {
1147            uid: "Action::\"other\"".parse().unwrap(),
1148            attrs: Some(BTreeMap::new()),
1149            ancestors: Some(HashSet::new()),
1150            tags: Some(BTreeMap::new()),
1151        };
1152
1153        assert_matches!(
1154            action.validate(&schema),
1155            Err(EntityValidationError::Concrete(
1156                EntitySchemaConformanceError::UndeclaredAction(_)
1157            ))
1158        );
1159    }
1160
1161    #[test]
1162    fn invalid_unexpected_entity_type() {
1163        let schema = test_schema();
1164        let entity = PartialEntity {
1165            uid: "UnknownType::\"test\"".parse().unwrap(),
1166            attrs: None,
1167            ancestors: None,
1168            tags: None,
1169        };
1170
1171        assert_matches!(
1172            entity.validate(&schema),
1173            Err(EntityValidationError::Concrete(
1174                EntitySchemaConformanceError::UnexpectedEntityType(_)
1175            ))
1176        );
1177    }
1178
1179    #[test]
1180    fn invalid_entity_invalid_ancestor() {
1181        let schema = test_schema();
1182        let entity = PartialEntity {
1183            uid: "User::\"alice\"".parse().unwrap(),
1184            attrs: None,
1185            ancestors: Some(HashSet::from_iter(["Resource::\"doc1\"".parse().unwrap()])),
1186            tags: None,
1187        };
1188
1189        assert_matches!(
1190            entity.validate(&schema),
1191            Err(EntityValidationError::Concrete(
1192                EntitySchemaConformanceError::InvalidAncestorType(_)
1193            ))
1194        );
1195    }
1196
1197    #[test]
1198    fn invalid_entity_invalid_attr() {
1199        let schema = test_schema();
1200        let entity = PartialEntity {
1201            uid: "User::\"alice\"".parse().unwrap(),
1202            attrs: Some(BTreeMap::from_iter([("name".into(), Value::from(42))])),
1203            ancestors: None,
1204            tags: None,
1205        };
1206
1207        assert_matches!(
1208            entity.validate(&schema),
1209            Err(EntityValidationError::Concrete(
1210                EntitySchemaConformanceError::TypeMismatch(_)
1211            ))
1212        );
1213    }
1214
1215    #[test]
1216    fn invalid_entity_invalid_tag() {
1217        let schema = test_schema();
1218        let entity = PartialEntity {
1219            uid: "User::\"alice\"".parse().unwrap(),
1220            attrs: None,
1221            ancestors: None,
1222            tags: Some(BTreeMap::from_iter([(
1223                "department".into(),
1224                Value::from(42),
1225            )])),
1226        };
1227
1228        assert_matches!(
1229            entity.validate(&schema),
1230            Err(EntityValidationError::Concrete(
1231                EntitySchemaConformanceError::TypeMismatch(_)
1232            ))
1233        );
1234    }
1235}
1236
1237#[cfg(test)]
1238mod test_consistency {
1239    use cool_asserts::assert_matches;
1240
1241    use crate::{
1242        ast::Entity,
1243        entities::{Entities, EntityJsonParser, TCComputation},
1244        extensions::Extensions,
1245        tpe::{self, entities::PartialEntities},
1246        validator::ValidatorSchema,
1247    };
1248
1249    fn schema() -> ValidatorSchema {
1250        ValidatorSchema::from_cedarschema_str(
1251            "entity A { a: Bool } tags Long;",
1252            Extensions::all_available(),
1253        )
1254        .unwrap()
1255        .0
1256    }
1257
1258    #[track_caller]
1259    fn parse_concrete_json(entity_json: serde_json::Value) -> Entity {
1260        let eparser: EntityJsonParser<'_, '_> =
1261            EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);
1262        eparser.single_from_json_value(entity_json).unwrap()
1263    }
1264
1265    #[test]
1266    fn consistent_eq_entity() {
1267        let entity_json = serde_json::json!(
1268            {
1269                "uid" : { "type" : "A", "id" : "foo", },
1270                "attrs": { "a": false },
1271                "tags" : { "t": 0 },
1272                "parents" : [ {"type": "A", "id": "bar"} ],
1273            }
1274        );
1275        let partial_entity = tpe::entities::parse_ejson(
1276            serde_json::from_value(entity_json.clone()).unwrap(),
1277            &schema(),
1278        )
1279        .unwrap();
1280        let entity = parse_concrete_json(entity_json);
1281        assert_matches!(partial_entity.check_consistency(&entity), Ok(()))
1282    }
1283
1284    #[test]
1285    fn consistent_missing_attrs() {
1286        let partial_entity_json = serde_json::json!(
1287            {
1288                "uid" : { "type" : "A", "id" : "foo", },
1289                "tags" : { "t": 0 },
1290                "parents" : [ {"type": "A", "id": "bar"} ],
1291            }
1292        );
1293        let concrete_entity_json = serde_json::json!(
1294            {
1295                "uid" : { "type" : "A", "id" : "foo", },
1296                "attrs": { "a": false },
1297                "tags" : { "t": 0 },
1298                "parents" : [ {"type": "A", "id": "bar"} ],
1299            }
1300        );
1301        let partial_entity = tpe::entities::parse_ejson(
1302            serde_json::from_value(partial_entity_json).unwrap(),
1303            &schema(),
1304        )
1305        .unwrap();
1306        let entity = parse_concrete_json(concrete_entity_json);
1307        assert_matches!(partial_entity.check_consistency(&entity), Ok(()))
1308    }
1309
1310    #[test]
1311    fn consistent_missing_tags() {
1312        let partial_entity_json = serde_json::json!(
1313            {
1314                "uid" : { "type" : "A", "id" : "foo", },
1315                "attrs": { "a": false },
1316                "parents" : [ {"type": "A", "id": "bar"} ],
1317            }
1318        );
1319        let concrete_entity_json = serde_json::json!(
1320            {
1321                "uid" : { "type" : "A", "id" : "foo", },
1322                "attrs": { "a": false },
1323                "tags" : { "t": 0 },
1324                "parents" : [ {"type": "A", "id": "bar"} ],
1325            }
1326        );
1327        let partial_entity = tpe::entities::parse_ejson(
1328            serde_json::from_value(partial_entity_json).unwrap(),
1329            &schema(),
1330        )
1331        .unwrap();
1332        let entity = parse_concrete_json(concrete_entity_json);
1333        assert_matches!(partial_entity.check_consistency(&entity), Ok(()))
1334    }
1335
1336    #[test]
1337    fn consistent_missing_parents() {
1338        let partial_entity_json = serde_json::json!(
1339            {
1340                "uid" : { "type" : "A", "id" : "foo", },
1341                "attrs": { "a": false },
1342                "tags" : { "t": 0 },
1343            }
1344        );
1345        let concrete_entity_json = serde_json::json!(
1346            {
1347                "uid" : { "type" : "A", "id" : "foo", },
1348                "attrs": { "a": false },
1349                "tags" : { "t": 0 },
1350                "parents" : [ {"type": "A", "id": "bar"} ],
1351            }
1352        );
1353        let partial_entity = tpe::entities::parse_ejson(
1354            serde_json::from_value(partial_entity_json).unwrap(),
1355            &schema(),
1356        )
1357        .unwrap();
1358        let entity = parse_concrete_json(concrete_entity_json);
1359        assert_matches!(partial_entity.check_consistency(&entity), Ok(()))
1360    }
1361
1362    #[test]
1363    fn not_consistent_different_attrs() {
1364        let partial_entity_json = serde_json::json!(
1365            {
1366                "uid" : { "type" : "A", "id" : "foo", },
1367                "attrs": { "a": true },
1368            }
1369        );
1370        let concrete_entity_json = serde_json::json!(
1371            {
1372                "uid" : { "type" : "A", "id" : "foo", },
1373                "attrs": { "a": false },
1374                "tags" : { "t": 0 },
1375                "parents" : [ {"type": "A", "id": "bar"} ],
1376            }
1377        );
1378        let partial_entity = tpe::entities::parse_ejson(
1379            serde_json::from_value(partial_entity_json).unwrap(),
1380            &schema(),
1381        )
1382        .unwrap();
1383        let entity = parse_concrete_json(concrete_entity_json);
1384        assert_matches!(
1385            partial_entity.check_consistency(&entity),
1386            Err(tpe::err::EntityConsistencyError::MismatchedAttribute(_))
1387        )
1388    }
1389
1390    #[test]
1391    fn not_consistent_different_tags() {
1392        let partial_entity_json = serde_json::json!(
1393            {
1394                "uid" : { "type" : "A", "id" : "foo", },
1395                "tags" : { "t": 1 },
1396            }
1397        );
1398        let concrete_entity_json = serde_json::json!(
1399            {
1400                "uid" : { "type" : "A", "id" : "foo", },
1401                "attrs": { "a": false },
1402                "tags" : { "t": 0 },
1403                "parents" : [ {"type": "A", "id": "bar"} ],
1404            }
1405        );
1406        let partial_entity = tpe::entities::parse_ejson(
1407            serde_json::from_value(partial_entity_json).unwrap(),
1408            &schema(),
1409        )
1410        .unwrap();
1411        let entity = parse_concrete_json(concrete_entity_json);
1412        assert_matches!(
1413            partial_entity.check_consistency(&entity),
1414            Err(tpe::err::EntityConsistencyError::MismatchedTag(_))
1415        )
1416    }
1417
1418    #[test]
1419    fn not_consistent_different_parents() {
1420        let partial_entity_json = serde_json::json!(
1421            {
1422                "uid" : { "type" : "A", "id" : "foo", },
1423                "parents" : [ {"type": "A", "id": "baz"} ],  // Different parent
1424            }
1425        );
1426        let concrete_entity_json = serde_json::json!(
1427            {
1428                "uid" : { "type" : "A", "id" : "foo", },
1429                "attrs": { "a": false },
1430                "tags" : { "t": 0 },
1431                "parents" : [ {"type": "A", "id": "bar"} ],  // Different parent
1432            }
1433        );
1434        let partial_entity = tpe::entities::parse_ejson(
1435            serde_json::from_value(partial_entity_json).unwrap(),
1436            &schema(),
1437        )
1438        .unwrap();
1439        let entity = parse_concrete_json(concrete_entity_json);
1440        assert_matches!(
1441            partial_entity.check_consistency(&entity),
1442            Err(tpe::err::EntityConsistencyError::MismatchedAncestor(_))
1443        )
1444    }
1445
1446    #[test]
1447    fn not_consistent_missing_entity() {
1448        let partial_entity_json = serde_json::json!(
1449            [{ "uid" : { "type" : "A", "id" : "foo", }, }]
1450        );
1451        let partial_entities = PartialEntities::from_json_value(
1452            serde_json::from_value(partial_entity_json).unwrap(),
1453            &schema(),
1454        )
1455        .unwrap();
1456        let concrete_entities = Entities::new();
1457        assert_matches!(
1458            partial_entities.check_consistency(&concrete_entities),
1459            Err(tpe::err::EntitiesConsistencyError::MissingEntity(_))
1460        )
1461    }
1462}