20260922 对齐 OSS 验证配置并补充 public TOML E2E
This commit is contained in:
@@ -13,12 +13,25 @@ use std::{
|
||||
path::{Path, PathBuf},
|
||||
time::Duration,
|
||||
};
|
||||
use time::OffsetDateTime;
|
||||
use x509_parser::prelude::{FromDer, X509Certificate};
|
||||
|
||||
pub(crate) fn validate_certificate(tal: &Tal, bytes: &[u8]) -> Result<Value, String> {
|
||||
TrustAnchor::bind_der(tal.clone(), bytes, None).map_err(|e| e.to_string())?;
|
||||
pub(crate) fn validate_certificate(
|
||||
tal: &Tal,
|
||||
bytes: &[u8],
|
||||
strict_name: bool,
|
||||
validation_time: OffsetDateTime,
|
||||
) -> Result<Value, String> {
|
||||
if strict_name {
|
||||
TrustAnchor::bind_der_with_strict_name(tal.clone(), bytes, None)
|
||||
.map_err(|e| e.to_string())?;
|
||||
} else {
|
||||
TrustAnchor::bind_der(tal.clone(), bytes, None).map_err(|e| e.to_string())?;
|
||||
}
|
||||
let (_, cert) = X509Certificate::from_der(bytes).map_err(|e| e.to_string())?;
|
||||
if !cert.validity().is_valid() {
|
||||
let not_before = cert.validity().not_before.to_datetime();
|
||||
let not_after = cert.validity().not_after.to_datetime();
|
||||
if validation_time < not_before || validation_time > not_after {
|
||||
return Err("TA certificate is expired or not yet valid".into());
|
||||
}
|
||||
Ok(
|
||||
@@ -31,8 +44,9 @@ pub(crate) fn cache_root(state: &Path) -> PathBuf {
|
||||
PathBuf::from(name)
|
||||
}
|
||||
|
||||
pub(crate) fn resolve(
|
||||
pub(crate) fn resolve_at(
|
||||
config: &Config,
|
||||
validation_time: OffsetDateTime,
|
||||
) -> Result<(Vec<crate::scheduler::types::TalInputSpec>, Vec<Value>), String> {
|
||||
let http = BlockingHttpFetcher::new(HttpFetcherConfig {
|
||||
timeout: Duration::from_secs(config.http_timeout_secs),
|
||||
@@ -44,18 +58,41 @@ pub(crate) fn resolve(
|
||||
.map_err(|e| e.to_string())?,
|
||||
..Default::default()
|
||||
})?;
|
||||
let rsync = SystemRsyncFetcher::new(SystemRsyncConfig::default());
|
||||
resolve_with(config, |uri| {
|
||||
match url::Url::parse(uri).map_err(|e| e.to_string())?.scheme() {
|
||||
let rsync = SystemRsyncFetcher::new(SystemRsyncConfig {
|
||||
rsync_bin: config
|
||||
.rsync_command
|
||||
.clone()
|
||||
.unwrap_or_else(|| PathBuf::from("rsync")),
|
||||
timeout: Duration::from_secs(config.rsync_timeout_secs.max(1)),
|
||||
mirror_root: config.rsync_mirror_root.clone(),
|
||||
scope_policy: config.rsync_scope,
|
||||
..Default::default()
|
||||
});
|
||||
resolve_with_at(
|
||||
config,
|
||||
validation_time,
|
||||
config.strict.name,
|
||||
|uri| match url::Url::parse(uri).map_err(|e| e.to_string())?.scheme() {
|
||||
"https" => http.fetch_bytes(uri),
|
||||
"rsync" => rsync.fetch_object(uri).map_err(|e| e.to_string()),
|
||||
_ => Err("unsupported TA URI scheme".into()),
|
||||
}
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn resolve_with(
|
||||
config: &Config,
|
||||
fetch: impl FnMut(&str) -> Result<Vec<u8>, String>,
|
||||
) -> Result<(Vec<crate::scheduler::types::TalInputSpec>, Vec<Value>), String> {
|
||||
let validation_time = config.effective_validation_time()?;
|
||||
resolve_with_at(config, validation_time, config.strict.name, fetch)
|
||||
}
|
||||
|
||||
fn resolve_with_at(
|
||||
config: &Config,
|
||||
validation_time: OffsetDateTime,
|
||||
strict_name: bool,
|
||||
mut fetch: impl FnMut(&str) -> Result<Vec<u8>, String>,
|
||||
) -> Result<(Vec<crate::scheduler::types::TalInputSpec>, Vec<Value>), String> {
|
||||
let root = cache_root(config.rrdp_state_dir.as_ref().ok_or("state path missing")?);
|
||||
@@ -67,7 +104,12 @@ pub(crate) fn resolve_with(
|
||||
let tal = Tal::decode_bytes(&fs::read(&anchor.tal).map_err(|e| e.to_string())?)
|
||||
.map_err(|e| e.to_string())?;
|
||||
if let Some(path) = &anchor.ta {
|
||||
let info = validate_certificate(&tal, &fs::read(path).map_err(|e| e.to_string())?)?;
|
||||
let info = validate_certificate(
|
||||
&tal,
|
||||
&fs::read(path).map_err(|e| e.to_string())?,
|
||||
strict_name,
|
||||
validation_time,
|
||||
)?;
|
||||
record
|
||||
.as_object_mut()
|
||||
.unwrap()
|
||||
@@ -82,27 +124,29 @@ pub(crate) fn resolve_with(
|
||||
let mut error_code = "download_failed";
|
||||
for uri in &tal.ta_uris {
|
||||
match fetch(uri.as_str()) {
|
||||
Ok(bytes) => match validate_certificate(&tal, &bytes) {
|
||||
Ok(info) => {
|
||||
record
|
||||
.as_object_mut()
|
||||
.unwrap()
|
||||
.extend(info.as_object().unwrap().clone());
|
||||
record["source"] = json!("download");
|
||||
record["fetchedAt"] = json!(super::publication::now());
|
||||
// Certificate and metadata are one atomic cache record; the .cer is a runtime projection.
|
||||
super::publication::write_json(
|
||||
&path,
|
||||
&json!({"der":hex::encode(&bytes),"record":record}),
|
||||
)?;
|
||||
super::publication::write_atomic(&der_path, &bytes)?;
|
||||
return Ok(der_path);
|
||||
Ok(bytes) => {
|
||||
match validate_certificate(&tal, &bytes, strict_name, validation_time) {
|
||||
Ok(info) => {
|
||||
record
|
||||
.as_object_mut()
|
||||
.unwrap()
|
||||
.extend(info.as_object().unwrap().clone());
|
||||
record["source"] = json!("download");
|
||||
record["fetchedAt"] = json!(super::publication::now());
|
||||
// Certificate and metadata are one atomic cache record; the .cer is a runtime projection.
|
||||
super::publication::write_json(
|
||||
&path,
|
||||
&json!({"der":hex::encode(&bytes),"record":record}),
|
||||
)?;
|
||||
super::publication::write_atomic(&der_path, &bytes)?;
|
||||
return Ok(der_path);
|
||||
}
|
||||
Err(e) => {
|
||||
last_error = e;
|
||||
error_code = "certificate_invalid";
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
last_error = e;
|
||||
error_code = "certificate_invalid";
|
||||
}
|
||||
},
|
||||
}
|
||||
Err(e) => {
|
||||
last_error = e;
|
||||
}
|
||||
@@ -134,9 +178,10 @@ pub(crate) fn resolve_with(
|
||||
return Err("cached TA metadata has no DER value".into());
|
||||
}
|
||||
};
|
||||
let info = validate_certificate(&tal, &bytes).inspect_err(|_| {
|
||||
record["errorCode"] = json!("cache_unusable");
|
||||
})?;
|
||||
let info = validate_certificate(&tal, &bytes, strict_name, validation_time)
|
||||
.inspect_err(|_| {
|
||||
record["errorCode"] = json!("cache_unusable");
|
||||
})?;
|
||||
record
|
||||
.as_object_mut()
|
||||
.unwrap()
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
//! One explicit TOML file is the sole source of runtime settings.
|
||||
use crate::repository::fetch::rsync_system::RsyncScopePolicy;
|
||||
use crate::validation::policy::{ResourceValidationMode, StrictPolicy, SyncPreference};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{Value, json};
|
||||
use std::collections::BTreeSet;
|
||||
use std::path::{Component, Path, PathBuf};
|
||||
use time::format_description::well_known::Rfc3339;
|
||||
use time::{OffsetDateTime, UtcOffset};
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
|
||||
@@ -34,6 +38,15 @@ pub struct Config {
|
||||
pub log_progress_interval_secs: u64,
|
||||
pub require_complete: bool,
|
||||
pub run_timeout_secs: u64,
|
||||
#[serde(with = "strict_config_serde")]
|
||||
pub strict: StrictPolicy,
|
||||
pub resource_validation_mode: ResourceValidationMode,
|
||||
pub validation_time: Option<String>,
|
||||
pub sync_preference: String,
|
||||
pub rsync_timeout_secs: u64,
|
||||
pub rsync_command: Option<PathBuf>,
|
||||
pub rsync_mirror_root: Option<PathBuf>,
|
||||
pub rsync_scope: RsyncScopePolicy,
|
||||
pub out: Option<PathBuf>,
|
||||
pub rrdp_state_dir: Option<PathBuf>,
|
||||
pub rrdp_sync_mode: String,
|
||||
@@ -45,6 +58,56 @@ pub struct Config {
|
||||
pub retain_runs: usize,
|
||||
pub shutdown_grace_secs: u64,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
#[serde(default, deny_unknown_fields)]
|
||||
struct StrictConfigFields {
|
||||
name: bool,
|
||||
#[serde(rename = "cms-der", alias = "cms_der")]
|
||||
cms_der: bool,
|
||||
#[serde(rename = "signed-attrs", alias = "signed_attrs")]
|
||||
signed_attrs: bool,
|
||||
}
|
||||
|
||||
impl Default for StrictConfigFields {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
name: false,
|
||||
cms_der: false,
|
||||
signed_attrs: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod strict_config_serde {
|
||||
use super::{StrictConfigFields, StrictPolicy};
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
|
||||
pub(super) fn serialize<S>(value: &StrictPolicy, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
StrictConfigFields {
|
||||
name: value.name,
|
||||
cms_der: value.cms_der,
|
||||
signed_attrs: value.signed_attrs,
|
||||
}
|
||||
.serialize(serializer)
|
||||
}
|
||||
|
||||
pub(super) fn deserialize<'de, D>(deserializer: D) -> Result<StrictPolicy, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let fields = StrictConfigFields::deserialize(deserializer)?;
|
||||
Ok(StrictPolicy {
|
||||
name: fields.name,
|
||||
cms_der: fields.cms_der,
|
||||
signed_attrs: fields.signed_attrs,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
@@ -65,6 +128,14 @@ impl Default for Config {
|
||||
log_progress_interval_secs: 10,
|
||||
require_complete: false,
|
||||
run_timeout_secs: 0,
|
||||
strict: StrictPolicy::default(),
|
||||
resource_validation_mode: ResourceValidationMode::default(),
|
||||
validation_time: None,
|
||||
sync_preference: "rrdp-then-rsync".into(),
|
||||
rsync_timeout_secs: 30,
|
||||
rsync_command: None,
|
||||
rsync_mirror_root: None,
|
||||
rsync_scope: RsyncScopePolicy::default(),
|
||||
out: None,
|
||||
rrdp_state_dir: None,
|
||||
rrdp_sync_mode: "auto".into(),
|
||||
@@ -113,6 +184,76 @@ pub(crate) fn absolute(base: &Path, path: &Path) -> Result<PathBuf, String> {
|
||||
}
|
||||
|
||||
impl Config {
|
||||
fn parse_validation_time(raw: &str) -> Result<OffsetDateTime, String> {
|
||||
let raw = raw.trim();
|
||||
if raw.is_empty() {
|
||||
return Err("validation-time must not be empty".into());
|
||||
}
|
||||
let parsed = OffsetDateTime::parse(raw, &Rfc3339)
|
||||
.map_err(|e| format!("invalid validation-time (RFC3339 required): {e}"))?;
|
||||
OffsetDateTime::from_unix_timestamp(parsed.unix_timestamp())
|
||||
.map(|value| value.to_offset(UtcOffset::UTC))
|
||||
.map_err(|e| format!("invalid validation-time: {e}"))
|
||||
}
|
||||
|
||||
fn format_validation_time(value: OffsetDateTime) -> Result<String, String> {
|
||||
value
|
||||
.to_offset(UtcOffset::UTC)
|
||||
.format(&Rfc3339)
|
||||
.map_err(|e| format!("format validation-time failed: {e}"))
|
||||
}
|
||||
|
||||
fn normalize_validation_time(value: OffsetDateTime) -> Result<OffsetDateTime, String> {
|
||||
OffsetDateTime::from_unix_timestamp(value.unix_timestamp())
|
||||
.map(|value| value.to_offset(UtcOffset::UTC))
|
||||
.map_err(|e| format!("normalize validation-time failed: {e}"))
|
||||
}
|
||||
|
||||
pub(crate) fn effective_validation_time(&self) -> Result<OffsetDateTime, String> {
|
||||
let value = self
|
||||
.validation_time
|
||||
.as_deref()
|
||||
.map(Self::parse_validation_time)
|
||||
.transpose()?
|
||||
.unwrap_or_else(OffsetDateTime::now_utc);
|
||||
Self::normalize_validation_time(value)
|
||||
}
|
||||
|
||||
pub(crate) fn sync_preference(&self) -> Result<SyncPreference, String> {
|
||||
match self.sync_preference.trim() {
|
||||
"rrdp-then-rsync" => Ok(SyncPreference::RrdpThenRsync),
|
||||
"rsync-only" => Ok(SyncPreference::RsyncOnly),
|
||||
value => Err(format!(
|
||||
"invalid sync-preference: {value}; expected rrdp-then-rsync or rsync-only"
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_rsync_command(&self) -> Result<(), String> {
|
||||
let Some(path) = &self.rsync_command else {
|
||||
return Ok(());
|
||||
};
|
||||
if !path.is_absolute() {
|
||||
return Err("rsync-command must be an absolute executable path".into());
|
||||
}
|
||||
let metadata = std::fs::metadata(path)
|
||||
.map_err(|e| format!("rsync-command is not usable: {}: {e}", path.display()))?;
|
||||
if !metadata.is_file() {
|
||||
return Err(format!("rsync-command is not a file: {}", path.display()));
|
||||
}
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
if metadata.permissions().mode() & 0o111 == 0 {
|
||||
return Err(format!(
|
||||
"rsync-command is not executable: {}",
|
||||
path.display()
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn load(path: &Path, mode: &str) -> Result<(Self, Value), String> {
|
||||
let path = absolute(&std::env::current_dir().map_err(|e| e.to_string())?, path)?;
|
||||
let raw = std::fs::read_to_string(&path)
|
||||
@@ -159,6 +300,7 @@ impl Config {
|
||||
&mut config.ccr_out,
|
||||
&mut config.publish_dir,
|
||||
&mut config.state_root,
|
||||
&mut config.rsync_mirror_root,
|
||||
]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
@@ -202,6 +344,14 @@ impl Config {
|
||||
}
|
||||
config.validate_inputs()?;
|
||||
let mut normalizations = Vec::new();
|
||||
if let Some(raw) = config.validation_time.as_deref() {
|
||||
let normalized = Self::format_validation_time(Self::parse_validation_time(raw)?)?;
|
||||
if raw != normalized {
|
||||
normalizations
|
||||
.push(json!({"field":"validation-time","input":raw,"effective":normalized}));
|
||||
}
|
||||
config.validation_time = Some(normalized);
|
||||
}
|
||||
if config.http_timeout_secs < 1 {
|
||||
normalizations.push(json!({"field":"http-timeout-secs","input":0,"effective":1}));
|
||||
config.http_timeout_secs = 1;
|
||||
@@ -211,6 +361,11 @@ impl Config {
|
||||
config.parallel_repo_worker_queue_capacity = 1024;
|
||||
}
|
||||
let mut effective = serde_json::to_value(&config).map_err(|e| e.to_string())?;
|
||||
if config.validation_time.is_none() {
|
||||
effective["validation-time"] = json!(Self::format_validation_time(
|
||||
config.effective_validation_time()?
|
||||
)?);
|
||||
}
|
||||
for key in forbidden {
|
||||
effective.as_object_mut().unwrap().remove(*key);
|
||||
}
|
||||
@@ -240,6 +395,12 @@ impl Config {
|
||||
if !["auto", "snapshot", "delta"].contains(&self.rrdp_sync_mode.as_str()) {
|
||||
return Err("invalid rrdp-sync-mode".into());
|
||||
}
|
||||
self.sync_preference()?;
|
||||
let validation_time = self.effective_validation_time()?;
|
||||
if self.rsync_timeout_secs == 0 {
|
||||
return Err("rsync-timeout-secs must be positive".into());
|
||||
}
|
||||
self.validate_rsync_command()?;
|
||||
crate::logging::parse_level(&self.log_level)?;
|
||||
if !["text", "json"].contains(&self.log_format.as_str()) {
|
||||
return Err("invalid log-format".into());
|
||||
@@ -270,6 +431,8 @@ impl Config {
|
||||
super::anchors::validate_certificate(
|
||||
&tal,
|
||||
&std::fs::read(ta).map_err(|e| format!("read TA: {e}"))?,
|
||||
self.strict.name,
|
||||
validation_time,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
@@ -290,6 +453,7 @@ impl Config {
|
||||
&self.state_root,
|
||||
&self.rrdp_state_dir,
|
||||
&self.publish_dir,
|
||||
&self.rsync_mirror_root,
|
||||
]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
@@ -415,6 +579,116 @@ fn collect_sources(
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn validation_time_is_parsed_as_utc_whole_seconds() {
|
||||
let parsed = Config::parse_validation_time("2026-09-22T08:00:00.750+08:00").unwrap();
|
||||
assert_eq!(
|
||||
Config::format_validation_time(parsed).unwrap(),
|
||||
"2026-09-22T00:00:00Z"
|
||||
);
|
||||
assert_eq!(parsed.nanosecond(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strict_table_rejects_unknown_fields() {
|
||||
let error = toml::from_str::<Config>(
|
||||
r#"
|
||||
[strict]
|
||||
enabled = true
|
||||
"#,
|
||||
)
|
||||
.expect_err("unknown strict field must be rejected");
|
||||
assert!(error.to_string().contains("unknown field"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn public_alignment_fields_load_and_print_effective_values() {
|
||||
let fixture = crate::test_support::synthetic_repository();
|
||||
let directory = tempfile::tempdir().unwrap();
|
||||
let config_path = directory.path().join("validate.toml");
|
||||
let executable = std::env::current_exe().unwrap();
|
||||
std::fs::write(
|
||||
&config_path,
|
||||
format!(
|
||||
r#"
|
||||
config-version = 1
|
||||
mode = "validate"
|
||||
out = "out"
|
||||
resource-validation-mode = "rfc6487"
|
||||
validation-time = "2026-09-22T08:00:00+08:00"
|
||||
sync-preference = "rsync-only"
|
||||
rsync-timeout-secs = 47
|
||||
rsync-command = {:?}
|
||||
rsync-mirror-root = "mirror"
|
||||
rsync-scope = "publication-point"
|
||||
|
||||
[strict]
|
||||
name = true
|
||||
cms-der = true
|
||||
signed-attrs = false
|
||||
|
||||
[[anchors]]
|
||||
id = "custom"
|
||||
tal = {:?}
|
||||
"#,
|
||||
executable.to_string_lossy(),
|
||||
fixture.tal().to_string_lossy(),
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let (config, printed) = Config::load(&config_path, "validate").unwrap();
|
||||
assert_eq!(
|
||||
config.strict,
|
||||
StrictPolicy {
|
||||
name: true,
|
||||
cms_der: true,
|
||||
signed_attrs: false,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
config.resource_validation_mode,
|
||||
ResourceValidationMode::Rfc6487
|
||||
);
|
||||
assert_eq!(config.sync_preference().unwrap(), SyncPreference::RsyncOnly);
|
||||
assert_eq!(config.rsync_timeout_secs, 47);
|
||||
assert_eq!(config.rsync_scope, RsyncScopePolicy::PublicationPoint);
|
||||
assert_eq!(
|
||||
config.effective_validation_time().unwrap().unix_timestamp(),
|
||||
Config::parse_validation_time("2026-09-22T00:00:00Z")
|
||||
.unwrap()
|
||||
.unix_timestamp()
|
||||
);
|
||||
assert_eq!(printed["effective"]["resource-validation-mode"], "rfc6487");
|
||||
assert_eq!(printed["effective"]["sync-preference"], "rsync-only");
|
||||
assert_eq!(printed["effective"]["rsync-timeout-secs"], 47);
|
||||
assert_eq!(
|
||||
printed["effective"]["validation-time"],
|
||||
"2026-09-22T00:00:00Z"
|
||||
);
|
||||
assert_eq!(
|
||||
printed["sources"]["strict.cms-der"]
|
||||
.as_str()
|
||||
.unwrap()
|
||||
.starts_with("file:"),
|
||||
true
|
||||
);
|
||||
assert_eq!(
|
||||
printed["sources"]["validation-time"]
|
||||
.as_str()
|
||||
.unwrap()
|
||||
.starts_with("file:"),
|
||||
true
|
||||
);
|
||||
assert!(
|
||||
printed["normalizations"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|item| item["field"] == "validation-time")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn constraints_selection_disable_override_and_reload() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
//! Execute one validation attempt, then commit complete successful outputs.
|
||||
use super::{config::Config, publication};
|
||||
use crate::validation::policy::SyncPreference;
|
||||
use serde_json::{Value, json};
|
||||
use std::fs::{self, File, OpenOptions};
|
||||
use std::io::{BufRead, BufReader, BufWriter, Write};
|
||||
@@ -75,12 +76,14 @@ fn execute(config: &Config, run_id: &str) -> Result<i32, String> {
|
||||
}
|
||||
let observer = crate::logging::observer::Observer::start(config.log_progress_interval_secs)?;
|
||||
let start = std::time::Instant::now();
|
||||
let validation_time = config.effective_validation_time()?;
|
||||
let sync_preference = config.sync_preference()?;
|
||||
crate::logging::emit(
|
||||
crate::logging::Level::Info,
|
||||
"validation_started",
|
||||
|| json!({"tal_count":config.anchors.len(),"sync_mode":config.rrdp_sync_mode}),
|
||||
);
|
||||
let (inputs, anchors) = super::anchors::resolve(config)?;
|
||||
let (inputs, anchors) = super::anchors::resolve_at(config, validation_time)?;
|
||||
let failed = config.anchors.len() - inputs.len();
|
||||
if inputs.is_empty() {
|
||||
publication::write_json(
|
||||
@@ -122,7 +125,14 @@ fn execute(config: &Config, run_id: &str) -> Result<i32, String> {
|
||||
max_ca_depth: config.max_ca_depth,
|
||||
http_timeout_secs: config.http_timeout_secs,
|
||||
http_root_cert_paths: config.http_root_cert.clone(),
|
||||
rsync_timeout_secs: 30,
|
||||
strict_policy: Some(config.strict),
|
||||
resource_validation_mode: Some(config.resource_validation_mode),
|
||||
disable_rrdp: sync_preference == SyncPreference::RsyncOnly,
|
||||
rsync_command: config.rsync_command.clone(),
|
||||
rsync_timeout_secs: config.rsync_timeout_secs,
|
||||
rsync_mirror_root: config.rsync_mirror_root.clone(),
|
||||
rsync_scope_policy: config.rsync_scope,
|
||||
validation_time: Some(validation_time),
|
||||
summary_out_path: Some(root.join("summary.json")),
|
||||
vrps_csv_out_path: Some(root.join(".vrps-source.csv")),
|
||||
vaps_csv_out_path: Some(root.join("vaps.csv")),
|
||||
|
||||
@@ -42,10 +42,12 @@ impl ResourceValidationMode {
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
#[serde(default, deny_unknown_fields)]
|
||||
pub struct StrictPolicy {
|
||||
pub name: bool,
|
||||
#[serde(alias = "cms-der")]
|
||||
pub cms_der: bool,
|
||||
#[serde(alias = "signed-attrs")]
|
||||
pub signed_attrs: bool,
|
||||
}
|
||||
|
||||
@@ -180,6 +182,27 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn policy_toml_accepts_public_kebab_case_strict_keys() {
|
||||
let policy = Policy::from_toml_str(
|
||||
r#"
|
||||
[strict]
|
||||
name = true
|
||||
cms-der = true
|
||||
signed-attrs = false
|
||||
"#,
|
||||
)
|
||||
.expect("parse public strict keys");
|
||||
assert_eq!(
|
||||
policy.strict,
|
||||
StrictPolicy {
|
||||
name: true,
|
||||
cms_der: true,
|
||||
signed_attrs: false,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn policy_toml_accepts_resource_validation_mode() {
|
||||
let policy = Policy::from_toml_str(
|
||||
|
||||
Reference in New Issue
Block a user