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
46 changes: 43 additions & 3 deletions keyhive_core/src/keyhive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -915,6 +915,45 @@ where
Ok(true)
}

/// Make sure every certificate `op` cites is in `doc`'s closure and
/// record in [`CitedCertificates`] any that are not yet recorded there.
/// Returns `false` and doesn't record anything if a cited certificate is
/// not in the closure.
async fn record_closure_citations(
&self,
op: &CgkaOperation,
doc: Identifier,
) -> Result<bool, KeylineStorageError<K::Error>> {
let closure: HashMap<CitationId, Certificate<ContentHeads<T>>> = self
.closure(doc)
.await?
.into_iter()
.map(|cert| (citation_of(cert.id()), cert))
.collect();

let mut unrecorded = Vec::new();
{
let recorded = self.certificates.cited.lock();
for citation in op.citations() {
let Some(cert) = closure.get(citation) else {
return Ok(false);
};
if !recorded.contains(citation) {
unrecorded.push(cert);
}
}
}

// The store verified them on insert. Verifying again is the only way
// back to a `Verified` delegation, which authorization reads.
let verified: Vec<_> = unrecorded
.into_iter()
.filter_map(|cert| cert.clone().verify().ok())
.collect();
self.certificates.cited.lock().insert_multiple(&verified);
Ok(true)
}

/// The certificate `citation` refers to, if the Keyline store has it.
async fn stored_certificate(
&self,
Expand Down Expand Up @@ -2749,8 +2788,9 @@ where
/// Denied if a predecessor was already denied. Otherwise pending until every
/// predecessor is in the CGKA graph, then denied if its citations are not
/// in strictly ascending order, and otherwise pending until every cited
/// certificate is recorded. After that the verdict depends only on the
/// operation and its causal past, so it is final and deterministic.
/// certificate is recorded and in the document's closure. After that the
/// verdict depends only on the operation and its causal past, so it is
/// final and deterministic.
///
/// Conditions:
/// * Its signer has `Read` or higher in its signing set (i.e., the predecessor
Expand Down Expand Up @@ -2790,7 +2830,7 @@ where
};

let cited = op.citations();
if !self.record_citations(op).await? {
if !self.record_closure_citations(op, doc).await? {
return Ok(Err(Pending));
}

Expand Down
17 changes: 17 additions & 0 deletions keyhive_core/tests/cgka_authz.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1436,3 +1436,20 @@ async fn a_group_admin_can_remove_a_member_from_the_groups_documents() -> TestRe
);
Ok(())
}

#[tokio::test]
async fn an_operation_citing_a_certificate_outside_its_documents_closure_is_pending() -> TestResult
{
let mut ctx = TestContext::new().await;
let alice = ctx.individual("alice").await?;
let mallory = ctx.individual("mallory").await?;
let doc = ctx.doc(&alice, "doc").await?;
let other = ctx.doc(&alice, "other").await?;
alice.add_member(mallory.id(), doc, Power::Read).await?;
let elsewhere = delegation_citation(&alice, kl(other), kl(alice.id())).await?;

let op = remove_op(&alice, doc, BTreeSet::from([elsewhere])).await;
let result = alice.receive_cgka_op(mallory.sign_cgka_op(op).await?).await;
assert!(is_pending(&result), "{result:?}");
Ok(())
}
Loading