1use std::collections::{BTreeMap, HashMap, HashSet};
21
22use cedar_policy_core::{
23 ast::{
24 Eid, EntityAttrEvaluationError, EntityType, EntityUID, Id, Name,
25 PartialValueSerializedAsExpr,
26 },
27 entities::{CedarValueJson, JsonDeserializationErrorContext},
28 evaluator::RestrictedEvaluator,
29 extensions::Extensions,
30 FromNormalizedStr,
31};
32use smol_str::{SmolStr, ToSmolStr};
33
34use super::ValidatorApplySpec;
35use crate::types::OpenTag;
36use crate::{
37 err::*,
38 schema_file_format,
39 types::{AttributeType, Attributes, Type},
40 ActionBehavior, ActionEntityUID, ActionType, NamespaceDefinition, SchemaType,
41 SchemaTypeVariant, TypeOfAttribute, SCHEMA_TYPE_VARIANT_TAGS,
42};
43
44pub(crate) static ACTION_ENTITY_TYPE: &str = "Action";
50
51#[test]
52fn action_entity_type_parses() {
53 Id::from_normalized_str(ACTION_ENTITY_TYPE).unwrap();
54}
55
56pub(crate) fn is_action_entity_type(ty: &Name) -> bool {
60 ty.basename().as_ref() == ACTION_ENTITY_TYPE
61}
62
63#[derive(Debug)]
71pub struct ValidatorNamespaceDef {
72 namespace: Option<Name>,
77 pub(super) type_defs: TypeDefs,
80 pub(super) entity_types: EntityTypesDef,
82 pub(super) actions: ActionsDef,
84}
85
86#[derive(Debug)]
90pub struct TypeDefs {
91 pub(super) type_defs: HashMap<Name, SchemaType>,
92}
93
94#[derive(Debug)]
97pub struct EntityTypesDef {
98 pub(super) entity_types: HashMap<Name, EntityTypeFragment>,
99}
100
101#[derive(Debug)]
105pub struct EntityTypeFragment {
106 pub(super) attributes: WithUnresolvedTypeDefs<Type>,
111 pub(super) parents: HashSet<Name>,
116}
117
118#[derive(Debug)]
121pub struct ActionsDef {
122 pub(super) actions: HashMap<EntityUID, ActionFragment>,
123}
124
125#[derive(Debug)]
126pub struct ActionFragment {
127 pub(super) context: WithUnresolvedTypeDefs<Type>,
131 pub(super) applies_to: ValidatorApplySpec,
133 pub(super) parents: HashSet<EntityUID>,
135 pub(super) attribute_types: Attributes,
137 pub(super) attributes: BTreeMap<SmolStr, PartialValueSerializedAsExpr>,
141}
142
143type ResolveFunc<T> = dyn FnOnce(&HashMap<Name, Type>) -> Result<T>;
144pub enum WithUnresolvedTypeDefs<T> {
147 WithUnresolved(Box<ResolveFunc<T>>),
148 WithoutUnresolved(T),
149}
150
151impl<T: 'static> WithUnresolvedTypeDefs<T> {
152 pub fn new(f: impl FnOnce(&HashMap<Name, Type>) -> Result<T> + 'static) -> Self {
153 Self::WithUnresolved(Box::new(f))
154 }
155
156 pub fn map<U: 'static>(self, f: impl FnOnce(T) -> U + 'static) -> WithUnresolvedTypeDefs<U> {
157 match self {
158 Self::WithUnresolved(_) => {
159 WithUnresolvedTypeDefs::new(|type_defs| self.resolve_type_defs(type_defs).map(f))
160 }
161 Self::WithoutUnresolved(v) => WithUnresolvedTypeDefs::WithoutUnresolved(f(v)),
162 }
163 }
164
165 pub fn resolve_type_defs(self, type_defs: &HashMap<Name, Type>) -> Result<T> {
168 match self {
169 WithUnresolvedTypeDefs::WithUnresolved(f) => f(type_defs),
170 WithUnresolvedTypeDefs::WithoutUnresolved(v) => Ok(v),
171 }
172 }
173}
174
175impl<T: 'static> From<T> for WithUnresolvedTypeDefs<T> {
176 fn from(value: T) -> Self {
177 Self::WithoutUnresolved(value)
178 }
179}
180
181impl<T: std::fmt::Debug> std::fmt::Debug for WithUnresolvedTypeDefs<T> {
182 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
183 match self {
184 WithUnresolvedTypeDefs::WithUnresolved(_) => f.debug_tuple("WithUnresolved").finish(),
185 WithUnresolvedTypeDefs::WithoutUnresolved(v) => {
186 f.debug_tuple("WithoutUnresolved").field(v).finish()
187 }
188 }
189 }
190}
191
192impl TryInto<ValidatorNamespaceDef> for NamespaceDefinition {
193 type Error = SchemaError;
194
195 fn try_into(self) -> Result<ValidatorNamespaceDef> {
196 ValidatorNamespaceDef::from_namespace_definition(
197 None,
198 self,
199 ActionBehavior::default(),
200 Extensions::all_available(),
201 )
202 }
203}
204
205impl ValidatorNamespaceDef {
206 pub fn from_namespace_definition(
210 namespace: Option<Name>,
211 namespace_def: NamespaceDefinition,
212 action_behavior: ActionBehavior,
213 extensions: Extensions<'_>,
214 ) -> Result<ValidatorNamespaceDef> {
215 let mut e_types_ids: HashSet<Id> = HashSet::new();
217 for name in namespace_def.entity_types.keys() {
218 if !e_types_ids.insert(name.clone()) {
219 return Err(SchemaError::DuplicateEntityType(name.to_string()));
221 }
222 }
223 let mut a_name_eids: HashSet<SmolStr> = HashSet::new();
224 for name in namespace_def.actions.keys() {
225 if !a_name_eids.insert(name.clone()) {
226 return Err(SchemaError::DuplicateAction(name.to_string()));
228 }
229 }
230
231 Self::check_action_behavior(&namespace_def, action_behavior)?;
234
235 let type_defs = Self::build_type_defs(namespace_def.common_types, namespace.as_ref())?;
238 let actions =
239 Self::build_action_ids(namespace_def.actions, namespace.as_ref(), extensions)?;
240 let entity_types =
241 Self::build_entity_types(namespace_def.entity_types, namespace.as_ref())?;
242
243 Ok(ValidatorNamespaceDef {
244 namespace,
245 type_defs,
246 entity_types,
247 actions,
248 })
249 }
250
251 fn is_builtin_type_name(name: &SmolStr) -> bool {
252 SCHEMA_TYPE_VARIANT_TAGS
253 .iter()
254 .any(|type_name| name == type_name)
255 }
256
257 fn build_type_defs(
258 schema_file_type_def: HashMap<Id, SchemaType>,
259 schema_namespace: Option<&Name>,
260 ) -> Result<TypeDefs> {
261 let type_defs = schema_file_type_def
262 .into_iter()
263 .map(|(name, schema_ty)| -> Result<_> {
264 let name_str = name.clone().into_smolstr();
265 if Self::is_builtin_type_name(&name_str) {
266 return Err(SchemaError::DuplicateCommonType(name_str.to_string()));
267 }
268 let name =
269 Name::from(name).prefix_namespace_if_unqualified(schema_namespace.cloned());
270 Ok((
271 name,
272 schema_ty
273 .prefix_common_type_references_with_namespace(schema_namespace.cloned()),
274 ))
275 })
276 .collect::<Result<HashMap<_, _>>>()?;
277 Ok(TypeDefs { type_defs })
278 }
279
280 fn build_entity_types(
284 schema_files_types: HashMap<Id, schema_file_format::EntityType>,
285 schema_namespace: Option<&Name>,
286 ) -> Result<EntityTypesDef> {
287 Ok(EntityTypesDef {
288 entity_types: schema_files_types
289 .into_iter()
290 .map(|(id, entity_type)| -> Result<_> {
291 let name =
292 Name::from(id).prefix_namespace_if_unqualified(schema_namespace.cloned());
293
294 let parents = entity_type
295 .member_of_types
296 .into_iter()
297 .map(|ty| ty.prefix_namespace_if_unqualified(schema_namespace.cloned()))
298 .collect();
299
300 let attributes = Self::try_schema_type_into_validator_type(
301 schema_namespace,
302 entity_type.shape.into_inner(),
303 )?;
304
305 Ok((
306 name,
307 EntityTypeFragment {
308 attributes,
309 parents,
310 },
311 ))
312 })
313 .collect::<Result<HashMap<_, _>>>()?,
314 })
315 }
316
317 fn jsonval_to_type_helper(v: &CedarValueJson, action_id: &EntityUID) -> Result<Type> {
323 match v {
324 CedarValueJson::Bool(_) => Ok(Type::primitive_boolean()),
325 CedarValueJson::Long(_) => Ok(Type::primitive_long()),
326 CedarValueJson::String(_) => Ok(Type::primitive_string()),
327 CedarValueJson::Record(r) => {
328 let mut required_attrs: HashMap<SmolStr, Type> = HashMap::new();
329 for (k, v_prime) in r {
330 let t = Self::jsonval_to_type_helper(v_prime, action_id);
331 match t {
332 Ok(ty) => required_attrs.insert(k.clone(), ty),
333 Err(e) => return Err(e),
334 };
335 }
336 Ok(Type::record_with_required_attributes(
337 required_attrs,
338 OpenTag::ClosedAttributes,
339 ))
340 }
341 CedarValueJson::Set(v) => match v.first() {
342 None => Err(SchemaError::ActionAttributesContainEmptySet(
344 action_id.clone(),
345 )),
346 Some(element) => {
347 let element_type = Self::jsonval_to_type_helper(element, action_id);
348 match element_type {
349 Ok(t) => Ok(Type::Set {
350 element_type: Some(Box::new(t)),
351 }),
352 Err(_) => element_type,
353 }
354 }
355 },
356 CedarValueJson::EntityEscape { __entity: _ } => {
357 Err(SchemaError::UnsupportedActionAttribute(
358 action_id.clone(),
359 "entity escape (`__entity`)".to_owned(),
360 ))
361 }
362 CedarValueJson::ExprEscape { __expr: _ } => {
363 Err(SchemaError::UnsupportedActionAttribute(
364 action_id.clone(),
365 "expression escape (`__expr`)".to_owned(),
366 ))
367 }
368 CedarValueJson::ExtnEscape { __extn: _ } => {
369 Err(SchemaError::UnsupportedActionAttribute(
370 action_id.clone(),
371 "extension function escape (`__extn`)".to_owned(),
372 ))
373 }
374 CedarValueJson::Null => Err(SchemaError::UnsupportedActionAttribute(
375 action_id.clone(),
376 "null".to_owned(),
377 )),
378 }
379 }
380
381 fn convert_attr_jsonval_map_to_attributes(
383 m: HashMap<SmolStr, CedarValueJson>,
384 action_id: &EntityUID,
385 extensions: Extensions<'_>,
386 ) -> Result<(Attributes, BTreeMap<SmolStr, PartialValueSerializedAsExpr>)> {
387 let mut attr_types: HashMap<SmolStr, Type> = HashMap::with_capacity(m.len());
388 let mut attr_values: BTreeMap<SmolStr, PartialValueSerializedAsExpr> = BTreeMap::new();
389 let evaluator = RestrictedEvaluator::new(&extensions);
390
391 for (k, v) in m {
392 let t = Self::jsonval_to_type_helper(&v, action_id);
393 match t {
394 Ok(ty) => attr_types.insert(k.clone(), ty),
395 Err(e) => return Err(e),
396 };
397
398 #[allow(clippy::expect_used)]
408 let e = v.into_expr(|| JsonDeserializationErrorContext::EntityAttribute { uid: action_id.clone(), attr: k.clone() }).expect("`Self::jsonval_to_type_helper` will always return `Err` for a `CedarValueJson` that might make `into_expr` return `Err`");
409 let pv = evaluator
410 .partial_interpret(e.as_borrowed())
411 .map_err(|err| {
412 SchemaError::ActionAttrEval(EntityAttrEvaluationError {
413 uid: action_id.clone(),
414 attr: k.clone(),
415 err,
416 })
417 })?;
418 attr_values.insert(k.clone(), pv.into());
419 }
420 Ok((
421 Attributes::with_required_attributes(attr_types),
422 attr_values,
423 ))
424 }
425
426 fn build_action_ids(
430 schema_file_actions: HashMap<SmolStr, ActionType>,
431 schema_namespace: Option<&Name>,
432 extensions: Extensions<'_>,
433 ) -> Result<ActionsDef> {
434 Ok(ActionsDef {
435 actions: schema_file_actions
436 .into_iter()
437 .map(|(action_id_str, action_type)| -> Result<_> {
438 let action_id = Self::parse_action_id_with_namespace(
439 &ActionEntityUID::default_type(action_id_str),
440 schema_namespace,
441 );
442
443 let (principal_types, resource_types, context) = action_type
444 .applies_to
445 .map(|applies_to| {
446 (
447 applies_to.principal_types,
448 applies_to.resource_types,
449 applies_to.context,
450 )
451 })
452 .unwrap_or_default();
453
454 let applies_to = ValidatorApplySpec::new(
458 Self::parse_apply_spec_type_list(principal_types, schema_namespace),
459 Self::parse_apply_spec_type_list(resource_types, schema_namespace),
460 );
461
462 let context = Self::try_schema_type_into_validator_type(
463 schema_namespace,
464 context.into_inner(),
465 )?;
466
467 let parents = action_type
468 .member_of
469 .unwrap_or_default()
470 .iter()
471 .map(|parent| {
472 Self::parse_action_id_with_namespace(parent, schema_namespace)
473 })
474 .collect::<HashSet<_>>();
475
476 let (attribute_types, attributes) =
477 Self::convert_attr_jsonval_map_to_attributes(
478 action_type.attributes.unwrap_or_default(),
479 &action_id,
480 extensions,
481 )?;
482
483 Ok((
484 action_id,
485 ActionFragment {
486 context,
487 applies_to,
488 parents,
489 attribute_types,
490 attributes,
491 },
492 ))
493 })
494 .collect::<Result<HashMap<_, _>>>()?,
495 })
496 }
497
498 fn check_action_behavior(
504 schema_file: &NamespaceDefinition,
505 action_behavior: ActionBehavior,
506 ) -> Result<()> {
507 if schema_file
508 .entity_types
509 .iter()
510 .any(|(name, _)| name.to_smolstr() == ACTION_ENTITY_TYPE)
514 {
515 return Err(SchemaError::ActionEntityTypeDeclared);
516 }
517 if action_behavior == ActionBehavior::ProhibitAttributes {
518 let mut actions_with_attributes: Vec<String> = Vec::new();
519 for (name, a) in &schema_file.actions {
520 if a.attributes.is_some() {
521 actions_with_attributes.push(name.to_string());
522 }
523 }
524 if !actions_with_attributes.is_empty() {
525 return Err(SchemaError::UnsupportedFeature(
526 UnsupportedFeature::ActionAttributes(actions_with_attributes),
527 ));
528 }
529 }
530
531 Ok(())
532 }
533
534 fn parse_record_attributes(
539 schema_namespace: Option<&Name>,
540 attrs: impl IntoIterator<Item = (SmolStr, TypeOfAttribute)>,
541 ) -> Result<WithUnresolvedTypeDefs<Attributes>> {
542 let attrs_with_type_defs = attrs
543 .into_iter()
544 .map(|(attr, ty)| -> Result<_> {
545 Ok((
546 attr,
547 (
548 Self::try_schema_type_into_validator_type(schema_namespace, ty.ty)?,
549 ty.required,
550 ),
551 ))
552 })
553 .collect::<Result<Vec<_>>>()?;
554 Ok(WithUnresolvedTypeDefs::new(|typ_defs| {
555 attrs_with_type_defs
556 .into_iter()
557 .map(|(s, (attr_ty, is_req))| {
558 attr_ty
559 .resolve_type_defs(typ_defs)
560 .map(|ty| (s, AttributeType::new(ty, is_req)))
561 })
562 .collect::<Result<Vec<_>>>()
563 .map(Attributes::with_attributes)
564 }))
565 }
566
567 fn parse_apply_spec_type_list(
572 types: Option<Vec<Name>>,
573 namespace: Option<&Name>,
574 ) -> HashSet<EntityType> {
575 types
576 .map(|types| {
577 types
578 .iter()
579 .map(|ty| {
583 EntityType::Specified(
584 ty.prefix_namespace_if_unqualified(namespace.cloned()),
585 )
586 })
587 .collect::<HashSet<_>>()
589 })
590 .unwrap_or_else(|| HashSet::from([EntityType::Unspecified]))
591 }
592
593 fn parse_action_id_with_namespace(
599 action_id: &ActionEntityUID,
600 namespace: Option<&Name>,
601 ) -> EntityUID {
602 let namespaced_action_type = if let Some(action_ty) = &action_id.ty {
603 action_ty.prefix_namespace_if_unqualified(namespace.cloned())
604 } else {
605 #[allow(clippy::expect_used)]
607 let id = Id::from_normalized_str(ACTION_ENTITY_TYPE).expect(
608 "Expected that the constant ACTION_ENTITY_TYPE would be a valid entity type.",
609 );
610 match namespace {
611 Some(namespace) => Name::type_in_namespace(id, namespace.clone(), None),
612 None => Name::unqualified_name(id),
613 }
614 };
615 EntityUID::from_components(namespaced_action_type, Eid::new(action_id.id.clone()), None)
616 }
617
618 pub(crate) fn try_schema_type_into_validator_type(
624 default_namespace: Option<&Name>,
625 schema_ty: SchemaType,
626 ) -> Result<WithUnresolvedTypeDefs<Type>> {
627 match schema_ty {
628 SchemaType::Type(SchemaTypeVariant::String) => Ok(Type::primitive_string().into()),
629 SchemaType::Type(SchemaTypeVariant::Long) => Ok(Type::primitive_long().into()),
630 SchemaType::Type(SchemaTypeVariant::Boolean) => Ok(Type::primitive_boolean().into()),
631 SchemaType::Type(SchemaTypeVariant::Set { element }) => Ok(
632 Self::try_schema_type_into_validator_type(default_namespace, *element)?
633 .map(Type::set),
634 ),
635 SchemaType::Type(SchemaTypeVariant::Record {
636 attributes,
637 additional_attributes,
638 }) => {
639 if cfg!(not(feature = "partial-validate")) && additional_attributes {
640 Err(SchemaError::UnsupportedFeature(
641 UnsupportedFeature::OpenRecordsAndEntities,
642 ))
643 } else {
644 Ok(
645 Self::parse_record_attributes(default_namespace, attributes)?.map(
646 move |attrs| {
647 Type::record_with_attributes(
648 attrs,
649 if additional_attributes {
650 OpenTag::OpenAttributes
651 } else {
652 OpenTag::ClosedAttributes
653 },
654 )
655 },
656 ),
657 )
658 }
659 }
660 SchemaType::Type(SchemaTypeVariant::Entity { name }) => {
661 Ok(Type::named_entity_reference(
662 name.prefix_namespace_if_unqualified(default_namespace.cloned()),
663 )
664 .into())
665 }
666 SchemaType::Type(SchemaTypeVariant::Extension { name }) => {
667 let extension_type_name = Name::unqualified_name(name);
668 Ok(Type::extension(extension_type_name).into())
669 }
670 SchemaType::TypeDef { type_name } => {
671 let defined_type_name =
672 type_name.prefix_namespace_if_unqualified(default_namespace.cloned());
673 Ok(WithUnresolvedTypeDefs::new(move |typ_defs| {
674 typ_defs.get(&defined_type_name).cloned().ok_or(
675 SchemaError::UndeclaredCommonTypes(HashSet::from([
676 defined_type_name.to_string()
677 ])),
678 )
679 }))
680 }
681 }
682 }
683
684 pub fn namespace(&self) -> &Option<Name> {
686 &self.namespace
687 }
688}