radicle-cli: more helpful error for non-delegate updates
When a non-delegate attempts to update the identity document, their action will be rejected. Provide a better error, along with a hint, to the non-delegate.
This commit is contained in:
parent
60959f7e83
commit
b937a93892
|
|
@ -15,6 +15,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
`tags`, `notes`, `rad`, and `cobs`. The restriction is lifted, and the only
|
`tags`, `notes`, `rad`, and `cobs`. The restriction is lifted, and the only
|
||||||
references filtered out are `refs/tmp/heads` – used by `radicle-remote-helper`
|
references filtered out are `refs/tmp/heads` – used by `radicle-remote-helper`
|
||||||
to create temporary patches.
|
to create temporary patches.
|
||||||
|
- The `rad id` command will provide a better error message when a non-delegate
|
||||||
|
attempts to modify the identity document.
|
||||||
|
|
||||||
## Fixed Bugs
|
## Fixed Bugs
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
Alice has created a new repository `rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji` with only herself as the sole delegate. After Bob has cloned it, let's ensure he can't add himself as a delegate too:
|
||||||
|
|
||||||
|
``` ~bob (fail)
|
||||||
|
$ rad id update --repo rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --title "Add myself!" --delegate did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk --no-confirm
|
||||||
|
✗ Error: did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk is not a delegate, and only delegates are allowed to create a revision
|
||||||
|
✗ Hint: bob (you) is attempting to modify the identity document but is not a delegate!
|
||||||
|
```
|
||||||
|
|
||||||
|
|
@ -19,6 +19,7 @@ use crate::git::unified_diff::Encode as _;
|
||||||
use crate::git::Rev;
|
use crate::git::Rev;
|
||||||
use crate::terminal as term;
|
use crate::terminal as term;
|
||||||
use crate::terminal::args::Error;
|
use crate::terminal::args::Error;
|
||||||
|
use crate::terminal::format::Author;
|
||||||
use crate::terminal::patch::Message;
|
use crate::terminal::patch::Message;
|
||||||
|
|
||||||
pub use args::Args;
|
pub use args::Args;
|
||||||
|
|
@ -193,7 +194,14 @@ pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
let signer = term::signer(&profile)?;
|
let signer = term::signer(&profile)?;
|
||||||
let revision = update(title, description, proposal, &mut identity, &signer)?;
|
let revision = update(
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
proposal,
|
||||||
|
&mut identity,
|
||||||
|
&signer,
|
||||||
|
&profile,
|
||||||
|
)?;
|
||||||
|
|
||||||
if revision.is_accepted() && revision.parent == Some(current.id) {
|
if revision.is_accepted() && revision.parent == Some(current.id) {
|
||||||
// Update the canonical head to point to the latest accepted revision.
|
// Update the canonical head to point to the latest accepted revision.
|
||||||
|
|
@ -420,13 +428,16 @@ fn update<R, G>(
|
||||||
doc: Doc,
|
doc: Doc,
|
||||||
current: &mut IdentityMut<R>,
|
current: &mut IdentityMut<R>,
|
||||||
signer: &Device<G>,
|
signer: &Device<G>,
|
||||||
|
profile: &Profile,
|
||||||
) -> anyhow::Result<Revision>
|
) -> anyhow::Result<Revision>
|
||||||
where
|
where
|
||||||
R: WriteRepository + cob::Store<Namespace = NodeId>,
|
R: WriteRepository + cob::Store<Namespace = NodeId>,
|
||||||
G: crypto::signature::Signer<crypto::Signature>,
|
G: crypto::signature::Signer<crypto::Signature>,
|
||||||
{
|
{
|
||||||
if let Some((title, description)) = edit_title_description(title, description)? {
|
if let Some((title, description)) = edit_title_description(title, description)? {
|
||||||
let id = current.update(title, description, &doc, signer)?;
|
let id = current
|
||||||
|
.update(title, description, &doc, signer)
|
||||||
|
.map_err(|e| on_identity_err(e, profile))?;
|
||||||
let revision = current
|
let revision = current
|
||||||
.revision(&id)
|
.revision(&id)
|
||||||
.ok_or(anyhow!("update failed: revision {id} is missing"))?;
|
.ok_or(anyhow!("update failed: revision {id} is missing"))?;
|
||||||
|
|
@ -437,6 +448,46 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn on_identity_err(e: identity::Error, profile: &Profile) -> anyhow::Error {
|
||||||
|
let e = anyhow::Error::from(e);
|
||||||
|
|
||||||
|
e.chain()
|
||||||
|
.find_map(|c| c.downcast_ref::<identity::ApplyError>())
|
||||||
|
.map(|e| on_apply_err(e, profile))
|
||||||
|
.unwrap_or(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_apply_err(e: &identity::ApplyError, profile: &Profile) -> anyhow::Error {
|
||||||
|
match e {
|
||||||
|
e @ identity::ApplyError::NonDelegateUnauthorized { author, .. } => {
|
||||||
|
let nid = NodeId::from(*author);
|
||||||
|
let labels = Author::new(&nid, profile, false).labels();
|
||||||
|
|
||||||
|
Error::with_hint(
|
||||||
|
anyhow!(e.to_string()),
|
||||||
|
format!(
|
||||||
|
"{} {} is attempting to modify the identity document but is not a delegate!",
|
||||||
|
labels.0, labels.1
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
e @ radicle::cob::identity::ApplyError::Missing(_)
|
||||||
|
| e @ radicle::cob::identity::ApplyError::Init(_)
|
||||||
|
| e @ radicle::cob::identity::ApplyError::InvalidSignature(..)
|
||||||
|
| e @ radicle::cob::identity::ApplyError::NotAuthorized
|
||||||
|
| e @ radicle::cob::identity::ApplyError::MissingParent
|
||||||
|
| e @ radicle::cob::identity::ApplyError::DuplicateVerdict
|
||||||
|
| e @ radicle::cob::identity::ApplyError::UnexpectedState
|
||||||
|
| e @ radicle::cob::identity::ApplyError::Redacted
|
||||||
|
| e @ radicle::cob::identity::ApplyError::DocUnchanged
|
||||||
|
| e @ radicle::cob::identity::ApplyError::Git(_)
|
||||||
|
| e @ radicle::cob::identity::ApplyError::Doc(_) => {
|
||||||
|
anyhow!(e.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn print_diff(
|
fn print_diff(
|
||||||
previous: Option<&RevisionId>,
|
previous: Option<&RevisionId>,
|
||||||
current: &RevisionId,
|
current: &RevisionId,
|
||||||
|
|
|
||||||
|
|
@ -602,6 +602,56 @@ fn rad_id_multi_delegate() {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rad_id_unauthorized_delegate() {
|
||||||
|
let mut environment = Environment::new();
|
||||||
|
let alice = environment.node("alice");
|
||||||
|
let bob = environment.node("bob");
|
||||||
|
let acme = RepoId::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap();
|
||||||
|
|
||||||
|
environment.repository(&alice);
|
||||||
|
|
||||||
|
test(
|
||||||
|
"examples/rad-init.md",
|
||||||
|
environment.work(&alice),
|
||||||
|
Some(&alice.home),
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let mut alice = alice.spawn();
|
||||||
|
let mut bob = bob.spawn();
|
||||||
|
|
||||||
|
// Alice sets up the seed
|
||||||
|
alice.handle.seed(acme, Scope::Followed).unwrap();
|
||||||
|
|
||||||
|
bob.connect(&alice).converge([&alice]);
|
||||||
|
bob.rad(
|
||||||
|
"clone",
|
||||||
|
&[acme.to_string().as_str()],
|
||||||
|
environment.work(&bob),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
formula(
|
||||||
|
&environment.tempdir(),
|
||||||
|
"examples/rad-id-unauthorized-delegate.md",
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
.home(
|
||||||
|
"alice",
|
||||||
|
environment.work(&alice),
|
||||||
|
[("RAD_HOME", alice.home.path().display())],
|
||||||
|
)
|
||||||
|
.home(
|
||||||
|
"bob",
|
||||||
|
environment.work(&bob),
|
||||||
|
[("RAD_HOME", bob.home.path().display())],
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[ignore = "slow"]
|
#[ignore = "slow"]
|
||||||
fn rad_id_collaboration() {
|
fn rad_id_collaboration() {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue