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