use crate::common::load_metadata_repo;
use crate::datetime::parse_datetime;
use crate::error::{self, Result};
use crate::source::parse_key_source;
use chrono::{DateTime, Utc};
use clap::Parser;
use snafu::{OptionExt, ResultExt};
use std::num::NonZeroU64;
use std::path::PathBuf;
use tough::editor::{targets::TargetsEditor, RepositoryEditor};
use tough::key_source::KeySource;
use tough::schema::{PathHashPrefix, PathPattern, PathSet};
use url::Url;
#[derive(Debug, Parser)]
pub(crate) struct AddRoleArgs {
#[clap(short = 'd', long = "delegated-role")]
delegatee: String,
#[clap(short = 'k', long = "key", required = true, parse(try_from_str = parse_key_source))]
keys: Vec<Box<dyn KeySource>>,
#[clap(short = 'e', long = "expires", parse(try_from_str = parse_datetime))]
expires: DateTime<Utc>,
#[clap(short = 'v', long = "version")]
version: NonZeroU64,
#[clap(short = 'r', long = "root")]
root: PathBuf,
#[clap(short = 'm', long = "metadata-url")]
metadata_base_url: Url,
#[clap(short = 'i', long = "incoming-metadata")]
indir: Url,
#[clap(short = 't', long = "threshold")]
threshold: NonZeroU64,
#[clap(short = 'o', long = "outdir")]
outdir: PathBuf,
#[clap(short = 'p', long = "paths", conflicts_with = "path-hash-prefixes")]
paths: Option<Vec<PathPattern>>,
#[clap(short = 'x', long = "path-hash-prefixes")]
path_hash_prefixes: Option<Vec<PathHashPrefix>>,
#[clap(long = "sign-all")]
sign_all: bool,
#[clap(long = "snapshot-version")]
snapshot_version: Option<NonZeroU64>,
#[clap(long = "snapshot-expires", parse(try_from_str = parse_datetime))]
snapshot_expires: Option<DateTime<Utc>>,
#[clap(long = "timestamp-version")]
timestamp_version: Option<NonZeroU64>,
#[clap(long = "timestamp-expires", parse(try_from_str = parse_datetime))]
timestamp_expires: Option<DateTime<Utc>>,
}
impl AddRoleArgs {
pub(crate) fn run(&self, role: &str) -> Result<()> {
let repository = load_metadata_repo(&self.root, self.metadata_base_url.clone())?;
if self.sign_all {
self.with_repo_editor(
role,
RepositoryEditor::from_repo(&self.root, repository)
.context(error::EditorFromRepoSnafu { path: &self.root })?,
)
} else {
self.add_role(
role,
TargetsEditor::from_repo(repository, role)
.context(error::EditorFromRepoSnafu { path: &self.root })?,
)
}
}
#[allow(clippy::option_if_let_else)]
fn add_role(&self, role: &str, mut editor: TargetsEditor) -> Result<()> {
let paths = if let Some(paths) = &self.paths {
PathSet::Paths(paths.clone())
} else if let Some(path_hash_prefixes) = &self.path_hash_prefixes {
PathSet::PathHashPrefixes(path_hash_prefixes.clone())
} else {
PathSet::Paths(Vec::new())
};
let updated_role = editor
.add_role(
&self.delegatee,
self.indir.as_str(),
paths,
self.threshold,
None,
)
.context(error::LoadMetadataSnafu)?
.version(self.version)
.expires(self.expires)
.sign(&self.keys)
.context(error::SignRepoSnafu)?;
let metadata_destination_out = &self.outdir.join("metadata");
updated_role
.write(metadata_destination_out, false)
.context(error::WriteRolesSnafu {
roles: [self.delegatee.clone(), role.to_string()].to_vec(),
})?;
Ok(())
}
#[allow(clippy::option_if_let_else)]
fn with_repo_editor(&self, role: &str, mut editor: RepositoryEditor) -> Result<()> {
let snapshot_version = self.snapshot_version.context(error::MissingSnafu {
what: "snapshot version".to_string(),
})?;
let snapshot_expires = self.snapshot_expires.context(error::MissingSnafu {
what: "snapshot expires".to_string(),
})?;
let timestamp_version = self.timestamp_version.context(error::MissingSnafu {
what: "timestamp version".to_string(),
})?;
let timestamp_expires = self.timestamp_expires.context(error::MissingSnafu {
what: "timestamp expires".to_string(),
})?;
let paths = if let Some(paths) = &self.paths {
PathSet::Paths(paths.clone())
} else if let Some(path_hash_prefixes) = &self.path_hash_prefixes {
PathSet::PathHashPrefixes(path_hash_prefixes.clone())
} else {
PathSet::Paths(Vec::new())
};
editor
.targets_version(self.version)
.context(error::DelegationStructureSnafu)?
.targets_expires(self.expires)
.context(error::DelegationStructureSnafu)?
.sign_targets_editor(&self.keys)
.context(error::DelegateeNotFoundSnafu {
role: role.to_string(),
})?;
editor
.change_delegated_targets(role)
.context(error::DelegateeNotFoundSnafu {
role: role.to_string(),
})?;
editor
.add_role(
&self.delegatee,
self.indir.as_str(),
paths,
self.threshold,
None,
)
.context(error::LoadMetadataSnafu)?
.targets_version(self.version)
.context(error::DelegationStructureSnafu)?
.targets_expires(self.expires)
.context(error::DelegationStructureSnafu)?
.snapshot_version(snapshot_version)
.snapshot_expires(snapshot_expires)
.timestamp_version(timestamp_version)
.timestamp_expires(timestamp_expires);
let signed_repo = editor.sign(&self.keys).context(error::SignRepoSnafu)?;
let metadata_destination_out = &self.outdir.join("metadata");
signed_repo
.write(metadata_destination_out)
.context(error::WriteRolesSnafu {
roles: [self.delegatee.clone(), role.to_string()].to_vec(),
})?;
Ok(())
}
}