cedar_policy_validator/schema/namespace_def.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 the definition of `ValidatorNamespaceDef` and of types
18//! it relies on
19
20use std::collections::{hash_map::Entry, BTreeMap, HashMap, HashSet};
21
22use cedar_policy_core::{
23 ast::{
24 EntityAttrEvaluationError, EntityType, EntityUID, InternalName, Name,
25 PartialValueSerializedAsExpr, UnreservedId,
26 },
27 entities::{json::err::JsonDeserializationErrorContext, CedarValueJson},
28 evaluator::RestrictedEvaluator,
29 extensions::Extensions,
30 fuzzy_match::fuzzy_search,
31};
32use itertools::Itertools;
33use nonempty::{nonempty, NonEmpty};
34use smol_str::{SmolStr, ToSmolStr};
35
36use super::{internal_name_to_entity_type, AllDefs, ValidatorApplySpec};
37use crate::{
38 err::{schema_errors::*, SchemaError},
39 json_schema::{self, CommonTypeId},
40 types::{AttributeType, Attributes, OpenTag, Type},
41 ActionBehavior, ConditionalName, RawName, ReferenceType,
42};
43
44/// A single namespace definition from the schema JSON or Cedar syntax,
45/// processed into a form which is closer to that used by the validator.
46/// The processing includes detection of some errors, for example, parse errors
47/// in entity/common type names or entity/common types which are declared
48/// multiple times.
49///
50/// In this representation, there may still be references to undeclared
51/// entity/common types, because any entity/common type may be declared in a
52/// different fragment that will only be known about when building the complete
53/// [`crate::ValidatorSchema`].
54///
55/// The parameter `N` is the type of entity type names and common type names in
56/// attributes/parents fields in this [`ValidatorNamespaceDef`], including
57/// recursively. (It doesn't affect the type of common and entity type names
58/// _that are being declared here_, which are already fully-qualified in this
59/// representation. It only affects the type of common and entity type
60/// _references_.)
61/// For example:
62/// - `N` = [`ConditionalName`]: References to entity/common types are not
63/// yet fully qualified/disambiguated
64/// - `N` = [`InternalName`]: All references to entity/common types have been
65/// resolved into fully-qualified [`InternalName`]s
66///
67/// `A` is like `N`, but `A` governs typenames in `appliesTo` fields, while
68/// `N` governs all other type references.
69#[derive(Debug, Clone)]
70pub struct ValidatorNamespaceDef<N, A> {
71 /// The (fully-qualified) name of the namespace this is a definition of, or
72 /// `None` if this is a definition for the empty namespace.
73 ///
74 /// This is informational only; it does not change the semantics of any
75 /// definition in `common_types`, `entity_types`, or `actions`. All
76 /// entity/common type names in `common_types`, `entity_types`, and
77 /// `actions` are already either fully qualified/disambiguated, or stored in
78 /// [`ConditionalName`] format which does not require referencing the
79 /// implicit `namespace` directly any longer.
80 /// This `namespace` field is used only in tests and by the `cedar_policy`
81 /// function `SchemaFragment::namespaces()`.
82 namespace: Option<InternalName>,
83 /// Common type definitions, which can be used to define entity
84 /// type attributes, action contexts, and other common types.
85 pub(super) common_types: CommonTypeDefs<N>,
86 /// Entity type declarations.
87 pub(super) entity_types: EntityTypesDef<N>,
88 /// Action declarations.
89 pub(super) actions: ActionsDef<N, A>,
90}
91
92impl<N, A> ValidatorNamespaceDef<N, A> {
93 /// Get the fully-qualified [`InternalName`]s of all entity types declared
94 /// in this [`ValidatorNamespaceDef`].
95 pub fn all_declared_entity_type_names(&self) -> impl Iterator<Item = &InternalName> {
96 self.entity_types
97 .defs
98 .keys()
99 .map(|ety| ety.as_ref().as_ref())
100 }
101
102 /// Get the fully-qualified [`InternalName`]s of all common types declared
103 /// in this [`ValidatorNamespaceDef`].
104 pub fn all_declared_common_type_names(&self) -> impl Iterator<Item = &InternalName> {
105 self.common_types.defs.keys()
106 }
107
108 /// Get the fully-qualified [`EntityUID`]s of all actions declared in this
109 /// [`ValidatorNamespaceDef`].
110 pub fn all_declared_action_names(&self) -> impl Iterator<Item = &EntityUID> {
111 self.actions.actions.keys()
112 }
113
114 /// The fully-qualified [`InternalName`] of the namespace this is a definition of.
115 /// `None` indicates this definition is for the empty namespace.
116 pub fn namespace(&self) -> Option<&InternalName> {
117 self.namespace.as_ref()
118 }
119}
120
121impl ValidatorNamespaceDef<ConditionalName, ConditionalName> {
122 /// Construct a new [`ValidatorNamespaceDef<ConditionalName>`] from the raw [`json_schema::NamespaceDefinition`]
123 pub fn from_namespace_definition(
124 namespace: Option<InternalName>,
125 namespace_def: json_schema::NamespaceDefinition<RawName>,
126 action_behavior: ActionBehavior,
127 extensions: &Extensions<'_>,
128 ) -> crate::err::Result<ValidatorNamespaceDef<ConditionalName, ConditionalName>> {
129 // Return early with an error if actions cannot be in groups or have
130 // attributes, but the schema contains action groups or attributes.
131 Self::check_action_behavior(&namespace_def, action_behavior)?;
132
133 // Convert the common types, actions and entity types from the schema
134 // file into the representation used by the validator.
135 let common_types = CommonTypeDefs::from_raw_common_types(
136 namespace_def
137 .common_types
138 .into_iter()
139 .map(|(key, value)| (key, value.ty)),
140 namespace.as_ref(),
141 )?;
142 let actions =
143 ActionsDef::from_raw_actions(namespace_def.actions, namespace.as_ref(), extensions)?;
144 let entity_types =
145 EntityTypesDef::from_raw_entity_types(namespace_def.entity_types, namespace.as_ref())?;
146
147 Ok(ValidatorNamespaceDef {
148 namespace,
149 common_types,
150 entity_types,
151 actions,
152 })
153 }
154
155 /// Construct a new [`ValidatorNamespaceDef<ConditionalName>`] containing
156 /// only the given common-type definitions, which are already given in
157 /// terms of [`ConditionalName`]s.
158 pub fn from_common_type_defs(
159 namespace: Option<InternalName>,
160 defs: HashMap<UnreservedId, json_schema::Type<ConditionalName>>,
161 ) -> crate::err::Result<ValidatorNamespaceDef<ConditionalName, ConditionalName>> {
162 let common_types = CommonTypeDefs::from_conditionalname_typedefs(defs, namespace.as_ref())?;
163 Ok(ValidatorNamespaceDef {
164 namespace,
165 common_types,
166 entity_types: EntityTypesDef::new(),
167 actions: ActionsDef::new(),
168 })
169 }
170
171 /// Construct a new [`ValidatorNamespaceDef<ConditionalName>`] containing
172 /// only a single given common-type definition, which is already given in
173 /// terms of [`ConditionalName`]s.
174 ///
175 /// Unlike `from_common_type_defs()`, this function cannot fail, because
176 /// there is only one def so it cannot have a name collision with itself
177 pub fn from_common_type_def(
178 namespace: Option<InternalName>,
179 def: (UnreservedId, json_schema::Type<ConditionalName>),
180 ) -> ValidatorNamespaceDef<ConditionalName, ConditionalName> {
181 let common_types = CommonTypeDefs::from_conditionalname_typedef(def, namespace.as_ref());
182 ValidatorNamespaceDef {
183 namespace,
184 common_types,
185 entity_types: EntityTypesDef::new(),
186 actions: ActionsDef::new(),
187 }
188 }
189
190 /// Convert this [`ValidatorNamespaceDef<ConditionalName>`] into a
191 /// [`ValidatorNamespaceDef<InternalName>`] by fully-qualifying all
192 /// typenames that appear anywhere in any definitions.
193 ///
194 /// `all_defs` needs to contain the full set of all fully-qualified typenames
195 /// and actions that are defined in the schema (in all schema fragments).
196 pub fn fully_qualify_type_references(
197 self,
198 all_defs: &AllDefs,
199 ) -> Result<ValidatorNamespaceDef<InternalName, EntityType>, SchemaError> {
200 match (
201 self.common_types.fully_qualify_type_references(all_defs),
202 self.entity_types.fully_qualify_type_references(all_defs),
203 self.actions.fully_qualify_type_references(all_defs),
204 ) {
205 (Ok(common_types), Ok(entity_types), Ok(actions)) => Ok(ValidatorNamespaceDef {
206 namespace: self.namespace,
207 common_types,
208 entity_types,
209 actions,
210 }),
211 (res1, res2, res3) => {
212 // PANIC SAFETY: at least one of the results is `Err`, so the input to `NonEmpty::collect()` cannot be an empty iterator
213 #[allow(clippy::expect_used)]
214 let errs = NonEmpty::collect(
215 res1.err()
216 .into_iter()
217 .map(SchemaError::from)
218 .chain(res2.err().map(SchemaError::from))
219 .chain(res3.err().map(SchemaError::from)),
220 )
221 .expect("there must be an error");
222 Err(SchemaError::join_nonempty(errs))
223 }
224 }
225 }
226
227 /// Check that `schema_nsdef` uses actions in a way consistent with the
228 /// specified `action_behavior`. When the behavior specifies that actions
229 /// should not be used in groups and should not have attributes, then this
230 /// function will return `Err` if it sees any action groups or attributes
231 /// declared in the schema.
232 fn check_action_behavior<N>(
233 schema_nsdef: &json_schema::NamespaceDefinition<N>,
234 action_behavior: ActionBehavior,
235 ) -> crate::err::Result<()> {
236 if schema_nsdef
237 .entity_types
238 .iter()
239 // The `name` in an entity type declaration cannot be qualified
240 // with a namespace (it always implicitly takes the schema
241 // namespace), so we do this comparison directly.
242 .any(|(name, _)| name.to_smolstr() == cedar_policy_core::ast::ACTION_ENTITY_TYPE)
243 {
244 return Err(ActionEntityTypeDeclaredError {}.into());
245 }
246 if action_behavior == ActionBehavior::ProhibitAttributes {
247 let mut actions_with_attributes: Vec<String> = Vec::new();
248 for (name, a) in &schema_nsdef.actions {
249 if a.attributes.is_some() {
250 actions_with_attributes.push(name.to_string());
251 }
252 }
253 if !actions_with_attributes.is_empty() {
254 actions_with_attributes.sort(); // TODO(#833): sort required for deterministic error messages
255 return Err(
256 UnsupportedFeatureError(UnsupportedFeature::ActionAttributes(
257 actions_with_attributes,
258 ))
259 .into(),
260 );
261 }
262 }
263
264 Ok(())
265 }
266}
267
268/// Holds a map from (fully qualified) [`InternalName`]s of common type
269/// definitions to their corresponding [`json_schema::Type`]. The common type
270/// [`InternalName`]s (keys in the map) are fully qualified, but inside the
271/// [`json_schema::Type`]s (values in the map), entity/common type references may or
272/// may not be fully qualified yet, depending on `N`; see notes on
273/// [`json_schema::Type`].
274#[derive(Debug, Clone)]
275pub struct CommonTypeDefs<N> {
276 pub(super) defs: HashMap<InternalName, json_schema::Type<N>>,
277}
278
279impl CommonTypeDefs<ConditionalName> {
280 /// Construct a [`CommonTypeDefs<ConditionalName>`] by converting the
281 /// structures used by the schema format to those used internally by the
282 /// validator.
283 pub(crate) fn from_raw_common_types(
284 schema_file_type_def: impl IntoIterator<Item = (CommonTypeId, json_schema::Type<RawName>)>,
285 schema_namespace: Option<&InternalName>,
286 ) -> crate::err::Result<Self> {
287 let mut defs = HashMap::new();
288 for (id, schema_ty) in schema_file_type_def {
289 let name = RawName::new_from_unreserved(id.into()).qualify_with(schema_namespace); // the declaration name is always (unconditionally) prefixed by the current/active namespace
290 match defs.entry(name) {
291 Entry::Vacant(ventry) => {
292 ventry
293 .insert(schema_ty.conditionally_qualify_type_references(schema_namespace));
294 }
295 Entry::Occupied(oentry) => {
296 return Err(SchemaError::DuplicateCommonType(DuplicateCommonTypeError {
297 ty: oentry.key().clone(),
298 }));
299 }
300 }
301 }
302 Ok(Self { defs })
303 }
304
305 /// Construct a [`CommonTypeDefs<ConditionalName>`] by converting the
306 /// structures used by the schema format to those used internally by the
307 /// validator; but unlike `from_raw_common_types()`, this function allows you to
308 /// directly supply [`ConditionalName`]s in the typedefs
309 pub(crate) fn from_conditionalname_typedefs(
310 input_type_defs: HashMap<UnreservedId, json_schema::Type<ConditionalName>>,
311 schema_namespace: Option<&InternalName>,
312 ) -> crate::err::Result<Self> {
313 let mut defs = HashMap::with_capacity(input_type_defs.len());
314 for (id, schema_ty) in input_type_defs {
315 let name = RawName::new_from_unreserved(id).qualify_with(schema_namespace); // the declaration name is always (unconditionally) prefixed by the current/active namespace
316 match defs.entry(name) {
317 Entry::Vacant(ventry) => {
318 ventry.insert(schema_ty);
319 }
320 Entry::Occupied(oentry) => {
321 return Err(SchemaError::DuplicateCommonType(DuplicateCommonTypeError {
322 ty: oentry.key().clone(),
323 }));
324 }
325 }
326 }
327 Ok(Self { defs })
328 }
329
330 /// Construct a [`CommonTypeDefs<ConditionalName>`] representing a single
331 /// typedef in the given namespace.
332 ///
333 /// Unlike [`from_conditionalname_typedefs()`], this function cannot fail,
334 /// because there is only one typedef so it cannot have a name collision
335 /// with itself
336 pub(crate) fn from_conditionalname_typedef(
337 (id, schema_ty): (UnreservedId, json_schema::Type<ConditionalName>),
338 schema_namespace: Option<&InternalName>,
339 ) -> Self {
340 Self {
341 defs: HashMap::from_iter([(
342 RawName::new_from_unreserved(id).qualify_with(schema_namespace),
343 schema_ty,
344 )]),
345 }
346 }
347
348 /// Convert this [`CommonTypeDefs<ConditionalName>`] into a
349 /// [`CommonTypeDefs<InternalName>`] by fully-qualifying all typenames that
350 /// appear anywhere in any definitions.
351 ///
352 /// `all_defs` needs to contain the full set of all fully-qualified typenames
353 /// and actions that are defined in the schema (in all schema fragments).
354 pub fn fully_qualify_type_references(
355 self,
356 all_defs: &AllDefs,
357 ) -> Result<CommonTypeDefs<InternalName>, TypeNotDefinedError> {
358 Ok(CommonTypeDefs {
359 defs: self
360 .defs
361 .into_iter()
362 .map(|(k, v)| Ok((k, v.fully_qualify_type_references(all_defs)?)))
363 .collect::<Result<_, TypeNotDefinedError>>()?,
364 })
365 }
366}
367
368/// Holds a map from (fully qualified) [`EntityType`]s (names of entity types) to
369/// their corresponding [`EntityTypeFragment`]. The [`EntityType`] keys in
370/// the map are fully qualified, but inside the [`EntityTypeFragment`]s (values
371/// in the map), entity/common type references may or may not be fully qualified
372/// yet, depending on `N`; see notes on [`EntityTypeFragment`].
373///
374/// Inside the [`EntityTypeFragment`]s, entity type parents and attributes may
375/// reference undeclared entity/common types (that will be declared in a
376/// different schema fragment).
377///
378/// All [`EntityType`] keys in this map are declared in this schema fragment.
379#[derive(Debug, Clone)]
380pub struct EntityTypesDef<N> {
381 pub(super) defs: HashMap<EntityType, EntityTypeFragment<N>>,
382}
383
384impl<N> EntityTypesDef<N> {
385 /// Construct an empty [`EntityTypesDef`] defining no entity types.
386 pub fn new() -> Self {
387 Self {
388 defs: HashMap::new(),
389 }
390 }
391}
392
393impl EntityTypesDef<ConditionalName> {
394 /// Construct a [`EntityTypesDef<ConditionalName>`] by converting the
395 /// structures used by the schema format to those used internally by the
396 /// validator.
397 pub(crate) fn from_raw_entity_types(
398 schema_files_types: impl IntoIterator<Item = (UnreservedId, json_schema::EntityType<RawName>)>,
399 schema_namespace: Option<&InternalName>,
400 ) -> crate::err::Result<Self> {
401 let mut defs: HashMap<EntityType, _> = HashMap::new();
402 for (id, entity_type) in schema_files_types {
403 let ety = internal_name_to_entity_type(
404 RawName::new_from_unreserved(id).qualify_with(schema_namespace), // the declaration name is always (unconditionally) prefixed by the current/active namespace
405 )?;
406 match defs.entry(ety) {
407 Entry::Vacant(ventry) => {
408 ventry.insert(EntityTypeFragment::from_raw_entity_type(
409 entity_type,
410 schema_namespace,
411 ));
412 }
413 Entry::Occupied(entry) => {
414 return Err(DuplicateEntityTypeError {
415 ty: entry.key().clone(),
416 }
417 .into());
418 }
419 }
420 }
421 Ok(EntityTypesDef { defs })
422 }
423
424 /// Convert this [`EntityTypesDef<ConditionalName>`] into a
425 /// [`EntityTypesDef<InternalName>`] by fully-qualifying all typenames that
426 /// appear anywhere in any definitions.
427 ///
428 /// `all_defs` needs to contain the full set of all fully-qualified typenames
429 /// and actions that are defined in the schema (in all schema fragments).
430 pub fn fully_qualify_type_references(
431 self,
432 all_defs: &AllDefs,
433 ) -> Result<EntityTypesDef<InternalName>, TypeNotDefinedError> {
434 Ok(EntityTypesDef {
435 defs: self
436 .defs
437 .into_iter()
438 .map(|(k, v)| Ok((k, v.fully_qualify_type_references(all_defs)?)))
439 .collect::<Result<_, TypeNotDefinedError>>()?,
440 })
441 }
442}
443
444/// Holds the attributes and parents information for an entity type definition.
445///
446/// In this representation, references to common types may not yet have been
447/// fully resolved/inlined, and `parents`, `attributes`, and `tags` may all
448/// reference undeclared entity/common types. Furthermore, entity/common type
449/// references in `parents`, `attributes`, and `tags` may or may not be fully
450/// qualified yet, depending on `N`.
451#[derive(Debug, Clone)]
452pub struct EntityTypeFragment<N> {
453 /// Description of the attribute types for this entity type.
454 ///
455 /// This may contain references to common types which have not yet been
456 /// resolved/inlined (e.g., because they are not defined in this schema
457 /// fragment).
458 /// In the extreme case, this may itself be just a common type pointing to a
459 /// `Record` type defined in another fragment.
460 pub(super) attributes: json_schema::AttributesOrContext<N>,
461 /// Direct parent entity types for this entity type.
462 /// These entity types may be declared in a different namespace or schema
463 /// fragment.
464 ///
465 /// We will check for undeclared parent types when combining fragments into
466 /// a [`crate::ValidatorSchema`].
467 pub(super) parents: HashSet<N>,
468 /// Tag type for this entity type. `None` means no tags are allowed on this
469 /// entity type.
470 ///
471 /// This may contain references to common types which have not yet been
472 /// resolved/inlined (e.g., because they are not defined in this schema
473 /// fragment).
474 pub(super) tags: Option<json_schema::Type<N>>,
475}
476
477impl EntityTypeFragment<ConditionalName> {
478 /// Construct a [`EntityTypeFragment<ConditionalName>`] by converting the
479 /// structures used by the schema format to those used internally by the
480 /// validator.
481 pub(crate) fn from_raw_entity_type(
482 schema_file_type: json_schema::EntityType<RawName>,
483 schema_namespace: Option<&InternalName>,
484 ) -> Self {
485 Self {
486 attributes: schema_file_type
487 .shape
488 .conditionally_qualify_type_references(schema_namespace),
489 parents: schema_file_type
490 .member_of_types
491 .into_iter()
492 .map(|raw_name| {
493 // Only entity, not common, here for now; see #1064
494 raw_name.conditionally_qualify_with(schema_namespace, ReferenceType::Entity)
495 })
496 .collect(),
497 tags: schema_file_type
498 .tags
499 .map(|tags| tags.conditionally_qualify_type_references(schema_namespace)),
500 }
501 }
502
503 /// Convert this [`EntityTypeFragment<ConditionalName>`] into a
504 /// [`EntityTypeFragment<InternalName>`] by fully-qualifying all typenames that
505 /// appear anywhere in any definitions.
506 ///
507 /// `all_defs` needs to contain the full set of all fully-qualified typenames
508 /// and actions that are defined in the schema (in all schema fragments).
509 pub fn fully_qualify_type_references(
510 self,
511 all_defs: &AllDefs,
512 ) -> Result<EntityTypeFragment<InternalName>, TypeNotDefinedError> {
513 // Fully qualify typenames appearing in `attributes`
514 let fully_qual_attributes = self.attributes.fully_qualify_type_references(all_defs);
515 // Fully qualify typenames appearing in `parents`
516 let parents: HashSet<InternalName> = self
517 .parents
518 .into_iter()
519 .map(|parent| parent.resolve(all_defs))
520 .collect::<Result<_, TypeNotDefinedError>>()?;
521 // Fully qualify typenames appearing in `tags`
522 let fully_qual_tags = self
523 .tags
524 .map(|tags| tags.fully_qualify_type_references(all_defs))
525 .transpose();
526 // Now is the time to check whether any parents are dangling, i.e.,
527 // refer to entity types that are not declared in any fragment (since we
528 // now have the set of typenames that are declared in all fragments).
529 let undeclared_parents: Option<NonEmpty<ConditionalName>> = NonEmpty::collect(
530 parents
531 .iter()
532 .filter(|ety| !all_defs.is_defined_as_entity(ety))
533 .map(|ety| ConditionalName::unconditional(ety.clone(), ReferenceType::Entity)),
534 );
535 match (fully_qual_attributes, fully_qual_tags, undeclared_parents) {
536 (Ok(attributes), Ok(tags), None) => Ok(EntityTypeFragment {
537 attributes,
538 parents,
539 tags,
540 }),
541 (Ok(_), Ok(_), Some(undeclared_parents)) => Err(TypeNotDefinedError {
542 undefined_types: undeclared_parents,
543 }),
544 (Err(e), Ok(_), None) | (Ok(_), Err(e), None) => Err(e),
545 (Err(e1), Err(e2), None) => Err(TypeNotDefinedError::join_nonempty(nonempty![e1, e2])),
546 (Err(e), Ok(_), Some(mut undeclared)) | (Ok(_), Err(e), Some(mut undeclared)) => {
547 undeclared.extend(e.undefined_types);
548 Err(TypeNotDefinedError {
549 undefined_types: undeclared,
550 })
551 }
552 (Err(e1), Err(e2), Some(mut undeclared)) => {
553 undeclared.extend(e1.undefined_types);
554 undeclared.extend(e2.undefined_types);
555 Err(TypeNotDefinedError {
556 undefined_types: undeclared,
557 })
558 }
559 }
560 }
561}
562
563/// Holds a map from (fully qualified) [`EntityUID`]s of action definitions
564/// to their corresponding [`ActionFragment`]. The action [`EntityUID`]s (keys
565/// in the map) are fully qualified, but inside the [`ActionFragment`]s (values
566/// in the map), entity/common type references (including references to other actions)
567/// may or may not be fully qualified yet, depending on `N` and `A`. See notes
568/// on [`ActionFragment`].
569///
570/// The [`ActionFragment`]s may also reference undeclared entity/common types
571/// and actions (that will be declared in a different schema fragment).
572///
573/// The current schema format specification does not include multiple action entity
574/// types. All action entities are required to use a single `Action` entity
575/// type. However, the action entity type may be namespaced, so an action entity
576/// may have a fully qualified entity type `My::Namespace::Action`.
577#[derive(Debug, Clone)]
578pub struct ActionsDef<N, A> {
579 pub(super) actions: HashMap<EntityUID, ActionFragment<N, A>>,
580}
581
582impl<N, A> ActionsDef<N, A> {
583 /// Construct an empty [`ActionsDef`] defining no entity types.
584 pub fn new() -> Self {
585 Self {
586 actions: HashMap::new(),
587 }
588 }
589}
590
591impl ActionsDef<ConditionalName, ConditionalName> {
592 /// Construct an [`ActionsDef<ConditionalName>`] by converting the structures used by the
593 /// schema format to those used internally by the validator.
594 pub(crate) fn from_raw_actions(
595 schema_file_actions: impl IntoIterator<Item = (SmolStr, json_schema::ActionType<RawName>)>,
596 schema_namespace: Option<&InternalName>,
597 extensions: &Extensions<'_>,
598 ) -> crate::err::Result<Self> {
599 let mut actions = HashMap::new();
600 for (action_id_str, action_type) in schema_file_actions {
601 let action_uid = json_schema::ActionEntityUID::default_type(action_id_str.clone())
602 .qualify_with(schema_namespace); // the declaration name is always (unconditionally) prefixed by the current/active namespace
603 match actions.entry(action_uid.try_into()?) {
604 Entry::Vacant(ventry) => {
605 let frag = ActionFragment::from_raw_action(
606 ventry.key(),
607 action_type,
608 schema_namespace,
609 extensions,
610 )?;
611 ventry.insert(frag);
612 }
613 Entry::Occupied(_) => {
614 return Err(DuplicateActionError(action_id_str).into());
615 }
616 }
617 }
618 Ok(Self { actions })
619 }
620
621 /// Convert this [`ActionsDef<ConditionalName>`] into a
622 /// [`ActionsDef<InternalName>`] by fully-qualifying all typenames that
623 /// appear anywhere in any definitions.
624 ///
625 /// `all_defs` needs to contain the full set of all fully-qualified typenames
626 /// and actions that are defined in the schema (in all schema fragments).
627 pub fn fully_qualify_type_references(
628 self,
629 all_defs: &AllDefs,
630 ) -> Result<ActionsDef<InternalName, EntityType>, SchemaError> {
631 Ok(ActionsDef {
632 actions: self
633 .actions
634 .into_iter()
635 .map(|(k, v)| Ok((k, v.fully_qualify_type_references(all_defs)?)))
636 .collect::<Result<_, SchemaError>>()?,
637 })
638 }
639}
640
641/// Holds the information about an action that comprises an action definition.
642///
643/// In this representation, references to common types may not yet have been
644/// fully resolved/inlined, and entity/common type references (including
645/// references to other actions) may not yet be fully qualified, depending on
646/// `N` and `A`. This [`ActionFragment`] may also reference undeclared entity/common
647/// types and actions (that will be declared in a different schema fragment).
648///
649/// `A` is used for typenames in `applies_to`, and `N` is used for all other
650/// type references.
651#[derive(Debug, Clone)]
652pub struct ActionFragment<N, A> {
653 /// The type of the context record for this action. This may contain
654 /// references to common types which have not yet been resolved/inlined
655 /// (e.g., because they are not defined in this schema fragment).
656 pub(super) context: json_schema::Type<N>,
657 /// The principals and resources that an action can be applied to.
658 pub(super) applies_to: ValidatorApplySpec<A>,
659 /// The direct parent action entities for this action.
660 /// These may be actions declared in a different namespace or schema
661 /// fragment, and thus not declared yet.
662 /// We will check for undeclared parents when combining fragments into a
663 /// [`crate::ValidatorSchema`].
664 pub(super) parents: HashSet<json_schema::ActionEntityUID<N>>,
665 /// The types for the attributes defined for this actions entity.
666 /// Here, common types have been fully resolved/inlined.
667 pub(super) attribute_types: Attributes,
668 /// The values for the attributes defined for this actions entity, stored
669 /// separately so that we can later extract these values to construct the
670 /// actual `Entity` objects defined by the schema.
671 pub(super) attributes: BTreeMap<SmolStr, PartialValueSerializedAsExpr>,
672}
673
674impl ActionFragment<ConditionalName, ConditionalName> {
675 pub(crate) fn from_raw_action(
676 action_uid: &EntityUID,
677 action_type: json_schema::ActionType<RawName>,
678 schema_namespace: Option<&InternalName>,
679 extensions: &Extensions<'_>,
680 ) -> crate::err::Result<Self> {
681 let (principal_types, resource_types, context) = action_type
682 .applies_to
683 .map(|applies_to| {
684 (
685 applies_to.principal_types,
686 applies_to.resource_types,
687 applies_to.context,
688 )
689 })
690 .unwrap_or_default();
691 let (attribute_types, attributes) = Self::convert_attr_jsonval_map_to_attributes(
692 action_type.attributes.unwrap_or_default(),
693 action_uid,
694 extensions,
695 )?;
696 Ok(Self {
697 context: context
698 .into_inner()
699 .conditionally_qualify_type_references(schema_namespace),
700 applies_to: ValidatorApplySpec::<ConditionalName>::new(
701 principal_types
702 .into_iter()
703 .map(|pty| {
704 pty.conditionally_qualify_with(schema_namespace, ReferenceType::Entity)
705 })
706 .collect(),
707 resource_types
708 .into_iter()
709 .map(|rty| {
710 rty.conditionally_qualify_with(schema_namespace, ReferenceType::Entity)
711 })
712 .collect(),
713 ),
714 parents: action_type
715 .member_of
716 .unwrap_or_default()
717 .into_iter()
718 .map(|parent| parent.conditionally_qualify_type_references(schema_namespace))
719 .collect(),
720 attribute_types,
721 attributes,
722 })
723 }
724
725 /// Convert this [`ActionFragment<ConditionalName>`] into an
726 /// [`ActionFragment<InternalName>`] by fully-qualifying all typenames that
727 /// appear anywhere in any definitions.
728 ///
729 /// `all_defs` needs to contain the full set of all fully-qualified typenames
730 /// and actions that are defined in the schema (in all schema fragments).
731 pub fn fully_qualify_type_references(
732 self,
733 all_defs: &AllDefs,
734 ) -> Result<ActionFragment<InternalName, EntityType>, SchemaError> {
735 Ok(ActionFragment {
736 context: self.context.fully_qualify_type_references(all_defs)?,
737 applies_to: self.applies_to.fully_qualify_type_references(all_defs)?,
738 parents: self
739 .parents
740 .into_iter()
741 .map(|parent| {
742 parent
743 .fully_qualify_type_references(all_defs)
744 .map_err(Into::into)
745 })
746 .collect::<Result<_, SchemaError>>()?,
747 attribute_types: self.attribute_types,
748 attributes: self.attributes,
749 })
750 }
751
752 fn convert_attr_jsonval_map_to_attributes(
753 m: HashMap<SmolStr, CedarValueJson>,
754 action_id: &EntityUID,
755 extensions: &Extensions<'_>,
756 ) -> crate::err::Result<(Attributes, BTreeMap<SmolStr, PartialValueSerializedAsExpr>)> {
757 let mut attr_types: HashMap<SmolStr, Type> = HashMap::with_capacity(m.len());
758 let mut attr_values: BTreeMap<SmolStr, PartialValueSerializedAsExpr> = BTreeMap::new();
759 let evaluator = RestrictedEvaluator::new(extensions);
760
761 for (k, v) in m {
762 let t = Self::jsonval_to_type_helper(&v, action_id);
763 match t {
764 Ok(ty) => attr_types.insert(k.clone(), ty),
765 Err(e) => return Err(e),
766 };
767
768 // As an artifact of the limited `CedarValueJson` variants accepted by
769 // `Self::jsonval_to_type_helper`, we know that this function will
770 // never error. Also note that this is only ever executed when
771 // action attributes are enabled, but they cannot be enabled when
772 // using Cedar through the public API. This is fortunate because
773 // handling an error here would mean adding a new error variant to
774 // `SchemaError` in the public API, but we didn't make that enum
775 // `non_exhaustive`, so any new variants are a breaking change.
776 // PANIC SAFETY: see above
777 #[allow(clippy::expect_used)]
778 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`");
779 let pv = evaluator
780 .partial_interpret(e.as_borrowed())
781 .map_err(|err| {
782 ActionAttrEvalError(EntityAttrEvaluationError {
783 uid: action_id.clone(),
784 attr_or_tag: k.clone(),
785 was_attr: true,
786 err,
787 })
788 })?;
789 attr_values.insert(k.clone(), pv.into());
790 }
791 Ok((
792 Attributes::with_required_attributes(attr_types),
793 attr_values,
794 ))
795 }
796
797 /// Helper to get types from `CedarValueJson`s. Currently doesn't support all
798 /// `CedarValueJson` types. Note: If this function is extended to cover move
799 /// `CedarValueJson`s, we must update `convert_attr_jsonval_map_to_attributes` to
800 /// handle errors that may occur when parsing these values. This will require
801 /// a breaking change in the `SchemaError` type in the public API.
802 fn jsonval_to_type_helper(
803 v: &CedarValueJson,
804 action_id: &EntityUID,
805 ) -> crate::err::Result<Type> {
806 match v {
807 CedarValueJson::Bool(_) => Ok(Type::primitive_boolean()),
808 CedarValueJson::Long(_) => Ok(Type::primitive_long()),
809 CedarValueJson::String(_) => Ok(Type::primitive_string()),
810 CedarValueJson::Record(r) => {
811 let mut required_attrs: HashMap<SmolStr, Type> = HashMap::with_capacity(r.len());
812 for (k, v_prime) in r {
813 let t = Self::jsonval_to_type_helper(v_prime, action_id);
814 match t {
815 Ok(ty) => required_attrs.insert(k.clone(), ty),
816 Err(e) => return Err(e),
817 };
818 }
819 Ok(Type::record_with_required_attributes(
820 required_attrs,
821 OpenTag::ClosedAttributes,
822 ))
823 }
824 CedarValueJson::Set(v) => match v.first() {
825 //sets with elements of different types will be rejected elsewhere
826 None => Err(ActionAttributesContainEmptySetError {
827 uid: action_id.clone(),
828 }
829 .into()),
830 Some(element) => {
831 let element_type = Self::jsonval_to_type_helper(element, action_id);
832 match element_type {
833 Ok(t) => Ok(Type::Set {
834 element_type: Some(Box::new(t)),
835 }),
836 Err(_) => element_type,
837 }
838 }
839 },
840 CedarValueJson::EntityEscape { __entity: _ } => Err(UnsupportedActionAttributeError {
841 uid: action_id.clone(),
842 attr: "entity escape (`__entity`)".into(),
843 }
844 .into()),
845 CedarValueJson::ExprEscape { __expr: _ } => Err(UnsupportedActionAttributeError {
846 uid: action_id.clone(),
847 attr: "expression escape (`__expr`)".into(),
848 }
849 .into()),
850 CedarValueJson::ExtnEscape { __extn: _ } => Err(UnsupportedActionAttributeError {
851 uid: action_id.clone(),
852 attr: "extension function escape (`__extn`)".into(),
853 }
854 .into()),
855 CedarValueJson::Null => Err(UnsupportedActionAttributeError {
856 uid: action_id.clone(),
857 attr: "null".into(),
858 }
859 .into()),
860 }
861 }
862}
863
864type ResolveFunc<T> = dyn FnOnce(&HashMap<&InternalName, Type>) -> crate::err::Result<T>;
865/// Represent a type that might be defined in terms of some common-type
866/// definitions which are not necessarily available in the current namespace.
867pub(crate) enum WithUnresolvedCommonTypeRefs<T> {
868 WithUnresolved(Box<ResolveFunc<T>>),
869 WithoutUnresolved(T),
870}
871
872impl<T: 'static> WithUnresolvedCommonTypeRefs<T> {
873 pub fn new(
874 f: impl FnOnce(&HashMap<&InternalName, Type>) -> crate::err::Result<T> + 'static,
875 ) -> Self {
876 Self::WithUnresolved(Box::new(f))
877 }
878
879 pub fn map<U: 'static>(
880 self,
881 f: impl FnOnce(T) -> U + 'static,
882 ) -> WithUnresolvedCommonTypeRefs<U> {
883 match self {
884 Self::WithUnresolved(_) => WithUnresolvedCommonTypeRefs::new(|common_type_defs| {
885 self.resolve_common_type_refs(common_type_defs).map(f)
886 }),
887 Self::WithoutUnresolved(v) => WithUnresolvedCommonTypeRefs::WithoutUnresolved(f(v)),
888 }
889 }
890
891 /// Resolve references to common types by inlining their definitions from
892 /// the given `HashMap`.
893 ///
894 /// Be warned that `common_type_defs` should contain all definitions, from
895 /// all schema fragments.
896 /// If `self` references any type not in `common_type_defs`, this will
897 /// return a `TypeNotDefinedError`.
898 pub fn resolve_common_type_refs(
899 self,
900 common_type_defs: &HashMap<&InternalName, Type>,
901 ) -> crate::err::Result<T> {
902 match self {
903 WithUnresolvedCommonTypeRefs::WithUnresolved(f) => f(common_type_defs),
904 WithUnresolvedCommonTypeRefs::WithoutUnresolved(v) => Ok(v),
905 }
906 }
907}
908
909impl<T: 'static> From<T> for WithUnresolvedCommonTypeRefs<T> {
910 fn from(value: T) -> Self {
911 Self::WithoutUnresolved(value)
912 }
913}
914
915impl<T: std::fmt::Debug> std::fmt::Debug for WithUnresolvedCommonTypeRefs<T> {
916 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
917 match self {
918 WithUnresolvedCommonTypeRefs::WithUnresolved(_) => {
919 f.debug_tuple("WithUnresolved").finish()
920 }
921 WithUnresolvedCommonTypeRefs::WithoutUnresolved(v) => {
922 f.debug_tuple("WithoutUnresolved").field(v).finish()
923 }
924 }
925 }
926}
927
928impl TryInto<ValidatorNamespaceDef<ConditionalName, ConditionalName>>
929 for json_schema::NamespaceDefinition<RawName>
930{
931 type Error = SchemaError;
932
933 fn try_into(
934 self,
935 ) -> crate::err::Result<ValidatorNamespaceDef<ConditionalName, ConditionalName>> {
936 ValidatorNamespaceDef::from_namespace_definition(
937 None,
938 self,
939 ActionBehavior::default(),
940 Extensions::all_available(),
941 )
942 }
943}
944
945/// Convert a [`json_schema::Type`] (with fully-qualified names) into the
946/// [`Type`] type used by the validator.
947///
948/// Conversion can fail if an entity or record attribute name is invalid. It
949/// will also fail for some types that can be written in the schema, but are
950/// not yet implemented in the typechecking logic.
951pub(crate) fn try_jsonschema_type_into_validator_type(
952 schema_ty: json_schema::Type<InternalName>,
953 extensions: &Extensions<'_>,
954) -> crate::err::Result<WithUnresolvedCommonTypeRefs<Type>> {
955 match schema_ty {
956 json_schema::Type::Type {
957 ty: json_schema::TypeVariant::String,
958 ..
959 } => Ok(Type::primitive_string().into()),
960 json_schema::Type::Type {
961 ty: json_schema::TypeVariant::Long,
962 ..
963 } => Ok(Type::primitive_long().into()),
964 json_schema::Type::Type {
965 ty: json_schema::TypeVariant::Boolean,
966 ..
967 } => Ok(Type::primitive_boolean().into()),
968 json_schema::Type::Type {
969 ty: json_schema::TypeVariant::Set { element },
970 ..
971 } => Ok(try_jsonschema_type_into_validator_type(*element, extensions)?.map(Type::set)),
972 json_schema::Type::Type {
973 ty: json_schema::TypeVariant::Record(rty),
974 ..
975 } => try_record_type_into_validator_type(rty, extensions),
976 json_schema::Type::Type {
977 ty: json_schema::TypeVariant::Entity { name },
978 ..
979 } => Ok(Type::named_entity_reference(internal_name_to_entity_type(name)?).into()),
980 json_schema::Type::Type {
981 ty: json_schema::TypeVariant::Extension { name },
982 ..
983 } => {
984 let extension_type_name = Name::unqualified_name(name);
985 if extensions.ext_types().contains(&extension_type_name) {
986 Ok(Type::extension(extension_type_name).into())
987 } else {
988 let suggested_replacement = fuzzy_search(
989 &extension_type_name.to_string(),
990 &extensions
991 .ext_types()
992 .map(|n| n.to_string())
993 .collect::<Vec<_>>(),
994 );
995 Err(SchemaError::UnknownExtensionType(
996 UnknownExtensionTypeError {
997 actual: extension_type_name,
998 suggested_replacement,
999 },
1000 ))
1001 }
1002 }
1003 json_schema::Type::CommonTypeRef { type_name, .. } => {
1004 Ok(WithUnresolvedCommonTypeRefs::new(move |common_type_defs| {
1005 common_type_defs
1006 .get(&type_name)
1007 .cloned()
1008 // We should always have `Some` here, because if the common type
1009 // wasn't defined, that error should have been caught earlier,
1010 // when the `json_schema::Type<InternalName>` was created by
1011 // resolving a `ConditionalName` into a fully-qualified
1012 // `InternalName`.
1013 // Nonetheless, instead of panicking if that internal
1014 // invariant is violated, it's easy to return this dynamic
1015 // error instead.
1016 .ok_or_else(|| CommonTypeInvariantViolationError { name: type_name }.into())
1017 }))
1018 }
1019 json_schema::Type::Type {
1020 ty: json_schema::TypeVariant::EntityOrCommon { type_name },
1021 ..
1022 } => {
1023 Ok(WithUnresolvedCommonTypeRefs::new(move |common_type_defs| {
1024 // First check if it's a common type, because in the edge case where
1025 // the name is both a valid common type name and a valid entity type
1026 // name, we give preference to the common type (see RFC 24).
1027 match common_type_defs.get(&type_name) {
1028 Some(def) => Ok(def.clone()),
1029 None => {
1030 // It wasn't a common type, so we assume it must be a valid
1031 // entity type. Otherwise, we would have had an error earlier,
1032 // when the `json_schema::Type<InternalName>` was created by
1033 // resolving a `ConditionalName` into a fully-qualified
1034 // `InternalName`.
1035 Ok(Type::named_entity_reference(internal_name_to_entity_type(
1036 type_name,
1037 )?))
1038 }
1039 }
1040 }))
1041 }
1042 }
1043}
1044
1045/// Convert a [`json_schema::RecordType`] (with fully qualified names) into the
1046/// [`Type`] type used by the validator.
1047pub(crate) fn try_record_type_into_validator_type(
1048 rty: json_schema::RecordType<InternalName>,
1049 extensions: &Extensions<'_>,
1050) -> crate::err::Result<WithUnresolvedCommonTypeRefs<Type>> {
1051 if cfg!(not(feature = "partial-validate")) && rty.additional_attributes {
1052 Err(UnsupportedFeatureError(UnsupportedFeature::OpenRecordsAndEntities).into())
1053 } else {
1054 Ok(
1055 parse_record_attributes(rty.attributes.into_iter(), extensions)?.map(move |attrs| {
1056 Type::record_with_attributes(
1057 attrs,
1058 if rty.additional_attributes {
1059 OpenTag::OpenAttributes
1060 } else {
1061 OpenTag::ClosedAttributes
1062 },
1063 )
1064 }),
1065 )
1066 }
1067}
1068
1069/// Given the attributes for an entity or record type in the schema file format
1070/// structures (but with fully-qualified names), convert the types of the
1071/// attributes into the [`Type`] data structure used by the validator, and
1072/// return the result as an [`Attributes`] structure.
1073fn parse_record_attributes(
1074 attrs: impl IntoIterator<Item = (SmolStr, json_schema::TypeOfAttribute<InternalName>)>,
1075 extensions: &Extensions<'_>,
1076) -> crate::err::Result<WithUnresolvedCommonTypeRefs<Attributes>> {
1077 let attrs_with_common_type_refs = attrs
1078 .into_iter()
1079 .map(|(attr, ty)| -> crate::err::Result<_> {
1080 Ok((
1081 attr,
1082 (
1083 try_jsonschema_type_into_validator_type(ty.ty, extensions)?,
1084 ty.required,
1085 ),
1086 ))
1087 })
1088 .collect::<crate::err::Result<Vec<_>>>()?;
1089 Ok(WithUnresolvedCommonTypeRefs::new(|common_type_defs| {
1090 attrs_with_common_type_refs
1091 .into_iter()
1092 .map(|(s, (attr_ty, is_req))| {
1093 attr_ty
1094 .resolve_common_type_refs(common_type_defs)
1095 .map(|ty| (s, AttributeType::new(ty, is_req)))
1096 })
1097 .collect::<crate::err::Result<Vec<_>>>()
1098 .map(Attributes::with_attributes)
1099 }))
1100}