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}