From 11ae14a8f1437b06d08e59ffcd4b535b8ca75f72 Mon Sep 17 00:00:00 2001 From: shaobo-he-aws <130499339+shaobo-he-aws@users.noreply.github.com> Date: Tue, 28 May 2024 11:27:18 -0700 Subject: [PATCH] Implement #745 for `api::SchemaError` (#876) Signed-off-by: Shaobo He --- cedar-policy-validator/src/err.rs | 431 +++++++++++------- .../src/human_schema/test.rs | 3 +- cedar-policy-validator/src/lib.rs | 2 + cedar-policy-validator/src/rbac.rs | 2 +- cedar-policy-validator/src/schema.rs | 93 ++-- .../src/schema/namespace_def.rs | 87 ++-- .../src/schema_file_format.rs | 3 +- .../src/typecheck/test/namespace.rs | 3 +- cedar-policy/src/api.rs | 38 +- cedar-policy/src/api/err.rs | 212 +-------- cedar-policy/src/tests.rs | 12 +- 11 files changed, 413 insertions(+), 473 deletions(-) diff --git a/cedar-policy-validator/src/err.rs b/cedar-policy-validator/src/err.rs index 8745298cc..6f2519d2f 100644 --- a/cedar-policy-validator/src/err.rs +++ b/cedar-policy-validator/src/err.rs @@ -14,13 +14,6 @@ * limitations under the License. */ -use std::{collections::HashSet, fmt::Display}; - -use cedar_policy_core::{ - ast::{EntityAttrEvaluationError, EntityUID, Name}, - transitive_closure, -}; -use itertools::Itertools; use miette::Diagnostic; use thiserror::Error; @@ -30,7 +23,7 @@ use crate::human_schema; pub enum HumanSchemaError { #[error(transparent)] #[diagnostic(transparent)] - Core(#[from] SchemaError), + Core(#[from] schema_error::SchemaError), #[error(transparent)] IO(#[from] std::io::Error), #[error(transparent)] @@ -112,204 +105,332 @@ impl HumanSyntaxParseError { } } -#[derive(Debug, Diagnostic, Error)] -pub enum SchemaError { - /// This error is thrown when `serde_json` fails to deserialize the JSON +pub mod schema_error { + use std::{collections::HashSet, fmt::Display}; + + use cedar_policy_core::{ + ast::{EntityAttrEvaluationError, EntityUID, Name}, + transitive_closure, + }; + use itertools::Itertools; + use miette::Diagnostic; + use smol_str::SmolStr; + use thiserror::Error; + + /// JSON deserialization error + #[derive(Debug, Diagnostic, Error)] #[error(transparent)] - #[diagnostic(transparent)] - JsonDeserialization(#[from] JsonDeserializationError), - /// Errors occurring while computing or enforcing transitive closure on - /// action hierarchy. + pub struct JsonSerializationError(#[from] pub(crate) serde_json::Error); + + /// Transitive closure of action hierarchy computation or enforcement error + #[derive(Debug, Diagnostic, Error)] #[error("transitive closure computation/enforcement error on action hierarchy: {0}")] #[diagnostic(transparent)] - ActionTransitiveClosure(Box>), - /// Errors occurring while computing or enforcing transitive closure on - /// entity type hierarchy. + pub struct ActionTransitiveClosureError( + #[from] pub(crate) Box>, + ); + + /// Transitive closure of entity type hierarchy computation or enforcement error + #[derive(Debug, Diagnostic, Error)] #[error("transitive closure computation/enforcement error on entity type hierarchy: {0}")] #[diagnostic(transparent)] - EntityTypeTransitiveClosure(#[from] transitive_closure::TcError), - /// Error generated when processing a schema file that uses unsupported features - #[error("unsupported feature used in schema: {0}")] - #[diagnostic(transparent)] - UnsupportedFeature(UnsupportedFeature), - /// Undeclared entity type(s) used in the `memberOf` field of an entity - /// type, the `appliesTo` fields of an action, or an attribute type in a - /// context or entity attribute record. Entity types in the error message - /// are fully qualified, including any implicit or explicit namespaces. + pub struct EntityTypeTransitiveClosureError( + #[from] pub(crate) Box>, + ); + + /// Undeclared entity types error + #[derive(Debug, Diagnostic, Error)] #[error("undeclared entity type(s): {0:?}")] #[diagnostic(help( "any entity types appearing anywhere in a schema need to be declared in `entityTypes`" ))] - UndeclaredEntityTypes(HashSet), - /// Undeclared action(s) used in the `memberOf` field of an action. + pub struct UndeclaredEntityTypesError(pub(crate) HashSet); + + /// Undeclared actions error + #[derive(Debug, Diagnostic, Error)] #[error("undeclared action(s): {0:?}")] #[diagnostic(help("any actions appearing in `memberOf` need to be declared in `actions`"))] - UndeclaredActions(HashSet), - /// This error occurs in either of the following cases (see discussion on #477): - /// - undeclared common type(s) appearing in entity or context attributes - /// - common type(s) (declared or not) appearing in declarations of other common types + pub struct UndeclaredActionsError(pub(crate) HashSet); + + /// Undeclared common types error + #[derive(Debug, Diagnostic, Error)] #[error("undeclared common type(s), or common type(s) used in the declaration of another common type: {0:?}")] #[diagnostic(help("any common types used in entity or context attributes need to be declared in `commonTypes`, and currently, common types may not reference other common types"))] - UndeclaredCommonTypes(HashSet), - /// Duplicate specifications for an entity type. Argument is the name of - /// the duplicate entity type. + pub struct UndeclaredCommonTypesError(pub(crate) HashSet); + + /// Duplicate entity type error + #[derive(Debug, Diagnostic, Error)] #[error("duplicate entity type `{0}`")] - DuplicateEntityType(String), - /// Duplicate specifications for an action. Argument is the name of the - /// duplicate action. + pub struct DuplicateEntityTypeError(pub(crate) Name); + + /// Duplicate action error + #[derive(Debug, Diagnostic, Error)] #[error("duplicate action `{0}`")] - DuplicateAction(String), - /// Duplicate specification for a reusable type declaration. - #[error("duplicate common type `{0}`")] - DuplicateCommonType(String), - /// Cycle in the schema's action hierarchy. + pub struct DuplicateActionError(pub(crate) SmolStr); + + /// Duplicate common type error + #[derive(Debug, Diagnostic, Error)] + #[error("duplicate common type type `{0}`")] + pub struct DuplicateCommonTypeError(pub(crate) Name); + + /// Cycle in action hierarchy error + #[derive(Debug, Diagnostic, Error)] #[error("cycle in action hierarchy containing `{0}`")] - CycleInActionHierarchy(EntityUID), - /// Cycle in the schema's common type declarations. + pub struct CycleInActionHierarchyError(pub(crate) EntityUID); + + /// Cycle in common type hierarchy error + #[derive(Debug, Diagnostic, Error)] #[error("cycle in common type references containing `{0}`")] - CycleInCommonTypeReferences(Name), - /// The schema file included an entity type `Action` in the entity type - /// list. The `Action` entity type is always implicitly declared, and it - /// cannot currently have attributes or be in any groups, so there is no - /// purposes in adding an explicit entry. + pub struct CycleInCommonTypeReferencesError(pub(crate) Name); + + /// Action declared in `entityType` list error + #[derive(Debug, Clone, Diagnostic, Error)] #[error("entity type `Action` declared in `entityTypes` list")] - ActionEntityTypeDeclared, - /// `context` or `shape` fields are not records + pub struct ActionEntityTypeDeclaredError {} + + /// Context or entity type shape not declared as record error + #[derive(Debug, Diagnostic, Error)] #[error("{0} is declared with a type other than `Record`")] #[diagnostic(help("{}", match .0 { - ContextOrShape::ActionContext(_) => "action contexts must have type `Record`", - ContextOrShape::EntityTypeShape(_) => "entity type shapes must have type `Record`", - }))] - ContextOrShapeNotRecord(ContextOrShape), - /// An action entity (transitively) has an attribute that is an empty set. - /// The validator cannot assign a type to an empty set. - /// This error variant should only be used when `PermitAttributes` is enabled. + ContextOrShape::ActionContext(_) => "action contexts must have type `Record`", + ContextOrShape::EntityTypeShape(_) => "entity type shapes must have type `Record`", +}))] + pub struct ContextOrShapeNotRecordError(pub(crate) ContextOrShape); + + /// Action attributes contain empty set error + #[derive(Debug, Diagnostic, Error)] #[error("action `{0}` has an attribute that is an empty set")] #[diagnostic(help( "actions are not currently allowed to have attributes whose value is an empty set" ))] - ActionAttributesContainEmptySet(EntityUID), - /// An action entity (transitively) has an attribute of unsupported type (`ExprEscape`, `EntityEscape` or `ExtnEscape`). - /// This error variant should only be used when `PermitAttributes` is enabled. + pub struct ActionAttributesContainEmptySetError(pub(crate) EntityUID); + + /// Unsupported action attribute error + #[derive(Debug, Diagnostic, Error)] #[error("action `{0}` has an attribute with unsupported JSON representation: {1}")] - UnsupportedActionAttribute(EntityUID, String), - /// Error when evaluating an action attribute - #[error(transparent)] - #[diagnostic(transparent)] - ActionAttrEval(EntityAttrEvaluationError), - /// Error thrown when the schema contains the `__expr` escape. - /// Support for this escape form has been dropped. + pub struct UnsupportedActionAttributeError(pub(crate) EntityUID, pub(crate) SmolStr); + + /// Unsupported `__expr` escape error + #[derive(Debug, Clone, Diagnostic, Error)] #[error("the `__expr` escape is no longer supported")] #[diagnostic(help("to create an entity reference, use `__entity`; to create an extension value, use `__extn`; and for all other values, use JSON directly"))] - ExprEscapeUsed, - /// The schema used an extension type that the validator doesn't know about. + pub struct ExprEscapeUsedError {} + + /// Action attribute evaluation error + #[derive(Debug, Diagnostic, Error)] #[error(transparent)] #[diagnostic(transparent)] - UnknownExtensionType(UnknownExtensionType), -} + pub struct ActionAttrEvalError(#[from] pub(crate) EntityAttrEvaluationError); -impl From> for SchemaError { - fn from(e: transitive_closure::TcError) -> Self { - // we use code in transitive_closure to check for cycles in the action - // hierarchy, but in case of an error we want to report the more descriptive - // CycleInActionHierarchy instead of ActionTransitiveClosureError - match e { - transitive_closure::TcError::MissingTcEdge { .. } => { - SchemaError::ActionTransitiveClosure(Box::new(e)) - } - transitive_closure::TcError::HasCycle(err) => { - SchemaError::CycleInActionHierarchy(err.vertex_with_loop().clone()) + /// Unsupported feature error + #[derive(Debug, Diagnostic, Error)] + #[error("unsupported feature used in schema: {0}")] + #[diagnostic(transparent)] + pub struct UnsupportedFeatureError(#[from] pub(crate) UnsupportedFeature); + + #[derive(Debug, Diagnostic, Error)] + pub enum SchemaError { + /// Error thrown by the `serde_json` crate during serialization + #[error(transparent)] + #[diagnostic(transparent)] + JsonSerialization(#[from] JsonSerializationError), + /// This error is thrown when `serde_json` fails to deserialize the JSON + #[error(transparent)] + #[diagnostic(transparent)] + JsonDeserialization(#[from] JsonDeserializationError), + /// Errors occurring while computing or enforcing transitive closure on + /// action hierarchy. + #[error(transparent)] + #[diagnostic(transparent)] + ActionTransitiveClosure(#[from] ActionTransitiveClosureError), + /// Errors occurring while computing or enforcing transitive closure on + /// entity type hierarchy. + #[error(transparent)] + #[diagnostic(transparent)] + EntityTypeTransitiveClosure(#[from] EntityTypeTransitiveClosureError), + /// Error generated when processing a schema file that uses unsupported features + #[error(transparent)] + #[diagnostic(transparent)] + UnsupportedFeature(#[from] UnsupportedFeatureError), + /// Undeclared entity type(s) used in the `memberOf` field of an entity + /// type, the `appliesTo` fields of an action, or an attribute type in a + /// context or entity attribute record. Entity types in the error message + /// are fully qualified, including any implicit or explicit namespaces. + #[error(transparent)] + #[diagnostic(transparent)] + UndeclaredEntityTypes(#[from] UndeclaredEntityTypesError), + /// Undeclared action(s) used in the `memberOf` field of an action. + #[error(transparent)] + #[diagnostic(transparent)] + UndeclaredActions(#[from] UndeclaredActionsError), + /// This error occurs in either of the following cases (see discussion on #477): + /// - undeclared common type(s) appearing in entity or context attributes + /// - common type(s) (declared or not) appearing in declarations of other common types + #[error(transparent)] + #[diagnostic(transparent)] + UndeclaredCommonTypes(#[from] UndeclaredCommonTypesError), + /// Duplicate specifications for an entity type. Argument is the name of + /// the duplicate entity type. + #[error(transparent)] + #[diagnostic(transparent)] + DuplicateEntityType(#[from] DuplicateEntityTypeError), + /// Duplicate specifications for an action. Argument is the name of the + /// duplicate action. + #[error(transparent)] + #[diagnostic(transparent)] + DuplicateAction(#[from] DuplicateActionError), + /// Duplicate specification for a reusable type declaration. + #[error(transparent)] + #[diagnostic(transparent)] + DuplicateCommonType(#[from] DuplicateCommonTypeError), + /// Cycle in the schema's action hierarchy. + #[error(transparent)] + #[diagnostic(transparent)] + CycleInActionHierarchy(#[from] CycleInActionHierarchyError), + /// Cycle in the schema's common type declarations. + #[error(transparent)] + #[diagnostic(transparent)] + CycleInCommonTypeReferences(#[from] CycleInCommonTypeReferencesError), + /// The schema file included an entity type `Action` in the entity type + /// list. The `Action` entity type is always implicitly declared, and it + /// cannot currently have attributes or be in any groups, so there is no + /// purposes in adding an explicit entry. + #[error(transparent)] + #[diagnostic(transparent)] + ActionEntityTypeDeclared(#[from] ActionEntityTypeDeclaredError), + /// `context` or `shape` fields are not records + #[error(transparent)] + #[diagnostic(transparent)] + ContextOrShapeNotRecord(#[from] ContextOrShapeNotRecordError), + /// An action entity (transitively) has an attribute that is an empty set. + /// The validator cannot assign a type to an empty set. + /// This error variant should only be used when `PermitAttributes` is enabled. + #[error(transparent)] + #[diagnostic(transparent)] + ActionAttributesContainEmptySet(#[from] ActionAttributesContainEmptySetError), + /// An action entity (transitively) has an attribute of unsupported type (`ExprEscape`, `EntityEscape` or `ExtnEscape`). + /// This error variant should only be used when `PermitAttributes` is enabled. + #[error(transparent)] + #[diagnostic(transparent)] + UnsupportedActionAttribute(#[from] UnsupportedActionAttributeError), + /// Error when evaluating an action attribute + #[error(transparent)] + #[diagnostic(transparent)] + ActionAttrEval(#[from] ActionAttrEvalError), + /// Error thrown when the schema contains the `__expr` escape. + /// Support for this escape form has been dropped. + #[error(transparent)] + #[diagnostic(transparent)] + ExprEscapeUsed(#[from] ExprEscapeUsedError), + /// The schema used an extension type that the validator doesn't know about. + #[error(transparent)] + #[diagnostic(transparent)] + UnknownExtensionType(UnknownExtensionTypeError), + } + + impl From> for SchemaError { + fn from(e: transitive_closure::TcError) -> Self { + // we use code in transitive_closure to check for cycles in the action + // hierarchy, but in case of an error we want to report the more descriptive + // CycleInActionHierarchy instead of ActionTransitiveClosureError + match e { + transitive_closure::TcError::MissingTcEdge { .. } => { + SchemaError::ActionTransitiveClosure(Box::new(e).into()) + } + transitive_closure::TcError::HasCycle(err) => { + CycleInActionHierarchyError(err.vertex_with_loop().clone()).into() + } } } } -} -pub type Result = std::result::Result; + pub type Result = std::result::Result; -#[derive(Debug)] -pub enum ContextOrShape { - ActionContext(EntityUID), - EntityTypeShape(Name), -} + #[derive(Debug)] + pub(crate) enum ContextOrShape { + ActionContext(EntityUID), + EntityTypeShape(Name), + } -impl std::fmt::Display for ContextOrShape { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - ContextOrShape::ActionContext(action) => write!(f, "Context for action {}", action), - ContextOrShape::EntityTypeShape(entity_type) => { - write!(f, "Shape for entity type {}", entity_type) + impl std::fmt::Display for ContextOrShape { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ContextOrShape::ActionContext(action) => write!(f, "Context for action {}", action), + ContextOrShape::EntityTypeShape(entity_type) => { + write!(f, "Shape for entity type {}", entity_type) + } } } } -} -#[derive(Debug, Diagnostic, Error)] -pub enum UnsupportedFeature { - #[error("records and entities with `additionalAttributes` are experimental, but the experimental `partial-validate` feature is not enabled")] - OpenRecordsAndEntities, - // Action attributes are allowed if `ActionBehavior` is `PermitAttributes` - #[error("action declared with attributes: [{}]", .0.iter().join(", "))] - ActionAttributes(Vec), -} + #[derive(Debug, Diagnostic, Error)] + pub(crate) enum UnsupportedFeature { + #[error("records and entities with `additionalAttributes` are experimental, but the experimental `partial-validate` feature is not enabled")] + OpenRecordsAndEntities, + // Action attributes are allowed if `ActionBehavior` is `PermitAttributes` + #[error("action declared with attributes: [{}]", .0.iter().join(", "))] + ActionAttributes(Vec), + } -/// This error is thrown when `serde_json` fails to deserialize the JSON -#[derive(Debug, Error)] -#[error("failed to parse schema in JSON format: {err}")] -pub struct JsonDeserializationError { - /// Error thrown by the `serde_json` crate - err: serde_json::Error, - /// Did the schema look like it was intended to be human format instead of - /// JSON? - suspect_human_format: bool, -} + /// This error is thrown when `serde_json` fails to deserialize the JSON + #[derive(Debug, Error)] + #[error("failed to parse schema in JSON format: {err}")] + pub struct JsonDeserializationError { + /// Error thrown by the `serde_json` crate + err: serde_json::Error, + /// Did the schema look like it was intended to be human format instead of + /// JSON? + suspect_human_format: bool, + } -impl Diagnostic for JsonDeserializationError { - fn help<'a>(&'a self) -> Option> { - if self.suspect_human_format { - Some(Box::new("this API was expecting a schema in the JSON format; did you mean to use a different function, which expects the Cedar schema format?")) - } else { - None + impl Diagnostic for JsonDeserializationError { + fn help<'a>(&'a self) -> Option> { + if self.suspect_human_format { + Some(Box::new("this API was expecting a schema in the JSON format; did you mean to use a different function, which expects the Cedar schema format?")) + } else { + None + } } } -} -impl JsonDeserializationError { - /// `err`: the `serde_json::Error` that was thrown - /// - /// `src`: the JSON that we were trying to deserialize (if available in string form) - pub(crate) fn new(err: serde_json::Error, src: Option<&str>) -> Self { - match src { - None => Self { - err, - suspect_human_format: false, - }, - Some(src) => { - // let's see what the first non-whitespace character is - let suspect_human_format = match src.trim_start().chars().next() { - None => false, // schema is empty or only whitespace; the problem is unlikely to be JSON vs human format - Some('{') => false, // yes, this looks like it was intended to be a JSON schema - Some(_) => true, // any character other than '{', we suspect it might be a human-format schema - }; - Self { + impl JsonDeserializationError { + /// `err`: the `serde_json::Error` that was thrown + /// + /// `src`: the JSON that we were trying to deserialize (if available in string form) + pub(crate) fn new(err: serde_json::Error, src: Option<&str>) -> Self { + match src { + None => Self { err, - suspect_human_format, + suspect_human_format: false, + }, + Some(src) => { + // let's see what the first non-whitespace character is + let suspect_human_format = match src.trim_start().chars().next() { + None => false, // schema is empty or only whitespace; the problem is unlikely to be JSON vs human format + Some('{') => false, // yes, this looks like it was intended to be a JSON schema + Some(_) => true, // any character other than '{', we suspect it might be a human-format schema + }; + Self { + err, + suspect_human_format, + } } } } } -} -#[derive(Error, Debug)] -#[error("unknown extension type `{actual}`")] -pub struct UnknownExtensionType { - pub(crate) actual: Name, - pub(crate) suggested_replacement: Option, -} + #[derive(Error, Debug)] + #[error("unknown extension type `{actual}`")] + pub struct UnknownExtensionTypeError { + pub(crate) actual: Name, + pub(crate) suggested_replacement: Option, + } -impl Diagnostic for UnknownExtensionType { - fn help<'a>(&'a self) -> Option> { - self.suggested_replacement - .as_ref() - .map(|suggestion| Box::new(format!("did you mean `{suggestion}`?")) as Box) + impl Diagnostic for UnknownExtensionTypeError { + fn help<'a>(&'a self) -> Option> { + self.suggested_replacement.as_ref().map(|suggestion| { + Box::new(format!("did you mean `{suggestion}`?")) as Box + }) + } } } diff --git a/cedar-policy-validator/src/human_schema/test.rs b/cedar-policy-validator/src/human_schema/test.rs index ccda69bdc..e7dda54b6 100644 --- a/cedar-policy-validator/src/human_schema/test.rs +++ b/cedar-policy-validator/src/human_schema/test.rs @@ -1742,8 +1742,9 @@ mod common_type_references { use cool_asserts::assert_matches; use crate::{ + schema_error::SchemaError, types::{AttributeType, EntityRecordKind, Type}, - SchemaError, SchemaFragment, ValidatorSchema, + SchemaFragment, ValidatorSchema, }; #[test] diff --git a/cedar-policy-validator/src/lib.rs b/cedar-policy-validator/src/lib.rs index b7994af92..5264c8d3b 100644 --- a/cedar-policy-validator/src/lib.rs +++ b/cedar-policy-validator/src/lib.rs @@ -202,6 +202,8 @@ mod test { }; use itertools::Itertools; + use crate::schema_error::Result; + #[test] fn top_level_validate() -> Result<()> { let mut set = PolicySet::new(); diff --git a/cedar-policy-validator/src/rbac.rs b/cedar-policy-validator/src/rbac.rs index b43a702e8..9b30ff3e2 100644 --- a/cedar-policy-validator/src/rbac.rs +++ b/cedar-policy-validator/src/rbac.rs @@ -482,7 +482,7 @@ mod test { use super::*; use crate::{ - err::*, + err::schema_error::*, schema_file_format::{NamespaceDefinition, *}, validation_errors::{UnrecognizedEntityType, UnspecifiedEntity}, ValidationMode, ValidationWarning, Validator, diff --git a/cedar-policy-validator/src/schema.rs b/cedar-policy-validator/src/schema.rs index b9db601f6..c5aebf374 100644 --- a/cedar-policy-validator/src/schema.rs +++ b/cedar-policy-validator/src/schema.rs @@ -30,9 +30,11 @@ use cedar_policy_core::{ }; use serde::{Deserialize, Serialize}; use serde_with::serde_as; +use smol_str::ToSmolStr; use super::NamespaceDefinition; use crate::{ + err::schema_error::*, err::*, human_schema::SchemaWarning, types::{Attributes, EntityRecordKind, OpenTag, Type}, @@ -251,7 +253,7 @@ impl ValidatorSchema { match type_defs.entry(name) { Entry::Vacant(v) => v.insert(ty), Entry::Occupied(o) => { - return Err(SchemaError::DuplicateCommonType(o.key().to_string())); + return Err(DuplicateCommonTypeError(o.key().clone()).into()); } }; } @@ -260,7 +262,7 @@ impl ValidatorSchema { match entity_type_fragments.entry(name) { Entry::Vacant(v) => v.insert(entity_type), Entry::Occupied(o) => { - return Err(SchemaError::DuplicateEntityType(o.key().to_string())) + return Err(DuplicateEntityTypeError(o.key().clone()).into()) } }; } @@ -269,7 +271,7 @@ impl ValidatorSchema { match action_fragments.entry(action_euid) { Entry::Vacant(v) => v.insert(action), Entry::Occupied(o) => { - return Err(SchemaError::DuplicateAction(o.key().to_string())) + return Err(DuplicateActionError(o.key().to_smolstr()).into()) } }; } @@ -306,9 +308,9 @@ impl ValidatorSchema { let (attributes, open_attributes) = Self::record_attributes_or_none( entity_type.attributes.resolve_type_defs(&type_defs)?, ) - .ok_or(SchemaError::ContextOrShapeNotRecord( + .ok_or(SchemaError::from(ContextOrShapeNotRecordError( ContextOrShape::EntityTypeShape(name.clone()), - ))?; + )))?; Ok(( name.clone(), ValidatorEntityType { @@ -336,9 +338,9 @@ impl ValidatorSchema { let descendants = action_children.remove(&name).unwrap_or_default(); let (context, open_context_attributes) = Self::record_attributes_or_none(action.context.resolve_type_defs(&type_defs)?) - .ok_or(SchemaError::ContextOrShapeNotRecord( + .ok_or(SchemaError::from(ContextOrShapeNotRecordError( ContextOrShape::ActionContext(name.clone()), - ))?; + )))?; Ok(( name.clone(), ValidatorActionId { @@ -358,7 +360,8 @@ impl ValidatorSchema { // We constructed entity types and actions with child maps, but we need // transitively closed descendants. - compute_tc(&mut entity_types, false)?; + compute_tc(&mut entity_types, false) + .map_err(|e| EntityTypeTransitiveClosureError::from(Box::new(e)))?; // Pass `true` here so that we also check that the action hierarchy does // not contain cycles. compute_tc(&mut action_ids, true)?; @@ -398,7 +401,6 @@ impl ValidatorSchema { // any undeclared entity types which appeared in a `memberOf` list. let mut undeclared_e = undeclared_parent_entities .into_iter() - .map(|n| n.to_string()) .collect::>(); // Looking at entity types, we need to check entity references in // attribute types. We already know that all elements of the @@ -418,7 +420,7 @@ impl ValidatorSchema { // Undeclared actions in a `memberOf` list. let undeclared_a = undeclared_parent_actions .into_iter() - .map(|n| n.to_string()) + .map(|n| n.to_smolstr()) .collect::>(); // For actions, we check entity references in the context attribute // types and `appliesTo` lists. See the `entity_types` loop for why the @@ -430,7 +432,7 @@ impl ValidatorSchema { match p_entity { EntityType::Specified(p_entity) => { if !entity_types.contains_key(&p_entity) { - undeclared_e.insert(p_entity.to_string()); + undeclared_e.insert(p_entity.clone()); } } EntityType::Unspecified => (), @@ -441,7 +443,7 @@ impl ValidatorSchema { match r_entity { EntityType::Specified(r_entity) => { if !entity_types.contains_key(&r_entity) { - undeclared_e.insert(r_entity.to_string()); + undeclared_e.insert(r_entity.clone()); } } EntityType::Unspecified => (), @@ -449,10 +451,10 @@ impl ValidatorSchema { } } if !undeclared_e.is_empty() { - return Err(SchemaError::UndeclaredEntityTypes(undeclared_e)); + return Err(UndeclaredEntityTypesError(undeclared_e).into()); } if !undeclared_a.is_empty() { - return Err(SchemaError::UndeclaredActions(undeclared_a)); + return Err(UndeclaredActionsError(undeclared_a).into()); } Ok(()) @@ -474,13 +476,13 @@ impl ValidatorSchema { fn check_undeclared_in_type( ty: &Type, entity_types: &HashMap, - undeclared_types: &mut HashSet, + undeclared_types: &mut HashSet, ) { match ty { Type::EntityOrRecord(EntityRecordKind::Entity(lub)) => { for name in lub.iter() { if !entity_types.contains_key(name) { - undeclared_types.insert(name.to_string()); + undeclared_types.insert(name.clone()); } } } @@ -793,9 +795,9 @@ impl<'a> CommonTypeResolver<'a> { match ty { SchemaType::TypeDef { type_name } => resolve_table .get(&type_name) - .ok_or(SchemaError::UndeclaredCommonTypes(HashSet::from_iter( - std::iter::once(type_name.to_string()), - ))) + .ok_or(SchemaError::UndeclaredCommonTypes( + UndeclaredCommonTypesError(HashSet::from_iter(std::iter::once(type_name))), + )) .cloned(), SchemaType::Type(SchemaTypeVariant::Set { element }) => { Ok(SchemaType::Type(SchemaTypeVariant::Set { @@ -828,9 +830,9 @@ impl<'a> CommonTypeResolver<'a> { // Resolve common type references fn resolve(&self, extensions: Extensions) -> Result> { - let sorted_names = self - .topo_sort() - .map_err(SchemaError::CycleInCommonTypeReferences)?; + let sorted_names = self.topo_sort().map_err(|n| { + SchemaError::CycleInCommonTypeReferences(CycleInCommonTypeReferencesError(n)) + })?; let mut resolve_table = HashMap::new(); let mut tys = HashMap::new(); @@ -1016,7 +1018,7 @@ mod test { let schema: Result = schema_file.try_into(); match schema { Ok(_) => panic!("from_schema_file should have failed"), - Err(SchemaError::UndeclaredEntityTypes(v)) => { + Err(SchemaError::UndeclaredEntityTypes(UndeclaredEntityTypesError(v))) => { assert_eq!(v.len(), 3) } _ => panic!("Unexpected error from from_schema_file"), @@ -1039,8 +1041,8 @@ mod test { let schema: Result = schema_file.try_into(); match schema { Ok(_) => panic!("try_into should have failed"), - Err(SchemaError::UndeclaredEntityTypes(v)) => { - assert_eq!(v, HashSet::from(["Bar::Group".to_string()])) + Err(SchemaError::UndeclaredEntityTypes(UndeclaredEntityTypesError(v))) => { + assert_eq!(v, HashSet::from(["Bar::Group".parse().unwrap()])) } _ => panic!("Unexpected error from try_into"), } @@ -1064,10 +1066,10 @@ mod test { let schema: Result = schema_file.try_into(); match schema { Ok(_) => panic!("try_into should have failed"), - Err(SchemaError::UndeclaredEntityTypes(v)) => { + Err(SchemaError::UndeclaredEntityTypes(UndeclaredEntityTypesError(v))) => { assert_eq!( v, - HashSet::from(["Bar::Photo".to_string(), "Bar::User".to_string()]) + HashSet::from(["Bar::Photo".parse().unwrap(), "Bar::User".parse().unwrap()]) ) } _ => panic!("Unexpected error from try_into"), @@ -1104,7 +1106,9 @@ mod test { let schema: Result = schema_file.try_into(); match schema { Ok(_) => panic!("from_schema_file should have failed"), - Err(SchemaError::UndeclaredActions(v)) => assert_eq!(v.len(), 1), + Err(SchemaError::UndeclaredActions(UndeclaredActionsError(v))) => { + assert_eq!(v.len(), 1) + } _ => panic!("Unexpected error from from_schema_file"), } } @@ -1126,7 +1130,7 @@ mod test { let schema: Result = schema_file.try_into(); assert_matches!( schema, - Err(SchemaError::CycleInActionHierarchy(euid)) => { + Err(SchemaError::CycleInActionHierarchy(CycleInActionHierarchyError(euid))) => { assert_eq!(euid, r#"Action::"view_photo""#.parse().unwrap()); } ) @@ -1266,8 +1270,8 @@ mod test { let schema: Result = schema_json.try_into(); match schema { - Err(SchemaError::UndeclaredEntityTypes(tys)) => { - assert_eq!(tys, HashSet::from(["C::D::Foo".to_string()])) + Err(SchemaError::UndeclaredEntityTypes(UndeclaredEntityTypesError(v))) => { + assert_eq!(v, HashSet::from(["C::D::Foo".parse().unwrap()])) } _ => panic!("Schema construction should have failed due to undeclared entity type."), } @@ -1325,7 +1329,10 @@ mod test { .expect("Expected valid schema"); let schema: Result = schema_json.try_into(); - assert!(matches!(schema, Err(SchemaError::ActionEntityTypeDeclared))); + assert!(matches!( + schema, + Err(SchemaError::ActionEntityTypeDeclared(_)) + )); } #[test] @@ -1372,7 +1379,9 @@ mod test { Extensions::all_available(), ); match schema { - Err(SchemaError::UnsupportedFeature(UnsupportedFeature::ActionAttributes(actions))) => { + Err(SchemaError::UnsupportedFeature(UnsupportedFeatureError( + UnsupportedFeature::ActionAttributes(actions), + ))) => { assert_eq!( actions.into_iter().collect::>(), HashSet::from([ @@ -1833,7 +1842,11 @@ mod test { ); match schema { - Err(SchemaError::DuplicateCommonType(s)) if s.contains("A::MyLong") => (), + Err(SchemaError::DuplicateCommonType(DuplicateCommonTypeError(s))) + if s == "A::MyLong".parse().unwrap() => + { + () + } _ => panic!("should have errored because schema fragments have duplicate types"), }; } @@ -2189,8 +2202,8 @@ mod test { } ); let schema = ValidatorSchema::from_json_value(src, Extensions::all_available()); - assert_matches!(schema, Err(SchemaError::UndeclaredCommonTypes(types)) => - assert_eq!(types, HashSet::from(["Demo::id".to_string()]))); + assert_matches!(schema, Err(SchemaError::UndeclaredCommonTypes(UndeclaredCommonTypesError(types))) => + assert_eq!(types, HashSet::from(["Demo::id".parse().unwrap()]))); } #[test] @@ -2223,8 +2236,8 @@ mod test { } ); let schema = ValidatorSchema::from_json_value(src, Extensions::all_available()); - assert_matches!(schema, Err(SchemaError::UndeclaredCommonTypes(types)) => - assert_eq!(types, HashSet::from(["Demo::id".to_string()]))); + assert_matches!(schema, Err(SchemaError::UndeclaredCommonTypes(UndeclaredCommonTypesError(types))) => + assert_eq!(types, HashSet::from(["Demo::id".parse().unwrap()]))); } #[test] @@ -2326,7 +2339,9 @@ mod test_resolver { use cool_asserts::assert_matches; use super::CommonTypeResolver; - use crate::{types::Type, SchemaError, SchemaFragment, ValidatorSchemaFragment}; + use crate::{ + err::schema_error::SchemaError, types::Type, SchemaFragment, ValidatorSchemaFragment, + }; fn resolve(schema: SchemaFragment) -> Result, SchemaError> { let schema: ValidatorSchemaFragment = schema.try_into().unwrap(); diff --git a/cedar-policy-validator/src/schema/namespace_def.rs b/cedar-policy-validator/src/schema/namespace_def.rs index d80f0425d..52bad0fbe 100644 --- a/cedar-policy-validator/src/schema/namespace_def.rs +++ b/cedar-policy-validator/src/schema/namespace_def.rs @@ -34,7 +34,7 @@ use smol_str::{SmolStr, ToSmolStr}; use super::ValidatorApplySpec; use crate::{ - err::*, + err::schema_error::*, schema_file_format, types::{AttributeType, Attributes, Type}, ActionBehavior, ActionEntityUID, ActionType, NamespaceDefinition, SchemaType, @@ -244,7 +244,9 @@ impl ValidatorNamespaceDef { let mut type_defs = HashMap::with_capacity(schema_file_type_def.len()); for (id, schema_ty) in schema_file_type_def { if Self::is_builtin_type_name(id.as_ref()) { - return Err(SchemaError::DuplicateCommonType(id.to_string())); + return Err(SchemaError::DuplicateCommonType(DuplicateCommonTypeError( + Name::unqualified_name(id), + ))); } let name = Name::from(id.clone()).prefix_namespace_if_unqualified(schema_namespace); match type_defs.entry(name) { @@ -254,7 +256,9 @@ impl ValidatorNamespaceDef { ); } Entry::Occupied(_) => { - return Err(SchemaError::DuplicateCommonType(id.to_string())); + return Err(SchemaError::DuplicateCommonType(DuplicateCommonTypeError( + Name::unqualified_name(id), + ))); } } } @@ -288,7 +292,7 @@ impl ValidatorNamespaceDef { }); } Entry::Occupied(_) => { - return Err(SchemaError::DuplicateEntityType(id.to_string())); + return Err(DuplicateEntityTypeError(Name::unqualified_name(id)).into()); } } } @@ -321,9 +325,7 @@ impl ValidatorNamespaceDef { } CedarValueJson::Set(v) => match v.first() { //sets with elements of different types will be rejected elsewhere - None => Err(SchemaError::ActionAttributesContainEmptySet( - action_id.clone(), - )), + None => Err(ActionAttributesContainEmptySetError(action_id.clone()).into()), Some(element) => { let element_type = Self::jsonval_to_type_helper(element, action_id); match element_type { @@ -334,28 +336,24 @@ impl ValidatorNamespaceDef { } } }, - CedarValueJson::EntityEscape { __entity: _ } => { - Err(SchemaError::UnsupportedActionAttribute( - action_id.clone(), - "entity escape (`__entity`)".to_owned(), - )) - } - CedarValueJson::ExprEscape { __expr: _ } => { - Err(SchemaError::UnsupportedActionAttribute( - action_id.clone(), - "expression escape (`__expr`)".to_owned(), - )) - } - CedarValueJson::ExtnEscape { __extn: _ } => { - Err(SchemaError::UnsupportedActionAttribute( - action_id.clone(), - "extension function escape (`__extn`)".to_owned(), - )) - } - CedarValueJson::Null => Err(SchemaError::UnsupportedActionAttribute( + CedarValueJson::EntityEscape { __entity: _ } => Err(UnsupportedActionAttributeError( action_id.clone(), - "null".to_owned(), - )), + "entity escape (`__entity`)".into(), + ) + .into()), + CedarValueJson::ExprEscape { __expr: _ } => Err(UnsupportedActionAttributeError( + action_id.clone(), + "expression escape (`__expr`)".into(), + ) + .into()), + CedarValueJson::ExtnEscape { __extn: _ } => Err(UnsupportedActionAttributeError( + action_id.clone(), + "extension function escape (`__extn`)".into(), + ) + .into()), + CedarValueJson::Null => { + Err(UnsupportedActionAttributeError(action_id.clone(), "null".into()).into()) + } } } @@ -389,7 +387,7 @@ impl ValidatorNamespaceDef { let pv = evaluator .partial_interpret(e.as_borrowed()) .map_err(|err| { - SchemaError::ActionAttrEval(EntityAttrEvaluationError { + ActionAttrEvalError(EntityAttrEvaluationError { uid: action_id.clone(), attr: k.clone(), err, @@ -469,7 +467,7 @@ impl ValidatorNamespaceDef { }); } Entry::Occupied(_) => { - return Err(SchemaError::DuplicateAction(action_id_str.to_string())); + return Err(DuplicateActionError(action_id_str).into()); } } } @@ -493,7 +491,7 @@ impl ValidatorNamespaceDef { // namespace), so we do this comparison directly. .any(|(name, _)| name.to_smolstr() == ACTION_ENTITY_TYPE) { - return Err(SchemaError::ActionEntityTypeDeclared); + return Err(ActionEntityTypeDeclaredError {}.into()); } if action_behavior == ActionBehavior::ProhibitAttributes { let mut actions_with_attributes: Vec = Vec::new(); @@ -503,9 +501,12 @@ impl ValidatorNamespaceDef { } } if !actions_with_attributes.is_empty() { - return Err(SchemaError::UnsupportedFeature( - UnsupportedFeature::ActionAttributes(actions_with_attributes), - )); + return Err( + UnsupportedFeatureError(UnsupportedFeature::ActionAttributes( + actions_with_attributes, + )) + .into(), + ); } } @@ -620,9 +621,7 @@ impl ValidatorNamespaceDef { additional_attributes, }) => { if cfg!(not(feature = "partial-validate")) && additional_attributes { - Err(SchemaError::UnsupportedFeature( - UnsupportedFeature::OpenRecordsAndEntities, - )) + Err(UnsupportedFeatureError(UnsupportedFeature::OpenRecordsAndEntities).into()) } else { Ok( Self::parse_record_attributes(default_namespace, attributes, extensions)? @@ -657,10 +656,12 @@ impl ValidatorNamespaceDef { .map(|n| n.to_string()) .collect::>(), ); - Err(SchemaError::UnknownExtensionType(UnknownExtensionType { - actual: extension_type_name, - suggested_replacement, - })) + Err(SchemaError::UnknownExtensionType( + UnknownExtensionTypeError { + actual: extension_type_name, + suggested_replacement, + }, + )) } } SchemaType::TypeDef { type_name } => { @@ -668,9 +669,7 @@ impl ValidatorNamespaceDef { type_name.prefix_namespace_if_unqualified(default_namespace); Ok(WithUnresolvedTypeDefs::new(move |typ_defs| { typ_defs.get(&defined_type_name).cloned().ok_or( - SchemaError::UndeclaredCommonTypes(HashSet::from([ - defined_type_name.to_string() - ])), + UndeclaredCommonTypesError(HashSet::from([defined_type_name])).into(), ) })) } diff --git a/cedar-policy-validator/src/schema_file_format.rs b/cedar-policy-validator/src/schema_file_format.rs index 0e1f5cd3a..d4fecff11 100644 --- a/cedar-policy-validator/src/schema_file_format.rs +++ b/cedar-policy-validator/src/schema_file_format.rs @@ -29,10 +29,11 @@ use smol_str::{SmolStr, ToSmolStr}; use std::collections::{BTreeMap, HashMap, HashSet}; use crate::{ + err::schema_error::*, human_schema::{ self, parser::parse_natural_schema_fragment, SchemaWarning, ToHumanSchemaStrError, }, - HumanSchemaError, HumanSyntaxParseError, JsonDeserializationError, Result, + HumanSchemaError, HumanSyntaxParseError, }; #[cfg(feature = "wasm")] diff --git a/cedar-policy-validator/src/typecheck/test/namespace.rs b/cedar-policy-validator/src/typecheck/test/namespace.rs index 49d65bca4..d36c593bd 100644 --- a/cedar-policy-validator/src/typecheck/test/namespace.rs +++ b/cedar-policy-validator/src/typecheck/test/namespace.rs @@ -34,9 +34,10 @@ use super::test_utils::{ }; use crate::{ diagnostics::ValidationError, + schema_error::SchemaError, types::{EntityLUB, Type}, validation_errors::AttributeAccess, - SchemaError, SchemaFragment, ValidationWarning, ValidatorSchema, + SchemaFragment, ValidationWarning, ValidatorSchema, }; fn namespaced_entity_type_schema() -> SchemaFragment { diff --git a/cedar-policy/src/api.rs b/cedar-policy/src/api.rs index 4ed4a31fd..809aa8c2d 100644 --- a/cedar-policy/src/api.rs +++ b/cedar-policy/src/api.rs @@ -107,8 +107,6 @@ pub mod entities { } } -use entities::json::err::JsonDeserializationError; - /// Entity datatype // INVARIANT(UidOfEntityNotUnspecified): The `EntityUid` of an `Entity` cannot be unspecified #[repr(transparent)] @@ -1164,7 +1162,7 @@ impl SchemaFragment { /// Create an `SchemaFragment` from a JSON value (which should be an /// object of the shape required for Cedar schemas). - pub fn from_json_value(json: serde_json::Value) -> Result { + pub fn from_json_value(json: serde_json::Value) -> Result { let lossless = cedar_policy_validator::SchemaFragment::from_json_value(json)?; Ok(Self { value: lossless.clone().try_into()?, @@ -1201,7 +1199,7 @@ impl SchemaFragment { } /// Create a `SchemaFragment` directly from a file. - pub fn from_file(file: impl std::io::Read) -> Result { + pub fn from_file(file: impl std::io::Read) -> Result { let lossless = cedar_policy_validator::SchemaFragment::from_file(file)?; Ok(Self { value: lossless.clone().try_into()?, @@ -1210,13 +1208,15 @@ impl SchemaFragment { } /// Serialize this [`SchemaFragment`] as a json value - pub fn to_json_value(self) -> Result { - serde_json::to_value(self.lossless).map_err(|e| SchemaError::JsonSerialization(e).into()) + pub fn to_json_value(self) -> Result { + serde_json::to_value(self.lossless) + .map_err(|e| schema_error::SchemaError::JsonSerialization(e.into())) } /// Serialize this [`SchemaFragment`] as a json value - pub fn as_json_string(&self) -> Result { - serde_json::to_string(&self.lossless).map_err(|e| SchemaError::JsonSerialization(e).into()) + pub fn as_json_string(&self) -> Result { + serde_json::to_string(&self.lossless) + .map_err(|e| schema_error::SchemaError::JsonSerialization(e.into())) } /// Serialize this [`SchemaFragment`] into the natural syntax @@ -1227,7 +1227,7 @@ impl SchemaFragment { } impl TryInto for SchemaFragment { - type Error = SchemaError; + type Error = schema_error::SchemaError; /// Convert `SchemaFragment` into a `Schema`. To build the `Schema` we /// need to have all entity types defined, so an error will be returned if @@ -1243,7 +1243,7 @@ impl TryInto for SchemaFragment { } impl FromStr for SchemaFragment { - type Err = SchemaError; + type Err = schema_error::SchemaError; /// Construct `SchemaFragment` from a string containing a schema formatted /// in the cedar schema format. This can fail if the string is not valid /// JSON, or if the JSON structure does not form a valid schema. This @@ -1265,7 +1265,7 @@ impl FromStr for SchemaFragment { pub struct Schema(pub(crate) cedar_policy_validator::ValidatorSchema); impl FromStr for Schema { - type Err = SchemaError; + type Err = schema_error::SchemaError; /// Construct a schema from a string containing a schema formatted in the /// Cedar schema format. This can fail if it is not possible to parse a @@ -1285,7 +1285,7 @@ impl Schema { /// fragment. pub fn from_schema_fragments( fragments: impl IntoIterator, - ) -> Result { + ) -> Result { Ok(Self( cedar_policy_validator::ValidatorSchema::from_schema_fragments( fragments.into_iter().map(|f| f.value), @@ -1296,7 +1296,7 @@ impl Schema { /// Create a `Schema` from a JSON value (which should be an object of the /// shape required for Cedar schemas). - pub fn from_json_value(json: serde_json::Value) -> Result { + pub fn from_json_value(json: serde_json::Value) -> Result { Ok(Self( cedar_policy_validator::ValidatorSchema::from_json_value( json, @@ -1307,7 +1307,7 @@ impl Schema { /// Create a `Schema` from a string containing JSON in the appropriate /// shape. - pub fn from_json_str(json: &str) -> Result { + pub fn from_json_str(json: &str) -> Result { Ok(Self( cedar_policy_validator::ValidatorSchema::from_json_str( json, @@ -1318,7 +1318,7 @@ impl Schema { /// Create a `Schema` directly from a file containing JSON in the /// appropriate shape. - pub fn from_file(file: impl std::io::Read) -> Result { + pub fn from_file(file: impl std::io::Read) -> Result { Ok(Self(cedar_policy_validator::ValidatorSchema::from_file( file, Extensions::all_available(), @@ -2173,8 +2173,8 @@ impl Template { id: Option, json: serde_json::Value, ) -> Result { - let est: est::Policy = - serde_json::from_value(json).map_err(|e| JsonDeserializationError::Serde(e.into()))?; + let est: est::Policy = serde_json::from_value(json) + .map_err(|e| entities::json::err::JsonDeserializationError::Serde(e.into()))?; Self::from_est(id, est) } @@ -2564,8 +2564,8 @@ impl Policy { id: Option, json: serde_json::Value, ) -> Result { - let est: est::Policy = - serde_json::from_value(json).map_err(|e| JsonDeserializationError::Serde(e.into()))?; + let est: est::Policy = serde_json::from_value(json) + .map_err(|e| entities::json::err::JsonDeserializationError::Serde(e.into()))?; Self::from_est(id, est) } diff --git a/cedar-policy/src/api/err.rs b/cedar-policy/src/api/err.rs index 3168c086e..2c4077006 100644 --- a/cedar-policy/src/api/err.rs +++ b/cedar-policy/src/api/err.rs @@ -16,11 +16,9 @@ //! This module defines the publicly exported error types. -use crate::EntityTypeName; use crate::EntityUid; use crate::PolicyId; use cedar_policy_core::ast; -use cedar_policy_core::ast::Name; pub use cedar_policy_core::ast::RestrictedExpressionParseError; use cedar_policy_core::authorizer; use cedar_policy_core::est; @@ -30,11 +28,10 @@ pub use cedar_policy_core::extensions::{ }; pub use cedar_policy_core::parser::err::{ParseError, ParseErrors}; pub use cedar_policy_validator::human_schema::SchemaWarning; -pub use cedar_policy_validator::UnsupportedFeature; +pub use cedar_policy_validator::schema_error; use miette::Diagnostic; use ref_cast::RefCast; use smol_str::SmolStr; -use std::collections::HashSet; use thiserror::Error; /// Errors that can occur during authorization @@ -113,90 +110,6 @@ pub enum ReAuthorizeError { }, } -/// Errors encountered during construction of a Validation Schema -#[derive(Debug, Diagnostic, Error)] -pub enum SchemaError { - /// Error thrown by the `serde_json` crate during deserialization - #[error(transparent)] - #[diagnostic(transparent)] - JsonDeserialization(#[from] cedar_policy_validator::JsonDeserializationError), - /// Error thrown by the `serde_json` crate during serialization - #[error(transparent)] - JsonSerialization(serde_json::Error), // no #[from], because if you just have a serde_json::Error you should choose between JsonDeserialization and JsonSerialization appropriately - /// Errors occurring while computing or enforcing transitive closure on - /// action hierarchy. - #[error("transitive closure computation/enforcement error on action hierarchy: {0}")] - ActionTransitiveClosure(String), - /// Errors occurring while computing or enforcing transitive closure on - /// entity type hierarchy. - #[error("transitive closure computation/enforcement error on entity type hierarchy: {0}")] - EntityTypeTransitiveClosure(String), - /// Error generated when processing a schema file that uses unsupported features - #[error("unsupported feature used in schema: {0}")] - UnsupportedFeature(String), - /// Undeclared entity type(s) used in the `memberOf` field of an entity - /// type, the `appliesTo` fields of an action, or an attribute type in a - /// context or entity attribute record. Entity types in the error message - /// are fully qualified, including any implicit or explicit namespaces. - #[error("undeclared entity type(s): {0:?}")] - UndeclaredEntityTypes(HashSet), - /// Undeclared action(s) used in the `memberOf` field of an action. - #[error("undeclared action(s): {0:?}")] - UndeclaredActions(HashSet), - /// Undeclared common type(s) used in entity or context attributes. - #[error("undeclared common type(s): {0:?}")] - UndeclaredCommonTypes(HashSet), - /// Duplicate specifications for an entity type. Argument is the name of - /// the duplicate entity type. - #[error("duplicate entity type `{0}`")] - DuplicateEntityType(String), - /// Duplicate specifications for an action. Argument is the name of the - /// duplicate action. - #[error("duplicate action `{0}`")] - DuplicateAction(String), - /// Duplicate specification for a reusable type declaration. - #[error("duplicate common type `{0}`")] - DuplicateCommonType(String), - /// Cycle in the schema's action hierarchy. - #[error("cycle in action hierarchy containing `{0}`")] - CycleInActionHierarchy(EntityUid), - /// Cycle in the schema's common type declarations. - #[error("cycle in common type references containing `{0}`")] - CycleInCommonTypeReferences(Name), - /// The schema file included an entity type `Action` in the entity type - /// list. The `Action` entity type is always implicitly declared, and it - /// cannot currently have attributes or be in any groups, so there is no - /// purposes in adding an explicit entry. - #[error("entity type `Action` declared in `entityTypes` list")] - ActionEntityTypeDeclared, - /// `context` or `shape` fields are not records - #[error("{0} is declared with a type other than `Record`")] - ContextOrShapeNotRecord(ContextOrShape), - /// An action entity (transitively) has an attribute that is an empty set. - /// The validator cannot assign a type to an empty set. - /// This error variant should only be used when `PermitAttributes` is enabled. - #[error("action `{0}` has an attribute that is an empty set")] - ActionAttributesContainEmptySet(EntityUid), - /// An action entity (transitively) has an attribute of unsupported type (`ExprEscape`, `EntityEscape` or `ExtnEscape`). - /// This error variant should only be used when `PermitAttributes` is enabled. - #[error("action `{0}` has an attribute with unsupported JSON representation: {1}")] - UnsupportedActionAttribute(EntityUid, String), - /// Error when evaluating an action attribute - #[error(transparent)] - #[diagnostic(transparent)] - ActionAttrEval(EntityAttrEvaluationError), - /// Error thrown when the schema contains the `__expr` escape. - /// Support for this escape form has been dropped. - #[error("schema contained the non-supported `__expr` escape")] - ExprEscapeUsed, - /// An error reported during schema parsing when an unknown extension type - /// is used in a common type definition, entity attribute or action context - /// attributes. - #[error(transparent)] - #[diagnostic(transparent)] - UnknownExtensionType(UnknownExtensionType), -} - /// Errors serializing Schemas to the natural syntax #[derive(Debug, Error, Diagnostic)] pub enum ToHumanSyntaxError { @@ -247,10 +160,10 @@ impl From for ToHum } mod human_schema_error { + use crate::schema_error::SchemaError; use miette::Diagnostic; use thiserror::Error; - use crate::SchemaError; /// Parsing errors for human-readable schemas #[derive(Debug, Error, Diagnostic)] #[error(transparent)] @@ -291,7 +204,7 @@ impl From for HumanSchemaError { fn from(value: cedar_policy_validator::HumanSchemaError) -> Self { match value { cedar_policy_validator::HumanSchemaError::Core(core) => { - human_schema_error::CoreError(core.into()).into() + human_schema_error::CoreError(core).into() } cedar_policy_validator::HumanSchemaError::IO(io_err) => { human_schema_error::IoError(io_err).into() @@ -304,9 +217,9 @@ impl From for HumanSchemaError { } #[doc(hidden)] -impl From for HumanSchemaError { - fn from(value: cedar_policy_validator::SchemaError) -> Self { - human_schema_error::CoreError(value.into()).into() +impl From for HumanSchemaError { + fn from(value: crate::schema_error::SchemaError) -> Self { + human_schema_error::CoreError(value).into() } } @@ -351,119 +264,6 @@ impl From for EntityAttrEvaluationError { } } -/// Describes in what action context or entity type shape a schema parsing error -/// occurred. -#[derive(Debug)] -pub enum ContextOrShape { - /// An error occurred when parsing the context for the action with this - /// `EntityUid`. - ActionContext(EntityUid), - /// An error occurred when parsing the shape for the entity type with this - /// `EntityTypeName`. - EntityTypeShape(EntityTypeName), -} - -impl std::fmt::Display for ContextOrShape { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::ActionContext(action) => write!(f, "Context for action {action}"), - Self::EntityTypeShape(entity_type) => { - write!(f, "Shape for entity type {entity_type}") - } - } - } -} - -#[doc(hidden)] -impl From for ContextOrShape { - fn from(value: cedar_policy_validator::ContextOrShape) -> Self { - match value { - cedar_policy_validator::ContextOrShape::ActionContext(euid) => { - Self::ActionContext(EntityUid::new(euid)) - } - cedar_policy_validator::ContextOrShape::EntityTypeShape(name) => { - Self::EntityTypeShape(EntityTypeName::new(name)) - } - } - } -} - -/// An error reported during schema parsing when an unknown extension type is -/// used in a common type definition, entity attribute or action context -/// attributes. -#[derive(Debug, Diagnostic, Error)] -#[error(transparent)] -#[diagnostic(transparent)] -pub struct UnknownExtensionType { - inner: cedar_policy_validator::UnknownExtensionType, -} - -#[doc(hidden)] -impl From for UnknownExtensionType { - fn from(value: cedar_policy_validator::UnknownExtensionType) -> Self { - Self { inner: value } - } -} - -#[doc(hidden)] -impl From for SchemaError { - fn from(value: cedar_policy_validator::SchemaError) -> Self { - match value { - cedar_policy_validator::SchemaError::JsonDeserialization(e) => { - Self::JsonDeserialization(e) - } - cedar_policy_validator::SchemaError::ActionTransitiveClosure(e) => { - Self::ActionTransitiveClosure(e.to_string()) - } - cedar_policy_validator::SchemaError::EntityTypeTransitiveClosure(e) => { - Self::EntityTypeTransitiveClosure(e.to_string()) - } - cedar_policy_validator::SchemaError::UnsupportedFeature(e) => { - Self::UnsupportedFeature(e.to_string()) - } - cedar_policy_validator::SchemaError::UndeclaredEntityTypes(e) => { - Self::UndeclaredEntityTypes(e) - } - cedar_policy_validator::SchemaError::UndeclaredActions(e) => Self::UndeclaredActions(e), - cedar_policy_validator::SchemaError::UndeclaredCommonTypes(c) => { - Self::UndeclaredCommonTypes(c) - } - cedar_policy_validator::SchemaError::DuplicateEntityType(e) => { - Self::DuplicateEntityType(e) - } - cedar_policy_validator::SchemaError::DuplicateAction(e) => Self::DuplicateAction(e), - cedar_policy_validator::SchemaError::DuplicateCommonType(c) => { - Self::DuplicateCommonType(c) - } - cedar_policy_validator::SchemaError::CycleInActionHierarchy(e) => { - Self::CycleInActionHierarchy(EntityUid::new(e)) - } - cedar_policy_validator::SchemaError::CycleInCommonTypeReferences(n) => { - Self::CycleInCommonTypeReferences(n) - } - cedar_policy_validator::SchemaError::ActionEntityTypeDeclared => { - Self::ActionEntityTypeDeclared - } - cedar_policy_validator::SchemaError::ContextOrShapeNotRecord(context_or_shape) => { - Self::ContextOrShapeNotRecord(context_or_shape.into()) - } - cedar_policy_validator::SchemaError::ActionAttributesContainEmptySet(uid) => { - Self::ActionAttributesContainEmptySet(EntityUid::new(uid)) - } - cedar_policy_validator::SchemaError::UnsupportedActionAttribute(uid, escape_type) => { - Self::UnsupportedActionAttribute(EntityUid::new(uid), escape_type) - } - cedar_policy_validator::SchemaError::ActionAttrEval(err) => { - Self::ActionAttrEval(err.into()) - } - cedar_policy_validator::SchemaError::ExprEscapeUsed => Self::ExprEscapeUsed, - cedar_policy_validator::SchemaError::UnknownExtensionType(err) => { - SchemaError::UnknownExtensionType(err.into()) - } - } - } -} - /// This module contains definitions for structs containing detailed information /// about each validation error. Errors are primarily documented on their /// variants in [`ValidationError`]. diff --git a/cedar-policy/src/tests.rs b/cedar-policy/src/tests.rs index 5b30ecb8d..9d3151cb6 100644 --- a/cedar-policy/src/tests.rs +++ b/cedar-policy/src/tests.rs @@ -1476,7 +1476,7 @@ mod schema_tests { } }}"# )), - Err(SchemaError::JsonDeserialization(_)) + Err(crate::schema_error::SchemaError::JsonDeserialization(_)) ); } } @@ -2454,7 +2454,7 @@ mod schema_based_parsing_tests { let src = "{ , .. }"; assert_matches!( Schema::from_str(src), - Err(super::SchemaError::JsonDeserialization(_)) + Err(crate::schema_error::SchemaError::JsonDeserialization(_)) ); } @@ -3068,11 +3068,11 @@ mod schema_based_parsing_tests { #[cfg(not(feature = "partial-validate"))] #[test] fn partial_schema_unsupported() { - use cool_asserts::assert_panics; + use cool_asserts::assert_matches; use serde_json::json; - assert_panics!( - Schema::from_json_value( json!({"": { "entityTypes": { "A": { "shape": { "type": "Record", "attributes": {}, "additionalAttributes": true } } }, "actions": {} }})).unwrap(), - includes("records and entities with `additionalAttributes` are experimental, but the experimental `partial-validate` feature is not enabled") + assert_matches!( + Schema::from_json_value( json!({"": { "entityTypes": { "A": { "shape": { "type": "Record", "attributes": {}, "additionalAttributes": true } } }, "actions": {} }})), + Err(e) if e.to_string().contains("records and entities with `additionalAttributes` are experimental, but the experimental `partial-validate` feature is not enabled") ); }