Skip to content

Commit 03fe144

Browse files
feat(rust/signed-doc): implement DocumentOwnershipRule (#567)
* feat(rust/signed-doc): implement DocumentOwnershipRule * fix(rust/signed-doc): Compare KIDs in the same form * chore(rust/signed-doc): cleanup report messages * fix(cat-gateway): fix spelling and format * fix(rust/signed-doc): catalyst-ci/earthly/rust:v3.5.17 AS rust-ci * fix(rust/signed-doc): make deny.toml and nextest.toml same as catalyst-ci * fix(rust/immutable-ledger): clippy lint * fix(rust/signed-doc): fixes after code review * chore(rust/signed-doc): Apply suggestions from code review Co-authored-by: Alex Pozhylenkov <[email protected]> * chore(rust): fmt fix --------- Co-authored-by: Alex Pozhylenkov <[email protected]>
1 parent 6b093b0 commit 03fe144

File tree

6 files changed

+336
-142
lines changed

6 files changed

+336
-142
lines changed

rust/Earthfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,4 +128,4 @@ check-builder-src-cache:
128128
# local-ci-run: This step simulates the full CI run for local purposes only.
129129
local-ci-run:
130130
BUILD +check
131-
BUILD +build
131+
BUILD +build

rust/signed_doc/src/providers.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,13 @@ pub trait CatalystSignedDocumentProvider: Send + Sync {
3333
id: UuidV7,
3434
) -> impl Future<Output = anyhow::Result<Option<CatalystSignedDocument>>> + Send;
3535

36+
/// Try to get the first known version of the `CatalystSignedDocument`, `id` and `ver`
37+
/// are equal.
38+
fn try_get_first_doc(
39+
&self,
40+
id: UuidV7,
41+
) -> impl Future<Output = anyhow::Result<Option<CatalystSignedDocument>>> + Send;
42+
3643
/// Returns a future threshold value, which is used in the validation of the `ver`
3744
/// field that it is not too far in the future.
3845
/// If `None` is returned, skips "too far in the future" validation.
@@ -116,6 +123,18 @@ pub mod tests {
116123
.map(|(_, doc)| doc.clone()))
117124
}
118125

126+
async fn try_get_first_doc(
127+
&self,
128+
id: catalyst_types::uuid::UuidV7,
129+
) -> anyhow::Result<Option<CatalystSignedDocument>> {
130+
Ok(self
131+
.signed_doc
132+
.iter()
133+
.filter(|(doc_ref, _)| doc_ref.id() == &id)
134+
.min_by_key(|(doc_ref, _)| doc_ref.ver().uuid())
135+
.map(|(_, doc)| doc.clone()))
136+
}
137+
119138
fn future_threshold(&self) -> Option<std::time::Duration> {
120139
Some(Duration::from_secs(5))
121140
}

rust/signed_doc/src/validator/rules/mod.rs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ mod content_encoding;
1414
mod content_type;
1515
mod doc_ref;
1616
mod id;
17-
mod original_author;
17+
mod ownership;
1818
mod parameters;
1919
mod reply;
2020
mod section;
@@ -30,7 +30,7 @@ pub(crate) use content_encoding::ContentEncodingRule;
3030
pub(crate) use content_type::ContentTypeRule;
3131
pub(crate) use doc_ref::RefRule;
3232
pub(crate) use id::IdRule;
33-
pub(crate) use original_author::OriginalAuthorRule;
33+
pub(crate) use ownership::DocumentOwnershipRule;
3434
pub(crate) use parameters::ParametersRule;
3535
pub(crate) use reply::ReplyRule;
3636
pub(crate) use section::SectionRule;
@@ -69,7 +69,7 @@ pub(crate) struct Rules {
6969
/// document's signatures validation rule
7070
pub(crate) signature: SignatureRule,
7171
/// Original Author validation rule.
72-
pub(crate) original_author: OriginalAuthorRule,
72+
pub(crate) ownership: DocumentOwnershipRule,
7373
}
7474

7575
impl Rules {
@@ -96,7 +96,7 @@ impl Rules {
9696
self.content.check(doc).boxed(),
9797
self.kid.check(doc).boxed(),
9898
self.signature.check(doc, provider).boxed(),
99-
self.original_author.check(doc, provider).boxed(),
99+
self.ownership.check(doc, provider).boxed(),
100100
];
101101

102102
let res = futures::future::join_all(rules)
@@ -141,7 +141,9 @@ impl Rules {
141141
content: ContentRule::new(&doc_spec.payload)?,
142142
kid: SignatureKidRule::new(&doc_spec.signers.roles)?,
143143
signature: SignatureRule { mutlisig: false },
144-
original_author: OriginalAuthorRule,
144+
ownership: DocumentOwnershipRule {
145+
allow_collaborators: false,
146+
},
145147
};
146148
let doc_type = doc_spec.doc_type.parse()?;
147149

rust/signed_doc/src/validator/rules/original_author.rs

Lines changed: 0 additions & 136 deletions
This file was deleted.
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
//! Original Author Validation Rule
2+
3+
#[cfg(test)]
4+
mod tests;
5+
6+
use std::collections::HashSet;
7+
8+
use catalyst_types::catalyst_id::CatalystId;
9+
10+
use crate::{providers::CatalystSignedDocumentProvider, CatalystSignedDocument};
11+
12+
/// Context for the validation problem report.
13+
const REPORT_CONTEXT: &str = "Document ownership validation";
14+
15+
/// Returns `true` if the document has a single author.
16+
///
17+
/// If not, it adds to the document's problem report.
18+
fn single_author_check(doc: &CatalystSignedDocument) -> bool {
19+
let is_valid = doc.authors().len() == 1;
20+
if !is_valid {
21+
doc.report()
22+
.functional_validation("Document must only be signed by one author", REPORT_CONTEXT);
23+
}
24+
is_valid
25+
}
26+
27+
/// Document Ownership Validation Rule
28+
#[derive(Debug)]
29+
pub(crate) struct DocumentOwnershipRule {
30+
/// Collaborators are allowed.
31+
pub(crate) allow_collaborators: bool,
32+
}
33+
34+
impl DocumentOwnershipRule {
35+
/// Check document ownership rule
36+
pub(crate) async fn check<Provider>(
37+
&self,
38+
doc: &CatalystSignedDocument,
39+
provider: &Provider,
40+
) -> anyhow::Result<bool>
41+
where
42+
Provider: CatalystSignedDocumentProvider,
43+
{
44+
let doc_id = doc.doc_id()?;
45+
let first_doc_opt = provider.try_get_first_doc(doc_id).await?;
46+
47+
if self.allow_collaborators {
48+
if let Some(first_doc) = first_doc_opt {
49+
// This a new version of an existing `doc_id`
50+
let Some(last_doc) = provider.try_get_last_doc(doc_id).await? else {
51+
anyhow::bail!(
52+
"A latest version of the document must exist if a first version exists"
53+
);
54+
};
55+
56+
// Create sets of authors for comparison, ensure that they are in the same form
57+
// (e.g. each `kid` is in `URI form`).
58+
//
59+
// Allowed authors for this document are the original author, and collaborators
60+
// defined in the last published version of the Document ID.
61+
let mut allowed_authors = first_doc
62+
.authors()
63+
.into_iter()
64+
.map(CatalystId::as_uri)
65+
.collect::<HashSet<CatalystId>>();
66+
allowed_authors.extend(
67+
last_doc
68+
.doc_meta()
69+
.collaborators()
70+
.iter()
71+
.cloned()
72+
.map(CatalystId::as_uri),
73+
);
74+
let doc_authors = doc
75+
.authors()
76+
.into_iter()
77+
.map(CatalystId::as_uri)
78+
.collect::<HashSet<_>>();
79+
80+
let is_valid = allowed_authors.intersection(&doc_authors).count() > 0;
81+
82+
if !is_valid {
83+
doc.report().functional_validation(
84+
"Document must only be signed by original author and/or by collaborators defined in the previous version",
85+
REPORT_CONTEXT,
86+
);
87+
}
88+
return Ok(is_valid);
89+
}
90+
91+
// This is a first version of the doc
92+
return Ok(single_author_check(doc));
93+
}
94+
95+
// No collaborators are allowed
96+
if let Some(first_doc) = first_doc_opt {
97+
// This a new version of an existing `doc_id`
98+
let is_valid = first_doc.authors() == doc.authors();
99+
if !is_valid {
100+
doc.report().functional_validation(
101+
"Document authors must match the author from the first version",
102+
REPORT_CONTEXT,
103+
);
104+
}
105+
return Ok(is_valid);
106+
}
107+
108+
// This is a first version of the doc
109+
Ok(single_author_check(doc))
110+
}
111+
}

0 commit comments

Comments
 (0)