node: Delete unsigned refs on fetch

This commit is contained in:
Alexis Sellier 2023-04-18 16:11:38 +02:00
parent e13bf0b75b
commit 2598886018
No known key found for this signature in database
5 changed files with 58 additions and 26 deletions

View File

@ -205,13 +205,16 @@ fn test_replication_no_delegates() {
let tmp = tempfile::tempdir().unwrap();
let alice = Node::init(tmp.path());
let mut bob = Node::init(tmp.path());
let (repo, _) = fixtures::repository(tmp.path().join("acme"));
let acme = bob.project_from("acme", "", &repo);
// Populate repo, but don't sign the refs.
let branches = fixtures::populate(&repo, 1);
git::push(&repo, "rad", branches.iter().map(|b| (b, b))).unwrap();
let acme = bob.project("acme", "");
// Delete one of the signed refs.
bob.storage
.repository_mut(acme)
.unwrap()
.reference(&bob.id, &git::qualified!("refs/heads/master"))
.unwrap()
.delete()
.unwrap();
let mut alice = alice.spawn(service::Config::default());
let bob = bob.spawn(service::Config::default());

View File

@ -88,6 +88,8 @@ enum VerifiedRemote {
// Nb. unused but we want to ensure that we verify the identity
_doc: Doc<Verified>,
remote: Remote<Verified>,
/// Unsigned refs.
unsigned: Vec<git::RefString>,
},
}
@ -251,6 +253,7 @@ impl<'a> StagingPhaseFinal<'a> {
let url = url::File::new(self.repo.path().to_path_buf()).to_string();
let mut remote = production.backend.remote_anonymous(&url)?;
let mut updates = Vec::new();
let mut delete = HashSet::new();
let callbacks = ref_updates(&mut updates);
let remotes = {
@ -266,10 +269,15 @@ impl<'a> StagingPhaseFinal<'a> {
);
vec![]
}
VerifiedRemote::Success { remote, .. } => {
VerifiedRemote::Success {
remote, unsigned, ..
} => {
let ns = remote.id.to_namespace();
let mut refspecs = vec![];
// Unsigned refs should be deleted.
delete.insert((remote.id, unsigned));
// First add the standard git refs.
let heads = ns.join(git::refname!("refs/heads"));
let cobs = ns.join(git::refname!("refs/cobs"));
@ -338,8 +346,22 @@ impl<'a> StagingPhaseFinal<'a> {
// should be devised.
opts.prune(git::raw::FetchPrune::Off);
// Fetch into production copy.
remote.fetch(&specs, Some(&mut opts), None)?;
// Delete unsigned refs.
for (namespace, unsigned) in delete {
for refstr in unsigned {
let q = git::Qualified::from_refstr(&refstr)
.expect("StagingPhaseFinal::transfer: unsigned references are qualified");
if let Ok(mut r) = production.reference(&namespace, &q) {
log::debug!(target: "worker", "Deleting unsigned ref {namespace}/{q}..");
r.delete()?;
}
}
}
fetching
};
let head = production.set_head()?;
@ -363,7 +385,11 @@ impl<'a> StagingPhaseFinal<'a> {
let remote_id = remote.id;
let verification = match self.repo.identity_doc_of(&remote_id) {
Ok(doc) => match self.repo.validate_remote(&remote) {
Ok(()) => VerifiedRemote::Success { _doc: doc, remote },
Ok(unsigned) => VerifiedRemote::Success {
_doc: doc,
remote,
unsigned,
},
Err(e) => VerifiedRemote::Failed {
reason: e.to_string(),
},

View File

@ -304,10 +304,11 @@ pub trait ReadRepository {
Ok(())
}
/// Validate all the remote's references in the repository, checking that they are signed as
/// part of the remote's signed refs. Also verify that no signed reference is missing from the
/// repository.
fn validate_remote(&self, remote: &Remote<Verified>) -> Result<(), VerifyError>;
/// Validates a remote's signed refs and identity.
///
/// Returns any ref found under that remote that isn't signed.
/// If a signed ref is missing from the repository, an error is returned.
fn validate_remote(&self, remote: &Remote<Verified>) -> Result<Vec<RefString>, VerifyError>;
/// Get the head of this repository.
///

View File

@ -202,8 +202,6 @@ pub enum VerifyError {
InvalidIdentity(#[from] IdentityError),
#[error("refs error: {0}")]
Refs(#[from] refs::Error),
#[error("unknown reference `{1}` in remote `{0}`")]
UnknownRef(RemoteId, git::RefString),
#[error("missing reference `{1}` in remote `{0}`")]
MissingRef(RemoteId, git::RefString),
#[error("git: {0}")]
@ -371,9 +369,10 @@ impl ReadRepository for Repository {
.get(&self.backend)
}
fn validate_remote(&self, remote: &Remote<Verified>) -> Result<(), VerifyError> {
fn validate_remote(&self, remote: &Remote<Verified>) -> Result<Vec<RefString>, VerifyError> {
// Contains a copy of the signed refs of this remote.
let mut refs = BTreeMap::from((*remote.refs).clone());
let mut signed = BTreeMap::from((*remote.refs).clone());
let mut unsigned = Vec::new();
// Check all repository references, making sure they are present in the signed refs map.
for (refname, oid) in self.references_of(&remote.id)? {
@ -381,24 +380,24 @@ impl ReadRepository for Repository {
if refname == refs::SIGREFS_BRANCH.to_ref_string() {
continue;
}
let signed_oid = refs
.remove(&refname)
.ok_or_else(|| VerifyError::UnknownRef(remote.id, refname.clone()))?;
if oid != signed_oid {
return Err(VerifyError::InvalidRefTarget(remote.id, refname, *oid));
if let Some(signed_oid) = signed.remove(&refname) {
if oid != signed_oid {
return Err(VerifyError::InvalidRefTarget(remote.id, refname, *oid));
}
} else {
unsigned.push(refname);
}
}
// The refs that are left in the map, are ones that were signed, but are not
// in the repository. If any are left, bail.
if let Some((name, _)) = refs.into_iter().next() {
if let Some((name, _)) = signed.into_iter().next() {
return Err(VerifyError::MissingRef(remote.id, name));
}
// Finally, verify the identity history of remote.
self.identity_of(&remote.id)?.verified(self.id)?;
Ok(())
Ok(unsigned)
}
fn reference(

View File

@ -140,8 +140,11 @@ impl ReadRepository for MockRepository {
todo!()
}
fn validate_remote(&self, _remote: &Remote<Verified>) -> Result<(), VerifyError> {
Ok(())
fn validate_remote(
&self,
_remote: &Remote<Verified>,
) -> Result<Vec<fmt::RefString>, VerifyError> {
Ok(vec![])
}
fn path(&self) -> &std::path::Path {