use std::{
collections::{BTreeMap, btree_map::Entry},
io,
path::{Path, PathBuf},
sync::OnceLock,
};
use anyhow::Context;
use git2::Repository;
use sequoia_openpgp as openpgp;
use openpgp::{
Cert,
Fingerprint,
KeyHandle,
armor,
serialize::Marshal,
};
use sequoia_cert_store as cert_store;
use cert_store::Store;
use crate::Config;
use crate::Error;
use crate::Policy;
use crate::Result;
use crate::cli::PolicySubcommand;
use crate::git_repo;
use crate::output;
const TRACE: bool = false;
pub fn dispatch(config: &Config, command: PolicySubcommand) -> Result<()> {
let git_ = OnceLock::new();
let git = || -> Result<&Repository, Error> {
if let Some(repo) = git_.get() {
return Ok(repo);
}
let repo = git_repo()?;
let _ = git_.set(repo);
Ok(git_.get().unwrap())
};
let git_policy = |git: &Repository, name: &str| -> Result<Policy> {
let (object, reference) = git.revparse_ext(name)
.with_context(|| {
format!("Looking up {:?}.", name)
})?;
let commit = if let Some(reference) = reference {
let commit = reference.peel_to_commit()
.with_context(|| {
format!("{:?} is not a commit", name)
})?;
commit.id()
} else {
if let Ok(commit) = object.into_commit() {
commit.id()
} else {
return Err(anyhow::anyhow!("{:?} is not a commit", name));
}
};
match Policy::read_from_commit(git, &commit) {
Ok(policy) => Ok(policy),
Err(err) => {
if let Error::MissingPolicy(_) = err {
eprintln!("Warning {} does not have a policy file. \
Defaulting to an empty policy.",
name);
Ok(Policy::default())
} else {
Err(err.into())
}
}
}
};
match command {
PolicySubcommand::Describe {
policy_file,
commit,
} => {
let p = if let Some(commit) = commit {
git_policy(git()?, &commit)?
} else {
config.read_policy(&policy_file)?
};
match config.output_format {
output::Format::HumanReadable => {
output::describe_policy(&p)?;
},
output::Format::Json =>
serde_json::to_writer_pretty(io::stdout(), &p)?,
}
},
PolicySubcommand::Diff {
old, old_commit, old_file, new, new_commit, new_file
} => {
tracer!(TRACE, "sq-git policy diff");
let r = |commit: Option<&str>, file: Option<&Path>|
-> Result<_>
{
let mut policy = None;
let mut err_commit = None;
if let Some(commit) = commit {
match git().and_then(|git| Ok(git_policy(git, &commit)?)) {
Ok(p) => {
t!("Read policy from commit {:?}", commit);
policy = Some(p)
},
Err(err) => {
t!("Error reading policy from commit {:?}: {}",
commit, err);
err_commit = Some(err);
}
}
}
let mut err_file = None;
if policy.is_none() {
if let Some(file) = file {
match Policy::read_file(&file) {
Ok(p) => {
t!("Read policy from file {}", file.display());
policy = Some(p);
}
Err(err) => {
t!("Error reading policy from file {:?}: {}",
file.display(), err);
err_file = Some(err)
}
}
}
}
match policy {
Some(policy) => Ok(policy),
None => {
if let (Some(commit), Some(err_commit))
= (commit, err_commit)
{
eprintln!("Reading commit {}: {}", commit, err_commit);
}
if let (Some(file), Some(err_file)) = (file, err_file)
{
eprintln!("Reading file {}: {}",
file.display(), err_file);
}
Err(anyhow::anyhow!("Failed to read policy"))
}
}
};
let old_commit = old_commit.as_deref().or(old.as_deref());
let old_as_path = old.as_deref().map(|p| PathBuf::from(p));
let old_file = old_file.as_deref().or(old_as_path.as_deref());
let old_policy = if old_commit.is_some() || old_file.is_some() {
r(old_commit, old_file)?
} else {
t!("Reading old policy from HEAD");
git_policy(git()?, "HEAD")?
};
let new_commit = new_commit.as_deref().or(new.as_deref());
let new_as_path = new.as_deref().map(|p| PathBuf::from(p));
let new_file = new_file.as_deref().or(new_as_path.as_deref());
let new_policy = if new_commit.is_some() || new_file.is_some() {
r(new_commit, new_file)?
} else {
t!("Reading new policy from git working tree");
Policy::read_from_working_dir()?
};
let diff = old_policy.diff(&new_policy)?;
match config.output_format {
output::Format::HumanReadable => {
output::describe_diff(&diff)?;
},
output::Format::Json =>
serde_json::to_writer_pretty(io::stdout(), &diff)?,
}
if ! diff.changes.is_empty() {
std::process::exit(1);
}
},
PolicySubcommand::Export {
policy_file,
commit,
name,
all,
} => {
let p = if let Some(commit) = commit {
git_policy(git()?, &commit)?
} else {
config.read_policy(&policy_file)?
};
let mut certs: BTreeMap<Fingerprint, Cert> = BTreeMap::new();
let mut merge = |cert: Cert| -> Result<()> {
match certs.entry(cert.fingerprint()) {
Entry::Occupied(oe) => {
let oe = oe.into_mut();
*oe = (*oe).clone().merge_public_and_secret(cert)
.context("Merging certificates")?;
}
Entry::Vacant(ve) => {
ve.insert(cert);
}
}
Ok(())
};
if all {
for (entity, a) in p.authorization().iter() {
for cert in a.certs()? {
let cert = cert.with_context(|| {
format!("Parsing {}'s keyring", entity)
})?;
let fpr = cert.fingerprint();
merge(Cert::try_from(cert).with_context(|| {
format!("Parsing {}'s keyring: {} is corrupted",
entity, fpr)
})?)?;
}
}
} else if let Some(name) = name {
if let Some(auth) = p.authorization().get(&name) {
for cert in auth.certs()? {
let cert = cert.with_context(|| {
format!("Parsing {}'s keyring", name)
})?;
let fpr = cert.fingerprint();
merge(Cert::try_from(cert).with_context(|| {
format!("Parsing {}'s keyring: {} is corrupted",
name, fpr)
})?)?;
}
} else {
eprintln!("Entity {:?} is not known.", name);
eprintln!("Known entities");
for name in p.authorization().keys() {
eprintln!(" - {}", name);
}
return Err(anyhow::anyhow!("Unknown entity"));
}
} else {
unreachable!("enforced by clap");
}
let stdout = std::io::stdout();
let mut output
= armor::Writer::new(stdout, armor::Kind::PublicKey)?;
for cert in certs.into_values() {
cert.serialize(&mut output)?;
}
output.finalize()?;
},
PolicySubcommand::Authorize {
policy_file,
name, cert,
sign_commit, no_sign_commit,
sign_tag, no_sign_tag,
sign_archive, no_sign_archive,
add_user, no_add_user,
retire_user, no_retire_user,
audit, no_audit,
project_maintainer: _,
release_manager: _,
committer: _,
} => {
let cert = cert.get(&config)?;
let fp = cert.fingerprint();
let old_policy = config.read_policy_or_default(&policy_file)?;
let mut p = old_policy.clone();
let (new_entry, a) = match p.authorization_mut().entry(name) {
Entry::Occupied(oe) => (false, oe.into_mut()),
Entry::Vacant(ve) => (true, ve.insert(Default::default())),
};
if new_entry
&& (! sign_commit && ! sign_tag && ! sign_archive
&& ! add_user && ! retire_user && ! audit)
{
eprintln!("Warning: Adding new entry with no capabilities. \
You probably want to add some capabilities by \
running the command again, and specifying \
\"--committer\", \"--release-manager\", \
or \"--project-maintainer\". Refer to the \
help for details.\"");
}
let mut merged = false;
let mut updated = Vec::new();
for c in a.certs()? {
let mut c = Cert::try_from(c?)?;
if c.fingerprint() == fp {
c = c.merge_public(cert.clone())?;
merged = true;
}
updated.push(c);
}
if ! merged {
updated.push(cert);
}
a.set_certs(updated)?;
a.sign_commit =
(a.sign_commit | sign_commit) & !no_sign_commit;
a.sign_tag =
(a.sign_tag | sign_tag) & !no_sign_tag;
a.sign_archive =
(a.sign_archive | sign_archive) & !no_sign_archive;
a.add_user =
(a.add_user | add_user) & !no_add_user;
a.retire_user =
(a.retire_user | retire_user) & !no_retire_user;
a.audit =
(a.audit | audit) & !no_audit;
let diff = old_policy.diff(&p)?;
match config.output_format {
output::Format::HumanReadable => {
output::describe_diff(&diff)?;
},
output::Format::Json =>
serde_json::to_writer_pretty(io::stdout(), &diff)?,
}
config.write_policy(&p, &policy_file)?;
},
PolicySubcommand::Sync {
policy_file,
keyserver: keyservers,
disable_keyservers,
} => {
let old_policy = config.read_policy(&policy_file)?;
let mut p = old_policy.clone();
let mut keyserver = keyservers
.into_iter()
.map(|keyserver| {
sequoia_net::KeyServer::new(&keyserver)
.map(|instance| {
(keyserver, instance)
})
})
.collect::<sequoia_net::Result<Vec<_>>>()?;
let cert_update = |cert: Cert, update: Cert| -> (Cert, bool) {
if cert.fingerprint() != update.fingerprint() {
eprintln!("bad server response, \
wrong certificate ({}).",
update.fingerprint());
(cert, false)
} else {
match cert.clone().insert_packets(update.into_packets()) {
Ok((cert, changed)) => {
if changed {
eprintln!("updated.");
} else {
eprintln!("unchanged.");
}
(cert, changed)
}
Err(err) => {
eprintln!("{}", err);
(cert, false)
}
}
}
};
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.unwrap()
.block_on(async {
let mut any_changed = false;
for (id, a) in p.authorization_mut().iter_mut() {
let mut changed = false;
let mut updated = Vec::new();
for cert in a.certs()? {
let mut cert = Cert::try_from(cert?)?;
let fp = cert.fingerprint();
eprint!("Updating {} ({}) from the local \
certificate store... ",
fp, id);
if let Ok(c) = config.cert_store()?
.lookup_by_cert_fpr(&fp)
.and_then(|lc| lc.to_cert().cloned())
{
let did_change;
(cert, did_change) = cert_update(cert, c);
changed |= did_change;
} else {
eprintln!("not found.");
};
if ! disable_keyservers {
let kh = KeyHandle::from(fp);
for (uri, keyserver) in keyserver.iter_mut() {
eprint!("Updating {} ({}) from {}... ",
kh, id, uri);
match keyserver.get(kh.clone()).await {
Ok(certs) => {
for c in certs {
if let Ok(c) = c {
let did_change;
(cert, did_change) = cert_update(cert, c);
changed |= did_change;
}
}
}
Err(err) => {
eprintln!("{}.", err);
}
}
}
}
updated.push(cert);
}
if changed {
a.set_certs(updated)?;
any_changed = true;
}
}
if any_changed {
eprintln!("Note: certificates are stripped so not \
all certificate updates may be relevant.");
}
Ok::<(), anyhow::Error>(())
})?;
let diff = old_policy.diff(&p)?;
match config.output_format {
output::Format::HumanReadable => {
output::describe_diff(&diff)?;
},
output::Format::Json =>
serde_json::to_writer_pretty(io::stdout(), &diff)?,
}
config.write_policy(&p, &policy_file)?;
},
PolicySubcommand::Goodlist {
policy_file,
commit,
} => {
let git = git()?;
let object = git.revparse_single(&commit)
.with_context(|| {
format!("Looking up \"{}\"", commit)
})?;
let commit = object.peel_to_commit()
.with_context(|| {
format!("\"{}\" does not refer to a commit",
commit)
})?;
let old_policy = config.read_policy(&policy_file)?;
let mut p = old_policy.clone();
p.commit_goodlist_mut().insert(commit.id().to_string());
let diff = old_policy.diff(&p)?;
match config.output_format {
output::Format::HumanReadable => {
output::describe_diff(&diff)?;
},
output::Format::Json =>
serde_json::to_writer_pretty(io::stdout(), &diff)?,
}
config.write_policy(&p, &policy_file)?;
},
}
Ok(())
}