Skip to content

Commit a9fd032

Browse files
committed
feat(rust/signed-doc): implement DocumentOwnershipRule
1 parent 4e7bfde commit a9fd032

File tree

6 files changed

+332
-146
lines changed

6 files changed

+332
-146
lines changed

rust/signed_doc/src/providers.rs

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

34+
/// Try to get the first known version of the `CatalystSignedDocument`, same
35+
/// `id` and the lowest known `ver`.
36+
fn try_get_first_doc(
37+
&self,
38+
id: UuidV7,
39+
) -> impl Future<Output = anyhow::Result<Option<CatalystSignedDocument>>> + Send;
40+
3441
/// Returns a future threshold value, which is used in the validation of the `ver`
3542
/// field that it is not too far in the future.
3643
/// If `None` is returned, skips "too far in the future" validation.
@@ -114,6 +121,18 @@ pub mod tests {
114121
.map(|(_, doc)| doc.clone()))
115122
}
116123

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

rust/signed_doc/src/validator/mod.rs

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ use std::{collections::HashMap, sync::LazyLock};
77

88
use catalyst_types::catalyst_id::role_index::RoleId;
99
use rules::{
10-
ContentEncodingRule, ContentRule, ContentSchema, ContentTypeRule, IdRule, OriginalAuthorRule,
11-
ParametersRule, RefRule, ReplyRule, Rules, SectionRule, SignatureKidRule, VerRule,
10+
ContentEncodingRule, ContentRule, ContentSchema, ContentTypeRule, DocumentOwnershipRule,
11+
IdRule, ParametersRule, RefRule, ReplyRule, Rules, SectionRule, SignatureKidRule, VerRule,
1212
};
1313

1414
use crate::{
@@ -61,7 +61,9 @@ fn proposal_rule() -> Rules {
6161
allowed_roles: vec![RoleId::Proposer],
6262
},
6363
signature: SignatureRule { mutlisig: false },
64-
original_author: OriginalAuthorRule,
64+
ownership: DocumentOwnershipRule {
65+
allow_collaborators: false,
66+
},
6567
}
6668
}
6769

@@ -108,7 +110,9 @@ fn proposal_comment_rule() -> Rules {
108110
allowed_roles: vec![RoleId::Role0],
109111
},
110112
signature: SignatureRule { mutlisig: false },
111-
original_author: OriginalAuthorRule,
113+
ownership: DocumentOwnershipRule {
114+
allow_collaborators: false,
115+
},
112116
}
113117
}
114118

@@ -161,7 +165,9 @@ fn proposal_submission_action_rule() -> Rules {
161165
allowed_roles: vec![RoleId::Proposer],
162166
},
163167
signature: SignatureRule { mutlisig: false },
164-
original_author: OriginalAuthorRule,
168+
ownership: DocumentOwnershipRule {
169+
allow_collaborators: false,
170+
},
165171
}
166172
}
167173

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: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
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+
/// Returns `true` if the document has a single author.
13+
///
14+
/// If not, it adds to the document's problem report.
15+
fn single_author_check(doc: &CatalystSignedDocument) -> bool {
16+
let is_valid = doc.authors().len() == 1;
17+
if !is_valid {
18+
doc.report().functional_validation(
19+
"New document must only be signed by a single author",
20+
"Valid documents must only be signed by the original author",
21+
);
22+
}
23+
is_valid
24+
}
25+
26+
/// Document Ownership Validation Rule
27+
#[derive(Debug)]
28+
pub(crate) struct DocumentOwnershipRule {
29+
/// Collaborators are allowed.
30+
pub(crate) allow_collaborators: bool,
31+
}
32+
33+
impl DocumentOwnershipRule {
34+
/// Check document ownership rule
35+
pub(crate) async fn check<Provider>(
36+
&self,
37+
doc: &CatalystSignedDocument,
38+
provider: &Provider,
39+
) -> anyhow::Result<bool>
40+
where
41+
Provider: CatalystSignedDocumentProvider,
42+
{
43+
let doc_id = doc.doc_id()?;
44+
let first_doc_opt = provider.try_get_first_doc(doc_id).await?;
45+
46+
if self.allow_collaborators {
47+
if let Some(first_doc) = first_doc_opt {
48+
// This a new version of an existing `doc_id`
49+
let Some(last_doc) = provider.try_get_last_doc(doc_id).await? else {
50+
anyhow::bail!(
51+
"A latest version of the document must exist if a first version exists"
52+
);
53+
};
54+
// Allowed authors for this document are the original author, and collaborators
55+
// defined in the last published version of the Document ID.
56+
let mut allowed_authors = first_doc
57+
.authors()
58+
.into_iter()
59+
.collect::<HashSet<CatalystId>>();
60+
allowed_authors.extend(last_doc.doc_meta().collaborators().iter().cloned());
61+
62+
let doc_authors = doc.authors().into_iter().collect::<HashSet<CatalystId>>();
63+
64+
let is_valid = allowed_authors.intersection(&doc_authors).count() > 0;
65+
66+
if !is_valid {
67+
doc.report().functional_validation(
68+
"New document must only be signed by a single author or collaborators defined in the previous version",
69+
"Valid documents must only be signed by the original author or known collaborators",
70+
);
71+
}
72+
return Ok(is_valid);
73+
}
74+
75+
// This is a first version of the doc
76+
return Ok(single_author_check(doc));
77+
}
78+
79+
// No collaborators are allowed
80+
if let Some(first_doc) = first_doc_opt {
81+
// This a new version of an existing `doc_id`
82+
let is_valid = first_doc.authors() == doc.authors();
83+
if !is_valid {
84+
doc.report().functional_validation(
85+
&format!("New document authors must match the authors from the first version for Document ID {doc_id}"),
86+
"Valid documents must only be signed by the original author",
87+
);
88+
}
89+
return Ok(is_valid);
90+
}
91+
92+
// This is a first version of the doc
93+
Ok(single_author_check(doc))
94+
}
95+
}

0 commit comments

Comments
 (0)