radicle/cob/identity: Guard `IdentityMut::update` against sibling proposals
Ensure that the `IdentityMut::update` API returns an error when it encounters an active, sibling revision by the same delegate.
This commit is contained in:
parent
b6f07074d9
commit
a63f91885f
|
|
@ -1226,13 +1226,29 @@ where
|
|||
}
|
||||
|
||||
/// Update the identity by proposing a new revision.
|
||||
/// If the signer is the only delegate, the revision is accepted automatically.
|
||||
///
|
||||
/// If the signer is the only delegate, the revision is adopted automatically.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// [`SiblingAccepted`]: If the delegate has already accepted an active
|
||||
/// revision that shares the same parent as the provided `revision`.
|
||||
///
|
||||
/// [`SiblingAccepted`]: ApplyError::SiblingAccepted
|
||||
pub fn update(
|
||||
&mut self,
|
||||
title: cob::Title,
|
||||
description: impl ToString,
|
||||
doc: &Doc,
|
||||
) -> Result<RevisionId, Error> {
|
||||
#[allow(deprecated)]
|
||||
let did: Did = self.store.signer().verifying_key().into();
|
||||
if let Some(revision_id) = self.has_active_sibling_accept(&self.current, &did) {
|
||||
return Err(Error::Apply(ApplyError::SiblingAccepted {
|
||||
revision: revision_id,
|
||||
}));
|
||||
}
|
||||
|
||||
let parent = Some(self.current);
|
||||
|
||||
#[allow(deprecated)]
|
||||
|
|
@ -1440,6 +1456,10 @@ mod test {
|
|||
let r2 = bob_identity.revision(&r2).unwrap();
|
||||
assert_eq!(r2.state, State::Rejected(RejectedBy::Vote));
|
||||
|
||||
// Reload so Alice sees r2 is rejected (no longer Active).
|
||||
// This allows her to propose a new sibling.
|
||||
identity.reload().unwrap();
|
||||
|
||||
// Now let's add another delegate.
|
||||
doc.delegate(eve.public_key().into());
|
||||
let r3 = identity
|
||||
|
|
@ -3168,4 +3188,53 @@ mod test {
|
|||
"Expected SiblingAccepted error, got: {err:?}"
|
||||
);
|
||||
}
|
||||
|
||||
/// The `update()` API rejects a second proposal if the delegate already
|
||||
/// has an accept on an Active sibling (child of current).
|
||||
#[test]
|
||||
fn update_rejects_second_sibling_proposal() {
|
||||
let network = Network::default();
|
||||
let alice = &network.alice;
|
||||
let bob = &network.bob;
|
||||
|
||||
let mut alice_identity = Identity::load_mut(&*alice.repo, &alice.signer).unwrap();
|
||||
let mut alice_doc = alice_identity.doc().clone().edit();
|
||||
alice_doc.delegate(bob.signer.public_key().into());
|
||||
let _a1 = alice_identity
|
||||
.update(
|
||||
cob::Title::new("Add Bob").unwrap(),
|
||||
"",
|
||||
&alice_doc.verified().unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Alice proposes first child.
|
||||
let mut doc1 = alice_identity.doc().clone().edit();
|
||||
doc1.visibility = Visibility::private([]);
|
||||
let _child1 = alice_identity
|
||||
.update(
|
||||
cob::Title::new("Child 1").unwrap(),
|
||||
"",
|
||||
&doc1.verified().unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Alice tries to propose a second child (sibling) — should fail.
|
||||
let mut doc2 = alice_identity.doc().clone().edit();
|
||||
doc2.visibility = Visibility::private([alice.signer.public_key().into()]);
|
||||
let err = alice_identity
|
||||
.update(
|
||||
cob::Title::new("Child 2").unwrap(),
|
||||
"",
|
||||
&doc2.verified().unwrap(),
|
||||
)
|
||||
.unwrap_err();
|
||||
|
||||
assert!(
|
||||
std::iter::successors(Some(&err as &dyn std::error::Error), |e| e.source())
|
||||
.filter_map(|e| e.downcast_ref::<ApplyError>())
|
||||
.any(|e| matches!(e, ApplyError::SiblingAccepted { .. })),
|
||||
"Expected SiblingAccepted error, got: {err:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -223,7 +223,14 @@ impl Harness {
|
|||
Ok(rev) => self.revisions.push(rev),
|
||||
Err(e)
|
||||
if Self::has_apply_error(&e, |err| {
|
||||
matches!(err, cob::identity::ApplyError::DocUnchanged)
|
||||
matches!(
|
||||
err,
|
||||
cob::identity::ApplyError::DocUnchanged
|
||||
// An `actor` for the above `update` could attempt
|
||||
// to create active, sibling revisions which is
|
||||
// disallowed.
|
||||
| cob::identity::ApplyError::SiblingAccepted { .. }
|
||||
)
|
||||
}) => {}
|
||||
Err(e) => {
|
||||
panic!("Delegate action should succeed, but failed with: {:?}", e)
|
||||
|
|
|
|||
Loading…
Reference in New Issue