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, 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#[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
148 pub fn uid(&self) -> &EntityUID {
150 &self.uid
151 }
152
153 pub fn attrs(&self) -> Option<&BTreeMap<SmolStr, Value>> {
155 self.attrs.as_ref()
156 }
157
158 pub fn ancestors(&self) -> Option<&HashSet<EntityUID>> {
160 self.ancestors.as_ref()
161 }
162
163 pub fn tags(&self) -> Option<&BTreeMap<SmolStr, Value>> {
165 self.tags.as_ref()
166 }
167
168 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
225pub 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 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 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
483pub(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#[derive(Clone, Debug, Default, PartialEq, Eq)]
509pub struct PartialEntities {
510 entities: HashMap<EntityUID, PartialEntity>,
513}
514
515impl PartialEntities {
516 pub fn new() -> Self {
518 Self::default()
519 }
520
521 pub fn entities(&self) -> impl Iterator<Item = &PartialEntity> {
523 self.entities.values()
524 }
525
526 pub fn compute_tc(&mut self) -> std::result::Result<(), TcError<EntityUID>> {
528 compute_tc(&mut self.entities, true)
529 }
530
531 pub fn enforce_tc_and_dag(&self) -> std::result::Result<(), TcError<EntityUID>> {
533 enforce_tc_and_dag(&self.entities)
534 }
535
536 pub fn get(&self, euid: &EntityUID) -> Option<&PartialEntity> {
538 self.entities.get(euid)
539 }
540
541 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 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 let mut entities = Self {
573 entities: entities_map,
574 };
575 entities.insert_actions(schema);
576 Ok(entities)
577 }
578
579 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 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 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 pub fn from_entities_unchecked(
664 entities: impl Iterator<Item = (EntityUID, PartialEntity)>,
665 ) -> Self {
666 Self {
667 entities: entities.collect(),
668 }
669 }
670
671 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 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 partial_entities.insert_actions(schema);
707 Ok(partial_entities)
708 }
709
710 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 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 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"} ], }
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"} ], }
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}