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