diff --git a/rust/signed_doc/src/metadata/document_refs/mod.rs b/rust/signed_doc/src/metadata/document_refs/mod.rs index 0ece194e873..f13a1eac942 100644 --- a/rust/signed_doc/src/metadata/document_refs/mod.rs +++ b/rust/signed_doc/src/metadata/document_refs/mod.rs @@ -2,7 +2,7 @@ mod doc_locator; mod doc_ref; -use std::fmt::Display; +use std::{fmt::Display, ops::Deref}; use catalyst_types::uuid::{CborContext, UuidV7}; use cbork_utils::{array::Array, decode_context::DecodeCtx}; @@ -17,6 +17,14 @@ use crate::CompatibilityPolicy; #[derive(Clone, Debug, PartialEq, Hash, Eq)] pub struct DocumentRefs(Vec); +impl Deref for DocumentRefs { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + impl DocumentRefs { /// Returns true if provided `cbor` bytes is a valid old format. /// ```cddl @@ -50,14 +58,6 @@ pub enum DocRefError { HexDecode(String), } -impl DocumentRefs { - /// Get a list of document reference instance. - #[must_use] - pub fn doc_refs(&self) -> &Vec { - &self.0 - } -} - impl From> for DocumentRefs { fn from(value: Vec) -> Self { DocumentRefs(value) diff --git a/rust/signed_doc/src/validator/mod.rs b/rust/signed_doc/src/validator/mod.rs index 4a03fbfc8cb..6a503a10a57 100644 --- a/rust/signed_doc/src/validator/mod.rs +++ b/rust/signed_doc/src/validator/mod.rs @@ -2,7 +2,6 @@ pub(crate) mod json_schema; pub(crate) mod rules; -pub(crate) mod utils; use std::{ collections::HashMap, diff --git a/rust/signed_doc/src/validator/rules/doc_ref.rs b/rust/signed_doc/src/validator/rules/doc_ref.rs index ce5aa631eab..eccb1d0a456 100644 --- a/rust/signed_doc/src/validator/rules/doc_ref.rs +++ b/rust/signed_doc/src/validator/rules/doc_ref.rs @@ -3,8 +3,8 @@ use catalyst_types::problem_report::ProblemReport; use crate::{ - providers::CatalystSignedDocumentProvider, validator::utils::validate_doc_refs, - CatalystSignedDocument, DocType, + providers::CatalystSignedDocumentProvider, CatalystSignedDocument, DocType, DocumentRef, + DocumentRefs, }; /// `ref` field validation rule @@ -36,11 +36,16 @@ impl RefRule { optional, } = self { - if let Some(doc_ref) = doc.doc_meta().doc_ref() { - let ref_validator = |ref_doc: CatalystSignedDocument| { - referenced_doc_check(&ref_doc, exp_ref_types, "ref", doc.report()) - }; - return validate_doc_refs(doc_ref, provider, doc.report(), ref_validator).await; + if let Some(doc_refs) = doc.doc_meta().doc_ref() { + return doc_refs_check( + doc_refs, + exp_ref_types, + "ref", + provider, + doc.report(), + |_| true, + ) + .await; } else if !optional { doc.report() .missing_field("ref", &format!("{context}, document must have ref field")); @@ -62,15 +67,98 @@ impl RefRule { } } -/// A generic implementation of the referenced document validation. -pub(crate) fn referenced_doc_check( +/// Validate all the document references by the defined validation rules, +/// plus conducting additional validations with the provided `validator`. +/// Document all possible error in doc report (no fail fast) +pub(crate) async fn doc_refs_check( + doc_refs: &DocumentRefs, + exp_ref_types: &[DocType], + field_name: &str, + provider: &Provider, + report: &ProblemReport, + validator: Validator, +) -> anyhow::Result +where + Provider: CatalystSignedDocumentProvider, + Validator: Fn(&CatalystSignedDocument) -> bool, +{ + let mut all_valid = true; + + for dr in doc_refs.iter() { + if let Some(ref ref_doc) = provider.try_get_doc(dr).await? { + let is_valid = referenced_doc_type_check(ref_doc, exp_ref_types, field_name, report) + && referenced_doc_id_and_ver_check(ref_doc, dr, field_name, report) + && validator(ref_doc); + + if !is_valid { + all_valid = false; + } + } else { + report.functional_validation( + &format!("Cannot retrieve a document {dr}"), + &format!("Referenced document validation for the `{field_name}` field"), + ); + all_valid = false; + } + } + Ok(all_valid) +} + +/// Validation check that the provided `ref_doc` is a correct referenced document found by +/// `original_doc_ref` +fn referenced_doc_id_and_ver_check( + ref_doc: &CatalystSignedDocument, + original_doc_ref: &DocumentRef, + field_name: &str, + report: &ProblemReport, +) -> bool { + let Ok(id) = ref_doc.doc_id() else { + report.missing_field( + "id", + &format!("Referenced document validation for the `{field_name}` field"), + ); + return false; + }; + + let Ok(ver) = ref_doc.doc_ver() else { + report.missing_field( + "ver", + &format!("Referenced document validation for the `{field_name}` field"), + ); + return false; + }; + + // id and version must match the values in ref doc + if &id != original_doc_ref.id() && &ver != original_doc_ref.ver() { + report.invalid_value( + "id and version", + &format!("id: {id}, ver: {ver}"), + &format!( + "id: {}, ver: {}", + original_doc_ref.id(), + original_doc_ref.ver() + ), + &format!("Referenced document validation for the `{field_name}` field"), + ); + return false; + } + + true +} + +/// Validation check that the provided `ref_doc` has an expected `type` field value from +/// the allowed `exp_ref_types` list +fn referenced_doc_type_check( ref_doc: &CatalystSignedDocument, exp_ref_types: &[DocType], field_name: &str, report: &ProblemReport, ) -> bool { let Ok(ref_doc_type) = ref_doc.doc_type() else { - report.missing_field("type", "Referenced document must have type field"); + report.missing_field( + "type", + &format!("Document reference validation for the `{field_name}` field"), + ); return false; }; diff --git a/rust/signed_doc/src/validator/rules/parameters.rs b/rust/signed_doc/src/validator/rules/parameters.rs index 34a506d3238..e26fdcfdc27 100644 --- a/rust/signed_doc/src/validator/rules/parameters.rs +++ b/rust/signed_doc/src/validator/rules/parameters.rs @@ -4,8 +4,7 @@ use catalyst_types::problem_report::ProblemReport; use futures::FutureExt; use crate::{ - providers::CatalystSignedDocumentProvider, - validator::{rules::doc_ref::referenced_doc_check, utils::validate_doc_refs}, + providers::CatalystSignedDocumentProvider, validator::rules::doc_ref::doc_refs_check, CatalystSignedDocument, DocType, DocumentRefs, }; @@ -41,12 +40,15 @@ impl ParametersRule { } = self { if let Some(parameters_ref) = doc.doc_meta().parameters() { - let parameters_validator = |ref_doc: CatalystSignedDocument| { - referenced_doc_check(&ref_doc, exp_parameters_type, "parameters", doc.report()) - }; - let parameters_check = - validate_doc_refs(parameters_ref, provider, doc.report(), parameters_validator) - .boxed(); + let parameters_check = doc_refs_check( + parameters_ref, + exp_parameters_type, + "parameters", + provider, + doc.report(), + |_| true, + ) + .boxed(); let template_link_check = link_check( doc.doc_meta().template(), @@ -125,32 +127,41 @@ where return Ok(true); }; - let link_validator = |ref_doc: CatalystSignedDocument| { - let Some(ref_doc_parameters) = ref_doc.doc_meta().parameters() else { - report.missing_field( - "parameters", - &format!( - "Referenced document via {field_name} must have `parameters` field. Referenced Document: {ref_doc}" - ), - ); - return false; - }; + let mut all_valid = true; - if exp_parameters != ref_doc_parameters { - report.invalid_value( - "parameters", - &format!("Reference doc param: {ref_doc_parameters}",), - &format!("Doc param: {exp_parameters}"), - &format!( - "Referenced document via {field_name} `parameters` field must match. Referenced Document: {ref_doc}" - ), + for dr in ref_field.iter() { + if let Some(ref ref_doc) = provider.try_get_doc(dr).await? { + let Some(ref_doc_parameters) = ref_doc.doc_meta().parameters() else { + report.missing_field( + "parameters", + &format!( + "Referenced document via {field_name} must have `parameters` field. Referenced Document: {ref_doc}" + ), + ); + all_valid = false; + continue; + }; + + if exp_parameters != ref_doc_parameters { + report.invalid_value( + "parameters", + &format!("Reference doc param: {ref_doc_parameters}",), + &format!("Doc param: {exp_parameters}"), + &format!( + "Referenced document via {field_name} `parameters` field must match. Referenced Document: {ref_doc}" + ), + ); + all_valid = false; + } + } else { + report.functional_validation( + &format!("Cannot retrieve a document {dr}"), + &format!("Referenced document link validation for the `{field_name}` field"), ); - return false; + all_valid = false; } - true - }; - - validate_doc_refs(ref_field, provider, report, link_validator).await + } + Ok(all_valid) } #[cfg(test)] diff --git a/rust/signed_doc/src/validator/rules/reply.rs b/rust/signed_doc/src/validator/rules/reply.rs index 7f803dcdef2..ecbf9b023f9 100644 --- a/rust/signed_doc/src/validator/rules/reply.rs +++ b/rust/signed_doc/src/validator/rules/reply.rs @@ -1,8 +1,7 @@ //! `reply` rule type impl. -use super::doc_ref::referenced_doc_check; use crate::{ - providers::CatalystSignedDocumentProvider, validator::utils::validate_doc_refs, + providers::CatalystSignedDocumentProvider, validator::rules::doc_ref::doc_refs_check, CatalystSignedDocument, DocType, }; @@ -37,17 +36,7 @@ impl ReplyRule { } = self { if let Some(reply_ref) = doc.doc_meta().reply() { - let reply_validator = |ref_doc: CatalystSignedDocument| { - // Validate type - if !referenced_doc_check( - &ref_doc, - std::slice::from_ref(exp_reply_type), - "reply", - doc.report(), - ) { - return false; - } - + let reply_validator = |ref_doc: &CatalystSignedDocument| { // Get `ref` from both the doc and the ref doc let Some(ref_doc_dr) = ref_doc.doc_meta().doc_ref() else { doc.report() @@ -73,7 +62,16 @@ impl ReplyRule { } true }; - return validate_doc_refs(reply_ref, provider, doc.report(), reply_validator).await; + + return doc_refs_check( + reply_ref, + std::slice::from_ref(exp_reply_type), + "reply", + provider, + doc.report(), + reply_validator, + ) + .await; } else if !optional { doc.report().missing_field( "reply", diff --git a/rust/signed_doc/src/validator/rules/template.rs b/rust/signed_doc/src/validator/rules/template.rs index b3de0fb0391..31f88b052a5 100644 --- a/rust/signed_doc/src/validator/rules/template.rs +++ b/rust/signed_doc/src/validator/rules/template.rs @@ -2,11 +2,10 @@ use std::fmt::Write; -use super::doc_ref::referenced_doc_check; use crate::{ metadata::ContentType, providers::CatalystSignedDocumentProvider, - validator::{json_schema, utils::validate_doc_refs}, + validator::{json_schema, rules::doc_ref::doc_refs_check}, CatalystSignedDocument, DocType, }; @@ -49,15 +48,8 @@ impl ContentRule { .missing_field("template", &format!("{context}, doc")); return Ok(false); }; - let template_validator = |template_doc: CatalystSignedDocument| { - if !referenced_doc_check( - &template_doc, - std::slice::from_ref(exp_template_type), - "template", - doc.report(), - ) { - return false; - } + + let template_validator = |template_doc: &CatalystSignedDocument| { let Ok(template_content_type) = template_doc.doc_content_type() else { doc.report().missing_field( "content-type", @@ -66,7 +58,7 @@ impl ContentRule { return false; }; match template_content_type { - ContentType::Json => templated_json_schema_check(doc, &template_doc), + ContentType::Json => templated_json_schema_check(doc, template_doc), ContentType::Cddl | ContentType::Cbor | ContentType::JsonSchema @@ -83,8 +75,16 @@ impl ContentRule { }, } }; - return validate_doc_refs(template_ref, provider, doc.report(), template_validator) - .await; + + return doc_refs_check( + template_ref, + std::slice::from_ref(exp_template_type), + "template", + provider, + doc.report(), + template_validator, + ) + .await; } if let Self::Static(content_schema) = self { if let Some(template) = doc.doc_meta().template() { diff --git a/rust/signed_doc/src/validator/utils.rs b/rust/signed_doc/src/validator/utils.rs deleted file mode 100644 index 2edcad7bcc2..00000000000 --- a/rust/signed_doc/src/validator/utils.rs +++ /dev/null @@ -1,76 +0,0 @@ -//! Validation utility functions - -use catalyst_types::problem_report::ProblemReport; - -use crate::{ - providers::CatalystSignedDocumentProvider, CatalystSignedDocument, DocumentRef, DocumentRefs, -}; - -/// A helper validation document function, which validates a document from the -/// `ValidationDataProvider`. -pub(crate) async fn validate_provided_doc( - doc_ref: &DocumentRef, - provider: &Provider, - report: &ProblemReport, - validator: Validator, -) -> anyhow::Result -where - Provider: CatalystSignedDocumentProvider, - Validator: Fn(CatalystSignedDocument) -> bool, -{ - const CONTEXT: &str = "Validation data provider"; - - // General check for document ref - - // Getting the Signed Document instance from a doc ref. - // The reference document must exist - if let Some(doc) = provider.try_get_doc(doc_ref).await? { - let id = doc - .doc_id() - .inspect_err(|_| report.missing_field("id", CONTEXT))?; - - let ver = doc - .doc_ver() - .inspect_err(|_| report.missing_field("ver", CONTEXT))?; - // id and version must match the values in ref doc - if &id != doc_ref.id() && &ver != doc_ref.ver() { - report.invalid_value( - "id and version", - &format!("id: {id}, ver: {ver}"), - &format!("id: {}, ver: {}", doc_ref.id(), doc_ref.ver()), - CONTEXT, - ); - return Ok(false); - } - Ok(validator(doc)) - } else { - report.functional_validation( - format!("Cannot retrieve a document {doc_ref}").as_str(), - CONTEXT, - ); - Ok(false) - } -} - -/// Validate the document references -/// Document all possible error in doc report (no fail fast) -pub(crate) async fn validate_doc_refs( - doc_refs: &DocumentRefs, - provider: &Provider, - report: &ProblemReport, - validator: Validator, -) -> anyhow::Result -where - Provider: CatalystSignedDocumentProvider, - Validator: Fn(CatalystSignedDocument) -> bool, -{ - let mut all_valid = true; - - for dr in doc_refs.doc_refs() { - let is_valid = validate_provided_doc(dr, provider, report, &validator).await?; - if !is_valid { - all_valid = false; - } - } - Ok(all_valid) -}