fetch: prevent missing default branch
If a delegate is missing the default branch for a project certain computations will fail, e.g. getting the canonical mainline branch. One prevention for corrupting data is refusing to fetch a delegate that does not have the default branch, when the repository is a project. It is assumed that the repository is not a project if calling `Doc::project` returns an error, since the error variants are not found and parsing errors. This is only checked in the case of a delegate since non-delegates are safe to created COBs that don't require the default branch, e.g. creating an issue.
This commit is contained in:
parent
9341d51d05
commit
511165bfc5
|
|
@ -7,7 +7,7 @@ use radicle::identity::{Doc, DocError};
|
||||||
|
|
||||||
use radicle::prelude::Verified;
|
use radicle::prelude::Verified;
|
||||||
use radicle::storage;
|
use radicle::storage;
|
||||||
use radicle::storage::refs::RefsAt;
|
use radicle::storage::refs::{RefsAt, SignedRefs};
|
||||||
use radicle::storage::{
|
use radicle::storage::{
|
||||||
git::Validation, Remote, RemoteId, RemoteRepository, Remotes, ValidateRepository, Validations,
|
git::Validation, Remote, RemoteId, RemoteRepository, Remotes, ValidateRepository, Validations,
|
||||||
};
|
};
|
||||||
|
|
@ -497,10 +497,19 @@ impl FetchState {
|
||||||
}
|
}
|
||||||
|
|
||||||
let cache = self.as_cached(handle);
|
let cache = self.as_cached(handle);
|
||||||
if let Some(fails) = sigrefs::validate(&cache, sigrefs)?.as_mut() {
|
// N.b. we only validate the existence of the
|
||||||
|
// default branch for delegates, since it safe for
|
||||||
|
// non-delegates to not have this branch.
|
||||||
|
let branch_validation =
|
||||||
|
validate_project_default_branch(&anchor, &sigrefs.sigrefs);
|
||||||
|
let fails = sigrefs::validate(&cache, sigrefs)?.map(|mut fails| {
|
||||||
|
fails.extend(branch_validation);
|
||||||
|
fails
|
||||||
|
});
|
||||||
|
if let Some(mut fails) = fails {
|
||||||
log::warn!(target: "fetch", "Pruning delegate {remote} tips, due to validation failures");
|
log::warn!(target: "fetch", "Pruning delegate {remote} tips, due to validation failures");
|
||||||
self.prune(&remote);
|
self.prune(&remote);
|
||||||
failures.append(fails)
|
failures.append(&mut fails)
|
||||||
} else {
|
} else {
|
||||||
remotes.insert(remote);
|
remotes.insert(remote);
|
||||||
}
|
}
|
||||||
|
|
@ -648,3 +657,20 @@ impl<'a, S> ValidateRepository for Cached<'a, S> {
|
||||||
Ok(validations)
|
Ok(validations)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// If the repository has a project payload, in `anchor`, then
|
||||||
|
/// validate that the `sigrefs` contains the listed default branch.
|
||||||
|
///
|
||||||
|
/// N.b. if the repository does not have the project payload or a
|
||||||
|
/// deserialization error occurs, then this will return `None`.
|
||||||
|
fn validate_project_default_branch(
|
||||||
|
anchor: &Doc<Verified>,
|
||||||
|
sigrefs: &SignedRefs<Verified>,
|
||||||
|
) -> Option<Validation> {
|
||||||
|
let proj = anchor.project().ok()?;
|
||||||
|
let branch = radicle::git::refs::branch(proj.default_branch()).to_ref_string();
|
||||||
|
(!sigrefs.contains_key(&branch)).then_some(Validation::MissingRef {
|
||||||
|
remote: sigrefs.id,
|
||||||
|
refname: branch,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,8 @@ use radicle::crypto::{test::signer::MockSigner, Signer};
|
||||||
use radicle::git;
|
use radicle::git;
|
||||||
use radicle::node::{Alias, FetchResult, Handle as _, DEFAULT_TIMEOUT};
|
use radicle::node::{Alias, FetchResult, Handle as _, DEFAULT_TIMEOUT};
|
||||||
use radicle::storage::{
|
use radicle::storage::{
|
||||||
ReadRepository, ReadStorage, RefUpdate, RemoteRepository, ValidateRepository, WriteRepository,
|
ReadRepository, ReadStorage, RefUpdate, RemoteRepository, SignRepository, ValidateRepository,
|
||||||
WriteStorage,
|
WriteRepository, WriteStorage,
|
||||||
};
|
};
|
||||||
use radicle::test::fixtures;
|
use radicle::test::fixtures;
|
||||||
use radicle::{assert_matches, rad};
|
use radicle::{assert_matches, rad};
|
||||||
|
|
@ -1039,3 +1039,55 @@ fn test_outdated_delegate_sigrefs() {
|
||||||
assert_ne!(alice_refs, old_refs);
|
assert_ne!(alice_refs, old_refs);
|
||||||
assert_eq!(alice_refs_expected, alice_refs);
|
assert_eq!(alice_refs_expected, alice_refs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn missing_default_branch() {
|
||||||
|
logger::init(log::Level::Debug);
|
||||||
|
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
|
||||||
|
let mut alice = Node::init(tmp.path(), Config::test(Alias::new("alice")));
|
||||||
|
let bob = Node::init(tmp.path(), Config::test(Alias::new("bob")));
|
||||||
|
|
||||||
|
let rid = alice.project("acme", "");
|
||||||
|
|
||||||
|
let mut alice = alice.spawn();
|
||||||
|
let mut bob = bob.spawn();
|
||||||
|
|
||||||
|
alice.handle.track_repo(rid, Scope::All).unwrap();
|
||||||
|
bob.handle.track_repo(rid, Scope::All).unwrap();
|
||||||
|
alice.connect(&bob);
|
||||||
|
converge([&alice, &bob]);
|
||||||
|
|
||||||
|
bob.handle.fetch(rid, alice.id, DEFAULT_TIMEOUT).unwrap();
|
||||||
|
assert!(bob.storage.contains(&rid).unwrap());
|
||||||
|
|
||||||
|
// Fetching from still works despite not having
|
||||||
|
// `refs/heads/master`, but has `rad/sigrefs`.
|
||||||
|
bob.issue(rid, "Hello, Acme", "Popping in to say hello");
|
||||||
|
alice.handle.fetch(rid, bob.id, DEFAULT_TIMEOUT).unwrap();
|
||||||
|
|
||||||
|
{
|
||||||
|
let repo = bob.storage.repository(rid).unwrap();
|
||||||
|
assert!(repo.canonical_head().is_ok());
|
||||||
|
assert!(repo.canonical_identity_doc().is_ok());
|
||||||
|
assert!(repo.head().is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
// If for some reason Alice managed to delete her master reference
|
||||||
|
{
|
||||||
|
let repo = alice.storage.repository_mut(rid).unwrap();
|
||||||
|
let mut r = repo
|
||||||
|
.backend
|
||||||
|
.find_reference(&format!("refs/namespaces/{}/refs/heads/master", alice.id))
|
||||||
|
.unwrap();
|
||||||
|
r.delete().unwrap();
|
||||||
|
repo.sign_refs(&alice.signer).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then fetching from her will fail
|
||||||
|
assert_matches!(
|
||||||
|
bob.handle.fetch(rid, alice.id, DEFAULT_TIMEOUT).unwrap(),
|
||||||
|
FetchResult::Failed { .. }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue