1use 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#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
70pub struct EntityJson {
71 uid: EntityUidJson,
73 #[serde(default)]
80 attrs: Option<DeduplicatedMap>,
82 #[serde(default)]
83 parents: Option<Vec<EntityUidJson>>,
85 #[serde(default)]
86 tags: Option<DeduplicatedMap>,
91}
92
93#[derive(Debug, Clone, PartialEq, Eq)]
95pub struct PartialEntity {
96 pub(crate) uid: EntityUID,
98 pub(crate) attrs: Option<BTreeMap<SmolStr, Value>>,
100 pub(crate) ancestors: Option<HashSet<EntityUID>>,
102 pub(crate) tags: Option<BTreeMap<SmolStr, Value>>,
104}
105
106impl 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 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 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
204pub 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 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 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
462pub(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#[derive(Clone, Debug, Default, PartialEq, Eq)]
488pub struct PartialEntities {
489 entities: HashMap<EntityUID, PartialEntity>,
492}
493
494impl PartialEntities {
495 pub fn new() -> Self {
497 Self::default()
498 }
499
500 pub fn entities(&self) -> impl Iterator<Item = &PartialEntity> {
502 self.entities.values()
503 }
504
505 pub fn compute_tc(&mut self) -> std::result::Result<(), TcError<EntityUID>> {
507 compute_tc(&mut self.entities, true)
508 }
509
510 pub fn enforce_tc_and_dag(&self) -> std::result::Result<(), TcError<EntityUID>> {
512 enforce_tc_and_dag(&self.entities)
513 }
514
515 pub fn get(&self, euid: &EntityUID) -> Option<&PartialEntity> {
517 self.entities.get(euid)
518 }
519
520 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 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 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 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 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 pub fn from_entities_unchecked(
625 entities: impl Iterator<Item = (EntityUID, PartialEntity)>,
626 ) -> Self {
627 Self {
628 entities: entities.collect(),
629 }
630 }
631
632 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 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 partial_entities.insert_actions(schema);
668 Ok(partial_entities)
669 }
670
671 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 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 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"} ], }
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"} ], }
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}