use crate::common::load_metadata_repo;
use crate::datetime::parse_datetime;
use crate::error::{self, Result};
use crate::source::parse_key_source;
use clap::Parser;
use jiff::Timestamp;
use snafu::{OptionExt, ResultExt};
use std::num::NonZeroU64;
use std::path::PathBuf;
use tough::editor::{targets::TargetsEditor, RepositoryEditor};
use tough::schema::{PathHashPrefix, PathPattern, PathSet};
use url::Url;
#[derive(Debug, Parser)]
pub(crate) struct AddRoleArgs {
#[arg(short, long = "delegated-role")]
delegatee: String,
#[arg(short, long, value_parser = parse_datetime)]
expires: Timestamp,
#[arg(short, long = "incoming-metadata")]
indir: Url,
#[arg(short, long = "key", required = true)]
keys: Vec<String>,
#[arg(short, long = "metadata-url")]
metadata_base_url: Url,
#[arg(short, long)]
outdir: PathBuf,
#[arg(short, long, conflicts_with = "path_hash_prefixes")]
paths: Option<Vec<PathPattern>>,
#[arg(long)]
terminating: bool,
#[arg(short, long)]
root: PathBuf,
#[arg(long)]
sign_all: bool,
#[arg(long, value_parser = parse_datetime)]
snapshot_expires: Option<Timestamp>,
#[arg(long)]
snapshot_version: Option<NonZeroU64>,
#[arg(short, long)]
threshold: NonZeroU64,
#[arg(long, value_parser = parse_datetime)]
timestamp_expires: Option<Timestamp>,
#[arg(long)]
timestamp_version: Option<NonZeroU64>,
#[arg(short, long)]
version: NonZeroU64,
#[arg(short = 'x', long)]
path_hash_prefixes: Option<Vec<PathHashPrefix>>,
}
impl AddRoleArgs {
pub(crate) async fn run(&self, role: &str) -> Result<()> {
let repository = load_metadata_repo(&self.root, self.metadata_base_url.clone()).await?;
if self.sign_all {
self.with_repo_editor(
role,
RepositoryEditor::from_repo(&self.root, repository)
.await
.context(error::EditorFromRepoSnafu { path: &self.root })?,
)
.await
} else {
self.add_role(
role,
TargetsEditor::from_repo(repository, role)
.context(error::EditorFromRepoSnafu { path: &self.root })?,
)
.await
}
}
#[allow(clippy::option_if_let_else)]
async 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 mut keys = Vec::new();
for source in &self.keys {
let key_source = parse_key_source(source)?;
keys.push(key_source);
}
let updated_role = editor
.add_role(
&self.delegatee,
self.indir.as_str(),
paths,
self.terminating,
self.threshold,
None,
)
.await
.context(error::LoadMetadataSnafu)?
.version(self.version)
.expires(self.expires)
.sign(&keys)
.await
.context(error::SignRepoSnafu)?;
let metadata_destination_out = &self.outdir.join("metadata");
updated_role
.write(metadata_destination_out, false)
.await
.context(error::WriteRolesSnafu {
roles: [self.delegatee.clone(), role.to_string()].to_vec(),
})?;
Ok(())
}
#[allow(clippy::option_if_let_else)]
async fn with_repo_editor(&self, role: &str, mut editor: RepositoryEditor) -> Result<()> {
let mut keys = Vec::new();
for source in &self.keys {
let key_source = parse_key_source(source)?;
keys.push(key_source);
}
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(&keys)
.await
.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.terminating,
self.threshold,
None,
)
.await
.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(&keys).await.context(error::SignRepoSnafu)?;
let metadata_destination_out = &self.outdir.join("metadata");
signed_repo
.write(metadata_destination_out)
.await
.context(error::WriteRolesSnafu {
roles: [self.delegatee.clone(), role.to_string()].to_vec(),
})?;
Ok(())
}
}