Skip to content

Commit 625f6ac

Browse files
authored
fix(cat-gateway): Publishing of existing document response issues (#3829)
* check the document was already submitted before running validation * fix * fix tests
1 parent 0a90ecc commit 625f6ac

File tree

6 files changed

+88
-132
lines changed

6 files changed

+88
-132
lines changed

catalyst-gateway/bin/src/db/event/signed_docs/full_signed_doc.rs

Lines changed: 43 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
//! `FullSignedDoc` struct implementation.
22
3-
use anyhow::Context;
4-
53
use super::SignedDocBody;
64
use crate::{
7-
db::event::{EventDB, EventDBConnectionError},
5+
db::event::EventDB,
86
jinja::{JinjaTemplateSource, get_template},
97
};
108

@@ -17,11 +15,6 @@ pub(crate) const SELECT_SIGNED_DOCS_TEMPLATE: JinjaTemplateSource = JinjaTemplat
1715
source: include_str!("./sql/select_signed_documents.sql.jinja"),
1816
};
1917

20-
/// `FullSignedDoc::store` method error.
21-
#[derive(thiserror::Error, Debug)]
22-
#[error("Document with the same `id` and `ver` already exists")]
23-
pub(crate) struct StoreError;
24-
2518
/// Full signed doc event db struct
2619
#[derive(Debug, Clone, PartialEq)]
2720
pub(crate) struct FullSignedDoc {
@@ -33,6 +26,45 @@ pub(crate) struct FullSignedDoc {
3326
raw: Vec<u8>,
3427
}
3528

29+
impl TryFrom<&catalyst_signed_doc::CatalystSignedDocument> for FullSignedDoc {
30+
type Error = anyhow::Error;
31+
32+
fn try_from(doc: &catalyst_signed_doc::CatalystSignedDocument) -> Result<Self, Self::Error> {
33+
let authors = doc
34+
.authors()
35+
.iter()
36+
.map(|v| v.as_short_id().to_string())
37+
.collect();
38+
39+
let doc_meta_json = doc.doc_meta().to_json()?;
40+
41+
let payload = if matches!(
42+
doc.doc_content_type(),
43+
Some(catalyst_signed_doc::ContentType::Json)
44+
) {
45+
match serde_json::from_slice(doc.decoded_content()?.as_slice()) {
46+
Ok(payload) => Some(payload),
47+
Err(e) => {
48+
anyhow::bail!("Invalid Document Content, not Json encoded: {e}");
49+
},
50+
}
51+
} else {
52+
None
53+
};
54+
55+
let doc_body = SignedDocBody::new(
56+
doc.doc_id()?.into(),
57+
doc.doc_ver()?.into(),
58+
doc.doc_type()?.uuid(),
59+
authors,
60+
Some(doc_meta_json),
61+
);
62+
let doc_bytes = doc.to_bytes()?;
63+
64+
Ok(FullSignedDoc::new(doc_body, payload, doc_bytes))
65+
}
66+
}
67+
3668
impl FullSignedDoc {
3769
/// Creates a `FullSignedDoc` instance.
3870
pub(crate) fn new(
@@ -60,6 +92,7 @@ impl FullSignedDoc {
6092
}
6193

6294
/// Returns the `SignedDocBody`.
95+
#[allow(dead_code)]
6396
pub(crate) fn body(&self) -> &SignedDocBody {
6497
&self.body
6598
}
@@ -70,37 +103,25 @@ impl FullSignedDoc {
70103
}
71104

72105
/// Uploads a `FullSignedDoc` to the event db.
73-
/// Returns `true` if document was added into the db, `false` if it was already added
74-
/// previously.
75106
///
76107
/// Make an insert query into the `event-db` by adding data into the `signed_docs`
77108
/// table.
78109
///
79110
/// * IF the record primary key (id,ver) does not exist, then add the new record.
80111
/// Return success.
81-
/// * IF the record does exist, but all values are the same as stored, return Success.
82112
/// * Otherwise return an error. (Can not over-write an existing record with new
83113
/// data).
84114
///
85115
/// # Arguments:
86116
/// - `id` is a UUID v7
87117
/// - `ver` is a UUID v7
88118
/// - `doc_type` is a UUID v4
89-
pub(crate) async fn store(&self) -> anyhow::Result<bool> {
119+
pub(crate) async fn store(&self) -> anyhow::Result<()> {
90120
// Perform insert before checking if the document already exists.
91121
// This should prevent race condition.
92122
match EventDB::modify(INSERT_SIGNED_DOCS, &self.postgres_db_fields()).await {
93123
// Able to insert, no conflict
94-
Ok(()) => Ok(true),
95-
Err(e) if !e.is::<EventDBConnectionError>() => {
96-
// Attempt to retrieve the document now that we failed to insert
97-
let doc = Self::retrieve(self.id(), Some(self.ver()))
98-
.await
99-
.context("Cannot retrieve the document which has failed")?;
100-
anyhow::ensure!(&doc == self, StoreError);
101-
// Document already exists and matches, return false
102-
Ok(false)
103-
},
124+
Ok(()) => Ok(()),
104125
Err(e) => Err(e),
105126
}
106127
}

catalyst-gateway/bin/src/db/event/signed_docs/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ mod signed_doc_body;
88
mod tests;
99

1010
pub(crate) use doc_ref::DocumentRef;
11-
pub(crate) use full_signed_doc::{FullSignedDoc, SELECT_SIGNED_DOCS_TEMPLATE, StoreError};
11+
pub(crate) use full_signed_doc::{FullSignedDoc, SELECT_SIGNED_DOCS_TEMPLATE};
1212
pub(crate) use query_filter::DocsQueryFilter;
1313
pub(crate) use signed_doc_body::{
1414
FILTERED_COUNT_SIGNED_DOCS_TEMPLATE, FILTERED_SELECT_SIGNED_DOCS_TEMPLATE, SignedDocBody,

catalyst-gateway/bin/src/db/event/signed_docs/signed_doc_body.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ impl SignedDocBody {
5353
}
5454

5555
/// Returns the document authors.
56+
#[allow(dead_code)]
5657
pub(crate) fn authors(&self) -> &Vec<String> {
5758
&self.authors
5859
}

catalyst-gateway/bin/src/db/event/signed_docs/tests/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,9 +94,9 @@ async fn store_full_signed_doc(
9494
doc: &FullSignedDoc,
9595
doc_type: uuid::Uuid,
9696
) {
97-
assert!(doc.store().await.unwrap());
97+
assert!(doc.store().await.is_ok());
9898
// try to insert the same data again
99-
assert!(!doc.store().await.unwrap());
99+
assert!(doc.store().await.is_err());
100100
// try another doc with the same `id` and `ver` and with different other fields
101101
let another_doc = FullSignedDoc::new(
102102
SignedDocBody::new(
Lines changed: 40 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,12 @@
11
//! Implementation of the PUT `/document` endpoint
22
3-
use std::{collections::HashSet, str::FromStr};
4-
5-
use catalyst_signed_doc::CatalystSignedDocument;
63
use poem_openapi::{ApiResponse, payload::Json};
74
use unprocessable_content_request::PutDocumentUnprocessableContent;
85

96
use super::common::{DocProvider, VerifyingKeyProvider};
107
use crate::{
118
db::{
12-
event::{
13-
error,
14-
signed_docs::{FullSignedDoc, SignedDocBody, StoreError},
15-
},
9+
event::{error::NotFoundError, signed_docs::FullSignedDoc},
1610
index::session::CassandraSessionError,
1711
},
1812
service::{
@@ -74,20 +68,43 @@ pub(crate) async fn endpoint(
7468
.into();
7569
};
7670

77-
// validate document signatures
78-
let verifying_key_provider =
79-
match VerifyingKeyProvider::try_new(&mut token, &doc.authors()).await {
80-
Ok(value) => value,
81-
Err(err) if err.is::<CassandraSessionError>() => {
82-
return AllResponses::service_unavailable(&err, RetryAfterOption::Default);
83-
},
84-
Err(err) => {
85-
return Responses::UnprocessableContent(Json(
86-
PutDocumentUnprocessableContent::new(&err, None),
87-
))
88-
.into();
89-
},
90-
};
71+
let db_doc = match FullSignedDoc::try_from(&doc) {
72+
Ok(doc) => doc,
73+
Err(err) => return AllResponses::handle_error(&err),
74+
};
75+
76+
// TODO: use the `ValidationProvider::try_get_doc` method and verify the returned
77+
// document cid values.
78+
// making sure that the document was already submitted before running validation,
79+
// otherwise validation would fail, because of the assumption that this document wasn't
80+
// published yet.
81+
match FullSignedDoc::retrieve(db_doc.id(), Some(db_doc.ver())).await {
82+
Ok(retrieved) if db_doc != retrieved => {
83+
return Responses::UnprocessableContent(Json(PutDocumentUnprocessableContent::new(
84+
"Document with the same `id` and `ver` already exists",
85+
Some(doc.problem_report()),
86+
)))
87+
.into();
88+
},
89+
Ok(_) => return Responses::NoContent.into(),
90+
Err(err) if err.is::<NotFoundError>() => (),
91+
Err(err) => return AllResponses::handle_error(&err),
92+
}
93+
94+
// validation provider
95+
let validation_provider = match VerifyingKeyProvider::try_new(&mut token, &doc.authors()).await
96+
{
97+
Ok(value) => ValidationProvider::new(DocProvider, value),
98+
Err(err) if err.is::<CassandraSessionError>() => {
99+
return AllResponses::service_unavailable(&err, RetryAfterOption::Default);
100+
},
101+
Err(err) => {
102+
return Responses::UnprocessableContent(Json(PutDocumentUnprocessableContent::new(
103+
&err, None,
104+
)))
105+
.into();
106+
},
107+
};
91108

92109
match doc.is_deprecated() {
93110
// apply older validation rule
@@ -113,8 +130,6 @@ pub(crate) async fn endpoint(
113130
},
114131
// apply newest validation rules
115132
Ok(false) => {
116-
let validation_provider = ValidationProvider::new(DocProvider, verifying_key_provider);
117-
118133
match catalyst_signed_doc::validator::validate(&doc, &validation_provider).await {
119134
Ok(true) => (),
120135
Ok(false) => {
@@ -143,88 +158,8 @@ pub(crate) async fn endpoint(
143158
.into();
144159
}
145160

146-
match validate_against_original_doc(&doc).await {
147-
Ok(true) => (),
148-
Ok(false) => {
149-
return Responses::UnprocessableContent(Json(
150-
PutDocumentUnprocessableContent::new("Failed validating document: catalyst-id or role does not match the current version.", None),
151-
))
152-
.into();
153-
},
154-
Err(err) => return AllResponses::handle_error(&err),
155-
}
156-
157-
// update the document storing in the db
158-
match store_document_in_db(&doc, doc_bytes).await {
159-
Ok(true) => Responses::Created.into(),
160-
Ok(false) => Responses::NoContent.into(),
161-
Err(err) if err.is::<StoreError>() => {
162-
Responses::UnprocessableContent(Json(PutDocumentUnprocessableContent::new(
163-
"Document with the same `id` and `ver` already exists",
164-
Some(doc.problem_report()),
165-
)))
166-
.into()
167-
},
161+
match db_doc.store().await {
162+
Ok(()) => Responses::Created.into(),
168163
Err(err) => AllResponses::handle_error(&err),
169164
}
170165
}
171-
172-
/// Fetch the latest version and ensure its catalyst-id match those in the newer version.
173-
async fn validate_against_original_doc(doc: &CatalystSignedDocument) -> anyhow::Result<bool> {
174-
let original_doc = match FullSignedDoc::retrieve(&doc.doc_id()?.uuid(), None).await {
175-
Ok(doc) => doc,
176-
Err(e) if e.is::<error::NotFoundError>() => return Ok(true),
177-
Err(e) => return Err(e),
178-
};
179-
180-
let original_authors = original_doc
181-
.body()
182-
.authors()
183-
.iter()
184-
.map(|author| catalyst_signed_doc::CatalystId::from_str(author))
185-
.collect::<Result<HashSet<_>, _>>()?;
186-
let authors: HashSet<_> = doc.authors().into_iter().collect();
187-
Ok(authors == original_authors)
188-
}
189-
190-
/// Store a provided and validated document inside the db.
191-
/// Returns `true` if its a new document.
192-
/// Returns `false` if the same document already exists.
193-
async fn store_document_in_db(
194-
doc: &catalyst_signed_doc::CatalystSignedDocument,
195-
doc_bytes: Vec<u8>,
196-
) -> anyhow::Result<bool> {
197-
let authors = doc
198-
.authors()
199-
.iter()
200-
.map(|v| v.as_short_id().to_string())
201-
.collect();
202-
203-
let doc_meta_json = doc.doc_meta().to_json()?;
204-
205-
let payload = if matches!(
206-
doc.doc_content_type(),
207-
Some(catalyst_signed_doc::ContentType::Json)
208-
) {
209-
match serde_json::from_slice(doc.decoded_content()?.as_slice()) {
210-
Ok(payload) => Some(payload),
211-
Err(e) => {
212-
anyhow::bail!("Invalid Document Content, not Json encoded: {e}");
213-
},
214-
}
215-
} else {
216-
None
217-
};
218-
219-
let doc_body = SignedDocBody::new(
220-
doc.doc_id()?.into(),
221-
doc.doc_ver()?.into(),
222-
doc.doc_type()?.uuid(),
223-
authors,
224-
Some(doc_meta_json),
225-
);
226-
227-
FullSignedDoc::new(doc_body, payload, doc_bytes)
228-
.store()
229-
.await
230-
}

catalyst-gateway/tests/api_tests/integration/signed_doc/test_signed_doc.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,7 @@ def test_document_put_and_get_endpoints(proposal_doc_factory, rbac_chain_factory
4848
data=new_doc_cbor,
4949
token=rbac_chain.auth_token(),
5050
)
51-
# TODO: fix it after fully integrating the latest changes of the 'catalyst-signed-doc' crate
52-
assert resp.status_code == 422, (
51+
assert resp.status_code == 204, (
5352
f"Failed to publish document: {resp.status_code} - {resp.text}"
5453
)
5554

0 commit comments

Comments
 (0)