Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion rust/Earthfile
Original file line number Diff line number Diff line change
Expand Up @@ -128,4 +128,4 @@ check-builder-src-cache:
# local-ci-run: This step simulates the full CI run for local purposes only.
local-ci-run:
BUILD +check
BUILD +build
BUILD +build
19 changes: 19 additions & 0 deletions rust/signed_doc/src/providers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,13 @@ pub trait CatalystSignedDocumentProvider: Send + Sync {
id: UuidV7,
) -> impl Future<Output = anyhow::Result<Option<CatalystSignedDocument>>> + Send;

/// Try to get the first known version of the `CatalystSignedDocument`, `id` and `ver`
/// are equal.
fn try_get_first_doc(
&self,
id: UuidV7,
) -> impl Future<Output = anyhow::Result<Option<CatalystSignedDocument>>> + Send;

/// Returns a future threshold value, which is used in the validation of the `ver`
/// field that it is not too far in the future.
/// If `None` is returned, skips "too far in the future" validation.
Expand Down Expand Up @@ -116,6 +123,18 @@ pub mod tests {
.map(|(_, doc)| doc.clone()))
}

async fn try_get_first_doc(
&self,
id: catalyst_types::uuid::UuidV7,
) -> anyhow::Result<Option<CatalystSignedDocument>> {
Ok(self
.signed_doc
.iter()
.filter(|(doc_ref, _)| doc_ref.id() == &id)
.min_by_key(|(doc_ref, _)| doc_ref.ver().uuid())
.map(|(_, doc)| doc.clone()))
}

fn future_threshold(&self) -> Option<std::time::Duration> {
Some(Duration::from_secs(5))
}
Expand Down
12 changes: 7 additions & 5 deletions rust/signed_doc/src/validator/rules/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ mod content_encoding;
mod content_type;
mod doc_ref;
mod id;
mod original_author;
mod ownership;
mod parameters;
mod reply;
mod section;
Expand All @@ -30,7 +30,7 @@ pub(crate) use content_encoding::ContentEncodingRule;
pub(crate) use content_type::ContentTypeRule;
pub(crate) use doc_ref::RefRule;
pub(crate) use id::IdRule;
pub(crate) use original_author::OriginalAuthorRule;
pub(crate) use ownership::DocumentOwnershipRule;
pub(crate) use parameters::ParametersRule;
pub(crate) use reply::ReplyRule;
pub(crate) use section::SectionRule;
Expand Down Expand Up @@ -69,7 +69,7 @@ pub(crate) struct Rules {
/// document's signatures validation rule
pub(crate) signature: SignatureRule,
/// Original Author validation rule.
pub(crate) original_author: OriginalAuthorRule,
pub(crate) ownership: DocumentOwnershipRule,
}

impl Rules {
Expand All @@ -96,7 +96,7 @@ impl Rules {
self.content.check(doc).boxed(),
self.kid.check(doc).boxed(),
self.signature.check(doc, provider).boxed(),
self.original_author.check(doc, provider).boxed(),
self.ownership.check(doc, provider).boxed(),
];

let res = futures::future::join_all(rules)
Expand Down Expand Up @@ -141,7 +141,9 @@ impl Rules {
content: ContentRule::new(&doc_spec.payload)?,
kid: SignatureKidRule::new(&doc_spec.signers.roles)?,
signature: SignatureRule { mutlisig: false },
original_author: OriginalAuthorRule,
ownership: DocumentOwnershipRule {
allow_collaborators: false,
},
};
let doc_type = doc_spec.doc_type.parse()?;

Expand Down
136 changes: 0 additions & 136 deletions rust/signed_doc/src/validator/rules/original_author.rs

This file was deleted.

111 changes: 111 additions & 0 deletions rust/signed_doc/src/validator/rules/ownership/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
//! Original Author Validation Rule

#[cfg(test)]
mod tests;

use std::collections::HashSet;

use catalyst_types::catalyst_id::CatalystId;

use crate::{providers::CatalystSignedDocumentProvider, CatalystSignedDocument};

/// Context for the validation problem report.
const REPORT_CONTEXT: &str = "Document ownership validation";

/// Returns `true` if the document has a single author.
///
/// If not, it adds to the document's problem report.
fn single_author_check(doc: &CatalystSignedDocument) -> bool {
let is_valid = doc.authors().len() == 1;
if !is_valid {
doc.report()
.functional_validation("Document must only be signed by one author", REPORT_CONTEXT);
}
is_valid
}

/// Document Ownership Validation Rule
#[derive(Debug)]
pub(crate) struct DocumentOwnershipRule {
/// Collaborators are allowed.
pub(crate) allow_collaborators: bool,
}

impl DocumentOwnershipRule {
/// Check document ownership rule
pub(crate) async fn check<Provider>(
&self,
doc: &CatalystSignedDocument,
provider: &Provider,
) -> anyhow::Result<bool>
where
Provider: CatalystSignedDocumentProvider,
{
let doc_id = doc.doc_id()?;
let first_doc_opt = provider.try_get_first_doc(doc_id).await?;

if self.allow_collaborators {
if let Some(first_doc) = first_doc_opt {
// This a new version of an existing `doc_id`
let Some(last_doc) = provider.try_get_last_doc(doc_id).await? else {
anyhow::bail!(
"A latest version of the document must exist if a first version exists"
);
};

// Create sets of authors for comparison, ensure that they are in the same form
// (e.g. each `kid` is in `URI form`).
//
// Allowed authors for this document are the original author, and collaborators
// defined in the last published version of the Document ID.
let mut allowed_authors = first_doc
.authors()
.into_iter()
.map(CatalystId::as_uri)
.collect::<HashSet<CatalystId>>();
allowed_authors.extend(
last_doc
.doc_meta()
.collaborators()
.iter()
.cloned()
.map(CatalystId::as_uri),
);
let doc_authors = doc
.authors()
.into_iter()
.map(CatalystId::as_uri)
.collect::<HashSet<_>>();

let is_valid = allowed_authors.intersection(&doc_authors).count() > 0;

if !is_valid {
doc.report().functional_validation(
"Document must only be signed by original author and/or by collaborators defined in the previous version",
REPORT_CONTEXT,
);
}
return Ok(is_valid);
}

// This is a first version of the doc
return Ok(single_author_check(doc));
}

// No collaborators are allowed
if let Some(first_doc) = first_doc_opt {
// This a new version of an existing `doc_id`
let is_valid = first_doc.authors() == doc.authors();
if !is_valid {
doc.report().functional_validation(
"Document authors must match the author from the first version",
REPORT_CONTEXT,
);
}
return Ok(is_valid);
}

// This is a first version of the doc
Ok(single_author_check(doc))
}
}
Loading